1
0
mirror of https://github.com/mastodon/mastodon-ios.git synced 2025-02-02 10:27:08 +01:00

feat: add content warning for post media

This commit is contained in:
CMK 2022-01-29 19:51:40 +08:00
parent caaf66286f
commit d332c98a0f
21 changed files with 355 additions and 362 deletions

View File

@ -25,7 +25,6 @@
0FB3D33825E6401400AAD544 /* PickServerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D33725E6401400AAD544 /* PickServerCell.swift */; }; 0FB3D33825E6401400AAD544 /* PickServerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D33725E6401400AAD544 /* PickServerCell.swift */; };
164F0EBC267D4FE400249499 /* BoopSound.caf in Resources */ = {isa = PBXBuildFile; fileRef = 164F0EBB267D4FE400249499 /* BoopSound.caf */; }; 164F0EBC267D4FE400249499 /* BoopSound.caf in Resources */ = {isa = PBXBuildFile; fileRef = 164F0EBB267D4FE400249499 /* BoopSound.caf */; };
18BC7629F65E6DB12CB8416D /* Pods_Mastodon_MastodonUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */; }; 18BC7629F65E6DB12CB8416D /* Pods_Mastodon_MastodonUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */; };
2D084B8D26258EA3003AA3AF /* NotificationViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D084B8C26258EA3003AA3AF /* NotificationViewModel+Diffable.swift */; };
2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198642261BF09500F0B013 /* SearchResultItem.swift */; }; 2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198642261BF09500F0B013 /* SearchResultItem.swift */; };
2D198649261C0B8500F0B013 /* SearchResultSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198648261C0B8500F0B013 /* SearchResultSection.swift */; }; 2D198649261C0B8500F0B013 /* SearchResultSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198648261C0B8500F0B013 /* SearchResultSection.swift */; };
2D206B7225F5D27F00143C56 /* AudioContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B7125F5D27F00143C56 /* AudioContainerView.swift */; }; 2D206B7225F5D27F00143C56 /* AudioContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B7125F5D27F00143C56 /* AudioContainerView.swift */; };
@ -413,6 +412,7 @@
DB852D1F26FB037800FC9D81 /* SidebarViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB852D1E26FB037800FC9D81 /* SidebarViewModel.swift */; }; DB852D1F26FB037800FC9D81 /* SidebarViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB852D1E26FB037800FC9D81 /* SidebarViewModel.swift */; };
DB87D4452609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */; }; DB87D4452609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */; };
DB87D4512609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB87D4502609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift */; }; DB87D4512609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB87D4502609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift */; };
DB894CC427A5490600684B74 /* BlurhashImageCacheService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB894CC327A5490600684B74 /* BlurhashImageCacheService.swift */; };
DB8AF52E25C13561002E6C99 /* ViewStateStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF52B25C13561002E6C99 /* ViewStateStore.swift */; }; DB8AF52E25C13561002E6C99 /* ViewStateStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF52B25C13561002E6C99 /* ViewStateStore.swift */; };
DB8AF52F25C13561002E6C99 /* DocumentStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF52C25C13561002E6C99 /* DocumentStore.swift */; }; DB8AF52F25C13561002E6C99 /* DocumentStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF52C25C13561002E6C99 /* DocumentStore.swift */; };
DB8AF53025C13561002E6C99 /* AppContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF52D25C13561002E6C99 /* AppContext.swift */; }; DB8AF53025C13561002E6C99 /* AppContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF52D25C13561002E6C99 /* AppContext.swift */; };
@ -477,7 +477,6 @@
DBAE3F942616E28B004B8251 /* APIService+Follow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F932616E28B004B8251 /* APIService+Follow.swift */; }; DBAE3F942616E28B004B8251 /* APIService+Follow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F932616E28B004B8251 /* APIService+Follow.swift */; };
DBAE3F9E2616E308004B8251 /* APIService+Mute.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F9D2616E308004B8251 /* APIService+Mute.swift */; }; DBAE3F9E2616E308004B8251 /* APIService+Mute.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F9D2616E308004B8251 /* APIService+Mute.swift */; };
DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */; }; DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */; };
DBAEDE5C267A058D00D25FF5 /* BlurhashImageCacheService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAEDE5B267A058D00D25FF5 /* BlurhashImageCacheService.swift */; };
DBB3BA2A26A81C020004F2D4 /* FLAnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */; }; DBB3BA2A26A81C020004F2D4 /* FLAnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */; };
DBB3BA2B26A81D060004F2D4 /* FLAnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */; }; DBB3BA2B26A81D060004F2D4 /* FLAnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */; };
DBB525082611EAC0002F1F29 /* Tabman in Frameworks */ = {isa = PBXBuildFile; productRef = DBB525072611EAC0002F1F29 /* Tabman */; }; DBB525082611EAC0002F1F29 /* Tabman in Frameworks */ = {isa = PBXBuildFile; productRef = DBB525072611EAC0002F1F29 /* Tabman */; };
@ -516,7 +515,6 @@
DBBC24D126A5484F00398BB9 /* UITextView+Placeholder in Frameworks */ = {isa = PBXBuildFile; productRef = DBBC24D026A5484F00398BB9 /* UITextView+Placeholder */; }; DBBC24D126A5484F00398BB9 /* UITextView+Placeholder in Frameworks */ = {isa = PBXBuildFile; productRef = DBBC24D026A5484F00398BB9 /* UITextView+Placeholder */; };
DBBC24DC26A54BCB00398BB9 /* MastodonRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24D626A54BCB00398BB9 /* MastodonRegex.swift */; }; DBBC24DC26A54BCB00398BB9 /* MastodonRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24D626A54BCB00398BB9 /* MastodonRegex.swift */; };
DBBC24DE26A54BCB00398BB9 /* MastodonMetricFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24D826A54BCB00398BB9 /* MastodonMetricFormatter.swift */; }; DBBC24DE26A54BCB00398BB9 /* MastodonMetricFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24D826A54BCB00398BB9 /* MastodonMetricFormatter.swift */; };
DBBC50BF278ED0E700AF0CC6 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC50BE278ED0E700AF0CC6 /* Date.swift */; };
DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */; }; DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */; };
DBBF1DBF2652401B00E5B703 /* AutoCompleteViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBF1DBE2652401B00E5B703 /* AutoCompleteViewModel.swift */; }; DBBF1DBF2652401B00E5B703 /* AutoCompleteViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBF1DBE2652401B00E5B703 /* AutoCompleteViewModel.swift */; };
DBBF1DC226524D2900E5B703 /* AutoCompleteTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBF1DC126524D2900E5B703 /* AutoCompleteTableViewCell.swift */; }; DBBF1DC226524D2900E5B703 /* AutoCompleteTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBF1DC126524D2900E5B703 /* AutoCompleteTableViewCell.swift */; };
@ -750,7 +748,6 @@
159AC43EFE0A1F95FCB358A4 /* Pods-MastodonIntent.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonIntent.release.xcconfig"; path = "Target Support Files/Pods-MastodonIntent/Pods-MastodonIntent.release.xcconfig"; sourceTree = "<group>"; }; 159AC43EFE0A1F95FCB358A4 /* Pods-MastodonIntent.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonIntent.release.xcconfig"; path = "Target Support Files/Pods-MastodonIntent/Pods-MastodonIntent.release.xcconfig"; sourceTree = "<group>"; };
164F0EBB267D4FE400249499 /* BoopSound.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = BoopSound.caf; sourceTree = "<group>"; }; 164F0EBB267D4FE400249499 /* BoopSound.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = BoopSound.caf; sourceTree = "<group>"; };
1D6D967E77A5357E2C6110D9 /* Pods-Mastodon.asdk - debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.asdk - debug.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.asdk - debug.xcconfig"; sourceTree = "<group>"; }; 1D6D967E77A5357E2C6110D9 /* Pods-Mastodon.asdk - debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.asdk - debug.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.asdk - debug.xcconfig"; sourceTree = "<group>"; };
2D084B8C26258EA3003AA3AF /* NotificationViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationViewModel+Diffable.swift"; sourceTree = "<group>"; };
2D198642261BF09500F0B013 /* SearchResultItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultItem.swift; sourceTree = "<group>"; }; 2D198642261BF09500F0B013 /* SearchResultItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultItem.swift; sourceTree = "<group>"; };
2D198648261C0B8500F0B013 /* SearchResultSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultSection.swift; sourceTree = "<group>"; }; 2D198648261C0B8500F0B013 /* SearchResultSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultSection.swift; sourceTree = "<group>"; };
2D206B7125F5D27F00143C56 /* AudioContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioContainerView.swift; sourceTree = "<group>"; }; 2D206B7125F5D27F00143C56 /* AudioContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioContainerView.swift; sourceTree = "<group>"; };
@ -1180,6 +1177,7 @@
DB852D1E26FB037800FC9D81 /* SidebarViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarViewModel.swift; sourceTree = "<group>"; }; DB852D1E26FB037800FC9D81 /* SidebarViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarViewModel.swift; sourceTree = "<group>"; };
DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollOptionCollectionViewCell.swift; sourceTree = "<group>"; }; DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollOptionCollectionViewCell.swift; sourceTree = "<group>"; };
DB87D4502609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollOptionAppendEntryCollectionViewCell.swift; sourceTree = "<group>"; }; DB87D4502609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollOptionAppendEntryCollectionViewCell.swift; sourceTree = "<group>"; };
DB894CC327A5490600684B74 /* BlurhashImageCacheService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurhashImageCacheService.swift; sourceTree = "<group>"; };
DB89BA1025C10FF5008580ED /* Mastodon.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Mastodon.entitlements; sourceTree = "<group>"; }; DB89BA1025C10FF5008580ED /* Mastodon.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Mastodon.entitlements; sourceTree = "<group>"; };
DB8AF52B25C13561002E6C99 /* ViewStateStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewStateStore.swift; sourceTree = "<group>"; }; DB8AF52B25C13561002E6C99 /* ViewStateStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewStateStore.swift; sourceTree = "<group>"; };
DB8AF52C25C13561002E6C99 /* DocumentStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DocumentStore.swift; sourceTree = "<group>"; }; DB8AF52C25C13561002E6C99 /* DocumentStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DocumentStore.swift; sourceTree = "<group>"; };
@ -1257,7 +1255,6 @@
DBAE3F932616E28B004B8251 /* APIService+Follow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Follow.swift"; sourceTree = "<group>"; }; DBAE3F932616E28B004B8251 /* APIService+Follow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Follow.swift"; sourceTree = "<group>"; };
DBAE3F9D2616E308004B8251 /* APIService+Mute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Mute.swift"; sourceTree = "<group>"; }; DBAE3F9D2616E308004B8251 /* APIService+Mute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Mute.swift"; sourceTree = "<group>"; };
DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteProfileViewModel.swift; sourceTree = "<group>"; }; DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteProfileViewModel.swift; sourceTree = "<group>"; };
DBAEDE5B267A058D00D25FF5 /* BlurhashImageCacheService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurhashImageCacheService.swift; sourceTree = "<group>"; };
DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FLAnimatedImageView.swift; sourceTree = "<group>"; }; DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FLAnimatedImageView.swift; sourceTree = "<group>"; };
DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSegmentedViewController.swift; sourceTree = "<group>"; }; DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSegmentedViewController.swift; sourceTree = "<group>"; };
DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePagingViewController.swift; sourceTree = "<group>"; }; DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePagingViewController.swift; sourceTree = "<group>"; };
@ -1282,7 +1279,6 @@
DBBC24CE26A547AE00398BB9 /* ThemeService+Appearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ThemeService+Appearance.swift"; sourceTree = "<group>"; }; DBBC24CE26A547AE00398BB9 /* ThemeService+Appearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ThemeService+Appearance.swift"; sourceTree = "<group>"; };
DBBC24D626A54BCB00398BB9 /* MastodonRegex.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonRegex.swift; sourceTree = "<group>"; }; DBBC24D626A54BCB00398BB9 /* MastodonRegex.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonRegex.swift; sourceTree = "<group>"; };
DBBC24D826A54BCB00398BB9 /* MastodonMetricFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonMetricFormatter.swift; sourceTree = "<group>"; }; DBBC24D826A54BCB00398BB9 /* MastodonMetricFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonMetricFormatter.swift; sourceTree = "<group>"; };
DBBC50BE278ED0E700AF0CC6 /* Date.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Date.swift; sourceTree = "<group>"; };
DBBC50C0278ED49200AF0CC6 /* MastodonAuthenticationBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonAuthenticationBox.swift; sourceTree = "<group>"; }; DBBC50C0278ED49200AF0CC6 /* MastodonAuthenticationBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonAuthenticationBox.swift; sourceTree = "<group>"; };
DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPickServerAppearance.swift; sourceTree = "<group>"; }; DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPickServerAppearance.swift; sourceTree = "<group>"; };
DBBF1DBE2652401B00E5B703 /* AutoCompleteViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleteViewModel.swift; sourceTree = "<group>"; }; DBBF1DBE2652401B00E5B703 /* AutoCompleteViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleteViewModel.swift; sourceTree = "<group>"; };
@ -1690,9 +1686,9 @@
DB6D9F6226357848008423CD /* SettingService.swift */, DB6D9F6226357848008423CD /* SettingService.swift */,
DBA5E7A2263AD0A3004598BB /* PhotoLibraryService.swift */, DBA5E7A2263AD0A3004598BB /* PhotoLibraryService.swift */,
DB297B1A2679FAE200704C90 /* PlaceholderImageCacheService.swift */, DB297B1A2679FAE200704C90 /* PlaceholderImageCacheService.swift */,
DBAEDE5B267A058D00D25FF5 /* BlurhashImageCacheService.swift */,
DB564BD2269F3B35001E39A7 /* StatusFilterService.swift */, DB564BD2269F3B35001E39A7 /* StatusFilterService.swift */,
DB73BF42271192BB00781945 /* InstanceService.swift */, DB73BF42271192BB00781945 /* InstanceService.swift */,
DB894CC327A5490600684B74 /* BlurhashImageCacheService.swift */,
); );
path = Service; path = Service;
sourceTree = "<group>"; sourceTree = "<group>";
@ -2724,7 +2720,6 @@
DBCC3B35261440BA0045B23D /* UINavigationController.swift */, DBCC3B35261440BA0045B23D /* UINavigationController.swift */,
DB73BF4827140BA300781945 /* UICollectionViewDiffableDataSource.swift */, DB73BF4827140BA300781945 /* UICollectionViewDiffableDataSource.swift */,
DB73BF4A27140C0800781945 /* UITableViewDiffableDataSource.swift */, DB73BF4A27140C0800781945 /* UITableViewDiffableDataSource.swift */,
DBBC50BE278ED0E700AF0CC6 /* Date.swift */,
); );
path = Extension; path = Extension;
sourceTree = "<group>"; sourceTree = "<group>";
@ -2794,7 +2789,6 @@
2D35237F26256F470031AF25 /* Cell */, 2D35237F26256F470031AF25 /* Cell */,
DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */, DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */,
2D607AD726242FC500B70763 /* NotificationViewModel.swift */, 2D607AD726242FC500B70763 /* NotificationViewModel.swift */,
2D084B8C26258EA3003AA3AF /* NotificationViewModel+Diffable.swift */,
); );
path = Notification; path = Notification;
sourceTree = "<group>"; sourceTree = "<group>";
@ -3782,7 +3776,6 @@
5DF1054125F886D400D6C0D4 /* VideoPlaybackService.swift in Sources */, 5DF1054125F886D400D6C0D4 /* VideoPlaybackService.swift in Sources */,
DB6B35182601FA3400DC1E11 /* MastodonAttachmentService.swift in Sources */, DB6B35182601FA3400DC1E11 /* MastodonAttachmentService.swift in Sources */,
0FB3D2F725E4C24D00AAD544 /* MastodonPickServerViewModel.swift in Sources */, 0FB3D2F725E4C24D00AAD544 /* MastodonPickServerViewModel.swift in Sources */,
DBBC50BF278ED0E700AF0CC6 /* Date.swift in Sources */,
2D61335E25C1894B00CAE157 /* APIService.swift in Sources */, 2D61335E25C1894B00CAE157 /* APIService.swift in Sources */,
2D9DB967263A76FB007C1D71 /* BlockDomainService.swift in Sources */, 2D9DB967263A76FB007C1D71 /* BlockDomainService.swift in Sources */,
DB336F43278EB1690031E64B /* MediaView+Configuration.swift in Sources */, DB336F43278EB1690031E64B /* MediaView+Configuration.swift in Sources */,
@ -3976,7 +3969,6 @@
DB4F0968269ED8AD00D62E92 /* SearchHistoryTableHeaderView.swift in Sources */, DB4F0968269ED8AD00D62E92 /* SearchHistoryTableHeaderView.swift in Sources */,
0FB3D2FE25E4CB6400AAD544 /* OnboardingHeadlineTableViewCell.swift in Sources */, 0FB3D2FE25E4CB6400AAD544 /* OnboardingHeadlineTableViewCell.swift in Sources */,
5DA732CC2629CEF500A92342 /* UIView+Remove.swift in Sources */, 5DA732CC2629CEF500A92342 /* UIView+Remove.swift in Sources */,
DBAEDE5C267A058D00D25FF5 /* BlurhashImageCacheService.swift in Sources */,
DB1D843026566512000346B3 /* KeyboardPreference.swift in Sources */, DB1D843026566512000346B3 /* KeyboardPreference.swift in Sources */,
DB852D1926FAEB6B00FC9D81 /* SidebarViewController.swift in Sources */, DB852D1926FAEB6B00FC9D81 /* SidebarViewController.swift in Sources */,
2D206B9225F60EA700143C56 /* UIControl.swift in Sources */, 2D206B9225F60EA700143C56 /* UIControl.swift in Sources */,
@ -4024,7 +4016,6 @@
DB0617EF277F12720030EE79 /* NavigationActionView.swift in Sources */, DB0617EF277F12720030EE79 /* NavigationActionView.swift in Sources */,
DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */, DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */,
2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */, 2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */,
2D084B8D26258EA3003AA3AF /* NotificationViewModel+Diffable.swift in Sources */,
DB3667A1268ABB2E0027D07F /* ComposeStatusAttachmentItem.swift in Sources */, DB3667A1268ABB2E0027D07F /* ComposeStatusAttachmentItem.swift in Sources */,
DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */, DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */,
DBA94434265CBB5300C537E1 /* ProfileFieldSection.swift in Sources */, DBA94434265CBB5300C537E1 /* ProfileFieldSection.swift in Sources */,
@ -4136,6 +4127,7 @@
2D5A3D0325CF8742002347D6 /* ControlContainableScrollViews.swift in Sources */, 2D5A3D0325CF8742002347D6 /* ControlContainableScrollViews.swift in Sources */,
DB36679D268AB91B0027D07F /* ComposeStatusAttachmentTableViewCell.swift in Sources */, DB36679D268AB91B0027D07F /* ComposeStatusAttachmentTableViewCell.swift in Sources */,
DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */, DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */,
DB894CC427A5490600684B74 /* BlurhashImageCacheService.swift in Sources */,
DBFEF07B26A6BCE8006D7ED1 /* APIService+Status+Publish.swift in Sources */, DBFEF07B26A6BCE8006D7ED1 /* APIService+Status+Publish.swift in Sources */,
DBA0A11325FB3FC10079C110 /* ComposeToolbarView.swift in Sources */, DBA0A11325FB3FC10079C110 /* ComposeToolbarView.swift in Sources */,
5B90C48526259BF10002E742 /* APIService+Subscriptions.swift in Sources */, 5B90C48526259BF10002E742 /* APIService+Subscriptions.swift in Sources */,

