feat: add push notification
This commit is contained in:
parent
f6e785a894
commit
9001289801
|
@ -16,7 +16,7 @@
|
|||
0F202227261411BB000C64BF /* HashtagTimelineViewController+StatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F202226261411BA000C64BF /* HashtagTimelineViewController+StatusProvider.swift */; };
|
||||
0F20222D261457EE000C64BF /* HashtagTimelineViewModel+LoadOldestState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F20222C261457EE000C64BF /* HashtagTimelineViewModel+LoadOldestState.swift */; };
|
||||
0F20223326145E51000C64BF /* HashtagTimelineViewModel+LoadMiddleState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F20223226145E51000C64BF /* HashtagTimelineViewModel+LoadMiddleState.swift */; };
|
||||
0F20223926146553000C64BF /* Array+removeDuplicates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F20223826146553000C64BF /* Array+removeDuplicates.swift */; };
|
||||
0F20223926146553000C64BF /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F20223826146553000C64BF /* Array.swift */; };
|
||||
0FAA0FDF25E0B57E0017CCDE /* WelcomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FAA0FDE25E0B57E0017CCDE /* WelcomeViewController.swift */; };
|
||||
0FAA101225E105390017CCDE /* PrimaryActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FAA101125E105390017CCDE /* PrimaryActionButton.swift */; };
|
||||
0FAA101C25E10E760017CCDE /* UIFont.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FAA101B25E10E760017CCDE /* UIFont.swift */; };
|
||||
|
@ -160,7 +160,9 @@
|
|||
5DFC35DF262068D20045711D /* SearchViewController+Follow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DFC35DE262068D20045711D /* SearchViewController+Follow.swift */; };
|
||||
5E0DEC05797A7E6933788DDB /* Pods_MastodonTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */; };
|
||||
5E44BF88AD33646E64727BCF /* Pods_MastodonTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */; };
|
||||
7D603B3DC600BB75956FAD7D /* Pods_Mastodon_NotificationService.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79D7EB88A6A8B34DFDFC96FC /* Pods_Mastodon_NotificationService.framework */; };
|
||||
87FFDA5D898A5C42ADCB35E7 /* Pods_Mastodon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */; };
|
||||
DB00CA972632DDB600A54956 /* CommonOSLog in Frameworks */ = {isa = PBXBuildFile; productRef = DB00CA962632DDB600A54956 /* CommonOSLog */; };
|
||||
DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140A025C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift */; };
|
||||
DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140A725C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift */; };
|
||||
DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140AD25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift */; };
|
||||
|
@ -173,6 +175,7 @@
|
|||
DB118A8225E4B6E600FAB162 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */; };
|
||||
DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB118A8B25E4BFB500FAB162 /* HighlightDimmableButton.swift */; };
|
||||
DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1D186B25EF5BA7003F1F23 /* PollTableView.swift */; };
|
||||
DB1E05E1263180F500201847 /* AppSecret.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1E05E0263180F500201847 /* AppSecret.swift */; };
|
||||
DB1E346825F518E20079D7DF /* CategoryPickerSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */; };
|
||||
DB1E347825F519300079D7DF /* PickServerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1E347725F519300079D7DF /* PickServerItem.swift */; };
|
||||
DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FD43525F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift */; };
|
||||
|
@ -220,6 +223,7 @@
|
|||
DB482A3F261331E8008AE74C /* UserTimelineViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB482A3E261331E8008AE74C /* UserTimelineViewModel+State.swift */; };
|
||||
DB482A45261335BA008AE74C /* UserTimelineViewController+StatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB482A44261335BA008AE74C /* UserTimelineViewController+StatusProvider.swift */; };
|
||||
DB482A4B261340A7008AE74C /* APIService+UserTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB482A4A261340A7008AE74C /* APIService+UserTimeline.swift */; };
|
||||
DB4924E226312AB200E9DB22 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4924E126312AB200E9DB22 /* NotificationService.swift */; };
|
||||
DB49A61425FF2C5600B98345 /* EmojiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A61325FF2C5600B98345 /* EmojiService.swift */; };
|
||||
DB49A61F25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A61E25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift */; };
|
||||
DB49A62525FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A62425FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift */; };
|
||||
|
@ -246,6 +250,10 @@
|
|||
DB6B35182601FA3400DC1E11 /* MastodonAttachmentService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B35172601FA3400DC1E11 /* MastodonAttachmentService.swift */; };
|
||||
DB6B351E2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift */; };
|
||||
DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */; };
|
||||
DB6D9F232635195E008423CD /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F222635195E008423CD /* String.swift */; };
|
||||
DB6D9F3526351B7A008423CD /* NotificationService+Decrypt.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F3426351B7A008423CD /* NotificationService+Decrypt.swift */; };
|
||||
DB6D9F3B26352019008423CD /* AppSecret.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1E05E0263180F500201847 /* AppSecret.swift */; };
|
||||
DB6D9F42263527CE008423CD /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB6D9F41263527CE008423CD /* AlamofireImage */; };
|
||||
DB71FD2C25F86A5100512AE1 /* AvatarStackContainerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD2B25F86A5100512AE1 /* AvatarStackContainerButton.swift */; };
|
||||
DB71FD3625F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD3525F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift */; };
|
||||
DB71FD3C25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD3B25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift */; };
|
||||
|
@ -367,6 +375,10 @@
|
|||
DBE3CE13261D7D4200430CC6 /* StatusTableViewControllerAspect.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE3CE12261D7D4200430CC6 /* StatusTableViewControllerAspect.swift */; };
|
||||
DBE64A8B260C49D200E6359A /* TwitterTextEditor in Frameworks */ = {isa = PBXBuildFile; productRef = DBE64A8A260C49D200E6359A /* TwitterTextEditor */; };
|
||||
DBE64A8C260C49D200E6359A /* TwitterTextEditor in Embed Frameworks */ = {isa = PBXBuildFile; productRef = DBE64A8A260C49D200E6359A /* TwitterTextEditor */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
||||
DBF8AE16263293E400C9C23C /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF8AE15263293E400C9C23C /* NotificationService.swift */; };
|
||||
DBF8AE1A263293E400C9C23C /* NotificationService.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = DBF8AE13263293E400C9C23C /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
DBF8AE862632992800C9C23C /* Base85 in Frameworks */ = {isa = PBXBuildFile; productRef = DBF8AE852632992800C9C23C /* Base85 */; };
|
||||
DBF96326262EC0A6001D8D25 /* AuthenticationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DBF96325262EC0A6001D8D25 /* AuthenticationServices.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
|
@ -405,6 +417,13 @@
|
|||
remoteGlobalIDString = DB89B9ED25C10FD0008580ED;
|
||||
remoteInfo = CoreDataStack;
|
||||
};
|
||||
DBF8AE18263293E400C9C23C /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = DB427DCA25BAA00100D1B89D /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = DBF8AE12263293E400C9C23C;
|
||||
remoteInfo = NotificationService;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
|
@ -420,6 +439,17 @@
|
|||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
DBF8AE1B263293E400C9C23C /* Embed App Extensions */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 13;
|
||||
files = (
|
||||
DBF8AE1A263293E400C9C23C /* NotificationService.appex in Embed App Extensions */,
|
||||
);
|
||||
name = "Embed App Extensions";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
|
@ -432,7 +462,7 @@
|
|||
0F202226261411BA000C64BF /* HashtagTimelineViewController+StatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HashtagTimelineViewController+StatusProvider.swift"; sourceTree = "<group>"; };
|
||||
0F20222C261457EE000C64BF /* HashtagTimelineViewModel+LoadOldestState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HashtagTimelineViewModel+LoadOldestState.swift"; sourceTree = "<group>"; };
|
||||
0F20223226145E51000C64BF /* HashtagTimelineViewModel+LoadMiddleState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HashtagTimelineViewModel+LoadMiddleState.swift"; sourceTree = "<group>"; };
|
||||
0F20223826146553000C64BF /* Array+removeDuplicates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+removeDuplicates.swift"; sourceTree = "<group>"; };
|
||||
0F20223826146553000C64BF /* Array.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Array.swift; sourceTree = "<group>"; };
|
||||
0FAA0FDE25E0B57E0017CCDE /* WelcomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeViewController.swift; sourceTree = "<group>"; };
|
||||
0FAA101125E105390017CCDE /* PrimaryActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryActionButton.swift; sourceTree = "<group>"; };
|
||||
0FAA101B25E10E760017CCDE /* UIFont.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIFont.swift; sourceTree = "<group>"; };
|
||||
|
@ -574,8 +604,11 @@
|
|||
5DF1058425F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NeedsDependency+AVPlayerViewControllerDelegate.swift"; sourceTree = "<group>"; };
|
||||
5DFC35DE262068D20045711D /* SearchViewController+Follow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchViewController+Follow.swift"; sourceTree = "<group>"; };
|
||||
75E3471C898DDD9631729B6E /* Pods-Mastodon.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.release.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.release.xcconfig"; sourceTree = "<group>"; };
|
||||
79D7EB88A6A8B34DFDFC96FC /* Pods_Mastodon_NotificationService.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon_NotificationService.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
8ED8C4B1F1BA2DCFF2926BB1 /* Pods-Mastodon-NotificationService.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-NotificationService.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon-NotificationService/Pods-Mastodon-NotificationService.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
9780A4C98FFC65B32B50D1C0 /* Pods-MastodonTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.release.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||
A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
B44342AC2B6585F8295F1DDF /* Pods-Mastodon-NotificationService.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-NotificationService.release.xcconfig"; path = "Target Support Files/Pods-Mastodon-NotificationService/Pods-Mastodon-NotificationService.release.xcconfig"; sourceTree = "<group>"; };
|
||||
BB482D32A7B9825BF5327C4F /* Pods-Mastodon-MastodonUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-MastodonUITests.release.xcconfig"; path = "Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests.release.xcconfig"; sourceTree = "<group>"; };
|
||||
CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MastodonTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
DB0140A025C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPinBasedAuthenticationViewController.swift; sourceTree = "<group>"; };
|
||||
|
@ -589,6 +622,7 @@
|
|||
DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||
DB118A8B25E4BFB500FAB162 /* HighlightDimmableButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightDimmableButton.swift; sourceTree = "<group>"; };
|
||||
DB1D186B25EF5BA7003F1F23 /* PollTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollTableView.swift; sourceTree = "<group>"; };
|
||||
DB1E05E0263180F500201847 /* AppSecret.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSecret.swift; sourceTree = "<group>"; };
|
||||
DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CategoryPickerSection.swift; sourceTree = "<group>"; };
|
||||
DB1E347725F519300079D7DF /* PickServerItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PickServerItem.swift; sourceTree = "<group>"; };
|
||||
DB1FD43525F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonPickServerViewModel+LoadIndexedServerState.swift"; sourceTree = "<group>"; };
|
||||
|
@ -643,6 +677,7 @@
|
|||
DB482A3E261331E8008AE74C /* UserTimelineViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserTimelineViewModel+State.swift"; sourceTree = "<group>"; };
|
||||
DB482A44261335BA008AE74C /* UserTimelineViewController+StatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserTimelineViewController+StatusProvider.swift"; sourceTree = "<group>"; };
|
||||
DB482A4A261340A7008AE74C /* APIService+UserTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+UserTimeline.swift"; sourceTree = "<group>"; };
|
||||
DB4924E126312AB200E9DB22 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
|
||||
DB49A61325FF2C5600B98345 /* EmojiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiService.swift; sourceTree = "<group>"; };
|
||||
DB49A61E25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EmojiService+CustomEmojiViewModel.swift"; sourceTree = "<group>"; };
|
||||
DB49A62425FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EmojiService+CustomEmojiViewModel+LoadState.swift"; sourceTree = "<group>"; };
|
||||
|
@ -668,6 +703,8 @@
|
|||
DB6B35172601FA3400DC1E11 /* MastodonAttachmentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonAttachmentService.swift; sourceTree = "<group>"; };
|
||||
DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusAttachmentCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Error.swift"; sourceTree = "<group>"; };
|
||||
DB6D9F222635195E008423CD /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = "<group>"; };
|
||||
DB6D9F3426351B7A008423CD /* NotificationService+Decrypt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationService+Decrypt.swift"; sourceTree = "<group>"; };
|
||||
DB71FD2B25F86A5100512AE1 /* AvatarStackContainerButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarStackContainerButton.swift; sourceTree = "<group>"; };
|
||||
DB71FD3525F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+PersistMemo.swift"; sourceTree = "<group>"; };
|
||||
DB71FD3B25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+PersistCache.swift"; sourceTree = "<group>"; };
|
||||
|
@ -790,6 +827,10 @@
|
|||
DBE3CE12261D7D4200430CC6 /* StatusTableViewControllerAspect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTableViewControllerAspect.swift; sourceTree = "<group>"; };
|
||||
DBF53F5F25C14E88008AAC7B /* Mastodon.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = Mastodon.xctestplan; path = Mastodon/Mastodon.xctestplan; sourceTree = "<group>"; };
|
||||
DBF53F6025C14E9D008AAC7B /* MastodonSDK.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = MastodonSDK.xctestplan; sourceTree = "<group>"; };
|
||||
DBF8AE13263293E400C9C23C /* NotificationService.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotificationService.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
DBF8AE15263293E400C9C23C /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
|
||||
DBF8AE17263293E400C9C23C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
DBF96325262EC0A6001D8D25 /* AuthenticationServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AuthenticationServices.framework; path = System/Library/Frameworks/AuthenticationServices.framework; sourceTree = SDKROOT; };
|
||||
EC6E707B68A67DB08EC288FA /* Pods-MastodonTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.debug.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
|
@ -806,6 +847,7 @@
|
|||
DBB525082611EAC0002F1F29 /* Tabman in Frameworks */,
|
||||
5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */,
|
||||
DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */,
|
||||
DBF96326262EC0A6001D8D25 /* AuthenticationServices.framework in Frameworks */,
|
||||
2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */,
|
||||
DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */,
|
||||
DBE64A8B260C49D200E6359A /* TwitterTextEditor in Frameworks */,
|
||||
|
@ -846,6 +888,17 @@
|
|||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
DBF8AE10263293E400C9C23C /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
DB00CA972632DDB600A54956 /* CommonOSLog in Frameworks */,
|
||||
DB6D9F42263527CE008423CD /* AlamofireImage in Frameworks */,
|
||||
DBF8AE862632992800C9C23C /* Base85 in Frameworks */,
|
||||
7D603B3DC600BB75956FAD7D /* Pods_Mastodon_NotificationService.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
|
@ -924,6 +977,8 @@
|
|||
BB482D32A7B9825BF5327C4F /* Pods-Mastodon-MastodonUITests.release.xcconfig */,
|
||||
EC6E707B68A67DB08EC288FA /* Pods-MastodonTests.debug.xcconfig */,
|
||||
9780A4C98FFC65B32B50D1C0 /* Pods-MastodonTests.release.xcconfig */,
|
||||
8ED8C4B1F1BA2DCFF2926BB1 /* Pods-Mastodon-NotificationService.debug.xcconfig */,
|
||||
B44342AC2B6585F8295F1DDF /* Pods-Mastodon-NotificationService.release.xcconfig */,
|
||||
);
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1053,6 +1108,7 @@
|
|||
5DF1054025F886D400D6C0D4 /* ViedeoPlaybackService.swift */,
|
||||
DB71FD4B25F8C80E00512AE1 /* StatusPrefetchingService.swift */,
|
||||
DBC7A67B260DFADE00E57475 /* StatusPublishService.swift */,
|
||||
DB4924E126312AB200E9DB22 /* NotificationService.swift */,
|
||||
);
|
||||
path = Service;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1216,9 +1272,11 @@
|
|||
3FE14AD363ED19AE7FF210A6 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DBF96325262EC0A6001D8D25 /* AuthenticationServices.framework */,
|
||||
A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */,
|
||||
3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */,
|
||||
452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */,
|
||||
79D7EB88A6A8B34DFDFC96FC /* Pods_Mastodon_NotificationService.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1318,6 +1376,7 @@
|
|||
children = (
|
||||
DB427DD525BAA00100D1B89D /* AppDelegate.swift */,
|
||||
DB427DD725BAA00100D1B89D /* SceneDelegate.swift */,
|
||||
DB1E05E0263180F500201847 /* AppSecret.swift */,
|
||||
DB427DDB25BAA00100D1B89D /* Main.storyboard */,
|
||||
DB427DE025BAA00100D1B89D /* LaunchScreen.storyboard */,
|
||||
DB68A05C25E9055900CFDF14 /* Settings.bundle */,
|
||||
|
@ -1347,6 +1406,7 @@
|
|||
DB427DF625BAA00100D1B89D /* MastodonUITests */,
|
||||
DB89B9EF25C10FD0008580ED /* CoreDataStack */,
|
||||
DB89B9FC25C10FD0008580ED /* CoreDataStackTests */,
|
||||
DBF8AE14263293E400C9C23C /* NotificationService */,
|
||||
DB427DD325BAA00100D1B89D /* Products */,
|
||||
1EBA4F56E920856A3FC84ACB /* Pods */,
|
||||
3FE14AD363ED19AE7FF210A6 /* Frameworks */,
|
||||
|
@ -1362,6 +1422,7 @@
|
|||
DB427DF325BAA00100D1B89D /* MastodonUITests.xctest */,
|
||||
DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */,
|
||||
DB89B9F625C10FD0008580ED /* CoreDataStackTests.xctest */,
|
||||
DBF8AE13263293E400C9C23C /* NotificationService.appex */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1516,6 +1577,14 @@
|
|||
path = MastodonSDK;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB6D9F2926351961008423CD /* Extension */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DB6D9F222635195E008423CD /* String.swift */,
|
||||
);
|
||||
path = Extension;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB72602125E36A2500235243 /* ServerRules */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -1715,7 +1784,7 @@
|
|||
2D3F9E0325DFA133004262D9 /* UITapGestureRecognizer.swift */,
|
||||
2D84350425FF858100EECE90 /* UIScrollView.swift */,
|
||||
DB9E0D6E25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift */,
|
||||
0F20223826146553000C64BF /* Array+removeDuplicates.swift */,
|
||||
0F20223826146553000C64BF /* Array.swift */,
|
||||
DBCC3B2F261440A50045B23D /* UITabBarController.swift */,
|
||||
DBCC3B35261440BA0045B23D /* UINavigationController.swift */,
|
||||
);
|
||||
|
@ -1951,6 +2020,17 @@
|
|||
path = Favorite;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DBF8AE14263293E400C9C23C /* NotificationService */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DBF8AE15263293E400C9C23C /* NotificationService.swift */,
|
||||
DB6D9F3426351B7A008423CD /* NotificationService+Decrypt.swift */,
|
||||
DB6D9F2926351961008423CD /* Extension */,
|
||||
DBF8AE17263293E400C9C23C /* Info.plist */,
|
||||
);
|
||||
path = NotificationService;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXHeadersBuildPhase section */
|
||||
|
@ -1976,11 +2056,13 @@
|
|||
DB427DD025BAA00100D1B89D /* Resources */,
|
||||
5532CB85BBE168B25B20720B /* [CP] Embed Pods Frameworks */,
|
||||
DB89BA0825C10FD0008580ED /* Embed Frameworks */,
|
||||
DBF8AE1B263293E400C9C23C /* Embed App Extensions */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
DB89BA0225C10FD0008580ED /* PBXTargetDependency */,
|
||||
DBF8AE19263293E400C9C23C /* PBXTargetDependency */,
|
||||
);
|
||||
name = Mastodon;
|
||||
packageProductDependencies = (
|
||||
|
@ -2076,6 +2158,29 @@
|
|||
productReference = DB89B9F625C10FD0008580ED /* CoreDataStackTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
DBF8AE12263293E400C9C23C /* NotificationService */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = DBF8AE1E263293E400C9C23C /* Build configuration list for PBXNativeTarget "NotificationService" */;
|
||||
buildPhases = (
|
||||
0DC740704503CA6BED56F5C8 /* [CP] Check Pods Manifest.lock */,
|
||||
DBF8AE0F263293E400C9C23C /* Sources */,
|
||||
DBF8AE10263293E400C9C23C /* Frameworks */,
|
||||
DBF8AE11263293E400C9C23C /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = NotificationService;
|
||||
packageProductDependencies = (
|
||||
DBF8AE852632992800C9C23C /* Base85 */,
|
||||
DB00CA962632DDB600A54956 /* CommonOSLog */,
|
||||
DB6D9F41263527CE008423CD /* AlamofireImage */,
|
||||
);
|
||||
productName = NotificationService;
|
||||
productReference = DBF8AE13263293E400C9C23C /* NotificationService.appex */;
|
||||
productType = "com.apple.product-type.app-extension";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
|
@ -2105,6 +2210,9 @@
|
|||
CreatedOnToolsVersion = 12.4;
|
||||
TestTargetID = DB427DD125BAA00100D1B89D;
|
||||
};
|
||||
DBF8AE12263293E400C9C23C = {
|
||||
CreatedOnToolsVersion = 12.4;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = DB427DCD25BAA00100D1B89D /* Build configuration list for PBXProject "Mastodon" */;
|
||||
|
@ -2127,6 +2235,7 @@
|
|||
DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */,
|
||||
DBE64A89260C49D200E6359A /* XCRemoteSwiftPackageReference "TwitterTextEditor" */,
|
||||
DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */,
|
||||
DBF8AE842632992700C9C23C /* XCRemoteSwiftPackageReference "Base85" */,
|
||||
);
|
||||
productRefGroup = DB427DD325BAA00100D1B89D /* Products */;
|
||||
projectDirPath = "";
|
||||
|
@ -2137,6 +2246,7 @@
|
|||
DB427DF225BAA00100D1B89D /* MastodonUITests */,
|
||||
DB89B9ED25C10FD0008580ED /* CoreDataStack */,
|
||||
DB89B9F525C10FD0008580ED /* CoreDataStackTests */,
|
||||
DBF8AE12263293E400C9C23C /* NotificationService */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
@ -2184,9 +2294,38 @@
|
|||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
DBF8AE11263293E400C9C23C /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
0DC740704503CA6BED56F5C8 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-Mastodon-NotificationService-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
5532CB85BBE168B25B20720B /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
|
@ -2551,7 +2690,7 @@
|
|||
DBA0A11325FB3FC10079C110 /* ComposeToolbarView.swift in Sources */,
|
||||
2D32EADA25CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift in Sources */,
|
||||
5B90C48526259BF10002E742 /* APIService+Subscriptions.swift in Sources */,
|
||||
0F20223926146553000C64BF /* Array+removeDuplicates.swift in Sources */,
|
||||
0F20223926146553000C64BF /* Array.swift in Sources */,
|
||||
5B90C460262599800002E742 /* SettingsAppearanceTableViewCell.swift in Sources */,
|
||||
DB8AF54425C13647002E6C99 /* SceneCoordinator.swift in Sources */,
|
||||
5DF1058525F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift in Sources */,
|
||||
|
@ -2594,6 +2733,7 @@
|
|||
DBB525302611EBF3002F1F29 /* ProfilePagingViewModel.swift in Sources */,
|
||||
2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */,
|
||||
DB49A62525FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift in Sources */,
|
||||
DB4924E226312AB200E9DB22 /* NotificationService.swift in Sources */,
|
||||
DBB525412611ED54002F1F29 /* ProfileHeaderViewController.swift in Sources */,
|
||||
DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */,
|
||||
0FB3D30825E524C600AAD544 /* PickServerCategoriesCell.swift in Sources */,
|
||||
|
@ -2609,6 +2749,7 @@
|
|||
2D6DE40026141DF600A63F6A /* SearchViewModel.swift in Sources */,
|
||||
DB51D172262832380062B7A1 /* BlurHashDecode.swift in Sources */,
|
||||
DBCC3B89261454BA0045B23D /* CGImage.swift in Sources */,
|
||||
DB1E05E1263180F500201847 /* AppSecret.swift in Sources */,
|
||||
DBCCC71E25F73297007E1AB6 /* APIService+Reblog.swift in Sources */,
|
||||
DB789A2B25F9F7AB0071ACA0 /* ComposeRepliedToStatusContentCollectionViewCell.swift in Sources */,
|
||||
DBE3CE13261D7D4200430CC6 /* StatusTableViewControllerAspect.swift in Sources */,
|
||||
|
@ -2675,6 +2816,17 @@
|
|||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
DBF8AE0F263293E400C9C23C /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
DB6D9F232635195E008423CD /* String.swift in Sources */,
|
||||
DB6D9F3B26352019008423CD /* AppSecret.swift in Sources */,
|
||||
DB6D9F3526351B7A008423CD /* NotificationService+Decrypt.swift in Sources */,
|
||||
DBF8AE16263293E400C9C23C /* NotificationService.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
|
@ -2703,6 +2855,11 @@
|
|||
target = DB89B9ED25C10FD0008580ED /* CoreDataStack */;
|
||||
targetProxy = DB89BA0125C10FD0008580ED /* PBXContainerItemProxy */;
|
||||
};
|
||||
DBF8AE19263293E400C9C23C /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = DBF8AE12263293E400C9C23C /* NotificationService */;
|
||||
targetProxy = DBF8AE18263293E400C9C23C /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
|
@ -2861,6 +3018,7 @@
|
|||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 2E1F6A67FDF9771D3E064FDC /* Pods-Mastodon.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
|
@ -2888,6 +3046,7 @@
|
|||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 75E3471C898DDD9631729B6E /* Pods-Mastodon.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
|
@ -3089,6 +3248,48 @@
|
|||
};
|
||||
name = Release;
|
||||
};
|
||||
DBF8AE1C263293E400C9C23C /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 8ED8C4B1F1BA2DCFF2926BB1 /* Pods-Mastodon-NotificationService.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = 7LFDZ96332;
|
||||
INFOPLIST_FILE = NotificationService/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.4;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.Mastodon.NotificationService;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
DBF8AE1D263293E400C9C23C /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = B44342AC2B6585F8295F1DDF /* Pods-Mastodon-NotificationService.release.xcconfig */;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = 7LFDZ96332;
|
||||
INFOPLIST_FILE = NotificationService/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.4;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.Mastodon.NotificationService;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
|
@ -3146,6 +3347,15 @@
|
|||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
DBF8AE1E263293E400C9C23C /* Build configuration list for PBXNativeTarget "NotificationService" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
DBF8AE1C263293E400C9C23C /* Debug */,
|
||||
DBF8AE1D263293E400C9C23C /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
|
@ -3229,6 +3439,14 @@
|
|||
kind = branch;
|
||||
};
|
||||
};
|
||||
DBF8AE842632992700C9C23C /* XCRemoteSwiftPackageReference "Base85" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/MainasuK/Base85.git";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 1.0.1;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
|
@ -3256,6 +3474,11 @@
|
|||
isa = XCSwiftPackageProductDependency;
|
||||
productName = MastodonSDK;
|
||||
};
|
||||
DB00CA962632DDB600A54956 /* CommonOSLog */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */;
|
||||
productName = CommonOSLog;
|
||||
};
|
||||
DB0140BC25C40D7500F9F3CF /* CommonOSLog */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */;
|
||||
|
@ -3271,6 +3494,11 @@
|
|||
package = DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */;
|
||||
productName = Kingfisher;
|
||||
};
|
||||
DB6D9F41263527CE008423CD /* AlamofireImage */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */;
|
||||
productName = AlamofireImage;
|
||||
};
|
||||
DB9A487D2603456B008B817C /* UITextView+Placeholder */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */;
|
||||
|
@ -3286,6 +3514,11 @@
|
|||
package = DBE64A89260C49D200E6359A /* XCRemoteSwiftPackageReference "TwitterTextEditor" */;
|
||||
productName = TwitterTextEditor;
|
||||
};
|
||||
DBF8AE852632992800C9C23C /* Base85 */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = DBF8AE842632992700C9C23C /* XCRemoteSwiftPackageReference "Base85" */;
|
||||
productName = Base85;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
|
||||
/* Begin XCVersionGroup section */
|
||||
|
|
|
@ -7,22 +7,27 @@
|
|||
<key>CoreDataStack.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>13</integer>
|
||||
<integer>14</integer>
|
||||
</dict>
|
||||
<key>Mastodon - RTL.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>9</integer>
|
||||
<integer>2</integer>
|
||||
</dict>
|
||||
<key>Mastodon - Release.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>2</integer>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
<key>Mastodon.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>7</integer>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
<key>NotificationService.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>17</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>SuppressBuildableAutocreation</key>
|
||||
|
|
|
@ -37,6 +37,15 @@
|
|||
"version": "3.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Base85",
|
||||
"repositoryURL": "https://github.com/MainasuK/Base85.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "626be96816618689627f806b5c875b5adb6346ef",
|
||||
"version": "1.0.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "CommonOSLog",
|
||||
"repositoryURL": "https://github.com/MainasuK/CommonOSLog",
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
//
|
||||
// Array+removeDuplicates.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by BradGao on 2021/3/31.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// https://www.hackingwithswift.com/example-code/language/how-to-remove-duplicate-items-from-an-array
|
||||
extension Array where Element: Hashable {
|
||||
func removingDuplicates() -> [Element] {
|
||||
var addedDict = [Element: Bool]()
|
||||
|
||||
return filter {
|
||||
addedDict.updateValue(true, forKey: $0) == nil
|
||||
}
|
||||
}
|
||||
|
||||
mutating func removeDuplicates() {
|
||||
self = self.removingDuplicates()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
//
|
||||
// Array.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by BradGao on 2021/3/31.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// https://www.hackingwithswift.com/example-code/language/how-to-remove-duplicate-items-from-an-array
|
||||
extension Array where Element: Hashable {
|
||||
func removingDuplicates() -> [Element] {
|
||||
var addedDict = [Element: Bool]()
|
||||
|
||||
return filter {
|
||||
addedDict.updateValue(true, forKey: $0) == nil
|
||||
}
|
||||
}
|
||||
|
||||
mutating func removeDuplicates() {
|
||||
self = self.removingDuplicates()
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// CryptoSwift
|
||||
//
|
||||
// Copyright (C) 2014-2017 Marcin Krzyżanowski <marcin@krzyzanowskim.com>
|
||||
// This software is provided 'as-is', without any express or implied warranty.
|
||||
//
|
||||
// In no event will the authors be held liable for any damages arising from the use of this software.
|
||||
//
|
||||
// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions:
|
||||
//
|
||||
// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required.
|
||||
// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.
|
||||
// - This notice may not be removed or altered from any source or binary distribution.
|
||||
//
|
||||
|
||||
extension Array {
|
||||
init(reserveCapacity: Int) {
|
||||
self = Array<Element>()
|
||||
self.reserveCapacity(reserveCapacity)
|
||||
}
|
||||
|
||||
var slice: ArraySlice<Element> {
|
||||
self[self.startIndex ..< self.endIndex]
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == UInt8 {
|
||||
public init(hex: String) {
|
||||
self.init(reserveCapacity: hex.unicodeScalars.lazy.underestimatedCount)
|
||||
var buffer: UInt8?
|
||||
var skip = hex.hasPrefix("0x") ? 2 : 0
|
||||
for char in hex.unicodeScalars.lazy {
|
||||
guard skip == 0 else {
|
||||
skip -= 1
|
||||
continue
|
||||
}
|
||||
guard char.value >= 48 && char.value <= 102 else {
|
||||
removeAll()
|
||||
return
|
||||
}
|
||||
let v: UInt8
|
||||
let c: UInt8 = UInt8(char.value)
|
||||
switch c {
|
||||
case let c where c <= 57:
|
||||
v = c - 48
|
||||
case let c where c >= 65 && c <= 70:
|
||||
v = c - 55
|
||||
case let c where c >= 97:
|
||||
v = c - 87
|
||||
default:
|
||||
removeAll()
|
||||
return
|
||||
}
|
||||
if let b = buffer {
|
||||
append(b << 4 | v)
|
||||
buffer = nil
|
||||
} else {
|
||||
buffer = v
|
||||
}
|
||||
}
|
||||
if let b = buffer {
|
||||
append(b)
|
||||
}
|
||||
}
|
||||
|
||||
public func toHexString() -> String {
|
||||
`lazy`.reduce(into: "") {
|
||||
var s = String($1, radix: 16)
|
||||
if s.count == 1 {
|
||||
s = "0" + s
|
||||
}
|
||||
$0 += s
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,6 +2,8 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.org.joinmastodon.mastodon-temp</string>
|
||||
|
|
|
@ -55,7 +55,7 @@ class SettingsViewController: UIViewController, NeedsDependency {
|
|||
let view = UIStackView()
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.isLayoutMarginsRelativeArrangement = true
|
||||
view.layoutMargins = UIEdgeInsets(top: 15, left: 4, bottom: 5, right: 4)
|
||||
//view.layoutMargins = UIEdgeInsets(top: 15, left: 4, bottom: 5, right: 4)
|
||||
view.axis = .horizontal
|
||||
view.alignment = .fill
|
||||
view.distribution = .equalSpacing
|
||||
|
@ -270,8 +270,9 @@ extension SettingsViewController: UITableViewDelegate {
|
|||
guard section < sections.count else { return nil }
|
||||
let sectionData = sections[section]
|
||||
|
||||
let header: SettingsSectionHeader
|
||||
if section == 1 {
|
||||
let header = SettingsSectionHeader(
|
||||
header = SettingsSectionHeader(
|
||||
frame: CGRect(x: 0, y: 0, width: 375, height: 66),
|
||||
customView: notifySectionHeader)
|
||||
header.update(title: sectionData.title)
|
||||
|
@ -282,16 +283,19 @@ extension SettingsViewController: UITableViewDelegate {
|
|||
let anyone = L10n.Scene.Settings.Section.Notifications.Trigger.anyone
|
||||
whoButton.setTitle(anyone, for: .normal)
|
||||
}
|
||||
return header
|
||||
} else {
|
||||
let header = SettingsSectionHeader(frame: CGRect(x: 0, y: 0, width: 375, height: 66))
|
||||
header = SettingsSectionHeader(frame: CGRect(x: 0, y: 0, width: 375, height: 66))
|
||||
header.update(title: sectionData.title)
|
||||
return header
|
||||
}
|
||||
|
||||
header.preservesSuperviewLayoutMargins = true
|
||||
|
||||
return header
|
||||
}
|
||||
|
||||
// remove the gap of table's footer
|
||||
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
|
||||
|
||||
return UIView()
|
||||
}
|
||||
|
||||
|
|
|
@ -96,49 +96,49 @@ class SettingsViewModel: NSObject, NeedsDependency {
|
|||
|
||||
func transform(input: Input?) -> Output? {
|
||||
typealias SubscriptionResponse = Mastodon.Response.Content<Mastodon.Entity.Subscription>
|
||||
createSubscriptionSubject
|
||||
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
|
||||
.sink { _ in
|
||||
} receiveValue: { [weak self] (arg) in
|
||||
let (triggerBy, values) = arg
|
||||
guard let self = self else {
|
||||
return
|
||||
}
|
||||
guard let activeMastodonAuthenticationBox =
|
||||
self.context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||
return
|
||||
}
|
||||
guard values.count >= 4 else {
|
||||
return
|
||||
}
|
||||
|
||||
self.createDisposeBag.removeAll()
|
||||
typealias Query = Mastodon.API.Subscriptions.CreateSubscriptionQuery
|
||||
let domain = activeMastodonAuthenticationBox.domain
|
||||
let query = Query(
|
||||
// FIXME: to replace the correct endpoint, p256dh, auth
|
||||
endpoint: "http://www.google.com",
|
||||
p256dh: "BLQELIDm-6b9Bl07YrEuXJ4BL_YBVQ0dvt9NQGGJxIQidJWHPNa9YrouvcQ9d7_MqzvGS9Alz60SZNCG3qfpk=",
|
||||
auth: "4vQK-SvRAN5eo-8ASlrwA==",
|
||||
favourite: values[0],
|
||||
follow: values[1],
|
||||
reblog: values[2],
|
||||
mention: values[3],
|
||||
poll: nil
|
||||
)
|
||||
self.context.apiService.changeSubscription(
|
||||
domain: domain,
|
||||
mastodonAuthenticationBox: activeMastodonAuthenticationBox,
|
||||
query: query,
|
||||
triggerBy: triggerBy,
|
||||
userID: activeMastodonAuthenticationBox.userID
|
||||
)
|
||||
.sink { (_) in
|
||||
} receiveValue: { (_) in
|
||||
}
|
||||
.store(in: &self.createDisposeBag)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
// createSubscriptionSubject
|
||||
// .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
|
||||
// .sink { _ in
|
||||
// } receiveValue: { [weak self] (arg) in
|
||||
// let (triggerBy, values) = arg
|
||||
// guard let self = self else {
|
||||
// return
|
||||
// }
|
||||
// guard let activeMastodonAuthenticationBox =
|
||||
// self.context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||
// return
|
||||
// }
|
||||
// guard values.count >= 4 else {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// self.createDisposeBag.removeAll()
|
||||
// typealias Query = Mastodon.API.Subscriptions.CreateSubscriptionQuery
|
||||
// let domain = activeMastodonAuthenticationBox.domain
|
||||
// let query = Query(
|
||||
// // FIXME: to replace the correct endpoint, p256dh, auth
|
||||
// endpoint: "http://www.google.com",
|
||||
// p256dh: "BLQELIDm-6b9Bl07YrEuXJ4BL_YBVQ0dvt9NQGGJxIQidJWHPNa9YrouvcQ9d7_MqzvGS9Alz60SZNCG3qfpk=",
|
||||
// auth: "4vQK-SvRAN5eo-8ASlrwA==",
|
||||
// favourite: values[0],
|
||||
// follow: values[1],
|
||||
// reblog: values[2],
|
||||
// mention: values[3],
|
||||
// poll: nil
|
||||
// )
|
||||
// self.context.apiService.changeSubscription(
|
||||
// domain: domain,
|
||||
// mastodonAuthenticationBox: activeMastodonAuthenticationBox,
|
||||
// query: query,
|
||||
// triggerBy: triggerBy,
|
||||
// userID: activeMastodonAuthenticationBox.userID
|
||||
// )
|
||||
// .sink { (_) in
|
||||
// } receiveValue: { (_) in
|
||||
// }
|
||||
// .store(in: &self.createDisposeBag)
|
||||
// }
|
||||
// .store(in: &disposeBag)
|
||||
|
||||
updateSubscriptionSubject
|
||||
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
|
||||
|
@ -160,11 +160,16 @@ class SettingsViewModel: NSObject, NeedsDependency {
|
|||
typealias Query = Mastodon.API.Subscriptions.UpdateSubscriptionQuery
|
||||
let domain = activeMastodonAuthenticationBox.domain
|
||||
let query = Query(
|
||||
favourite: values[0],
|
||||
follow: values[1],
|
||||
reblog: values[2],
|
||||
mention: values[3],
|
||||
poll: nil)
|
||||
data: Mastodon.API.Subscriptions.QueryData(
|
||||
alerts: Mastodon.API.Subscriptions.QueryData.Alerts(
|
||||
favourite: values[0],
|
||||
follow: values[1],
|
||||
reblog: values[2],
|
||||
mention: values[3],
|
||||
poll: nil
|
||||
)
|
||||
)
|
||||
)
|
||||
self.context.apiService.updateSubscription(
|
||||
domain: domain,
|
||||
mastodonAuthenticationBox: activeMastodonAuthenticationBox,
|
||||
|
|
|
@ -14,11 +14,11 @@ import MastodonSDK
|
|||
extension APIService {
|
||||
|
||||
func subscription(
|
||||
domain: String,
|
||||
userID: String,
|
||||
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Subscription>, Error> {
|
||||
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||
let domain = mastodonAuthenticationBox.domain
|
||||
let userID = mastodonAuthenticationBox.userID
|
||||
|
||||
let findSettings: Setting? = {
|
||||
let request = Setting.sortedFetchRequest
|
||||
|
@ -58,18 +58,21 @@ extension APIService {
|
|||
}.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func changeSubscription(
|
||||
domain: String,
|
||||
func createSubscription(
|
||||
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox,
|
||||
query: Mastodon.API.Subscriptions.CreateSubscriptionQuery,
|
||||
triggerBy: String,
|
||||
userID: String
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Subscription>, Error> {
|
||||
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||
let domain = mastodonAuthenticationBox.domain
|
||||
let userID = mastodonAuthenticationBox.userID
|
||||
|
||||
let setting = self.createSettingIfNeed(domain: domain,
|
||||
userId: userID,
|
||||
triggerBy: triggerBy)
|
||||
let setting = self.createSettingIfNeed(
|
||||
domain: domain,
|
||||
userId: userID,
|
||||
triggerBy: triggerBy
|
||||
)
|
||||
return Mastodon.API.Subscriptions.createSubscription(
|
||||
session: session,
|
||||
domain: domain,
|
||||
|
|
|
@ -0,0 +1,241 @@
|
|||
//
|
||||
// NotificationService.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-4-22.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
|
||||
final class NotificationService {
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
let workingQueue = DispatchQueue(label: "org.joinmastodon.Mastodon.NotificationService.working-queue")
|
||||
|
||||
// input
|
||||
weak var apiService: APIService?
|
||||
weak var authenticationService: AuthenticationService?
|
||||
let isNotificationPermissionGranted = CurrentValueSubject<Bool, Never>(false)
|
||||
let deviceToken = CurrentValueSubject<Data?, Never>(nil)
|
||||
let mastodonAuthenticationBoxes = CurrentValueSubject<[AuthenticationService.MastodonAuthenticationBox], Never>([])
|
||||
|
||||
// output
|
||||
/// [Token: UserID]
|
||||
let notificationSubscriptionDict: [String: NotificationSubscription] = [:]
|
||||
|
||||
init(
|
||||
apiService: APIService,
|
||||
authenticationService: AuthenticationService
|
||||
) {
|
||||
self.apiService = apiService
|
||||
self.authenticationService = authenticationService
|
||||
|
||||
authenticationService.mastodonAuthentications
|
||||
.handleEvents(receiveOutput: { [weak self] mastodonAuthentications in
|
||||
guard let self = self else { return }
|
||||
|
||||
// request permission when sign-in
|
||||
guard !mastodonAuthentications.isEmpty else { return }
|
||||
self.requestNotificationPermission()
|
||||
})
|
||||
.map { authentications -> [AuthenticationService.MastodonAuthenticationBox] in
|
||||
return authentications.compactMap { authentication -> AuthenticationService.MastodonAuthenticationBox? in
|
||||
return AuthenticationService.MastodonAuthenticationBox(
|
||||
domain: authentication.domain,
|
||||
userID: authentication.userID,
|
||||
appAuthorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.appAccessToken),
|
||||
userAuthorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.userAccessToken)
|
||||
)
|
||||
}
|
||||
}
|
||||
.assign(to: \.value, on: mastodonAuthenticationBoxes)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
deviceToken
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] deviceToken in
|
||||
guard let self = self else { return }
|
||||
guard let deviceToken = deviceToken else { return }
|
||||
let token = [UInt8](deviceToken).toHexString()
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: deviceToken: %s", ((#file as NSString).lastPathComponent), #line, #function, token)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
Publishers.CombineLatest3(
|
||||
isNotificationPermissionGranted,
|
||||
deviceToken,
|
||||
mastodonAuthenticationBoxes
|
||||
)
|
||||
.sink { [weak self] isNotificationPermissionGranted, deviceToken, mastodonAuthenticationBoxes in
|
||||
guard let self = self else { return }
|
||||
guard isNotificationPermissionGranted else { return }
|
||||
guard let deviceToken = deviceToken else { return }
|
||||
self.registerNotificationSubscriptions(
|
||||
deviceToken: [UInt8](deviceToken).toHexString(),
|
||||
mastodonAuthenticationBoxes: mastodonAuthenticationBoxes
|
||||
)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension NotificationService {
|
||||
private func requestNotificationPermission() {
|
||||
let center = UNUserNotificationCenter.current()
|
||||
center.requestAuthorization(options: [.alert, .sound, .badge]) { [weak self] granted, error in
|
||||
guard let self = self else { return }
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: request notification permission: %s", ((#file as NSString).lastPathComponent), #line, #function, granted ? "granted" : "fail")
|
||||
|
||||
self.isNotificationPermissionGranted.value = granted
|
||||
|
||||
if let _ = error {
|
||||
// Handle the error here.
|
||||
}
|
||||
|
||||
// Enable or disable features based on the authorization.
|
||||
}
|
||||
}
|
||||
|
||||
private func registerNotificationSubscriptions(
|
||||
deviceToken: String,
|
||||
mastodonAuthenticationBoxes: [AuthenticationService.MastodonAuthenticationBox]
|
||||
) {
|
||||
for mastodonAuthenticationBox in mastodonAuthenticationBoxes {
|
||||
guard let notificationSubscription = dequeueNotificationSubscription(mastodonAuthenticationBox: mastodonAuthenticationBox) else { continue }
|
||||
let token = NotificationSubscription.SubscribeToken(
|
||||
deviceToken: deviceToken,
|
||||
authenticationBox: mastodonAuthenticationBox
|
||||
)
|
||||
guard let subscription = subscribe(
|
||||
notificationSubscription: notificationSubscription,
|
||||
token: token
|
||||
) else { continue }
|
||||
|
||||
subscription
|
||||
.sink { completion in
|
||||
// handle error
|
||||
} receiveValue: { response in
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: did create subscription %s with userToken %s", ((#file as NSString).lastPathComponent), #line, #function, response.value.id, mastodonAuthenticationBox.userAuthorization.accessToken)
|
||||
// do nothing
|
||||
}
|
||||
.store(in: &self.disposeBag)
|
||||
}
|
||||
}
|
||||
|
||||
private func dequeueNotificationSubscription(mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox) -> NotificationSubscription? {
|
||||
var _notificationSubscription: NotificationSubscription?
|
||||
workingQueue.sync {
|
||||
let domain = mastodonAuthenticationBox.domain
|
||||
let userID = mastodonAuthenticationBox.userID
|
||||
let key = [domain, userID].joined(separator: "@")
|
||||
|
||||
if let notificationSubscription = notificationSubscriptionDict[key] {
|
||||
_notificationSubscription = notificationSubscription
|
||||
} else {
|
||||
let notificationSubscription = NotificationSubscription(domain: domain, userID: userID)
|
||||
_notificationSubscription = notificationSubscription
|
||||
}
|
||||
}
|
||||
return _notificationSubscription
|
||||
}
|
||||
|
||||
private func subscribe(
|
||||
notificationSubscription: NotificationSubscription,
|
||||
token: NotificationSubscription.SubscribeToken
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Subscription>, Error>? {
|
||||
guard let apiService = self.apiService else { return nil }
|
||||
|
||||
if let oldToken = notificationSubscription.token {
|
||||
guard oldToken != token else { return nil }
|
||||
}
|
||||
notificationSubscription.token = token
|
||||
|
||||
let appSecret = AppSecret.default
|
||||
let endpoint = appSecret.notificationEndpoint + "/" + token.deviceToken
|
||||
let p256dh = appSecret.uncompressionNotificationPublicKeyData
|
||||
let auth = appSecret.notificationAuth
|
||||
|
||||
let query = Mastodon.API.Subscriptions.CreateSubscriptionQuery(
|
||||
subscription: Mastodon.API.Subscriptions.QuerySubscription(
|
||||
endpoint: endpoint,
|
||||
keys: Mastodon.API.Subscriptions.QuerySubscription.Keys(
|
||||
p256dh: p256dh,
|
||||
auth: auth
|
||||
)
|
||||
),
|
||||
data: Mastodon.API.Subscriptions.QueryData(
|
||||
alerts: Mastodon.API.Subscriptions.QueryData.Alerts(
|
||||
favourite: true,
|
||||
follow: true,
|
||||
reblog: true,
|
||||
mention: true,
|
||||
poll: true
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
return apiService.createSubscription(
|
||||
mastodonAuthenticationBox: token.authenticationBox,
|
||||
query: query,
|
||||
triggerBy: "anyone",
|
||||
userID: token.authenticationBox.userID
|
||||
)
|
||||
}
|
||||
|
||||
static func createRandomAuthBytes() -> Data {
|
||||
let byteCount = 16
|
||||
var bytes = Data(count: byteCount)
|
||||
_ = bytes.withUnsafeMutableBytes { SecRandomCopyBytes(kSecRandomDefault, byteCount, $0.baseAddress!) }
|
||||
return bytes
|
||||
}
|
||||
}
|
||||
|
||||
extension NotificationService {
|
||||
final class NotificationSubscription {
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
// input
|
||||
let domain: String
|
||||
let userID: Mastodon.Entity.Account.ID
|
||||
|
||||
var token: SubscribeToken?
|
||||
|
||||
init(domain: String, userID: Mastodon.Entity.Account.ID) {
|
||||
self.domain = domain
|
||||
self.userID = userID
|
||||
}
|
||||
|
||||
struct SubscribeToken: Equatable {
|
||||
|
||||
let deviceToken: String
|
||||
let authenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
// TODO: set other parameter
|
||||
|
||||
init(
|
||||
deviceToken: String,
|
||||
authenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
) {
|
||||
self.deviceToken = deviceToken
|
||||
self.authenticationBox = authenticationBox
|
||||
}
|
||||
|
||||
static func == (
|
||||
lhs: NotificationService.NotificationSubscription.SubscribeToken,
|
||||
rhs: NotificationService.NotificationSubscription.SubscribeToken
|
||||
) -> Bool {
|
||||
return lhs.deviceToken == rhs.deviceToken &&
|
||||
lhs.authenticationBox.domain == rhs.authenticationBox.domain &&
|
||||
lhs.authenticationBox.userID == rhs.authenticationBox.userID &&
|
||||
lhs.authenticationBox.userAuthorization.accessToken == rhs.authenticationBox.userAuthorization.accessToken
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -28,6 +28,7 @@ class AppContext: ObservableObject {
|
|||
let videoPlaybackService = VideoPlaybackService()
|
||||
let statusPrefetchingService: StatusPrefetchingService
|
||||
let statusPublishService = StatusPublishService()
|
||||
let notificationService: NotificationService
|
||||
|
||||
let documentStore: DocumentStore
|
||||
private var documentStoreSubscription: AnyCancellable!
|
||||
|
@ -45,11 +46,12 @@ class AppContext: ObservableObject {
|
|||
let _apiService = APIService(backgroundManagedObjectContext: _backgroundManagedObjectContext)
|
||||
apiService = _apiService
|
||||
|
||||
authenticationService = AuthenticationService(
|
||||
let _authenticationService = AuthenticationService(
|
||||
managedObjectContext: _managedObjectContext,
|
||||
backgroundManagedObjectContext: _backgroundManagedObjectContext,
|
||||
apiService: _apiService
|
||||
)
|
||||
authenticationService = _authenticationService
|
||||
|
||||
emojiService = EmojiService(
|
||||
apiService: apiService
|
||||
|
@ -57,6 +59,10 @@ class AppContext: ObservableObject {
|
|||
statusPrefetchingService = StatusPrefetchingService(
|
||||
apiService: _apiService
|
||||
)
|
||||
notificationService = NotificationService(
|
||||
apiService: _apiService,
|
||||
authenticationService: _authenticationService
|
||||
)
|
||||
|
||||
documentStore = DocumentStore()
|
||||
documentStoreSubscription = documentStore.objectWillChange
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
// Created by MainasuK Cirno on 2021/1/22.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
|
||||
@main
|
||||
|
@ -18,6 +19,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||
UserDefaults.standard.setValue(UIApplication.appVersion(), forKey: "Mastodon.appVersion")
|
||||
UserDefaults.standard.setValue(UIApplication.appBuild(), forKey: "Mastodon.appBundle")
|
||||
|
||||
// UNUserNotificationCenter.current().delegate = self
|
||||
application.registerForRemoteNotifications()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -38,13 +42,23 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||
|
||||
}
|
||||
|
||||
|
||||
extension AppDelegate {
|
||||
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
|
||||
return UIDevice.current.userInterfaceIdiom == .phone ? .portrait : .all
|
||||
}
|
||||
}
|
||||
|
||||
extension AppDelegate {
|
||||
|
||||
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
|
||||
appContext.notificationService.deviceToken.value = deviceToken
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UNUserNotificationCenterDelegate
|
||||
extension AppDelegate: UNUserNotificationCenterDelegate {
|
||||
|
||||
}
|
||||
|
||||
extension AppContext {
|
||||
static var shared: AppContext {
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
//
|
||||
// AppSecret.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-4-22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
import Keys
|
||||
|
||||
final class AppSecret {
|
||||
|
||||
let notificationEndpoint: String
|
||||
|
||||
let notificationPrivateKey: P256.KeyAgreement.PrivateKey!
|
||||
let notificationPublicKey: P256.KeyAgreement.PublicKey!
|
||||
let notificationAuth: Data
|
||||
|
||||
static let `default`: AppSecret = {
|
||||
return AppSecret()
|
||||
}()
|
||||
|
||||
init() {
|
||||
let keys = MastodonKeys()
|
||||
|
||||
#if DEBUG
|
||||
self.notificationEndpoint = keys.notification_endpoint_debug
|
||||
let nonce = keys.notification_key_nonce_debug
|
||||
let auth = keys.notification_key_auth_debug
|
||||
#else
|
||||
self.notificationEndpoint = keys.notification_endpoint
|
||||
let nonce = keys.notification_key_nonce
|
||||
let auth = keys.notification_key_auth
|
||||
#endif
|
||||
|
||||
notificationPrivateKey = try! P256.KeyAgreement.PrivateKey(rawRepresentation: Data(base64Encoded: nonce)!)
|
||||
notificationPublicKey = notificationPrivateKey!.publicKey
|
||||
notificationAuth = Data(base64Encoded: auth)!
|
||||
}
|
||||
|
||||
var uncompressionNotificationPublicKeyData: Data {
|
||||
var data = notificationPublicKey.rawRepresentation
|
||||
if data.count == 64 {
|
||||
let prefix: [UInt8] = [0x04]
|
||||
data = Data(prefix) + data
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
}
|
|
@ -117,110 +117,75 @@ extension Mastodon.API.Subscriptions {
|
|||
}
|
||||
|
||||
extension Mastodon.API.Subscriptions {
|
||||
public struct CreateSubscriptionQuery: Codable, PostQuery {
|
||||
|
||||
public struct QuerySubscription: Codable {
|
||||
let endpoint: String
|
||||
let p256dh: String
|
||||
let auth: String
|
||||
let favourite: Bool?
|
||||
let follow: Bool?
|
||||
let reblog: Bool?
|
||||
let mention: Bool?
|
||||
let poll: Bool?
|
||||
|
||||
var queryItems: [URLQueryItem]? {
|
||||
var items = [URLQueryItem]()
|
||||
|
||||
items.append(URLQueryItem(name: "subscription[endpoint]", value: endpoint))
|
||||
items.append(URLQueryItem(name: "subscription[keys][p256dh]", value: p256dh))
|
||||
items.append(URLQueryItem(name: "subscription[keys][auth]", value: auth))
|
||||
|
||||
if let followValue = follow?.queryItemValue {
|
||||
let followItem = URLQueryItem(name: "data[alerts][follow]", value: followValue)
|
||||
items.append(followItem)
|
||||
}
|
||||
|
||||
if let favouriteValue = favourite?.queryItemValue {
|
||||
let favouriteItem = URLQueryItem(name: "data[alerts][favourite]", value: favouriteValue)
|
||||
items.append(favouriteItem)
|
||||
}
|
||||
|
||||
if let reblogValue = reblog?.queryItemValue {
|
||||
let reblogItem = URLQueryItem(name: "data[alerts][reblog]", value: reblogValue)
|
||||
items.append(reblogItem)
|
||||
}
|
||||
|
||||
if let mentionValue = mention?.queryItemValue {
|
||||
let mentionItem = URLQueryItem(name: "data[alerts][mention]", value: mentionValue)
|
||||
items.append(mentionItem)
|
||||
}
|
||||
return items
|
||||
}
|
||||
let keys: Keys
|
||||
|
||||
public init(
|
||||
endpoint: String,
|
||||
p256dh: String,
|
||||
auth: String,
|
||||
favourite: Bool?,
|
||||
follow: Bool?,
|
||||
reblog: Bool?,
|
||||
mention: Bool?,
|
||||
poll: Bool?
|
||||
keys: Keys
|
||||
) {
|
||||
self.endpoint = endpoint
|
||||
self.p256dh = p256dh
|
||||
self.auth = auth
|
||||
self.favourite = favourite
|
||||
self.follow = follow
|
||||
self.reblog = reblog
|
||||
self.mention = mention
|
||||
self.poll = poll
|
||||
self.keys = keys
|
||||
}
|
||||
|
||||
public struct Keys: Codable {
|
||||
let p256dh: String
|
||||
let auth: String
|
||||
|
||||
public init(p256dh: Data, auth: Data) {
|
||||
self.p256dh = p256dh.base64UrlEncodedString()
|
||||
self.auth = auth.base64UrlEncodedString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct QueryData: Codable {
|
||||
let alerts: Alerts
|
||||
|
||||
public init(alerts: Mastodon.API.Subscriptions.QueryData.Alerts) {
|
||||
self.alerts = alerts
|
||||
}
|
||||
|
||||
public struct Alerts: Codable {
|
||||
let favourite: Bool?
|
||||
let follow: Bool?
|
||||
let reblog: Bool?
|
||||
let mention: Bool?
|
||||
let poll: Bool?
|
||||
|
||||
public init(favourite: Bool?, follow: Bool?, reblog: Bool?, mention: Bool?, poll: Bool?) {
|
||||
self.favourite = favourite
|
||||
self.follow = follow
|
||||
self.reblog = reblog
|
||||
self.mention = mention
|
||||
self.poll = poll
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct CreateSubscriptionQuery: Codable, PostQuery {
|
||||
let subscription: QuerySubscription
|
||||
let data: QueryData
|
||||
|
||||
public init(
|
||||
subscription: Mastodon.API.Subscriptions.QuerySubscription,
|
||||
data: Mastodon.API.Subscriptions.QueryData
|
||||
) {
|
||||
self.subscription = subscription
|
||||
self.data = data
|
||||
}
|
||||
}
|
||||
|
||||
public struct UpdateSubscriptionQuery: Codable, PutQuery {
|
||||
let favourite: Bool?
|
||||
let follow: Bool?
|
||||
let reblog: Bool?
|
||||
let mention: Bool?
|
||||
let poll: Bool?
|
||||
|
||||
var queryItems: [URLQueryItem]? {
|
||||
var items = [URLQueryItem]()
|
||||
|
||||
if let followValue = follow?.queryItemValue {
|
||||
let followItem = URLQueryItem(name: "data[alerts][follow]", value: followValue)
|
||||
items.append(followItem)
|
||||
}
|
||||
|
||||
if let favouriteValue = favourite?.queryItemValue {
|
||||
let favouriteItem = URLQueryItem(name: "data[alerts][favourite]", value: favouriteValue)
|
||||
items.append(favouriteItem)
|
||||
}
|
||||
|
||||
if let reblogValue = reblog?.queryItemValue {
|
||||
let reblogItem = URLQueryItem(name: "data[alerts][reblog]", value: reblogValue)
|
||||
items.append(reblogItem)
|
||||
}
|
||||
|
||||
if let mentionValue = mention?.queryItemValue {
|
||||
let mentionItem = URLQueryItem(name: "data[alerts][mention]", value: mentionValue)
|
||||
items.append(mentionItem)
|
||||
}
|
||||
return items
|
||||
let data: QueryData
|
||||
|
||||
public init(data: Mastodon.API.Subscriptions.QueryData) {
|
||||
self.data = data
|
||||
}
|
||||
|
||||
public init(
|
||||
favourite: Bool?,
|
||||
follow: Bool?,
|
||||
reblog: Bool?,
|
||||
mention: Bool?,
|
||||
poll: Bool?
|
||||
) {
|
||||
self.favourite = favourite
|
||||
self.follow = follow
|
||||
self.reblog = reblog
|
||||
self.mention = mention
|
||||
self.poll = poll
|
||||
}
|
||||
var queryItems: [URLQueryItem]? { nil }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,3 +35,12 @@ extension Data {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
extension Data {
|
||||
func base64UrlEncodedString() -> String {
|
||||
return base64EncodedString()
|
||||
.replacingOccurrences(of: "+", with: "-")
|
||||
.replacingOccurrences(of: "/", with: "_")
|
||||
.replacingOccurrences(of: "=", with: "")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
//
|
||||
// String.swift
|
||||
// NotificationService
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-4-25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension String {
|
||||
static func normalize(base64String: String) -> String {
|
||||
let base64 = base64String
|
||||
.replacingOccurrences(of: "-", with: "+")
|
||||
.replacingOccurrences(of: "_", with: "/")
|
||||
.padding()
|
||||
return base64
|
||||
}
|
||||
|
||||
private func padding() -> String {
|
||||
let remainder = self.count % 4
|
||||
if remainder > 0 {
|
||||
return self.padding(
|
||||
toLength: self.count + 4 - remainder,
|
||||
withPad: "=",
|
||||
startingAt: 0
|
||||
)
|
||||
}
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>NotificationService</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.usernotifications.service</string>
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).NotificationService</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
|
@ -0,0 +1,96 @@
|
|||
//
|
||||
// NotificationService+Decrypt.swift
|
||||
// NotificationService
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-4-25.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
|
||||
extension NotificationService {
|
||||
|
||||
static func decrypt(payload: Data, salt: Data, auth: Data, privateKey: P256.KeyAgreement.PrivateKey, publicKey: P256.KeyAgreement.PublicKey) -> Data? {
|
||||
guard let sharedSecret = try? privateKey.sharedSecretFromKeyAgreement(with: publicKey) else {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: failed to craete shared secret", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
return nil
|
||||
}
|
||||
|
||||
let keyMaterial = sharedSecret.hkdfDerivedSymmetricKey(using: SHA256.self, salt: auth, sharedInfo: Data("Content-Encoding: auth\0".utf8), outputByteCount: 32)
|
||||
|
||||
let keyInfo = info(type: "aesgcm", clientPublicKey: privateKey.publicKey.x963Representation, serverPublicKey: publicKey.x963Representation)
|
||||
let key = HKDF<SHA256>.deriveKey(inputKeyMaterial: keyMaterial, salt: salt, info: keyInfo, outputByteCount: 16)
|
||||
|
||||
let nonceInfo = info(type: "nonce", clientPublicKey: privateKey.publicKey.x963Representation, serverPublicKey: publicKey.x963Representation)
|
||||
let nonce = HKDF<SHA256>.deriveKey(inputKeyMaterial: keyMaterial, salt: salt, info: nonceInfo, outputByteCount: 12)
|
||||
|
||||
let nonceData = nonce.withUnsafeBytes(Array.init)
|
||||
|
||||
guard let sealedBox = try? AES.GCM.SealedBox(combined: nonceData + payload) else {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: failed to create sealedBox", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let plaintext = try? AES.GCM.open(sealedBox, using: key) else {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: failed to open sealedBox", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
return nil
|
||||
}
|
||||
|
||||
let paddingLength = Int(plaintext[0]) * 256 + Int(plaintext[1])
|
||||
guard plaintext.count >= 2 + paddingLength else {
|
||||
print("1")
|
||||
fatalError()
|
||||
}
|
||||
let unpadded = plaintext.suffix(from: paddingLength + 2)
|
||||
|
||||
return Data(unpadded)
|
||||
}
|
||||
|
||||
static private func info(type: String, clientPublicKey: Data, serverPublicKey: Data) -> Data {
|
||||
var info = Data()
|
||||
|
||||
info.append("Content-Encoding: ".data(using: .utf8)!)
|
||||
info.append(type.data(using: .utf8)!)
|
||||
info.append(0)
|
||||
info.append("P-256".data(using: .utf8)!)
|
||||
info.append(0)
|
||||
info.append(0)
|
||||
info.append(65)
|
||||
info.append(clientPublicKey)
|
||||
info.append(0)
|
||||
info.append(65)
|
||||
info.append(serverPublicKey)
|
||||
|
||||
return info
|
||||
}
|
||||
}
|
||||
|
||||
extension NotificationService {
|
||||
struct MastodonNotification: Codable {
|
||||
|
||||
private let _accessToken: String
|
||||
var accessToken: String {
|
||||
return String.normalize(base64String: _accessToken)
|
||||
}
|
||||
|
||||
let notificationID: Int
|
||||
let notificationType: String
|
||||
|
||||
let preferredLocale: String?
|
||||
let icon: String?
|
||||
let title: String
|
||||
let body: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case _accessToken = "access_token"
|
||||
case notificationID = "notification_id"
|
||||
case notificationType = "notification_type"
|
||||
case preferredLocale = "preferred_locale"
|
||||
case icon
|
||||
case title
|
||||
case body
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
//
|
||||
// NotificationService.swift
|
||||
// NotificationService
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-4-23.
|
||||
//
|
||||
|
||||
import UserNotifications
|
||||
import CommonOSLog
|
||||
import CryptoKit
|
||||
import AlamofireImage
|
||||
import Base85
|
||||
import Keys
|
||||
|
||||
class NotificationService: UNNotificationServiceExtension {
|
||||
|
||||
var contentHandler: ((UNNotificationContent) -> Void)?
|
||||
var bestAttemptContent: UNMutableNotificationContent?
|
||||
|
||||
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
|
||||
self.contentHandler = contentHandler
|
||||
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
|
||||
|
||||
if let bestAttemptContent = bestAttemptContent {
|
||||
// Modify the notification content here...
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
|
||||
let privateKey = AppSecret.default.notificationPrivateKey!
|
||||
|
||||
guard let encodedPayload = bestAttemptContent.userInfo["p"] as? String,
|
||||
let payload = Data(base85Encoded: encodedPayload, options: [], encoding: .z85) else {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: invalid payload", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
contentHandler(bestAttemptContent)
|
||||
return
|
||||
}
|
||||
|
||||
guard let encodedPublicKey = bestAttemptContent.userInfo["k"] as? String,
|
||||
let publicKey = NotificationService.publicKey(encodedPublicKey: encodedPublicKey) else {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: invalid public key", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
contentHandler(bestAttemptContent)
|
||||
return
|
||||
}
|
||||
|
||||
guard let encodedSalt = bestAttemptContent.userInfo["s"] as? String,
|
||||
let salt = Data(base85Encoded: encodedSalt, options: [], encoding: .z85) else {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: invalid salt", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
contentHandler(bestAttemptContent)
|
||||
return
|
||||
}
|
||||
|
||||
let auth = AppSecret.default.notificationAuth
|
||||
guard let plaintextData = NotificationService.decrypt(payload: payload, salt: salt, auth: auth, privateKey: privateKey, publicKey: publicKey),
|
||||
let notification = try? JSONDecoder().decode(MastodonNotification.self, from: plaintextData) else {
|
||||
contentHandler(bestAttemptContent)
|
||||
return
|
||||
}
|
||||
|
||||
bestAttemptContent.title = notification.title
|
||||
bestAttemptContent.subtitle = ""
|
||||
bestAttemptContent.body = notification.body
|
||||
|
||||
if let urlString = notification.icon, let url = URL(string: urlString) {
|
||||
let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("notification-attachments")
|
||||
try? FileManager.default.createDirectory(at: temporaryDirectoryURL, withIntermediateDirectories: true, attributes: nil)
|
||||
let filename = url.lastPathComponent
|
||||
let fileURL = temporaryDirectoryURL.appendingPathComponent(filename)
|
||||
|
||||
ImageDownloader.default.download(URLRequest(url: url), completion: { [weak self] response in
|
||||
guard let _ = self else { return }
|
||||
switch response.result {
|
||||
case .failure(let error):
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: download image %s fail: %s", ((#file as NSString).lastPathComponent), #line, #function, url.debugDescription, error.localizedDescription)
|
||||
case .success(let image):
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: download image %s success", ((#file as NSString).lastPathComponent), #line, #function, url.debugDescription)
|
||||
try? image.pngData()?.write(to: fileURL)
|
||||
if let attachment = try? UNNotificationAttachment(identifier: filename, url: fileURL, options: nil) {
|
||||
bestAttemptContent.attachments = [attachment]
|
||||
}
|
||||
}
|
||||
contentHandler(bestAttemptContent)
|
||||
})
|
||||
} else {
|
||||
contentHandler(bestAttemptContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func serviceExtensionTimeWillExpire() {
|
||||
// Called just before the extension will be terminated by the system.
|
||||
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
|
||||
if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
|
||||
contentHandler(bestAttemptContent)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension NotificationService {
|
||||
static func publicKey(encodedPublicKey: String) -> P256.KeyAgreement.PublicKey? {
|
||||
guard let publicKeyData = Data(base85Encoded: encodedPublicKey, options: [], encoding: .z85) else { return nil }
|
||||
return try? P256.KeyAgreement.PublicKey(x963Representation: publicKeyData)
|
||||
}
|
||||
}
|
16
Podfile
16
Podfile
|
@ -23,4 +23,20 @@ target 'Mastodon' do
|
|||
# Pods for testing
|
||||
end
|
||||
|
||||
target 'NotificationService' do
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
plugin 'cocoapods-keys', {
|
||||
:project => "Mastodon",
|
||||
:keys => [
|
||||
"notification_endpoint",
|
||||
"notification_endpoint_debug",
|
||||
"notification_key_nonce",
|
||||
"notification_key_nonce_debug",
|
||||
"notification_key_auth",
|
||||
"notification_key_auth_debug"
|
||||
]
|
||||
}
|
|
@ -1,12 +1,14 @@
|
|||
PODS:
|
||||
- DateToolsSwift (5.0.0)
|
||||
- Kanna (5.2.4)
|
||||
- Keys (1.0.1)
|
||||
- SwiftGen (6.4.0)
|
||||
- "UITextField+Shake (1.2.1)"
|
||||
|
||||
DEPENDENCIES:
|
||||
- DateToolsSwift (~> 5.0.0)
|
||||
- Kanna (~> 5.2.2)
|
||||
- Keys (from `Pods/CocoaPodsKeys`)
|
||||
- SwiftGen (~> 6.4.0)
|
||||
- "UITextField+Shake (~> 1.2)"
|
||||
|
||||
|
@ -17,12 +19,17 @@ SPEC REPOS:
|
|||
- SwiftGen
|
||||
- "UITextField+Shake"
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
Keys:
|
||||
:path: Pods/CocoaPodsKeys
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
DateToolsSwift: 4207ada6ad615d8dc076323d27037c94916dbfa6
|
||||
Kanna: b9d00d7c11428308c7f95e1f1f84b8205f567a8f
|
||||
Keys: a576f4c9c1c641ca913a959a9c62ed3f215a8de9
|
||||
SwiftGen: 67860cc7c3cfc2ed25b9b74cfd55495fc89f9108
|
||||
"UITextField+Shake": 298ac5a0f239d731bdab999b19b628c956ca0ac3
|
||||
|
||||
PODFILE CHECKSUM: 30e8e3a555251a512e7b5e91183747152f126e7a
|
||||
PODFILE CHECKSUM: b99204f58cb11d471cfad7269bbf0abb853dc953
|
||||
|
||||
COCOAPODS: 1.10.1
|
||||
|
|
|
@ -48,6 +48,7 @@ arch -x86_64 pod install
|
|||
- [AlamofireNetworkActivityIndicator](https://github.com/Alamofire/AlamofireNetworkActivityIndicator)
|
||||
- [Alamofire](https://github.com/Alamofire/Alamofire)
|
||||
- [CommonOSLog](https://github.com/mainasuk/CommonOSLog)
|
||||
- [CryptoSwift](https://github.com/krzyzanowskim/CryptoSwift)
|
||||
- [DateToolSwift](https://github.com/MatthewYork/DateTools)
|
||||
- [Kanna](https://github.com/tid-kijyun/Kanna)
|
||||
- [Kingfisher](https://github.com/onevcat/Kingfisher)
|
||||
|
|
Loading…
Reference in New Issue