Merge branch 'dev' into security-2

This commit is contained in:
nielsandriesse 2020-10-28 10:32:55 +11:00
commit d4e1ad123c
48 changed files with 1268 additions and 1016 deletions

2
Pods

@ -1 +1 @@
Subproject commit f818a61c04eeb78662dc4626014575bff9eb879b Subproject commit 46670a1c228057915024e6ff96657a051eb54f82

View File

@ -261,7 +261,6 @@
34D1F0B01F867BFC0066283D /* OWSSystemMessageCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0A61F867BFC0066283D /* OWSSystemMessageCell.m */; }; 34D1F0B01F867BFC0066283D /* OWSSystemMessageCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0A61F867BFC0066283D /* OWSSystemMessageCell.m */; };
34D1F0B41F86D31D0066283D /* ConversationCollectionView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0B31F86D31D0066283D /* ConversationCollectionView.m */; }; 34D1F0B41F86D31D0066283D /* ConversationCollectionView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0B31F86D31D0066283D /* ConversationCollectionView.m */; };
34D1F0B71F87F8850066283D /* OWSGenericAttachmentView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0B61F87F8850066283D /* OWSGenericAttachmentView.m */; }; 34D1F0B71F87F8850066283D /* OWSGenericAttachmentView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0B61F87F8850066283D /* OWSGenericAttachmentView.m */; };
34D1F0BA1F8800D90066283D /* OWSAudioMessageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0B91F8800D90066283D /* OWSAudioMessageView.m */; };
34D1F0BD1F8D108C0066283D /* AttachmentUploadView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0BC1F8D108C0066283D /* AttachmentUploadView.m */; }; 34D1F0BD1F8D108C0066283D /* AttachmentUploadView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0BC1F8D108C0066283D /* AttachmentUploadView.m */; };
34D1F0C01F8EC1760066283D /* MessageRecipientStatusUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0BF1F8EC1760066283D /* MessageRecipientStatusUtils.swift */; }; 34D1F0C01F8EC1760066283D /* MessageRecipientStatusUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0BF1F8EC1760066283D /* MessageRecipientStatusUtils.swift */; };
34D2CCDA2062E7D000CB1A14 /* OWSScreenLockUI.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D2CCD92062E7D000CB1A14 /* OWSScreenLockUI.m */; }; 34D2CCDA2062E7D000CB1A14 /* OWSScreenLockUI.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D2CCD92062E7D000CB1A14 /* OWSScreenLockUI.m */; };
@ -572,6 +571,7 @@
C31D1DDD25217014005D4DA8 /* UserCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31D1DDC25217014005D4DA8 /* UserCell.swift */; }; C31D1DDD25217014005D4DA8 /* UserCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31D1DDC25217014005D4DA8 /* UserCell.swift */; };
C31D1DE32521718E005D4DA8 /* UserSelectionVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31D1DE22521718E005D4DA8 /* UserSelectionVC.swift */; }; C31D1DE32521718E005D4DA8 /* UserSelectionVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31D1DE22521718E005D4DA8 /* UserSelectionVC.swift */; };
C31D1DE9252172D4005D4DA8 /* ContactUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31D1DE8252172D4005D4DA8 /* ContactUtilities.swift */; }; C31D1DE9252172D4005D4DA8 /* ContactUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31D1DE8252172D4005D4DA8 /* ContactUtilities.swift */; };
C31F812625258FB000DD9FD9 /* Storage+VolumeSamples.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31F812525258FB000DD9FD9 /* Storage+VolumeSamples.swift */; };
C329FEEC24F7277900B1C64C /* LightModeSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C329FEEB24F7277900B1C64C /* LightModeSheet.swift */; }; C329FEEC24F7277900B1C64C /* LightModeSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C329FEEB24F7277900B1C64C /* LightModeSheet.swift */; };
C329FEEF24F7743F00B1C64C /* UIViewController+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C329FEED24F7742E00B1C64C /* UIViewController+Utilities.swift */; }; C329FEEF24F7743F00B1C64C /* UIViewController+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C329FEED24F7742E00B1C64C /* UIViewController+Utilities.swift */; };
C34C8F7423A7830B00D82669 /* SpaceMono-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = C34C8F7323A7830A00D82669 /* SpaceMono-Bold.ttf */; }; C34C8F7423A7830B00D82669 /* SpaceMono-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = C34C8F7323A7830A00D82669 /* SpaceMono-Bold.ttf */; };
@ -583,6 +583,8 @@
C35E8AA92485C85800ACB629 /* GeoLite2-Country-Blocks-IPv4.csv in Resources */ = {isa = PBXBuildFile; fileRef = C35E8AA62485C85600ACB629 /* GeoLite2-Country-Blocks-IPv4.csv */; }; C35E8AA92485C85800ACB629 /* GeoLite2-Country-Blocks-IPv4.csv in Resources */ = {isa = PBXBuildFile; fileRef = C35E8AA62485C85600ACB629 /* GeoLite2-Country-Blocks-IPv4.csv */; };
C35E8AAE2485E51D00ACB629 /* IP2Country.swift in Sources */ = {isa = PBXBuildFile; fileRef = C35E8AAD2485E51D00ACB629 /* IP2Country.swift */; }; C35E8AAE2485E51D00ACB629 /* IP2Country.swift in Sources */ = {isa = PBXBuildFile; fileRef = C35E8AAD2485E51D00ACB629 /* IP2Country.swift */; };
C3638C0524C7F0B500AF29BC /* LK002RemoveFriendRequests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3638C0424C7F0B500AF29BC /* LK002RemoveFriendRequests.swift */; }; C3638C0524C7F0B500AF29BC /* LK002RemoveFriendRequests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3638C0424C7F0B500AF29BC /* LK002RemoveFriendRequests.swift */; };
C3645350252449260045C478 /* VoiceMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C364534F252449260045C478 /* VoiceMessageView.swift */; };
C364535C252467900045C478 /* AudioUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C364535B252467900045C478 /* AudioUtilities.swift */; };
C369549D24D27A3500CEB4E3 /* MultiDeviceRemovalSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C369549C24D27A3500CEB4E3 /* MultiDeviceRemovalSheet.swift */; }; C369549D24D27A3500CEB4E3 /* MultiDeviceRemovalSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C369549C24D27A3500CEB4E3 /* MultiDeviceRemovalSheet.swift */; };
C36B8707243C50C60049991D /* SignalMessaging.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 453518921FC63DBF00210559 /* SignalMessaging.framework */; }; C36B8707243C50C60049991D /* SignalMessaging.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 453518921FC63DBF00210559 /* SignalMessaging.framework */; };
C396DAEF2518408B00FF6DC5 /* ParsingState.swift in Sources */ = {isa = PBXBuildFile; fileRef = C396DAE82518408900FF6DC5 /* ParsingState.swift */; }; C396DAEF2518408B00FF6DC5 /* ParsingState.swift in Sources */ = {isa = PBXBuildFile; fileRef = C396DAE82518408900FF6DC5 /* ParsingState.swift */; };
@ -1033,8 +1035,6 @@
34D1F0B31F86D31D0066283D /* ConversationCollectionView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ConversationCollectionView.m; sourceTree = "<group>"; }; 34D1F0B31F86D31D0066283D /* ConversationCollectionView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ConversationCollectionView.m; sourceTree = "<group>"; };
34D1F0B51F87F8850066283D /* OWSGenericAttachmentView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSGenericAttachmentView.h; sourceTree = "<group>"; }; 34D1F0B51F87F8850066283D /* OWSGenericAttachmentView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSGenericAttachmentView.h; sourceTree = "<group>"; };
34D1F0B61F87F8850066283D /* OWSGenericAttachmentView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSGenericAttachmentView.m; sourceTree = "<group>"; }; 34D1F0B61F87F8850066283D /* OWSGenericAttachmentView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSGenericAttachmentView.m; sourceTree = "<group>"; };
34D1F0B81F8800D90066283D /* OWSAudioMessageView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSAudioMessageView.h; sourceTree = "<group>"; };
34D1F0B91F8800D90066283D /* OWSAudioMessageView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSAudioMessageView.m; sourceTree = "<group>"; };
34D1F0BB1F8D108C0066283D /* AttachmentUploadView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AttachmentUploadView.h; sourceTree = "<group>"; }; 34D1F0BB1F8D108C0066283D /* AttachmentUploadView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AttachmentUploadView.h; sourceTree = "<group>"; };
34D1F0BC1F8D108C0066283D /* AttachmentUploadView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AttachmentUploadView.m; sourceTree = "<group>"; }; 34D1F0BC1F8D108C0066283D /* AttachmentUploadView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AttachmentUploadView.m; sourceTree = "<group>"; };
34D1F0BF1F8EC1760066283D /* MessageRecipientStatusUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageRecipientStatusUtils.swift; sourceTree = "<group>"; }; 34D1F0BF1F8EC1760066283D /* MessageRecipientStatusUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageRecipientStatusUtils.swift; sourceTree = "<group>"; };
@ -1372,6 +1372,7 @@
C31D1DDC25217014005D4DA8 /* UserCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserCell.swift; sourceTree = "<group>"; }; C31D1DDC25217014005D4DA8 /* UserCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserCell.swift; sourceTree = "<group>"; };
C31D1DE22521718E005D4DA8 /* UserSelectionVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSelectionVC.swift; sourceTree = "<group>"; }; C31D1DE22521718E005D4DA8 /* UserSelectionVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSelectionVC.swift; sourceTree = "<group>"; };
C31D1DE8252172D4005D4DA8 /* ContactUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactUtilities.swift; sourceTree = "<group>"; }; C31D1DE8252172D4005D4DA8 /* ContactUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactUtilities.swift; sourceTree = "<group>"; };
C31F812525258FB000DD9FD9 /* Storage+VolumeSamples.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+VolumeSamples.swift"; sourceTree = "<group>"; };
C329FEEB24F7277900B1C64C /* LightModeSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LightModeSheet.swift; sourceTree = "<group>"; }; C329FEEB24F7277900B1C64C /* LightModeSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LightModeSheet.swift; sourceTree = "<group>"; };
C329FEED24F7742E00B1C64C /* UIViewController+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Utilities.swift"; sourceTree = "<group>"; }; C329FEED24F7742E00B1C64C /* UIViewController+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Utilities.swift"; sourceTree = "<group>"; };
C34C8F7323A7830A00D82669 /* SpaceMono-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SpaceMono-Bold.ttf"; sourceTree = "<group>"; }; C34C8F7323A7830A00D82669 /* SpaceMono-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SpaceMono-Bold.ttf"; sourceTree = "<group>"; };
@ -1384,6 +1385,8 @@
C35E8AA62485C85600ACB629 /* GeoLite2-Country-Blocks-IPv4.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "GeoLite2-Country-Blocks-IPv4.csv"; sourceTree = "<group>"; }; C35E8AA62485C85600ACB629 /* GeoLite2-Country-Blocks-IPv4.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "GeoLite2-Country-Blocks-IPv4.csv"; sourceTree = "<group>"; };
C35E8AAD2485E51D00ACB629 /* IP2Country.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IP2Country.swift; sourceTree = "<group>"; }; C35E8AAD2485E51D00ACB629 /* IP2Country.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IP2Country.swift; sourceTree = "<group>"; };
C3638C0424C7F0B500AF29BC /* LK002RemoveFriendRequests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LK002RemoveFriendRequests.swift; sourceTree = "<group>"; }; C3638C0424C7F0B500AF29BC /* LK002RemoveFriendRequests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LK002RemoveFriendRequests.swift; sourceTree = "<group>"; };
C364534F252449260045C478 /* VoiceMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageView.swift; sourceTree = "<group>"; };
C364535B252467900045C478 /* AudioUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioUtilities.swift; sourceTree = "<group>"; };
C369549C24D27A3500CEB4E3 /* MultiDeviceRemovalSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiDeviceRemovalSheet.swift; sourceTree = "<group>"; }; C369549C24D27A3500CEB4E3 /* MultiDeviceRemovalSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiDeviceRemovalSheet.swift; sourceTree = "<group>"; };
C396469C2509D3ED00B0B9F5 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = translations/pl.lproj/Localizable.strings; sourceTree = "<group>"; }; C396469C2509D3ED00B0B9F5 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = translations/pl.lproj/Localizable.strings; sourceTree = "<group>"; };
C396469D2509D3F400B0B9F5 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = translations/ja.lproj/Localizable.strings; sourceTree = "<group>"; }; C396469D2509D3F400B0B9F5 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = translations/ja.lproj/Localizable.strings; sourceTree = "<group>"; };
@ -2048,8 +2051,6 @@
34A8B3502190A40E00218A25 /* MediaAlbumCellView.swift */, 34A8B3502190A40E00218A25 /* MediaAlbumCellView.swift */,
34EA693F2194933900702471 /* MediaDownloadView.swift */, 34EA693F2194933900702471 /* MediaDownloadView.swift */,
34EA69412194DE7F00702471 /* MediaUploadView.swift */, 34EA69412194DE7F00702471 /* MediaUploadView.swift */,
34D1F0B81F8800D90066283D /* OWSAudioMessageView.h */,
34D1F0B91F8800D90066283D /* OWSAudioMessageView.m */,
34DBF005206C3CB100025978 /* OWSBubbleShapeView.h */, 34DBF005206C3CB100025978 /* OWSBubbleShapeView.h */,
34DBF006206C3CB200025978 /* OWSBubbleShapeView.m */, 34DBF006206C3CB200025978 /* OWSBubbleShapeView.m */,
34DBF002206BD5A500025978 /* OWSBubbleView.h */, 34DBF002206BD5A500025978 /* OWSBubbleView.h */,
@ -2603,6 +2604,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
B8CCF63B239757C10091D419 /* Components */, B8CCF63B239757C10091D419 /* Components */,
C31F812425258F9C00DD9FD9 /* Database */,
C32B405424A961E1001117B5 /* Dependencies */, C32B405424A961E1001117B5 /* Dependencies */,
B8CCF63C239757DB0091D419 /* Utilities */, B8CCF63C239757DB0091D419 /* Utilities */,
B8CCF63D2397580E0091D419 /* View Controllers */, B8CCF63D2397580E0091D419 /* View Controllers */,
@ -2692,6 +2694,7 @@
B8BB82B423947F2D00BA5194 /* TextField.swift */, B8BB82B423947F2D00BA5194 /* TextField.swift */,
C3C3CF8824D8EED300E1CCE7 /* TextView.swift */, C3C3CF8824D8EED300E1CCE7 /* TextView.swift */,
C31D1DDC25217014005D4DA8 /* UserCell.swift */, C31D1DDC25217014005D4DA8 /* UserCell.swift */,
C364534F252449260045C478 /* VoiceMessageView.swift */,
); );
path = Components; path = Components;
sourceTree = "<group>"; sourceTree = "<group>";
@ -2700,6 +2703,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
B8544E3223D50E4900299F14 /* AppearanceUtilities.swift */, B8544E3223D50E4900299F14 /* AppearanceUtilities.swift */,
C364535B252467900045C478 /* AudioUtilities.swift */,
C3D0972A2510499C00F6E3E4 /* BackgroundPoller.swift */, C3D0972A2510499C00F6E3E4 /* BackgroundPoller.swift */,
C31A6C5B247F2CF3001123EF /* CGRect+Utilities.swift */, C31A6C5B247F2CF3001123EF /* CGRect+Utilities.swift */,
C31D1DE8252172D4005D4DA8 /* ContactUtilities.swift */, C31D1DE8252172D4005D4DA8 /* ContactUtilities.swift */,
@ -2754,6 +2758,14 @@
path = "View Controllers"; path = "View Controllers";
sourceTree = "<group>"; sourceTree = "<group>";
}; };
C31F812425258F9C00DD9FD9 /* Database */ = {
isa = PBXGroup;
children = (
C31F812525258FB000DD9FD9 /* Storage+VolumeSamples.swift */,
);
path = Database;
sourceTree = "<group>";
};
C32B405424A961E1001117B5 /* Dependencies */ = { C32B405424A961E1001117B5 /* Dependencies */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -3782,7 +3794,6 @@
34DC9BD921543E0C00FDDCEC /* DebugContactsUtils.m in Sources */, 34DC9BD921543E0C00FDDCEC /* DebugContactsUtils.m in Sources */,
34DBF007206C3CB200025978 /* OWSBubbleShapeView.m in Sources */, 34DBF007206C3CB200025978 /* OWSBubbleShapeView.m in Sources */,
4C04392A220A9EC800BAEA63 /* VoiceNoteLock.swift in Sources */, 4C04392A220A9EC800BAEA63 /* VoiceNoteLock.swift in Sources */,
34D1F0BA1F8800D90066283D /* OWSAudioMessageView.m in Sources */,
34D8C02B1ED3685800188D7C /* DebugUIContacts.m in Sources */, 34D8C02B1ED3685800188D7C /* DebugUIContacts.m in Sources */,
3496956E21A301A100DCFE74 /* OWSBackupExportJob.m in Sources */, 3496956E21A301A100DCFE74 /* OWSBackupExportJob.m in Sources */,
4C1885D2218F8E1C00B67051 /* PhotoGridViewCell.swift in Sources */, 4C1885D2218F8E1C00B67051 /* PhotoGridViewCell.swift in Sources */,
@ -3800,6 +3811,7 @@
B879D449247E1BE300DB3608 /* PathVC.swift in Sources */, B879D449247E1BE300DB3608 /* PathVC.swift in Sources */,
34E3EF0D1EFC235B007F6822 /* DebugUIDiskUsage.m in Sources */, 34E3EF0D1EFC235B007F6822 /* DebugUIDiskUsage.m in Sources */,
454A84042059C787008B8C75 /* MediaTileViewController.swift in Sources */, 454A84042059C787008B8C75 /* MediaTileViewController.swift in Sources */,
C364535C252467900045C478 /* AudioUtilities.swift in Sources */,
340FC8B4204DAC8D007AEB0F /* OWSBackupSettingsViewController.m in Sources */, 340FC8B4204DAC8D007AEB0F /* OWSBackupSettingsViewController.m in Sources */,
34D1F0871F8678AA0066283D /* ConversationViewItem.m in Sources */, 34D1F0871F8678AA0066283D /* ConversationViewItem.m in Sources */,
451A13B11E13DED2000A50FD /* AppNotifications.swift in Sources */, 451A13B11E13DED2000A50FD /* AppNotifications.swift in Sources */,
@ -3997,6 +4009,7 @@
340FC8AC204DAC8D007AEB0F /* PrivacySettingsTableViewController.m in Sources */, 340FC8AC204DAC8D007AEB0F /* PrivacySettingsTableViewController.m in Sources */,
B88847BC23E10BC6009836D2 /* GroupMembersVC.swift in Sources */, B88847BC23E10BC6009836D2 /* GroupMembersVC.swift in Sources */,
B85357BF23A1AE0800AAF6CD /* SeedReminderView.swift in Sources */, B85357BF23A1AE0800AAF6CD /* SeedReminderView.swift in Sources */,
C31F812625258FB000DD9FD9 /* Storage+VolumeSamples.swift in Sources */,
B85357C723A1FB5100AAF6CD /* LinkDeviceVCDelegate.swift in Sources */, B85357C723A1FB5100AAF6CD /* LinkDeviceVCDelegate.swift in Sources */,
340FC8C5204DE223007AEB0F /* DebugUIBackup.m in Sources */, 340FC8C5204DE223007AEB0F /* DebugUIBackup.m in Sources */,
C35E8AAE2485E51D00ACB629 /* IP2Country.swift in Sources */, C35E8AAE2485E51D00ACB629 /* IP2Country.swift in Sources */,
@ -4018,6 +4031,7 @@
3427C64320F500E000EEC730 /* OWSMessageTimerView.m in Sources */, 3427C64320F500E000EEC730 /* OWSMessageTimerView.m in Sources */,
B90418E6183E9DD40038554A /* DateUtil.m in Sources */, B90418E6183E9DD40038554A /* DateUtil.m in Sources */,
340FC8BD204DAC8D007AEB0F /* ShowGroupMembersViewController.m in Sources */, 340FC8BD204DAC8D007AEB0F /* ShowGroupMembersViewController.m in Sources */,
C3645350252449260045C478 /* VoiceMessageView.swift in Sources */,
3496956F21A301A100DCFE74 /* OWSBackupLazyRestore.swift in Sources */, 3496956F21A301A100DCFE74 /* OWSBackupLazyRestore.swift in Sources */,
459311FC1D75C948008DD4F0 /* OWSDeviceTableViewCell.m in Sources */, 459311FC1D75C948008DD4F0 /* OWSDeviceTableViewCell.m in Sources */,
); );
@ -4147,7 +4161,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 125; CURRENT_PROJECT_VERSION = 133;
DEBUG_INFORMATION_FORMAT = dwarf; DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = SUQ8J2PCT7; DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = "$(inherited)"; FRAMEWORK_SEARCH_PATHS = "$(inherited)";
@ -4161,7 +4175,7 @@
INFOPLIST_FILE = SignalShareExtension/Info.plist; INFOPLIST_FILE = SignalShareExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0; IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 1.6.0; MARKETING_VERSION = 1.6.2;
MTL_ENABLE_DEBUG_INFO = YES; MTL_ENABLE_DEBUG_INFO = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.share-extension"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.share-extension";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@ -4209,7 +4223,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 125; CURRENT_PROJECT_VERSION = 133;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7; DEVELOPMENT_TEAM = SUQ8J2PCT7;
ENABLE_NS_ASSERTIONS = NO; ENABLE_NS_ASSERTIONS = NO;
@ -4228,7 +4242,7 @@
INFOPLIST_FILE = SignalShareExtension/Info.plist; INFOPLIST_FILE = SignalShareExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0; IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 1.6.0; MARKETING_VERSION = 1.6.2;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.share-extension"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.share-extension";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@ -4263,7 +4277,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 125; CURRENT_PROJECT_VERSION = 133;
DEBUG_INFORMATION_FORMAT = dwarf; DEBUG_INFORMATION_FORMAT = dwarf;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = SUQ8J2PCT7; DEVELOPMENT_TEAM = SUQ8J2PCT7;
@ -4282,7 +4296,7 @@
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 12.0; IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
MARKETING_VERSION = 1.6.0; MARKETING_VERSION = 1.6.2;
MTL_ENABLE_DEBUG_INFO = YES; MTL_ENABLE_DEBUG_INFO = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.utilities"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.utilities";
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
@ -4333,7 +4347,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 125; CURRENT_PROJECT_VERSION = 133;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = SUQ8J2PCT7; DEVELOPMENT_TEAM = SUQ8J2PCT7;
@ -4357,7 +4371,7 @@
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 12.0; IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
MARKETING_VERSION = 1.6.0; MARKETING_VERSION = 1.6.2;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.utilities"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.utilities";
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
@ -4395,7 +4409,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 125; CURRENT_PROJECT_VERSION = 133;
DEBUG_INFORMATION_FORMAT = dwarf; DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = SUQ8J2PCT7; DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = "$(inherited)"; FRAMEWORK_SEARCH_PATHS = "$(inherited)";
@ -4407,7 +4421,7 @@
INFOPLIST_FILE = LokiPushNotificationService/Info.plist; INFOPLIST_FILE = LokiPushNotificationService/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0; IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 1.6.0; MARKETING_VERSION = 1.6.2;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.push-notification-service"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.push-notification-service";
@ -4458,7 +4472,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 125; CURRENT_PROJECT_VERSION = 133;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7; DEVELOPMENT_TEAM = SUQ8J2PCT7;
ENABLE_NS_ASSERTIONS = NO; ENABLE_NS_ASSERTIONS = NO;
@ -4475,7 +4489,7 @@
INFOPLIST_FILE = LokiPushNotificationService/Info.plist; INFOPLIST_FILE = LokiPushNotificationService/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0; IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 1.6.0; MARKETING_VERSION = 1.6.2;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.push-notification-service"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.push-notification-service";
@ -4659,7 +4673,7 @@
CODE_SIGN_ENTITLEMENTS = Signal/Signal.entitlements; CODE_SIGN_ENTITLEMENTS = Signal/Signal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 125; CURRENT_PROJECT_VERSION = 133;
DEVELOPMENT_TEAM = SUQ8J2PCT7; DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@ -4694,7 +4708,7 @@
"$(SRCROOT)", "$(SRCROOT)",
); );
LLVM_LTO = NO; LLVM_LTO = NO;
MARKETING_VERSION = 1.6.0; MARKETING_VERSION = 1.6.2;
OTHER_LDFLAGS = "$(inherited)"; OTHER_LDFLAGS = "$(inherited)";
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
@ -4726,7 +4740,7 @@
CODE_SIGN_ENTITLEMENTS = Signal/Signal.entitlements; CODE_SIGN_ENTITLEMENTS = Signal/Signal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 125; CURRENT_PROJECT_VERSION = 133;
DEVELOPMENT_TEAM = SUQ8J2PCT7; DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@ -4761,7 +4775,7 @@
"$(SRCROOT)", "$(SRCROOT)",
); );
LLVM_LTO = NO; LLVM_LTO = NO;
MARKETING_VERSION = 1.6.0; MARKETING_VERSION = 1.6.2;
OTHER_LDFLAGS = "$(inherited)"; OTHER_LDFLAGS = "$(inherited)";
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
PRODUCT_NAME = Session; PRODUCT_NAME = Session;

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "Pause.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,85 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm
0.000000 0.000000 0.000000 scn
5.142858 0.000000 m
1.714286 0.000000 l
0.767857 0.000000 0.000000 0.767857 0.000000 1.714285 c
0.000000 14.285714 l
0.000000 15.232142 0.767857 16.000000 1.714286 16.000000 c
5.142858 16.000000 l
6.089286 16.000000 6.857143 15.232142 6.857143 14.285714 c
6.857143 1.714285 l
6.857143 0.767857 6.089286 0.000000 5.142858 0.000000 c
h
16.000000 1.714285 m
16.000000 14.285714 l
16.000000 15.232142 15.232143 16.000000 14.285715 16.000000 c
10.857143 16.000000 l
9.910715 16.000000 9.142858 15.232142 9.142858 14.285714 c
9.142858 1.714285 l
9.142858 0.767857 9.910715 0.000000 10.857143 0.000000 c
14.285715 0.000000 l
15.232143 0.000000 16.000000 0.767857 16.000000 1.714285 c
h
f
n
Q
endstream
endobj
3 0 obj
804
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 16.000000 16.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Type /Catalog
/Pages 5 0 R
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000000894 00000 n
0000000916 00000 n
0000001089 00000 n
0000001163 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1222
%%EOF