View File

@ -86,6 +86,13 @@
ReferencedContainer = "container:Mastodon.xcodeproj"> ReferencedContainer = "container:Mastodon.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
<AdditionalOptions>
<AdditionalOption
key = "NSZombieEnabled"
value = "YES"
isEnabled = "YES">
</AdditionalOption>
</AdditionalOptions>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction
buildConfiguration = "Release" buildConfiguration = "Release"

View File

@ -1,44 +0,0 @@
//
// Date.swift
// Mastodon
//
// Created by MainasuK on 2022-1-12.
//
import Foundation
import MastodonAsset
import MastodonLocalization
extension Date {
public static let relativeTimestampFormatter: RelativeDateTimeFormatter = {
let formatter = RelativeDateTimeFormatter()
formatter.dateTimeStyle = .numeric
formatter.unitsStyle = .full
return formatter
}()
public var localizedSlowedTimeAgoSinceNow: String {
return self.localizedTimeAgo(since: Date(), isSlowed: true, isAbbreviated: true)
}
public var localizedTimeAgoSinceNow: String {
return self.localizedTimeAgo(since: Date(), isSlowed: false, isAbbreviated: false)
}
public func localizedTimeAgo(since date: Date, isSlowed: Bool, isAbbreviated: Bool) -> String {
let earlierDate = date < self ? date : self
let latestDate = earlierDate == date ? self : date
if isSlowed, earlierDate.timeIntervalSince(latestDate) >= -60 {
return L10n.Common.Controls.Timeline.Timestamp.now
} else {
if isAbbreviated {
return latestDate.localizedShortTimeAgo(since: earlierDate)
} else {
return Date.relativeTimestampFormatter.localizedString(for: earlierDate, relativeTo: latestDate)
}
}
}
}

View File

@ -47,7 +47,7 @@ extension DataSourceFacade {
switch target { switch target {
case .status: case .status:
return status.reblog ?? status return status.reblog ?? status
case .repost: case .reblog:
return status return status
} }
} }

View File

@ -10,7 +10,7 @@ import Foundation
enum DataSourceFacade { enum DataSourceFacade {
enum StatusTarget { enum StatusTarget {
case status // remove repost wrapper case status // remove reblog wrapper
case repost // keep repost wrapper case reblog // keep reblog wrapper
} }
} }

View File

@ -30,7 +30,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider {
} }
await DataSourceFacade.coordinateToProfileScene( await DataSourceFacade.coordinateToProfileScene(
provider: self, provider: self,
target: .status, // without reblog header target: .reblog, // keep the wrapper for header author
status: status status: status
) )
} }
@ -117,6 +117,24 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & MediaPrev
assertionFailure("only works for status data provider") assertionFailure("only works for status data provider")
return return
} }
let managedObjectContext = self.context.managedObjectContext
let needsToggleMediaSensitive: Bool = try await managedObjectContext.perform {
guard let _status = status.object(in: managedObjectContext) else { return false }
let status = _status.reblog ?? _status
guard status.sensitive else { return false }
guard status.isMediaSensitiveToggled else { return true }
return false
}
guard !needsToggleMediaSensitive else {
try await DataSourceFacade.responseToToggleMediaSensitiveAction(
dependency: self,
status: status
)
return
}
try await DataSourceFacade.coordinateToMediaPreviewScene( try await DataSourceFacade.coordinateToMediaPreviewScene(
dependency: self, dependency: self,
status: status, status: status,

View File

@ -374,7 +374,7 @@ extension HomeTimelineViewController {
@objc private func findPeopleButtonPressed(_ sender: PrimaryActionButton) { @objc private func findPeopleButtonPressed(_ sender: PrimaryActionButton) {
// TODO: // TODO:
let viewModel = SuggestionAccountViewModel(context: context) // let viewModel = SuggestionAccountViewModel(context: context)
// viewModel.delegate = self.viewModel // viewModel.delegate = self.viewModel
// coordinator.present(scene: .suggestionAccount(viewModel: viewModel), from: self, transition: .modal(animated: true, completion: nil)) // coordinator.present(scene: .suggestionAccount(viewModel: viewModel), from: self, transition: .modal(animated: true, completion: nil))
} }
@ -553,40 +553,9 @@ extension HomeTimelineViewController: UITableViewDelegate, AutoGenerateTableView
// func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) { // func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
// aspectTableView(tableView, didEndDisplaying: cell, forRowAt: indexPath) // aspectTableView(tableView, didEndDisplaying: cell, forRowAt: indexPath)
// } // }
//
// func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// aspectTableView(tableView, didSelectRowAt: indexPath)
// }
//
// func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
// return aspectTableView(tableView, contextMenuConfigurationForRowAt: indexPath, point: point)
// }
//
// func tableView(_ tableView: UITableView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
// return aspectTableView(tableView, previewForHighlightingContextMenuWithConfiguration: configuration)
// }
//
// func tableView(_ tableView: UITableView, previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
// return aspectTableView(tableView, previewForDismissingContextMenuWithConfiguration: configuration)
// }
//
// func tableView(_ tableView: UITableView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
// aspectTableView(tableView, willPerformPreviewActionForMenuWith: configuration, animator: animator)
// }
} }
// MARK: - UITableViewDataSourcePrefetching
//extension HomeTimelineViewController: UITableViewDataSourcePrefetching {
// func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
// aspectTableView(tableView, prefetchRowsAt: indexPaths)
// }
//
// func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
// aspectTableView(tableView, cancelPrefetchingForRowsAt: indexPaths)
// }
//}
// MARK: - ContentOffsetAdjustableTimelineViewControllerDelegate // MARK: - ContentOffsetAdjustableTimelineViewControllerDelegate
extension HomeTimelineViewController: ContentOffsetAdjustableTimelineViewControllerDelegate { extension HomeTimelineViewController: ContentOffsetAdjustableTimelineViewControllerDelegate {
func navigationBar() -> UINavigationBar? { func navigationBar() -> UINavigationBar? {
@ -613,24 +582,23 @@ extension HomeTimelineViewController: ScrollViewContainer {
var scrollView: UIScrollView { return tableView } var scrollView: UIScrollView { return tableView }
func scrollToTop(animated: Bool) { func scrollToTop(animated: Bool) {
// TODO: if scrollView.contentOffset.y < scrollView.frame.height,
// if scrollView.contentOffset.y < scrollView.frame.height, viewModel.loadLatestStateMachine.canEnterState(HomeTimelineViewModel.LoadLatestState.Loading.self),
// viewModel.loadLatestStateMachine.canEnterState(HomeTimelineViewModel.LoadLatestState.Loading.self), (scrollView.contentOffset.y + scrollView.adjustedContentInset.top) == 0.0,
// (scrollView.contentOffset.y + scrollView.adjustedContentInset.top) == 0.0, !refreshControl.isRefreshing {
// !refreshControl.isRefreshing { scrollView.scrollRectToVisible(CGRect(origin: CGPoint(x: 0, y: -refreshControl.frame.height), size: CGSize(width: 1, height: 1)), animated: animated)
// scrollView.scrollRectToVisible(CGRect(origin: CGPoint(x: 0, y: -refreshControl.frame.height), size: CGSize(width: 1, height: 1)), animated: animated) DispatchQueue.main.async { [weak self] in
// DispatchQueue.main.async { [weak self] in guard let self = self else { return }
// guard let self = self else { return } self.refreshControl.beginRefreshing()
// self.refreshControl.beginRefreshing() self.refreshControl.sendActions(for: .valueChanged)
// self.refreshControl.sendActions(for: .valueChanged) }
// } } else {
// } else { let indexPath = IndexPath(row: 0, section: 0)
// let indexPath = IndexPath(row: 0, section: 0) guard viewModel.diffableDataSource?.itemIdentifier(for: indexPath) != nil else { return }
// guard viewModel.diffableDataSource?.itemIdentifier(for: indexPath) != nil else { return } // save position
// // save position savePositionBeforeScrollToTop()
// savePositionBeforeScrollToTop() tableView.scrollToRow(at: indexPath, at: .top, animated: true)
// tableView.scrollToRow(at: indexPath, at: .top, animated: true) }
// }
} }
} }

View File

@ -42,7 +42,7 @@ extension NotificationTableViewCell {
case .feed(let feed): case .feed(let feed):
notificationView.configure(feed: feed) notificationView.configure(feed: feed)
} }
//
self.delegate = delegate self.delegate = delegate
} }

View File

@ -1,94 +0,0 @@
//
// NotificationViewModel+Diffable.swift
// Mastodon
//
// Created by sxiaojian on 2021/4/13.
//
import CoreData
import CoreDataStack
import os.log
import UIKit
import MastodonSDK
//extension NotificationViewModel: NSFetchedResultsControllerDelegate {
// func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
// os_log("%{public}s[%{public}ld], %{public}s", (#file as NSString).lastPathComponent, #line, #function)
// }
//
// func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
// os_log("%{public}s[%{public}ld], %{public}s", (#file as NSString).lastPathComponent, #line, #function)
//
// guard let tableView = self.tableView else { return }
// guard let navigationBar = contentOffsetAdjustableTimelineViewControllerDelegate?.navigationBar() else { return }
//
// guard let diffableDataSource = self.diffableDataSource else { return }
//
// let predicate: NSPredicate = {
// let notificationTypePredicate = MastodonNotification.predicate(
// validTypesRaws: Mastodon.Entity.Notification.NotificationType.knownCases.map { $0.rawValue }
// )
// return fetchedResultsController.fetchRequest.predicate.flatMap {
// NSCompoundPredicate(andPredicateWithSubpredicates: [$0, notificationTypePredicate])
// } ?? notificationTypePredicate
// }()
// let parentManagedObjectContext = fetchedResultsController.managedObjectContext
// let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
// managedObjectContext.parent = parentManagedObjectContext
//
// managedObjectContext.perform {
// let notifications: [MastodonNotification] = {
// let request = MastodonNotification.sortedFetchRequest
// request.returnsObjectsAsFaults = false
// request.predicate = predicate
// do {
// return try managedObjectContext.fetch(request)
// } catch {
// assertionFailure(error.localizedDescription)
// return []
// }
// }()
//
// DispatchQueue.main.async {
// let oldSnapshot = diffableDataSource.snapshot()
// var oldSnapshotAttributeDict: [NSManagedObjectID : Item.StatusAttribute] = [:]
// for item in oldSnapshot.itemIdentifiers {
// guard case let .notification(objectID, attribute) = item else { continue }
// oldSnapshotAttributeDict[objectID] = attribute
// }
// var newSnapshot = NSDiffableDataSourceSnapshot<NotificationSection, NotificationItem>()
// newSnapshot.appendSections([.main])
//
// let segment = self.selectedIndex.value
// switch segment {
// case .everyThing:
// let items: [NotificationItem] = notifications.map { notification in
// let attribute: Item.StatusAttribute = oldSnapshotAttributeDict[notification.objectID] ?? Item.StatusAttribute()
// return NotificationItem.notification(objectID: notification.objectID, attribute: attribute)
// }
// newSnapshot.appendItems(items, toSection: .main)
// case .mentions:
// let items: [NotificationItem] = notifications.map { notification in
// let attribute: Item.StatusAttribute = oldSnapshotAttributeDict[notification.objectID] ?? Item.StatusAttribute()
// return NotificationItem.notificationStatus(objectID: notification.objectID, attribute: attribute)
// }
// newSnapshot.appendItems(items, toSection: .main)
// }
//
// if !notifications.isEmpty, self.noMoreNotification.value == false {
// newSnapshot.appendItems([.bottomLoader], toSection: .main)
// }
//
// self.isFetchingLatestNotification.value = false
//
// diffableDataSource.apply(newSnapshot, animatingDifferences: false) { [weak self] in
// guard let self = self else { return }
// self.dataSourceDidUpdated.send()
// }
// }
// }
// }
//
//}

View File

@ -25,24 +25,54 @@ extension MediaView {
return status.publisher(for: \.attachments) return status.publisher(for: \.attachments)
.map { attachments -> [MediaView.Configuration] in .map { attachments -> [MediaView.Configuration] in
return attachments.map { attachment -> MediaView.Configuration in return attachments.map { attachment -> MediaView.Configuration in
switch attachment.kind { let configuration: MediaView.Configuration = {
case .image: switch attachment.kind {
let info = MediaView.Configuration.ImageInfo( case .image:
aspectRadio: attachment.size, let info = MediaView.Configuration.ImageInfo(
assetURL: attachment.assetURL aspectRadio: attachment.size,
assetURL: attachment.assetURL
)
return .init(
info: .image(info: info),
blurhash: attachment.blurhash
)
case .video:
let info = videoInfo(from: attachment)
return .init(
info: .video(info: info),
blurhash: attachment.blurhash
)
case .gifv:
let info = videoInfo(from: attachment)
return .init(
info: .gif(info: info),
blurhash: attachment.blurhash
)
case .audio:
// TODO:
let info = videoInfo(from: attachment)
return .init(
info: .video(info: info),
blurhash: attachment.blurhash
)
} // end switch
}()
if let assetURL = configuration.assetURL,
let blurhash = configuration.blurhash
{
AppContext.shared.blurhashImageCacheService.image(
blurhash: blurhash,
size: configuration.aspectRadio,
url: assetURL
) )
return .image(info: info) .assign(to: \.blurhashImage, on: configuration)
case .video: .store(in: &configuration.blurhashImageDisposeBag)
let info = videoInfo(from: attachment)
return .video(info: info)
case .gifv:
let info = videoInfo(from: attachment)
return .gif(info: info)
case .audio:
// TODO:
let info = videoInfo(from: attachment)
return .video(info: info)
} }
configuration.isReveal = status.sensitive ? status.isMediaSensitiveToggled : true
return configuration
} }
} }
.eraseToAnyPublisher() .eraseToAnyPublisher()

View File

@ -92,13 +92,7 @@ extension NotificationView {
.assign(to: \.authorUsername, on: viewModel) .assign(to: \.authorUsername, on: viewModel)
.store(in: &disposeBag) .store(in: &disposeBag)
// timestamp // timestamp
viewModel.timestampFormatter = { (date: Date) in viewModel.timestamp = notification.createAt
date.localizedSlowedTimeAgoSinceNow
}
notification.publisher(for: \.createAt)
.map { $0 as Date? }
.assign(to: \.timestamp, on: viewModel)
.store(in: &disposeBag)
// notification type indicator // notification type indicator
Publishers.CombineLatest3( Publishers.CombineLatest3(
notification.publisher(for: \.typeRaw), notification.publisher(for: \.typeRaw),
@ -111,7 +105,7 @@ extension NotificationView {
self.viewModel.notificationIndicatorText = nil self.viewModel.notificationIndicatorText = nil
return return
} }
func createMetaContent(text: String, emojis: MastodonContent.Emojis) -> MetaContent { func createMetaContent(text: String, emojis: MastodonContent.Emojis) -> MetaContent {
let content = MastodonContent(content: text, emojis: emojis) let content = MastodonContent(content: text, emojis: emojis)
guard let metaContent = try? MastodonMetaContent.convert(document: content) else { guard let metaContent = try? MastodonMetaContent.convert(document: content) else {
@ -119,7 +113,7 @@ extension NotificationView {
} }
return metaContent return metaContent
} }
// TODO: fix the i18n. The subject should assert place at the string beginning // TODO: fix the i18n. The subject should assert place at the string beginning
switch type { switch type {
case .follow: case .follow:

View File

@ -173,14 +173,10 @@ extension StatusView {
.map { $0 as String? } .map { $0 as String? }
.assign(to: \.authorUsername, on: viewModel) .assign(to: \.authorUsername, on: viewModel)
.store(in: &disposeBag) .store(in: &disposeBag)
// locked
// // protected author.publisher(for: \.locked)
// author.publisher(for: \.locked) .assign(to: \.locked, on: viewModel)
// .assign(to: \.protected, on: viewModel) .store(in: &disposeBag)
// .store(in: &disposeBag)
// // visibility
// viewModel.visibility = status.visibility.asStatusVisibility
// isMuting // isMuting
Publishers.CombineLatest( Publishers.CombineLatest(
viewModel.$userIdentifier, viewModel.$userIdentifier,
@ -267,42 +263,22 @@ extension StatusView {
status.publisher(for: \.isContentSensitiveToggled) status.publisher(for: \.isContentSensitiveToggled)
.assign(to: \.isContentSensitiveToggled, on: viewModel) .assign(to: \.isContentSensitiveToggled, on: viewModel)
.store(in: &disposeBag) .store(in: &disposeBag)
status.publisher(for: \.isMediaSensitiveToggled)
.assign(to: \.isMediaSensitiveToggled, on: viewModel)
.store(in: &disposeBag)
// viewModel.source = status.source // viewModel.source = status.source
} }
private func configureMedia(status: Status) { private func configureMedia(status: Status) {
let status = status.reblog ?? status let status = status.reblog ?? status
// mediaGridContainerView.viewModel.resetContentWarningOverlay() viewModel.isMediaSensitive = status.sensitive && !status.attachments.isEmpty // some servers set media sensitive even empty attachments
// viewModel.isMediaSensitiveSwitchable = true
viewModel.isMediaSensitive = status.sensitive
MediaView.configuration(status: status) MediaView.configuration(status: status)
.assign(to: \.mediaViewConfigurations, on: viewModel) .assign(to: \.mediaViewConfigurations, on: viewModel)
.store(in: &disposeBag) .store(in: &disposeBag)
// // set directly without delay status.publisher(for: \.isMediaSensitiveToggled)
// viewModel.isMediaSensitiveToggled = status.isMediaSensitiveToggled .assign(to: \.isMediaSensitiveToggled, on: viewModel)
// viewModel.isMediaSensitive = status.isMediaSensitive .store(in: &disposeBag)
// mediaGridContainerView.configureOverlayDisplay(
// isDisplay: status.isMediaSensitiveToggled ? !status.isMediaSensitive : !status.isMediaSensitive,
// animated: false
// )
//
// status.publisher(for: \.isMediaSensitive)
// .receive(on: DispatchQueue.main)
// .assign(to: \.isMediaSensitive, on: viewModel)
// .store(in: &disposeBag)
//
// status.publisher(for: \.isMediaSensitiveToggled)
// .receive(on: DispatchQueue.main)
// .assign(to: \.isMediaSensitiveToggled, on: viewModel)
// .store(in: &disposeBag)
} }
private func configurePoll(status: Status) { private func configurePoll(status: Status) {

View File

@ -8,13 +8,19 @@
import UIKit import UIKit
import Combine import Combine
final class BlurhashImageCacheService { public final class BlurhashImageCacheService {
static let edgeMaxLength: CGFloat = 20
let cache = NSCache<Key, UIImage>() let cache = NSCache<Key, UIImage>()
let workingQueue = DispatchQueue(label: "org.joinmastodon.app.BlurhashImageCacheService.working-queue", qos: .userInitiated, attributes: .concurrent) let workingQueue = DispatchQueue(label: "org.joinmastodon.app.BlurhashImageCacheService.working-queue", qos: .userInitiated, attributes: .concurrent)
func image(blurhash: String, size: CGSize, url: URL) -> AnyPublisher<UIImage?, Never> { public func image(
blurhash: String,
size: CGSize,
url: String
) -> AnyPublisher<UIImage?, Never> {
let key = Key(blurhash: blurhash, size: size, url: url) let key = Key(blurhash: blurhash, size: size, url: url)
if let image = self.cache.object(forKey: key) { if let image = self.cache.object(forKey: key) {
@ -23,7 +29,7 @@ final class BlurhashImageCacheService {
return Future { promise in return Future { promise in
self.workingQueue.async { self.workingQueue.async {
guard let image = BlurhashImageCacheService.blurhashImage(blurhash: blurhash, size: size, url: url) else { guard let image = BlurhashImageCacheService.blurhashImage(blurhash: blurhash, size: size) else {
promise(.success(nil)) promise(.success(nil))
return return
} }
@ -33,27 +39,25 @@ final class BlurhashImageCacheService {
} }
.receive(on: RunLoop.main) .receive(on: RunLoop.main)
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
static func blurhashImage(blurhash: String, size: CGSize, url: URL) -> UIImage? { static func blurhashImage(blurhash: String, size: CGSize) -> UIImage? {
fatalError() let imageSize: CGSize = {
// let imageSize: CGSize = { let aspectRadio = size.width / size.height
// let aspectRadio = size.width / size.height if size.width > size.height {
// if size.width > size.height { let width: CGFloat = BlurhashImageCacheService.edgeMaxLength
// let width: CGFloat = MosaicMeta.edgeMaxLength let height = width / aspectRadio
// let height = width / aspectRadio return CGSize(width: width, height: height)
// return CGSize(width: width, height: height) } else {
// } else { let height: CGFloat = BlurhashImageCacheService.edgeMaxLength
// let height: CGFloat = MosaicMeta.edgeMaxLength let width = height * aspectRadio
// let width = height * aspectRadio return CGSize(width: width, height: height)
// return CGSize(width: width, height: height) }
// } }()
// }()
// let image = UIImage(blurHash: blurhash, size: imageSize)
// let image = UIImage(blurHash: blurhash, size: imageSize)
// return image
// return image
} }
} }
@ -62,9 +66,9 @@ extension BlurhashImageCacheService {
class Key: NSObject { class Key: NSObject {
let blurhash: String let blurhash: String
let size: CGSize let size: CGSize
let url: URL let url: String
init(blurhash: String, size: CGSize, url: URL) { init(blurhash: String, size: CGSize, url: String) {
self.blurhash = blurhash self.blurhash = blurhash
self.size = size self.size = size
self.url = url self.url = url
@ -83,6 +87,5 @@ extension BlurhashImageCacheService {
size.height.hashValue ^ size.height.hashValue ^
url.hashValue url.hashValue
} }
} }
} }

View File

@ -129,11 +129,6 @@
<relationship name="account" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="notifications" inverseEntity="MastodonUser"/> <relationship name="account" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="notifications" inverseEntity="MastodonUser"/>
<relationship name="feeds" toMany="YES" deletionRule="Cascade" destinationEntity="Feed" inverseName="notification" inverseEntity="Feed"/> <relationship name="feeds" toMany="YES" deletionRule="Cascade" destinationEntity="Feed" inverseName="notification" inverseEntity="Feed"/>
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="notifications" inverseEntity="Status"/> <relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="notifications" inverseEntity="Status"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="id"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity> </entity>
<entity name="Poll" representedClassName="CoreDataStack.Poll" syncable="YES"> <entity name="Poll" representedClassName="CoreDataStack.Poll" syncable="YES">
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/> <attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>

View File

@ -0,0 +1,12 @@
//
// DateTimeProvider.swift
//
//
// Created by MainasuK on 2022-1-29.
//
import Foundation
public protocol DateTimeProvider {
func shortTimeAgoSinceNow(to date: Date?) -> String?
}

View File

@ -9,6 +9,40 @@ import Foundation
import MastodonAsset import MastodonAsset
import MastodonLocalization import MastodonLocalization
extension Date {
public static let relativeTimestampFormatter: RelativeDateTimeFormatter = {
let formatter = RelativeDateTimeFormatter()
formatter.dateTimeStyle = .numeric
formatter.unitsStyle = .full
return formatter
}()
public var localizedSlowedTimeAgoSinceNow: String {
return self.localizedTimeAgo(since: Date(), isSlowed: true, isAbbreviated: true)
}
public var localizedTimeAgoSinceNow: String {
return self.localizedTimeAgo(since: Date(), isSlowed: false, isAbbreviated: false)
}
public func localizedTimeAgo(since date: Date, isSlowed: Bool, isAbbreviated: Bool) -> String {
let earlierDate = date < self ? date : self
let latestDate = earlierDate == date ? self : date
if isSlowed, earlierDate.timeIntervalSince(latestDate) >= -60 {
return L10n.Common.Controls.Timeline.Timestamp.now
} else {
if isAbbreviated {
return latestDate.localizedShortTimeAgo(since: earlierDate)
} else {
return Date.relativeTimestampFormatter.localizedString(for: earlierDate, relativeTo: latestDate)
}
}
}
}
extension Date { extension Date {
public func localizedShortTimeAgo(since date: Date) -> String { public func localizedShortTimeAgo(since date: Date) -> String {

View File

@ -12,13 +12,25 @@ import CoreData
import Photos import Photos
extension MediaView { extension MediaView {
public enum Configuration: Hashable { public class Configuration: Hashable {
case image(info: ImageInfo)
case gif(info: VideoInfo) public let info: Info
case video(info: VideoInfo) public let blurhash: String?
@Published public var isReveal = true
@Published public var blurhashImage: UIImage?
public var blurhashImageDisposeBag = Set<AnyCancellable>()
public init(
info: MediaView.Configuration.Info,
blurhash: String?
) {
self.info = info
self.blurhash = blurhash
}
public var aspectRadio: CGSize { public var aspectRadio: CGSize {
switch self { switch info {
case .image(let info): return info.aspectRadio case .image(let info): return info.aspectRadio
case .gif(let info): return info.aspectRadio case .gif(let info): return info.aspectRadio
case .video(let info): return info.aspectRadio case .video(let info): return info.aspectRadio
@ -26,7 +38,7 @@ extension MediaView {
} }
public var assetURL: String? { public var assetURL: String? {
switch self { switch info {
case .image(let info): case .image(let info):
return info.assetURL return info.assetURL
case .gif(let info): case .gif(let info):
@ -37,7 +49,7 @@ extension MediaView {
} }
public var resourceType: PHAssetResourceType { public var resourceType: PHAssetResourceType {
switch self { switch info {
case .image: case .image:
return .photo return .photo
case .gif: case .gif:
@ -47,51 +59,72 @@ extension MediaView {
} }
} }
public struct ImageInfo: Hashable { public static func == (lhs: MediaView.Configuration, rhs: MediaView.Configuration) -> Bool {
public let aspectRadio: CGSize return lhs.info == rhs.info
public let assetURL: String? && lhs.blurhash == rhs.blurhash
&& lhs.isReveal == rhs.isReveal
public init(
aspectRadio: CGSize,
assetURL: String?
) {
self.aspectRadio = aspectRadio
self.assetURL = assetURL
}
public func hash(into hasher: inout Hasher) {
hasher.combine(aspectRadio.width)
hasher.combine(aspectRadio.height)
assetURL.flatMap { hasher.combine($0) }
}
} }
public struct VideoInfo: Hashable { public func hash(into hasher: inout Hasher) {
public let aspectRadio: CGSize hasher.combine(info)
public let assetURL: String? hasher.combine(blurhash)
public let previewURL: String?
public let durationMS: Int?
public init(
aspectRadio: CGSize,
assetURL: String?,
previewURL: String?,
durationMS: Int?
) {
self.aspectRadio = aspectRadio
self.assetURL = assetURL
self.previewURL = previewURL
self.durationMS = durationMS
}
public func hash(into hasher: inout Hasher) {
hasher.combine(aspectRadio.width)
hasher.combine(aspectRadio.height)
assetURL.flatMap { hasher.combine($0) }
previewURL.flatMap { hasher.combine($0) }
durationMS.flatMap { hasher.combine($0) }
}
} }
} }
} }
extension MediaView.Configuration {
public enum Info: Hashable {
case image(info: ImageInfo)
case gif(info: VideoInfo)
case video(info: VideoInfo)
}
public struct ImageInfo: Hashable {
public let aspectRadio: CGSize
public let assetURL: String?
public init(
aspectRadio: CGSize,
assetURL: String?
) {
self.aspectRadio = aspectRadio
self.assetURL = assetURL
}
public func hash(into hasher: inout Hasher) {
hasher.combine(aspectRadio.width)
hasher.combine(aspectRadio.height)
assetURL.flatMap { hasher.combine($0) }
}
}
public struct VideoInfo: Hashable {
public let aspectRadio: CGSize
public let assetURL: String?
public let previewURL: String?
public let durationMS: Int?
public init(
aspectRadio: CGSize,
assetURL: String?,
previewURL: String?,
durationMS: Int?
) {
self.aspectRadio = aspectRadio
self.assetURL = assetURL
self.previewURL = previewURL
self.durationMS = durationMS
}
public func hash(into hasher: inout Hasher) {
hasher.combine(aspectRadio.width)
hasher.combine(aspectRadio.height)
assetURL.flatMap { hasher.combine($0) }
previewURL.flatMap { hasher.combine($0) }
durationMS.flatMap { hasher.combine($0) }
}
}
}

View File

@ -8,9 +8,12 @@
import AVKit import AVKit
import UIKit import UIKit
import Combine
public final class MediaView: UIView { public final class MediaView: UIView {
var _disposeBag = Set<AnyCancellable>()
public static let cornerRadius: CGFloat = 0 public static let cornerRadius: CGFloat = 0
public static let durationFormatter: DateComponentsFormatter = { public static let durationFormatter: DateComponentsFormatter = {
let formatter = DateComponentsFormatter() let formatter = DateComponentsFormatter()
@ -23,6 +26,14 @@ public final class MediaView: UIView {
public private(set) var configuration: Configuration? public private(set) var configuration: Configuration?
private(set) lazy var blurhashImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.isUserInteractionEnabled = false
imageView.layer.masksToBounds = true // clip overflow
return imageView
}()
private(set) lazy var imageView: UIImageView = { private(set) lazy var imageView: UIImageView = {
let imageView = UIImageView() let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill imageView.contentMode = .scaleAspectFill
@ -91,7 +102,7 @@ extension MediaView {
setupContainerViewHierarchy() setupContainerViewHierarchy()
switch configuration { switch configuration.info {
case .image(let info): case .image(let info):
configure(image: info) configure(image: info)
case .gif(let info): case .gif(let info):
@ -99,6 +110,31 @@ extension MediaView {
case .video(let info): case .video(let info):
configure(video: info) configure(video: info)
} }
if let blurhash = configuration.blurhash {
configure(blurhash: blurhash)
configuration.$blurhashImage
.receive(on: DispatchQueue.main)
.assign(to: \.image, on: blurhashImageView)
.store(in: &_disposeBag)
blurhashImageView.alpha = configuration.isReveal ? 0 : 1
}
configuration.$isReveal
.dropFirst()
.removeDuplicates()
.receive(on: DispatchQueue.main)
.sink { [weak self] isReveal in
guard let self = self else { return }
let animator = UIViewPropertyAnimator(duration: 0.3, curve: .easeInOut)
animator.addAnimations {
self.blurhashImageView.alpha = isReveal ? 0 : 1
}
animator.startAnimation()
}
.store(in: &_disposeBag)
} }
private func configure(image info: Configuration.ImageInfo) { private func configure(image info: Configuration.ImageInfo) {
@ -122,7 +158,7 @@ extension MediaView {
placeholderImage: placeholder placeholderImage: placeholder
) )
} }
private func configure(gif info: Configuration.VideoInfo) { private func configure(gif info: Configuration.VideoInfo) {
// use view controller as View here // use view controller as View here
playerViewController.view.translatesAutoresizingMaskIntoConstraints = false playerViewController.view.translatesAutoresizingMaskIntoConstraints = false
@ -188,7 +224,22 @@ extension MediaView {
} }
private func configure(blurhash: String) {
blurhashImageView.translatesAutoresizingMaskIntoConstraints = false
container.addSubview(blurhashImageView)
NSLayoutConstraint.activate([
blurhashImageView.topAnchor.constraint(equalTo: container.topAnchor),
blurhashImageView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
blurhashImageView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
blurhashImageView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
])
blurhashImageView.backgroundColor = .systemGray
}
public func prepareForReuse() { public func prepareForReuse() {
_disposeBag.removeAll()
// reset appearance // reset appearance
alpha = 1 alpha = 1
@ -207,6 +258,11 @@ extension MediaView {
playerViewController.player = nil playerViewController.player = nil
playerLooper = nil playerLooper = nil
// blurhash
blurhashImageView.removeFromSuperview()
blurhashImageView.removeConstraints(blurhashImageView.constraints)
blurhashImageView.image = nil
// reset indicator // reset indicator
indicatorBlurEffectView.removeFromSuperview() indicatorBlurEffectView.removeFromSuperview()

View File

@ -34,7 +34,6 @@ extension NotificationView {
@Published public var isBlocking = false @Published public var isBlocking = false
@Published public var timestamp: Date? @Published public var timestamp: Date?
public var timestampFormatter: ((_ date: Date) -> String)?
let timestampUpdatePublisher = Timer.publish(every: 1.0, on: .main, in: .common) let timestampUpdatePublisher = Timer.publish(every: 1.0, on: .main, in: .common)
.autoconnect() .autoconnect()
@ -100,13 +99,12 @@ extension NotificationView.ViewModel {
) )
.sink { [weak self] timestamp, _ in .sink { [weak self] timestamp, _ in
guard let self = self else { return } guard let self = self else { return }
guard let timestamp = timestamp, guard let timestamp = timestamp else {
let text = self.timestampFormatter?(timestamp)
else {
notificationView.dateLabel.configure(content: PlaintextMetaContent(string: "")) notificationView.dateLabel.configure(content: PlaintextMetaContent(string: ""))
return return
} }
let text = timestamp.localizedTimeAgoSinceNow
notificationView.dateLabel.configure(content: PlaintextMetaContent(string: text)) notificationView.dateLabel.configure(content: PlaintextMetaContent(string: text))
} }
.store(in: &disposeBag) .store(in: &disposeBag)

View File

@ -35,6 +35,8 @@ extension StatusView {
@Published public var authorName: MetaContent? @Published public var authorName: MetaContent?
@Published public var authorUsername: String? @Published public var authorUsername: String?
@Published public var locked = false
@Published public var isMyself = false @Published public var isMyself = false
@Published public var isMuting = false @Published public var isMuting = false
@Published public var isBlocking = false @Published public var isBlocking = false
@ -125,6 +127,10 @@ extension StatusView {
} }
init() { init() {
// isReblogEnabled
$locked
.map { !$0 }
.assign(to: &$isReblogEnabled)
// isContentSensitive // isContentSensitive
$spoilerContent $spoilerContent
.map { $0 != nil } .map { $0 != nil }
@ -141,14 +147,14 @@ extension StatusView {
$isContentSensitive, $isContentSensitive,
$isContentSensitiveToggled $isContentSensitiveToggled
) )
.map { $1 ? $0 : !$0 } .map { $0 ? $1 : true }
.assign(to: &$isContentReveal) .assign(to: &$isContentReveal)
// $isMediaReveal // $isMediaReveal
Publishers.CombineLatest( Publishers.CombineLatest(
$isMediaSensitive, $isMediaSensitive,
$isMediaSensitiveToggled $isMediaSensitiveToggled
) )
.map { $1 ? !$0 : $0} .map { $0 ? $1 : true }
.assign(to: &$isMediaReveal) .assign(to: &$isMediaReveal)
} }
} }
@ -300,19 +306,16 @@ extension StatusView.ViewModel {
} }
} }
.store(in: &disposeBag) .store(in: &disposeBag)
Publishers.CombineLatest( $isSensitive
$isContentSensitive, .sink { isSensitive in
$isMediaSensitive if isSensitive {
) let image = Asset.Human.eyeCircleFill.image
.sink { isContentSensitive, isMediaSensitive in statusView.contentWarningToggleButton.setImage(image, for: .normal)
if isContentSensitive || isMediaSensitive { statusView.contentWarningToggleButton.tintColor = .systemGray
let image = Asset.Human.eyeCircleFill.image statusView.setContentWarningToggleButtonDisplay()
statusView.contentWarningToggleButton.setImage(image, for: .normal) }
statusView.contentWarningToggleButton.tintColor = .systemGray
statusView.setContentWarningToggleButtonDisplay()
} }
} .store(in: &disposeBag)
.store(in: &disposeBag)
// $spoilerContent // $spoilerContent
// .sink { metaContent in // .sink { metaContent in
// guard let metaContent = metaContent else { // guard let metaContent = metaContent else {
@ -411,6 +414,17 @@ extension StatusView.ViewModel {
} }
.store(in: &disposeBag) .store(in: &disposeBag)
Publishers.CombineLatest(
$mediaViewConfigurations,
$isMediaReveal
)
.sink { configurations, isMediaReveal in
for configuration in configurations {
configuration.isReveal = isMediaReveal
}
}
.store(in: &disposeBag)
// FIXME: // FIXME:
statusView.mediaGridContainerView.viewModel.isContentWarningOverlayDisplay = false statusView.mediaGridContainerView.viewModel.isContentWarningOverlayDisplay = false
// $isMediaReveal // $isMediaReveal

View File

@ -559,6 +559,7 @@ extension StatusView.Style {
statusView.usernameTrialingDotLabel.removeFromSuperview() statusView.usernameTrialingDotLabel.removeFromSuperview()
statusView.dateLabel.removeFromSuperview() statusView.dateLabel.removeFromSuperview()
statusView.contentContainer.removeFromSuperview() statusView.contentContainer.removeFromSuperview()
statusView.spoilerOverlayView.removeFromSuperview()
statusView.mediaContainerView.removeFromSuperview() statusView.mediaContainerView.removeFromSuperview()
statusView.pollContainerView.removeFromSuperview() statusView.pollContainerView.removeFromSuperview()
statusView.statusVisibilityView.removeFromSuperview() statusView.statusVisibilityView.removeFromSuperview()