File diff suppressed because one or more lines are too long

View File

@ -6,10 +6,14 @@
<dict> <dict>
<key>CarthageVersion</key> <key>CarthageVersion</key>
<string>0.36.0</string> <string>0.36.0</string>
<key>DateTime</key>
<string>Tue Oct 27 21:55:52 UTC 2020</string>
<key>OSXVersion</key> <key>OSXVersion</key>
<string>10.15.6</string> <string>10.15.6</string>
<key>WebRTCCommit</key> <key>WebRTCCommit</key>
<string>1445d719bf05280270e9f77576f80f973fd847f8 M73</string> <string>1445d719bf05280270e9f77576f80f973fd847f8 M73</string>
<key>XCodeVersion</key>
<string>1200.1210</string>
</dict> </dict>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>en</string> <string>en</string>

View File

@ -16,13 +16,10 @@ final class PathStatusView : UIView {
private func setUpViewHierarchy() { private func setUpViewHierarchy() {
layer.cornerRadius = Values.pathStatusViewSize / 2 layer.cornerRadius = Values.pathStatusViewSize / 2
layer.masksToBounds = false layer.masksToBounds = false
if OnionRequestAPI.paths.count < OnionRequestAPI.pathCount { if OnionRequestAPI.paths.isEmpty {
let storage = OWSPrimaryStorage.shared() OnionRequestAPI.paths = Storage.getOnionRequestPaths()
storage.dbReadConnection.read { transaction in
OnionRequestAPI.paths = storage.getOnionRequestPaths(in: transaction)
}
} }
let color = (OnionRequestAPI.paths.count >= OnionRequestAPI.pathCount) ? Colors.accent : Colors.pathsBuilding let color = (!OnionRequestAPI.paths.isEmpty) ? Colors.accent : Colors.pathsBuilding
setColor(to: color, isAnimated: false) setColor(to: color, isAnimated: false)
} }

View File

@ -0,0 +1,214 @@
import Accelerate
import NVActivityIndicatorView
@objc(LKVoiceMessageView)
final class VoiceMessageView : UIView {
private let voiceMessage: TSAttachment
private let isOutgoing: Bool
private var isLoading = false
private var isForcedAnimation = false
private var volumeSamples: [Float] = [] { didSet { updateShapeLayers() } }
@objc var progress: CGFloat = 0 { didSet { updateShapeLayers() } }
@objc var duration: Int = 0 { didSet { updateDurationLabel() } }
@objc var isPlaying = false { didSet { updateToggleImageView() } }
// MARK: Components
private lazy var toggleImageView = UIImageView(image: #imageLiteral(resourceName: "Play"))
private lazy var spinner = NVActivityIndicatorView(frame: CGRect.zero, type: .circleStrokeSpin, color: .black, padding: nil)
private lazy var durationLabel: UILabel = {
let result = UILabel()
result.textColor = Colors.text
result.font = .systemFont(ofSize: Values.mediumFontSize)
return result
}()
private lazy var backgroundShapeLayer: CAShapeLayer = {
let result = CAShapeLayer()
result.fillColor = Colors.text.cgColor
return result
}()
private lazy var foregroundShapeLayer: CAShapeLayer = {
let result = CAShapeLayer()
result.fillColor = (isLightMode && isOutgoing) ? UIColor.white.cgColor : Colors.accent.cgColor
return result
}()
// MARK: Settings
private let leadingInset: CGFloat = 0
private let sampleSpacing: CGFloat = 1
private let targetSampleCount = 48
private let toggleContainerSize: CGFloat = 32
private let vMargin: CGFloat = 0
@objc public static let contentHeight: CGFloat = 40
// MARK: Initialization
@objc(initWithVoiceMessage:isOutgoing:)
init(voiceMessage: TSAttachment, isOutgoing: Bool) {
self.voiceMessage = voiceMessage
self.isOutgoing = isOutgoing
super.init(frame: CGRect.zero)
}
override init(frame: CGRect) {
preconditionFailure("Use init(voiceMessage:associatedWith:) instead.")
}
required init?(coder: NSCoder) {
preconditionFailure("Use init(voiceMessage:associatedWith:) instead.")
}
@objc func initialize() {
setUpViewHierarchy()
if voiceMessage.isDownloaded {
guard let url = (voiceMessage as? TSAttachmentStream)?.originalMediaURL else {
return print("[Loki] Couldn't get URL for voice message.")
}
if let cachedVolumeSamples = Storage.getVolumeSamples(for: voiceMessage.uniqueId!), cachedVolumeSamples.count == targetSampleCount {
self.hideLoader()
self.volumeSamples = cachedVolumeSamples
} else {
let voiceMessageID = voiceMessage.uniqueId!
AudioUtilities.getVolumeSamples(for: url, targetSampleCount: targetSampleCount).done(on: DispatchQueue.main) { [weak self] volumeSamples in
guard let self = self else { return }
self.hideLoader()
self.isForcedAnimation = true
self.volumeSamples = volumeSamples
Storage.write { transaction in
Storage.setVolumeSamples(for: voiceMessageID, to: volumeSamples, using: transaction)
}
}.catch(on: DispatchQueue.main) { error in
print("[Loki] Couldn't sample audio file due to error: \(error).")
}
}
} else {
showLoader()
}
}
private func setUpViewHierarchy() {
set(.width, to: 200)
set(.height, to: VoiceMessageView.contentHeight)
layer.insertSublayer(backgroundShapeLayer, at: 0)
layer.insertSublayer(foregroundShapeLayer, at: 1)
let toggleContainer = UIView()
toggleContainer.clipsToBounds = false
toggleContainer.addSubview(toggleImageView)
toggleImageView.set(.width, to: 12)
toggleImageView.set(.height, to: 12)
toggleImageView.center(in: toggleContainer)
toggleContainer.addSubview(spinner)
spinner.set(.width, to: 24)
spinner.set(.height, to: 24)
spinner.center(in: toggleContainer)
toggleContainer.set(.width, to: toggleContainerSize)
toggleContainer.set(.height, to: toggleContainerSize)
toggleContainer.layer.cornerRadius = toggleContainerSize / 2
toggleContainer.backgroundColor = UIColor.white
let glowRadius: CGFloat = isLightMode ? 1 : 2
let glowColor = isLightMode ? UIColor.black.withAlphaComponent(0.4) : UIColor.black
let glowConfiguration = UIView.CircularGlowConfiguration(size: toggleContainerSize, color: glowColor, radius: glowRadius)
toggleContainer.setCircularGlow(with: glowConfiguration)
addSubview(toggleContainer)
toggleContainer.center(.vertical, in: self)
toggleContainer.pin(.leading, to: .leading, of: self, withInset: leadingInset)
addSubview(durationLabel)
durationLabel.center(.vertical, in: self)
durationLabel.pin(.trailing, to: .trailing, of: self)
}
// MARK: UI & Updating
private func showLoader() {
isLoading = true
toggleImageView.isHidden = true
spinner.startAnimating()
spinner.isHidden = false
Timer.scheduledTimer(withTimeInterval: 0.25, repeats: true) { [weak self] timer in
guard let self = self else { return timer.invalidate() }
if self.isLoading {
self.updateFakeVolumeSamples()
} else {
timer.invalidate()
}
}
updateFakeVolumeSamples()
}
private func updateFakeVolumeSamples() {
let fakeVolumeSamples = (0..<targetSampleCount).map { _ in Float.random(in: 0...1) }
volumeSamples = fakeVolumeSamples
}
private func hideLoader() {
isLoading = false
toggleImageView.isHidden = false
spinner.stopAnimating()
spinner.isHidden = true
}
override func layoutSubviews() {
super.layoutSubviews()
updateShapeLayers()
}
private func updateShapeLayers() {
clipsToBounds = false // Bit of a hack to do this here, but the containing stack view turns this off all the time
guard !volumeSamples.isEmpty else { return }
let sMin = CGFloat(volumeSamples.min()!)
let sMax = CGFloat(volumeSamples.max()!)
let w = width() - leadingInset - toggleContainerSize - durationLabel.width() - 2 * Values.smallSpacing
let h = height() - 2 * vMargin
let sW = (w - sampleSpacing * CGFloat(volumeSamples.count - 1)) / CGFloat(volumeSamples.count)
let backgroundPath = UIBezierPath()
let foregroundPath = UIBezierPath()
for (i, value) in volumeSamples.enumerated() {
let x = leadingInset + toggleContainerSize + Values.smallSpacing + CGFloat(i) * (sW + sampleSpacing)
let fraction = (CGFloat(value) - sMin) / (sMax - sMin)
let sH = max(8, h * fraction)
let y = vMargin + (h - sH) / 2
let subPath = UIBezierPath(roundedRect: CGRect(x: x, y: y, width: sW, height: sH), cornerRadius: sW / 2)
backgroundPath.append(subPath)
if progress > CGFloat(i) / CGFloat(volumeSamples.count) { foregroundPath.append(subPath) }
}
backgroundPath.close()
foregroundPath.close()
if isLoading || isForcedAnimation {
let animation = CABasicAnimation(keyPath: "path")
animation.duration = 0.25
animation.toValue = backgroundPath
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
backgroundShapeLayer.add(animation, forKey: "path")
backgroundShapeLayer.path = backgroundPath.cgPath
} else {
backgroundShapeLayer.path = backgroundPath.cgPath
}
foregroundShapeLayer.path = foregroundPath.cgPath
isForcedAnimation = false
}
private func updateDurationLabel() {
durationLabel.text = OWSFormat.formatDurationSeconds(duration)
updateShapeLayers()
}
private func updateToggleImageView() {
toggleImageView.image = isPlaying ? #imageLiteral(resourceName: "Pause") : #imageLiteral(resourceName: "Play")
}
// MARK: Interaction
@objc(getCurrentTime:)
func getCurrentTime(for panGestureRecognizer: UIPanGestureRecognizer) -> TimeInterval {
guard voiceMessage.isDownloaded else { return 0 }
let locationInSelf = panGestureRecognizer.location(in: self)
let waveformFrameOrigin = CGPoint(x: leadingInset + toggleContainerSize + Values.smallSpacing, y: vMargin)
let waveformFrameSize = CGSize(width: width() - leadingInset - toggleContainerSize - durationLabel.width() - 2 * Values.smallSpacing,
height: height() - 2 * vMargin)
let waveformFrame = CGRect(origin: waveformFrameOrigin, size: waveformFrameSize)
guard waveformFrame.contains(locationInSelf) else { return 0 }
let fraction = (locationInSelf.x - waveformFrame.minX) / (waveformFrame.maxX - waveformFrame.minX)
return Double(fraction) * Double(duration)
}
}

View File

@ -0,0 +1,17 @@
extension Storage {
static let volumeSamplesCollection = "LokiVolumeSamplesCollection"
static func getVolumeSamples(for attachment: String) -> [Float]? {
var result: [Float]?
read { transaction in
result = transaction.object(forKey: attachment, inCollection: volumeSamplesCollection) as? [Float]
}
return result
}
static func setVolumeSamples(for attachment: String, to volumeSamples: [Float], using transaction: YapDatabaseReadWriteTransaction) {
transaction.setObject(volumeSamples, forKey: attachment, inCollection: volumeSamplesCollection)
}
}

View File

@ -0,0 +1,190 @@
import Accelerate
import PromiseKit
enum AudioUtilities {
private static let noiseFloor: Float = -80
private struct FileInfo {
let sampleCount: Int
let asset: AVAsset
let track: AVAssetTrack
}
enum Error : LocalizedError {
case noAudioTrack
case noAudioFormatDescription
case loadingFailed
case parsingFailed
var errorDescription: String? {
switch self {
case .noAudioTrack: return "No audio track."
case .noAudioFormatDescription: return "No audio format description."
case .loadingFailed: return "Couldn't load asset."
case .parsingFailed: return "Couldn't parse asset."
}
}
}
static func getVolumeSamples(for audioFileURL: URL, targetSampleCount: Int) -> Promise<[Float]> {
return loadFile(audioFileURL).then { fileInfo in
AudioUtilities.parseSamples(from: fileInfo, with: targetSampleCount)
}
}
private static func loadFile(_ audioFileURL: URL, isRetry: Bool = false) -> Promise<FileInfo> {
let asset = AVURLAsset(url: audioFileURL)
guard let track = asset.tracks(withMediaType: AVMediaType.audio).first else {
if isRetry {
return Promise(error: Error.loadingFailed)
} else {
// Workaround for issue where MP3 files sent by Android get saved as M4A
var newAudioFileURL = audioFileURL.deletingPathExtension()
let fileName = newAudioFileURL.lastPathComponent
newAudioFileURL = newAudioFileURL.deletingLastPathComponent()
newAudioFileURL = newAudioFileURL.appendingPathComponent("\(fileName).mp3")
let fileManager = FileManager.default
if fileManager.fileExists(atPath: newAudioFileURL.path) {
return loadFile(newAudioFileURL, isRetry: true)
} else {
do {
try FileManager.default.copyItem(at: audioFileURL, to: newAudioFileURL)
} catch {
return Promise(error: Error.loadingFailed)
}
return loadFile(newAudioFileURL, isRetry: true)
}
}
}
let (promise, seal) = Promise<FileInfo>.pending()
asset.loadValuesAsynchronously(forKeys: [ #keyPath(AVAsset.duration) ]) {
var nsError: NSError?
let status = asset.statusOfValue(forKey: #keyPath(AVAsset.duration), error: &nsError)
switch status {
case .loaded:
guard let formatDescriptions = track.formatDescriptions as? [CMAudioFormatDescription],
let audioFormatDescription = formatDescriptions.first,
let asbd = CMAudioFormatDescriptionGetStreamBasicDescription(audioFormatDescription)
else { return seal.reject(Error.noAudioFormatDescription) }
let sampleCount = Int((asbd.pointee.mSampleRate) * Float64(asset.duration.value) / Float64(asset.duration.timescale))
let fileInfo = FileInfo(sampleCount: sampleCount, asset: asset, track: track)
seal.fulfill(fileInfo)
default:
print("Couldn't load asset due to error: \(nsError?.localizedDescription ?? "no description provided").")
seal.reject(Error.loadingFailed)
}
}
return promise
}
private static func parseSamples(from fileInfo: FileInfo, with targetSampleCount: Int) -> Promise<[Float]> {
// Prepare the reader
guard let reader = try? AVAssetReader(asset: fileInfo.asset) else { return Promise(error: Error.parsingFailed) }
let range = 0..<fileInfo.sampleCount
reader.timeRange = CMTimeRange(start: CMTime(value: Int64(range.lowerBound), timescale: fileInfo.asset.duration.timescale),
duration: CMTime(value: Int64(range.count), timescale: fileInfo.asset.duration.timescale))
let outputSettings: [String:Any] = [
AVFormatIDKey : Int(kAudioFormatLinearPCM),
AVLinearPCMBitDepthKey : 16,
AVLinearPCMIsBigEndianKey : false,
AVLinearPCMIsFloatKey : false,
AVLinearPCMIsNonInterleaved : false
]
let output = AVAssetReaderTrackOutput(track: fileInfo.track, outputSettings: outputSettings)
output.alwaysCopiesSampleData = false
reader.add(output)
var channelCount = 1
let formatDescriptions = fileInfo.track.formatDescriptions as! [CMAudioFormatDescription]
for audioFormatDescription in formatDescriptions {
guard let asbd = CMAudioFormatDescriptionGetStreamBasicDescription(audioFormatDescription) else {
return Promise(error: Error.parsingFailed)
}
channelCount = Int(asbd.pointee.mChannelsPerFrame)
}
let samplesPerPixel = max(1, channelCount * range.count / targetSampleCount)
let filter = [Float](repeating: 1 / Float(samplesPerPixel), count: samplesPerPixel)
var result = [Float]()
var sampleBuffer = Data()
// Read the file
reader.startReading()
defer { reader.cancelReading() }
while reader.status == .reading {
guard let readSampleBuffer = output.copyNextSampleBuffer(),
let readBuffer = CMSampleBufferGetDataBuffer(readSampleBuffer) else { break }
var readBufferLength = 0
var readBufferPointer: UnsafeMutablePointer<Int8>?
CMBlockBufferGetDataPointer(readBuffer,
atOffset: 0,
lengthAtOffsetOut: &readBufferLength,
totalLengthOut: nil,
dataPointerOut: &readBufferPointer)
sampleBuffer.append(UnsafeBufferPointer(start: readBufferPointer, count: readBufferLength))
CMSampleBufferInvalidate(readSampleBuffer)
let sampleCount = sampleBuffer.count / MemoryLayout<Int16>.size
let downSampledLength = sampleCount / samplesPerPixel
let samplesToProcess = downSampledLength * samplesPerPixel
guard samplesToProcess > 0 else { continue }
processSamples(from: &sampleBuffer,
outputSamples: &result,
samplesToProcess: samplesToProcess,
downSampledLength: downSampledLength,
samplesPerPixel: samplesPerPixel,
filter: filter)
}
// Process any remaining samples
let samplesToProcess = sampleBuffer.count / MemoryLayout<Int16>.size
if samplesToProcess > 0 {
let downSampledLength = 1
let samplesPerPixel = samplesToProcess
let filter = [Float](repeating: 1.0 / Float(samplesPerPixel), count: samplesPerPixel)
processSamples(from: &sampleBuffer,
outputSamples: &result,
samplesToProcess: samplesToProcess,
downSampledLength: downSampledLength,
samplesPerPixel: samplesPerPixel,
filter: filter)
}
guard reader.status == .completed else { return Promise(error: Error.parsingFailed) }
// Return
return Promise { $0.fulfill(result) }
}
private static func processSamples(from sampleBuffer: inout Data, outputSamples: inout [Float], samplesToProcess: Int,
downSampledLength: Int, samplesPerPixel: Int, filter: [Float]) {
sampleBuffer.withUnsafeBytes { (samples: UnsafeRawBufferPointer) in
var processingBuffer = [Float](repeating: 0, count: samplesToProcess)
let sampleCount = vDSP_Length(samplesToProcess)
// Create an UnsafePointer<Int16> from the samples
let unsafeBufferPointer = samples.bindMemory(to: Int16.self)
let unsafePointer = unsafeBufferPointer.baseAddress!
// Convert 16 bit int samples to floats
vDSP_vflt16(unsafePointer, 1, &processingBuffer, 1, sampleCount)
// Take the absolute values to get the amplitude
vDSP_vabs(processingBuffer, 1, &processingBuffer, 1, sampleCount)
// Get the corresponding dB values and clip the results
getdB(from: &processingBuffer)
// Downsample and average
var downSampledData = [Float](repeating: 0, count: downSampledLength)
vDSP_desamp(processingBuffer,
vDSP_Stride(samplesPerPixel),
filter,
&downSampledData,
vDSP_Length(downSampledLength),
vDSP_Length(samplesPerPixel))
// Remove the processed samples
sampleBuffer.removeFirst(samplesToProcess * MemoryLayout<Int16>.size)
// Update the output samples
outputSamples += downSampledData
}
}
static func getdB(from normalizedSamples: inout [Float]) {
// Convert samples to a log scale
var zero: Float = 32768.0
vDSP_vdbcon(normalizedSamples, 1, &zero, &normalizedSamples, 1, vDSP_Length(normalizedSamples.count), 1)
// Clip to [noiseFloor, 0]
var ceil: Float = 0.0
var noiseFloorMutable = AudioUtilities.noiseFloor
vDSP_vclip(normalizedSamples, 1, &noiseFloorMutable, &ceil, &normalizedSamples, 1, vDSP_Length(normalizedSamples.count))
}
}

View File

@ -51,14 +51,11 @@ final class IP2Country {
} }
func populateCacheIfNeeded() -> Bool { func populateCacheIfNeeded() -> Bool {
if OnionRequestAPI.paths.count < OnionRequestAPI.pathCount { if OnionRequestAPI.paths.isEmpty {
let storage = OWSPrimaryStorage.shared() OnionRequestAPI.paths = Storage.getOnionRequestPaths()
storage.dbReadConnection.read { transaction in
OnionRequestAPI.paths = storage.getOnionRequestPaths(in: transaction)
}
} }
let paths = OnionRequestAPI.paths let paths = OnionRequestAPI.paths
guard paths.count >= OnionRequestAPI.pathCount else { return false } guard !paths.isEmpty else { return false }
let pathToDisplay = paths.first! let pathToDisplay = paths.first!
pathToDisplay.forEach { snode in pathToDisplay.forEach { snode in
let _ = self.cacheCountry(for: snode.ip) // Preload if needed let _ = self.cacheCountry(for: snode.ip) // Preload if needed

View File

@ -10,7 +10,7 @@ extension UIView {
let opacity: Float let opacity: Float
let radius: CGFloat let radius: CGFloat
init(size: CGFloat, color: UIColor, isAnimated: Bool, animationDuration: TimeInterval = 0.25, offset: CGSize = CGSize(width: 0, height: 0.8), opacity: Float = isLightMode ? 0.4 : 1, radius: CGFloat) { init(size: CGFloat, color: UIColor, isAnimated: Bool = false, animationDuration: TimeInterval = 0.25, offset: CGSize = CGSize(width: 0, height: 0.8), opacity: Float = isLightMode ? 0.4 : 1, radius: CGFloat) {
self.size = size self.size = size
self.color = color self.color = color
self.isAnimated = isAnimated self.isAnimated = isAnimated

View File

@ -22,6 +22,14 @@ final class EditClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelega
return result return result
}() }()
private lazy var addMembersButton: Button = {
let result = Button(style: .prominentOutline, size: .large)
result.setTitle("Add Members", for: UIControl.State.normal)
result.addTarget(self, action: #selector(addMembers), for: UIControl.Event.touchUpInside)
result.contentEdgeInsets = UIEdgeInsets(top: 0, leading: Values.mediumSpacing, bottom: 0, trailing: Values.mediumSpacing)
return result
}()
@objc private lazy var tableView: UITableView = { @objc private lazy var tableView: UITableView = {
let result = UITableView() let result = UITableView()
result.dataSource = self result.dataSource = self
@ -56,13 +64,13 @@ final class EditClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelega
let backButton = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil) let backButton = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil)
backButton.tintColor = Colors.text backButton.tintColor = Colors.text
navigationItem.backBarButtonItem = backButton navigationItem.backBarButtonItem = backButton
setUpViewHierarchy()
updateNavigationBarButtons()
name = thread.groupModel.groupName!
func getDisplayName(for publicKey: String) -> String { func getDisplayName(for publicKey: String) -> String {
return UserDisplayNameUtilities.getPrivateChatDisplayName(for: publicKey) ?? publicKey return UserDisplayNameUtilities.getPrivateChatDisplayName(for: publicKey) ?? publicKey
} }
members = GroupUtilities.getClosedGroupMembers(thread).sorted { getDisplayName(for: $0) < getDisplayName(for: $1) } members = GroupUtilities.getClosedGroupMembers(thread).sorted { getDisplayName(for: $0) < getDisplayName(for: $1) }
setUpViewHierarchy()
updateNavigationBarButtons()
name = thread.groupModel.groupName!
} }
private func setUpViewHierarchy() { private func setUpViewHierarchy() {
@ -88,11 +96,8 @@ final class EditClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelega
membersLabel.font = .systemFont(ofSize: Values.mediumFontSize) membersLabel.font = .systemFont(ofSize: Values.mediumFontSize)
membersLabel.text = "Members" membersLabel.text = "Members"
// Add members button // Add members button
let addMembersButton = Button(style: .prominentOutline, size: .large) let hasContactsToAdd = !Set(ContactUtilities.getAllContacts()).subtracting(self.members).isEmpty
addMembersButton.setTitle("Add Members", for: UIControl.State.normal) if (!hasContactsToAdd) {
addMembersButton.addTarget(self, action: #selector(addMembers), for: UIControl.Event.touchUpInside)
addMembersButton.contentEdgeInsets = UIEdgeInsets(top: 0, leading: Values.mediumSpacing, bottom: 0, trailing: Values.mediumSpacing)
if (Set(ContactUtilities.getAllContacts()).subtracting(members).isEmpty) {
addMembersButton.isUserInteractionEnabled = false addMembersButton.isUserInteractionEnabled = false
let disabledColor = Colors.text.withAlphaComponent(Values.unimportantElementOpacity) let disabledColor = Colors.text.withAlphaComponent(Values.unimportantElementOpacity)
addMembersButton.layer.borderColor = disabledColor.cgColor addMembersButton.layer.borderColor = disabledColor.cgColor
@ -222,6 +227,11 @@ final class EditClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelega
return UserDisplayNameUtilities.getPrivateChatDisplayName(for: publicKey) ?? publicKey return UserDisplayNameUtilities.getPrivateChatDisplayName(for: publicKey) ?? publicKey
} }
self.members = members.sorted { getDisplayName(for: $0) < getDisplayName(for: $1) } self.members = members.sorted { getDisplayName(for: $0) < getDisplayName(for: $1) }
let hasContactsToAdd = !Set(ContactUtilities.getAllContacts()).subtracting(self.members).isEmpty
self.addMembersButton.isUserInteractionEnabled = hasContactsToAdd
let color = hasContactsToAdd ? Colors.accent : Colors.text.withAlphaComponent(Values.unimportantElementOpacity)
self.addMembersButton.layer.borderColor = color.cgColor
self.addMembersButton.setTitleColor(color, for: UIControl.State.normal)
} }
navigationController!.pushViewController(userSelectionVC, animated: true, completion: nil) navigationController!.pushViewController(userSelectionVC, animated: true, completion: nil)
} }
@ -241,13 +251,17 @@ final class EditClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelega
guard members != Set(thread.groupModel.groupMemberIds) || name != thread.groupModel.groupName else { guard members != Set(thread.groupModel.groupMemberIds) || name != thread.groupModel.groupName else {
return popToConversationVC(self) return popToConversationVC(self)
} }
try! Storage.writeSync { [weak self] transaction in ModalActivityIndicatorViewController.present(fromViewController: navigationController!, canCancel: false) { [weak self] _ in
ClosedGroupsProtocol.update(groupPublicKey, with: members, name: name, transaction: transaction).done(on: DispatchQueue.main) { try! Storage.writeSync { [weak self] transaction in
guard let self = self else { return } ClosedGroupsProtocol.update(groupPublicKey, with: members, name: name, transaction: transaction).done(on: DispatchQueue.main) {
popToConversationVC(self) guard let self = self else { return }
}.catch(on: DispatchQueue.main) { error in self.dismiss(animated: true, completion: nil) // Dismiss the loader
guard let self = self else { return } popToConversationVC(self)
self.showError(title: "Couldn't Update Group", message: "Please check your internet connection and try again.") }.catch(on: DispatchQueue.main) { error in
guard let self = self else { return }
self.dismiss(animated: true, completion: nil) // Dismiss the loader
self.showError(title: "Couldn't Update Group", message: "Please check your internet connection and try again.")
}
} }
} }
} }

View File

@ -105,7 +105,7 @@ final class PathVC : BaseVC {
private func update() { private func update() {
pathStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } pathStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
if OnionRequestAPI.paths.count >= OnionRequestAPI.pathCount { if !OnionRequestAPI.paths.isEmpty {
let pathToDisplay = OnionRequestAPI.paths.first! let pathToDisplay = OnionRequestAPI.paths.first!
let dotAnimationRepeatInterval = Double(pathToDisplay.count) + 2 let dotAnimationRepeatInterval = Double(pathToDisplay.count) + 2
let snodeRows: [UIStackView] = pathToDisplay.enumerated().map { index, snode in let snodeRows: [UIStackView] = pathToDisplay.enumerated().map { index, snode in

View File

@ -7,8 +7,8 @@ final class RestoreVC : BaseVC {
private var bottomConstraint: NSLayoutConstraint! private var bottomConstraint: NSLayoutConstraint!
// MARK: Components // MARK: Components
private lazy var mnemonicTextField: TextField = { private lazy var mnemonicTextView: TextView = {
let result = TextField(placeholder: NSLocalizedString("vc_restore_seed_text_field_hint", comment: "")) let result = TextView(placeholder: NSLocalizedString("vc_restore_seed_text_field_hint", comment: ""))
result.layer.borderColor = Colors.text.cgColor result.layer.borderColor = Colors.text.cgColor
return result return result
}() }()
@ -77,7 +77,7 @@ final class RestoreVC : BaseVC {
restoreButtonContainer.pin(.trailing, to: .trailing, of: restoreButton, withInset: Values.massiveSpacing) restoreButtonContainer.pin(.trailing, to: .trailing, of: restoreButton, withInset: Values.massiveSpacing)
restoreButtonContainer.pin(.bottom, to: .bottom, of: restoreButton) restoreButtonContainer.pin(.bottom, to: .bottom, of: restoreButton)
// Set up top stack view // Set up top stack view
let topStackView = UIStackView(arrangedSubviews: [ titleLabel, spacer1, explanationLabel, spacer2, mnemonicTextField, spacer3, legalLabel ]) let topStackView = UIStackView(arrangedSubviews: [ titleLabel, spacer1, explanationLabel, spacer2, mnemonicTextView, spacer3, legalLabel ])
topStackView.axis = .vertical topStackView.axis = .vertical
topStackView.alignment = .fill topStackView.alignment = .fill
// Set up top stack view container // Set up top stack view container
@ -111,7 +111,7 @@ final class RestoreVC : BaseVC {
// On small screens we hide the legal label when the keyboard is up, but it's important that the user sees it so // On small screens we hide the legal label when the keyboard is up, but it's important that the user sees it so
// in those instances we don't make the keyboard come up automatically // in those instances we don't make the keyboard come up automatically
if !isIPhone5OrSmaller { if !isIPhone5OrSmaller {
mnemonicTextField.becomeFirstResponder() mnemonicTextView.becomeFirstResponder()
} }
} }
@ -121,7 +121,7 @@ final class RestoreVC : BaseVC {
// MARK: General // MARK: General
@objc private func dismissKeyboard() { @objc private func dismissKeyboard() {
mnemonicTextField.resignFirstResponder() mnemonicTextView.resignFirstResponder()
} }
// MARK: Updating // MARK: Updating
@ -159,7 +159,7 @@ final class RestoreVC : BaseVC {
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil)) alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
presentAlert(alert) presentAlert(alert)
} }
let mnemonic = mnemonicTextField.text! let mnemonic = mnemonicTextView.text!
do { do {
let hexEncodedSeed = try Mnemonic.decode(mnemonic: mnemonic) let hexEncodedSeed = try Mnemonic.decode(mnemonic: mnemonic)
let seed = Data(hex: hexEncodedSeed) let seed = Data(hex: hexEncodedSeed)
@ -171,7 +171,7 @@ final class RestoreVC : BaseVC {
TSAccountManager.sharedInstance().phoneNumberAwaitingVerification = keyPair.hexEncodedPublicKey TSAccountManager.sharedInstance().phoneNumberAwaitingVerification = keyPair.hexEncodedPublicKey
OWSPrimaryStorage.shared().setRestorationTime(Date().timeIntervalSince1970) OWSPrimaryStorage.shared().setRestorationTime(Date().timeIntervalSince1970)
UserDefaults.standard[.hasViewedSeed] = true UserDefaults.standard[.hasViewedSeed] = true
mnemonicTextField.resignFirstResponder() mnemonicTextView.resignFirstResponder()
Timer.scheduledTimer(withTimeInterval: 0.25, repeats: false) { _ in Timer.scheduledTimer(withTimeInterval: 0.25, repeats: false) { _ in
let displayNameVC = DisplayNameVC() let displayNameVC = DisplayNameVC()
self.navigationController!.pushViewController(displayNameVC, animated: true) self.navigationController!.pushViewController(displayNameVC, animated: true)

View File

@ -22,7 +22,6 @@
#import "NotificationSettingsViewController.h" #import "NotificationSettingsViewController.h"
#import "OWSAddToContactViewController.h" #import "OWSAddToContactViewController.h"
#import "OWSAnyTouchGestureRecognizer.h" #import "OWSAnyTouchGestureRecognizer.h"
#import "OWSAudioMessageView.h"
#import "OWSAudioPlayer.h" #import "OWSAudioPlayer.h"
#import "OWSBackup.h" #import "OWSBackup.h"
#import "OWSBackupIO.h" #import "OWSBackupIO.h"

View File

@ -228,6 +228,7 @@ class ColorPickerView: UIView, ColorViewDelegate {
} }
private func updateMockConversationView() { private func updateMockConversationView() {
/*
conversationStyle.viewWidth = max(bounds.size.width, kMinimumConversationWidth) conversationStyle.viewWidth = max(bounds.size.width, kMinimumConversationWidth)
mockConversationView.subviews.forEach { $0.removeFromSuperview() } mockConversationView.subviews.forEach { $0.removeFromSuperview() }
@ -275,6 +276,7 @@ class ColorPickerView: UIView, ColorViewDelegate {
mockConversationView.addSubview(messagesStackView) mockConversationView.addSubview(messagesStackView)
messagesStackView.autoPinEdgesToSuperviewMargins() messagesStackView.autoPinEdgesToSuperviewMargins()
*/
} }
private func buildPaletteView(colorViews: [ColorView]) -> UIView { private func buildPaletteView(colorViews: [ColorView]) -> UIView {
@ -303,6 +305,7 @@ class ColorPickerView: UIView, ColorViewDelegate {
// MARK: Mock Classes for rendering demo conversation // MARK: Mock Classes for rendering demo conversation
/*
@objc @objc
private class MockConversationViewItem: NSObject, ConversationViewItem { private class MockConversationViewItem: NSObject, ConversationViewItem {
var userCanDeleteGroupMessage: Bool = false var userCanDeleteGroupMessage: Bool = false
@ -444,6 +447,7 @@ private class MockConversationViewItem: NSObject, ConversationViewItem {
return false return false
} }
} }
*/
private class MockIncomingMessage: TSIncomingMessage { private class MockIncomingMessage: TSIncomingMessage {
init(messageBody: String) { init(messageBody: String) {

View File

@ -170,9 +170,17 @@ public class ConversationMediaView: UIView {
} }
backgroundColor = (Theme.isDarkThemeEnabled ? .ows_gray90 : .ows_gray05) backgroundColor = (Theme.isDarkThemeEnabled ? .ows_gray90 : .ows_gray05)
let progressView = MediaDownloadView(attachmentId: attachmentId, radius: maxMessageWidth * 0.1) let view: UIView
self.addSubview(progressView) if isOnionRouted { // Loki: Due to the way onion routing works we can't get upload progress for those attachments
progressView.autoPinEdgesToSuperviewEdges() let activityIndicatorView = UIActivityIndicatorView(style: .white)
activityIndicatorView.isHidden = false
activityIndicatorView.startAnimating()
view = activityIndicatorView
} else {
view = MediaDownloadView(attachmentId: attachmentId, radius: maxMessageWidth * 0.1)
}
addSubview(view)
view.autoPinEdgesToSuperviewEdges()
} }
private func addUploadProgressIfNecessary(_ subview: UIView) -> Bool { private func addUploadProgressIfNecessary(_ subview: UIView) -> Bool {

View File

@ -1,27 +0,0 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
NS_ASSUME_NONNULL_BEGIN
@class ConversationStyle;
@class TSAttachment;
@protocol ConversationViewItem;
@interface OWSAudioMessageView : UIStackView
- (instancetype)initWithAttachment:(TSAttachment *)attachment
isIncoming:(BOOL)isIncoming
viewItem:(id<ConversationViewItem>)viewItem
conversationStyle:(ConversationStyle *)conversationStyle;
- (void)createContents;
+ (CGFloat)bubbleHeight;
- (void)updateContents;
@end
NS_ASSUME_NONNULL_END

View File

@ -1,305 +0,0 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
#import "OWSAudioMessageView.h"
#import "ConversationViewItem.h"
#import "Session-Swift.h"
#import "UIColor+OWS.h"
#import "ViewControllerUtils.h"
#import <SignalMessaging/OWSFormat.h>
#import <SignalMessaging/UIColor+OWS.h>
#import <SessionServiceKit/MIMETypeUtil.h>
NS_ASSUME_NONNULL_BEGIN
@interface OWSAudioMessageView ()
@property (nonatomic) TSAttachment *attachment;
@property (nonatomic, nullable) TSAttachmentStream *attachmentStream;
@property (nonatomic) BOOL isIncoming;
@property (nonatomic, weak) id<ConversationViewItem> viewItem;
@property (nonatomic, readonly) ConversationStyle *conversationStyle;
@property (nonatomic, nullable) UIButton *audioPlayPauseButton;
@property (nonatomic, nullable) UILabel *audioBottomLabel;
@property (nonatomic, nullable) AudioProgressView *audioProgressView;
@end
#pragma mark -
@implementation OWSAudioMessageView
- (instancetype)initWithAttachment:(TSAttachment *)attachment
isIncoming:(BOOL)isIncoming
viewItem:(id<ConversationViewItem>)viewItem
conversationStyle:(ConversationStyle *)conversationStyle
{
self = [super init];
if (self) {
_attachment = attachment;
if ([attachment isKindOfClass:[TSAttachmentStream class]]) {
_attachmentStream = (TSAttachmentStream *)attachment;
}
_isIncoming = isIncoming;
_viewItem = viewItem;
_conversationStyle = conversationStyle;
}
return self;
}
- (void)updateContents
{
[self updateAudioProgressView];
[self updateAudioBottomLabel];
if (self.audioPlaybackState == AudioPlaybackState_Playing) {
[self setAudioIconToPause];
} else {
[self setAudioIconToPlay];
}
}
- (CGFloat)audioProgressSeconds
{
return [self.viewItem audioProgressSeconds];
}
- (CGFloat)audioDurationSeconds
{
return self.viewItem.audioDurationSeconds;
}
- (AudioPlaybackState)audioPlaybackState
{
return [self.viewItem audioPlaybackState];
}
- (BOOL)isAudioPlaying
{
return self.audioPlaybackState == AudioPlaybackState_Playing;
}
- (void)updateAudioBottomLabel
{
if (self.isAudioPlaying && self.audioProgressSeconds > 0 && self.audioDurationSeconds > 0) {
self.audioBottomLabel.text =
[NSString stringWithFormat:@"%@ / %@",
[OWSFormat formatDurationSeconds:(long)round(self.audioProgressSeconds)],
[OWSFormat formatDurationSeconds:(long)round(self.audioDurationSeconds)]];
} else {
self.audioBottomLabel.text =
[NSString stringWithFormat:@"%@", [OWSFormat formatDurationSeconds:(long)round(self.audioDurationSeconds)]];
}
}
- (void)setAudioIcon:(UIImage *)icon
{
icon = [icon resizedImageToSize:CGSizeMake(self.iconSize, self.iconSize)];
[_audioPlayPauseButton setImage:icon forState:UIControlStateNormal];
[_audioPlayPauseButton setImage:icon forState:UIControlStateDisabled];
}
- (void)setAudioIconToPlay
{
[self setAudioIcon:[UIImage imageNamed:@"CirclePlay"]];
}
- (void)setAudioIconToPause
{
[self setAudioIcon:[UIImage imageNamed:@"CirclePause"]];
}
- (void)updateAudioProgressView
{
[self.audioProgressView
setProgress:(self.audioDurationSeconds > 0 ? self.audioProgressSeconds / self.audioDurationSeconds : 0.f)];
UIColor *progressColor = [self.conversationStyle bubbleSecondaryTextColorWithIsIncoming:self.isIncoming];
self.audioProgressView.horizontalBarColor = progressColor;
self.audioProgressView.progressColor = progressColor;
}
- (void)replaceIconWithDownloadProgressIfNecessary:(UIView *)iconView
{
if (!self.viewItem.attachmentPointer) {
return;
}
switch (self.viewItem.attachmentPointer.state) {
case TSAttachmentPointerStateFailed:
// We don't need to handle the "tap to retry" state here,
// only download progress.
return;
case TSAttachmentPointerStateEnqueued:
case TSAttachmentPointerStateDownloading:
break;
}
switch (self.viewItem.attachmentPointer.pointerType) {
case TSAttachmentPointerTypeRestoring:
// TODO: Show "restoring" indicator and possibly progress.
return;
case TSAttachmentPointerTypeUnknown:
case TSAttachmentPointerTypeIncoming:
break;
}
NSString *_Nullable uniqueId = self.viewItem.attachmentPointer.uniqueId;
if (uniqueId.length < 1) {
OWSFailDebug(@"Missing uniqueId.");
return;
}
CGFloat downloadViewSize = self.iconSize;
MediaDownloadView *downloadView =
[[MediaDownloadView alloc] initWithAttachmentId:uniqueId radius:downloadViewSize * 0.5f];
iconView.layer.opacity = 0.01f;
[self addSubview:downloadView];
[downloadView autoSetDimensionsToSize:CGSizeMake(downloadViewSize, downloadViewSize)];
[downloadView autoAlignAxis:ALAxisHorizontal toSameAxisOfView:iconView];
[downloadView autoAlignAxis:ALAxisVertical toSameAxisOfView:iconView];
}
#pragma mark -
- (CGFloat)hMargin
{
return 0.f;
}
- (CGFloat)hSpacing
{
return 8.f;
}
+ (CGFloat)vMargin
{
return 0.f;
}
- (CGFloat)vMargin
{
return [OWSAudioMessageView vMargin];
}
+ (CGFloat)bubbleHeight
{
CGFloat iconHeight = self.iconSize;
CGFloat labelsHeight = ([OWSAudioMessageView labelFont].lineHeight * 2 +
[OWSAudioMessageView audioProgressViewHeight] + [OWSAudioMessageView labelVSpacing] * 2);
CGFloat contentHeight = MAX(iconHeight, labelsHeight);
return contentHeight + self.vMargin * 2;
}
- (CGFloat)bubbleHeight
{
return [OWSAudioMessageView bubbleHeight];
}
+ (CGFloat)iconSize
{
return 72.f;
}
- (CGFloat)iconSize
{
return [OWSAudioMessageView iconSize];
}
- (BOOL)isVoiceMessage
{
return self.attachment.isVoiceMessage;
}
- (void)createContents
{
self.axis = UILayoutConstraintAxisHorizontal;
self.alignment = UIStackViewAlignmentCenter;
self.spacing = self.hSpacing;
self.layoutMarginsRelativeArrangement = YES;
self.layoutMargins = UIEdgeInsetsMake(self.vMargin, 0, self.vMargin, 0);
_audioPlayPauseButton = [UIButton buttonWithType:UIButtonTypeCustom];
self.audioPlayPauseButton.enabled = NO;
[self addArrangedSubview:self.audioPlayPauseButton];
self.audioPlayPauseButton.imageView.contentMode = UIViewContentModeCenter;
[self.audioPlayPauseButton autoSetDimension:ALDimensionWidth toSize:56.f];
[self.audioPlayPauseButton autoSetDimension:ALDimensionHeight toSize:56.f];
self.audioPlayPauseButton.imageView.clipsToBounds = NO;
self.audioPlayPauseButton.clipsToBounds = NO;
self.clipsToBounds = NO;
[self replaceIconWithDownloadProgressIfNecessary:self.audioPlayPauseButton];
NSString *_Nullable filename = self.attachment.sourceFilename;
if (filename.length < 1) {
filename = [self.attachmentStream.originalFilePath lastPathComponent];
}
NSString *topText = [[filename stringByDeletingPathExtension] ows_stripped];
if (topText.length < 1) {
topText = [MIMETypeUtil fileExtensionForMIMEType:self.attachment.contentType].localizedUppercaseString;
}
if (topText.length < 1) {
topText = NSLocalizedString(@"GENERIC_ATTACHMENT_LABEL", @"A label for generic attachments.");
}
if (self.isVoiceMessage) {
topText = nil;
}
UILabel *topLabel = [UILabel new];
topLabel.text = topText;
topLabel.textColor = [self.conversationStyle bubbleTextColorWithIsIncoming:self.isIncoming];
topLabel.lineBreakMode = NSLineBreakByTruncatingMiddle;
topLabel.font = [OWSAudioMessageView labelFont];
AudioProgressView *audioProgressView = [AudioProgressView new];
self.audioProgressView = audioProgressView;
[self updateAudioProgressView];
[audioProgressView autoSetDimension:ALDimensionHeight toSize:[OWSAudioMessageView audioProgressViewHeight]];
UILabel *bottomLabel = [UILabel new];
self.audioBottomLabel = bottomLabel;
[self updateAudioBottomLabel];
bottomLabel.textColor = [self.conversationStyle bubbleSecondaryTextColorWithIsIncoming:self.isIncoming];
bottomLabel.lineBreakMode = NSLineBreakByTruncatingMiddle;
bottomLabel.font = [OWSAudioMessageView labelFont];
UIStackView *labelsView = [UIStackView new];
labelsView.axis = UILayoutConstraintAxisVertical;
labelsView.spacing = [OWSAudioMessageView labelVSpacing];
[labelsView addArrangedSubview:topLabel];
[labelsView addArrangedSubview:audioProgressView];
[labelsView addArrangedSubview:bottomLabel];
// Ensure the "audio progress" and "play button" are v-center-aligned using a container.
UIView *labelsContainerView = [UIView containerView];
[self addArrangedSubview:labelsContainerView];
[labelsContainerView addSubview:labelsView];
[labelsView autoPinWidthToSuperview];
[labelsView autoPinEdgeToSuperviewMargin:ALEdgeTop relation:NSLayoutRelationGreaterThanOrEqual];
[labelsView autoPinEdgeToSuperviewMargin:ALEdgeBottom relation:NSLayoutRelationGreaterThanOrEqual];
[audioProgressView autoAlignAxis:ALAxisHorizontal toSameAxisOfView:self.audioPlayPauseButton];
[self updateContents];
}
+ (CGFloat)audioProgressViewHeight
{
return 12.f;
}
+ (UIFont *)labelFont
{
return [UIFont ows_dynamicTypeCaption2Font];
}
+ (CGFloat)labelVSpacing
{
return 2.f;
}
@end
NS_ASSUME_NONNULL_END

View File

@ -36,6 +36,7 @@ typedef NS_ENUM(NSUInteger, OWSMessageGestureLocation) {
imageView:(UIView *)imageView; imageView:(UIView *)imageView;
- (void)didTapAudioViewItem:(id<ConversationViewItem>)viewItem attachmentStream:(TSAttachmentStream *)attachmentStream; - (void)didTapAudioViewItem:(id<ConversationViewItem>)viewItem attachmentStream:(TSAttachmentStream *)attachmentStream;
- (void)didPanAudioViewItemToCurrentTime:(NSTimeInterval)currentTime;
- (void)didTapTruncatedTextMessage:(id<ConversationViewItem>)conversationItem; - (void)didTapTruncatedTextMessage:(id<ConversationViewItem>)conversationItem;
@ -102,6 +103,7 @@ typedef NS_ENUM(NSUInteger, OWSMessageGestureLocation) {
- (void)addTapGestureHandler; - (void)addTapGestureHandler;
- (void)handleTapGesture:(UITapGestureRecognizer *)sender; - (void)handleTapGesture:(UITapGestureRecognizer *)sender;
- (void)handlePanGesture:(UIPanGestureRecognizer *)sender;
@end @end

View File

@ -5,7 +5,6 @@
#import "OWSMessageBubbleView.h" #import "OWSMessageBubbleView.h"
#import "AttachmentUploadView.h" #import "AttachmentUploadView.h"
#import "ConversationViewItem.h" #import "ConversationViewItem.h"
#import "OWSAudioMessageView.h"
#import "OWSBubbleShapeView.h" #import "OWSBubbleShapeView.h"
#import "OWSBubbleView.h" #import "OWSBubbleView.h"
#import "OWSContactShareButtonsView.h" #import "OWSContactShareButtonsView.h"
@ -840,13 +839,12 @@ NS_ASSUME_NONNULL_BEGIN
OWSAssertDebug(attachment); OWSAssertDebug(attachment);
OWSAssertDebug([attachment isAudio]); OWSAssertDebug([attachment isAudio]);
OWSAudioMessageView *audioMessageView = [[OWSAudioMessageView alloc] initWithAttachment:attachment LKVoiceMessageView *voiceMessageView = [[LKVoiceMessageView alloc] initWithVoiceMessage:attachment isOutgoing:self.isOutgoing];
isIncoming:self.isIncoming [voiceMessageView setDuration:(int)self.viewItem.audioDurationSeconds];
viewItem:self.viewItem [voiceMessageView setProgress:self.viewItem.audioProgressSeconds / self.viewItem.audioDurationSeconds];
conversationStyle:self.conversationStyle]; [voiceMessageView initialize];
self.viewItem.lastAudioMessageView = audioMessageView;
[audioMessageView createContents]; self.viewItem.lastAudioMessageView = voiceMessageView;
[self addProgressViewsIfNecessary:audioMessageView shouldShowDownloadProgress:NO];
self.loadCellContentBlock = ^{ self.loadCellContentBlock = ^{
// Do nothing. // Do nothing.
@ -855,7 +853,7 @@ NS_ASSUME_NONNULL_BEGIN
// Do nothing. // Do nothing.
}; };
return audioMessageView; return voiceMessageView;
} }
- (UIView *)loadViewForGenericAttachment - (UIView *)loadViewForGenericAttachment
@ -1068,7 +1066,7 @@ NS_ASSUME_NONNULL_BEGIN
return nil; return nil;
} }
case OWSMessageCellType_Audio: case OWSMessageCellType_Audio:
result = CGSizeMake(maxMessageWidth, OWSAudioMessageView.bubbleHeight); result = CGSizeMake(maxMessageWidth, LKVoiceMessageView.contentHeight);
break; break;
case OWSMessageCellType_GenericAttachment: { case OWSMessageCellType_GenericAttachment: {
TSAttachment *attachment = (self.viewItem.attachmentStream ?: self.viewItem.attachmentPointer); TSAttachment *attachment = (self.viewItem.attachmentStream ?: self.viewItem.attachmentPointer);
@ -1534,6 +1532,22 @@ NS_ASSUME_NONNULL_BEGIN
} }
} }
- (void)handlePanGesture:(UIPanGestureRecognizer *)sender
{
switch (self.cellType) {
case OWSMessageCellType_Audio: {
LKVoiceMessageView *voiceMessageView = self.viewItem.lastAudioMessageView;
NSTimeInterval currentTime = [voiceMessageView getCurrentTime:sender];
[self.viewItem setAudioProgress:((CGFloat)currentTime) duration:self.viewItem.audioDurationSeconds];
CGFloat progress = self.viewItem.audioProgressSeconds / self.viewItem.audioDurationSeconds;
[voiceMessageView setProgress:progress];
[self.delegate didPanAudioViewItemToCurrentTime:currentTime];
return;
}
default: return;
}
}
- (OWSMessageGestureLocation)gestureLocationForLocation:(CGPoint)locationInMessageBubble - (OWSMessageGestureLocation)gestureLocationForLocation:(CGPoint)locationInMessageBubble
{ {
if (self.quotedMessageView) { if (self.quotedMessageView) {

View File

@ -11,7 +11,7 @@
NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN
@interface OWSMessageCell () @interface OWSMessageCell () <UIGestureRecognizerDelegate>
// The nullable properties are created as needed. // The nullable properties are created as needed.
// The non-nullable properties are so frequently used that it's easier // The non-nullable properties are so frequently used that it's easier
@ -78,6 +78,11 @@ NS_ASSUME_NONNULL_BEGIN
UILongPressGestureRecognizer *longPress = UILongPressGestureRecognizer *longPress =
[[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPressGesture:)]; [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPressGesture:)];
[self.contentView addGestureRecognizer:longPress]; [self.contentView addGestureRecognizer:longPress];
UIPanGestureRecognizer *pan =
[[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePanGesture:)];
pan.delegate = self;
[self.contentView addGestureRecognizer:pan];
} }
- (void)dealloc - (void)dealloc
@ -488,6 +493,22 @@ NS_ASSUME_NONNULL_BEGIN
} }
} }
- (void)handlePanGesture:(UIPanGestureRecognizer *)sender
{
[self.messageBubbleView handlePanGesture:sender];
}
-(BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
LKVoiceMessageView *voiceMessageView = self.viewItem.lastAudioMessageView;
if (![gestureRecognizer isKindOfClass:UIPanGestureRecognizer.class] || voiceMessageView == nil) { return NO; }
UIPanGestureRecognizer *panGestureRecognizer = (UIPanGestureRecognizer *)gestureRecognizer;
CGPoint location = [panGestureRecognizer locationInView:voiceMessageView];
if (!CGRectContainsPoint(voiceMessageView.bounds, location)) { return NO; }
CGPoint velocity = [panGestureRecognizer velocityInView:voiceMessageView];
return fabs(velocity.x) > fabs(velocity.y);
}
- (BOOL)isGestureInCellHeader:(UIGestureRecognizer *)sender - (BOOL)isGestureInCellHeader:(UIGestureRecognizer *)sender
{ {
OWSAssertDebug(self.viewItem); OWSAssertDebug(self.viewItem);

View File

@ -2430,7 +2430,7 @@ typedef enum : NSUInteger {
NSFileManager *fileManager = [NSFileManager defaultManager]; NSFileManager *fileManager = [NSFileManager defaultManager];
if (![fileManager fileExistsAtPath:attachmentStream.originalFilePath]) { if (![fileManager fileExistsAtPath:attachmentStream.originalFilePath]) {
OWSFailDebug(@"Missing video file: %@", attachmentStream.originalMediaURL); OWSFailDebug(@"Missing audio file: %@", attachmentStream.originalMediaURL);
} }
[self dismissKeyBoard]; [self dismissKeyBoard];
@ -2452,6 +2452,12 @@ typedef enum : NSUInteger {
// Associate the player with this media adapter. // Associate the player with this media adapter.
self.audioAttachmentPlayer.owner = viewItem; self.audioAttachmentPlayer.owner = viewItem;
[self.audioAttachmentPlayer play]; [self.audioAttachmentPlayer play];
[self.audioAttachmentPlayer setCurrentTime:viewItem.audioProgressSeconds];
}
- (void)didPanAudioViewItemToCurrentTime:(NSTimeInterval)currentTime
{
[self.audioAttachmentPlayer setCurrentTime:currentTime];
} }
- (void)didTapTruncatedTextMessage:(id<ConversationViewItem>)conversationItem - (void)didTapTruncatedTextMessage:(id<ConversationViewItem>)conversationItem
@ -5398,13 +5404,13 @@ typedef enum : NSUInteger {
} }
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
__block TSInteraction *targetInteraction; __block TSInteraction *targetInteraction;
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { [LKStorage readWithBlock:^(YapDatabaseReadTransaction *transaction) {
[self.thread enumerateInteractionsWithTransaction:transaction usingBlock:^(TSInteraction *interaction, YapDatabaseReadTransaction *t) { [self.thread enumerateInteractionsWithTransaction:transaction usingBlock:^(TSInteraction *interaction, YapDatabaseReadTransaction *t) {
if (interaction.timestampForUI == timestamp.unsignedLongLongValue) { if (interaction.timestampForUI == timestamp.unsignedLongLongValue) {
targetInteraction = interaction; targetInteraction = interaction;
} }
}]; }];
} error:nil]; }];
if (targetInteraction == nil || targetInteraction.interactionType != OWSInteractionType_OutgoingMessage) { return; } if (targetInteraction == nil || targetInteraction.interactionType != OWSInteractionType_OutgoingMessage) { return; }
NSString *hexEncodedPublicKey = targetInteraction.thread.contactIdentifier; NSString *hexEncodedPublicKey = targetInteraction.thread.contactIdentifier;
if (hexEncodedPublicKey == nil) { return; } if (hexEncodedPublicKey == nil) { return; }

View File

@ -24,7 +24,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType);
@class ContactShareViewModel; @class ContactShareViewModel;
@class ConversationViewCell; @class ConversationViewCell;
@class DisplayableText; @class DisplayableText;
@class OWSAudioMessageView; @class LKVoiceMessageView;
@class OWSLinkPreview; @class OWSLinkPreview;
@class OWSQuotedReplyModel; @class OWSQuotedReplyModel;
@class OWSUnreadIndicator; @class OWSUnreadIndicator;
@ -99,7 +99,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType);
#pragma mark - Audio Playback #pragma mark - Audio Playback
@property (nonatomic, weak) OWSAudioMessageView *lastAudioMessageView; @property (nonatomic, weak) LKVoiceMessageView *lastAudioMessageView;
@property (nonatomic, readonly) CGFloat audioDurationSeconds; @property (nonatomic, readonly) CGFloat audioDurationSeconds;
@property (nonatomic, readonly) CGFloat audioProgressSeconds; @property (nonatomic, readonly) CGFloat audioProgressSeconds;

View File

@ -3,7 +3,6 @@
// //
#import "ConversationViewItem.h" #import "ConversationViewItem.h"
#import "OWSAudioMessageView.h"
#import "OWSContactOffersCell.h" #import "OWSContactOffersCell.h"
#import "OWSMessageCell.h" #import "OWSMessageCell.h"
#import "OWSMessageHeaderView.h" #import "OWSMessageHeaderView.h"
@ -476,7 +475,8 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
{ {
_audioPlaybackState = audioPlaybackState; _audioPlaybackState = audioPlaybackState;
[self.lastAudioMessageView updateContents]; BOOL isPlaying = (audioPlaybackState == AudioPlaybackState_Playing);
[self.lastAudioMessageView setIsPlaying:isPlaying];
} }
- (void)setAudioProgress:(CGFloat)progress duration:(CGFloat)duration - (void)setAudioProgress:(CGFloat)progress duration:(CGFloat)duration
@ -485,7 +485,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
self.audioProgressSeconds = progress; self.audioProgressSeconds = progress;
[self.lastAudioMessageView updateContents]; [self.lastAudioMessageView setProgress:progress / duration];
} }
#pragma mark - Displayable Text #pragma mark - Displayable Text

View File

@ -669,6 +669,10 @@ class MessageDetailViewController: OWSViewController, MediaGalleryDataSourceDele
audioAttachmentPlayer.play() audioAttachmentPlayer.play()
} }
func didPanAudioViewItem(toCurrentTime currentTime: TimeInterval) {
// TODO: Implement
}
func didTapTruncatedTextMessage(_ conversationItem: ConversationViewItem) { func didTapTruncatedTextMessage(_ conversationItem: ConversationViewItem) {
guard let navigationController = self.navigationController else { guard let navigationController = self.navigationController else {
owsFailDebug("navigationController was unexpectedly nil") owsFailDebug("navigationController was unexpectedly nil")

View File

@ -64,8 +64,8 @@ public final class Values : NSObject {
@objc public static let onboardingButtonBottomOffset = isIPhone5OrSmaller ? CGFloat(52) : CGFloat(72) @objc public static let onboardingButtonBottomOffset = isIPhone5OrSmaller ? CGFloat(52) : CGFloat(72)
// MARK: - Animation Values // MARK: - Animation Values
@objc public static let fakeChatStartDelay: TimeInterval = 1.5 @objc public static let fakeChatStartDelay: TimeInterval = 1
@objc public static let fakeChatAnimationDuration: TimeInterval = 0.4 @objc public static let fakeChatAnimationDuration: TimeInterval = 0.4
@objc public static let fakeChatDelay: TimeInterval = 2 @objc public static let fakeChatDelay: TimeInterval = 1.5
@objc public static let fakeChatMessagePopAnimationStartScale: CGFloat = 0.6 @objc public static let fakeChatMessagePopAnimationStartScale: CGFloat = 0.6
} }

View File

@ -1157,43 +1157,13 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
OWSLogVerbose(@"downloading profile avatar: %@", userProfile.uniqueId); OWSLogVerbose(@"downloading profile avatar: %@", userProfile.uniqueId);
NSString *tempDirectory = OWSTemporaryDirectory();
NSString *tempFilePath = [tempDirectory stringByAppendingPathComponent:fileName];
NSString *profilePictureURL = userProfile.avatarUrlPath; NSString *profilePictureURL = userProfile.avatarUrlPath;
NSError *serializationError; [[LKFileServerAPI downloadAttachmentFrom:profilePictureURL].then(^(NSData *data) {
NSMutableURLRequest *request =
[self.avatarHTTPManager.requestSerializer requestWithMethod:@"GET"
URLString:profilePictureURL
parameters:nil
error:&serializationError];
if (serializationError) {
OWSFailDebug(@"serializationError: %@", serializationError);
return;
}
NSURLSession* session = [NSURLSession sharedSession];
NSURLSessionTask* downloadTask = [session downloadTaskWithRequest:request completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
@synchronized(self.currentAvatarDownloads) @synchronized(self.currentAvatarDownloads)
{ {
[self.currentAvatarDownloads removeObject:userProfile.recipientId]; [self.currentAvatarDownloads removeObject:userProfile.recipientId];
} }
NSData *_Nullable encryptedData = data;
if (error) {
OWSLogError(@"Dowload failed: %@", error);
return;
}
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *tempFileUrl = [NSURL fileURLWithPath:tempFilePath];
NSError *moveError;
if (![fileManager moveItemAtURL:location toURL:tempFileUrl error:&moveError]) {
OWSLogError(@"MoveItemAtURL for avatar failed: %@", moveError);
return;
}
NSData *_Nullable encryptedData = (error ? nil : [NSData dataWithContentsOfFile:tempFilePath]);
NSData *_Nullable decryptedData = [self decryptProfileData:encryptedData profileKey:profileKeyAtStart]; NSData *_Nullable decryptedData = [self decryptProfileData:encryptedData profileKey:profileKeyAtStart];
UIImage *_Nullable image = nil; UIImage *_Nullable image = nil;
if (decryptedData) { if (decryptedData) {
@ -1213,19 +1183,12 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
if (latestUserProfile.avatarUrlPath.length > 0) { if (latestUserProfile.avatarUrlPath.length > 0) {
[self downloadAvatarForUserProfile:latestUserProfile]; [self downloadAvatarForUserProfile:latestUserProfile];
} }
} else if (error) {
if ([response isKindOfClass:NSHTTPURLResponse.class]
&& ((NSHTTPURLResponse *)response).statusCode == 403) {
OWSLogInfo(@"no avatar for: %@", userProfile.recipientId);
} else {
OWSLogError(@"avatar download for %@ failed with error: %@", userProfile.recipientId, error);
}
} else if (!encryptedData) { } else if (!encryptedData) {
OWSLogError(@"avatar encrypted data for %@ could not be read.", userProfile.recipientId); OWSLogError(@"avatar encrypted data for %@ could not be read.", userProfile.recipientId);
} else if (!decryptedData) { } else if (!decryptedData) {
OWSLogError(@"avatar data for %@ could not be decrypted.", userProfile.recipientId); OWSLogError(@"avatar data for %@ could not be decrypted.", userProfile.recipientId);
} else if (!image) { } else if (!image) {
OWSLogError(@"avatar image for %@ could not be loaded with error: %@", userProfile.recipientId, error); OWSLogError(@"avatar image for %@ could not be loaded.", userProfile.recipientId);
} else { } else {
[self updateProfileAvatarCache:image filename:fileName]; [self updateProfileAvatarCache:image filename:fileName];
@ -1248,9 +1211,7 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
OWSAssertDebug(backgroundTask); OWSAssertDebug(backgroundTask);
backgroundTask = nil; backgroundTask = nil;
}]; }) retainUntilComplete];
[downloadTask resume];
}); });
} }

View File

@ -46,6 +46,7 @@ typedef NS_ENUM(NSUInteger, OWSAudioBehavior) {
delegate:(id<OWSAudioPlayerDelegate>)delegate; delegate:(id<OWSAudioPlayerDelegate>)delegate;
- (void)play; - (void)play;
- (void)setCurrentTime:(NSTimeInterval)currentTime;
- (void)pause; - (void)pause;
- (void)stop; - (void)stop;
- (void)togglePlayState; - (void)togglePlayState;

View File

@ -156,6 +156,11 @@ NS_ASSUME_NONNULL_BEGIN
[DeviceSleepManager.sharedInstance addBlockWithBlockObject:self]; [DeviceSleepManager.sharedInstance addBlockWithBlockObject:self];
} }
- (void)setCurrentTime:(NSTimeInterval)currentTime
{
[self.audioPlayer setCurrentTime:currentTime];
}
- (void)pause - (void)pause
{ {
OWSAssertIsOnMainThread(); OWSAssertIsOnMainThread();

View File

@ -252,7 +252,6 @@ message DataMessage {
INFO = 1; // groupPublicKey, name, senderKeys, members, admins INFO = 1; // groupPublicKey, name, senderKeys, members, admins
SENDER_KEY_REQUEST = 2; // groupPublicKey SENDER_KEY_REQUEST = 2; // groupPublicKey
SENDER_KEY = 3; // groupPublicKey, senderKeys SENDER_KEY = 3; // groupPublicKey, senderKeys
} }
message SenderKey { message SenderKey {

View File

@ -8,6 +8,7 @@ public class DotNetAPI : NSObject {
// MARK: Settings // MARK: Settings
private static let attachmentType = "network.loki" private static let attachmentType = "network.loki"
private static let maxRetryCount: UInt = 4
// MARK: Error // MARK: Error
@objc(LKDotNetAPIError) @objc(LKDotNetAPIError)
@ -101,6 +102,41 @@ public class DotNetAPI : NSObject {
} }
// MARK: Public API // MARK: Public API
@objc(downloadAttachmentFrom:)
public static func objc_downloadAttachment(from url: String) -> AnyPromise {
return AnyPromise.from(downloadAttachment(from: url))
}
public static func downloadAttachment(from url: String) -> Promise<Data> {
var error: NSError?
var host = "https://\(URL(string: url)!.host!)"
let sanitizedURL: String
if FileServerAPI.fileStorageBucketURL.contains(host) {
sanitizedURL = url.replacingOccurrences(of: FileServerAPI.fileStorageBucketURL, with: "\(FileServerAPI.server)/loki/v1")
host = FileServerAPI.server
} else {
sanitizedURL = url.replacingOccurrences(of: host, with: "\(host)/loki/v1")
}
let request = AFHTTPRequestSerializer().request(withMethod: "GET", urlString: sanitizedURL, parameters: nil, error: &error)
if let error = error {
print("[Loki] Couldn't download attachment due to error: \(error).")
return Promise(error: error)
}
let serverPublicKeyPromise = FileServerAPI.server.contains(host) ? Promise.value(FileServerAPI.fileServerPublicKey)
: PublicChatAPI.getOpenGroupServerPublicKey(for: host)
return attempt(maxRetryCount: maxRetryCount, recoveringOn: SnodeAPI.workQueue) {
serverPublicKeyPromise.then2 { serverPublicKey in
return OnionRequestAPI.sendOnionRequest(request, to: host, using: serverPublicKey, isJSONRequired: false).map2 { json in
guard let body = json["body"] as? JSON, let data = body["data"] as? [UInt8] else {
print("[Loki] Couldn't parse attachment from: \(json).")
throw DotNetAPIError.parsingFailed
}
return Data(data)
}
}
}
}
@objc(uploadAttachment:withID:toServer:) @objc(uploadAttachment:withID:toServer:)
public static func objc_uploadAttachment(_ attachment: TSAttachmentStream, with attachmentID: String, to server: String) -> AnyPromise { public static func objc_uploadAttachment(_ attachment: TSAttachmentStream, with attachmentID: String, to server: String) -> AnyPromise {
return AnyPromise.from(uploadAttachment(attachment, with: attachmentID, to: server)) return AnyPromise.from(uploadAttachment(attachment, with: attachmentID, to: server))

View File

@ -11,13 +11,15 @@ public final class FileServerAPI : DotNetAPI {
public static let maxFileSize = 10_000_000 // 10 MB public static let maxFileSize = 10_000_000 // 10 MB
/// The file server has a file size limit of `maxFileSize`, which the Service Nodes try to enforce as well. However, the limit applied by the Service Nodes /// The file server has a file size limit of `maxFileSize`, which the Service Nodes try to enforce as well. However, the limit applied by the Service Nodes
/// is on the **HTTP request** and not the file size. Because of onion request encryption, a file that's about 4 MB will result in a request that's about 18 MB. /// is on the **HTTP request** and not the actual file size. Because the file server expects the file data to be base 64 encoded, the size of the HTTP
/// On average the multiplier appears to be about 6, so when checking whether the file will exceed the file size limit when uploading a file we just divide /// request for a given file will be at least `ceil(n / 3) * 4` bytes, where n is the file size in bytes. This is the minimum size because there might also
/// the size of the file by this number. The alternative would be to actually check the size of the HTTP request but that's only possible after proof of work /// be other parameters in the request. On average the multiplier appears to be about 1.5, so when checking whether the file will exceed the file size limit when
/// has been calculated and the onion request encryption has happened, which takes several seconds. /// uploading a file we just divide the size of the file by this number. The alternative would be to actually check the size of the HTTP request but that's only
public static let fileSizeORMultiplier: Double = 6 /// possible after proof of work has been calculated and the onion request encryption has happened, which takes several seconds.
public static let fileSizeORMultiplier: Double = 1.5
@objc public static let server = "https://file.getsession.org" @objc public static let server = "https://file.getsession.org"
@objc public static let fileStorageBucketURL = "https://file-static.lokinet.org"
// MARK: Storage // MARK: Storage
override internal class var authTokenCollection: String { return "LokiStorageAuthTokenCollection" } override internal class var authTokenCollection: String { return "LokiStorageAuthTokenCollection" }

View File

@ -3,6 +3,15 @@ import PromiseKit
extension OnionRequestAPI { extension OnionRequestAPI {
internal static func encode(ciphertext: Data, json: JSON) throws -> Data {
// The encoding of V2 onion requests looks like: | 4 bytes: size N of ciphertext | N bytes: ciphertext | json as utf8 |
guard JSONSerialization.isValidJSONObject(json) else { throw HTTP.Error.invalidJSON }
let jsonAsData = try JSONSerialization.data(withJSONObject: json, options: [ .fragmentsAllowed ])
let ciphertextSize = Int32(ciphertext.count).littleEndian
let ciphertextSizeAsData = withUnsafePointer(to: ciphertextSize) { Data(bytes: $0, count: MemoryLayout<Int32>.size) }
return ciphertextSizeAsData + ciphertext + jsonAsData
}
/// Encrypts `payload` for `destination` and returns the result. Use this to build the core of an onion request. /// Encrypts `payload` for `destination` and returns the result. Use this to build the core of an onion request.
internal static func encrypt(_ payload: JSON, for destination: Destination) -> Promise<EncryptionResult> { internal static func encrypt(_ payload: JSON, for destination: Destination) -> Promise<EncryptionResult> {
let (promise, seal) = Promise<EncryptionResult>.pending() let (promise, seal) = Promise<EncryptionResult>.pending()
@ -14,10 +23,7 @@ extension OnionRequestAPI {
case .snode(let snode): case .snode(let snode):
guard let snodeX25519PublicKey = snode.publicKeySet?.x25519Key else { return seal.reject(Error.snodePublicKeySetMissing) } guard let snodeX25519PublicKey = snode.publicKeySet?.x25519Key else { return seal.reject(Error.snodePublicKeySetMissing) }
let payloadAsData = try JSONSerialization.data(withJSONObject: payload, options: [ .fragmentsAllowed ]) let payloadAsData = try JSONSerialization.data(withJSONObject: payload, options: [ .fragmentsAllowed ])
let payloadAsString = String(data: payloadAsData, encoding: .utf8)! // Snodes only accept this as a string let plaintext = try encode(ciphertext: payloadAsData, json: [ "headers" : "" ])
let wrapper: JSON = [ "body" : payloadAsString, "headers" : "" ]
guard JSONSerialization.isValidJSONObject(wrapper) else { return seal.reject(HTTP.Error.invalidJSON) }
let plaintext = try JSONSerialization.data(withJSONObject: wrapper, options: [ .fragmentsAllowed ])
let result = try EncryptionUtilities.encrypt(plaintext, using: snodeX25519PublicKey) let result = try EncryptionUtilities.encrypt(plaintext, using: snodeX25519PublicKey)
seal.fulfill(result) seal.fulfill(result)
case .server(_, let serverX25519PublicKey): case .server(_, let serverX25519PublicKey):
@ -42,9 +48,8 @@ extension OnionRequestAPI {
guard let snodeED25519PublicKey = snode.publicKeySet?.ed25519Key else { return seal.reject(Error.snodePublicKeySetMissing) } guard let snodeED25519PublicKey = snode.publicKeySet?.ed25519Key else { return seal.reject(Error.snodePublicKeySetMissing) }
parameters = [ "destination" : snodeED25519PublicKey ] parameters = [ "destination" : snodeED25519PublicKey ]
case .server(let host, _): case .server(let host, _):
parameters = [ "host" : host, "target" : "/loki/v1/lsrpc", "method" : "POST" ] parameters = [ "host" : host, "target" : "/loki/v2/lsrpc", "method" : "POST" ]
} }
parameters["ciphertext"] = previousEncryptionResult.ciphertext.base64EncodedString()
parameters["ephemeral_key"] = previousEncryptionResult.ephemeralPublicKey.toHexString() parameters["ephemeral_key"] = previousEncryptionResult.ephemeralPublicKey.toHexString()
let x25519PublicKey: String let x25519PublicKey: String
switch lhs { switch lhs {
@ -55,8 +60,7 @@ extension OnionRequestAPI {
x25519PublicKey = serverX25519PublicKey x25519PublicKey = serverX25519PublicKey
} }
do { do {
guard JSONSerialization.isValidJSONObject(parameters) else { return seal.reject(HTTP.Error.invalidJSON) } let plaintext = try encode(ciphertext: previousEncryptionResult.ciphertext, json: parameters)
let plaintext = try JSONSerialization.data(withJSONObject: parameters, options: [ .fragmentsAllowed ])
let result = try EncryptionUtilities.encrypt(plaintext, using: x25519PublicKey) let result = try EncryptionUtilities.encrypt(plaintext, using: x25519PublicKey)
seal.fulfill(result) seal.fulfill(result)
} catch (let error) { } catch (let error) {

View File

@ -3,16 +3,23 @@ import PromiseKit
/// See the "Onion Requests" section of [The Session Whitepaper](https://arxiv.org/pdf/2002.04609.pdf) for more information. /// See the "Onion Requests" section of [The Session Whitepaper](https://arxiv.org/pdf/2002.04609.pdf) for more information.
public enum OnionRequestAPI { public enum OnionRequestAPI {
private static var pathFailureCount: [Path:UInt] = [:]
private static var snodeFailureCount: [Snode:UInt] = [:]
public static var guardSnodes: Set<Snode> = [] public static var guardSnodes: Set<Snode> = []
public static var paths: [Path] = [] // Not a set to ensure we consistently show the same path to the user public static var paths: [Path] = [] // Not a set to ensure we consistently show the same path to the user
// MARK: Settings // MARK: Settings
/// The number of snodes (including the guard snode) in a path. /// The number of snodes (including the guard snode) in a path.
private static let pathSize: UInt = 3 private static let pathSize: UInt = 3
/// The number of times a path can fail before it's replaced.
private static let pathFailureThreshold: UInt = 3
/// The number of times a snode can fail before it's replaced.
private static let snodeFailureThreshold: UInt = 3
/// The number of paths to maintain.
public static let targetPathCount: UInt = 2
public static let pathCount: UInt = 2 /// The number of guard snodes required to maintain `targetPathCount` paths.
private static var targetGuardSnodeCount: UInt { return targetPathCount } // One per path
private static var guardSnodeCount: UInt { return pathCount } // One per path
// MARK: Destination // MARK: Destination
internal enum Destination { internal enum Destination {
@ -56,7 +63,7 @@ public enum OnionRequestAPI {
let timeout: TimeInterval = 3 // Use a shorter timeout for testing let timeout: TimeInterval = 3 // Use a shorter timeout for testing
HTTP.execute(.get, url, timeout: timeout).done2 { rawResponse in HTTP.execute(.get, url, timeout: timeout).done2 { rawResponse in
guard let json = rawResponse as? JSON, let version = json["version"] as? String else { return seal.reject(Error.missingSnodeVersion) } guard let json = rawResponse as? JSON, let version = json["version"] as? String else { return seal.reject(Error.missingSnodeVersion) }
if version >= "2.0.0" { if version >= "2.0.7" {
seal.fulfill(()) seal.fulfill(())
} else { } else {
print("[Loki] [Onion Request API] Unsupported snode version: \(version).") print("[Loki] [Onion Request API] Unsupported snode version: \(version).")
@ -69,16 +76,17 @@ public enum OnionRequestAPI {
return promise return promise
} }
/// Finds `guardSnodeCount` guard snodes to use for path building. The returned promise errors out with `Error.insufficientSnodes` /// Finds `targetGuardSnodeCount` guard snodes to use for path building. The returned promise errors out with `Error.insufficientSnodes`
/// if not enough (reliable) snodes are available. /// if not enough (reliable) snodes are available.
private static func getGuardSnodes() -> Promise<Set<Snode>> { private static func getGuardSnodes(reusing reusableGuardSnodes: [Snode]) -> Promise<Set<Snode>> {
if guardSnodes.count >= guardSnodeCount { if guardSnodes.count >= targetGuardSnodeCount {
return Promise<Set<Snode>> { $0.fulfill(guardSnodes) } return Promise<Set<Snode>> { $0.fulfill(guardSnodes) }
} else { } else {
print("[Loki] [Onion Request API] Populating guard snode cache.") print("[Loki] [Onion Request API] Populating guard snode cache.")
return SnodeAPI.getRandomSnode().then2 { _ -> Promise<Set<Snode>> in // Just used to populate the snode pool return SnodeAPI.getRandomSnode().then2 { _ -> Promise<Set<Snode>> in // Just used to populate the snode pool
var unusedSnodes = SnodeAPI.snodePool // Sync on LokiAPI.workQueue var unusedSnodes = SnodeAPI.snodePool.subtracting(reusableGuardSnodes) // Sync on LokiAPI.workQueue
guard unusedSnodes.count >= guardSnodeCount else { throw Error.insufficientSnodes } let reusableGuardSnodeCount = UInt(reusableGuardSnodes.count)
guard unusedSnodes.count >= (targetGuardSnodeCount - reusableGuardSnodeCount) else { throw Error.insufficientSnodes }
func getGuardSnode() -> Promise<Snode> { func getGuardSnode() -> Promise<Snode> {
// randomElement() uses the system's default random generator, which is cryptographically secure // randomElement() uses the system's default random generator, which is cryptographically secure
guard let candidate = unusedSnodes.randomElement() else { return Promise<Snode> { $0.reject(Error.insufficientSnodes) } } guard let candidate = unusedSnodes.randomElement() else { return Promise<Snode> { $0.reject(Error.insufficientSnodes) } }
@ -86,12 +94,12 @@ public enum OnionRequestAPI {
print("[Loki] [Onion Request API] Testing guard snode: \(candidate).") print("[Loki] [Onion Request API] Testing guard snode: \(candidate).")
// Loop until a reliable guard snode is found // Loop until a reliable guard snode is found
return testSnode(candidate).map2 { candidate }.recover(on: DispatchQueue.main) { _ in return testSnode(candidate).map2 { candidate }.recover(on: DispatchQueue.main) { _ in
withDelay(0.25, completionQueue: SnodeAPI.workQueue) { getGuardSnode() } withDelay(0.1, completionQueue: SnodeAPI.workQueue) { getGuardSnode() }
} }
} }
let promises = (0..<guardSnodeCount).map { _ in getGuardSnode() } let promises = (0..<(targetGuardSnodeCount - reusableGuardSnodeCount)).map { _ in getGuardSnode() }
return when(fulfilled: promises).map2 { guardSnodes in return when(fulfilled: promises).map2 { guardSnodes in
let guardSnodesAsSet = Set(guardSnodes) let guardSnodesAsSet = Set(guardSnodes + reusableGuardSnodes)
OnionRequestAPI.guardSnodes = guardSnodesAsSet OnionRequestAPI.guardSnodes = guardSnodesAsSet
return guardSnodesAsSet return guardSnodesAsSet
} }
@ -99,20 +107,22 @@ public enum OnionRequestAPI {
} }
} }
/// Builds and returns `pathCount` paths. The returned promise errors out with `Error.insufficientSnodes` /// Builds and returns `targetPathCount` paths. The returned promise errors out with `Error.insufficientSnodes`
/// if not enough (reliable) snodes are available. /// if not enough (reliable) snodes are available.
private static func buildPaths() -> Promise<[Path]> { private static func buildPaths(reusing reusablePaths: [Path]) -> Promise<[Path]> {
print("[Loki] [Onion Request API] Building onion request paths.") print("[Loki] [Onion Request API] Building onion request paths.")
DispatchQueue.main.async { DispatchQueue.main.async {
NotificationCenter.default.post(name: .buildingPaths, object: nil) NotificationCenter.default.post(name: .buildingPaths, object: nil)
} }
return SnodeAPI.getRandomSnode().then2 { _ -> Promise<[Path]> in // Just used to populate the snode pool return SnodeAPI.getRandomSnode().then2 { _ -> Promise<[Path]> in // Just used to populate the snode pool
return getGuardSnodes().map2 { guardSnodes -> [Path] in let reusableGuardSnodes = reusablePaths.map { $0[0] }
var unusedSnodes = SnodeAPI.snodePool.subtracting(guardSnodes) return getGuardSnodes(reusing: reusableGuardSnodes).map2 { guardSnodes -> [Path] in
let pathSnodeCount = guardSnodeCount * pathSize - guardSnodeCount var unusedSnodes = SnodeAPI.snodePool.subtracting(guardSnodes).subtracting(reusablePaths.flatMap { $0 })
let reusableGuardSnodeCount = UInt(reusableGuardSnodes.count)
let pathSnodeCount = (targetGuardSnodeCount - reusableGuardSnodeCount) * pathSize - (targetGuardSnodeCount - reusableGuardSnodeCount)
guard unusedSnodes.count >= pathSnodeCount else { throw Error.insufficientSnodes } guard unusedSnodes.count >= pathSnodeCount else { throw Error.insufficientSnodes }
// Don't test path snodes as this would reveal the user's IP to them // Don't test path snodes as this would reveal the user's IP to them
return guardSnodes.map { guardSnode in return guardSnodes.subtracting(reusableGuardSnodes).map { guardSnode in
let result = [ guardSnode ] + (0..<(pathSize - 1)).map { _ in let result = [ guardSnode ] + (0..<(pathSize - 1)).map { _ in
// randomElement() uses the system's default random generator, which is cryptographically secure // randomElement() uses the system's default random generator, which is cryptographically secure
let pathSnode = unusedSnodes.randomElement()! // Safe because of the pathSnodeCount check above let pathSnode = unusedSnodes.randomElement()! // Safe because of the pathSnodeCount check above
@ -123,10 +133,10 @@ public enum OnionRequestAPI {
return result return result
} }
}.map2 { paths in }.map2 { paths in
OnionRequestAPI.paths = paths OnionRequestAPI.paths = paths + reusablePaths
try! Storage.writeSync { transaction in try! Storage.writeSync { transaction in
print("[Loki] Persisting onion request paths to database.") print("[Loki] Persisting onion request paths to database.")
OWSPrimaryStorage.shared().setOnionRequestPaths(paths, in: transaction) Storage.setOnionRequestPaths(paths, using: transaction)
} }
DispatchQueue.main.async { DispatchQueue.main.async {
NotificationCenter.default.post(name: .pathsBuilt, object: nil) NotificationCenter.default.post(name: .pathsBuilt, object: nil)
@ -137,32 +147,42 @@ public enum OnionRequestAPI {
} }
/// Returns a `Path` to be used for building an onion request. Builds new paths as needed. /// Returns a `Path` to be used for building an onion request. Builds new paths as needed.
///
/// - Note: Exposed for testing purposes.
private static func getPath(excluding snode: Snode?) -> Promise<Path> { private static func getPath(excluding snode: Snode?) -> Promise<Path> {
guard pathSize >= 1 else { preconditionFailure("Can't build path of size zero.") } guard pathSize >= 1 else { preconditionFailure("Can't build path of size zero.") }
var paths = OnionRequestAPI.paths var paths = OnionRequestAPI.paths
if paths.count < pathCount { if paths.isEmpty {
let storage = OWSPrimaryStorage.shared() paths = Storage.getOnionRequestPaths()
storage.dbReadConnection.read { transaction in OnionRequestAPI.paths = paths
paths = storage.getOnionRequestPaths(in: transaction) if !paths.isEmpty {
OnionRequestAPI.paths = paths guardSnodes.formUnion([ paths[0][0] ])
if paths.count >= pathCount { if paths.count >= 2 {
guardSnodes.formUnion([ paths[0][0], paths[1][0] ]) guardSnodes.formUnion([ paths[1][0] ])
} }
} }
} }
// randomElement() uses the system's default random generator, which is cryptographically secure // randomElement() uses the system's default random generator, which is cryptographically secure
if paths.count >= pathCount { if paths.count >= targetPathCount {
return Promise<Path> { seal in if let snode = snode {
if let snode = snode { return Promise { $0.fulfill(paths.filter { !$0.contains(snode) }.randomElement()!) }
seal.fulfill(paths.filter { !$0.contains(snode) }.randomElement()!) } else {
return Promise { $0.fulfill(paths.randomElement()!) }
}
} else if !paths.isEmpty {
if let snode = snode {
if let path = paths.first(where: { !$0.contains(snode) }) {
buildPaths(reusing: paths).retainUntilComplete() // Re-build paths in the background
return Promise { $0.fulfill(path) }
} else { } else {
seal.fulfill(paths.randomElement()!) return buildPaths(reusing: paths).map2 { paths in
return paths.filter { !$0.contains(snode) }.randomElement()!
}
} }
} else {
buildPaths(reusing: paths).retainUntilComplete() // Re-build paths in the background
return Promise { $0.fulfill(paths.randomElement()!) }
} }
} else { } else {
return buildPaths().map2 { paths in return buildPaths(reusing: []).map2 { paths in
if let snode = snode { if let snode = snode {
return paths.filter { !$0.contains(snode) }.randomElement()! return paths.filter { !$0.contains(snode) }.randomElement()!
} else { } else {
@ -172,15 +192,48 @@ public enum OnionRequestAPI {
} }
} }
private static func dropAllPaths() { private static func dropGuardSnode(_ snode: Snode) {
paths.removeAll() guardSnodes = guardSnodes.filter { $0 != snode }
}
private static func drop(_ snode: Snode) throws {
// We repair the path here because we can do it sync. In the case where we drop a whole
// path we leave the re-building up to getPath(excluding:) because re-building the path
// in that case is async.
OnionRequestAPI.snodeFailureCount[snode] = 0
var oldPaths = paths
guard let pathIndex = oldPaths.firstIndex(where: { $0.contains(snode) }) else { return }
var path = oldPaths[pathIndex]
guard let snodeIndex = path.firstIndex(of: snode) else { return }
path.remove(at: snodeIndex)
let unusedSnodes = SnodeAPI.snodePool.subtracting(oldPaths.flatMap { $0 })
guard !unusedSnodes.isEmpty else { throw Error.insufficientSnodes }
// randomElement() uses the system's default random generator, which is cryptographically secure
path.append(unusedSnodes.randomElement()!)
// Don't test the new snode as this would reveal the user's IP
oldPaths.remove(at: pathIndex)
let newPaths = oldPaths + [ path ]
paths = newPaths
try! Storage.writeSync { transaction in try! Storage.writeSync { transaction in
OWSPrimaryStorage.shared().clearOnionRequestPaths(in: transaction) print("[Loki] Persisting onion request paths to database.")
Storage.setOnionRequestPaths(newPaths, using: transaction)
} }
} }
private static func dropGuardSnode(_ snode: Snode) { private static func drop(_ path: Path) {
guardSnodes = guardSnodes.filter { $0 != snode } OnionRequestAPI.pathFailureCount[path] = 0
var paths = OnionRequestAPI.paths
guard let pathIndex = paths.firstIndex(of: path) else { return }
paths.remove(at: pathIndex)
OnionRequestAPI.paths = paths
try! Storage.writeSync { transaction in
if !paths.isEmpty {
print("[Loki] Persisting onion request paths to database.")
Storage.setOnionRequestPaths(paths, using: transaction)
} else {
Storage.clearOnionRequestPaths(using: transaction)
}
}
} }
/// Builds an onion around `payload` and returns the result. /// Builds an onion around `payload` and returns the result.
@ -265,15 +318,16 @@ public enum OnionRequestAPI {
} }
let payload: JSON = [ let payload: JSON = [
"body" : parametersAsString, "body" : parametersAsString,
"endpoint": endpoint, "endpoint" : endpoint,
"method" : request.httpMethod, "method" : request.httpMethod!,
"headers" : headers "headers" : headers
] ]
let destination = Destination.server(host: host, x25519PublicKey: x25519PublicKey) let destination = Destination.server(host: host, x25519PublicKey: x25519PublicKey)
return sendOnionRequest(with: payload, to: destination, isJSONRequired: isJSONRequired).recover2 { error -> Promise<JSON> in let promise = sendOnionRequest(with: payload, to: destination, isJSONRequired: isJSONRequired)
promise.catch2 { error in
print("[Loki] [Onion Request API] Couldn't reach server: \(url) due to error: \(error).") print("[Loki] [Onion Request API] Couldn't reach server: \(url) due to error: \(error).")
throw error
} }
return promise
} }
internal static func sendOnionRequest(with payload: JSON, to destination: Destination, isJSONRequired: Bool = true) -> Promise<JSON> { internal static func sendOnionRequest(with payload: JSON, to destination: Destination, isJSONRequired: Bool = true) -> Promise<JSON> {
@ -282,18 +336,23 @@ public enum OnionRequestAPI {
SnodeAPI.workQueue.async { // Avoid race conditions on `guardSnodes` and `paths` SnodeAPI.workQueue.async { // Avoid race conditions on `guardSnodes` and `paths`
buildOnion(around: payload, targetedAt: destination).done2 { intermediate in buildOnion(around: payload, targetedAt: destination).done2 { intermediate in
guardSnode = intermediate.guardSnode guardSnode = intermediate.guardSnode
let url = "\(guardSnode.address):\(guardSnode.port)/onion_req" let url = "\(guardSnode.address):\(guardSnode.port)/onion_req/v2"
let finalEncryptionResult = intermediate.finalEncryptionResult let finalEncryptionResult = intermediate.finalEncryptionResult
let onion = finalEncryptionResult.ciphertext let onion = finalEncryptionResult.ciphertext
if case Destination.server = destination, Double(onion.count) > 0.75 * Double(FileServerAPI.maxFileSize) { if case Destination.server = destination, Double(onion.count) > 0.75 * Double(FileServerAPI.maxFileSize) {
print("[Loki] Approaching request size limit: ~\(onion.count) bytes.") print("[Loki] Approaching request size limit: ~\(onion.count) bytes.")
} }
let parameters: JSON = [ let parameters: JSON = [
"ciphertext" : onion.base64EncodedString(),
"ephemeral_key" : finalEncryptionResult.ephemeralPublicKey.toHexString() "ephemeral_key" : finalEncryptionResult.ephemeralPublicKey.toHexString()
] ]
let body: Data
do {
body = try encode(ciphertext: onion, json: parameters)
} catch {
return seal.reject(error)
}
let destinationSymmetricKey = intermediate.destinationSymmetricKey let destinationSymmetricKey = intermediate.destinationSymmetricKey
HTTP.execute(.post, url, parameters: parameters).done2 { rawResponse in HTTP.execute(.post, url, body: body).done2 { rawResponse in
guard let json = rawResponse as? JSON, let base64EncodedIVAndCiphertext = json["result"] as? String, guard let json = rawResponse as? JSON, let base64EncodedIVAndCiphertext = json["result"] as? String,
let ivAndCiphertext = Data(base64Encoded: base64EncodedIVAndCiphertext), ivAndCiphertext.count >= EncryptionUtilities.ivSize else { return seal.reject(HTTP.Error.invalidJSON) } let ivAndCiphertext = Data(base64Encoded: base64EncodedIVAndCiphertext), ivAndCiphertext.count >= EncryptionUtilities.ivSize else { return seal.reject(HTTP.Error.invalidJSON) }
do { do {
@ -330,14 +389,45 @@ public enum OnionRequestAPI {
} }
promise.catch2 { error in // Must be invoked on LokiAPI.workQueue promise.catch2 { error in // Must be invoked on LokiAPI.workQueue
guard case HTTP.Error.httpRequestFailed(let statusCode, let json) = error else { return } guard case HTTP.Error.httpRequestFailed(let statusCode, let json) = error else { return }
// Marking all the snodes in the path as unreliable here is aggressive, but otherwise users
// can get stuck with a failing path that just refreshes to the same path.
let path = paths.first { $0.contains(guardSnode) } let path = paths.first { $0.contains(guardSnode) }
path?.forEach { snode in func handleUnspecificError() {
SnodeAPI.handleError(withStatusCode: statusCode, json: json, forSnode: snode) // Intentionally don't throw guard let path = path else { return }
var pathFailureCount = OnionRequestAPI.pathFailureCount[path] ?? 0
pathFailureCount += 1
if pathFailureCount >= pathFailureThreshold {
dropGuardSnode(guardSnode)
path.forEach { snode in
SnodeAPI.handleError(withStatusCode: statusCode, json: json, forSnode: snode) // Intentionally don't throw
}
drop(path)
} else {
OnionRequestAPI.pathFailureCount[path] = pathFailureCount
}
}
let prefix = "Next node not found: "
if let message = json?["result"] as? String, message.hasPrefix(prefix) {
let ed25519PublicKey = message.substring(from: prefix.count)
if let path = path, let snode = path.first(where: { $0.publicKeySet?.ed25519Key == ed25519PublicKey }) {
var snodeFailureCount = OnionRequestAPI.snodeFailureCount[snode] ?? 0
snodeFailureCount += 1
if snodeFailureCount >= snodeFailureThreshold {
SnodeAPI.handleError(withStatusCode: statusCode, json: json, forSnode: snode) // Intentionally don't throw
do {
try drop(snode)
} catch {
handleUnspecificError()
}
} else {
OnionRequestAPI.snodeFailureCount[snode] = snodeFailureCount
}
} else {
handleUnspecificError()
}
} else if let message = json?["result"] as? String, message == "Loki Server error" {
// Do nothing
} else {
handleUnspecificError()
} }
dropAllPaths() // A snode in the path is bad; retry with a different path
dropGuardSnode(guardSnode)
} }
return promise return promise
} }

View File

@ -0,0 +1,48 @@
public extension Storage {
// MARK: Onion Request Paths
internal static let onionRequestPathCollection = "LokiOnionRequestPathCollection"
internal static func setOnionRequestPaths(_ paths: [OnionRequestAPI.Path], using transaction: YapDatabaseReadWriteTransaction) {
let collection = onionRequestPathCollection
// FIXME: This approach assumes either 1 or 2 paths of length 3 each. We should do better than this.
clearOnionRequestPaths(using: transaction)
guard paths.count >= 1 else { return }
let path0 = paths[0]
guard path0.count == 3 else { return }
transaction.setObject(path0[0], forKey: "0-0", inCollection: collection)
transaction.setObject(path0[1], forKey: "0-1", inCollection: collection)
transaction.setObject(path0[2], forKey: "0-2", inCollection: collection)
guard paths.count >= 2 else { return }
let path1 = paths[1]
guard path1.count == 3 else { return }
transaction.setObject(path1[0], forKey: "1-0", inCollection: collection)
transaction.setObject(path1[1], forKey: "1-1", inCollection: collection)
transaction.setObject(path1[2], forKey: "1-2", inCollection: collection)
}
public static func getOnionRequestPaths() -> [OnionRequestAPI.Path] {
let collection = onionRequestPathCollection
var result: [OnionRequestAPI.Path] = []
read { transaction in
if
let path0Snode0 = transaction.object(forKey: "0-0", inCollection: collection) as? Snode,
let path0Snode1 = transaction.object(forKey: "0-1", inCollection: collection) as? Snode,
let path0Snode2 = transaction.object(forKey: "0-2", inCollection: collection) as? Snode {
result.append([ path0Snode0, path0Snode1, path0Snode2 ])
if
let path1Snode0 = transaction.object(forKey: "1-0", inCollection: collection) as? Snode,
let path1Snode1 = transaction.object(forKey: "1-1", inCollection: collection) as? Snode,
let path1Snode2 = transaction.object(forKey: "1-2", inCollection: collection) as? Snode {
result.append([ path1Snode0, path1Snode1, path1Snode2 ])
}
}
}
return result
}
internal static func clearOnionRequestPaths(using transaction: YapDatabaseReadWriteTransaction) {
transaction.removeAllObjects(inCollection: onionRequestPathCollection)
}
}

View File

@ -388,27 +388,12 @@ public final class PublicChatAPI : DotNetAPI {
if oldProfilePictureURL != info.profilePictureURL || groupModel.groupImage == nil { if oldProfilePictureURL != info.profilePictureURL || groupModel.groupImage == nil {
storage.setProfilePictureURL(info.profilePictureURL, forPublicChatWithID: publicChatID, in: transaction) storage.setProfilePictureURL(info.profilePictureURL, forPublicChatWithID: publicChatID, in: transaction)
if let profilePictureURL = info.profilePictureURL { if let profilePictureURL = info.profilePictureURL {
let configuration = URLSessionConfiguration.default let url = server.hasSuffix("/") ? "\(server)\(profilePictureURL)" : "\(server)/\(profilePictureURL)"
let manager = AFURLSessionManager.init(sessionConfiguration: configuration) FileServerAPI.downloadAttachment(from: url).map2 { data in
let url = URL(string: "\(server)\(profilePictureURL)")! let attachmentStream = TSAttachmentStream(contentType: OWSMimeTypeImageJpeg, byteCount: UInt32(data.count), sourceFilename: nil, caption: nil, albumMessageId: nil)
let request = URLRequest(url: url) try attachmentStream.write(data)
let task = manager.downloadTask(with: request, progress: nil, groupThread.updateAvatar(with: attachmentStream)
destination: { (targetPath: URL, response: URLResponse) -> URL in }
let tempFilePath = URL(fileURLWithPath: OWSTemporaryDirectoryAccessibleAfterFirstAuth()).appendingPathComponent(UUID().uuidString)
return tempFilePath
},
completionHandler: { (response: URLResponse, filePath: URL?, error: Error?) in
if let error = error {
print("[Loki] Couldn't download profile picture for public chat channel with ID: \(channel) on server: \(server).")
return
}
if let filePath = filePath, let avatarData = try? Data.init(contentsOf: filePath) {
let attachmentStream = TSAttachmentStream(contentType: OWSMimeTypeImageJpeg, byteCount: UInt32(avatarData.count), sourceFilename: nil, caption: nil, albumMessageId: nil)
try! attachmentStream.write(avatarData)
groupThread.updateAvatar(with: attachmentStream)
}
})
task.resume()
} }
} }
} }

View File

@ -18,7 +18,7 @@ public final class SnodeAPI : NSObject {
private static let minimumSnodePoolCount = 64 private static let minimumSnodePoolCount = 64
private static let minimumSwarmSnodeCount = 2 private static let minimumSwarmSnodeCount = 2
private static let seedNodePool: Set<String> = [ "https://storage.seed1.loki.network", "https://storage.seed3.loki.network", "https://public.loki.foundation" ] private static let seedNodePool: Set<String> = [ "https://storage.seed1.loki.network", "https://storage.seed3.loki.network", "https://public.loki.foundation" ]
private static let snodeFailureThreshold = 2 private static let snodeFailureThreshold = 4
private static let targetSwarmSnodeCount = 2 private static let targetSwarmSnodeCount = 2
internal static var powDifficulty: UInt = 1 internal static var powDifficulty: UInt = 1
@ -89,7 +89,11 @@ public final class SnodeAPI : NSObject {
return Snode(address: "https://\(address)", port: UInt16(port), publicKeySet: Snode.KeySet(ed25519Key: ed25519PublicKey, x25519Key: x25519PublicKey)) return Snode(address: "https://\(address)", port: UInt16(port), publicKeySet: Snode.KeySet(ed25519Key: ed25519PublicKey, x25519Key: x25519PublicKey))
}) })
// randomElement() uses the system's default random generator, which is cryptographically secure // randomElement() uses the system's default random generator, which is cryptographically secure
return snodePool.randomElement()! if !snodePool.isEmpty {
return snodePool.randomElement()!
} else {
throw SnodeAPIError.randomSnodePoolUpdatingFailed
}
} }
}.done2 { snode in }.done2 { snode in
seal.fulfill(snode) seal.fulfill(snode)
@ -121,8 +125,10 @@ public final class SnodeAPI : NSObject {
} else { } else {
print("[Loki] Getting swarm for: \(publicKey == getUserHexEncodedPublicKey() ? "self" : publicKey).") print("[Loki] Getting swarm for: \(publicKey == getUserHexEncodedPublicKey() ? "self" : publicKey).")
let parameters: [String:Any] = [ "pubKey" : publicKey ] let parameters: [String:Any] = [ "pubKey" : publicKey ]
return getRandomSnode().then2 { return getRandomSnode().then2 { snode in
invoke(.getSwarm, on: $0, associatedWith: publicKey, parameters: parameters) attempt(maxRetryCount: 4, recoveringOn: SnodeAPI.workQueue) {
invoke(.getSwarm, on: snode, associatedWith: publicKey, parameters: parameters)
}
}.map2 { rawSnodes in }.map2 { rawSnodes in
let swarm = parseSnodes(from: rawSnodes) let swarm = parseSnodes(from: rawSnodes)
swarmCache[publicKey] = swarm swarmCache[publicKey] = swarm

View File

@ -41,17 +41,28 @@ public enum HTTP {
} }
// MARK: Main // MARK: Main
public static func execute(_ verb: Verb, _ url: String, parameters: JSON? = nil, timeout: TimeInterval = HTTP.timeout, useSeedNodeURLSession: Bool = false) -> Promise<JSON> { public static func execute(_ verb: Verb, _ url: String, timeout: TimeInterval = HTTP.timeout, useSeedNodeURLSession: Bool = false) -> Promise<JSON> {
var request = URLRequest(url: URL(string: url)!) return execute(verb, url, body: nil, timeout: timeout, useSeedNodeURLSession: useSeedNodeURLSession)
request.httpMethod = verb.rawValue }
public static func execute(_ verb: Verb, _ url: String, parameters: JSON?, timeout: TimeInterval = HTTP.timeout, useSeedNodeURLSession: Bool = false) -> Promise<JSON> {
if let parameters = parameters { if let parameters = parameters {
do { do {
guard JSONSerialization.isValidJSONObject(parameters) else { return Promise(error: Error.invalidJSON) } guard JSONSerialization.isValidJSONObject(parameters) else { return Promise(error: Error.invalidJSON) }
request.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: [ .fragmentsAllowed ]) let body = try JSONSerialization.data(withJSONObject: parameters, options: [ .fragmentsAllowed ])
return execute(verb, url, body: body, timeout: timeout, useSeedNodeURLSession: useSeedNodeURLSession)
} catch (let error) { } catch (let error) {
return Promise(error: error) return Promise(error: error)
} }
} else {
return execute(verb, url, body: nil, timeout: timeout, useSeedNodeURLSession: useSeedNodeURLSession)
} }
}
public static func execute(_ verb: Verb, _ url: String, body: Data?, timeout: TimeInterval = HTTP.timeout, useSeedNodeURLSession: Bool = false) -> Promise<JSON> {
var request = URLRequest(url: URL(string: url)!)
request.httpMethod = verb.rawValue
request.httpBody = body
request.timeoutInterval = timeout request.timeoutInterval = timeout
let (promise, seal) = Promise<JSON>.pending() let (promise, seal) = Promise<JSON>.pending()
let urlSession = useSeedNodeURLSession ? seedNodeURLSession : defaultURLSession let urlSession = useSeedNodeURLSession ? seedNodeURLSession : defaultURLSession

View File

@ -53,38 +53,6 @@ public extension OWSPrimaryStorage {
return result return result
} }
// MARK: Onion Request Paths
public func setOnionRequestPaths(_ paths: [OnionRequestAPI.Path], in transaction: YapDatabaseReadWriteTransaction) {
// FIXME: This is a bit of a dirty approach that assumes 2 paths of length 3 each. We should do better than this.
guard paths.count == 2 else { return }
let path0 = paths[0]
let path1 = paths[1]
guard path0.count == 3, path1.count == 3 else { return }
let collection = Storage.onionRequestPathCollection
transaction.setObject(path0[0], forKey: "0-0", inCollection: collection)
transaction.setObject(path0[1], forKey: "0-1", inCollection: collection)
transaction.setObject(path0[2], forKey: "0-2", inCollection: collection)
transaction.setObject(path1[0], forKey: "1-0", inCollection: collection)
transaction.setObject(path1[1], forKey: "1-1", inCollection: collection)
transaction.setObject(path1[2], forKey: "1-2", inCollection: collection)
}
public func getOnionRequestPaths(in transaction: YapDatabaseReadTransaction) -> [OnionRequestAPI.Path] {
let collection = Storage.onionRequestPathCollection
guard
let path0Snode0 = transaction.object(forKey: "0-0", inCollection: collection) as? Snode,
let path0Snode1 = transaction.object(forKey: "0-1", inCollection: collection) as? Snode,
let path0Snode2 = transaction.object(forKey: "0-2", inCollection: collection) as? Snode,
let path1Snode0 = transaction.object(forKey: "1-0", inCollection: collection) as? Snode,
let path1Snode1 = transaction.object(forKey: "1-1", inCollection: collection) as? Snode,
let path1Snode2 = transaction.object(forKey: "1-2", inCollection: collection) as? Snode else { return [] }
return [ [ path0Snode0, path0Snode1, path0Snode2 ], [ path1Snode0, path1Snode1, path1Snode2 ] ]
}
public func clearOnionRequestPaths(in transaction: YapDatabaseReadWriteTransaction) {
transaction.removeAllObjects(inCollection: Storage.onionRequestPathCollection)
}
// MARK: Session Requests // MARK: Session Requests
public func setSessionRequestTimestamp(for publicKey: String, to timestamp: Date, in transaction: YapDatabaseReadWriteTransaction) { public func setSessionRequestTimestamp(for publicKey: String, to timestamp: Date, in transaction: YapDatabaseReadWriteTransaction) {
transaction.setDate(timestamp, forKey: publicKey, inCollection: Storage.sessionRequestTimestampCollection) transaction.setDate(timestamp, forKey: publicKey, inCollection: Storage.sessionRequestTimestampCollection)

View File

@ -13,7 +13,6 @@
return "LokiSwarmCollection-\(publicKey)" return "LokiSwarmCollection-\(publicKey)"
} }
@objc public static let onionRequestPathCollection = "LokiOnionRequestPathCollection"
@objc public static let openGroupCollection = "LokiPublicChatCollection" @objc public static let openGroupCollection = "LokiPublicChatCollection"
@objc public static let openGroupProfilePictureURLCollection = "LokiPublicChatAvatarURLCollection" @objc public static let openGroupProfilePictureURLCollection = "LokiPublicChatAvatarURLCollection"
@objc public static let openGroupUserCountCollection = "LokiPublicChatUserCountCollection" @objc public static let openGroupUserCountCollection = "LokiPublicChatUserCountCollection"

View File

@ -10,6 +10,7 @@ public final class ClosedGroupUtilities : NSObject {
@objc public static let invalidGroupPublicKey = SSKDecryptionError(domain: "SSKErrorDomain", code: 1, userInfo: [ NSLocalizedDescriptionKey : "Invalid group public key." ]) @objc public static let invalidGroupPublicKey = SSKDecryptionError(domain: "SSKErrorDomain", code: 1, userInfo: [ NSLocalizedDescriptionKey : "Invalid group public key." ])
@objc public static let noData = SSKDecryptionError(domain: "SSKErrorDomain", code: 2, userInfo: [ NSLocalizedDescriptionKey : "Received an empty envelope." ]) @objc public static let noData = SSKDecryptionError(domain: "SSKErrorDomain", code: 2, userInfo: [ NSLocalizedDescriptionKey : "Received an empty envelope." ])
@objc public static let noGroupPrivateKey = SSKDecryptionError(domain: "SSKErrorDomain", code: 3, userInfo: [ NSLocalizedDescriptionKey : "Missing group private key." ]) @objc public static let noGroupPrivateKey = SSKDecryptionError(domain: "SSKErrorDomain", code: 3, userInfo: [ NSLocalizedDescriptionKey : "Missing group private key." ])
@objc public static let selfSend = SSKDecryptionError(domain: "SSKErrorDomain", code: 4, userInfo: [ NSLocalizedDescriptionKey : "Message addressed at self." ])
} }
@objc(encryptData:usingGroupPublicKey:transaction:error:) @objc(encryptData:usingGroupPublicKey:transaction:error:)
@ -59,6 +60,7 @@ public final class ClosedGroupUtilities : NSObject {
// 4. ) Parse the closed group ciphertext message // 4. ) Parse the closed group ciphertext message
let closedGroupCiphertextMessage = ClosedGroupCiphertextMessage(_throws_with: closedGroupCiphertextMessageAsData) let closedGroupCiphertextMessage = ClosedGroupCiphertextMessage(_throws_with: closedGroupCiphertextMessageAsData)
let senderPublicKey = closedGroupCiphertextMessage.senderPublicKey.toHexString() let senderPublicKey = closedGroupCiphertextMessage.senderPublicKey.toHexString()
guard senderPublicKey != getUserHexEncodedPublicKey() else { throw SSKDecryptionError.selfSend }
// 5. ) Use the info inside the closed group ciphertext message to decrypt the actual message content // 5. ) Use the info inside the closed group ciphertext message to decrypt the actual message content
let plaintext = try SharedSenderKeysImplementation.shared.decrypt(closedGroupCiphertextMessage.ivAndCiphertext, forGroupWithPublicKey: groupPublicKey, let plaintext = try SharedSenderKeysImplementation.shared.decrypt(closedGroupCiphertextMessage.ivAndCiphertext, forGroupWithPublicKey: groupPublicKey,
senderPublicKey: senderPublicKey, keyIndex: UInt(closedGroupCiphertextMessage.keyIndex), protocolContext: transaction) senderPublicKey: senderPublicKey, keyIndex: UInt(closedGroupCiphertextMessage.keyIndex), protocolContext: transaction)

View File

@ -118,13 +118,25 @@ public final class ClosedGroupsProtocol : NSObject {
print("[Loki] Can't remove self and others simultaneously.") print("[Loki] Can't remove self and others simultaneously.")
return Promise(error: Error.invalidUpdate) return Promise(error: Error.invalidUpdate)
} }
// Send the update to the group (don't include new ratchets as everyone should regenerate new ratchets individually) // Establish sessions if needed
let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage.Kind.info(groupPublicKey: Data(hex: groupPublicKey), name: name, senderKeys: [], establishSessionsIfNeeded(with: [String](members), using: transaction)
members: membersAsData, admins: adminsAsData) // Send the update to the existing members using established channels (don't include new ratchets as everyone should regenerate new ratchets individually)
let closedGroupUpdateMessage = ClosedGroupUpdateMessage(thread: thread, kind: closedGroupUpdateMessageKind) let promises: [Promise<Void>] = oldMembers.map { member in
SSKEnvironment.shared.messageSender.send(closedGroupUpdateMessage, success: { seal.fulfill(()) }, failure: { seal.reject($0) }) let thread = TSContactThread.getOrCreateThread(withContactId: member, transaction: transaction)
thread.save(with: transaction)
let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage.Kind.info(groupPublicKey: Data(hex: groupPublicKey), name: name, senderKeys: [],
members: membersAsData, admins: adminsAsData)
let closedGroupUpdateMessage = ClosedGroupUpdateMessage(thread: thread, kind: closedGroupUpdateMessageKind)
return SSKEnvironment.shared.messageSender.sendPromise(message: closedGroupUpdateMessage)
}
when(resolved: promises).done2 { _ in seal.fulfill(()) }.catch2 { seal.reject($0) }
promise.done { promise.done {
try! Storage.writeSync { transaction in try! Storage.writeSync { transaction in
let allOldRatchets = Storage.getAllClosedGroupRatchets(for: groupPublicKey)
for (senderPublicKey, oldRatchet) in allOldRatchets {
let collection = Storage.ClosedGroupRatchetCollectionType.old
Storage.setClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, ratchet: oldRatchet, in: collection, using: transaction)
}
// Delete all ratchets (it's important that this happens * after * sending out the update) // Delete all ratchets (it's important that this happens * after * sending out the update)
Storage.removeAllClosedGroupRatchets(for: groupPublicKey, using: transaction) Storage.removeAllClosedGroupRatchets(for: groupPublicKey, using: transaction)
// Remove the group from the user's set of public keys to poll for if the user is leaving. Otherwise generate a new ratchet and // Remove the group from the user's set of public keys to poll for if the user is leaving. Otherwise generate a new ratchet and
@ -134,8 +146,6 @@ public final class ClosedGroupsProtocol : NSObject {
// Notify the PN server // Notify the PN server
LokiPushNotificationManager.performOperation(.unsubscribe, for: groupPublicKey, publicKey: userPublicKey) LokiPushNotificationManager.performOperation(.unsubscribe, for: groupPublicKey, publicKey: userPublicKey)
} else { } else {
// Establish sessions if needed
establishSessionsIfNeeded(with: [String](members), using: transaction)
// Send closed group update messages to any new members using established channels // Send closed group update messages to any new members using established channels
for member in newMembers { for member in newMembers {
let thread = TSContactThread.getOrCreateThread(withContactId: member, transaction: transaction) let thread = TSContactThread.getOrCreateThread(withContactId: member, transaction: transaction)
@ -203,13 +213,13 @@ public final class ClosedGroupsProtocol : NSObject {
return promise return promise
} }
/// The returned promise is fulfilled when the message has been sent **to the group**. It doesn't wait for the user's new ratchet to be distributed. /// The returned promise is fulfilled when the group update message has been sent. It doesn't wait for the user's new ratchet to be distributed.
@objc(leaveGroupWithPublicKey:transaction:) @objc(leaveGroupWithPublicKey:transaction:)
public static func objc_leave(_ groupPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) -> AnyPromise { public static func objc_leave(_ groupPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) -> AnyPromise {
return AnyPromise.from(leave(groupPublicKey, using: transaction)) return AnyPromise.from(leave(groupPublicKey, using: transaction))
} }
/// The returned promise is fulfilled when the message has been sent **to the group**. It doesn't wait for the user's new ratchet to be distributed. /// The returned promise is fulfilled when the group update message has been sent. It doesn't wait for the user's new ratchet to be distributed.
public static func leave(_ groupPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) -> Promise<Void> { public static func leave(_ groupPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) -> Promise<Void> {
let userPublicKey = UserDefaults.standard[.masterHexEncodedPublicKey] ?? getUserHexEncodedPublicKey() let userPublicKey = UserDefaults.standard[.masterHexEncodedPublicKey] ?? getUserHexEncodedPublicKey()
let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey) let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey)
@ -358,6 +368,11 @@ public final class ClosedGroupsProtocol : NSObject {
let userPublicKey = UserDefaults.standard[.masterHexEncodedPublicKey] ?? getUserHexEncodedPublicKey() let userPublicKey = UserDefaults.standard[.masterHexEncodedPublicKey] ?? getUserHexEncodedPublicKey()
let wasUserRemoved = !members.contains(userPublicKey) let wasUserRemoved = !members.contains(userPublicKey)
if Set(members).intersection(oldMembers) != Set(oldMembers) { if Set(members).intersection(oldMembers) != Set(oldMembers) {
let allOldRatchets = Storage.getAllClosedGroupRatchets(for: groupPublicKey)
for (senderPublicKey, oldRatchet) in allOldRatchets {
let collection = Storage.ClosedGroupRatchetCollectionType.old
Storage.setClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, ratchet: oldRatchet, in: collection, using: transaction)
}
Storage.removeAllClosedGroupRatchets(for: groupPublicKey, using: transaction) Storage.removeAllClosedGroupRatchets(for: groupPublicKey, using: transaction)
if wasUserRemoved { if wasUserRemoved {
Storage.removeClosedGroupPrivateKey(for: groupPublicKey, using: transaction) Storage.removeClosedGroupPrivateKey(for: groupPublicKey, using: transaction)

View File

@ -40,11 +40,13 @@ public final class SharedSenderKeysImplementation : NSObject {
public enum RatchetingError : LocalizedError { public enum RatchetingError : LocalizedError {
case loadingFailed(groupPublicKey: String, senderPublicKey: String) case loadingFailed(groupPublicKey: String, senderPublicKey: String)
case messageKeyMissing(targetKeyIndex: UInt, groupPublicKey: String, senderPublicKey: String) case messageKeyMissing(targetKeyIndex: UInt, groupPublicKey: String, senderPublicKey: String)
case generic
public var errorDescription: String? { public var errorDescription: String? {
switch self { switch self {
case .loadingFailed(let groupPublicKey, let senderPublicKey): return "Couldn't get ratchet for closed group with public key: \(groupPublicKey), sender public key: \(senderPublicKey)." case .loadingFailed(let groupPublicKey, let senderPublicKey): return "Couldn't get ratchet for closed group with public key: \(groupPublicKey), sender public key: \(senderPublicKey)."
case .messageKeyMissing(let targetKeyIndex, let groupPublicKey, let senderPublicKey): return "Couldn't find message key for old key index: \(targetKeyIndex), public key: \(groupPublicKey), sender public key: \(senderPublicKey)." case .messageKeyMissing(let targetKeyIndex, let groupPublicKey, let senderPublicKey): return "Couldn't find message key for old key index: \(targetKeyIndex), public key: \(groupPublicKey), sender public key: \(senderPublicKey)."
case .generic: return "An error occurred"
} }
} }
} }
@ -66,7 +68,8 @@ public final class SharedSenderKeysImplementation : NSObject {
let nextMessageKey = try HMAC(key: Data(hex: ratchet.chainKey).bytes, variant: .sha256).authenticate([ UInt8(1) ]) let nextMessageKey = try HMAC(key: Data(hex: ratchet.chainKey).bytes, variant: .sha256).authenticate([ UInt8(1) ])
let nextChainKey = try HMAC(key: Data(hex: ratchet.chainKey).bytes, variant: .sha256).authenticate([ UInt8(2) ]) let nextChainKey = try HMAC(key: Data(hex: ratchet.chainKey).bytes, variant: .sha256).authenticate([ UInt8(2) ])
let nextKeyIndex = ratchet.keyIndex + 1 let nextKeyIndex = ratchet.keyIndex + 1
return ClosedGroupRatchet(chainKey: nextChainKey.toHexString(), keyIndex: nextKeyIndex, messageKeys: [ nextMessageKey.toHexString() ]) let messageKeys = ratchet.messageKeys + [ nextMessageKey.toHexString() ]
return ClosedGroupRatchet(chainKey: nextChainKey.toHexString(), keyIndex: nextKeyIndex, messageKeys: messageKeys)
} }
/// - Note: Sync. Don't call from the main thread. /// - Note: Sync. Don't call from the main thread.
@ -90,11 +93,12 @@ public final class SharedSenderKeysImplementation : NSObject {
} }
/// - Note: Sync. Don't call from the main thread. /// - Note: Sync. Don't call from the main thread.
private func stepRatchet(for groupPublicKey: String, senderPublicKey: String, until targetKeyIndex: UInt, using transaction: YapDatabaseReadWriteTransaction) throws -> ClosedGroupRatchet { private func stepRatchet(for groupPublicKey: String, senderPublicKey: String, until targetKeyIndex: UInt, using transaction: YapDatabaseReadWriteTransaction, isRetry: Bool = false) throws -> ClosedGroupRatchet {
#if DEBUG #if DEBUG
assert(!Thread.isMainThread) assert(!Thread.isMainThread)
#endif #endif
guard let ratchet = Storage.getClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey) else { let collection: Storage.ClosedGroupRatchetCollectionType = (isRetry) ? .old : .current
guard let ratchet = Storage.getClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, from: collection) else {
let error = RatchetingError.loadingFailed(groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey) let error = RatchetingError.loadingFailed(groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey)
print("[Loki] \(error.errorDescription!)") print("[Loki] \(error.errorDescription!)")
throw error throw error
@ -109,20 +113,18 @@ public final class SharedSenderKeysImplementation : NSObject {
return ratchet return ratchet
} else { } else {
var currentKeyIndex = ratchet.keyIndex var currentKeyIndex = ratchet.keyIndex
var current = ratchet var result = ratchet
var messageKeys: [String] = []
while currentKeyIndex < targetKeyIndex { while currentKeyIndex < targetKeyIndex {
do { do {
current = try step(current) result = try step(result)
messageKeys += current.messageKeys currentKeyIndex = result.keyIndex
currentKeyIndex = current.keyIndex
} catch { } catch {
print("[Loki] Couldn't step ratchet due to error: \(error).") print("[Loki] Couldn't step ratchet due to error: \(error).")
throw error throw error
} }
} }
let result = ClosedGroupRatchet(chainKey: current.chainKey, keyIndex: current.keyIndex, messageKeys: messageKeys) // Includes any skipped message keys let collection: Storage.ClosedGroupRatchetCollectionType = (isRetry) ? .old : .current
Storage.setClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, ratchet: result, using: transaction) Storage.setClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, ratchet: result, in: collection, using: transaction)
return result return result
} }
} }
@ -161,30 +163,49 @@ public final class SharedSenderKeysImplementation : NSObject {
return try decrypt(ivAndCiphertext, for: groupPublicKey, senderPublicKey: senderPublicKey, keyIndex: keyIndex, using: transaction) return try decrypt(ivAndCiphertext, for: groupPublicKey, senderPublicKey: senderPublicKey, keyIndex: keyIndex, using: transaction)
} }
public func decrypt(_ ivAndCiphertext: Data, for groupPublicKey: String, senderPublicKey: String, keyIndex: UInt, using transaction: YapDatabaseReadWriteTransaction) throws -> Data { public func decrypt(_ ivAndCiphertext: Data, for groupPublicKey: String, senderPublicKey: String, keyIndex: UInt, using transaction: YapDatabaseReadWriteTransaction, isRetry: Bool = false) throws -> Data {
let ratchet: ClosedGroupRatchet let ratchet: ClosedGroupRatchet
do { do {
ratchet = try stepRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, until: keyIndex, using: transaction) ratchet = try stepRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, until: keyIndex, using: transaction, isRetry: isRetry)
} catch { } catch {
// FIXME: It'd be cleaner to handle this in OWSMessageDecrypter (where all the other decryption errors are handled), but this was a lot more if !isRetry {
// convenient because there's an easy way to get the sender public key from here. return try decrypt(ivAndCiphertext, for: groupPublicKey, senderPublicKey: senderPublicKey, keyIndex: keyIndex, using: transaction, isRetry: true)
if case RatchetingError.loadingFailed(_, _) = error { } else {
ClosedGroupsProtocol.requestSenderKey(for: groupPublicKey, senderPublicKey: senderPublicKey, using: transaction) // FIXME: It'd be cleaner to handle this in OWSMessageDecrypter (where all the other decryption errors are handled), but this was a lot more
// convenient because there's an easy way to get the sender public key from here.
if case RatchetingError.loadingFailed(_, _) = error {
ClosedGroupsProtocol.requestSenderKey(for: groupPublicKey, senderPublicKey: senderPublicKey, using: transaction)
}
throw error
} }
throw error
} }
let iv = ivAndCiphertext[0..<Int(SharedSenderKeysImplementation.ivSize)] let iv = ivAndCiphertext[0..<Int(SharedSenderKeysImplementation.ivSize)]
let ciphertext = ivAndCiphertext[Int(SharedSenderKeysImplementation.ivSize)...] let ciphertext = ivAndCiphertext[Int(SharedSenderKeysImplementation.ivSize)...]
let gcm = GCM(iv: iv.bytes, tagLength: Int(SharedSenderKeysImplementation.gcmTagSize), mode: .combined) let gcm = GCM(iv: iv.bytes, tagLength: Int(SharedSenderKeysImplementation.gcmTagSize), mode: .combined)
guard let messageKey = ratchet.messageKeys.last else { let messageKeys = ratchet.messageKeys
let lastNMessageKeys: [String]
if messageKeys.count > 16 { // Pick an arbitrary number of message keys to try; this helps resolve issues caused by messages arriving out of order
lastNMessageKeys = [String](messageKeys[messageKeys.index(messageKeys.endIndex, offsetBy: -16)..<messageKeys.endIndex])
} else {
lastNMessageKeys = messageKeys
}
guard !lastNMessageKeys.isEmpty else {
throw RatchetingError.messageKeyMissing(targetKeyIndex: keyIndex, groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey) throw RatchetingError.messageKeyMissing(targetKeyIndex: keyIndex, groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey)
} }
let aes = try AES(key: Data(hex: messageKey).bytes, blockMode: gcm, padding: .noPadding) var error: Error?
do { for messageKey in lastNMessageKeys.reversed() { // Reversed because most likely the last one is the one we need
return Data(try aes.decrypt(ciphertext.bytes)) let aes = try AES(key: Data(hex: messageKey).bytes, blockMode: gcm, padding: .noPadding)
} catch { do {
return Data(try aes.decrypt(ciphertext.bytes))
} catch (let e) {
error = e
}
}
if !isRetry {
return try decrypt(ivAndCiphertext, for: groupPublicKey, senderPublicKey: senderPublicKey, keyIndex: keyIndex, using: transaction, isRetry: true)
} else {
ClosedGroupsProtocol.requestSenderKey(for: groupPublicKey, senderPublicKey: senderPublicKey, using: transaction) ClosedGroupsProtocol.requestSenderKey(for: groupPublicKey, senderPublicKey: senderPublicKey, using: transaction)
throw error throw error ?? RatchetingError.generic
} }
} }

View File

@ -1,13 +1,20 @@
public extension Storage { public extension Storage {
// MARK: Ratchets internal enum ClosedGroupRatchetCollectionType {
internal static func getClosedGroupRatchetCollection(for groupPublicKey: String) -> String { case old, current
return "LokiClosedGroupRatchetCollection.\(groupPublicKey)"
} }
internal static func getClosedGroupRatchet(for groupPublicKey: String, senderPublicKey: String) -> ClosedGroupRatchet? { // MARK: Ratchets
let collection = getClosedGroupRatchetCollection(for: groupPublicKey) internal static func getClosedGroupRatchetCollection(_ collection: ClosedGroupRatchetCollectionType, for groupPublicKey: String) -> String {
switch collection {
case .old: return "LokiOldClosedGroupRatchetCollection.\(groupPublicKey)"
case .current: return "LokiClosedGroupRatchetCollection.\(groupPublicKey)"
}
}
internal static func getClosedGroupRatchet(for groupPublicKey: String, senderPublicKey: String, from collection: ClosedGroupRatchetCollectionType = .current) -> ClosedGroupRatchet? {
let collection = getClosedGroupRatchetCollection(collection, for: groupPublicKey)
var result: ClosedGroupRatchet? var result: ClosedGroupRatchet?
read { transaction in read { transaction in
result = transaction.object(forKey: senderPublicKey, inCollection: collection) as? ClosedGroupRatchet result = transaction.object(forKey: senderPublicKey, inCollection: collection) as? ClosedGroupRatchet
@ -15,26 +22,31 @@ public extension Storage {
return result return result
} }
internal static func setClosedGroupRatchet(for groupPublicKey: String, senderPublicKey: String, ratchet: ClosedGroupRatchet, using transaction: YapDatabaseReadWriteTransaction) { internal static func setClosedGroupRatchet(for groupPublicKey: String, senderPublicKey: String, ratchet: ClosedGroupRatchet, in collection: ClosedGroupRatchetCollectionType = .current, using transaction: YapDatabaseReadWriteTransaction) {
let collection = getClosedGroupRatchetCollection(for: groupPublicKey) let collection = getClosedGroupRatchetCollection(collection, for: groupPublicKey)
transaction.setObject(ratchet, forKey: senderPublicKey, inCollection: collection) transaction.setObject(ratchet, forKey: senderPublicKey, inCollection: collection)
} }
internal static func getAllClosedGroupSenderKeys(for groupPublicKey: String) -> Set<ClosedGroupSenderKey> { internal static func getAllClosedGroupRatchets(for groupPublicKey: String, from collection: ClosedGroupRatchetCollectionType = .current) -> [(senderPublicKey: String, ratchet: ClosedGroupRatchet)] {
let collection = getClosedGroupRatchetCollection(for: groupPublicKey) let collection = getClosedGroupRatchetCollection(collection, for: groupPublicKey)
var result: Set<ClosedGroupSenderKey> = [] var result: [(senderPublicKey: String, ratchet: ClosedGroupRatchet)] = []
read { transaction in read { transaction in
transaction.enumerateRows(inCollection: collection) { key, object, _, _ in transaction.enumerateRows(inCollection: collection) { key, object, _, _ in
guard let publicKey = key as? String, let ratchet = object as? ClosedGroupRatchet else { return } guard let senderPublicKey = key as? String, let ratchet = object as? ClosedGroupRatchet else { return }
let senderKey = ClosedGroupSenderKey(chainKey: Data(hex: ratchet.chainKey), keyIndex: ratchet.keyIndex, publicKey: Data(hex: publicKey)) result.append((senderPublicKey: senderPublicKey, ratchet: ratchet))
result.insert(senderKey)
} }
} }
return result return result
} }
internal static func removeAllClosedGroupRatchets(for groupPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) { internal static func getAllClosedGroupSenderKeys(for groupPublicKey: String, from collection: ClosedGroupRatchetCollectionType = .current) -> Set<ClosedGroupSenderKey> {
let collection = getClosedGroupRatchetCollection(for: groupPublicKey) return Set(getAllClosedGroupRatchets(for: groupPublicKey, from: collection).map { senderPublicKey, ratchet in
ClosedGroupSenderKey(chainKey: Data(hex: ratchet.chainKey), keyIndex: ratchet.keyIndex, publicKey: Data(hex: senderPublicKey))
})
}
internal static func removeAllClosedGroupRatchets(for groupPublicKey: String, from collection: ClosedGroupRatchetCollectionType = .current, using transaction: YapDatabaseReadWriteTransaction) {
let collection = getClosedGroupRatchetCollection(collection, for: groupPublicKey)
transaction.removeAllObjects(inCollection: collection) transaction.removeAllObjects(inCollection: collection)
} }
} }

View File

@ -10,6 +10,7 @@ public final class LokiPushNotificationManager : NSObject {
private static let server = "https://live.apns.getsession.org" private static let server = "https://live.apns.getsession.org"
#endif #endif
internal static let pnServerPublicKey = "642a6585919742e5a2d4dc51244964fbcd8bcab2b75612407de58b810740d049" internal static let pnServerPublicKey = "642a6585919742e5a2d4dc51244964fbcd8bcab2b75612407de58b810740d049"
private static let maxRetryCount: UInt = 4
private static let tokenExpirationInterval: TimeInterval = 12 * 60 * 60 private static let tokenExpirationInterval: TimeInterval = 12 * 60 * 60
public enum ClosedGroupOperation: String { public enum ClosedGroupOperation: String {
@ -28,12 +29,14 @@ public final class LokiPushNotificationManager : NSObject {
let url = URL(string: "\(server)/unregister")! let url = URL(string: "\(server)/unregister")!
let request = TSRequest(url: url, method: "POST", parameters: parameters) let request = TSRequest(url: url, method: "POST", parameters: parameters)
request.allHTTPHeaderFields = [ "Content-Type" : "application/json" ] request.allHTTPHeaderFields = [ "Content-Type" : "application/json" ]
let promise: Promise<Void> = OnionRequestAPI.sendOnionRequest(request, to: server, using: pnServerPublicKey).map2 { response in let promise: Promise<Void> = attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global()) {
guard let json = response["body"] as? JSON else { OnionRequestAPI.sendOnionRequest(request, to: server, using: pnServerPublicKey).map2 { response in
return print("[Loki] Couldn't unregister from push notifications.") guard let json = response["body"] as? JSON else {
} return print("[Loki] Couldn't unregister from push notifications.")
guard json["code"] as? Int != 0 else { }
return print("[Loki] Couldn't unregister from push notifications due to error: \(json["message"] as? String ?? "nil").") guard json["code"] as? Int != 0 else {
return print("[Loki] Couldn't unregister from push notifications due to error: \(json["message"] as? String ?? "nil").")
}
} }
} }
promise.catch2 { error in promise.catch2 { error in
@ -68,16 +71,18 @@ public final class LokiPushNotificationManager : NSObject {
let url = URL(string: "\(server)/register")! let url = URL(string: "\(server)/register")!
let request = TSRequest(url: url, method: "POST", parameters: parameters) let request = TSRequest(url: url, method: "POST", parameters: parameters)
request.allHTTPHeaderFields = [ "Content-Type" : "application/json" ] request.allHTTPHeaderFields = [ "Content-Type" : "application/json" ]
let promise: Promise<Void> = OnionRequestAPI.sendOnionRequest(request, to: server, using: pnServerPublicKey).map2 { response in let promise: Promise<Void> = attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global()) {
guard let json = response["body"] as? JSON else { OnionRequestAPI.sendOnionRequest(request, to: server, using: pnServerPublicKey).map2 { response in
return print("[Loki] Couldn't register device token.") guard let json = response["body"] as? JSON else {
return print("[Loki] Couldn't register device token.")
}
guard json["code"] as? Int != 0 else {
return print("[Loki] Couldn't register device token due to error: \(json["message"] as? String ?? "nil").")
}
userDefaults[.deviceToken] = hexEncodedToken
userDefaults[.lastDeviceTokenUpload] = now
userDefaults[.isUsingFullAPNs] = true
} }
guard json["code"] as? Int != 0 else {
return print("[Loki] Couldn't register device token due to error: \(json["message"] as? String ?? "nil").")
}
userDefaults[.deviceToken] = hexEncodedToken
userDefaults[.lastDeviceTokenUpload] = now
userDefaults[.isUsingFullAPNs] = true
} }
promise.catch2 { error in promise.catch2 { error in
print("[Loki] Couldn't register device token.") print("[Loki] Couldn't register device token.")
@ -103,14 +108,15 @@ public final class LokiPushNotificationManager : NSObject {
let url = URL(string: "\(server)/\(operation.rawValue)")! let url = URL(string: "\(server)/\(operation.rawValue)")!
let request = TSRequest(url: url, method: "POST", parameters: parameters) let request = TSRequest(url: url, method: "POST", parameters: parameters)
request.allHTTPHeaderFields = [ "Content-Type" : "application/json" ] request.allHTTPHeaderFields = [ "Content-Type" : "application/json" ]
let promise = OnionRequestAPI.sendOnionRequest(request, to: server, using: pnServerPublicKey).map2 { response in let promise: Promise<Void> = attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global()) {
guard let json = response["body"] as? JSON else { OnionRequestAPI.sendOnionRequest(request, to: server, using: pnServerPublicKey).map2 { response in
return print("[Loki] Couldn't subscribe/unsubscribe closed group: \(closedGroupPublicKey).") guard let json = response["body"] as? JSON else {
return print("[Loki] Couldn't subscribe/unsubscribe closed group: \(closedGroupPublicKey).")
}
guard json["code"] as? Int != 0 else {
return print("[Loki] Couldn't subscribe/unsubscribe for closed group: \(closedGroupPublicKey) due to error: \(json["message"] as? String ?? "nil").")
}
} }
guard json["code"] as? Int != 0 else {
return print("[Loki] Couldn't subscribe/unsubscribe for closed group: \(closedGroupPublicKey) due to error: \(json["message"] as? String ?? "nil").")
}
return
} }
promise.catch2 { error in promise.catch2 { error in
print("[Loki] Couldn't subscribe/unsubscribe closed group: \(closedGroupPublicKey).") print("[Loki] Couldn't subscribe/unsubscribe closed group: \(closedGroupPublicKey).")
@ -124,14 +130,15 @@ public final class LokiPushNotificationManager : NSObject {
let url = URL(string: "\(server)/notify")! let url = URL(string: "\(server)/notify")!
let request = TSRequest(url: url, method: "POST", parameters: parameters) let request = TSRequest(url: url, method: "POST", parameters: parameters)
request.allHTTPHeaderFields = [ "Content-Type" : "application/json" ] request.allHTTPHeaderFields = [ "Content-Type" : "application/json" ]
let promise = OnionRequestAPI.sendOnionRequest(request, to: server, using: pnServerPublicKey).map2 { response in let promise: Promise<Void> = attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global()) {
guard let json = response["body"] as? JSON else { OnionRequestAPI.sendOnionRequest(request, to: server, using: pnServerPublicKey).map2 { response in
return print("[Loki] Couldn't notify PN server.") guard let json = response["body"] as? JSON else {
return print("[Loki] Couldn't notify PN server.")
}
guard json["code"] as? Int != 0 else {
return print("[Loki] Couldn't notify PN server due to error: \(json["message"] as? String ?? "nil").")
}
} }
guard json["code"] as? Int != 0 else {
return print("[Loki] Couldn't notify PN server due to error: \(json["message"] as? String ?? "nil").")
}
return
} }
promise.catch2 { error in promise.catch2 { error in
print("[Loki] Couldn't notify PN server.") print("[Loki] Couldn't notify PN server.")

View File

@ -500,12 +500,6 @@ typedef void (^AttachmentDownloadFailure)(NSError *error);
OWSAssertDebug(job); OWSAssertDebug(job);
TSAttachmentPointer *attachmentPointer = job.attachmentPointer; TSAttachmentPointer *attachmentPointer = job.attachmentPointer;
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.requestSerializer = [AFHTTPRequestSerializer serializer];
[manager.requestSerializer setValue:OWSMimeTypeApplicationOctetStream forHTTPHeaderField:@"Content-Type"];
manager.responseSerializer = [AFHTTPResponseSerializer serializer];
manager.completionQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// We want to avoid large downloads from a compromised or buggy service. // We want to avoid large downloads from a compromised or buggy service.
const long kMaxDownloadSize = 10 * 1024 * 1024; const long kMaxDownloadSize = 10 * 1024 * 1024;
__block BOOL hasCheckedContentLength = NO; __block BOOL hasCheckedContentLength = NO;
@ -513,7 +507,6 @@ typedef void (^AttachmentDownloadFailure)(NSError *error);
NSString *tempFilePath = NSString *tempFilePath =
[OWSTemporaryDirectoryAccessibleAfterFirstAuth() stringByAppendingPathComponent:[NSUUID UUID].UUIDString]; [OWSTemporaryDirectoryAccessibleAfterFirstAuth() stringByAppendingPathComponent:[NSUUID UUID].UUIDString];
NSURL *tempFileURL = [NSURL fileURLWithPath:tempFilePath]; NSURL *tempFileURL = [NSURL fileURLWithPath:tempFilePath];
__block NSURLSessionDownloadTask *task; __block NSURLSessionDownloadTask *task;
void (^failureHandler)(NSError *) = ^(NSError *error) { void (^failureHandler)(NSError *) = ^(NSError *error) {
OWSLogError(@"Failed to download attachment with error: %@", error.description); OWSLogError(@"Failed to download attachment with error: %@", error.description);
@ -525,124 +518,26 @@ typedef void (^AttachmentDownloadFailure)(NSError *error);
failureHandlerParam(task, error); failureHandlerParam(task, error);
}; };
NSString *method = @"GET"; [[LKFileServerAPI downloadAttachmentFrom:location].then(^(NSData *data) {
NSError *serializationError = nil; BOOL success = [data writeToFile:tempFilePath atomically:YES];
NSMutableURLRequest *request = [manager.requestSerializer requestWithMethod:method if (success) {
URLString:location
parameters:nil
error:&serializationError];
if (serializationError) {
return failureHandler(serializationError);
}
task = [manager downloadTaskWithRequest:request
progress:^(NSProgress *progress) {
OWSAssertDebug(progress != nil);
// Don't do anything until we've received at least one byte of data.
if (progress.completedUnitCount < 1) {
return;
}
void (^abortDownload)(void) = ^{
OWSFailDebug(@"Download aborted.");
[task cancel];
};
if (progress.totalUnitCount > kMaxDownloadSize || progress.completedUnitCount > kMaxDownloadSize) {
// A malicious service might send a misleading content length header,
// so....
//
// If the current downloaded bytes or the expected total byes
// exceed the max download size, abort the download.
OWSLogError(@"Attachment download exceed expected content length: %lld, %lld.",
(long long)progress.totalUnitCount,
(long long)progress.completedUnitCount);
abortDownload();
return;
}
job.progress = progress.fractionCompleted;
[self fireProgressNotification:MAX(kAttachmentDownloadProgressTheta, progress.fractionCompleted)
attachmentId:attachmentPointer.uniqueId];
// We only need to check the content length header once.
if (hasCheckedContentLength) {
return;
}
// Once we've received some bytes of the download, check the content length
// header for the download.
//
// If the task doesn't exist, or doesn't have a response, or is missing
// the expected headers, or has an invalid or oversize content length, etc.,
// abort the download.
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)task.response;
if (![httpResponse isKindOfClass:[NSHTTPURLResponse class]]) {
OWSLogError(@"Attachment download has missing or invalid response.");
abortDownload();
return;
}
NSDictionary *headers = [httpResponse allHeaderFields];
if (![headers isKindOfClass:[NSDictionary class]]) {
OWSLogError(@"Attachment download invalid headers.");
abortDownload();
return;
}
NSString *contentLength = headers[@"Content-Length"];
if (![contentLength isKindOfClass:[NSString class]]) {
OWSLogError(@"Attachment download missing or invalid content length.");
abortDownload();
return;
}
if (contentLength.longLongValue > kMaxDownloadSize) {
OWSLogError(@"Attachment download content length exceeds max download size.");
abortDownload();
return;
}
// This response has a valid content length that is less
// than our max download size. Proceed with the download.
hasCheckedContentLength = YES;
}
destination:^(NSURL *targetPath, NSURLResponse *response) {
return tempFileURL;
}
completionHandler:^(NSURLResponse *response, NSURL *_Nullable filePath, NSError *_Nullable error) {
if (error) {
failureHandler(error);
return;
}
if (![tempFileURL isEqual:filePath]) {
OWSLogError(@"Unexpected temp file path.");
NSError *error = OWSErrorWithCodeDescription(
OWSErrorCodeInvalidMessage, NSLocalizedString(@"ERROR_MESSAGE_INVALID_MESSAGE", @""));
return failureHandler(error);
}
NSNumber *_Nullable fileSize = [OWSFileSystem fileSizeOfPath:tempFilePath];
if (!fileSize) {
OWSLogError(@"Could not determine attachment file size.");
NSError *error = OWSErrorWithCodeDescription(
OWSErrorCodeInvalidMessage, NSLocalizedString(@"ERROR_MESSAGE_INVALID_MESSAGE", @""));
return failureHandler(error);
}
if (fileSize.unsignedIntegerValue > kMaxDownloadSize) {
OWSLogError(@"Attachment download length exceeds max size.");
NSError *error = OWSErrorWithCodeDescription(
OWSErrorCodeInvalidMessage, NSLocalizedString(@"ERROR_MESSAGE_INVALID_MESSAGE", @""));
return failureHandler(error);
}
successHandler(tempFilePath); successHandler(tempFilePath);
}]; }
[task resume]; NSNumber *_Nullable fileSize = [OWSFileSystem fileSizeOfPath:tempFilePath];
if (!fileSize) {
OWSLogError(@"Could not determine attachment file size.");
NSError *error = OWSErrorWithCodeDescription(
OWSErrorCodeInvalidMessage, NSLocalizedString(@"ERROR_MESSAGE_INVALID_MESSAGE", @""));
return failureHandler(error);
}
if (fileSize.unsignedIntegerValue > kMaxDownloadSize) {
OWSLogError(@"Attachment download length exceeds max size.");
NSError *error = OWSErrorWithCodeDescription(
OWSErrorCodeInvalidMessage, NSLocalizedString(@"ERROR_MESSAGE_INVALID_MESSAGE", @""));
return failureHandler(error);
}
}) retainUntilComplete];
} }
- (void)fireProgressNotification:(CGFloat)progress attachmentId:(NSString *)attachmentId - (void)fireProgressNotification:(CGFloat)progress attachmentId:(NSString *)attachmentId