Implement post editing / edit history (#875)
Co-authored-by: Marcus Kida <marcus.kida@bearologics.com> Co-authored-by: Jed Fox <git@jedfox.com>
This commit is contained in:
parent
625ebb00fb
commit
0c224f47df
|
@ -101,7 +101,8 @@
|
||||||
"translate_post": {
|
"translate_post": {
|
||||||
"title": "Translate from %s",
|
"title": "Translate from %s",
|
||||||
"unknown_language": "Unknown"
|
"unknown_language": "Unknown"
|
||||||
}
|
},
|
||||||
|
"edit_post": "Edit"
|
||||||
},
|
},
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"home": "Home",
|
"home": "Home",
|
||||||
|
@ -193,7 +194,20 @@
|
||||||
"expand_image_hint": "Expands the image. Double-tap and hold to show actions",
|
"expand_image_hint": "Expands the image. Double-tap and hold to show actions",
|
||||||
"expand_gif_hint": "Expands the GIF. Double-tap and hold to show actions",
|
"expand_gif_hint": "Expands the GIF. Double-tap and hold to show actions",
|
||||||
"expand_video_hint": "Shows the video player. Double-tap and hold to show actions"
|
"expand_video_hint": "Shows the video player. Double-tap and hold to show actions"
|
||||||
|
},
|
||||||
|
"posted_via_application": "%s via %s",
|
||||||
|
"buttons": {
|
||||||
|
"reblogs_title": "Reblogs",
|
||||||
|
"favorites_title": "Favorites",
|
||||||
|
"edit_history_title": "Edit History",
|
||||||
|
"edit_history_detail": "Last edit %s"
|
||||||
|
},
|
||||||
|
"edited_at_timestamp_prefix": "Edited %s",
|
||||||
|
"edit_history": {
|
||||||
|
"title": "Edit History",
|
||||||
|
"original_post": "Original Post · %s"
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
"friendship": {
|
"friendship": {
|
||||||
"follow": "Follow",
|
"follow": "Follow",
|
||||||
|
@ -437,7 +451,8 @@
|
||||||
"compose": {
|
"compose": {
|
||||||
"title": {
|
"title": {
|
||||||
"new_post": "New Post",
|
"new_post": "New Post",
|
||||||
"new_reply": "New Reply"
|
"new_reply": "New Reply",
|
||||||
|
"edit_post": "Edit Post"
|
||||||
},
|
},
|
||||||
"media_selection": {
|
"media_selection": {
|
||||||
"camera": "Take Photo",
|
"camera": "Take Photo",
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
2A1FE47E2938C11200784BF1 /* Collection+IsNotEmpty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A1FE47D2938C11200784BF1 /* Collection+IsNotEmpty.swift */; };
|
2A1FE47E2938C11200784BF1 /* Collection+IsNotEmpty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A1FE47D2938C11200784BF1 /* Collection+IsNotEmpty.swift */; };
|
||||||
2A33062D2987DBFA001D4C51 /* FollowersCountHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A33062C2987DBFA001D4C51 /* FollowersCountHistory.swift */; };
|
2A33062D2987DBFA001D4C51 /* FollowersCountHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A33062C2987DBFA001D4C51 /* FollowersCountHistory.swift */; };
|
||||||
2A33AB662982C4AF008A7FB1 /* FollowersCountWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A33AB652982C4AF008A7FB1 /* FollowersCountWidgetView.swift */; };
|
2A33AB662982C4AF008A7FB1 /* FollowersCountWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A33AB652982C4AF008A7FB1 /* FollowersCountWidgetView.swift */; };
|
||||||
|
2A3D9B7E29A8F33A00F30313 /* StatusHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3D9B7D29A8F33A00F30313 /* StatusHistoryView.swift */; };
|
||||||
2A3F6FE3292ECB5E002E6DA7 /* FollowedTagsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3F6FE2292ECB5E002E6DA7 /* FollowedTagsViewModel.swift */; };
|
2A3F6FE3292ECB5E002E6DA7 /* FollowedTagsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3F6FE2292ECB5E002E6DA7 /* FollowedTagsViewModel.swift */; };
|
||||||
2A3F6FE5292F6E44002E6DA7 /* FollowedTagsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3F6FE4292F6E44002E6DA7 /* FollowedTagsTableViewCell.swift */; };
|
2A3F6FE5292F6E44002E6DA7 /* FollowedTagsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3F6FE4292F6E44002E6DA7 /* FollowedTagsTableViewCell.swift */; };
|
||||||
2A506CF4292CD85800059C37 /* FollowedTagsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A506CF3292CD85800059C37 /* FollowedTagsViewController.swift */; };
|
2A506CF4292CD85800059C37 /* FollowedTagsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A506CF3292CD85800059C37 /* FollowedTagsViewController.swift */; };
|
||||||
|
@ -134,6 +135,8 @@
|
||||||
9E44C7202967AD17004B2A72 /* MastodonSDKDynamic in Frameworks */ = {isa = PBXBuildFile; productRef = 9E44C71F2967AD17004B2A72 /* MastodonSDKDynamic */; };
|
9E44C7202967AD17004B2A72 /* MastodonSDKDynamic in Frameworks */ = {isa = PBXBuildFile; productRef = 9E44C71F2967AD17004B2A72 /* MastodonSDKDynamic */; };
|
||||||
9E44C7222967AD17004B2A72 /* MastodonSDKDynamic in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 9E44C71F2967AD17004B2A72 /* MastodonSDKDynamic */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
9E44C7222967AD17004B2A72 /* MastodonSDKDynamic in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 9E44C71F2967AD17004B2A72 /* MastodonSDKDynamic */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
||||||
C24C97032922F30500BAE8CB /* RefreshControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = C24C97022922F30500BAE8CB /* RefreshControl.swift */; };
|
C24C97032922F30500BAE8CB /* RefreshControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = C24C97022922F30500BAE8CB /* RefreshControl.swift */; };
|
||||||
|
D808B94C296ECFDC0031EB1E /* StatusEditHistoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D808B94B296ECFDC0031EB1E /* StatusEditHistoryViewModel.swift */; };
|
||||||
|
D808B94E296EFBBA0031EB1E /* StatusEditHistoryTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D808B94D296EFBBA0031EB1E /* StatusEditHistoryTableViewCell.swift */; };
|
||||||
D8099078294BC8A30050219F /* PrivacyTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8099077294BC8A30050219F /* PrivacyTableViewController.swift */; };
|
D8099078294BC8A30050219F /* PrivacyTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8099077294BC8A30050219F /* PrivacyTableViewController.swift */; };
|
||||||
D809907A294BC9390050219F /* PrivacyTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8099079294BC9390050219F /* PrivacyTableViewCell.swift */; };
|
D809907A294BC9390050219F /* PrivacyTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8099079294BC9390050219F /* PrivacyTableViewCell.swift */; };
|
||||||
D809907C294D25510050219F /* PrivacyViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D809907B294D25510050219F /* PrivacyViewModel.swift */; };
|
D809907C294D25510050219F /* PrivacyViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D809907B294D25510050219F /* PrivacyViewModel.swift */; };
|
||||||
|
@ -145,6 +148,8 @@
|
||||||
D8A6AB6C291C5136003AB663 /* MastodonLoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A6AB6B291C5136003AB663 /* MastodonLoginViewController.swift */; };
|
D8A6AB6C291C5136003AB663 /* MastodonLoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A6AB6B291C5136003AB663 /* MastodonLoginViewController.swift */; };
|
||||||
D8A6FE5B293244B500666A47 /* WelcomeContentPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A6FE5A293244B500666A47 /* WelcomeContentPage.swift */; };
|
D8A6FE5B293244B500666A47 /* WelcomeContentPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A6FE5A293244B500666A47 /* WelcomeContentPage.swift */; };
|
||||||
D8A6FE5F29324BBC00666A47 /* WelcomeContentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A6FE5E29324BBC00666A47 /* WelcomeContentCollectionViewCell.swift */; };
|
D8A6FE5F29324BBC00666A47 /* WelcomeContentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A6FE5E29324BBC00666A47 /* WelcomeContentCollectionViewCell.swift */; };
|
||||||
|
D8E5C346296DAB84007E76A7 /* DataSourceFacade+Status+History.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E5C345296DAB84007E76A7 /* DataSourceFacade+Status+History.swift */; };
|
||||||
|
D8E5C349296DB8A3007E76A7 /* StatusEditHistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E5C348296DB8A3007E76A7 /* StatusEditHistoryViewController.swift */; };
|
||||||
DB0009A626AEE5DC009B9D2D /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = DB0009A926AEE5DC009B9D2D /* Intents.intentdefinition */; settings = {ATTRIBUTES = (codegen, ); }; };
|
DB0009A626AEE5DC009B9D2D /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = DB0009A926AEE5DC009B9D2D /* Intents.intentdefinition */; settings = {ATTRIBUTES = (codegen, ); }; };
|
||||||
DB0009A726AEE5DC009B9D2D /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = DB0009A926AEE5DC009B9D2D /* Intents.intentdefinition */; };
|
DB0009A726AEE5DC009B9D2D /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = DB0009A926AEE5DC009B9D2D /* Intents.intentdefinition */; };
|
||||||
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140CE25C42AEE00F9F3CF /* OSLog.swift */; };
|
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140CE25C42AEE00F9F3CF /* OSLog.swift */; };
|
||||||
|
@ -615,6 +620,7 @@
|
||||||
2A33062C2987DBFA001D4C51 /* FollowersCountHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersCountHistory.swift; sourceTree = "<group>"; };
|
2A33062C2987DBFA001D4C51 /* FollowersCountHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersCountHistory.swift; sourceTree = "<group>"; };
|
||||||
2A33625329759B4200481A90 /* OpenInActionExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = OpenInActionExtension.entitlements; sourceTree = "<group>"; };
|
2A33625329759B4200481A90 /* OpenInActionExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = OpenInActionExtension.entitlements; sourceTree = "<group>"; };
|
||||||
2A33AB652982C4AF008A7FB1 /* FollowersCountWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersCountWidgetView.swift; sourceTree = "<group>"; };
|
2A33AB652982C4AF008A7FB1 /* FollowersCountWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersCountWidgetView.swift; sourceTree = "<group>"; };
|
||||||
|
2A3D9B7D29A8F33A00F30313 /* StatusHistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusHistoryView.swift; sourceTree = "<group>"; };
|
||||||
2A3F6FE2292ECB5E002E6DA7 /* FollowedTagsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedTagsViewModel.swift; sourceTree = "<group>"; };
|
2A3F6FE2292ECB5E002E6DA7 /* FollowedTagsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedTagsViewModel.swift; sourceTree = "<group>"; };
|
||||||
2A3F6FE4292F6E44002E6DA7 /* FollowedTagsTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedTagsTableViewCell.swift; sourceTree = "<group>"; };
|
2A3F6FE4292F6E44002E6DA7 /* FollowedTagsTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedTagsTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
2A506CF3292CD85800059C37 /* FollowedTagsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedTagsViewController.swift; sourceTree = "<group>"; };
|
2A506CF3292CD85800059C37 /* FollowedTagsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedTagsViewController.swift; sourceTree = "<group>"; };
|
||||||
|
@ -769,6 +775,8 @@
|
||||||
C3789232A52F43529CA67E95 /* Pods-MastodonIntent.asdk - debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonIntent.asdk - debug.xcconfig"; path = "Target Support Files/Pods-MastodonIntent/Pods-MastodonIntent.asdk - debug.xcconfig"; sourceTree = "<group>"; };
|
C3789232A52F43529CA67E95 /* Pods-MastodonIntent.asdk - debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonIntent.asdk - debug.xcconfig"; path = "Target Support Files/Pods-MastodonIntent/Pods-MastodonIntent.asdk - debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MastodonTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MastodonTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
D7D7CF93E262178800077512 /* Pods-Mastodon-AppShared.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-AppShared.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon-AppShared/Pods-Mastodon-AppShared.debug.xcconfig"; sourceTree = "<group>"; };
|
D7D7CF93E262178800077512 /* Pods-Mastodon-AppShared.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-AppShared.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon-AppShared/Pods-Mastodon-AppShared.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
D808B94B296ECFDC0031EB1E /* StatusEditHistoryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditHistoryViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
D808B94D296EFBBA0031EB1E /* StatusEditHistoryTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditHistoryTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
D8099077294BC8A30050219F /* PrivacyTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyTableViewController.swift; sourceTree = "<group>"; };
|
D8099077294BC8A30050219F /* PrivacyTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyTableViewController.swift; sourceTree = "<group>"; };
|
||||||
D8099079294BC9390050219F /* PrivacyTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyTableViewCell.swift; sourceTree = "<group>"; };
|
D8099079294BC9390050219F /* PrivacyTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
D809907B294D25510050219F /* PrivacyViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyViewModel.swift; sourceTree = "<group>"; };
|
D809907B294D25510050219F /* PrivacyViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
@ -786,6 +794,8 @@
|
||||||
D8A6FE6429325F5900666A47 /* StringsConvertor */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = StringsConvertor; sourceTree = "<group>"; };
|
D8A6FE6429325F5900666A47 /* StringsConvertor */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = StringsConvertor; sourceTree = "<group>"; };
|
||||||
D8A6FE6529325F5900666A47 /* ios-infoPlist.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "ios-infoPlist.json"; sourceTree = "<group>"; };
|
D8A6FE6529325F5900666A47 /* ios-infoPlist.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "ios-infoPlist.json"; sourceTree = "<group>"; };
|
||||||
D8A6FE6629325F5900666A47 /* Localizable.stringsdict */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; path = Localizable.stringsdict; sourceTree = "<group>"; };
|
D8A6FE6629325F5900666A47 /* Localizable.stringsdict */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; path = Localizable.stringsdict; sourceTree = "<group>"; };
|
||||||
|
D8E5C345296DAB84007E76A7 /* DataSourceFacade+Status+History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Status+History.swift"; sourceTree = "<group>"; };
|
||||||
|
D8E5C348296DB8A3007E76A7 /* StatusEditHistoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditHistoryViewController.swift; sourceTree = "<group>"; };
|
||||||
DB0009A826AEE5DC009B9D2D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; name = Base; path = Base.lproj/Intents.intentdefinition; sourceTree = "<group>"; };
|
DB0009A826AEE5DC009B9D2D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; name = Base; path = Base.lproj/Intents.intentdefinition; sourceTree = "<group>"; };
|
||||||
DB0009AD26AEE5E4009B9D2D /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Intents.strings; sourceTree = "<group>"; };
|
DB0009AD26AEE5E4009B9D2D /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Intents.strings; sourceTree = "<group>"; };
|
||||||
DB0140CE25C42AEE00F9F3CF /* OSLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSLog.swift; sourceTree = "<group>"; };
|
DB0140CE25C42AEE00F9F3CF /* OSLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSLog.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1824,6 +1834,17 @@
|
||||||
path = Localization;
|
path = Localization;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
D8E5C347296DB896007E76A7 /* Edit History */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D8E5C348296DB8A3007E76A7 /* StatusEditHistoryViewController.swift */,
|
||||||
|
D808B94B296ECFDC0031EB1E /* StatusEditHistoryViewModel.swift */,
|
||||||
|
D808B94D296EFBBA0031EB1E /* StatusEditHistoryTableViewCell.swift */,
|
||||||
|
2A3D9B7D29A8F33A00F30313 /* StatusHistoryView.swift */,
|
||||||
|
);
|
||||||
|
path = "Edit History";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
DB01409B25C40BB600F9F3CF /* Onboarding */ = {
|
DB01409B25C40BB600F9F3CF /* Onboarding */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -2378,6 +2399,7 @@
|
||||||
DB603110279EB38500A935FE /* DataSourceFacade+Mute.swift */,
|
DB603110279EB38500A935FE /* DataSourceFacade+Mute.swift */,
|
||||||
DB603112279EBEBA00A935FE /* DataSourceFacade+Block.swift */,
|
DB603112279EBEBA00A935FE /* DataSourceFacade+Block.swift */,
|
||||||
DB0FCB7327956939006C02E2 /* DataSourceFacade+Status.swift */,
|
DB0FCB7327956939006C02E2 /* DataSourceFacade+Status.swift */,
|
||||||
|
D8E5C345296DAB84007E76A7 /* DataSourceFacade+Status+History.swift */,
|
||||||
DB63F778279ABF9C00455B82 /* DataSourceFacade+Reblog.swift */,
|
DB63F778279ABF9C00455B82 /* DataSourceFacade+Reblog.swift */,
|
||||||
DB63F77A279ACAE500455B82 /* DataSourceFacade+Favorite.swift */,
|
DB63F77A279ACAE500455B82 /* DataSourceFacade+Favorite.swift */,
|
||||||
DB0FCB67279507EF006C02E2 /* DataSourceFacade+Meta.swift */,
|
DB0FCB67279507EF006C02E2 /* DataSourceFacade+Meta.swift */,
|
||||||
|
@ -2566,6 +2588,7 @@
|
||||||
DB938EEB2623F52600E5B6C1 /* Thread */ = {
|
DB938EEB2623F52600E5B6C1 /* Thread */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
D8E5C347296DB896007E76A7 /* Edit History */,
|
||||||
DB938EE52623F50700E5B6C1 /* ThreadViewController.swift */,
|
DB938EE52623F50700E5B6C1 /* ThreadViewController.swift */,
|
||||||
DB0FCB75279571C5006C02E2 /* ThreadViewController+DataSourceProvider.swift */,
|
DB0FCB75279571C5006C02E2 /* ThreadViewController+DataSourceProvider.swift */,
|
||||||
DB938EEC2623F79B00E5B6C1 /* ThreadViewModel.swift */,
|
DB938EEC2623F79B00E5B6C1 /* ThreadViewModel.swift */,
|
||||||
|
@ -3583,6 +3606,7 @@
|
||||||
DBDFF1902805543100557A48 /* DiscoveryPostsViewController.swift in Sources */,
|
DBDFF1902805543100557A48 /* DiscoveryPostsViewController.swift in Sources */,
|
||||||
DB697DD9278F4CED004EF2F7 /* HomeTimelineViewController+DataSourceProvider.swift in Sources */,
|
DB697DD9278F4CED004EF2F7 /* HomeTimelineViewController+DataSourceProvider.swift in Sources */,
|
||||||
DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */,
|
DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */,
|
||||||
|
D808B94E296EFBBA0031EB1E /* StatusEditHistoryTableViewCell.swift in Sources */,
|
||||||
2D8434F525FF465D00EECE90 /* HomeTimelineNavigationBarTitleViewModel.swift in Sources */,
|
2D8434F525FF465D00EECE90 /* HomeTimelineNavigationBarTitleViewModel.swift in Sources */,
|
||||||
DB938F0F2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift in Sources */,
|
DB938F0F2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift in Sources */,
|
||||||
DB6180F226391CF40018D199 /* MediaPreviewImageViewModel.swift in Sources */,
|
DB6180F226391CF40018D199 /* MediaPreviewImageViewModel.swift in Sources */,
|
||||||
|
@ -3619,6 +3643,7 @@
|
||||||
DB938F0926240F3C00E5B6C1 /* RemoteThreadViewModel.swift in Sources */,
|
DB938F0926240F3C00E5B6C1 /* RemoteThreadViewModel.swift in Sources */,
|
||||||
DB5B54A62833BE0000DEF8B2 /* UserListViewModel+State.swift in Sources */,
|
DB5B54A62833BE0000DEF8B2 /* UserListViewModel+State.swift in Sources */,
|
||||||
DB0617ED277F02C50030EE79 /* OnboardingNavigationController.swift in Sources */,
|
DB0617ED277F02C50030EE79 /* OnboardingNavigationController.swift in Sources */,
|
||||||
|
D808B94C296ECFDC0031EB1E /* StatusEditHistoryViewModel.swift in Sources */,
|
||||||
DB0617F527855AB90030EE79 /* ServerRuleSection.swift in Sources */,
|
DB0617F527855AB90030EE79 /* ServerRuleSection.swift in Sources */,
|
||||||
DB75BF1E263C1C1B00EDBF1F /* CustomScheduler.swift in Sources */,
|
DB75BF1E263C1C1B00EDBF1F /* CustomScheduler.swift in Sources */,
|
||||||
0FAA102725E1126A0017CCDE /* MastodonPickServerViewController.swift in Sources */,
|
0FAA102725E1126A0017CCDE /* MastodonPickServerViewController.swift in Sources */,
|
||||||
|
@ -3685,6 +3710,7 @@
|
||||||
2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */,
|
2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */,
|
||||||
2DAC9E38262FC2320062E1A6 /* SuggestionAccountViewController.swift in Sources */,
|
2DAC9E38262FC2320062E1A6 /* SuggestionAccountViewController.swift in Sources */,
|
||||||
DB6180E02639194B0018D199 /* MediaPreviewPagingViewController.swift in Sources */,
|
DB6180E02639194B0018D199 /* MediaPreviewPagingViewController.swift in Sources */,
|
||||||
|
D8E5C349296DB8A3007E76A7 /* StatusEditHistoryViewController.swift in Sources */,
|
||||||
DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */,
|
DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */,
|
||||||
5B90C45E262599800002E742 /* SettingsViewModel.swift in Sources */,
|
5B90C45E262599800002E742 /* SettingsViewModel.swift in Sources */,
|
||||||
DB5B54A12833A89600DEF8B2 /* FamiliarFollowersViewController+DataSourceProvider.swift in Sources */,
|
DB5B54A12833A89600DEF8B2 /* FamiliarFollowersViewController+DataSourceProvider.swift in Sources */,
|
||||||
|
@ -3795,11 +3821,13 @@
|
||||||
DB6D9F8426358EEC008423CD /* SettingsItem.swift in Sources */,
|
DB6D9F8426358EEC008423CD /* SettingsItem.swift in Sources */,
|
||||||
2D364F7825E66D8300204FDC /* MastodonResendEmailViewModel.swift in Sources */,
|
2D364F7825E66D8300204FDC /* MastodonResendEmailViewModel.swift in Sources */,
|
||||||
DBEFCD7B282A162400C0ABEA /* ReportReasonView.swift in Sources */,
|
DBEFCD7B282A162400C0ABEA /* ReportReasonView.swift in Sources */,
|
||||||
|
D8E5C346296DAB84007E76A7 /* DataSourceFacade+Status+History.swift in Sources */,
|
||||||
DB8AF54525C13647002E6C99 /* NeedsDependency.swift in Sources */,
|
DB8AF54525C13647002E6C99 /* NeedsDependency.swift in Sources */,
|
||||||
DB63F77B279ACAE500455B82 /* DataSourceFacade+Favorite.swift in Sources */,
|
DB63F77B279ACAE500455B82 /* DataSourceFacade+Favorite.swift in Sources */,
|
||||||
DB9D6BF825E4F5690051B173 /* NotificationViewController.swift in Sources */,
|
DB9D6BF825E4F5690051B173 /* NotificationViewController.swift in Sources */,
|
||||||
2DAC9E46262FC9FD0062E1A6 /* SuggestionAccountTableViewCell.swift in Sources */,
|
2DAC9E46262FC9FD0062E1A6 /* SuggestionAccountTableViewCell.swift in Sources */,
|
||||||
DB4FFC2C269EC39600D62E92 /* SearchTransitionController.swift in Sources */,
|
DB4FFC2C269EC39600D62E92 /* SearchTransitionController.swift in Sources */,
|
||||||
|
2A3D9B7E29A8F33A00F30313 /* StatusHistoryView.swift in Sources */,
|
||||||
6213AF5E2893A8B200BCADB6 /* DataSourceFacade+Bookmark.swift in Sources */,
|
6213AF5E2893A8B200BCADB6 /* DataSourceFacade+Bookmark.swift in Sources */,
|
||||||
DBA5E7A9263BD3A4004598BB /* ContextMenuImagePreviewViewController.swift in Sources */,
|
DBA5E7A9263BD3A4004598BB /* ContextMenuImagePreviewViewController.swift in Sources */,
|
||||||
DBF156E22702DA6900EC00B7 /* UIStatusBarManager+HandleTapAction.m in Sources */,
|
DBF156E22702DA6900EC00B7 /* UIStatusBarManager+HandleTapAction.m in Sources */,
|
||||||
|
|
|
@ -157,9 +157,11 @@ extension SceneCoordinator {
|
||||||
|
|
||||||
// compose
|
// compose
|
||||||
case compose(viewModel: ComposeViewModel)
|
case compose(viewModel: ComposeViewModel)
|
||||||
|
case editStatus(viewModel: ComposeViewModel)
|
||||||
|
|
||||||
// thread
|
// thread
|
||||||
case thread(viewModel: ThreadViewModel)
|
case thread(viewModel: ThreadViewModel)
|
||||||
|
case editHistory(viewModel: StatusEditHistoryViewModel)
|
||||||
|
|
||||||
// Hashtag Timeline
|
// Hashtag Timeline
|
||||||
case hashtagTimeline(viewModel: HashtagTimelineViewModel)
|
case hashtagTimeline(viewModel: HashtagTimelineViewModel)
|
||||||
|
@ -273,7 +275,7 @@ extension SceneCoordinator {
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func present(scene: Scene, from sender: UIViewController?, transition: Transition) -> UIViewController? {
|
func present(scene: Scene, from sender: UIViewController? = nil, transition: Transition) -> UIViewController? {
|
||||||
guard let viewController = get(scene: scene) else {
|
guard let viewController = get(scene: scene) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -430,13 +432,15 @@ private extension SceneCoordinator {
|
||||||
_viewController.viewModel = viewModel
|
_viewController.viewModel = viewModel
|
||||||
viewController = _viewController
|
viewController = _viewController
|
||||||
case .compose(let viewModel):
|
case .compose(let viewModel):
|
||||||
let _viewController = ComposeViewController()
|
let _viewController = ComposeViewController(viewModel: viewModel)
|
||||||
_viewController.viewModel = viewModel
|
|
||||||
viewController = _viewController
|
viewController = _viewController
|
||||||
case .thread(let viewModel):
|
case .thread(let viewModel):
|
||||||
let _viewController = ThreadViewController()
|
let _viewController = ThreadViewController()
|
||||||
_viewController.viewModel = viewModel
|
_viewController.viewModel = viewModel
|
||||||
viewController = _viewController
|
viewController = _viewController
|
||||||
|
case .editHistory(let viewModel):
|
||||||
|
let editHistoryViewController = StatusEditHistoryViewController(viewModel: viewModel)
|
||||||
|
viewController = editHistoryViewController
|
||||||
case .hashtagTimeline(let viewModel):
|
case .hashtagTimeline(let viewModel):
|
||||||
let _viewController = HashtagTimelineViewController()
|
let _viewController = HashtagTimelineViewController()
|
||||||
_viewController.viewModel = viewModel
|
_viewController.viewModel = viewModel
|
||||||
|
@ -536,6 +540,9 @@ private extension SceneCoordinator {
|
||||||
let _viewController = SettingsViewController()
|
let _viewController = SettingsViewController()
|
||||||
_viewController.viewModel = viewModel
|
_viewController.viewModel = viewModel
|
||||||
viewController = _viewController
|
viewController = _viewController
|
||||||
|
case .editStatus(let viewModel):
|
||||||
|
let composeViewController = ComposeViewController(viewModel: viewModel)
|
||||||
|
viewController = composeViewController
|
||||||
}
|
}
|
||||||
|
|
||||||
setupDependency(for: viewController as? NeedsDependency)
|
setupDependency(for: viewController as? NeedsDependency)
|
||||||
|
|
|
@ -167,6 +167,8 @@ extension StatusSection {
|
||||||
let managedObjectContext = context.managedObjectContext
|
let managedObjectContext = context.managedObjectContext
|
||||||
statusView.pollTableViewDiffableDataSource = UITableViewDiffableDataSource<PollSection, PollItem>(tableView: statusView.pollTableView) { tableView, indexPath, item in
|
statusView.pollTableViewDiffableDataSource = UITableViewDiffableDataSource<PollSection, PollItem>(tableView: statusView.pollTableView) { tableView, indexPath, item in
|
||||||
switch item {
|
switch item {
|
||||||
|
case .history:
|
||||||
|
return nil
|
||||||
case .option(let record):
|
case .option(let record):
|
||||||
// Fix cell reuse animation issue
|
// Fix cell reuse animation issue
|
||||||
let cell: PollOptionTableViewCell = {
|
let cell: PollOptionTableViewCell = {
|
||||||
|
@ -188,9 +190,11 @@ extension StatusSection {
|
||||||
// trigger update if needs
|
// trigger update if needs
|
||||||
let needsUpdatePoll: Bool = {
|
let needsUpdatePoll: Bool = {
|
||||||
// check first option in poll to trigger update poll only once
|
// check first option in poll to trigger update poll only once
|
||||||
guard option.index == 0 else { return false }
|
guard
|
||||||
|
let poll = option.poll,
|
||||||
|
option.index == 0
|
||||||
|
else { return false }
|
||||||
|
|
||||||
let poll = option.poll
|
|
||||||
guard !poll.expired else {
|
guard !poll.expired else {
|
||||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): poll expired. Skip update poll \(poll.id)")
|
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): poll expired. Skip update poll \(poll.id)")
|
||||||
return false
|
return false
|
||||||
|
@ -213,7 +217,8 @@ extension StatusSection {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if needsUpdatePoll {
|
if needsUpdatePoll {
|
||||||
let pollRecord: ManagedObjectRecord<Poll> = .init(objectID: option.poll.objectID)
|
guard let poll = option.poll else { return }
|
||||||
|
let pollRecord: ManagedObjectRecord<Poll> = .init(objectID: poll.objectID)
|
||||||
Task { [weak context] in
|
Task { [weak context] in
|
||||||
guard let context = context else { return }
|
guard let context = context else { return }
|
||||||
_ = try await context.apiService.poll(
|
_ = try await context.apiService.poll(
|
||||||
|
@ -232,6 +237,33 @@ extension StatusSection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension StatusSection {
|
||||||
|
|
||||||
|
public static func setupStatusPollHistoryDataSource(
|
||||||
|
context: AppContext,
|
||||||
|
authContext: AuthContext,
|
||||||
|
statusView: StatusView
|
||||||
|
) {
|
||||||
|
statusView.pollTableViewDiffableDataSource = UITableViewDiffableDataSource<PollSection, PollItem>(tableView: statusView.pollTableView) { tableView, indexPath, item in
|
||||||
|
switch item {
|
||||||
|
case .option:
|
||||||
|
return nil
|
||||||
|
case let .history(option):
|
||||||
|
// Fix cell reuse animation issue
|
||||||
|
let cell: PollOptionTableViewCell = {
|
||||||
|
let _cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PollOptionTableViewCell.self) + "@\(indexPath.row)#\(indexPath.section)") as? PollOptionTableViewCell
|
||||||
|
_cell?.prepareForReuse()
|
||||||
|
return _cell ?? PollOptionTableViewCell()
|
||||||
|
}()
|
||||||
|
|
||||||
|
cell.pollOptionView.configure(historyPollOption: option)
|
||||||
|
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension StatusSection {
|
extension StatusSection {
|
||||||
|
|
||||||
static func configure(
|
static func configure(
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import MastodonCore
|
||||||
|
import MastodonSDK
|
||||||
|
import CoreDataStack
|
||||||
|
|
||||||
|
extension DataSourceFacade {
|
||||||
|
public static func getEditHistory(
|
||||||
|
forStatus status: Status,
|
||||||
|
provider: NeedsDependency & AuthContextProvider
|
||||||
|
) async throws -> [Mastodon.Entity.StatusEdit] {
|
||||||
|
let reponse = try await provider.context.apiService.getHistory(forStatusID: status.id, authenticationBox: provider.authContext.mastodonAuthenticationBox)
|
||||||
|
|
||||||
|
return reponse.value
|
||||||
|
}
|
||||||
|
}
|
|
@ -117,6 +117,7 @@ extension DataSourceFacade {
|
||||||
let composeViewModel = ComposeViewModel(
|
let composeViewModel = ComposeViewModel(
|
||||||
context: provider.context,
|
context: provider.context,
|
||||||
authContext: provider.authContext,
|
authContext: provider.authContext,
|
||||||
|
composeContext: .composeStatus,
|
||||||
destination: .reply(parent: status)
|
destination: .reply(parent: status)
|
||||||
)
|
)
|
||||||
_ = provider.coordinator.present(
|
_ = provider.coordinator.present(
|
||||||
|
@ -373,6 +374,21 @@ extension DataSourceFacade {
|
||||||
alertController.addAction(UIAlertAction(title: L10n.Common.Alerts.TranslationFailed.button, style: .default))
|
alertController.addAction(UIAlertAction(title: L10n.Common.Alerts.TranslationFailed.button, style: .default))
|
||||||
dependency.present(alertController, animated: true)
|
dependency.present(alertController, animated: true)
|
||||||
}
|
}
|
||||||
|
case .editStatus:
|
||||||
|
|
||||||
|
guard let status = menuContext.status?.object(in: dependency.context.managedObjectContext) else { return }
|
||||||
|
|
||||||
|
let statusSource = try await dependency.context.apiService.getStatusSource(
|
||||||
|
forStatusID: status.id,
|
||||||
|
authenticationBox: dependency.authContext.mastodonAuthenticationBox
|
||||||
|
).value
|
||||||
|
|
||||||
|
let editStatusViewModel = ComposeViewModel(
|
||||||
|
context: dependency.coordinator.appContext,
|
||||||
|
authContext: dependency.authContext,
|
||||||
|
composeContext: .editStatus(status: status, statusSource: statusSource),
|
||||||
|
destination: .topLevel)
|
||||||
|
_ = dependency.coordinator.present(scene: .editStatus(viewModel: editStatusViewModel), transition: .modal(animated: true))
|
||||||
}
|
}
|
||||||
} // end func
|
} // end func
|
||||||
}
|
}
|
||||||
|
|
|
@ -233,6 +233,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte
|
||||||
scene: .compose(viewModel: ComposeViewModel(
|
scene: .compose(viewModel: ComposeViewModel(
|
||||||
context: self.context,
|
context: self.context,
|
||||||
authContext: self.authContext,
|
authContext: self.authContext,
|
||||||
|
composeContext: .composeStatus,
|
||||||
destination: .topLevel,
|
destination: .topLevel,
|
||||||
initialContent: L10n.Common.Controls.Status.linkViaUser(url.absoluteString, "@" + (statusView.viewModel.authorUsername ?? ""))
|
initialContent: L10n.Common.Controls.Status.linkViaUser(url.absoluteString, "@" + (statusView.viewModel.authorUsername ?? ""))
|
||||||
)),
|
)),
|
||||||
|
@ -318,7 +319,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte
|
||||||
|
|
||||||
try await managedObjectContext.performChanges {
|
try await managedObjectContext.performChanges {
|
||||||
guard let pollOption = pollOption.object(in: managedObjectContext) else { return }
|
guard let pollOption = pollOption.object(in: managedObjectContext) else { return }
|
||||||
let poll = pollOption.poll
|
guard let poll = pollOption.poll else { return }
|
||||||
_poll = .init(objectID: poll.objectID)
|
_poll = .init(objectID: poll.objectID)
|
||||||
|
|
||||||
_isMultiple = poll.multiple
|
_isMultiple = poll.multiple
|
||||||
|
@ -357,8 +358,10 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte
|
||||||
|
|
||||||
// restore voting state
|
// restore voting state
|
||||||
try await managedObjectContext.performChanges {
|
try await managedObjectContext.performChanges {
|
||||||
guard let pollOption = pollOption.object(in: managedObjectContext) else { return }
|
guard
|
||||||
let poll = pollOption.poll
|
let pollOption = pollOption.object(in: managedObjectContext),
|
||||||
|
let poll = pollOption.poll
|
||||||
|
else { return }
|
||||||
poll.update(isVoting: false)
|
poll.update(isVoting: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -652,6 +655,24 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte
|
||||||
)
|
)
|
||||||
} // end Task
|
} // end Task
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, statusMetricView: StatusMetricView, showEditHistory button: UIButton) {
|
||||||
|
Task {
|
||||||
|
|
||||||
|
let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil)
|
||||||
|
guard let item = await self.item(from: source),
|
||||||
|
case let .status(status) = item else {
|
||||||
|
assertionFailure("only works for status data provider")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let status = status.object(in: context.managedObjectContext),
|
||||||
|
let edits = status.editHistory?.sorted(by: { $0.createdAt > $1.createdAt }) else { return }
|
||||||
|
|
||||||
|
let viewModel = StatusEditHistoryViewModel(status: status, edits: edits, appContext: context, authContext: authContext)
|
||||||
|
_ = await coordinator.present(scene: .editHistory(viewModel: viewModel), from: self, transition: .show)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: a11y
|
// MARK: a11y
|
||||||
|
|
|
@ -100,6 +100,7 @@ extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvid
|
||||||
let composeViewModel = ComposeViewModel(
|
let composeViewModel = ComposeViewModel(
|
||||||
context: self.context,
|
context: self.context,
|
||||||
authContext: authContext,
|
authContext: authContext,
|
||||||
|
composeContext: .composeStatus,
|
||||||
destination: .reply(parent: status)
|
destination: .reply(parent: status)
|
||||||
)
|
)
|
||||||
_ = self.coordinator.present(
|
_ = self.coordinator.present(
|
||||||
|
|
|
@ -19,7 +19,6 @@ import MastodonLocalization
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
|
|
||||||
final class ComposeViewController: UIViewController, NeedsDependency {
|
final class ComposeViewController: UIViewController, NeedsDependency {
|
||||||
|
|
||||||
static let minAutoCompleteVisibleHeight: CGFloat = 100
|
static let minAutoCompleteVisibleHeight: CGFloat = 100
|
||||||
|
|
||||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||||
|
@ -29,13 +28,34 @@ final class ComposeViewController: UIViewController, NeedsDependency {
|
||||||
var viewModel: ComposeViewModel!
|
var viewModel: ComposeViewModel!
|
||||||
|
|
||||||
let logger = Logger(subsystem: "ComposeViewController", category: "logic")
|
let logger = Logger(subsystem: "ComposeViewController", category: "logic")
|
||||||
|
|
||||||
|
init(viewModel: ComposeViewModel) {
|
||||||
|
self.viewModel = viewModel
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||||
|
|
||||||
lazy var composeContentViewModel: ComposeContentViewModel = {
|
lazy var composeContentViewModel: ComposeContentViewModel = {
|
||||||
|
|
||||||
|
let composeContext: ComposeContentViewModel.ComposeContext
|
||||||
|
let initialContent: String
|
||||||
|
|
||||||
|
switch viewModel.composeContext {
|
||||||
|
case .composeStatus:
|
||||||
|
composeContext = .composeStatus
|
||||||
|
initialContent = viewModel.initialContent
|
||||||
|
case .editStatus(let status, let statusSource):
|
||||||
|
composeContext = .editStatus(status: status, statusSource: statusSource)
|
||||||
|
initialContent = statusSource.text
|
||||||
|
}
|
||||||
|
|
||||||
return ComposeContentViewModel(
|
return ComposeContentViewModel(
|
||||||
context: context,
|
context: context,
|
||||||
authContext: viewModel.authContext,
|
authContext: viewModel.authContext,
|
||||||
|
composeContext: composeContext,
|
||||||
destination: viewModel.destination,
|
destination: viewModel.destination,
|
||||||
initialContent: viewModel.initialContent
|
initialContent: initialContent
|
||||||
)
|
)
|
||||||
}()
|
}()
|
||||||
private(set) lazy var composeContentViewController: ComposeContentViewController = {
|
private(set) lazy var composeContentViewController: ComposeContentViewController = {
|
||||||
|
@ -46,16 +66,38 @@ final class ComposeViewController: UIViewController, NeedsDependency {
|
||||||
|
|
||||||
private(set) lazy var cancelBarButtonItem = UIBarButtonItem(title: L10n.Common.Controls.Actions.cancel, style: .plain, target: self, action: #selector(ComposeViewController.cancelBarButtonItemPressed(_:)))
|
private(set) lazy var cancelBarButtonItem = UIBarButtonItem(title: L10n.Common.Controls.Actions.cancel, style: .plain, target: self, action: #selector(ComposeViewController.cancelBarButtonItemPressed(_:)))
|
||||||
|
|
||||||
let publishButton: UIButton = {
|
private lazy var publishButton: UIButton = {
|
||||||
let button = RoundedEdgesButton(type: .custom)
|
let button = RoundedEdgesButton(type: .custom)
|
||||||
button.cornerRadius = 10
|
button.cornerRadius = 10
|
||||||
button.contentEdgeInsets = UIEdgeInsets(top: 6, left: 16, bottom: 5, right: 16) // set 28pt height
|
button.contentEdgeInsets = UIEdgeInsets(top: 6, left: 16, bottom: 5, right: 16) // set 28pt height
|
||||||
button.titleLabel?.font = .systemFont(ofSize: 14, weight: .bold)
|
button.titleLabel?.font = .systemFont(ofSize: 14, weight: .bold)
|
||||||
button.setTitle(L10n.Scene.Compose.composeAction, for: .normal)
|
button.setTitle(L10n.Scene.Compose.composeAction, for: .normal)
|
||||||
|
button.addTarget(self, action: #selector(ComposeViewController.publishBarButtonItemPressed(_:)), for: .touchUpInside)
|
||||||
return button
|
return button
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
private lazy var saveButton: UIButton = {
|
||||||
|
let button = RoundedEdgesButton(type: .custom)
|
||||||
|
button.cornerRadius = 10
|
||||||
|
button.contentEdgeInsets = UIEdgeInsets(top: 6, left: 16, bottom: 5, right: 16) // set 28pt height
|
||||||
|
button.titleLabel?.font = .systemFont(ofSize: 14, weight: .bold)
|
||||||
|
button.setTitle(L10n.Common.Controls.Actions.save, for: .normal)
|
||||||
|
button.addTarget(self, action: #selector(ComposeViewController.publishStatusEdit(_:)), for: .touchUpInside)
|
||||||
|
return button
|
||||||
|
}()
|
||||||
|
|
||||||
|
private(set) lazy var saveBarButtonItem: UIBarButtonItem = {
|
||||||
|
configurePublishButtonApperance(button: saveButton)
|
||||||
|
let shadowBackgroundContainer = ShadowBackgroundContainer()
|
||||||
|
saveButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
shadowBackgroundContainer.addSubview(saveButton)
|
||||||
|
saveButton.pinToParent()
|
||||||
|
let barButtonItem = UIBarButtonItem(customView: shadowBackgroundContainer)
|
||||||
|
return barButtonItem
|
||||||
|
}()
|
||||||
|
|
||||||
private(set) lazy var publishBarButtonItem: UIBarButtonItem = {
|
private(set) lazy var publishBarButtonItem: UIBarButtonItem = {
|
||||||
configurePublishButtonApperance()
|
configurePublishButtonApperance(button: publishButton)
|
||||||
let shadowBackgroundContainer = ShadowBackgroundContainer()
|
let shadowBackgroundContainer = ShadowBackgroundContainer()
|
||||||
publishButton.translatesAutoresizingMaskIntoConstraints = false
|
publishButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
shadowBackgroundContainer.addSubview(publishButton)
|
shadowBackgroundContainer.addSubview(publishButton)
|
||||||
|
@ -63,12 +105,13 @@ final class ComposeViewController: UIViewController, NeedsDependency {
|
||||||
let barButtonItem = UIBarButtonItem(customView: shadowBackgroundContainer)
|
let barButtonItem = UIBarButtonItem(customView: shadowBackgroundContainer)
|
||||||
return barButtonItem
|
return barButtonItem
|
||||||
}()
|
}()
|
||||||
private func configurePublishButtonApperance() {
|
|
||||||
publishButton.adjustsImageWhenHighlighted = false
|
private func configurePublishButtonApperance(button: UIButton) {
|
||||||
publishButton.setBackgroundImage(.placeholder(color: Asset.Colors.Label.primary.color), for: .normal)
|
button.adjustsImageWhenHighlighted = false
|
||||||
publishButton.setBackgroundImage(.placeholder(color: Asset.Colors.Label.primary.color.withAlphaComponent(0.5)), for: .highlighted)
|
button.setBackgroundImage(.placeholder(color: Asset.Colors.Label.primary.color), for: .normal)
|
||||||
publishButton.setBackgroundImage(.placeholder(color: Asset.Colors.Button.disabled.color), for: .disabled)
|
button.setBackgroundImage(.placeholder(color: Asset.Colors.Label.primary.color.withAlphaComponent(0.5)), for: .highlighted)
|
||||||
publishButton.setTitleColor(Asset.Colors.Label.primaryReverse.color, for: .normal)
|
button.setBackgroundImage(.placeholder(color: Asset.Colors.Button.disabled.color), for: .disabled)
|
||||||
|
button.setTitleColor(Asset.Colors.Label.primaryReverse.color, for: .normal)
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
|
@ -83,18 +126,17 @@ extension ComposeViewController {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
navigationItem.leftBarButtonItem = cancelBarButtonItem
|
navigationItem.leftBarButtonItem = cancelBarButtonItem
|
||||||
navigationItem.rightBarButtonItem = publishBarButtonItem
|
|
||||||
viewModel.traitCollectionDidChangePublisher
|
viewModel.traitCollectionDidChangePublisher
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] _ in
|
.sink { [weak self] _ in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
guard self.traitCollection.userInterfaceIdiom == .pad else { return }
|
guard self.traitCollection.userInterfaceIdiom == .pad else { return }
|
||||||
let items = [self.publishBarButtonItem]
|
self.navigationItem.rightBarButtonItem = self.rightBarButtonItemForCurrentContext
|
||||||
self.navigationItem.rightBarButtonItems = items
|
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
publishButton.addTarget(self, action: #selector(ComposeViewController.publishBarButtonItemPressed(_:)), for: .touchUpInside)
|
|
||||||
|
navigationItem.rightBarButtonItem = rightBarButtonItemForCurrentContext
|
||||||
|
|
||||||
addChild(composeContentViewController)
|
addChild(composeContentViewController)
|
||||||
composeContentViewController.view.translatesAutoresizingMaskIntoConstraints = false
|
composeContentViewController.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
view.addSubview(composeContentViewController.view)
|
view.addSubview(composeContentViewController.view)
|
||||||
|
@ -119,8 +161,14 @@ extension ComposeViewController {
|
||||||
|
|
||||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||||
super.traitCollectionDidChange(previousTraitCollection)
|
super.traitCollectionDidChange(previousTraitCollection)
|
||||||
|
|
||||||
configurePublishButtonApperance()
|
switch viewModel.composeContext {
|
||||||
|
case .composeStatus:
|
||||||
|
configurePublishButtonApperance(button: publishButton)
|
||||||
|
case .editStatus:
|
||||||
|
configurePublishButtonApperance(button: saveButton)
|
||||||
|
}
|
||||||
|
|
||||||
viewModel.traitCollectionDidChangePublisher.send()
|
viewModel.traitCollectionDidChangePublisher.send()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,6 +189,14 @@ extension ComposeViewController {
|
||||||
present(alertController, animated: true, completion: nil)
|
present(alertController, animated: true, completion: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var rightBarButtonItemForCurrentContext: UIBarButtonItem {
|
||||||
|
switch viewModel.composeContext {
|
||||||
|
case .composeStatus:
|
||||||
|
return publishBarButtonItem
|
||||||
|
case .editStatus:
|
||||||
|
return saveBarButtonItem
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ComposeViewController {
|
extension ComposeViewController {
|
||||||
|
@ -155,8 +211,7 @@ extension ComposeViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func publishBarButtonItemPressed(_ sender: UIBarButtonItem) {
|
@objc private func publishBarButtonItemPressed(_ sender: UIBarButtonItem) {
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try composeContentViewModel.checkAttachmentPrecondition()
|
try composeContentViewModel.checkAttachmentPrecondition()
|
||||||
} catch {
|
} catch {
|
||||||
|
@ -185,7 +240,34 @@ extension ComposeViewController {
|
||||||
|
|
||||||
dismiss(animated: true, completion: nil)
|
dismiss(animated: true, completion: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc
|
||||||
|
private func publishStatusEdit(_ sender: Any) {
|
||||||
|
do {
|
||||||
|
try composeContentViewModel.checkAttachmentPrecondition()
|
||||||
|
} catch {
|
||||||
|
let alertController = UIAlertController(for: error, title: nil, preferredStyle: .alert)
|
||||||
|
let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil)
|
||||||
|
alertController.addAction(okAction)
|
||||||
|
_ = coordinator.present(scene: .alertController(alertController: alertController), from: nil, transition: .alertController(animated: true, completion: nil))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
guard let editStatusPublisher = try composeContentViewModel.statusEditPublisher() else { return }
|
||||||
|
viewModel.context.publisherService.enqueue(
|
||||||
|
statusPublisher: editStatusPublisher,
|
||||||
|
authContext: viewModel.authContext
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
let alertController = UIAlertController.standardAlert(of: error)
|
||||||
|
present(alertController, animated: true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dismiss(animated: true, completion: nil)
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ComposeViewController {
|
extension ComposeViewController {
|
||||||
|
|
|
@ -19,7 +19,12 @@ import MastodonMeta
|
||||||
import MastodonUI
|
import MastodonUI
|
||||||
|
|
||||||
final class ComposeViewModel {
|
final class ComposeViewModel {
|
||||||
|
|
||||||
|
enum Context {
|
||||||
|
case composeStatus
|
||||||
|
case editStatus(status: Status, statusSource: Mastodon.Entity.StatusSource)
|
||||||
|
}
|
||||||
|
|
||||||
let logger = Logger(subsystem: "ComposeViewModel", category: "ViewModel")
|
let logger = Logger(subsystem: "ComposeViewModel", category: "ViewModel")
|
||||||
|
|
||||||
var disposeBag = Set<AnyCancellable>()
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
@ -29,6 +34,7 @@ final class ComposeViewModel {
|
||||||
// input
|
// input
|
||||||
let context: AppContext
|
let context: AppContext
|
||||||
let authContext: AuthContext
|
let authContext: AuthContext
|
||||||
|
let composeContext: Context
|
||||||
let destination: ComposeContentViewModel.Destination
|
let destination: ComposeContentViewModel.Destination
|
||||||
let initialContent: String
|
let initialContent: String
|
||||||
|
|
||||||
|
@ -42,6 +48,7 @@ final class ComposeViewModel {
|
||||||
init(
|
init(
|
||||||
context: AppContext,
|
context: AppContext,
|
||||||
authContext: AuthContext,
|
authContext: AuthContext,
|
||||||
|
composeContext: ComposeViewModel.Context,
|
||||||
destination: ComposeContentViewModel.Destination,
|
destination: ComposeContentViewModel.Destination,
|
||||||
initialContent: String = ""
|
initialContent: String = ""
|
||||||
) {
|
) {
|
||||||
|
@ -49,18 +56,23 @@ final class ComposeViewModel {
|
||||||
self.authContext = authContext
|
self.authContext = authContext
|
||||||
self.destination = destination
|
self.destination = destination
|
||||||
self.initialContent = initialContent
|
self.initialContent = initialContent
|
||||||
|
self.composeContext = composeContext
|
||||||
// end init
|
// end init
|
||||||
|
|
||||||
self.title = {
|
let title: String
|
||||||
|
|
||||||
|
switch composeContext {
|
||||||
|
case .composeStatus:
|
||||||
switch destination {
|
switch destination {
|
||||||
case .topLevel: return L10n.Scene.Compose.Title.newPost
|
case .topLevel:
|
||||||
case .reply: return L10n.Scene.Compose.Title.newReply
|
title = L10n.Scene.Compose.Title.newPost
|
||||||
|
case .reply:
|
||||||
|
title = L10n.Scene.Compose.Title.newReply
|
||||||
}
|
}
|
||||||
}()
|
case .editStatus(_, _):
|
||||||
|
title = L10n.Scene.Compose.Title.editPost
|
||||||
|
}
|
||||||
|
|
||||||
|
self.title = title
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -211,6 +211,7 @@ extension HashtagTimelineViewController {
|
||||||
let composeViewModel = ComposeViewModel(
|
let composeViewModel = ComposeViewModel(
|
||||||
context: context,
|
context: context,
|
||||||
authContext: viewModel.authContext,
|
authContext: viewModel.authContext,
|
||||||
|
composeContext: .composeStatus,
|
||||||
destination: .topLevel,
|
destination: .topLevel,
|
||||||
initialContent: hashtag
|
initialContent: hashtag
|
||||||
)
|
)
|
||||||
|
|
|
@ -200,6 +200,12 @@ extension HomeTimelineViewController {
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
context.publisherService.statusPublishResult.sink { result in
|
||||||
|
if case .success(.edit) = result {
|
||||||
|
self.viewModel.hasPendingStatusEditReload = true
|
||||||
|
}
|
||||||
|
}.store(in: &disposeBag)
|
||||||
|
|
||||||
context.publisherService.$currentPublishProgress
|
context.publisherService.$currentPublishProgress
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] progress in
|
.sink { [weak self] progress in
|
||||||
|
|
|
@ -91,7 +91,7 @@ extension HomeTimelineViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
let hasChanges = newSnapshot.itemIdentifiers != oldSnapshot.itemIdentifiers
|
let hasChanges = newSnapshot.itemIdentifiers != oldSnapshot.itemIdentifiers
|
||||||
if !hasChanges {
|
if !hasChanges && !self.hasPendingStatusEditReload {
|
||||||
self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): snapshot not changes")
|
self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): snapshot not changes")
|
||||||
self.didLoadLatest.send()
|
self.didLoadLatest.send()
|
||||||
return
|
return
|
||||||
|
@ -117,6 +117,7 @@ extension HomeTimelineViewModel {
|
||||||
tableView.setContentOffset(contentOffset, animated: false)
|
tableView.setContentOffset(contentOffset, animated: false)
|
||||||
self.didLoadLatest.send()
|
self.didLoadLatest.send()
|
||||||
self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): applied new snapshot")
|
self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): applied new snapshot")
|
||||||
|
self.hasPendingStatusEditReload = false
|
||||||
} // end Task
|
} // end Task
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
|
@ -35,6 +35,7 @@ final class HomeTimelineViewModel: NSObject {
|
||||||
@Published var lastAutomaticFetchTimestamp: Date? = nil
|
@Published var lastAutomaticFetchTimestamp: Date? = nil
|
||||||
@Published var scrollPositionRecord: ScrollPositionRecord? = nil
|
@Published var scrollPositionRecord: ScrollPositionRecord? = nil
|
||||||
@Published var displaySettingBarButtonItem = true
|
@Published var displaySettingBarButtonItem = true
|
||||||
|
@Published var hasPendingStatusEditReload = false
|
||||||
|
|
||||||
weak var tableView: UITableView?
|
weak var tableView: UITableView?
|
||||||
weak var timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate?
|
weak var timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate?
|
||||||
|
|
|
@ -558,6 +558,7 @@ extension ProfileViewController {
|
||||||
let composeViewModel = ComposeViewModel(
|
let composeViewModel = ComposeViewModel(
|
||||||
context: context,
|
context: context,
|
||||||
authContext: viewModel.authContext,
|
authContext: viewModel.authContext,
|
||||||
|
composeContext: .composeStatus,
|
||||||
destination: .topLevel,
|
destination: .topLevel,
|
||||||
initialContent: mention
|
initialContent: mention
|
||||||
)
|
)
|
||||||
|
|
|
@ -386,9 +386,10 @@ extension MainTabBarController {
|
||||||
let composeViewModel = ComposeViewModel(
|
let composeViewModel = ComposeViewModel(
|
||||||
context: context,
|
context: context,
|
||||||
authContext: authContext,
|
authContext: authContext,
|
||||||
|
composeContext: .composeStatus,
|
||||||
destination: .topLevel
|
destination: .topLevel
|
||||||
)
|
)
|
||||||
_ = coordinator.present(scene: .compose(viewModel: composeViewModel), from: nil, transition: .modal(animated: true, completion: nil))
|
_ = coordinator.present(scene: .compose(viewModel: composeViewModel), transition: .modal(animated: true, completion: nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
private func touchedTab(by sender: UIGestureRecognizer) -> Tab? {
|
private func touchedTab(by sender: UIGestureRecognizer) -> Tab? {
|
||||||
|
@ -815,6 +816,7 @@ extension MainTabBarController {
|
||||||
let composeViewModel = ComposeViewModel(
|
let composeViewModel = ComposeViewModel(
|
||||||
context: context,
|
context: context,
|
||||||
authContext: authContext,
|
authContext: authContext,
|
||||||
|
composeContext: .composeStatus,
|
||||||
destination: .topLevel
|
destination: .topLevel
|
||||||
)
|
)
|
||||||
_ = coordinator.present(scene: .compose(viewModel: composeViewModel), from: nil, transition: .modal(animated: true, completion: nil))
|
_ = coordinator.present(scene: .compose(viewModel: composeViewModel), from: nil, transition: .modal(animated: true, completion: nil))
|
||||||
|
|
|
@ -227,6 +227,7 @@ extension SidebarViewController: UICollectionViewDelegate {
|
||||||
let composeViewModel = ComposeViewModel(
|
let composeViewModel = ComposeViewModel(
|
||||||
context: context,
|
context: context,
|
||||||
authContext: authContext,
|
authContext: authContext,
|
||||||
|
composeContext: .composeStatus,
|
||||||
destination: .topLevel
|
destination: .topLevel
|
||||||
)
|
)
|
||||||
_ = coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil))
|
_ = coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil))
|
||||||
|
|
|
@ -14,6 +14,11 @@ import MastodonUI
|
||||||
|
|
||||||
extension PollOptionView {
|
extension PollOptionView {
|
||||||
public func configure(pollOption option: PollOption) {
|
public func configure(pollOption option: PollOption) {
|
||||||
|
guard let poll = option.poll, let status = poll.status else {
|
||||||
|
assertionFailure("PollOption to be configured is expected to be part of Poll with Status")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
viewModel.objects.insert(option)
|
viewModel.objects.insert(option)
|
||||||
|
|
||||||
// background
|
// background
|
||||||
|
@ -33,7 +38,7 @@ extension PollOptionView {
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
// percentage
|
// percentage
|
||||||
Publishers.CombineLatest(
|
Publishers.CombineLatest(
|
||||||
option.poll.publisher(for: \.votersCount),
|
poll.publisher(for: \.votersCount),
|
||||||
option.publisher(for: \.votesCount)
|
option.publisher(for: \.votesCount)
|
||||||
)
|
)
|
||||||
.map { pollVotersCount, optionVotesCount -> Double? in
|
.map { pollVotersCount, optionVotesCount -> Double? in
|
||||||
|
@ -43,15 +48,15 @@ extension PollOptionView {
|
||||||
.assign(to: \.percentage, on: viewModel)
|
.assign(to: \.percentage, on: viewModel)
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
// $isExpire
|
// $isExpire
|
||||||
option.poll.publisher(for: \.expired)
|
poll.publisher(for: \.expired)
|
||||||
.assign(to: \.isExpire, on: viewModel)
|
.assign(to: \.isExpire, on: viewModel)
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
// isMultiple
|
// isMultiple
|
||||||
viewModel.isMultiple = option.poll.multiple
|
viewModel.isMultiple = poll.multiple
|
||||||
|
|
||||||
let optionIndex = option.index
|
let optionIndex = option.index
|
||||||
let authorDomain = option.poll.status.author.domain
|
let authorDomain = status.author.domain
|
||||||
let authorID = option.poll.status.author.id
|
let authorID = status.author.id
|
||||||
// isSelect, isPollVoted, isMyPoll
|
// isSelect, isPollVoted, isMyPoll
|
||||||
Publishers.CombineLatest4(
|
Publishers.CombineLatest4(
|
||||||
option.publisher(for: \.poll),
|
option.publisher(for: \.poll),
|
||||||
|
@ -60,7 +65,7 @@ extension PollOptionView {
|
||||||
viewModel.$authContext
|
viewModel.$authContext
|
||||||
)
|
)
|
||||||
.sink { [weak self] poll, optionVotedBy, isSelected, authContext in
|
.sink { [weak self] poll, optionVotedBy, isSelected, authContext in
|
||||||
guard let self = self else { return }
|
guard let self = self, let poll = poll else { return }
|
||||||
|
|
||||||
let domain = authContext?.mastodonAuthenticationBox.domain ?? ""
|
let domain = authContext?.mastodonAuthenticationBox.domain ?? ""
|
||||||
let userID = authContext?.mastodonAuthenticationBox.userID ?? ""
|
let userID = authContext?.mastodonAuthenticationBox.userID ?? ""
|
||||||
|
@ -109,3 +114,30 @@ extension PollOptionView {
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension PollOptionView {
|
||||||
|
public func configure(historyPollOption option: StatusEdit.Poll.Option) {
|
||||||
|
// background
|
||||||
|
ThemeService.shared.currentTheme
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] theme in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.viewModel.roundedBackgroundViewColor = theme.systemElevatedBackgroundColor
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
// metaContent
|
||||||
|
viewModel.metaContent = PlaintextMetaContent(string: option.title)
|
||||||
|
// show left-hand-side dots, otherwise view looks "incomplete"
|
||||||
|
viewModel.selectState = .off
|
||||||
|
// appearance
|
||||||
|
ThemeService.shared.currentTheme
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] theme in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.checkmarkBackgroundView.backgroundColor = UIColor(dynamicProvider: { trailtCollection in
|
||||||
|
return trailtCollection.userInterfaceStyle == .light ? .white : theme.tableViewCellSelectionBackgroundColor
|
||||||
|
})
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -37,6 +37,7 @@ protocol StatusTableViewCellDelegate: AnyObject, AutoGenerateProtocolDelegate {
|
||||||
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, mediaGridContainerView: MediaGridContainerView, mediaSensitiveButtonDidPressed button: UIButton)
|
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, mediaGridContainerView: MediaGridContainerView, mediaSensitiveButtonDidPressed button: UIButton)
|
||||||
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, statusMetricView: StatusMetricView, reblogButtonDidPressed button: UIButton)
|
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, statusMetricView: StatusMetricView, reblogButtonDidPressed button: UIButton)
|
||||||
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, statusMetricView: StatusMetricView, favoriteButtonDidPressed button: UIButton)
|
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, statusMetricView: StatusMetricView, favoriteButtonDidPressed button: UIButton)
|
||||||
|
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, statusMetricView: StatusMetricView, showEditHistory button: UIButton)
|
||||||
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, cardControl: StatusCardControl, didTapURL url: URL)
|
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, cardControl: StatusCardControl, didTapURL url: URL)
|
||||||
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, cardControlMenu: StatusCardControl) -> UIMenu?
|
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, cardControlMenu: StatusCardControl) -> UIMenu?
|
||||||
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, accessibilityActivate: Void)
|
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, accessibilityActivate: Void)
|
||||||
|
@ -104,6 +105,10 @@ extension StatusViewDelegate where Self: StatusViewContainerTableViewCell {
|
||||||
delegate?.tableViewCell(self, statusView: statusView, statusMetricView: statusMetricView, favoriteButtonDidPressed: button)
|
delegate?.tableViewCell(self, statusView: statusView, statusMetricView: statusMetricView, favoriteButtonDidPressed: button)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func statusView(_ statusView: StatusView, statusMetricView: StatusMetricView, showEditHistory button: UIButton) {
|
||||||
|
delegate?.tableViewCell(self, statusView: statusView, statusMetricView: statusMetricView, showEditHistory: button)
|
||||||
|
}
|
||||||
|
|
||||||
func statusView(_ statusView: StatusView, cardControl: StatusCardControl, didTapURL url: URL) {
|
func statusView(_ statusView: StatusView, cardControl: StatusCardControl, didTapURL url: URL) {
|
||||||
delegate?.tableViewCell(self, statusView: statusView, cardControl: cardControl, didTapURL: url)
|
delegate?.tableViewCell(self, statusView: statusView, cardControl: cardControl, didTapURL: url)
|
||||||
}
|
}
|
||||||
|
|
|
@ -112,8 +112,8 @@ extension StatusThreadRootTableViewCell {
|
||||||
statusView.mediaGridContainerView,
|
statusView.mediaGridContainerView,
|
||||||
statusView.pollTableView,
|
statusView.pollTableView,
|
||||||
statusView.pollStatusStackView,
|
statusView.pollStatusStackView,
|
||||||
statusView.actionToolbarContainer
|
statusView.actionToolbarContainer,
|
||||||
// statusMetricView is intentionally excluded
|
statusView.statusMetricView,
|
||||||
]
|
]
|
||||||
|
|
||||||
if statusView.viewModel.isContentReveal {
|
if statusView.viewModel.isContentReveal {
|
||||||
|
|
|
@ -0,0 +1,103 @@
|
||||||
|
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import MastodonUI
|
||||||
|
import MastodonSDK
|
||||||
|
import CoreDataStack
|
||||||
|
import MastodonAsset
|
||||||
|
|
||||||
|
class StatusEditHistoryTableViewCell: UITableViewCell {
|
||||||
|
var containerViewLeadingLayoutConstraint: NSLayoutConstraint!
|
||||||
|
var containerViewTrailingLayoutConstraint: NSLayoutConstraint!
|
||||||
|
|
||||||
|
static let identifier = "StatusEditHistoryTableViewCell"
|
||||||
|
static let verticalMargin: CGFloat = 12
|
||||||
|
static let horizontalMargin: CGFloat = 16
|
||||||
|
|
||||||
|
let dateLabel: UILabel
|
||||||
|
let statusHistoryView: StatusHistoryView
|
||||||
|
private let grayBackground: UIView
|
||||||
|
var statusViewBottomConstraint: NSLayoutConstraint?
|
||||||
|
|
||||||
|
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||||
|
dateLabel = UILabel()
|
||||||
|
dateLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
dateLabel.textColor = Asset.Colors.Label.secondary.color
|
||||||
|
dateLabel.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular))
|
||||||
|
|
||||||
|
statusHistoryView = StatusHistoryView()
|
||||||
|
statusHistoryView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
grayBackground = UIView()
|
||||||
|
grayBackground.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
grayBackground.backgroundColor = Asset.Scene.EditHistory.statusBackground.color
|
||||||
|
grayBackground.layer.borderWidth = 1
|
||||||
|
grayBackground.layer.borderColor = Asset.Scene.EditHistory.statusBackgroundBorder.color.cgColor
|
||||||
|
grayBackground.applyCornerRadius(radius: 8)
|
||||||
|
|
||||||
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||||
|
|
||||||
|
isAccessibilityElement = true
|
||||||
|
|
||||||
|
selectionStyle = .none
|
||||||
|
grayBackground.addSubview(statusHistoryView)
|
||||||
|
contentView.addSubview(dateLabel)
|
||||||
|
contentView.addSubview(grayBackground)
|
||||||
|
|
||||||
|
setupContainerViewMarginConstraints()
|
||||||
|
setupConstraints()
|
||||||
|
updateContainerViewMarginConstraints()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||||
|
|
||||||
|
private func setupConstraints() {
|
||||||
|
statusViewBottomConstraint = statusHistoryView.bottomAnchor.constraint(equalTo: grayBackground.bottomAnchor, constant: -Self.verticalMargin)
|
||||||
|
let constraints = [
|
||||||
|
dateLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8),
|
||||||
|
dateLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
|
||||||
|
dateLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
|
||||||
|
|
||||||
|
grayBackground.topAnchor.constraint(equalTo: dateLabel.bottomAnchor, constant: Self.verticalMargin),
|
||||||
|
grayBackground.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
|
||||||
|
grayBackground.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
|
||||||
|
grayBackground.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -Self.verticalMargin),
|
||||||
|
|
||||||
|
statusHistoryView.topAnchor.constraint(equalTo: grayBackground.topAnchor, constant: Self.verticalMargin),
|
||||||
|
statusHistoryView.leadingAnchor.constraint(equalTo: grayBackground.leadingAnchor),
|
||||||
|
statusHistoryView.trailingAnchor.constraint(equalTo: grayBackground.trailingAnchor),
|
||||||
|
statusViewBottomConstraint,
|
||||||
|
].compactMap { $0 }
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate(constraints)
|
||||||
|
}
|
||||||
|
|
||||||
|
func configure(status: Status, statusEdit: StatusEdit, dateText: String) {
|
||||||
|
dateLabel.text = dateText
|
||||||
|
statusHistoryView.statusView.configure(status: status, statusEdit: statusEdit)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func prepareForReuse() {
|
||||||
|
statusHistoryView.prepareForReuse()
|
||||||
|
super.prepareForReuse()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||||
|
super.traitCollectionDidChange(previousTraitCollection)
|
||||||
|
updateContainerViewMarginConstraints()
|
||||||
|
}
|
||||||
|
|
||||||
|
override var accessibilityLabel: String? {
|
||||||
|
get {
|
||||||
|
(dateLabel.text ?? "") + ", " + (statusHistoryView.statusView.accessibilityLabel ?? "")
|
||||||
|
}
|
||||||
|
set {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - AdaptiveContainerMarginTableViewCell
|
||||||
|
extension StatusEditHistoryTableViewCell: AdaptiveContainerMarginTableViewCell {
|
||||||
|
var containerView: StatusHistoryView {
|
||||||
|
statusHistoryView
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import MastodonSDK
|
||||||
|
import CoreDataStack
|
||||||
|
import MastodonCore
|
||||||
|
import MastodonLocalization
|
||||||
|
|
||||||
|
class StatusEditHistoryViewController: UIViewController {
|
||||||
|
|
||||||
|
private let tableView: UITableView
|
||||||
|
|
||||||
|
var tableViewDataSource: UITableViewDiffableDataSource<Int, StatusEdit>?
|
||||||
|
var viewModel: StatusEditHistoryViewModel
|
||||||
|
private let dateFormatter: DateFormatter
|
||||||
|
|
||||||
|
init(viewModel: StatusEditHistoryViewModel) {
|
||||||
|
|
||||||
|
self.viewModel = viewModel
|
||||||
|
dateFormatter = DateFormatter()
|
||||||
|
dateFormatter.dateStyle = .medium
|
||||||
|
dateFormatter.timeStyle = .short
|
||||||
|
|
||||||
|
tableView = UITableView(frame: .zero)
|
||||||
|
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
tableView.separatorStyle = .none
|
||||||
|
tableView.register(StatusEditHistoryTableViewCell.self, forCellReuseIdentifier: StatusEditHistoryTableViewCell.identifier)
|
||||||
|
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
|
||||||
|
let tableViewDataSource = UITableViewDiffableDataSource<Int, StatusEdit>(tableView: tableView) {tableView, indexPath, itemIdentifier in
|
||||||
|
guard let cell = tableView.dequeueReusableCell(withIdentifier: StatusEditHistoryTableViewCell.identifier, for: indexPath) as? StatusEditHistoryTableViewCell else {
|
||||||
|
fatalError("Wrong cell")
|
||||||
|
}
|
||||||
|
|
||||||
|
let statusEdit = viewModel.edits[indexPath.row]
|
||||||
|
let dateText: String
|
||||||
|
|
||||||
|
if statusEdit == viewModel.edits.last {
|
||||||
|
dateText = L10n.Common.Controls.Status.EditHistory.originalPost(self.dateFormatter.string(from: statusEdit.createdAt))
|
||||||
|
} else {
|
||||||
|
dateText = self.dateFormatter.string(from: statusEdit.createdAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.prepareCell(cell, in: tableView)
|
||||||
|
cell.configure(status: viewModel.status, statusEdit: statusEdit, dateText: dateText)
|
||||||
|
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
tableView.dataSource = tableViewDataSource
|
||||||
|
tableView.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor
|
||||||
|
self.tableViewDataSource = tableViewDataSource
|
||||||
|
|
||||||
|
|
||||||
|
view.addSubview(tableView)
|
||||||
|
|
||||||
|
view.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor
|
||||||
|
setupConstraints()
|
||||||
|
|
||||||
|
title = L10n.Common.Controls.Status.EditHistory.title
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||||
|
|
||||||
|
private func setupConstraints() {
|
||||||
|
let constraints = tableView.pinTo(to: view)
|
||||||
|
NSLayoutConstraint.activate(constraints)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
var snapshot = NSDiffableDataSourceSnapshot<Int, StatusEdit>()
|
||||||
|
snapshot.appendSections([0])
|
||||||
|
snapshot.appendItems(viewModel.edits)
|
||||||
|
|
||||||
|
tableViewDataSource?.apply(snapshot)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CoreDataStack
|
||||||
|
import MastodonCore
|
||||||
|
import MastodonUI
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
struct StatusEditHistoryViewModel {
|
||||||
|
let status: Status
|
||||||
|
let edits: [StatusEdit]
|
||||||
|
|
||||||
|
let appContext: AppContext
|
||||||
|
let authContext: AuthContext
|
||||||
|
|
||||||
|
func prepareCell(_ cell: StatusEditHistoryTableViewCell, in tableView: UITableView) {
|
||||||
|
StatusSection.setupStatusPollHistoryDataSource(
|
||||||
|
context: appContext,
|
||||||
|
authContext: authContext,
|
||||||
|
statusView: cell.statusHistoryView.statusView
|
||||||
|
)
|
||||||
|
|
||||||
|
cell.statusHistoryView.statusView.frame.size.width = tableView.frame.width - cell.containerViewHorizontalMargin
|
||||||
|
cell.statusViewBottomConstraint?.constant = cell.statusHistoryView.statusView.mediaContainerView.isHidden ? -StatusEditHistoryTableViewCell.verticalMargin : 0
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import MastodonUI
|
||||||
|
|
||||||
|
class StatusHistoryView: UIView {
|
||||||
|
let statusView = StatusView()
|
||||||
|
|
||||||
|
private var statusViewLeadingConstraint: NSLayoutConstraint!
|
||||||
|
private var statusViewTrailingConstraint: NSLayoutConstraint!
|
||||||
|
|
||||||
|
init() {
|
||||||
|
super.init(frame: .zero)
|
||||||
|
statusView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
statusView.setup(style: .editHistory)
|
||||||
|
addSubview(statusView)
|
||||||
|
|
||||||
|
statusViewLeadingConstraint = statusView.leadingAnchor.constraint(equalTo: leadingAnchor)
|
||||||
|
statusViewTrailingConstraint = statusView.trailingAnchor.constraint(equalTo: trailingAnchor)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
statusView.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
statusView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
statusViewLeadingConstraint,
|
||||||
|
statusViewTrailingConstraint
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareForReuse() {
|
||||||
|
statusView.prepareForReuse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension StatusHistoryView: AdaptiveContainerView {
|
||||||
|
func updateContainerViewComponentsLayoutMarginsRelativeArrangementBehavior(isEnabled: Bool) {
|
||||||
|
statusView.updateContainerViewComponentsLayoutMarginsRelativeArrangementBehavior(isEnabled: isEnabled)
|
||||||
|
statusViewLeadingConstraint.constant = isEnabled ? 0 : StatusEditHistoryTableViewCell.horizontalMargin
|
||||||
|
statusViewTrailingConstraint.constant = isEnabled ? 0 : -StatusEditHistoryTableViewCell.horizontalMargin
|
||||||
|
}
|
||||||
|
}
|
|
@ -117,6 +117,7 @@ extension ThreadViewController {
|
||||||
let composeViewModel = ComposeViewModel(
|
let composeViewModel = ComposeViewModel(
|
||||||
context: context,
|
context: context,
|
||||||
authContext: viewModel.authContext,
|
authContext: viewModel.authContext,
|
||||||
|
composeContext: .composeStatus,
|
||||||
destination: .reply(parent: threadContext.status)
|
destination: .reply(parent: threadContext.status)
|
||||||
)
|
)
|
||||||
_ = coordinator.present(
|
_ = coordinator.present(
|
||||||
|
|
|
@ -115,7 +115,7 @@ extension ThreadViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
let hasChanges = newSnapshot.itemIdentifiers != oldSnapshot.itemIdentifiers
|
let hasChanges = newSnapshot.itemIdentifiers != oldSnapshot.itemIdentifiers
|
||||||
if !hasChanges {
|
if !hasChanges && !self.hasPendingStatusEditReload {
|
||||||
self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): snapshot not changes")
|
self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): snapshot not changes")
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
|
@ -140,6 +140,7 @@ extension ThreadViewModel {
|
||||||
newSnapshot: newSnapshot,
|
newSnapshot: newSnapshot,
|
||||||
difference: difference
|
difference: difference
|
||||||
)
|
)
|
||||||
|
self.hasPendingStatusEditReload = false
|
||||||
} // end Task
|
} // end Task
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
|
@ -66,7 +66,7 @@ extension ThreadViewModel.LoadThreadState {
|
||||||
override func didEnter(from previousState: GKState?) {
|
override func didEnter(from previousState: GKState?) {
|
||||||
super.didEnter(from: previousState)
|
super.didEnter(from: previousState)
|
||||||
|
|
||||||
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
|
guard let viewModel, let stateMachine else { return }
|
||||||
|
|
||||||
guard let threadContext = viewModel.threadContext else {
|
guard let threadContext = viewModel.threadContext else {
|
||||||
stateMachine.enter(Fail.self)
|
stateMachine.enter(Fail.self)
|
||||||
|
@ -79,11 +79,14 @@ extension ThreadViewModel.LoadThreadState {
|
||||||
statusID: threadContext.statusID,
|
statusID: threadContext.statusID,
|
||||||
authenticationBox: viewModel.authContext.mastodonAuthenticationBox
|
authenticationBox: viewModel.authContext.mastodonAuthenticationBox
|
||||||
)
|
)
|
||||||
|
|
||||||
await enter(state: NoMore.self)
|
await enter(state: NoMore.self)
|
||||||
|
|
||||||
// assert(!Thread.isMainThread)
|
// assert(!Thread.isMainThread)
|
||||||
// await Task.sleep(1_000_000_000) // 1s delay to prevent UI render issue
|
// await Task.sleep(1_000_000_000) // 1s delay to prevent UI render issue
|
||||||
|
|
||||||
|
let statusHistory = try await viewModel.context.apiService.getHistory(forStatusID: threadContext.statusID,
|
||||||
|
authenticationBox: viewModel.authContext.mastodonAuthenticationBox)
|
||||||
|
|
||||||
viewModel.mastodonStatusThreadViewModel.appendAncestor(
|
viewModel.mastodonStatusThreadViewModel.appendAncestor(
|
||||||
domain: threadContext.domain,
|
domain: threadContext.domain,
|
||||||
|
|
|
@ -33,6 +33,7 @@ class ThreadViewModel {
|
||||||
var diffableDataSource: UITableViewDiffableDataSource<StatusSection, StatusItem>?
|
var diffableDataSource: UITableViewDiffableDataSource<StatusSection, StatusItem>?
|
||||||
@Published var root: StatusItem.Thread?
|
@Published var root: StatusItem.Thread?
|
||||||
@Published var threadContext: ThreadContext?
|
@Published var threadContext: ThreadContext?
|
||||||
|
@Published var hasPendingStatusEditReload = false
|
||||||
|
|
||||||
private(set) lazy var loadThreadStateMachine: GKStateMachine = {
|
private(set) lazy var loadThreadStateMachine: GKStateMachine = {
|
||||||
let stateMachine = GKStateMachine(states: [
|
let stateMachine = GKStateMachine(states: [
|
||||||
|
@ -95,6 +96,15 @@ class ThreadViewModel {
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
context.publisherService
|
||||||
|
.statusPublishResult
|
||||||
|
.sink { [weak self] value in
|
||||||
|
if case let Result.success(result) = value, case StatusPublishResult.edit = result {
|
||||||
|
self?.hasPendingStatusEditReload = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
|
|
|
@ -226,6 +226,7 @@ extension SceneDelegate {
|
||||||
let composeViewModel = ComposeViewModel(
|
let composeViewModel = ComposeViewModel(
|
||||||
context: AppContext.shared,
|
context: AppContext.shared,
|
||||||
authContext: authContext,
|
authContext: authContext,
|
||||||
|
composeContext: .composeStatus,
|
||||||
destination: .topLevel
|
destination: .topLevel
|
||||||
)
|
)
|
||||||
_ = coordinator?.present(scene: .compose(viewModel: composeViewModel), from: nil, transition: .modal(animated: true, completion: nil))
|
_ = coordinator?.present(scene: .compose(viewModel: composeViewModel), from: nil, transition: .modal(animated: true, completion: nil))
|
||||||
|
|
|
@ -3,6 +3,6 @@
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>_XCCurrentVersionName</key>
|
<key>_XCCurrentVersionName</key>
|
||||||
<string>CoreData 7.xcdatamodel</string>
|
<string>CoreData 8.xcdatamodel</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
|
@ -0,0 +1,292 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21513" systemVersion="22D68" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||||
|
<entity name="Application" representedClassName="CoreDataStack.Application" syncable="YES">
|
||||||
|
<attribute name="identifier" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="name" attributeType="String"/>
|
||||||
|
<attribute name="vapidKey" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="website" optional="YES" attributeType="String"/>
|
||||||
|
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="application" inverseEntity="Status"/>
|
||||||
|
</entity>
|
||||||
|
<entity name="Card" representedClassName="CoreDataStack.Card" syncable="YES">
|
||||||
|
<attribute name="authorName" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="authorURLRaw" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="blurhash" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="desc" attributeType="String"/>
|
||||||
|
<attribute name="embedURLRaw" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="height" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="html" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="image" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="providerName" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="providerURLRaw" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="title" attributeType="String"/>
|
||||||
|
<attribute name="typeRaw" attributeType="String"/>
|
||||||
|
<attribute name="urlRaw" attributeType="String"/>
|
||||||
|
<attribute name="width" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
|
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="card" inverseEntity="Status"/>
|
||||||
|
</entity>
|
||||||
|
<entity name="DomainBlock" representedClassName="CoreDataStack.DomainBlock" syncable="YES">
|
||||||
|
<attribute name="blockedDomain" attributeType="String"/>
|
||||||
|
<attribute name="createAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="domain" attributeType="String"/>
|
||||||
|
<attribute name="userID" attributeType="String"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="userID"/>
|
||||||
|
<constraint value="domain"/>
|
||||||
|
<constraint value="blockedDomain"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<entity name="Emoji" representedClassName="CoreDataStack.Emoji" syncable="YES">
|
||||||
|
<attribute name="category" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="createAt" attributeType="Date" defaultDateTimeInterval="631123200" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="identifier" attributeType="UUID" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="shortcode" attributeType="String"/>
|
||||||
|
<attribute name="staticURL" attributeType="String"/>
|
||||||
|
<attribute name="url" attributeType="String"/>
|
||||||
|
<attribute name="visibleInPicker" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||||
|
</entity>
|
||||||
|
<entity name="Feed" representedClassName="CoreDataStack.Feed" syncable="YES">
|
||||||
|
<attribute name="acctRaw" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="hasMore" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="isLoadingMore" transient="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="kindRaw" attributeType="String"/>
|
||||||
|
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<relationship name="notification" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Notification" inverseName="feeds" inverseEntity="Notification"/>
|
||||||
|
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="feeds" inverseEntity="Status"/>
|
||||||
|
</entity>
|
||||||
|
<entity name="Instance" representedClassName="CoreDataStack.Instance" syncable="YES">
|
||||||
|
<attribute name="configurationRaw" optional="YES" attributeType="Binary"/>
|
||||||
|
<attribute name="configurationV2Raw" optional="YES" attributeType="Binary"/>
|
||||||
|
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="domain" attributeType="String"/>
|
||||||
|
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="version" optional="YES" attributeType="String"/>
|
||||||
|
<relationship name="authentications" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonAuthentication" inverseName="instance" inverseEntity="MastodonAuthentication"/>
|
||||||
|
</entity>
|
||||||
|
<entity name="MastodonAuthentication" representedClassName="CoreDataStack.MastodonAuthentication" syncable="YES">
|
||||||
|
<attribute name="activedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="appAccessToken" attributeType="String"/>
|
||||||
|
<attribute name="clientID" attributeType="String"/>
|
||||||
|
<attribute name="clientSecret" attributeType="String"/>
|
||||||
|
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="domain" attributeType="String"/>
|
||||||
|
<attribute name="identifier" attributeType="UUID" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="userAccessToken" attributeType="String"/>
|
||||||
|
<attribute name="userID" attributeType="String"/>
|
||||||
|
<attribute name="username" attributeType="String"/>
|
||||||
|
<relationship name="instance" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Instance" inverseName="authentications" inverseEntity="Instance"/>
|
||||||
|
<relationship name="user" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="mastodonAuthentication" inverseEntity="MastodonUser"/>
|
||||||
|
</entity>
|
||||||
|
<entity name="MastodonUser" representedClassName="CoreDataStack.MastodonUser" syncable="YES">
|
||||||
|
<attribute name="acct" attributeType="String"/>
|
||||||
|
<attribute name="avatar" attributeType="String"/>
|
||||||
|
<attribute name="avatarStatic" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="bot" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="displayName" attributeType="String"/>
|
||||||
|
<attribute name="domain" attributeType="String"/>
|
||||||
|
<attribute name="emojis" optional="YES" attributeType="Binary"/>
|
||||||
|
<attribute name="fields" optional="YES" attributeType="Binary"/>
|
||||||
|
<attribute name="followersCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="followingCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="header" attributeType="String"/>
|
||||||
|
<attribute name="headerStatic" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="id" attributeType="String"/>
|
||||||
|
<attribute name="identifier" attributeType="String"/>
|
||||||
|
<attribute name="locked" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="note" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="statusesCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="suspended" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="url" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="username" attributeType="String"/>
|
||||||
|
<relationship name="blocking" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="blockingBy" inverseEntity="MastodonUser"/>
|
||||||
|
<relationship name="blockingBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="blocking" inverseEntity="MastodonUser"/>
|
||||||
|
<relationship name="bookmarked" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="bookmarkedBy" inverseEntity="Status"/>
|
||||||
|
<relationship name="domainBlocking" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="domainBlockingBy" inverseEntity="MastodonUser"/>
|
||||||
|
<relationship name="domainBlockingBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="domainBlocking" inverseEntity="MastodonUser"/>
|
||||||
|
<relationship name="endorsed" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="endorsedBy" inverseEntity="MastodonUser"/>
|
||||||
|
<relationship name="endorsedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="endorsed" inverseEntity="MastodonUser"/>
|
||||||
|
<relationship name="favourite" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="favouritedBy" inverseEntity="Status"/>
|
||||||
|
<relationship name="followedTags" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Tag" inverseName="followedBy" inverseEntity="Tag"/>
|
||||||
|
<relationship name="following" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="followingBy" inverseEntity="MastodonUser"/>
|
||||||
|
<relationship name="followingBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="following" inverseEntity="MastodonUser"/>
|
||||||
|
<relationship name="followRequested" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="followRequestedBy" inverseEntity="MastodonUser"/>
|
||||||
|
<relationship name="followRequestedBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="followRequested" inverseEntity="MastodonUser"/>
|
||||||
|
<relationship name="mastodonAuthentication" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonAuthentication" inverseName="user" inverseEntity="MastodonAuthentication"/>
|
||||||
|
<relationship name="muted" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="mutedBy" inverseEntity="Status"/>
|
||||||
|
<relationship name="muting" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="mutingBy" inverseEntity="MastodonUser"/>
|
||||||
|
<relationship name="mutingBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="muting" inverseEntity="MastodonUser"/>
|
||||||
|
<relationship name="notifications" toMany="YES" deletionRule="Nullify" destinationEntity="Notification" inverseName="account" inverseEntity="Notification"/>
|
||||||
|
<relationship name="pinnedStatus" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="pinnedBy" inverseEntity="Status"/>
|
||||||
|
<relationship name="privateNotes" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="PrivateNote" inverseName="to" inverseEntity="PrivateNote"/>
|
||||||
|
<relationship name="privateNotesTo" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="PrivateNote" inverseName="from" inverseEntity="PrivateNote"/>
|
||||||
|
<relationship name="reblogged" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="rebloggedBy" inverseEntity="Status"/>
|
||||||
|
<relationship name="searchHistories" toMany="YES" deletionRule="Nullify" destinationEntity="SearchHistory" inverseName="account" inverseEntity="SearchHistory"/>
|
||||||
|
<relationship name="showingReblogs" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="showingReblogsBy" inverseEntity="MastodonUser"/>
|
||||||
|
<relationship name="showingReblogsBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="showingReblogs" inverseEntity="MastodonUser"/>
|
||||||
|
<relationship name="statuses" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="author" inverseEntity="Status"/>
|
||||||
|
<relationship name="votePollOptions" toMany="YES" deletionRule="Nullify" destinationEntity="PollOption" inverseName="votedBy" inverseEntity="PollOption"/>
|
||||||
|
<relationship name="votePolls" toMany="YES" deletionRule="Nullify" destinationEntity="Poll" inverseName="votedBy" inverseEntity="Poll"/>
|
||||||
|
</entity>
|
||||||
|
<entity name="Notification" representedClassName="CoreDataStack.Notification" syncable="YES">
|
||||||
|
<attribute name="createAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="domain" attributeType="String"/>
|
||||||
|
<attribute name="followRequestState" optional="YES" attributeType="Binary"/>
|
||||||
|
<attribute name="id" attributeType="String"/>
|
||||||
|
<attribute name="transientFollowRequestState" optional="YES" transient="YES" attributeType="Binary"/>
|
||||||
|
<attribute name="typeRaw" attributeType="String"/>
|
||||||
|
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="userID" attributeType="String"/>
|
||||||
|
<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="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="notifications" inverseEntity="Status"/>
|
||||||
|
</entity>
|
||||||
|
<entity name="Poll" representedClassName="CoreDataStack.Poll" syncable="YES">
|
||||||
|
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="domain" attributeType="String" defaultValueString=""/>
|
||||||
|
<attribute name="expired" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="expiresAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="id" attributeType="String"/>
|
||||||
|
<attribute name="isVoting" transient="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="multiple" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="votersCount" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="votesCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
|
<relationship name="options" toMany="YES" deletionRule="Cascade" destinationEntity="PollOption" inverseName="poll" inverseEntity="PollOption"/>
|
||||||
|
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="poll" inverseEntity="Status"/>
|
||||||
|
<relationship name="votedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="votePolls" inverseEntity="MastodonUser"/>
|
||||||
|
</entity>
|
||||||
|
<entity name="PollOption" representedClassName="CoreDataStack.PollOption" syncable="YES">
|
||||||
|
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="index" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="isSelected" transient="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="title" attributeType="String"/>
|
||||||
|
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="votesCount" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
|
<relationship name="poll" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Poll" inverseName="options" inverseEntity="Poll"/>
|
||||||
|
<relationship name="votedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="votePollOptions" inverseEntity="MastodonUser"/>
|
||||||
|
</entity>
|
||||||
|
<entity name="PrivateNote" representedClassName="CoreDataStack.PrivateNote" syncable="YES">
|
||||||
|
<attribute name="note" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<relationship name="from" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="privateNotesTo" inverseEntity="MastodonUser"/>
|
||||||
|
<relationship name="to" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="privateNotes" inverseEntity="MastodonUser"/>
|
||||||
|
</entity>
|
||||||
|
<entity name="SearchHistory" representedClassName="CoreDataStack.SearchHistory" syncable="YES">
|
||||||
|
<attribute name="createAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="domain" attributeType="String" defaultValueString=""/>
|
||||||
|
<attribute name="identifier" attributeType="UUID" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="updatedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="userID" attributeType="String" defaultValueString=""/>
|
||||||
|
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="searchHistories" inverseEntity="MastodonUser"/>
|
||||||
|
<relationship name="hashtag" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Tag" inverseName="searchHistories" inverseEntity="Tag"/>
|
||||||
|
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="searchHistories" inverseEntity="Status"/>
|
||||||
|
</entity>
|
||||||
|
<entity name="Setting" representedClassName="CoreDataStack.Setting" syncable="YES">
|
||||||
|
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="domain" attributeType="String"/>
|
||||||
|
<attribute name="preferredStaticAvatar" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="preferredStaticEmoji" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="preferredTrueBlackDarkMode" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="preferredUsingDefaultBrowser" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="rawRecentLanguages" optional="YES" attributeType="Binary"/>
|
||||||
|
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="userID" attributeType="String"/>
|
||||||
|
<relationship name="subscriptions" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Subscription" inverseName="setting" inverseEntity="Subscription"/>
|
||||||
|
</entity>
|
||||||
|
<entity name="Status" representedClassName="CoreDataStack.Status" syncable="YES">
|
||||||
|
<attribute name="attachments" optional="YES" attributeType="Binary"/>
|
||||||
|
<attribute name="content" attributeType="String"/>
|
||||||
|
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="deletedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="domain" attributeType="String"/>
|
||||||
|
<attribute name="editedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="emojis" optional="YES" attributeType="Binary"/>
|
||||||
|
<attribute name="favouritesCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="id" attributeType="String"/>
|
||||||
|
<attribute name="identifier" attributeType="String"/>
|
||||||
|
<attribute name="inReplyToAccountID" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="inReplyToID" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="isSensitiveToggled" transient="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="language" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="mentions" optional="YES" attributeType="Binary"/>
|
||||||
|
<attribute name="reblogsCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="repliesCount" optional="YES" attributeType="Integer 64" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="revealedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="sensitive" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="spoilerText" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="text" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="translatedContent" optional="YES" transient="YES" attributeType="Transformable"/>
|
||||||
|
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="uri" attributeType="String"/>
|
||||||
|
<attribute name="url" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="visibilityRaw" optional="YES" attributeType="String" elementID="visibility"/>
|
||||||
|
<relationship name="application" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Application" inverseName="status" inverseEntity="Application"/>
|
||||||
|
<relationship name="author" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="statuses" inverseEntity="MastodonUser"/>
|
||||||
|
<relationship name="bookmarkedBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="bookmarked" inverseEntity="MastodonUser"/>
|
||||||
|
<relationship name="card" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Card" inverseName="status" inverseEntity="Card"/>
|
||||||
|
<relationship name="editHistory" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="StatusEdit" inverseName="status" inverseEntity="StatusEdit"/>
|
||||||
|
<relationship name="favouritedBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="favourite" inverseEntity="MastodonUser"/>
|
||||||
|
<relationship name="feeds" toMany="YES" deletionRule="Cascade" destinationEntity="Feed" inverseName="status" inverseEntity="Feed"/>
|
||||||
|
<relationship name="mutedBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="muted" inverseEntity="MastodonUser"/>
|
||||||
|
<relationship name="notifications" toMany="YES" deletionRule="Cascade" destinationEntity="Notification" inverseName="status" inverseEntity="Notification"/>
|
||||||
|
<relationship name="pinnedBy" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="pinnedStatus" inverseEntity="MastodonUser"/>
|
||||||
|
<relationship name="poll" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="Poll" inverseName="status" inverseEntity="Poll"/>
|
||||||
|
<relationship name="reblog" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="reblogFrom" inverseEntity="Status"/>
|
||||||
|
<relationship name="reblogFrom" toMany="YES" deletionRule="Cascade" destinationEntity="Status" inverseName="reblog" inverseEntity="Status"/>
|
||||||
|
<relationship name="rebloggedBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="reblogged" inverseEntity="MastodonUser"/>
|
||||||
|
<relationship name="replyFrom" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="replyTo" inverseEntity="Status"/>
|
||||||
|
<relationship name="replyTo" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="replyFrom" inverseEntity="Status"/>
|
||||||
|
<relationship name="searchHistories" toMany="YES" deletionRule="Cascade" destinationEntity="SearchHistory" inverseName="status" inverseEntity="SearchHistory"/>
|
||||||
|
</entity>
|
||||||
|
<entity name="StatusEdit" representedClassName="CoreDataStack.StatusEdit" syncable="YES">
|
||||||
|
<attribute name="attachments" optional="YES" attributeType="Binary"/>
|
||||||
|
<attribute name="content" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="createdAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="emojis" optional="YES" attributeType="Binary"/>
|
||||||
|
<attribute name="poll" optional="YES" attributeType="Binary"/>
|
||||||
|
<attribute name="sensitive" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="spoilerText" optional="YES" attributeType="String"/>
|
||||||
|
<relationship name="author" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser"/>
|
||||||
|
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="editHistory" inverseEntity="Status"/>
|
||||||
|
</entity>
|
||||||
|
<entity name="Subscription" representedClassName="CoreDataStack.Subscription" syncable="YES">
|
||||||
|
<attribute name="activedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="endpoint" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="id" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="policyRaw" attributeType="String"/>
|
||||||
|
<attribute name="serverKey" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="userToken" optional="YES" attributeType="String"/>
|
||||||
|
<relationship name="alert" maxCount="1" deletionRule="Cascade" destinationEntity="SubscriptionAlerts" inverseName="subscription" inverseEntity="SubscriptionAlerts"/>
|
||||||
|
<relationship name="setting" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Setting" inverseName="subscriptions" inverseEntity="Setting"/>
|
||||||
|
</entity>
|
||||||
|
<entity name="SubscriptionAlerts" representedClassName="CoreDataStack.SubscriptionAlerts" syncable="YES">
|
||||||
|
<attribute name="createdAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="favouriteRaw" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="followRaw" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="followRequestRaw" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="mentionRaw" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="pollRaw" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="reblogRaw" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="updatedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<relationship name="subscription" maxCount="1" deletionRule="Nullify" destinationEntity="Subscription" inverseName="alert" inverseEntity="Subscription"/>
|
||||||
|
</entity>
|
||||||
|
<entity name="Tag" representedClassName="CoreDataStack.Tag" syncable="YES">
|
||||||
|
<attribute name="createAt" attributeType="Date" defaultDateTimeInterval="631123200" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="domain" attributeType="String" defaultValueString=""/>
|
||||||
|
<attribute name="following" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="histories" optional="YES" attributeType="Binary"/>
|
||||||
|
<attribute name="identifier" attributeType="UUID" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="name" attributeType="String"/>
|
||||||
|
<attribute name="updatedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="url" attributeType="String"/>
|
||||||
|
<relationship name="followedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="followedTags" inverseEntity="MastodonUser"/>
|
||||||
|
<relationship name="searchHistories" toMany="YES" deletionRule="Nullify" destinationEntity="SearchHistory" inverseName="hashtag" inverseEntity="SearchHistory"/>
|
||||||
|
</entity>
|
||||||
|
</model>
|
|
@ -1,5 +1,5 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="18154" systemVersion="20G71" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21512" systemVersion="21G115" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||||
<entity name="Application" representedClassName=".Application" syncable="YES">
|
<entity name="Application" representedClassName=".Application" syncable="YES">
|
||||||
<attribute name="identifier" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
|
<attribute name="identifier" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
|
||||||
<attribute name="name" attributeType="String"/>
|
<attribute name="name" attributeType="String"/>
|
||||||
|
@ -276,25 +276,4 @@
|
||||||
<relationship name="searchHistory" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SearchHistory" inverseName="hashtag" inverseEntity="SearchHistory"/>
|
<relationship name="searchHistory" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SearchHistory" inverseName="hashtag" inverseEntity="SearchHistory"/>
|
||||||
<relationship name="statuses" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="tags" inverseEntity="Status"/>
|
<relationship name="statuses" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="tags" inverseEntity="Status"/>
|
||||||
</entity>
|
</entity>
|
||||||
<elements>
|
|
||||||
<element name="Application" positionX="0" positionY="0" width="128" height="104"/>
|
|
||||||
<element name="Attachment" positionX="0" positionY="0" width="128" height="254"/>
|
|
||||||
<element name="DomainBlock" positionX="45" positionY="162" width="128" height="89"/>
|
|
||||||
<element name="Emoji" positionX="0" positionY="0" width="128" height="134"/>
|
|
||||||
<element name="History" positionX="0" positionY="0" width="128" height="119"/>
|
|
||||||
<element name="HomeTimelineIndex" positionX="0" positionY="0" width="128" height="134"/>
|
|
||||||
<element name="MastodonAuthentication" positionX="0" positionY="0" width="128" height="209"/>
|
|
||||||
<element name="MastodonNotification" positionX="9" positionY="162" width="128" height="164"/>
|
|
||||||
<element name="MastodonUser" positionX="0" positionY="0" width="128" height="734"/>
|
|
||||||
<element name="Mention" positionX="0" positionY="0" width="128" height="149"/>
|
|
||||||
<element name="Poll" positionX="0" positionY="0" width="128" height="194"/>
|
|
||||||
<element name="PollOption" positionX="0" positionY="0" width="128" height="134"/>
|
|
||||||
<element name="PrivateNote" positionX="0" positionY="0" width="128" height="89"/>
|
|
||||||
<element name="SearchHistory" positionX="0" positionY="0" width="128" height="149"/>
|
|
||||||
<element name="Setting" positionX="72" positionY="162" width="128" height="179"/>
|
|
||||||
<element name="Status" positionX="0" positionY="0" width="128" height="614"/>
|
|
||||||
<element name="Subscription" positionX="81" positionY="171" width="128" height="179"/>
|
|
||||||
<element name="SubscriptionAlerts" positionX="72" positionY="162" width="128" height="14"/>
|
|
||||||
<element name="Tag" positionX="0" positionY="0" width="128" height="149"/>
|
|
||||||
</elements>
|
|
||||||
</model>
|
</model>
|
|
@ -38,7 +38,7 @@ public final class Poll: NSManagedObject {
|
||||||
@NSManaged public private(set) var isVoting: Bool
|
@NSManaged public private(set) var isVoting: Bool
|
||||||
|
|
||||||
// one-to-one relationship
|
// one-to-one relationship
|
||||||
@NSManaged public private(set) var status: Status
|
@NSManaged public private(set) var status: Status?
|
||||||
|
|
||||||
// one-to-many relationship
|
// one-to-many relationship
|
||||||
@NSManaged public private(set) var options: Set<PollOption>
|
@NSManaged public private(set) var options: Set<PollOption>
|
||||||
|
@ -324,3 +324,9 @@ extension Poll: AutoUpdatableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public extension Set<PollOption> {
|
||||||
|
func sortedByIndex() -> [PollOption] {
|
||||||
|
sorted(by: { lhs, rhs in lhs.index < rhs.index })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -27,7 +27,8 @@ public final class PollOption: NSManagedObject {
|
||||||
@NSManaged public private(set) var isSelected: Bool
|
@NSManaged public private(set) var isSelected: Bool
|
||||||
|
|
||||||
// many-to-one relationship
|
// many-to-one relationship
|
||||||
@NSManaged public private(set) var poll: Poll
|
// sourcery: autoUpdatableObject, autoGenerateProperty
|
||||||
|
@NSManaged public private(set) var poll: Poll?
|
||||||
|
|
||||||
// many-to-many relationship
|
// many-to-many relationship
|
||||||
@NSManaged public private(set) var votedBy: Set<MastodonUser>?
|
@NSManaged public private(set) var votedBy: Set<MastodonUser>?
|
||||||
|
@ -125,19 +126,22 @@ extension PollOption: AutoGenerateProperty {
|
||||||
public let votesCount: Int64
|
public let votesCount: Int64
|
||||||
public let createdAt: Date
|
public let createdAt: Date
|
||||||
public let updatedAt: Date
|
public let updatedAt: Date
|
||||||
|
public let poll: Poll?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
index: Int64,
|
index: Int64,
|
||||||
title: String,
|
title: String,
|
||||||
votesCount: Int64,
|
votesCount: Int64,
|
||||||
createdAt: Date,
|
createdAt: Date,
|
||||||
updatedAt: Date
|
updatedAt: Date,
|
||||||
|
poll: Poll?
|
||||||
) {
|
) {
|
||||||
self.index = index
|
self.index = index
|
||||||
self.title = title
|
self.title = title
|
||||||
self.votesCount = votesCount
|
self.votesCount = votesCount
|
||||||
self.createdAt = createdAt
|
self.createdAt = createdAt
|
||||||
self.updatedAt = updatedAt
|
self.updatedAt = updatedAt
|
||||||
|
self.poll = poll
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,12 +151,14 @@ extension PollOption: AutoGenerateProperty {
|
||||||
self.votesCount = property.votesCount
|
self.votesCount = property.votesCount
|
||||||
self.createdAt = property.createdAt
|
self.createdAt = property.createdAt
|
||||||
self.updatedAt = property.updatedAt
|
self.updatedAt = property.updatedAt
|
||||||
|
self.poll = property.poll
|
||||||
}
|
}
|
||||||
|
|
||||||
public func update(property: Property) {
|
public func update(property: Property) {
|
||||||
update(title: property.title)
|
update(title: property.title)
|
||||||
update(votesCount: property.votesCount)
|
update(votesCount: property.votesCount)
|
||||||
update(updatedAt: property.updatedAt)
|
update(updatedAt: property.updatedAt)
|
||||||
|
update(poll: property.poll)
|
||||||
}
|
}
|
||||||
// sourcery:end
|
// sourcery:end
|
||||||
}
|
}
|
||||||
|
@ -183,6 +189,11 @@ extension PollOption: AutoUpdatableObject {
|
||||||
self.isSelected = isSelected
|
self.isSelected = isSelected
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
public func update(poll: Poll?) {
|
||||||
|
if self.poll != poll {
|
||||||
|
self.poll = poll
|
||||||
|
}
|
||||||
|
}
|
||||||
// sourcery:end
|
// sourcery:end
|
||||||
|
|
||||||
public func update(voted: Bool, by: MastodonUser) {
|
public func update(voted: Bool, by: MastodonUser) {
|
||||||
|
|
|
@ -32,6 +32,10 @@ public final class Status: NSManagedObject {
|
||||||
|
|
||||||
// sourcery: autoUpdatableObject, autoGenerateProperty
|
// sourcery: autoUpdatableObject, autoGenerateProperty
|
||||||
@NSManaged public private(set) var createdAt: Date
|
@NSManaged public private(set) var createdAt: Date
|
||||||
|
|
||||||
|
// sourcery: autoUpdatableObject, autoGenerateProperty
|
||||||
|
@NSManaged public private(set) var editedAt: Date?
|
||||||
|
|
||||||
// sourcery: autoUpdatableObject, autoGenerateProperty
|
// sourcery: autoUpdatableObject, autoGenerateProperty
|
||||||
@NSManaged public private(set) var content: String
|
@NSManaged public private(set) var content: String
|
||||||
|
|
||||||
|
@ -53,7 +57,8 @@ public final class Status: NSManagedObject {
|
||||||
|
|
||||||
// sourcery: autoUpdatableObject
|
// sourcery: autoUpdatableObject
|
||||||
@NSManaged public private(set) var isSensitiveToggled: Bool
|
@NSManaged public private(set) var isSensitiveToggled: Bool
|
||||||
|
|
||||||
|
// sourcery: autoGenerateRelationship
|
||||||
@NSManaged public private(set) var application: Application?
|
@NSManaged public private(set) var application: Application?
|
||||||
|
|
||||||
// Informational
|
// Informational
|
||||||
|
@ -104,6 +109,8 @@ public final class Status: NSManagedObject {
|
||||||
@NSManaged public private(set) var replyFrom: Set<Status>
|
@NSManaged public private(set) var replyFrom: Set<Status>
|
||||||
@NSManaged public private(set) var notifications: Set<Notification>
|
@NSManaged public private(set) var notifications: Set<Notification>
|
||||||
@NSManaged public private(set) var searchHistories: Set<SearchHistory>
|
@NSManaged public private(set) var searchHistories: Set<SearchHistory>
|
||||||
|
|
||||||
|
@NSManaged public private(set) var editHistory: Set<StatusEdit>?
|
||||||
|
|
||||||
// sourcery: autoUpdatableObject, autoGenerateProperty
|
// sourcery: autoUpdatableObject, autoGenerateProperty
|
||||||
@NSManaged public private(set) var updatedAt: Date
|
@NSManaged public private(set) var updatedAt: Date
|
||||||
|
@ -176,8 +183,8 @@ extension Status {
|
||||||
didAccessValue(forKey: keyPath)
|
didAccessValue(forKey: keyPath)
|
||||||
do {
|
do {
|
||||||
guard let data = _data else { return [] }
|
guard let data = _data else { return [] }
|
||||||
let emojis = try JSONDecoder().decode([MastodonMention].self, from: data)
|
let mentions = try JSONDecoder().decode([MastodonMention].self, from: data)
|
||||||
return emojis
|
return mentions
|
||||||
} catch {
|
} catch {
|
||||||
assertionFailure(error.localizedDescription)
|
assertionFailure(error.localizedDescription)
|
||||||
return []
|
return []
|
||||||
|
@ -269,6 +276,7 @@ extension Status: AutoGenerateProperty {
|
||||||
public let id: String
|
public let id: String
|
||||||
public let uri: String
|
public let uri: String
|
||||||
public let createdAt: Date
|
public let createdAt: Date
|
||||||
|
public let editedAt: Date?
|
||||||
public let content: String
|
public let content: String
|
||||||
public let visibility: MastodonVisibility
|
public let visibility: MastodonVisibility
|
||||||
public let sensitive: Bool
|
public let sensitive: Bool
|
||||||
|
@ -293,6 +301,7 @@ extension Status: AutoGenerateProperty {
|
||||||
id: String,
|
id: String,
|
||||||
uri: String,
|
uri: String,
|
||||||
createdAt: Date,
|
createdAt: Date,
|
||||||
|
editedAt: Date?,
|
||||||
content: String,
|
content: String,
|
||||||
visibility: MastodonVisibility,
|
visibility: MastodonVisibility,
|
||||||
sensitive: Bool,
|
sensitive: Bool,
|
||||||
|
@ -316,6 +325,7 @@ extension Status: AutoGenerateProperty {
|
||||||
self.id = id
|
self.id = id
|
||||||
self.uri = uri
|
self.uri = uri
|
||||||
self.createdAt = createdAt
|
self.createdAt = createdAt
|
||||||
|
self.editedAt = editedAt
|
||||||
self.content = content
|
self.content = content
|
||||||
self.visibility = visibility
|
self.visibility = visibility
|
||||||
self.sensitive = sensitive
|
self.sensitive = sensitive
|
||||||
|
@ -342,6 +352,7 @@ extension Status: AutoGenerateProperty {
|
||||||
self.id = property.id
|
self.id = property.id
|
||||||
self.uri = property.uri
|
self.uri = property.uri
|
||||||
self.createdAt = property.createdAt
|
self.createdAt = property.createdAt
|
||||||
|
self.editedAt = property.editedAt
|
||||||
self.content = property.content
|
self.content = property.content
|
||||||
self.visibility = property.visibility
|
self.visibility = property.visibility
|
||||||
self.sensitive = property.sensitive
|
self.sensitive = property.sensitive
|
||||||
|
@ -363,6 +374,7 @@ extension Status: AutoGenerateProperty {
|
||||||
|
|
||||||
public func update(property: Property) {
|
public func update(property: Property) {
|
||||||
update(createdAt: property.createdAt)
|
update(createdAt: property.createdAt)
|
||||||
|
update(editedAt: property.editedAt)
|
||||||
update(content: property.content)
|
update(content: property.content)
|
||||||
update(visibility: property.visibility)
|
update(visibility: property.visibility)
|
||||||
update(sensitive: property.sensitive)
|
update(sensitive: property.sensitive)
|
||||||
|
@ -391,17 +403,20 @@ extension Status: AutoGenerateRelationship {
|
||||||
// Generated using Sourcery
|
// Generated using Sourcery
|
||||||
// DO NOT EDIT
|
// DO NOT EDIT
|
||||||
public struct Relationship {
|
public struct Relationship {
|
||||||
|
public let application: Application?
|
||||||
public let author: MastodonUser
|
public let author: MastodonUser
|
||||||
public let reblog: Status?
|
public let reblog: Status?
|
||||||
public let poll: Poll?
|
public let poll: Poll?
|
||||||
public let card: Card?
|
public let card: Card?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
|
application: Application?,
|
||||||
author: MastodonUser,
|
author: MastodonUser,
|
||||||
reblog: Status?,
|
reblog: Status?,
|
||||||
poll: Poll?,
|
poll: Poll?,
|
||||||
card: Card?
|
card: Card?
|
||||||
) {
|
) {
|
||||||
|
self.application = application
|
||||||
self.author = author
|
self.author = author
|
||||||
self.reblog = reblog
|
self.reblog = reblog
|
||||||
self.poll = poll
|
self.poll = poll
|
||||||
|
@ -410,6 +425,7 @@ extension Status: AutoGenerateRelationship {
|
||||||
}
|
}
|
||||||
|
|
||||||
public func configure(relationship: Relationship) {
|
public func configure(relationship: Relationship) {
|
||||||
|
self.application = relationship.application
|
||||||
self.author = relationship.author
|
self.author = relationship.author
|
||||||
self.reblog = relationship.reblog
|
self.reblog = relationship.reblog
|
||||||
self.poll = relationship.poll
|
self.poll = relationship.poll
|
||||||
|
@ -429,6 +445,11 @@ extension Status: AutoUpdatableObject {
|
||||||
self.createdAt = createdAt
|
self.createdAt = createdAt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
public func update(editedAt: Date?) {
|
||||||
|
if self.editedAt != editedAt {
|
||||||
|
self.editedAt = editedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
public func update(content: String) {
|
public func update(content: String) {
|
||||||
if self.content != content {
|
if self.content != content {
|
||||||
self.content = content
|
self.content = content
|
||||||
|
@ -587,6 +608,10 @@ extension Status: AutoUpdatableObject {
|
||||||
public func update(isReveal: Bool) {
|
public func update(isReveal: Bool) {
|
||||||
revealedAt = isReveal ? Date() : nil
|
revealedAt = isReveal ? Date() : nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func update(editHistory: Set<StatusEdit>) {
|
||||||
|
self.editHistory = editHistory
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Status {
|
extension Status {
|
||||||
|
|
|
@ -0,0 +1,226 @@
|
||||||
|
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CoreData
|
||||||
|
|
||||||
|
public final class StatusEdit: NSManagedObject {
|
||||||
|
public final class Poll: NSObject, Codable {
|
||||||
|
public final class Option: NSObject, Codable {
|
||||||
|
public let title: String
|
||||||
|
|
||||||
|
public init(title: String) {
|
||||||
|
self.title = title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public let options: [Option]
|
||||||
|
|
||||||
|
public init(options: [Option]) {
|
||||||
|
self.options = options
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sourcery: autoUpdatableObject, autoGenerateProperty
|
||||||
|
@NSManaged public var createdAt: Date
|
||||||
|
|
||||||
|
// sourcery: autoUpdatableObject, autoGenerateProperty
|
||||||
|
@NSManaged public var content: String
|
||||||
|
|
||||||
|
// sourcery: autoUpdatableObject, autoGenerateProperty
|
||||||
|
@NSManaged public var sensitive: Bool
|
||||||
|
|
||||||
|
// sourcery: autoUpdatableObject, autoGenerateProperty
|
||||||
|
@NSManaged public var spoilerText: String?
|
||||||
|
|
||||||
|
// MARK: - AutoGenerateProperty
|
||||||
|
// sourcery:inline:StatusEdit.AutoGenerateProperty
|
||||||
|
|
||||||
|
// Generated using Sourcery
|
||||||
|
// DO NOT EDIT
|
||||||
|
public struct Property {
|
||||||
|
public let createdAt: Date
|
||||||
|
public let content: String
|
||||||
|
public let sensitive: Bool
|
||||||
|
public let spoilerText: String?
|
||||||
|
public let emojis: [MastodonEmoji]
|
||||||
|
public let attachments: [MastodonAttachment]
|
||||||
|
public let poll: Poll?
|
||||||
|
|
||||||
|
public init(
|
||||||
|
createdAt: Date,
|
||||||
|
content: String,
|
||||||
|
sensitive: Bool,
|
||||||
|
spoilerText: String?,
|
||||||
|
emojis: [MastodonEmoji],
|
||||||
|
attachments: [MastodonAttachment],
|
||||||
|
poll: Poll?
|
||||||
|
) {
|
||||||
|
self.createdAt = createdAt
|
||||||
|
self.content = content
|
||||||
|
self.sensitive = sensitive
|
||||||
|
self.spoilerText = spoilerText
|
||||||
|
self.emojis = emojis
|
||||||
|
self.attachments = attachments
|
||||||
|
self.poll = poll
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func configure(property: Property) {
|
||||||
|
self.createdAt = property.createdAt
|
||||||
|
self.content = property.content
|
||||||
|
self.sensitive = property.sensitive
|
||||||
|
self.spoilerText = property.spoilerText
|
||||||
|
self.emojis = property.emojis
|
||||||
|
self.attachments = property.attachments
|
||||||
|
self.poll = property.poll
|
||||||
|
}
|
||||||
|
|
||||||
|
public func update(property: Property) {
|
||||||
|
update(createdAt: property.createdAt)
|
||||||
|
update(content: property.content)
|
||||||
|
update(sensitive: property.sensitive)
|
||||||
|
update(spoilerText: property.spoilerText)
|
||||||
|
update(emojis: property.emojis)
|
||||||
|
update(attachments: property.attachments)
|
||||||
|
update(poll: property.poll)
|
||||||
|
}
|
||||||
|
// sourcery:end
|
||||||
|
|
||||||
|
// sourcery: autoUpdatableObject, autoGenerateProperty
|
||||||
|
@objc public var emojis: [MastodonEmoji] {
|
||||||
|
get {
|
||||||
|
let keyPath = #keyPath(StatusEdit.emojis)
|
||||||
|
willAccessValue(forKey: keyPath)
|
||||||
|
let _data = primitiveValue(forKey: keyPath) as? Data
|
||||||
|
didAccessValue(forKey: keyPath)
|
||||||
|
do {
|
||||||
|
guard let data = _data else { return [] }
|
||||||
|
let emojis = try JSONDecoder().decode([MastodonEmoji].self, from: data)
|
||||||
|
return emojis
|
||||||
|
} catch {
|
||||||
|
assertionFailure(error.localizedDescription)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
let keyPath = #keyPath(StatusEdit.emojis)
|
||||||
|
let data = try? JSONEncoder().encode(newValue)
|
||||||
|
willChangeValue(forKey: keyPath)
|
||||||
|
setPrimitiveValue(data, forKey: keyPath)
|
||||||
|
didChangeValue(forKey: keyPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension StatusEdit {
|
||||||
|
// sourcery: autoUpdatableObject, autoGenerateProperty
|
||||||
|
@objc public var attachments: [MastodonAttachment] {
|
||||||
|
get {
|
||||||
|
let keyPath = #keyPath(StatusEdit.attachments)
|
||||||
|
willAccessValue(forKey: keyPath)
|
||||||
|
let _data = primitiveValue(forKey: keyPath) as? Data
|
||||||
|
didAccessValue(forKey: keyPath)
|
||||||
|
do {
|
||||||
|
guard let data = _data else { return [] }
|
||||||
|
let attachments = try JSONDecoder().decode([MastodonAttachment].self, from: data)
|
||||||
|
return attachments
|
||||||
|
} catch {
|
||||||
|
assertionFailure(error.localizedDescription)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
let keyPath = #keyPath(StatusEdit.attachments)
|
||||||
|
let data = try? JSONEncoder().encode(newValue)
|
||||||
|
willChangeValue(forKey: keyPath)
|
||||||
|
setPrimitiveValue(data, forKey: keyPath)
|
||||||
|
didChangeValue(forKey: keyPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension StatusEdit {
|
||||||
|
// sourcery: autoUpdatableObject, autoGenerateProperty
|
||||||
|
@objc public var poll: Poll? {
|
||||||
|
get {
|
||||||
|
let keyPath = #keyPath(StatusEdit.poll)
|
||||||
|
willAccessValue(forKey: keyPath)
|
||||||
|
let _data = primitiveValue(forKey: keyPath) as? Data
|
||||||
|
didAccessValue(forKey: keyPath)
|
||||||
|
do {
|
||||||
|
guard let data = _data else { return nil }
|
||||||
|
let poll = try JSONDecoder().decode(Poll.self, from: data)
|
||||||
|
return poll
|
||||||
|
} catch {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
let keyPath = #keyPath(StatusEdit.poll)
|
||||||
|
let data = try? JSONEncoder().encode(newValue)
|
||||||
|
willChangeValue(forKey: keyPath)
|
||||||
|
setPrimitiveValue(data, forKey: keyPath)
|
||||||
|
didChangeValue(forKey: keyPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension StatusEdit: Managed {
|
||||||
|
@discardableResult
|
||||||
|
public static func insert(
|
||||||
|
into context: NSManagedObjectContext,
|
||||||
|
property: Property
|
||||||
|
) -> StatusEdit {
|
||||||
|
let object: StatusEdit = context.insertObject()
|
||||||
|
|
||||||
|
object.configure(property: property)
|
||||||
|
|
||||||
|
return object
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension StatusEdit: AutoUpdatableObject {
|
||||||
|
// sourcery:inline:StatusEdit.AutoUpdatableObject
|
||||||
|
|
||||||
|
// Generated using Sourcery
|
||||||
|
// DO NOT EDIT
|
||||||
|
public func update(createdAt: Date) {
|
||||||
|
if self.createdAt != createdAt {
|
||||||
|
self.createdAt = createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public func update(content: String) {
|
||||||
|
if self.content != content {
|
||||||
|
self.content = content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public func update(sensitive: Bool) {
|
||||||
|
if self.sensitive != sensitive {
|
||||||
|
self.sensitive = sensitive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public func update(spoilerText: String?) {
|
||||||
|
if self.spoilerText != spoilerText {
|
||||||
|
self.spoilerText = spoilerText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public func update(emojis: [MastodonEmoji]) {
|
||||||
|
if self.emojis != emojis {
|
||||||
|
self.emojis = emojis
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public func update(attachments: [MastodonAttachment]) {
|
||||||
|
if self.attachments != attachments {
|
||||||
|
self.attachments = attachments
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public func update(poll: Poll?) {
|
||||||
|
if self.poll != poll {
|
||||||
|
self.poll = poll
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// sourcery:end
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -11,6 +11,24 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "0.300",
|
||||||
|
"blue" : "0.737",
|
||||||
|
"green" : "0.765",
|
||||||
|
"red" : "0.765"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0xD5",
|
||||||
|
"green" : "0xD1",
|
||||||
|
"red" : "0xD1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0x3C",
|
||||||
|
"green" : "0x3A",
|
||||||
|
"red" : "0x3A"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"provides-namespace" : true
|
||||||
|
}
|
||||||
|
}
|
15
MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Edit History/Edit.imageset/Contents.json
vendored
Normal file
15
MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Edit History/Edit.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "edit.pdf",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"preserves-vector-representation" : true
|
||||||
|
}
|
||||||
|
}
|
93
MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Edit History/Edit.imageset/edit.pdf
vendored
Normal file
93
MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Edit History/Edit.imageset/edit.pdf
vendored
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
%PDF-1.7
|
||||||
|
|
||||||
|
1 0 obj
|
||||||
|
<< >>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
2 0 obj
|
||||||
|
<< /Length 3 0 R >>
|
||||||
|
stream
|
||||||
|
/DeviceRGB CS
|
||||||
|
/DeviceRGB cs
|
||||||
|
q
|
||||||
|
1.000000 0.000000 -0.000000 1.000000 1.999985 1.609241 cm
|
||||||
|
0.411765 0.400000 0.521569 scn
|
||||||
|
18.951868 19.342661 m
|
||||||
|
17.554346 20.740183 15.288496 20.740116 13.891058 19.342512 c
|
||||||
|
1.941028 7.391070 l
|
||||||
|
1.534704 6.984698 1.249101 6.473557 1.115997 5.914522 c
|
||||||
|
0.020410 1.313066 l
|
||||||
|
-0.039914 1.059704 0.035522 0.793184 0.219685 0.609020 c
|
||||||
|
0.403848 0.424858 0.670367 0.349422 0.923730 0.409746 c
|
||||||
|
5.524981 1.505281 l
|
||||||
|
6.084182 1.638426 6.595463 1.924147 7.001908 2.330639 c
|
||||||
|
18.952013 14.282148 l
|
||||||
|
20.349335 15.679634 20.349274 17.945255 18.951868 19.342661 c
|
||||||
|
h
|
||||||
|
14.951780 18.281914 m
|
||||||
|
15.763443 19.093672 17.079496 19.093712 17.891207 18.282001 c
|
||||||
|
18.702848 17.470360 18.702887 16.154438 17.891291 15.342747 c
|
||||||
|
16.999950 14.451301 l
|
||||||
|
14.060611 17.390640 l
|
||||||
|
14.951780 18.281914 l
|
||||||
|
h
|
||||||
|
13.000013 16.329916 m
|
||||||
|
15.939351 13.390577 l
|
||||||
|
5.941185 3.391237 l
|
||||||
|
5.731036 3.181063 5.466681 3.033333 5.177550 2.964491 c
|
||||||
|
1.761908 2.151243 l
|
||||||
|
2.575206 5.567090 l
|
||||||
|
2.644022 5.856114 2.791680 6.120377 3.001751 6.330473 c
|
||||||
|
13.000013 16.329916 l
|
||||||
|
h
|
||||||
|
f
|
||||||
|
n
|
||||||
|
Q
|
||||||
|
|
||||||
|
endstream
|
||||||
|
endobj
|
||||||
|
|
||||||
|
3 0 obj
|
||||||
|
1034
|
||||||
|
endobj
|
||||||
|
|
||||||
|
4 0 obj
|
||||||
|
<< /Annots []
|
||||||
|
/Type /Page
|
||||||
|
/MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
|
||||||
|
/Resources 1 0 R
|
||||||
|
/Contents 2 0 R
|
||||||
|
/Parent 5 0 R
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
5 0 obj
|
||||||
|
<< /Kids [ 4 0 R ]
|
||||||
|
/Count 1
|
||||||
|
/Type /Pages
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
6 0 obj
|
||||||
|
<< /Pages 5 0 R
|
||||||
|
/Type /Catalog
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
xref
|
||||||
|
0 7
|
||||||
|
0000000000 65535 f
|
||||||
|
0000000010 00000 n
|
||||||
|
0000000034 00000 n
|
||||||
|
0000001124 00000 n
|
||||||
|
0000001147 00000 n
|
||||||
|
0000001320 00000 n
|
||||||
|
0000001394 00000 n
|
||||||
|
trailer
|
||||||
|
<< /ID [ (some) (id) ]
|
||||||
|
/Root 6 0 R
|
||||||
|
/Size 7
|
||||||
|
>>
|
||||||
|
startxref
|
||||||
|
1453
|
||||||
|
%%EOF
|
|
@ -0,0 +1,38 @@
|
||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0xF9",
|
||||||
|
"green" : "0xF5",
|
||||||
|
"red" : "0xF5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0x2C",
|
||||||
|
"green" : "0x1B",
|
||||||
|
"red" : "0x1B"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0xDE",
|
||||||
|
"green" : "0xD1",
|
||||||
|
"red" : "0xD1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0x12",
|
||||||
|
"green" : "0x0D",
|
||||||
|
"red" : "0x0D"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -103,6 +103,7 @@ public enum Asset {
|
||||||
public static let disabled = ColorAsset(name: "Colors/disabled")
|
public static let disabled = ColorAsset(name: "Colors/disabled")
|
||||||
public static let inactive = ColorAsset(name: "Colors/inactive")
|
public static let inactive = ColorAsset(name: "Colors/inactive")
|
||||||
public static let mediaTypeIndicotor = ColorAsset(name: "Colors/media.type.indicotor")
|
public static let mediaTypeIndicotor = ColorAsset(name: "Colors/media.type.indicotor")
|
||||||
|
public static let selectionHighlight = ColorAsset(name: "Colors/selection.highlight")
|
||||||
public static let successGreen = ColorAsset(name: "Colors/success.green")
|
public static let successGreen = ColorAsset(name: "Colors/success.green")
|
||||||
public static let systemOrange = ColorAsset(name: "Colors/system.orange")
|
public static let systemOrange = ColorAsset(name: "Colors/system.orange")
|
||||||
}
|
}
|
||||||
|
@ -164,6 +165,11 @@ public enum Asset {
|
||||||
public enum Discovery {
|
public enum Discovery {
|
||||||
public static let profileCardBackground = ColorAsset(name: "Scene/Discovery/profile.card.background")
|
public static let profileCardBackground = ColorAsset(name: "Scene/Discovery/profile.card.background")
|
||||||
}
|
}
|
||||||
|
public enum EditHistory {
|
||||||
|
public static let edit = ImageAsset(name: "Scene/Edit History/Edit")
|
||||||
|
public static let statusBackground = ColorAsset(name: "Scene/Edit History/StatusBackground")
|
||||||
|
public static let statusBackgroundBorder = ColorAsset(name: "Scene/Edit History/StatusBackgroundBorder")
|
||||||
|
}
|
||||||
public enum Notification {
|
public enum Notification {
|
||||||
public static let confirmFollowRequestButtonBackground = ColorAsset(name: "Scene/Notification/confirm.follow.request.button.background")
|
public static let confirmFollowRequestButtonBackground = ColorAsset(name: "Scene/Notification/confirm.follow.request.button.background")
|
||||||
public static let deleteFollowRequestButtonBackground = ColorAsset(name: "Scene/Notification/delete.follow.request.button.background")
|
public static let deleteFollowRequestButtonBackground = ColorAsset(name: "Scene/Notification/delete.follow.request.button.background")
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CoreDataStack
|
||||||
|
import MastodonMeta
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
|
extension [Mastodon.Entity.Attachment]? {
|
||||||
|
public var mastodonAttachments: [MastodonAttachment] {
|
||||||
|
guard let mediaAttachments = self else { return [] }
|
||||||
|
|
||||||
|
let attachments = mediaAttachments.compactMap { media -> MastodonAttachment? in
|
||||||
|
guard let kind = media.attachmentKind
|
||||||
|
else { return nil }
|
||||||
|
|
||||||
|
let width: Int;
|
||||||
|
let height: Int;
|
||||||
|
let durationMS: Int?;
|
||||||
|
|
||||||
|
if let meta = media.meta,
|
||||||
|
let original = meta.original,
|
||||||
|
let originalWidth = original.width,
|
||||||
|
let originalHeight = original.height {
|
||||||
|
width = originalWidth // audio has width/height
|
||||||
|
height = originalHeight
|
||||||
|
durationMS = original.duration.map { Int($0 * 1000) }
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// In case metadata field is missing, use default values.
|
||||||
|
width = 32;
|
||||||
|
height = 32;
|
||||||
|
durationMS = nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
return MastodonAttachment(
|
||||||
|
id: media.id,
|
||||||
|
kind: kind,
|
||||||
|
size: CGSize(width: width, height: height),
|
||||||
|
focus: nil, // TODO:
|
||||||
|
blurhash: media.blurhash,
|
||||||
|
assetURL: media.url,
|
||||||
|
previewURL: media.previewURL,
|
||||||
|
textURL: media.textURL,
|
||||||
|
durationMS: durationMS,
|
||||||
|
altDescription: media.description
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return attachments
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ import CoreDataStack
|
||||||
|
|
||||||
extension PollOption.Property {
|
extension PollOption.Property {
|
||||||
public init(
|
public init(
|
||||||
|
poll: Poll,
|
||||||
index: Int,
|
index: Int,
|
||||||
entity: Mastodon.Entity.Poll.Option,
|
entity: Mastodon.Entity.Poll.Option,
|
||||||
networkDate: Date
|
networkDate: Date
|
||||||
|
@ -20,7 +21,8 @@ extension PollOption.Property {
|
||||||
title: entity.title,
|
title: entity.title,
|
||||||
votesCount: Int64(entity.votesCount ?? 0),
|
votesCount: Int64(entity.votesCount ?? 0),
|
||||||
createdAt: networkDate,
|
createdAt: networkDate,
|
||||||
updatedAt: networkDate
|
updatedAt: networkDate,
|
||||||
|
poll: poll
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ extension Status.Property {
|
||||||
id: entity.id,
|
id: entity.id,
|
||||||
uri: entity.uri,
|
uri: entity.uri,
|
||||||
createdAt: entity.createdAt,
|
createdAt: entity.createdAt,
|
||||||
|
editedAt: entity.editedAt,
|
||||||
content: entity.content ?? "",
|
content: entity.content ?? "",
|
||||||
visibility: entity.mastodonVisibility,
|
visibility: entity.mastodonVisibility,
|
||||||
sensitive: entity.sensitive ?? false,
|
sensitive: entity.sensitive ?? false,
|
||||||
|
@ -48,46 +49,7 @@ extension Mastodon.Entity.Status {
|
||||||
|
|
||||||
extension Mastodon.Entity.Status {
|
extension Mastodon.Entity.Status {
|
||||||
public var mastodonAttachments: [MastodonAttachment] {
|
public var mastodonAttachments: [MastodonAttachment] {
|
||||||
guard let mediaAttachments = mediaAttachments else { return [] }
|
mediaAttachments.mastodonAttachments
|
||||||
|
|
||||||
let attachments = mediaAttachments.compactMap { media -> MastodonAttachment? in
|
|
||||||
guard let kind = media.attachmentKind
|
|
||||||
else { return nil }
|
|
||||||
|
|
||||||
let width: Int;
|
|
||||||
let height: Int;
|
|
||||||
let durationMS: Int?;
|
|
||||||
|
|
||||||
if let meta = media.meta,
|
|
||||||
let original = meta.original,
|
|
||||||
let originalWidth = original.width,
|
|
||||||
let originalHeight = original.height {
|
|
||||||
width = originalWidth // audio has width/height
|
|
||||||
height = originalHeight
|
|
||||||
durationMS = original.duration.map { Int($0 * 1000) }
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// In case metadata field is missing, use default values.
|
|
||||||
width = 32;
|
|
||||||
height = 32;
|
|
||||||
durationMS = nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
return MastodonAttachment(
|
|
||||||
id: media.id,
|
|
||||||
kind: kind,
|
|
||||||
size: CGSize(width: width, height: height),
|
|
||||||
focus: nil, // TODO:
|
|
||||||
blurhash: media.blurhash,
|
|
||||||
assetURL: media.url,
|
|
||||||
previewURL: media.previewURL,
|
|
||||||
textURL: media.textURL,
|
|
||||||
durationMS: durationMS,
|
|
||||||
altDescription: media.description
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return attachments
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CoreDataStack
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
|
extension StatusEdit.Property {
|
||||||
|
init(entity: Mastodon.Entity.StatusEdit) {
|
||||||
|
self.init(
|
||||||
|
createdAt: entity.createdAt,
|
||||||
|
content: entity.content,
|
||||||
|
sensitive: entity.sensitive,
|
||||||
|
spoilerText: entity.spoilerText,
|
||||||
|
emojis: entity.mastodonEmojis,
|
||||||
|
attachments: entity.mastodonAttachments,
|
||||||
|
poll: entity.poll.map { StatusEdit.Poll(options: $0.options.map { StatusEdit.Poll.Option(title: $0.title) } ) } )
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Mastodon.Entity.StatusEdit {
|
||||||
|
public var mastodonAttachments: [MastodonAttachment] {
|
||||||
|
mediaAttachments.mastodonAttachments
|
||||||
|
}
|
||||||
|
}
|
|
@ -86,6 +86,26 @@ extension PollComposeItem {
|
||||||
case .sevenDays: return 60 * 60 * 24 * 7
|
case .sevenDays: return 60 * 60 * 24 * 7
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public init(closestDateToExpiry date: Date) {
|
||||||
|
let expiresInSeconds = Int(date.timeIntervalSince(.now))
|
||||||
|
switch expiresInSeconds {
|
||||||
|
case _ where expiresInSeconds <= Self.thirtyMinutes.seconds:
|
||||||
|
self = .thirtyMinutes
|
||||||
|
case _ where expiresInSeconds > Self.thirtyMinutes.seconds && expiresInSeconds <= Self.oneHour.seconds:
|
||||||
|
self = .oneHour
|
||||||
|
case _ where expiresInSeconds > Self.oneHour.seconds && expiresInSeconds <= Self.sixHours.seconds:
|
||||||
|
self = .sixHours
|
||||||
|
case _ where expiresInSeconds > Self.sixHours.seconds && expiresInSeconds <= Self.oneDay.seconds:
|
||||||
|
self = .oneDay
|
||||||
|
case _ where expiresInSeconds > Self.oneDay.seconds && expiresInSeconds <= Self.threeDays.seconds:
|
||||||
|
self = .threeDays
|
||||||
|
case _ where expiresInSeconds > Self.threeDays.seconds && expiresInSeconds <= Self.sevenDays.seconds:
|
||||||
|
self = .sevenDays
|
||||||
|
default:
|
||||||
|
self = .oneDay
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,4 +11,5 @@ import CoreDataStack
|
||||||
|
|
||||||
public enum PollItem: Hashable {
|
public enum PollItem: Hashable {
|
||||||
case option(record: ManagedObjectRecord<PollOption>)
|
case option(record: ManagedObjectRecord<PollOption>)
|
||||||
|
case history(option: StatusEdit.Poll.Option)
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,30 +59,16 @@ extension Persistence.Poll {
|
||||||
) -> PersistResult {
|
) -> PersistResult {
|
||||||
|
|
||||||
if let old = fetch(in: managedObjectContext, context: context) {
|
if let old = fetch(in: managedObjectContext, context: context) {
|
||||||
merge(poll: old, context: context)
|
merge(in: managedObjectContext, poll: old, context: context)
|
||||||
return PersistResult(
|
return PersistResult(
|
||||||
poll: old,
|
poll: old,
|
||||||
isNewInsertion: false
|
isNewInsertion: false
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
let options: [PollOption] = context.entity.options.enumerated().map { i, entity in
|
|
||||||
let optionResult = Persistence.PollOption.persist(
|
|
||||||
in: managedObjectContext,
|
|
||||||
context: Persistence.PollOption.PersistContext(
|
|
||||||
index: i,
|
|
||||||
entity: entity,
|
|
||||||
me: context.me,
|
|
||||||
networkDate: context.networkDate
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return optionResult.option
|
|
||||||
}
|
|
||||||
|
|
||||||
let poll = create(
|
let poll = create(
|
||||||
in: managedObjectContext,
|
in: managedObjectContext,
|
||||||
context: context
|
context: context
|
||||||
)
|
)
|
||||||
poll.attach(options: options)
|
|
||||||
|
|
||||||
return PersistResult(
|
return PersistResult(
|
||||||
poll: poll,
|
poll: poll,
|
||||||
|
@ -124,11 +110,12 @@ extension Persistence.Poll {
|
||||||
into: managedObjectContext,
|
into: managedObjectContext,
|
||||||
property: property
|
property: property
|
||||||
)
|
)
|
||||||
update(poll: poll, context: context)
|
update(in: managedObjectContext, poll: poll, context: context)
|
||||||
return poll
|
return poll
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func merge(
|
public static func merge(
|
||||||
|
in managedObjectContext: NSManagedObjectContext,
|
||||||
poll: Poll,
|
poll: Poll,
|
||||||
context: PersistContext
|
context: PersistContext
|
||||||
) {
|
) {
|
||||||
|
@ -139,10 +126,11 @@ extension Persistence.Poll {
|
||||||
networkDate: context.networkDate
|
networkDate: context.networkDate
|
||||||
)
|
)
|
||||||
poll.update(property: property)
|
poll.update(property: property)
|
||||||
update(poll: poll, context: context)
|
update(in: managedObjectContext, poll: poll, context: context)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func update(
|
public static func update(
|
||||||
|
in managedObjectContext: NSManagedObjectContext,
|
||||||
poll: Poll,
|
poll: Poll,
|
||||||
context: PersistContext
|
context: PersistContext
|
||||||
) {
|
) {
|
||||||
|
@ -153,6 +141,7 @@ extension Persistence.Poll {
|
||||||
option: option,
|
option: option,
|
||||||
context: Persistence.PollOption.PersistContext(
|
context: Persistence.PollOption.PersistContext(
|
||||||
index: Int(option.index),
|
index: Int(option.index),
|
||||||
|
poll: poll,
|
||||||
entity: entity,
|
entity: entity,
|
||||||
me: context.me,
|
me: context.me,
|
||||||
networkDate: context.networkDate
|
networkDate: context.networkDate
|
||||||
|
@ -173,7 +162,53 @@ extension Persistence.Poll {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// update options
|
||||||
|
if needsPollOptionsUpdate(context: context, poll: poll) {
|
||||||
|
// options differ, update them
|
||||||
|
for option in poll.options {
|
||||||
|
option.update(poll: nil)
|
||||||
|
managedObjectContext.delete(option)
|
||||||
|
}
|
||||||
|
var attachableOptions = [PollOption]()
|
||||||
|
for (index, option) in context.entity.options.enumerated() {
|
||||||
|
attachableOptions.append(
|
||||||
|
Persistence.PollOption.create(
|
||||||
|
in: managedObjectContext,
|
||||||
|
context: Persistence.PollOption.PersistContext(
|
||||||
|
index: index,
|
||||||
|
poll: poll,
|
||||||
|
entity: option,
|
||||||
|
me: context.me,
|
||||||
|
networkDate: context.networkDate
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
poll.attach(options: attachableOptions)
|
||||||
|
}
|
||||||
|
|
||||||
poll.update(updatedAt: context.networkDate)
|
poll.update(updatedAt: context.networkDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static func needsPollOptionsUpdate(context: PersistContext, poll: Poll) -> Bool {
|
||||||
|
let entityPollOptions = context.entity.options.map { (title: $0.title, votes: $0.votesCount) }
|
||||||
|
let pollOptions = poll.options.sortedByIndex().map { (title: $0.title, votes: Int($0.votesCount)) }
|
||||||
|
|
||||||
|
guard entityPollOptions.count == pollOptions.count else {
|
||||||
|
// poll definitely needs to be updated due to differences in count of options
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for (entityPollOption, pollOption) in zip(entityPollOptions, pollOptions) {
|
||||||
|
guard entityPollOption.title == pollOption.title else {
|
||||||
|
// update poll because at least one title differs
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
guard entityPollOption.votes == pollOption.votes else {
|
||||||
|
// update poll because at least one vote count differs
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ extension Persistence.PollOption {
|
||||||
|
|
||||||
public struct PersistContext {
|
public struct PersistContext {
|
||||||
public let index: Int
|
public let index: Int
|
||||||
|
public let poll: Poll
|
||||||
public let entity: Mastodon.Entity.Poll.Option
|
public let entity: Mastodon.Entity.Poll.Option
|
||||||
public let me: MastodonUser?
|
public let me: MastodonUser?
|
||||||
public let networkDate: Date
|
public let networkDate: Date
|
||||||
|
@ -22,11 +23,13 @@ extension Persistence.PollOption {
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
index: Int,
|
index: Int,
|
||||||
|
poll: Poll,
|
||||||
entity: Mastodon.Entity.Poll.Option,
|
entity: Mastodon.Entity.Poll.Option,
|
||||||
me: MastodonUser?,
|
me: MastodonUser?,
|
||||||
networkDate: Date
|
networkDate: Date
|
||||||
) {
|
) {
|
||||||
self.index = index
|
self.index = index
|
||||||
|
self.poll = poll
|
||||||
self.entity = entity
|
self.entity = entity
|
||||||
self.me = me
|
self.me = me
|
||||||
self.networkDate = networkDate
|
self.networkDate = networkDate
|
||||||
|
@ -66,6 +69,7 @@ extension Persistence.PollOption {
|
||||||
context: PersistContext
|
context: PersistContext
|
||||||
) -> PollOption {
|
) -> PollOption {
|
||||||
let property = PollOption.Property(
|
let property = PollOption.Property(
|
||||||
|
poll: context.poll,
|
||||||
index: context.index,
|
index: context.index,
|
||||||
entity: context.entity,
|
entity: context.entity,
|
||||||
networkDate: context.networkDate
|
networkDate: context.networkDate
|
||||||
|
@ -81,6 +85,7 @@ extension Persistence.PollOption {
|
||||||
) {
|
) {
|
||||||
guard context.networkDate > option.updatedAt else { return }
|
guard context.networkDate > option.updatedAt else { return }
|
||||||
let property = PollOption.Property(
|
let property = PollOption.Property(
|
||||||
|
poll: context.poll,
|
||||||
index: context.index,
|
index: context.index,
|
||||||
entity: context.entity,
|
entity: context.entity,
|
||||||
networkDate: context.networkDate
|
networkDate: context.networkDate
|
||||||
|
|
|
@ -120,8 +120,10 @@ extension Persistence.Status {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
let author = authorResult.user
|
let author = authorResult.user
|
||||||
|
let application: Application? = createApplication(in: managedObjectContext, context: .init(entity: context.entity))
|
||||||
|
|
||||||
let relationship = Status.Relationship(
|
let relationship = Status.Relationship(
|
||||||
|
application: application,
|
||||||
author: author,
|
author: author,
|
||||||
reblog: reblog,
|
reblog: reblog,
|
||||||
poll: poll,
|
poll: poll,
|
||||||
|
@ -197,7 +199,9 @@ extension Persistence.Status {
|
||||||
)
|
)
|
||||||
status.update(property: property)
|
status.update(property: property)
|
||||||
if let poll = status.poll, let entity = context.entity.poll {
|
if let poll = status.poll, let entity = context.entity.poll {
|
||||||
Persistence.Poll.merge(
|
// update poll
|
||||||
|
Persistence.Poll.update(
|
||||||
|
in: managedObjectContext,
|
||||||
poll: poll,
|
poll: poll,
|
||||||
context: Persistence.Poll.PersistContext(
|
context: Persistence.Poll.PersistContext(
|
||||||
domain: context.domain,
|
domain: context.domain,
|
||||||
|
@ -206,6 +210,40 @@ extension Persistence.Status {
|
||||||
networkDate: context.networkDate
|
networkDate: context.networkDate
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
} else if let entity = context.entity.poll {
|
||||||
|
// add poll
|
||||||
|
let result = Persistence.Poll.createOrMerge(
|
||||||
|
in: managedObjectContext,
|
||||||
|
context: Persistence.Poll.PersistContext(
|
||||||
|
domain: context.domain,
|
||||||
|
entity: entity,
|
||||||
|
me: context.me,
|
||||||
|
networkDate: context.networkDate
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
status.configure(
|
||||||
|
relationship:
|
||||||
|
Status.Relationship(
|
||||||
|
application: status.application,
|
||||||
|
author: status.author,
|
||||||
|
reblog: status.reblog,
|
||||||
|
poll: result.poll,
|
||||||
|
card: status.card
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else if status.poll != nil, context.entity.poll == nil {
|
||||||
|
// remove poll
|
||||||
|
status.configure(
|
||||||
|
relationship:
|
||||||
|
Status.Relationship(
|
||||||
|
application: status.application,
|
||||||
|
author: status.author,
|
||||||
|
reblog: status.reblog,
|
||||||
|
poll: nil,
|
||||||
|
card: status.card
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if status.card == nil, context.entity.card != nil {
|
if status.card == nil, context.entity.card != nil {
|
||||||
|
@ -243,5 +281,21 @@ extension Persistence.Status {
|
||||||
context.entity.favourited.flatMap { status.update(liked: $0, by: user) }
|
context.entity.favourited.flatMap { status.update(liked: $0, by: user) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static func createApplication(
|
||||||
|
in managedObjectContext: NSManagedObjectContext,
|
||||||
|
context: MastodonApplication.PersistContext
|
||||||
|
) -> Application? {
|
||||||
|
guard let application = context.entity.application else { return nil }
|
||||||
|
|
||||||
|
let persistedApplication = Application.insert(into: managedObjectContext, property: .init(name: application.name, website: application.website, vapidKey: application.vapidKey))
|
||||||
|
|
||||||
|
return persistedApplication
|
||||||
|
}
|
||||||
|
|
||||||
|
enum MastodonApplication {
|
||||||
|
public struct PersistContext {
|
||||||
|
let entity: Mastodon.Entity.Status
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||||
|
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
|
extension Persistence.StatusEdit {
|
||||||
|
|
||||||
|
public static func createOrMerge(
|
||||||
|
in managedObjectContext: NSManagedObjectContext,
|
||||||
|
statusEdits: [Mastodon.Entity.StatusEdit],
|
||||||
|
forStatus status: Status
|
||||||
|
) {
|
||||||
|
guard statusEdits.isEmpty == false else { return }
|
||||||
|
|
||||||
|
// remove all edits for status
|
||||||
|
|
||||||
|
if let editHistory = status.editHistory {
|
||||||
|
for statusEdit in Array(editHistory) {
|
||||||
|
managedObjectContext.delete(statusEdit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
status.update(editHistory: Set())
|
||||||
|
let persistedEdits = create(in: managedObjectContext, statusEdits: statusEdits, forStatus: status)
|
||||||
|
status.update(editHistory: Set(persistedEdits))
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func create(
|
||||||
|
in managedObjectContext: NSManagedObjectContext,
|
||||||
|
statusEdits: [Mastodon.Entity.StatusEdit],
|
||||||
|
forStatus status: Status
|
||||||
|
) -> [StatusEdit] {
|
||||||
|
|
||||||
|
var entries: [StatusEdit] = []
|
||||||
|
|
||||||
|
for statusEdit in statusEdits {
|
||||||
|
let property = StatusEdit.Property(createdAt: statusEdit.createdAt, content: statusEdit.content, sensitive: statusEdit.sensitive, spoilerText: statusEdit.spoilerText, emojis: statusEdit.mastodonEmojis, attachments: statusEdit.mastodonAttachments, poll: statusEdit.poll.map { StatusEdit.Poll(options: $0.options.map { StatusEdit.Poll.Option(title: $0.title) } ) })
|
||||||
|
let statusEditEntry = StatusEdit.insert(into: managedObjectContext, property: property)
|
||||||
|
|
||||||
|
entries.append(statusEditEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
status.update(editHistory: Set(entries))
|
||||||
|
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ extension Persistence {
|
||||||
public enum Tag { }
|
public enum Tag { }
|
||||||
public enum SearchHistory { }
|
public enum SearchHistory { }
|
||||||
public enum Notification { }
|
public enum Notification { }
|
||||||
|
public enum StatusEdit {}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Persistence {
|
extension Persistence {
|
||||||
|
|
|
@ -24,3 +24,4 @@ extension MastodonEmojiContainer {
|
||||||
|
|
||||||
extension Mastodon.Entity.Account: MastodonEmojiContainer { }
|
extension Mastodon.Entity.Account: MastodonEmojiContainer { }
|
||||||
extension Mastodon.Entity.Status: MastodonEmojiContainer { }
|
extension Mastodon.Entity.Status: MastodonEmojiContainer { }
|
||||||
|
extension Mastodon.Entity.StatusEdit: MastodonEmojiContainer { }
|
||||||
|
|
|
@ -0,0 +1,102 @@
|
||||||
|
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import MastodonSDK
|
||||||
|
import CoreDataStack
|
||||||
|
|
||||||
|
extension APIService {
|
||||||
|
|
||||||
|
public func getStatusSource(
|
||||||
|
forStatusID statusID: Status.ID,
|
||||||
|
authenticationBox: MastodonAuthenticationBox) async throws -> Mastodon.Response.Content<Mastodon.Entity.StatusSource> {
|
||||||
|
let domain = authenticationBox.domain
|
||||||
|
let authorization = authenticationBox.userAuthorization
|
||||||
|
|
||||||
|
let response = try await Mastodon.API.Statuses.statusSource(
|
||||||
|
forStatusID: statusID,
|
||||||
|
session: session,
|
||||||
|
domain: domain,
|
||||||
|
authorization: authorization).singleOutput()
|
||||||
|
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
public func getHistory(
|
||||||
|
forStatusID statusID: Status.ID,
|
||||||
|
authenticationBox: MastodonAuthenticationBox) async throws -> Mastodon.Response.Content<[Mastodon.Entity.StatusEdit]> {
|
||||||
|
let domain = authenticationBox.domain
|
||||||
|
let authorization = authenticationBox.userAuthorization
|
||||||
|
|
||||||
|
let response = try await Mastodon.API.Statuses.editHistory(
|
||||||
|
forStatusID: statusID,
|
||||||
|
session: session,
|
||||||
|
domain: domain,
|
||||||
|
authorization: authorization).singleOutput()
|
||||||
|
|
||||||
|
guard response.value.isEmpty == false else { return response }
|
||||||
|
|
||||||
|
let managedObjectContext = self.backgroundManagedObjectContext
|
||||||
|
|
||||||
|
try await managedObjectContext.performChanges {
|
||||||
|
// get status
|
||||||
|
guard let status = Status.fetch(in: managedObjectContext, configurationBlock: {
|
||||||
|
$0.predicate = Status.predicate(domain: domain, id: statusID)
|
||||||
|
}).first else { return }
|
||||||
|
|
||||||
|
Persistence.StatusEdit.createOrMerge(in: managedObjectContext,
|
||||||
|
statusEdits: response.value,
|
||||||
|
forStatus: status)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
public func publishStatusEdit(
|
||||||
|
forStatusID statusID: Status.ID,
|
||||||
|
editStatusQuery: Mastodon.API.Statuses.EditStatusQuery,
|
||||||
|
authenticationBox: MastodonAuthenticationBox) async throws -> Mastodon.Response.Content<Mastodon.Entity.Status> {
|
||||||
|
let domain = authenticationBox.domain
|
||||||
|
let authorization = authenticationBox.userAuthorization
|
||||||
|
|
||||||
|
let response = try await Mastodon.API.Statuses.editStatus(
|
||||||
|
forStatusID: statusID,
|
||||||
|
editStatusQuery: editStatusQuery,
|
||||||
|
session: session,
|
||||||
|
domain: domain,
|
||||||
|
authorization: authorization).singleOutput()
|
||||||
|
|
||||||
|
let responseHistory = try await Mastodon.API.Statuses.editHistory(
|
||||||
|
forStatusID: statusID,
|
||||||
|
session: session,
|
||||||
|
domain: domain,
|
||||||
|
authorization: authorization
|
||||||
|
).singleOutput()
|
||||||
|
|
||||||
|
#if !APP_EXTENSION
|
||||||
|
let managedObjectContext = self.backgroundManagedObjectContext
|
||||||
|
|
||||||
|
try await managedObjectContext.performChanges {
|
||||||
|
let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user
|
||||||
|
let status = Persistence.Status.createOrMerge(
|
||||||
|
in: managedObjectContext,
|
||||||
|
context: Persistence.Status.PersistContext(
|
||||||
|
domain: domain,
|
||||||
|
entity: response.value,
|
||||||
|
me: me,
|
||||||
|
statusCache: nil,
|
||||||
|
userCache: nil,
|
||||||
|
networkDate: response.networkDate
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Persistence.StatusEdit.createOrMerge(
|
||||||
|
in: managedObjectContext,
|
||||||
|
statusEdits: responseHistory.value,
|
||||||
|
forStatus: status.status
|
||||||
|
)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,5 +9,6 @@ import Foundation
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
|
|
||||||
public enum StatusPublishResult {
|
public enum StatusPublishResult {
|
||||||
case mastodon(Mastodon.Response.Content<Mastodon.Entity.Status>)
|
case post(Mastodon.Response.Content<Mastodon.Entity.Status>)
|
||||||
|
case edit(Mastodon.Response.Content<Mastodon.Entity.Status>)
|
||||||
}
|
}
|
||||||
|
|
|
@ -132,6 +132,8 @@ public enum L10n {
|
||||||
public static let done = L10n.tr("Localizable", "Common.Controls.Actions.Done", fallback: "Done")
|
public static let done = L10n.tr("Localizable", "Common.Controls.Actions.Done", fallback: "Done")
|
||||||
/// Edit
|
/// Edit
|
||||||
public static let edit = L10n.tr("Localizable", "Common.Controls.Actions.Edit", fallback: "Edit")
|
public static let edit = L10n.tr("Localizable", "Common.Controls.Actions.Edit", fallback: "Edit")
|
||||||
|
/// Edit
|
||||||
|
public static let editPost = L10n.tr("Localizable", "Common.Controls.Actions.EditPost", fallback: "Edit")
|
||||||
/// Find people to follow
|
/// Find people to follow
|
||||||
public static let findPeople = L10n.tr("Localizable", "Common.Controls.Actions.FindPeople", fallback: "Find people to follow")
|
public static let findPeople = L10n.tr("Localizable", "Common.Controls.Actions.FindPeople", fallback: "Find people to follow")
|
||||||
/// Manually search instead
|
/// Manually search instead
|
||||||
|
@ -290,6 +292,10 @@ public enum L10n {
|
||||||
public enum Status {
|
public enum Status {
|
||||||
/// Content Warning
|
/// Content Warning
|
||||||
public static let contentWarning = L10n.tr("Localizable", "Common.Controls.Status.ContentWarning", fallback: "Content Warning")
|
public static let contentWarning = L10n.tr("Localizable", "Common.Controls.Status.ContentWarning", fallback: "Content Warning")
|
||||||
|
/// Edited %@
|
||||||
|
public static func editedAtTimestampPrefix(_ p1: Any) -> String {
|
||||||
|
return L10n.tr("Localizable", "Common.Controls.Status.EditedAtTimestampPrefix", String(describing: p1), fallback: "Edited %@")
|
||||||
|
}
|
||||||
/// %@ via %@
|
/// %@ via %@
|
||||||
public static func linkViaUser(_ p1: Any, _ p2: Any) -> String {
|
public static func linkViaUser(_ p1: Any, _ p2: Any) -> String {
|
||||||
return L10n.tr("Localizable", "Common.Controls.Status.LinkViaUser", String(describing: p1), String(describing: p2), fallback: "%@ via %@")
|
return L10n.tr("Localizable", "Common.Controls.Status.LinkViaUser", String(describing: p1), String(describing: p2), fallback: "%@ via %@")
|
||||||
|
@ -298,6 +304,10 @@ public enum L10n {
|
||||||
public static let loadEmbed = L10n.tr("Localizable", "Common.Controls.Status.LoadEmbed", fallback: "Load Embed")
|
public static let loadEmbed = L10n.tr("Localizable", "Common.Controls.Status.LoadEmbed", fallback: "Load Embed")
|
||||||
/// Tap anywhere to reveal
|
/// Tap anywhere to reveal
|
||||||
public static let mediaContentWarning = L10n.tr("Localizable", "Common.Controls.Status.MediaContentWarning", fallback: "Tap anywhere to reveal")
|
public static let mediaContentWarning = L10n.tr("Localizable", "Common.Controls.Status.MediaContentWarning", fallback: "Tap anywhere to reveal")
|
||||||
|
/// %@ via %@
|
||||||
|
public static func postedViaApplication(_ p1: Any, _ p2: Any) -> String {
|
||||||
|
return L10n.tr("Localizable", "Common.Controls.Status.PostedViaApplication", String(describing: p1), String(describing: p2), fallback: "%@ via %@")
|
||||||
|
}
|
||||||
/// Sensitive Content
|
/// Sensitive Content
|
||||||
public static let sensitiveContent = L10n.tr("Localizable", "Common.Controls.Status.SensitiveContent", fallback: "Sensitive Content")
|
public static let sensitiveContent = L10n.tr("Localizable", "Common.Controls.Status.SensitiveContent", fallback: "Sensitive Content")
|
||||||
/// Show Post
|
/// Show Post
|
||||||
|
@ -340,6 +350,26 @@ public enum L10n {
|
||||||
/// Undo reblog
|
/// Undo reblog
|
||||||
public static let unreblog = L10n.tr("Localizable", "Common.Controls.Status.Actions.Unreblog", fallback: "Undo reblog")
|
public static let unreblog = L10n.tr("Localizable", "Common.Controls.Status.Actions.Unreblog", fallback: "Undo reblog")
|
||||||
}
|
}
|
||||||
|
public enum Buttons {
|
||||||
|
/// Last edit %@
|
||||||
|
public static func editHistoryDetail(_ p1: Any) -> String {
|
||||||
|
return L10n.tr("Localizable", "Common.Controls.Status.Buttons.EditHistoryDetail", String(describing: p1), fallback: "Last edit %@")
|
||||||
|
}
|
||||||
|
/// Edit History
|
||||||
|
public static let editHistoryTitle = L10n.tr("Localizable", "Common.Controls.Status.Buttons.EditHistoryTitle", fallback: "Edit History")
|
||||||
|
/// Favorites
|
||||||
|
public static let favoritesTitle = L10n.tr("Localizable", "Common.Controls.Status.Buttons.FavoritesTitle", fallback: "Favorites")
|
||||||
|
/// Reblogs
|
||||||
|
public static let reblogsTitle = L10n.tr("Localizable", "Common.Controls.Status.Buttons.ReblogsTitle", fallback: "Reblogs")
|
||||||
|
}
|
||||||
|
public enum EditHistory {
|
||||||
|
/// Original Post · %@
|
||||||
|
public static func originalPost(_ p1: Any) -> String {
|
||||||
|
return L10n.tr("Localizable", "Common.Controls.Status.EditHistory.OriginalPost", String(describing: p1), fallback: "Original Post · %@")
|
||||||
|
}
|
||||||
|
/// Edit History
|
||||||
|
public static let title = L10n.tr("Localizable", "Common.Controls.Status.EditHistory.Title", fallback: "Edit History")
|
||||||
|
}
|
||||||
public enum Media {
|
public enum Media {
|
||||||
/// %@, attachment %d of %d
|
/// %@, attachment %d of %d
|
||||||
public static func accessibilityLabel(_ p1: Any, _ p2: Int, _ p3: Int) -> String {
|
public static func accessibilityLabel(_ p1: Any, _ p2: Int, _ p3: Int) -> String {
|
||||||
|
@ -629,6 +659,8 @@ public enum L10n {
|
||||||
public static let title = L10n.tr("Localizable", "Scene.Compose.Poll.Title", fallback: "Poll")
|
public static let title = L10n.tr("Localizable", "Scene.Compose.Poll.Title", fallback: "Poll")
|
||||||
}
|
}
|
||||||
public enum Title {
|
public enum Title {
|
||||||
|
/// Edit Post
|
||||||
|
public static let editPost = L10n.tr("Localizable", "Scene.Compose.Title.EditPost", fallback: "Edit Post")
|
||||||
/// New Post
|
/// New Post
|
||||||
public static let newPost = L10n.tr("Localizable", "Scene.Compose.Title.NewPost", fallback: "New Post")
|
public static let newPost = L10n.tr("Localizable", "Scene.Compose.Title.NewPost", fallback: "New Post")
|
||||||
/// New Reply
|
/// New Reply
|
||||||
|
|
|
@ -65,6 +65,7 @@ Please check your internet connection.";
|
||||||
"Common.Controls.Actions.TakePhoto" = "Take Photo";
|
"Common.Controls.Actions.TakePhoto" = "Take Photo";
|
||||||
"Common.Controls.Actions.TranslatePost.Title" = "Translate from %@";
|
"Common.Controls.Actions.TranslatePost.Title" = "Translate from %@";
|
||||||
"Common.Controls.Actions.TranslatePost.UnknownLanguage" = "Unknown";
|
"Common.Controls.Actions.TranslatePost.UnknownLanguage" = "Unknown";
|
||||||
|
"Common.Controls.Actions.EditPost" = "Edit";
|
||||||
"Common.Controls.Actions.TryAgain" = "Try Again";
|
"Common.Controls.Actions.TryAgain" = "Try Again";
|
||||||
"Common.Controls.Actions.UnblockDomain" = "Unblock %@";
|
"Common.Controls.Actions.UnblockDomain" = "Unblock %@";
|
||||||
"Common.Controls.Friendship.Block" = "Block";
|
"Common.Controls.Friendship.Block" = "Block";
|
||||||
|
@ -147,6 +148,14 @@ Please check your internet connection.";
|
||||||
"Common.Controls.Status.Visibility.Private" = "Only their followers can see this post.";
|
"Common.Controls.Status.Visibility.Private" = "Only their followers can see this post.";
|
||||||
"Common.Controls.Status.Visibility.PrivateFromMe" = "Only my followers can see this post.";
|
"Common.Controls.Status.Visibility.PrivateFromMe" = "Only my followers can see this post.";
|
||||||
"Common.Controls.Status.Visibility.Unlisted" = "Everyone can see this post but not display in the public timeline.";
|
"Common.Controls.Status.Visibility.Unlisted" = "Everyone can see this post but not display in the public timeline.";
|
||||||
|
"Common.Controls.Status.PostedViaApplication" = "%@ via %@";
|
||||||
|
"Common.Controls.Status.Buttons.ReblogsTitle" = "Reblogs";
|
||||||
|
"Common.Controls.Status.Buttons.FavoritesTitle" = "Favorites";
|
||||||
|
"Common.Controls.Status.Buttons.EditHistoryTitle" = "Edit History";
|
||||||
|
"Common.Controls.Status.Buttons.EditHistoryDetail" = "Last edit %@";
|
||||||
|
"Common.Controls.Status.EditedAtTimestampPrefix" = "Edited %@";
|
||||||
|
"Common.Controls.Status.EditHistory.Title" = "Edit History";
|
||||||
|
"Common.Controls.Status.EditHistory.OriginalPost" = "Original Post · %@";
|
||||||
"Common.Controls.Tabs.Home" = "Home";
|
"Common.Controls.Tabs.Home" = "Home";
|
||||||
"Common.Controls.Tabs.Notifications" = "Notifications";
|
"Common.Controls.Tabs.Notifications" = "Notifications";
|
||||||
"Common.Controls.Tabs.Profile" = "Profile";
|
"Common.Controls.Tabs.Profile" = "Profile";
|
||||||
|
@ -229,6 +238,7 @@ uploaded to Mastodon.";
|
||||||
"Scene.Compose.ReplyingToUser" = "replying to %@";
|
"Scene.Compose.ReplyingToUser" = "replying to %@";
|
||||||
"Scene.Compose.Title.NewPost" = "New Post";
|
"Scene.Compose.Title.NewPost" = "New Post";
|
||||||
"Scene.Compose.Title.NewReply" = "New Reply";
|
"Scene.Compose.Title.NewReply" = "New Reply";
|
||||||
|
"Scene.Compose.Title.EditPost" = "Edit Post";
|
||||||
"Scene.Compose.Visibility.Direct" = "Only people I mention";
|
"Scene.Compose.Visibility.Direct" = "Only people I mention";
|
||||||
"Scene.Compose.Visibility.Private" = "Followers only";
|
"Scene.Compose.Visibility.Private" = "Followers only";
|
||||||
"Scene.Compose.Visibility.Public" = "Public";
|
"Scene.Compose.Visibility.Public" = "Public";
|
||||||
|
|
|
@ -70,7 +70,7 @@ extension Mastodon.API.Media {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct UploadMediaQuery: PostQuery, PutQuery {
|
public struct UploadMediaQuery: PostQuery {
|
||||||
public let file: Mastodon.Query.MediaAttachment?
|
public let file: Mastodon.Query.MediaAttachment?
|
||||||
public let thumbnail: Mastodon.Query.MediaAttachment?
|
public let thumbnail: Mastodon.Query.MediaAttachment?
|
||||||
public let description: String?
|
public let description: String?
|
||||||
|
|
|
@ -36,7 +36,7 @@ extension Mastodon.API.Statuses {
|
||||||
public static func favoriteBy(
|
public static func favoriteBy(
|
||||||
session: URLSession,
|
session: URLSession,
|
||||||
domain: String,
|
domain: String,
|
||||||
statusID: Mastodon.Entity.Poll.ID,
|
statusID: Mastodon.Entity.Status.ID,
|
||||||
query: FavoriteByQuery,
|
query: FavoriteByQuery,
|
||||||
authorization: Mastodon.API.OAuth.Authorization?
|
authorization: Mastodon.API.OAuth.Authorization?
|
||||||
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Account]>, Error> {
|
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Account]>, Error> {
|
||||||
|
|
|
@ -0,0 +1,166 @@
|
||||||
|
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
extension Mastodon.API.Statuses {
|
||||||
|
private static func historyEndpointURL(domain: String, statusID: Mastodon.Entity.Status.ID) -> URL {
|
||||||
|
return Mastodon.API.endpointURL(domain: domain)
|
||||||
|
.appendingPathComponent("statuses")
|
||||||
|
.appendingPathComponent(statusID)
|
||||||
|
.appendingPathComponent("history")
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func statusSourceEndpointURL(domain: String, statusID: Mastodon.Entity.Status.ID) -> URL {
|
||||||
|
return Mastodon.API.endpointURL(domain: domain)
|
||||||
|
.appendingPathComponent("statuses")
|
||||||
|
.appendingPathComponent(statusID)
|
||||||
|
.appendingPathComponent("source")
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func statusSource(
|
||||||
|
forStatusID statusID: Mastodon.Entity.Status.ID,
|
||||||
|
session: URLSession,
|
||||||
|
domain: String,
|
||||||
|
authorization: Mastodon.API.OAuth.Authorization?
|
||||||
|
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.StatusSource>, Error> {
|
||||||
|
let url = statusSourceEndpointURL(domain: domain, statusID: statusID)
|
||||||
|
let request = Mastodon.API.get(url: url, authorization: authorization)
|
||||||
|
|
||||||
|
return session.dataTaskPublisher(for: request)
|
||||||
|
.tryMap { (data: Data, response: URLResponse) in
|
||||||
|
let value = try Mastodon.API.decode(type: Mastodon.Entity.StatusSource.self, from: data, response: response)
|
||||||
|
return Mastodon.Response.Content(value: value, response: response)
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Get all known versions of a status, including the initial and current states.
|
||||||
|
///
|
||||||
|
/// - Since: 3.5.0
|
||||||
|
///
|
||||||
|
/// # Last Update
|
||||||
|
///
|
||||||
|
/// # Reference
|
||||||
|
/// [Document](https://docs.joinmastodon.org/methods/statuses/#history)
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - session: `URLSession`
|
||||||
|
/// - domain: Mastodon instance domain. e.g. "example.com"
|
||||||
|
/// - statusID: id for status
|
||||||
|
/// - authorization: User token. Could be nil if status is public
|
||||||
|
/// - Returns: `AnyPublisher` contains `StatusEdit` nested in the response
|
||||||
|
public static func editHistory(
|
||||||
|
forStatusID statusID: Mastodon.Entity.Status.ID,
|
||||||
|
session: URLSession,
|
||||||
|
domain: String,
|
||||||
|
authorization: Mastodon.API.OAuth.Authorization?
|
||||||
|
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.StatusEdit]>, Error> {
|
||||||
|
|
||||||
|
let url = historyEndpointURL(domain: domain, statusID: statusID)
|
||||||
|
let request = Mastodon.API.get(url: url, authorization: authorization)
|
||||||
|
|
||||||
|
return session.dataTaskPublisher(for: request)
|
||||||
|
.tryMap { (data: Data, response: URLResponse) in
|
||||||
|
let value = try Mastodon.API.decode(type: [Mastodon.Entity.StatusEdit].self, from: data, response: response)
|
||||||
|
return Mastodon.Response.Content(value: value, response: response)
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Edit a given status to change its text, sensitivity, media attachments, or poll. Note that editing a poll’s options will reset the votes.
|
||||||
|
///
|
||||||
|
/// - Since: 3.5.0
|
||||||
|
/// - Version: 4.0.0
|
||||||
|
/// # Last Update
|
||||||
|
/// 2021/3/18
|
||||||
|
/// # Reference
|
||||||
|
/// [Document](https://docs.joinmastodon.org/methods/statuses/#edit)
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - statusID: ID of the status that is to be edited
|
||||||
|
/// - editStatusQuery: Basically the edits (Status, Emoji, Media...), is a `EditStatusQuery`
|
||||||
|
/// - session: `URLSession`
|
||||||
|
/// - domain: Mastodon instance domain. e.g. "example.com"
|
||||||
|
/// - authorization: User token
|
||||||
|
/// - Returns: `AnyPublisher` that contains the updated `Status` nested in the response
|
||||||
|
public static func editStatus(
|
||||||
|
forStatusID statusID: Mastodon.Entity.Status.ID,
|
||||||
|
editStatusQuery: EditStatusQuery,
|
||||||
|
session: URLSession,
|
||||||
|
domain: String,
|
||||||
|
authorization: Mastodon.API.OAuth.Authorization?
|
||||||
|
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Status>, Error> {
|
||||||
|
let url = statusEndpointURL(domain: domain, statusID: statusID)
|
||||||
|
let request = Mastodon.API.put(url: url, query: editStatusQuery, authorization: authorization)
|
||||||
|
|
||||||
|
return session.dataTaskPublisher(for: request)
|
||||||
|
.tryMap { (data: Data, response: URLResponse) in
|
||||||
|
let editedStatus = try Mastodon.API.decode(type: Mastodon.Entity.Status.self, from: data, response: response)
|
||||||
|
return Mastodon.Response.Content(value: editedStatus, response: response)
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Mastodon.API.Statuses {
|
||||||
|
public struct EditStatusQuery: Codable, PutQuery {
|
||||||
|
public let status: String?
|
||||||
|
public let mediaIDs: [String]?
|
||||||
|
public let pollOptions: [String]?
|
||||||
|
public let pollExpiresIn: Int?
|
||||||
|
public let pollMultipleAnswers: Bool?
|
||||||
|
public let sensitive: Bool?
|
||||||
|
public let spoilerText: String?
|
||||||
|
public let visibility: Mastodon.Entity.Status.Visibility?
|
||||||
|
public let language: String?
|
||||||
|
|
||||||
|
public init(
|
||||||
|
status: String?,
|
||||||
|
mediaIDs: [String]?,
|
||||||
|
pollOptions: [String]?,
|
||||||
|
pollExpiresIn: Int?,
|
||||||
|
pollMultipleAnswers: Bool?,
|
||||||
|
sensitive: Bool?,
|
||||||
|
spoilerText: String?,
|
||||||
|
visibility: Mastodon.Entity.Status.Visibility?,
|
||||||
|
language: String?
|
||||||
|
) {
|
||||||
|
self.status = status
|
||||||
|
self.mediaIDs = mediaIDs
|
||||||
|
self.pollOptions = pollOptions
|
||||||
|
self.pollExpiresIn = pollExpiresIn
|
||||||
|
self.pollMultipleAnswers = pollMultipleAnswers
|
||||||
|
self.sensitive = sensitive
|
||||||
|
self.spoilerText = spoilerText
|
||||||
|
self.visibility = visibility
|
||||||
|
self.language = language
|
||||||
|
}
|
||||||
|
|
||||||
|
var contentType: String? {
|
||||||
|
return Self.multipartContentType()
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: Data? {
|
||||||
|
var data = Data()
|
||||||
|
|
||||||
|
status.flatMap { data.append(Data.multipart(key: "status", value: $0)) }
|
||||||
|
for mediaID in mediaIDs ?? [] {
|
||||||
|
data.append(Data.multipart(key: "media_ids[]", value: mediaID))
|
||||||
|
}
|
||||||
|
for pollOption in pollOptions ?? [] {
|
||||||
|
data.append(Data.multipart(key: "poll[options][]", value: pollOption))
|
||||||
|
}
|
||||||
|
pollExpiresIn.flatMap { data.append(Data.multipart(key: "poll[expires_in]", value: $0)) }
|
||||||
|
sensitive.flatMap { data.append(Data.multipart(key: "sensitive", value: $0)) }
|
||||||
|
spoilerText.flatMap { data.append(Data.multipart(key: "spoiler_text", value: $0)) }
|
||||||
|
visibility.flatMap { data.append(Data.multipart(key: "visibility", value: $0.rawValue)) }
|
||||||
|
language.flatMap { data.append(Data.multipart(key: "language", value: $0)) }
|
||||||
|
|
||||||
|
data.append(Data.multipartEnd())
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -133,7 +133,7 @@ extension Mastodon.API {
|
||||||
|
|
||||||
static func get(
|
static func get(
|
||||||
url: URL,
|
url: URL,
|
||||||
query: GetQuery?,
|
query: GetQuery? = nil,
|
||||||
authorization: OAuth.Authorization?
|
authorization: OAuth.Authorization?
|
||||||
) -> URLRequest {
|
) -> URLRequest {
|
||||||
return buildRequest(url: url, method: .GET, query: query, authorization: authorization)
|
return buildRequest(url: url, method: .GET, query: query, authorization: authorization)
|
||||||
|
@ -157,7 +157,7 @@ extension Mastodon.API {
|
||||||
|
|
||||||
static func put(
|
static func put(
|
||||||
url: URL,
|
url: URL,
|
||||||
query: PutQuery?,
|
query: PutQuery? = nil,
|
||||||
authorization: OAuth.Authorization?
|
authorization: OAuth.Authorization?
|
||||||
) -> URLRequest {
|
) -> URLRequest {
|
||||||
return buildRequest(url: url, method: .PUT, query: query, authorization: authorization)
|
return buildRequest(url: url, method: .PUT, query: query, authorization: authorization)
|
||||||
|
|
|
@ -25,6 +25,7 @@ extension Mastodon.Entity {
|
||||||
public let id: ID
|
public let id: ID
|
||||||
public let uri: String
|
public let uri: String
|
||||||
public let createdAt: Date
|
public let createdAt: Date
|
||||||
|
public let editedAt: Date?
|
||||||
public let account: Account
|
public let account: Account
|
||||||
public let content: String? // will be optional when delete status
|
public let content: String? // will be optional when delete status
|
||||||
|
|
||||||
|
@ -65,6 +66,7 @@ extension Mastodon.Entity {
|
||||||
case id
|
case id
|
||||||
case uri
|
case uri
|
||||||
case createdAt = "created_at"
|
case createdAt = "created_at"
|
||||||
|
case editedAt = "edited_at"
|
||||||
case account
|
case account
|
||||||
case content
|
case content
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Mastodon.Entity {
|
||||||
|
|
||||||
|
/// StatusEdit
|
||||||
|
///
|
||||||
|
/// - Since: 0.1.0
|
||||||
|
/// - Version: 3.5.0
|
||||||
|
/// # Last Update
|
||||||
|
/// 2022/12/14
|
||||||
|
/// # Reference
|
||||||
|
/// [Document](https://docs.joinmastodon.org/entities/statusedit/)
|
||||||
|
public class StatusEdit: Codable {
|
||||||
|
public class Poll: Codable {
|
||||||
|
public class Option: Codable {
|
||||||
|
public let title: String
|
||||||
|
}
|
||||||
|
public let options: [Option]
|
||||||
|
public let title: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
public let content: String
|
||||||
|
public let spoilerText: String?
|
||||||
|
public let sensitive: Bool
|
||||||
|
public let createdAt: Date
|
||||||
|
public let account: Account
|
||||||
|
public let poll: Poll?
|
||||||
|
public let mediaAttachments: [Attachment]?
|
||||||
|
public let emojis: [Emoji]?
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case content
|
||||||
|
case spoilerText = "spoiler_text"
|
||||||
|
case sensitive
|
||||||
|
case createdAt = "created_at"
|
||||||
|
case account
|
||||||
|
case poll
|
||||||
|
case mediaAttachments = "media_attachments"
|
||||||
|
case emojis
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Mastodon.Entity {
|
||||||
|
public struct StatusSource: Codable {
|
||||||
|
public let id: String
|
||||||
|
public let text: String
|
||||||
|
public let spoilerText: String
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case id
|
||||||
|
case text
|
||||||
|
case spoilerText = "spoiler_text"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -48,7 +48,7 @@ extension GetQuery {
|
||||||
protocol PostQuery: RequestQuery { }
|
protocol PostQuery: RequestQuery { }
|
||||||
|
|
||||||
extension PostQuery {
|
extension PostQuery {
|
||||||
// By default a `PostQuery` does not has query items
|
// By default a `PostQuery` does not have query items
|
||||||
var queryItems: [URLQueryItem]? { nil }
|
var queryItems: [URLQueryItem]? { nil }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,10 +58,15 @@ protocol PatchQuery: RequestQuery { }
|
||||||
// PUT
|
// PUT
|
||||||
protocol PutQuery: RequestQuery { }
|
protocol PutQuery: RequestQuery { }
|
||||||
|
|
||||||
|
extension PutQuery {
|
||||||
|
// By default a `PutQuery` does not have query items
|
||||||
|
var queryItems: [URLQueryItem]? { nil }
|
||||||
|
}
|
||||||
|
|
||||||
// DELETE
|
// DELETE
|
||||||
protocol DeleteQuery: RequestQuery { }
|
protocol DeleteQuery: RequestQuery { }
|
||||||
|
|
||||||
extension DeleteQuery {
|
extension DeleteQuery {
|
||||||
// By default a `DeleteQuery` does not has query items
|
// By default a `DeleteQuery` does not have query items
|
||||||
var queryItems: [URLQueryItem]? { nil }
|
var queryItems: [URLQueryItem]? { nil }
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CoreDataStack
|
||||||
|
|
||||||
|
public protocol StatusCompatible {
|
||||||
|
var reblog: Status? { get }
|
||||||
|
var attachments: [MastodonAttachment] { get }
|
||||||
|
var isMediaSensitive: Bool { get }
|
||||||
|
var isSensitiveToggled: Bool { get }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Status: StatusCompatible {}
|
||||||
|
|
||||||
|
extension StatusEdit: StatusCompatible {
|
||||||
|
public var reblog: Status? {
|
||||||
|
nil
|
||||||
|
}
|
||||||
|
|
||||||
|
public var isMediaSensitive: Bool {
|
||||||
|
sensitive
|
||||||
|
}
|
||||||
|
|
||||||
|
public var isSensitiveToggled: Bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
|
@ -71,6 +71,7 @@ public struct AttachmentView: View {
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
}
|
}
|
||||||
.padding(EdgeInsets(top: 6, leading: 0, bottom: 10, trailing: 0))
|
.padding(EdgeInsets(top: 6, leading: 0, bottom: 10, trailing: 0))
|
||||||
|
.disabled(!viewModel.isCaptionEditable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -44,7 +44,7 @@ extension AttachmentViewModel: NSItemProviderWriting {
|
||||||
switch input {
|
switch input {
|
||||||
case .image:
|
case .image:
|
||||||
typeIdentifiers.append(UTType.png.identifier)
|
typeIdentifiers.append(UTType.png.identifier)
|
||||||
case .url(let url):
|
case .url(let url), .mastodonAssetUrl(let url, _):
|
||||||
let _uti = UTType(filenameExtension: url.pathExtension)
|
let _uti = UTType(filenameExtension: url.pathExtension)
|
||||||
if let uti = _uti {
|
if let uti = _uti {
|
||||||
if uti.conforms(to: .image) {
|
if uti.conforms(to: .image) {
|
||||||
|
|
|
@ -28,6 +28,8 @@ extension AttachmentViewModel {
|
||||||
} catch {
|
} catch {
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
case .mastodonAssetUrl(let url, _):
|
||||||
|
return try await Self.loadMastodonAsset(url: url)
|
||||||
case .pickerResult(let pickerResult):
|
case .pickerResult(let pickerResult):
|
||||||
do {
|
do {
|
||||||
let output = try await AttachmentViewModel.load(itemProvider: pickerResult.itemProvider)
|
let output = try await AttachmentViewModel.load(itemProvider: pickerResult.itemProvider)
|
||||||
|
@ -45,6 +47,14 @@ extension AttachmentViewModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static func loadMastodonAsset(url: URL) async throws -> Output {
|
||||||
|
guard !url.isFileURL else {
|
||||||
|
throw AttachmentError.invalidAttachmentType
|
||||||
|
}
|
||||||
|
let (imageData, _) = try await URLSession.shared.data(from: url)
|
||||||
|
return .image(imageData, imageKind: AssetType(imageData) == .png ? .png : .jpg)
|
||||||
|
}
|
||||||
|
|
||||||
private static func load(url: URL) async throws -> Output {
|
private static func load(url: URL) async throws -> Output {
|
||||||
guard let uti = UTType(filenameExtension: url.pathExtension) else {
|
guard let uti = UTType(filenameExtension: url.pathExtension) else {
|
||||||
throw AttachmentError.invalidAttachmentType
|
throw AttachmentError.invalidAttachmentType
|
||||||
|
|
|
@ -66,7 +66,10 @@ extension AttachmentViewModel {
|
||||||
let authContext: AuthContext
|
let authContext: AuthContext
|
||||||
}
|
}
|
||||||
|
|
||||||
public typealias UploadResult = Mastodon.Entity.Attachment
|
public enum UploadResult {
|
||||||
|
case uploadedMastodonAttachment(Mastodon.Entity.Attachment)
|
||||||
|
case exists
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AttachmentViewModel {
|
extension AttachmentViewModel {
|
||||||
|
@ -194,7 +197,7 @@ extension AttachmentViewModel {
|
||||||
|
|
||||||
// escape here
|
// escape here
|
||||||
progress.completedUnitCount = progress.totalUnitCount
|
progress.completedUnitCount = progress.totalUnitCount
|
||||||
return attachmentStatusResponse.value
|
return .uploadedMastodonAttachment(attachmentStatusResponse.value)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
AttachmentViewModel.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): attachment processing. Retry \(waitProcessRetryCount)/\(waitProcessRetryLimit)")
|
AttachmentViewModel.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): attachment processing. Retry \(waitProcessRetryCount)/\(waitProcessRetryLimit)")
|
||||||
|
@ -207,7 +210,7 @@ extension AttachmentViewModel {
|
||||||
} else {
|
} else {
|
||||||
AttachmentViewModel.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): upload attachment success: \(attachmentUploadResponse.value.url ?? "<nil>")")
|
AttachmentViewModel.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): upload attachment success: \(attachmentUploadResponse.value.url ?? "<nil>")")
|
||||||
|
|
||||||
return attachmentUploadResponse.value
|
return .uploadedMastodonAttachment(attachmentUploadResponse.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,7 +50,8 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable
|
||||||
public let input: Input
|
public let input: Input
|
||||||
public let sizeLimit: SizeLimit
|
public let sizeLimit: SizeLimit
|
||||||
@Published var caption = ""
|
@Published var caption = ""
|
||||||
|
@Published public private(set) var isCaptionEditable = true
|
||||||
|
|
||||||
// output
|
// output
|
||||||
@Published public private(set) var output: Output?
|
@Published public private(set) var output: Output?
|
||||||
@Published public private(set) var thumbnail: UIImage? // original size image thumbnail
|
@Published public private(set) var thumbnail: UIImage? // original size image thumbnail
|
||||||
|
@ -137,6 +138,17 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable
|
||||||
do {
|
do {
|
||||||
var output = try await load(input: input)
|
var output = try await load(input: input)
|
||||||
|
|
||||||
|
switch input {
|
||||||
|
case .mastodonAssetUrl:
|
||||||
|
self.isCaptionEditable = false
|
||||||
|
self.uploadState = .finish
|
||||||
|
self.output = output
|
||||||
|
self.uploadResult = .exists
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
switch output {
|
switch output {
|
||||||
case .image(let data, _):
|
case .image(let data, _):
|
||||||
self.output = output
|
self.output = output
|
||||||
|
@ -253,6 +265,7 @@ extension AttachmentViewModel {
|
||||||
public enum Input: Hashable {
|
public enum Input: Hashable {
|
||||||
case image(UIImage)
|
case image(UIImage)
|
||||||
case url(URL)
|
case url(URL)
|
||||||
|
case mastodonAssetUrl(URL, String)
|
||||||
case pickerResult(PHPickerResult)
|
case pickerResult(PHPickerResult)
|
||||||
case itemProvider(NSItemProvider)
|
case itemProvider(NSItemProvider)
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,11 +83,6 @@ public final class ComposeContentViewController: UIViewController {
|
||||||
)
|
)
|
||||||
return view
|
return view
|
||||||
}()
|
}()
|
||||||
|
|
||||||
deinit {
|
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ComposeContentViewController {
|
extension ComposeContentViewController {
|
||||||
|
@ -331,6 +326,7 @@ extension ComposeContentViewController {
|
||||||
viewModel.$isEmojiActive.assign(to: &composeContentToolbarViewModel.$isEmojiActive)
|
viewModel.$isEmojiActive.assign(to: &composeContentToolbarViewModel.$isEmojiActive)
|
||||||
viewModel.$isContentWarningActive.assign(to: &composeContentToolbarViewModel.$isContentWarningActive)
|
viewModel.$isContentWarningActive.assign(to: &composeContentToolbarViewModel.$isContentWarningActive)
|
||||||
viewModel.$visibility.assign(to: &composeContentToolbarViewModel.$visibility)
|
viewModel.$visibility.assign(to: &composeContentToolbarViewModel.$visibility)
|
||||||
|
viewModel.$isVisibilityButtonEnabled.assign(to: &composeContentToolbarViewModel.$isVisibilityButtonEnabled)
|
||||||
viewModel.$maxTextInputLimit.assign(to: &composeContentToolbarViewModel.$maxTextInputLimit)
|
viewModel.$maxTextInputLimit.assign(to: &composeContentToolbarViewModel.$maxTextInputLimit)
|
||||||
viewModel.$contentWeightedLength.assign(to: &composeContentToolbarViewModel.$contentWeightedLength)
|
viewModel.$contentWeightedLength.assign(to: &composeContentToolbarViewModel.$contentWeightedLength)
|
||||||
viewModel.$contentWarningWeightedLength.assign(to: &composeContentToolbarViewModel.$contentWarningWeightedLength)
|
viewModel.$contentWarningWeightedLength.assign(to: &composeContentToolbarViewModel.$contentWarningWeightedLength)
|
||||||
|
|
|
@ -37,7 +37,6 @@ extension ComposeContentViewModel: UITextViewDelegate {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let backedString = metaText.backedString
|
let backedString = metaText.backedString
|
||||||
logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(backedString)")
|
|
||||||
|
|
||||||
// configure auto completion
|
// configure auto completion
|
||||||
setupAutoComplete(for: textView)
|
setupAutoComplete(for: textView)
|
||||||
|
|
|
@ -21,6 +21,11 @@ public protocol ComposeContentViewModelDelegate: AnyObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class ComposeContentViewModel: NSObject, ObservableObject {
|
public final class ComposeContentViewModel: NSObject, ObservableObject {
|
||||||
|
|
||||||
|
public enum ComposeContext {
|
||||||
|
case composeStatus
|
||||||
|
case editStatus(status: Status, statusSource: Mastodon.Entity.StatusSource)
|
||||||
|
}
|
||||||
|
|
||||||
let logger = Logger(subsystem: "ComposeContentViewModel", category: "ViewModel")
|
let logger = Logger(subsystem: "ComposeContentViewModel", category: "ViewModel")
|
||||||
|
|
||||||
|
@ -32,6 +37,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
|
||||||
|
|
||||||
// input
|
// input
|
||||||
let context: AppContext
|
let context: AppContext
|
||||||
|
let composeContext: ComposeContext
|
||||||
let destination: Destination
|
let destination: Destination
|
||||||
weak var delegate: ComposeContentViewModelDelegate?
|
weak var delegate: ComposeContentViewModelDelegate?
|
||||||
|
|
||||||
|
@ -111,7 +117,8 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
|
||||||
|
|
||||||
// visibility
|
// visibility
|
||||||
@Published public var visibility: Mastodon.Entity.Status.Visibility
|
@Published public var visibility: Mastodon.Entity.Status.Visibility
|
||||||
|
@Published public var isVisibilityButtonEnabled = false
|
||||||
|
|
||||||
// language
|
// language
|
||||||
@Published public var language: String
|
@Published public var language: String
|
||||||
@Published public private(set) var recentLanguages: [String]
|
@Published public private(set) var recentLanguages: [String]
|
||||||
|
@ -141,12 +148,14 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
|
||||||
public init(
|
public init(
|
||||||
context: AppContext,
|
context: AppContext,
|
||||||
authContext: AuthContext,
|
authContext: AuthContext,
|
||||||
|
composeContext: ComposeContext,
|
||||||
destination: Destination,
|
destination: Destination,
|
||||||
initialContent: String
|
initialContent: String
|
||||||
) {
|
) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.authContext = authContext
|
self.authContext = authContext
|
||||||
self.destination = destination
|
self.destination = destination
|
||||||
|
self.composeContext = composeContext
|
||||||
self.visibility = {
|
self.visibility = {
|
||||||
// default private when user locked
|
// default private when user locked
|
||||||
var visibility: Mastodon.Entity.Status.Visibility = {
|
var visibility: Mastodon.Entity.Status.Visibility = {
|
||||||
|
@ -179,10 +188,30 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
|
||||||
}
|
}
|
||||||
return visibility
|
return visibility
|
||||||
}()
|
}()
|
||||||
|
|
||||||
self.customEmojiViewModel = context.emojiService.dequeueCustomEmojiViewModel(
|
self.customEmojiViewModel = context.emojiService.dequeueCustomEmojiViewModel(
|
||||||
for: authContext.mastodonAuthenticationBox.domain
|
for: authContext.mastodonAuthenticationBox.domain
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if case let ComposeContext.editStatus(status, _) = composeContext {
|
||||||
|
if status.isContentSensitive {
|
||||||
|
isContentWarningActive = true
|
||||||
|
contentWarning = status.spoilerText ?? ""
|
||||||
|
}
|
||||||
|
if let poll = status.poll {
|
||||||
|
isPollActive = !poll.expired
|
||||||
|
pollMultipleConfigurationOption = poll.multiple
|
||||||
|
if let pollExpiresAt = poll.expiresAt {
|
||||||
|
pollExpireConfigurationOption = .init(closestDateToExpiry: pollExpiresAt)
|
||||||
|
}
|
||||||
|
pollOptions = poll.options.sortedByIndex().map {
|
||||||
|
let option = PollComposeItem.Option()
|
||||||
|
option.text = $0.title
|
||||||
|
return option
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let recentLanguages = context.settingService.currentSetting.value?.recentLanguages ?? []
|
let recentLanguages = context.settingService.currentSetting.value?.recentLanguages ?? []
|
||||||
self.recentLanguages = recentLanguages
|
self.recentLanguages = recentLanguages
|
||||||
self.language = recentLanguages.first ?? Locale.current.languageCode ?? "en"
|
self.language = recentLanguages.first ?? Locale.current.languageCode ?? "en"
|
||||||
|
@ -259,6 +288,28 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
|
||||||
// TODO: more limit
|
// TODO: more limit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch composeContext {
|
||||||
|
case .composeStatus:
|
||||||
|
self.isVisibilityButtonEnabled = true
|
||||||
|
case let .editStatus(status, _):
|
||||||
|
if let visibility = Mastodon.Entity.Status.Visibility(rawValue: status.visibility.rawValue) {
|
||||||
|
self.visibility = visibility
|
||||||
|
}
|
||||||
|
self.isVisibilityButtonEnabled = false
|
||||||
|
self.attachmentViewModels = status.attachments.compactMap {
|
||||||
|
guard let assetURL = $0.assetURL, let url = URL(string: assetURL) else { return nil }
|
||||||
|
let attachmentViewModel = AttachmentViewModel(
|
||||||
|
api: context.apiService,
|
||||||
|
authContext: authContext,
|
||||||
|
input: .mastodonAssetUrl(url, $0.id),
|
||||||
|
sizeLimit: sizeLimit,
|
||||||
|
delegate: self
|
||||||
|
)
|
||||||
|
attachmentViewModel.caption = $0.altDescription ?? ""
|
||||||
|
return attachmentViewModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bind()
|
bind()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -557,7 +608,57 @@ extension ComposeContentViewModel {
|
||||||
visibility: visibility,
|
visibility: visibility,
|
||||||
language: language
|
language: language
|
||||||
)
|
)
|
||||||
} // end func publisher()
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MastodonEditStatusPublisher
|
||||||
|
public func statusEditPublisher() throws -> StatusPublisher? {
|
||||||
|
let authContext = self.authContext
|
||||||
|
guard case let .editStatus(status, _) = composeContext else { return nil }
|
||||||
|
|
||||||
|
// author
|
||||||
|
let managedObjectContext = self.context.managedObjectContext
|
||||||
|
var _author: ManagedObjectRecord<MastodonUser>?
|
||||||
|
managedObjectContext.performAndWait {
|
||||||
|
_author = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: managedObjectContext)?.user.asRecord
|
||||||
|
}
|
||||||
|
guard let author = _author else {
|
||||||
|
throw AppError.badAuthentication
|
||||||
|
}
|
||||||
|
|
||||||
|
// poll
|
||||||
|
_ = try {
|
||||||
|
guard isPollActive else { return }
|
||||||
|
let isAllNonEmpty = pollOptions
|
||||||
|
.map { $0.text }
|
||||||
|
.allSatisfy { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }
|
||||||
|
guard isAllNonEmpty else {
|
||||||
|
throw ComposeError.pollHasEmptyOption
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// save language to recent languages
|
||||||
|
if let settings = context.settingService.currentSetting.value {
|
||||||
|
settings.managedObjectContext?.performAndWait {
|
||||||
|
settings.recentLanguages = [language] + settings.recentLanguages.filter { $0 != language }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return MastodonEditStatusPublisher(statusID: status.id,
|
||||||
|
author: author,
|
||||||
|
isContentWarningComposing: isContentWarningActive,
|
||||||
|
contentWarning: contentWarning,
|
||||||
|
content: content,
|
||||||
|
isMediaSensitive: isContentWarningActive,
|
||||||
|
attachmentViewModels: attachmentViewModels,
|
||||||
|
isPollComposing: isPollActive,
|
||||||
|
pollOptions: pollOptions,
|
||||||
|
pollExpireConfigurationOption: pollExpireConfigurationOption,
|
||||||
|
pollMultipleConfigurationOption: pollMultipleConfigurationOption,
|
||||||
|
visibility: visibility,
|
||||||
|
language: language)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ComposeContentViewModel {
|
extension ComposeContentViewModel {
|
||||||
|
|
|
@ -0,0 +1,183 @@
|
||||||
|
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import MastodonCore
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
|
public final class MastodonEditStatusPublisher: NSObject, ProgressReporting {
|
||||||
|
|
||||||
|
// Input
|
||||||
|
public let statusID: Status.ID
|
||||||
|
public let author: ManagedObjectRecord<MastodonUser>
|
||||||
|
|
||||||
|
// content warning
|
||||||
|
public let isContentWarningComposing: Bool
|
||||||
|
public let contentWarning: String
|
||||||
|
// status content
|
||||||
|
public let content: String
|
||||||
|
// media
|
||||||
|
public let isMediaSensitive: Bool
|
||||||
|
public let attachmentViewModels: [AttachmentViewModel]
|
||||||
|
// poll
|
||||||
|
public let isPollComposing: Bool
|
||||||
|
public let pollOptions: [PollComposeItem.Option]
|
||||||
|
public let pollExpireConfigurationOption: PollComposeItem.ExpireConfiguration.Option
|
||||||
|
public let pollMultipleConfigurationOption: PollComposeItem.MultipleConfiguration.Option
|
||||||
|
// visibility
|
||||||
|
public let visibility: Mastodon.Entity.Status.Visibility
|
||||||
|
// language
|
||||||
|
public let language: String
|
||||||
|
|
||||||
|
// Output
|
||||||
|
let _progress = Progress()
|
||||||
|
public var progress: Progress { _progress }
|
||||||
|
@Published var _state: StatusPublisherState = .pending
|
||||||
|
public var state: Published<StatusPublisherState>.Publisher { $_state }
|
||||||
|
|
||||||
|
public var reactor: StatusPublisherReactor?
|
||||||
|
|
||||||
|
public init(
|
||||||
|
statusID: Status.ID,
|
||||||
|
author: ManagedObjectRecord<MastodonUser>,
|
||||||
|
isContentWarningComposing: Bool,
|
||||||
|
contentWarning: String,
|
||||||
|
content: String,
|
||||||
|
isMediaSensitive: Bool,
|
||||||
|
attachmentViewModels: [AttachmentViewModel],
|
||||||
|
isPollComposing: Bool,
|
||||||
|
pollOptions: [PollComposeItem.Option],
|
||||||
|
pollExpireConfigurationOption: PollComposeItem.ExpireConfiguration.Option,
|
||||||
|
pollMultipleConfigurationOption: PollComposeItem.MultipleConfiguration.Option,
|
||||||
|
visibility: Mastodon.Entity.Status.Visibility,
|
||||||
|
language: String
|
||||||
|
) {
|
||||||
|
self.author = author
|
||||||
|
self.statusID = statusID
|
||||||
|
self.isContentWarningComposing = isContentWarningComposing
|
||||||
|
self.contentWarning = contentWarning
|
||||||
|
self.content = content
|
||||||
|
self.isMediaSensitive = isMediaSensitive
|
||||||
|
self.attachmentViewModels = attachmentViewModels
|
||||||
|
self.isPollComposing = isPollComposing
|
||||||
|
self.pollOptions = pollOptions
|
||||||
|
self.pollExpireConfigurationOption = pollExpireConfigurationOption
|
||||||
|
self.pollMultipleConfigurationOption = pollMultipleConfigurationOption
|
||||||
|
self.visibility = visibility
|
||||||
|
self.language = language
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - StatusPublisher
|
||||||
|
extension MastodonEditStatusPublisher: StatusPublisher {
|
||||||
|
|
||||||
|
public func publish(
|
||||||
|
api: APIService,
|
||||||
|
authContext: AuthContext
|
||||||
|
) async throws -> StatusPublishResult {
|
||||||
|
let idempotencyKey = UUID().uuidString
|
||||||
|
|
||||||
|
let publishStatusTaskStartDelayWeight: Int64 = 20
|
||||||
|
let publishStatusTaskStartDelayCount: Int64 = publishStatusTaskStartDelayWeight
|
||||||
|
|
||||||
|
let publishAttachmentTaskWeight: Int64 = 100
|
||||||
|
let publishAttachmentTaskCount: Int64 = Int64(attachmentViewModels.count) * publishAttachmentTaskWeight
|
||||||
|
|
||||||
|
let publishStatusTaskWeight: Int64 = 20
|
||||||
|
let publishStatusTaskCount: Int64 = publishStatusTaskWeight
|
||||||
|
|
||||||
|
let taskCount = [
|
||||||
|
publishStatusTaskStartDelayCount,
|
||||||
|
publishAttachmentTaskCount,
|
||||||
|
publishStatusTaskCount
|
||||||
|
].reduce(0, +)
|
||||||
|
progress.totalUnitCount = taskCount
|
||||||
|
progress.completedUnitCount = 0
|
||||||
|
|
||||||
|
// start delay
|
||||||
|
try? await Task.sleep(nanoseconds: 1 * .second)
|
||||||
|
progress.completedUnitCount += publishStatusTaskStartDelayWeight
|
||||||
|
|
||||||
|
// Task: attachment
|
||||||
|
|
||||||
|
var attachmentIDs: [Mastodon.Entity.Attachment.ID] = []
|
||||||
|
for attachmentViewModel in attachmentViewModels {
|
||||||
|
// set progress
|
||||||
|
progress.addChild(attachmentViewModel.progress, withPendingUnitCount: publishAttachmentTaskWeight)
|
||||||
|
// upload media
|
||||||
|
do {
|
||||||
|
switch attachmentViewModel.uploadResult {
|
||||||
|
case .none:
|
||||||
|
// precondition: all media uploaded
|
||||||
|
throw AppError.badRequest
|
||||||
|
case .exists:
|
||||||
|
guard case let AttachmentViewModel.Input.mastodonAssetUrl(_, attachmentId) = attachmentViewModel.input else {
|
||||||
|
throw AppError.badRequest
|
||||||
|
}
|
||||||
|
attachmentIDs.append(attachmentId)
|
||||||
|
break
|
||||||
|
case let .uploadedMastodonAttachment(attachment):
|
||||||
|
attachmentIDs.append(attachment.id)
|
||||||
|
|
||||||
|
let caption = attachmentViewModel.caption
|
||||||
|
guard !caption.isEmpty else { continue }
|
||||||
|
|
||||||
|
_ = try await api.updateMedia(
|
||||||
|
domain: authContext.mastodonAuthenticationBox.domain,
|
||||||
|
attachmentID: attachment.id,
|
||||||
|
query: .init(
|
||||||
|
file: nil,
|
||||||
|
thumbnail: nil,
|
||||||
|
description: caption,
|
||||||
|
focus: nil
|
||||||
|
),
|
||||||
|
mastodonAuthenticationBox: authContext.mastodonAuthenticationBox
|
||||||
|
).singleOutput()
|
||||||
|
|
||||||
|
// TODO: allow background upload
|
||||||
|
// let attachment = try await attachmentViewModel.upload(context: uploadContext)
|
||||||
|
// let attachmentID = attachment.id
|
||||||
|
// attachmentIDs.append(attachmentID)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
_state = .failure(error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let pollOptions: [String]? = {
|
||||||
|
guard self.isPollComposing else { return nil }
|
||||||
|
let options = self.pollOptions.compactMap { $0.text.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||||
|
return options.isEmpty ? nil : options
|
||||||
|
}()
|
||||||
|
let pollExpiresIn: Int? = {
|
||||||
|
guard self.isPollComposing else { return nil }
|
||||||
|
guard pollOptions != nil else { return nil }
|
||||||
|
return self.pollExpireConfigurationOption.seconds
|
||||||
|
}()
|
||||||
|
|
||||||
|
let query = Mastodon.API.Statuses.EditStatusQuery(
|
||||||
|
status: content,
|
||||||
|
mediaIDs: attachmentIDs.isEmpty ? nil : attachmentIDs,
|
||||||
|
pollOptions: pollOptions,
|
||||||
|
pollExpiresIn: pollExpiresIn,
|
||||||
|
pollMultipleAnswers: pollMultipleConfigurationOption,
|
||||||
|
sensitive: isMediaSensitive,
|
||||||
|
spoilerText: isContentWarningComposing ? contentWarning : nil,
|
||||||
|
visibility: visibility,
|
||||||
|
language: language
|
||||||
|
)
|
||||||
|
|
||||||
|
let editStatusResponse = try await api.publishStatusEdit(forStatusID: statusID,
|
||||||
|
editStatusQuery: query,
|
||||||
|
authenticationBox: authContext.mastodonAuthenticationBox)
|
||||||
|
|
||||||
|
progress.completedUnitCount += publishStatusTaskCount
|
||||||
|
_state = .success
|
||||||
|
|
||||||
|
return .edit(editStatusResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -119,31 +119,35 @@ extension MastodonStatusPublisher: StatusPublisher {
|
||||||
progress.addChild(attachmentViewModel.progress, withPendingUnitCount: publishAttachmentTaskWeight)
|
progress.addChild(attachmentViewModel.progress, withPendingUnitCount: publishAttachmentTaskWeight)
|
||||||
// upload media
|
// upload media
|
||||||
do {
|
do {
|
||||||
guard let attachment = attachmentViewModel.uploadResult else {
|
switch attachmentViewModel.uploadResult {
|
||||||
|
case .none:
|
||||||
// precondition: all media uploaded
|
// precondition: all media uploaded
|
||||||
throw AppError.badRequest
|
throw AppError.badRequest
|
||||||
|
case .exists:
|
||||||
|
break
|
||||||
|
case let .uploadedMastodonAttachment(attachment):
|
||||||
|
attachmentIDs.append(attachment.id)
|
||||||
|
|
||||||
|
let caption = attachmentViewModel.caption
|
||||||
|
guard !caption.isEmpty else { continue }
|
||||||
|
|
||||||
|
_ = try await api.updateMedia(
|
||||||
|
domain: authContext.mastodonAuthenticationBox.domain,
|
||||||
|
attachmentID: attachment.id,
|
||||||
|
query: .init(
|
||||||
|
file: nil,
|
||||||
|
thumbnail: nil,
|
||||||
|
description: caption,
|
||||||
|
focus: nil
|
||||||
|
),
|
||||||
|
mastodonAuthenticationBox: authContext.mastodonAuthenticationBox
|
||||||
|
).singleOutput()
|
||||||
|
|
||||||
|
// TODO: allow background upload
|
||||||
|
// let attachment = try await attachmentViewModel.upload(context: uploadContext)
|
||||||
|
// let attachmentID = attachment.id
|
||||||
|
// attachmentIDs.append(attachmentID)
|
||||||
}
|
}
|
||||||
attachmentIDs.append(attachment.id)
|
|
||||||
|
|
||||||
let caption = attachmentViewModel.caption
|
|
||||||
guard !caption.isEmpty else { continue }
|
|
||||||
|
|
||||||
_ = try await api.updateMedia(
|
|
||||||
domain: authContext.mastodonAuthenticationBox.domain,
|
|
||||||
attachmentID: attachment.id,
|
|
||||||
query: .init(
|
|
||||||
file: nil,
|
|
||||||
thumbnail: nil,
|
|
||||||
description: caption,
|
|
||||||
focus: nil
|
|
||||||
),
|
|
||||||
mastodonAuthenticationBox: authContext.mastodonAuthenticationBox
|
|
||||||
).singleOutput()
|
|
||||||
|
|
||||||
// TODO: allow background upload
|
|
||||||
// let attachment = try await attachmentViewModel.upload(context: uploadContext)
|
|
||||||
// let attachmentID = attachment.id
|
|
||||||
// attachmentIDs.append(attachmentID)
|
|
||||||
} catch {
|
} catch {
|
||||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): upload attachment fail: \(error.localizedDescription)")
|
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): upload attachment fail: \(error.localizedDescription)")
|
||||||
_state = .failure(error)
|
_state = .failure(error)
|
||||||
|
@ -188,7 +192,7 @@ extension MastodonStatusPublisher: StatusPublisher {
|
||||||
_state = .success
|
_state = .success
|
||||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): status published: \(publishResponse.value.id)")
|
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): status published: \(publishResponse.value.id)")
|
||||||
|
|
||||||
return .mastodon(publishResponse)
|
return .post(publishResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ extension ComposeContentToolbarView {
|
||||||
var allVisibilities: [Mastodon.Entity.Status.Visibility] {
|
var allVisibilities: [Mastodon.Entity.Status.Visibility] {
|
||||||
return [.public, .private, .direct]
|
return [.public, .private, .direct]
|
||||||
}
|
}
|
||||||
|
@Published var isVisibilityButtonEnabled = false
|
||||||
@Published var isPollActive = false
|
@Published var isPollActive = false
|
||||||
@Published var isEmojiActive = false
|
@Published var isEmojiActive = false
|
||||||
@Published var isContentWarningActive = false
|
@Published var isContentWarningActive = false
|
||||||
|
|
|
@ -70,7 +70,9 @@ struct ComposeContentToolbarView: View {
|
||||||
} label: {
|
} label: {
|
||||||
label(for: viewModel.visibility.image)
|
label(for: viewModel.visibility.image)
|
||||||
.accessibilityLabel(L10n.Scene.Compose.Keyboard.selectVisibilityEntry(viewModel.visibility.title))
|
.accessibilityLabel(L10n.Scene.Compose.Keyboard.selectVisibilityEntry(viewModel.visibility.title))
|
||||||
|
.opacity(viewModel.isVisibilityButtonEnabled ? 1.0 : 0.5)
|
||||||
}
|
}
|
||||||
|
.disabled(!viewModel.isVisibilityButtonEnabled)
|
||||||
.frame(width: 48, height: 48)
|
.frame(width: 48, height: 48)
|
||||||
case .poll:
|
case .poll:
|
||||||
Button {
|
Button {
|
||||||
|
|
|
@ -62,6 +62,10 @@ public struct MetaTextViewRepresentable: UIViewRepresentable {
|
||||||
public func updateUIView(_ metaTextView: MetaTextView, context: Context) {
|
public func updateUIView(_ metaTextView: MetaTextView, context: Context) {
|
||||||
// update layout
|
// update layout
|
||||||
context.coordinator.widthLayoutConstraint.constant = width
|
context.coordinator.widthLayoutConstraint.constant = width
|
||||||
|
|
||||||
|
// trigger layout engine update to adjust to text height
|
||||||
|
metaText.textView.setNeedsLayout()
|
||||||
|
metaText.textView.layoutIfNeeded()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func makeCoordinator() -> Coordinator {
|
public func makeCoordinator() -> Coordinator {
|
||||||
|
|
|
@ -179,7 +179,7 @@ extension MediaView.Configuration {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MediaView {
|
extension MediaView {
|
||||||
public static func configuration(status: Status) -> [MediaView.Configuration] {
|
public static func configuration(status: StatusCompatible) -> [MediaView.Configuration] {
|
||||||
func videoInfo(from attachment: MastodonAttachment) -> MediaView.Configuration.VideoInfo {
|
func videoInfo(from attachment: MastodonAttachment) -> MediaView.Configuration.VideoInfo {
|
||||||
MediaView.Configuration.VideoInfo(
|
MediaView.Configuration.VideoInfo(
|
||||||
aspectRadio: attachment.size,
|
aspectRadio: attachment.size,
|
||||||
|
@ -190,7 +190,7 @@ extension MediaView {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
let status = status.reblog ?? status
|
let status: StatusCompatible = status.reblog ?? status
|
||||||
let attachments = status.attachments
|
let attachments = status.attachments
|
||||||
let configurations = attachments.enumerated().map { (idx, attachment) -> MediaView.Configuration in
|
let configurations = attachments.enumerated().map { (idx, attachment) -> MediaView.Configuration in
|
||||||
let configuration: MediaView.Configuration = {
|
let configuration: MediaView.Configuration = {
|
||||||
|
|
|
@ -597,6 +597,10 @@ extension NotificationView: StatusViewDelegate {
|
||||||
public func statusView(_ statusView: StatusView, statusMetricView: StatusMetricView, favoriteButtonDidPressed button: UIButton) {
|
public func statusView(_ statusView: StatusView, statusMetricView: StatusMetricView, favoriteButtonDidPressed button: UIButton) {
|
||||||
assertionFailure()
|
assertionFailure()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func statusView(_ statusView: StatusView, statusMetricView: StatusMetricView, showEditHistory button: UIButton) {
|
||||||
|
assertionFailure()
|
||||||
|
}
|
||||||
|
|
||||||
public func statusView(_ statusView: StatusView, accessibilityActivate: Void) {
|
public func statusView(_ statusView: StatusView, accessibilityActivate: Void) {
|
||||||
assertionFailure()
|
assertionFailure()
|
||||||
|
|
|
@ -81,6 +81,7 @@ public class StatusAuthorView: UIStackView {
|
||||||
case .notificationQuote: layoutNotificationQuote()
|
case .notificationQuote: layoutNotificationQuote()
|
||||||
case .composeStatusReplica: layoutComposeStatusReplica()
|
case .composeStatusReplica: layoutComposeStatusReplica()
|
||||||
case .composeStatusAuthor: layoutComposeStatusAuthor()
|
case .composeStatusAuthor: layoutComposeStatusAuthor()
|
||||||
|
case .editHistory: layoutBase()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -158,6 +159,10 @@ extension StatusAuthorView {
|
||||||
public func setupAuthorMenu(menuContext: AuthorMenuContext) -> (UIMenu, [UIAccessibilityCustomAction]) {
|
public func setupAuthorMenu(menuContext: AuthorMenuContext) -> (UIMenu, [UIAccessibilityCustomAction]) {
|
||||||
var actions = [MastodonMenu.Action]()
|
var actions = [MastodonMenu.Action]()
|
||||||
|
|
||||||
|
if menuContext.isMyself {
|
||||||
|
actions.append(.editStatus)
|
||||||
|
}
|
||||||
|
|
||||||
if !menuContext.isMyself {
|
if !menuContext.isMyself {
|
||||||
if let statusLanguage = menuContext.statusLanguage, menuContext.isTranslationEnabled, !menuContext.isTranslated {
|
if let statusLanguage = menuContext.statusLanguage, menuContext.isTranslationEnabled, !menuContext.isTranslated {
|
||||||
actions.append(
|
actions.append(
|
||||||
|
|
|
@ -0,0 +1,149 @@
|
||||||
|
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import MastodonAsset
|
||||||
|
|
||||||
|
public final class StatusMetricRowView: UIButton {
|
||||||
|
let icon: UIImageView
|
||||||
|
let textLabel: UILabel
|
||||||
|
let detailLabel: UILabel
|
||||||
|
let chevron: UIImageView
|
||||||
|
|
||||||
|
private var disposableConstraints: [NSLayoutConstraint] = []
|
||||||
|
private var isVerticalAxis: Bool?
|
||||||
|
|
||||||
|
public init(iconImage: UIImage? = nil, text: String? = nil, detailText: String? = nil) {
|
||||||
|
|
||||||
|
icon = UIImageView(image: iconImage?.withRenderingMode(.alwaysTemplate))
|
||||||
|
icon.tintColor = Asset.Colors.Label.secondary.color
|
||||||
|
icon.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
textLabel = UILabel()
|
||||||
|
textLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
textLabel.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular))
|
||||||
|
textLabel.textColor = Asset.Colors.Label.primary.color
|
||||||
|
textLabel.numberOfLines = 0
|
||||||
|
textLabel.text = text
|
||||||
|
|
||||||
|
detailLabel = UILabel()
|
||||||
|
detailLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
detailLabel.text = detailText
|
||||||
|
detailLabel.textColor = Asset.Colors.Label.secondary.color
|
||||||
|
detailLabel.numberOfLines = 0
|
||||||
|
detailLabel.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular))
|
||||||
|
|
||||||
|
chevron = UIImageView(image: UIImage(systemName: "chevron.right"))
|
||||||
|
chevron.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
chevron.tintColor = Asset.Colors.Label.tertiary.color
|
||||||
|
|
||||||
|
super.init(frame: .zero)
|
||||||
|
|
||||||
|
addSubview(icon)
|
||||||
|
addSubview(textLabel)
|
||||||
|
addSubview(detailLabel)
|
||||||
|
addSubview(chevron)
|
||||||
|
|
||||||
|
accessibilityTraits.insert(.button)
|
||||||
|
|
||||||
|
setupConstraints()
|
||||||
|
traitCollectionDidChange(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||||
|
|
||||||
|
public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||||
|
super.traitCollectionDidChange(previousTraitCollection)
|
||||||
|
let isVerticalAxis = traitCollection.preferredContentSizeCategory.isAccessibilityCategory
|
||||||
|
|
||||||
|
if isVerticalAxis {
|
||||||
|
detailLabel.textAlignment = .natural
|
||||||
|
} else {
|
||||||
|
switch traitCollection.layoutDirection {
|
||||||
|
case .leftToRight, .unspecified: detailLabel.textAlignment = .right
|
||||||
|
case .rightToLeft: detailLabel.textAlignment = .left
|
||||||
|
@unknown default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard isVerticalAxis != self.isVerticalAxis else { return }
|
||||||
|
self.isVerticalAxis = isVerticalAxis
|
||||||
|
NSLayoutConstraint.deactivate(disposableConstraints)
|
||||||
|
|
||||||
|
if isVerticalAxis {
|
||||||
|
disposableConstraints = [
|
||||||
|
textLabel.topAnchor.constraint(equalTo: topAnchor, constant: 11),
|
||||||
|
|
||||||
|
detailLabel.leadingAnchor.constraint(equalTo: textLabel.leadingAnchor),
|
||||||
|
detailLabel.topAnchor.constraint(equalTo: textLabel.bottomAnchor, constant: 8),
|
||||||
|
bottomAnchor.constraint(equalTo: detailLabel.bottomAnchor, constant: 11),
|
||||||
|
|
||||||
|
chevron.leadingAnchor.constraint(greaterThanOrEqualTo: textLabel.trailingAnchor, constant: 12),
|
||||||
|
chevron.leadingAnchor.constraint(greaterThanOrEqualTo: detailLabel.trailingAnchor, constant: 12),
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
disposableConstraints = [
|
||||||
|
textLabel.topAnchor.constraint(greaterThanOrEqualTo: topAnchor, constant: 11),
|
||||||
|
textLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||||
|
bottomAnchor.constraint(greaterThanOrEqualTo: textLabel.bottomAnchor, constant: 11),
|
||||||
|
|
||||||
|
detailLabel.leadingAnchor.constraint(greaterThanOrEqualTo: textLabel.trailingAnchor, constant: 8),
|
||||||
|
|
||||||
|
detailLabel.topAnchor.constraint(greaterThanOrEqualTo: topAnchor, constant: 11),
|
||||||
|
detailLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||||
|
bottomAnchor.constraint(greaterThanOrEqualTo: detailLabel.bottomAnchor, constant: 11),
|
||||||
|
|
||||||
|
chevron.leadingAnchor.constraint(equalTo: detailLabel.trailingAnchor, constant: 12),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
NSLayoutConstraint.activate(disposableConstraints)
|
||||||
|
}
|
||||||
|
|
||||||
|
var margin: CGFloat = 0 {
|
||||||
|
didSet {
|
||||||
|
layoutMargins = UIEdgeInsets(horizontal: margin, vertical: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupConstraints() {
|
||||||
|
icon.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
||||||
|
chevron.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
||||||
|
chevron.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||||
|
let constraints = [
|
||||||
|
icon.topAnchor.constraint(greaterThanOrEqualTo: topAnchor, constant: 10),
|
||||||
|
icon.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
|
||||||
|
icon.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||||
|
textLabel.leadingAnchor.constraint(equalTo: icon.trailingAnchor, constant: 16),
|
||||||
|
icon.widthAnchor.constraint(greaterThanOrEqualToConstant: 24),
|
||||||
|
icon.heightAnchor.constraint(greaterThanOrEqualToConstant: 24),
|
||||||
|
bottomAnchor.constraint(greaterThanOrEqualTo: icon.bottomAnchor, constant: 10),
|
||||||
|
|
||||||
|
chevron.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||||
|
layoutMarginsGuide.trailingAnchor.constraint(equalTo: chevron.trailingAnchor),
|
||||||
|
]
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate(constraints)
|
||||||
|
}
|
||||||
|
|
||||||
|
public override var isHighlighted: Bool {
|
||||||
|
get { super.isHighlighted }
|
||||||
|
set {
|
||||||
|
super.isHighlighted = newValue
|
||||||
|
if newValue {
|
||||||
|
backgroundColor = Asset.Colors.selectionHighlight.color
|
||||||
|
} else {
|
||||||
|
backgroundColor = .clear
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override var accessibilityLabel: String? {
|
||||||
|
get { textLabel.text }
|
||||||
|
set {}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override var accessibilityValue: String? {
|
||||||
|
get { detailLabel.text }
|
||||||
|
set {}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,29 +5,45 @@
|
||||||
// Created by MainasuK on 2022-1-17.
|
// Created by MainasuK on 2022-1-17.
|
||||||
//
|
//
|
||||||
|
|
||||||
import os.log
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import MastodonAsset
|
||||||
|
import MastodonLocalization
|
||||||
|
|
||||||
protocol StatusMetricViewDelegate: AnyObject {
|
protocol StatusMetricViewDelegate: AnyObject {
|
||||||
func statusMetricView(_ statusMetricView: StatusMetricView, reblogButtonDidPressed button: UIButton)
|
func statusMetricView(_ statusMetricView: StatusMetricView, reblogButtonDidPressed button: UIButton)
|
||||||
func statusMetricView(_ statusMetricView: StatusMetricView, favoriteButtonDidPressed button: UIButton)
|
func statusMetricView(_ statusMetricView: StatusMetricView, favoriteButtonDidPressed button: UIButton)
|
||||||
|
func statusMetricView(_ statusMetricView: StatusMetricView, didPressEditHistoryButton button: UIButton)
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class StatusMetricView: UIView {
|
public final class StatusMetricView: UIView {
|
||||||
|
|
||||||
let logger = Logger(subsystem: "StatusMetricView", category: "View")
|
|
||||||
|
|
||||||
weak var delegate: StatusMetricViewDelegate?
|
weak var delegate: StatusMetricViewDelegate?
|
||||||
|
|
||||||
|
var margin: CGFloat = 0 {
|
||||||
|
didSet {
|
||||||
|
dateAdaptiveMarginContainerView.margin = margin
|
||||||
|
reblogButton.margin = margin
|
||||||
|
favoriteButton.margin = margin
|
||||||
|
editHistoryButton.margin = margin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// container
|
// container
|
||||||
public let containerStackView: UIStackView = {
|
private let containerStackView: UIStackView = {
|
||||||
let stackView = UIStackView()
|
let stackView = UIStackView()
|
||||||
stackView.axis = .horizontal
|
stackView.axis = .vertical
|
||||||
stackView.spacing = 4
|
stackView.alignment = .leading
|
||||||
return stackView
|
return stackView
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
private let separator: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = Asset.Theme.Mastodon.separator.color
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
// date
|
// date
|
||||||
|
let dateAdaptiveMarginContainerView = AdaptiveMarginContainerView()
|
||||||
public let dateLabel: UILabel = {
|
public let dateLabel: UILabel = {
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
label.font = UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .systemFont(ofSize: 15, weight: .regular))
|
label.font = UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .systemFont(ofSize: 15, weight: .regular))
|
||||||
|
@ -35,33 +51,34 @@ public final class StatusMetricView: UIView {
|
||||||
label.adjustsFontSizeToFitWidth = true
|
label.adjustsFontSizeToFitWidth = true
|
||||||
label.minimumScaleFactor = 0.5
|
label.minimumScaleFactor = 0.5
|
||||||
label.numberOfLines = 2
|
label.numberOfLines = 2
|
||||||
|
label.textColor = Asset.Colors.Label.secondary.color
|
||||||
return label
|
return label
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// meter
|
// reblog meter
|
||||||
public let meterContainer: UIStackView = {
|
private let buttonStackView: UIStackView = {
|
||||||
let stackView = UIStackView()
|
let stackView = UIStackView()
|
||||||
stackView.axis = .horizontal
|
stackView.axis = .vertical
|
||||||
stackView.spacing = 20
|
stackView.alignment = .leading
|
||||||
return stackView
|
return stackView
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// reblog meter
|
public let reblogButton: StatusMetricRowView = {
|
||||||
public let reblogButton: UIButton = {
|
let button = StatusMetricRowView(iconImage: Asset.Arrow.repeat.image, text: L10n.Common.Controls.Status.Buttons.reblogsTitle, detailText: "")
|
||||||
let button = UIButton(type: .system)
|
|
||||||
button.titleLabel?.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold))
|
|
||||||
button.setTitle("0 reblog", for: .normal)
|
|
||||||
return button
|
return button
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// favorite meter
|
// favorite meter
|
||||||
public let favoriteButton: UIButton = {
|
public let favoriteButton: StatusMetricRowView = {
|
||||||
let button = UIButton(type: .system)
|
let button = StatusMetricRowView(iconImage: UIImage(systemName: "star"), text: L10n.Common.Controls.Status.Buttons.favoritesTitle, detailText: "")
|
||||||
button.titleLabel?.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold))
|
|
||||||
button.setTitle("0 favorite", for: .normal)
|
|
||||||
return button
|
return button
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
public let editHistoryButton: StatusMetricRowView = {
|
||||||
|
let button = StatusMetricRowView(iconImage: Asset.Scene.EditHistory.edit.image, text: L10n.Common.Controls.Status.Buttons.editHistoryTitle)
|
||||||
|
return button
|
||||||
|
}()
|
||||||
|
|
||||||
public override init(frame: CGRect) {
|
public override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
_init()
|
_init()
|
||||||
|
@ -71,7 +88,6 @@ public final class StatusMetricView: UIView {
|
||||||
super.init(coder: coder)
|
super.init(coder: coder)
|
||||||
_init()
|
_init()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension StatusMetricView {
|
extension StatusMetricView {
|
||||||
|
@ -79,40 +95,63 @@ extension StatusMetricView {
|
||||||
// container: H - [ dateLabel | meterContainer ]
|
// container: H - [ dateLabel | meterContainer ]
|
||||||
containerStackView.translatesAutoresizingMaskIntoConstraints = false
|
containerStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
addSubview(containerStackView)
|
addSubview(containerStackView)
|
||||||
|
|
||||||
|
separator.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
containerStackView.addArrangedSubview(separator)
|
||||||
|
|
||||||
|
reblogButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
buttonStackView.addArrangedSubview(reblogButton)
|
||||||
|
|
||||||
|
favoriteButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
buttonStackView.addArrangedSubview(favoriteButton)
|
||||||
|
|
||||||
|
editHistoryButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
buttonStackView.addArrangedSubview(editHistoryButton)
|
||||||
|
|
||||||
|
buttonStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
containerStackView.addArrangedSubview(buttonStackView)
|
||||||
|
containerStackView.setCustomSpacing(11, after: buttonStackView)
|
||||||
|
|
||||||
|
dateLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
dateAdaptiveMarginContainerView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
dateAdaptiveMarginContainerView.contentView = dateLabel
|
||||||
|
containerStackView.addArrangedSubview(dateAdaptiveMarginContainerView)
|
||||||
|
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
containerStackView.topAnchor.constraint(equalTo: topAnchor, constant: 8),
|
containerStackView.topAnchor.constraint(equalTo: topAnchor, constant: 8),
|
||||||
containerStackView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
containerStackView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
containerStackView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
containerStackView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
bottomAnchor.constraint(equalTo: containerStackView.bottomAnchor, constant: 12),
|
bottomAnchor.constraint(equalTo: containerStackView.bottomAnchor, constant: 12),
|
||||||
|
|
||||||
|
separator.heightAnchor.constraint(equalToConstant: 0.5),
|
||||||
|
|
||||||
|
buttonStackView.widthAnchor.constraint(equalTo: containerStackView.widthAnchor),
|
||||||
|
|
||||||
|
reblogButton.widthAnchor.constraint(equalTo: buttonStackView.widthAnchor),
|
||||||
|
favoriteButton.widthAnchor.constraint(equalTo: reblogButton.widthAnchor),
|
||||||
|
editHistoryButton.widthAnchor.constraint(equalTo: reblogButton.widthAnchor),
|
||||||
|
dateLabel.widthAnchor.constraint(equalTo: reblogButton.widthAnchor),
|
||||||
])
|
])
|
||||||
|
|
||||||
containerStackView.addArrangedSubview(dateLabel)
|
reblogButton.addTarget(self, action: #selector(StatusMetricView.didPressReblogButton(_:)), for: .touchUpInside)
|
||||||
dateLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
favoriteButton.addTarget(self, action: #selector(StatusMetricView.didPressFavoriteButton(_:)), for: .touchUpInside)
|
||||||
containerStackView.addArrangedSubview(meterContainer)
|
editHistoryButton.addTarget(self, action: #selector(StatusMetricView.didPressEditHistoryButton(_:)), for: .touchUpInside)
|
||||||
|
|
||||||
// meterContainer: H - [ reblogButton | favoriteButton ]
|
|
||||||
meterContainer.addArrangedSubview(reblogButton)
|
|
||||||
meterContainer.addArrangedSubview(favoriteButton)
|
|
||||||
reblogButton.setContentHuggingPriority(.required - 2, for: .horizontal)
|
|
||||||
reblogButton.setContentCompressionResistancePriority(.required - 2, for: .horizontal)
|
|
||||||
favoriteButton.setContentHuggingPriority(.required - 1, for: .horizontal)
|
|
||||||
favoriteButton.setContentCompressionResistancePriority(.required - 1, for: .horizontal)
|
|
||||||
|
|
||||||
reblogButton.addTarget(self, action: #selector(StatusMetricView.reblogButtonDidPressed(_:)), for: .touchUpInside)
|
|
||||||
favoriteButton.addTarget(self, action: #selector(StatusMetricView.favoriteButtonDidPressed(_:)), for: .touchUpInside)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension StatusMetricView {
|
extension StatusMetricView {
|
||||||
|
|
||||||
@objc private func reblogButtonDidPressed(_ sender: UIButton) {
|
@objc private func didPressReblogButton(_ sender: UIButton) {
|
||||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
|
||||||
delegate?.statusMetricView(self, reblogButtonDidPressed: sender)
|
delegate?.statusMetricView(self, reblogButtonDidPressed: sender)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func favoriteButtonDidPressed(_ sender: UIButton) {
|
@objc private func didPressFavoriteButton(_ sender: UIButton) {
|
||||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
|
||||||
delegate?.statusMetricView(self, favoriteButtonDidPressed: sender)
|
delegate?.statusMetricView(self, favoriteButtonDidPressed: sender)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func didPressEditHistoryButton(_ sender: UIButton) {
|
||||||
|
delegate?.statusMetricView(self, didPressEditHistoryButton: sender)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,6 +39,33 @@ extension StatusView {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension StatusView {
|
extension StatusView {
|
||||||
|
|
||||||
|
public func configure(status: Status, statusEdit: StatusEdit) {
|
||||||
|
viewModel.objects.insert(status)
|
||||||
|
if let reblog = status.reblog {
|
||||||
|
viewModel.objects.insert(reblog)
|
||||||
|
}
|
||||||
|
|
||||||
|
configureHeader(status: status)
|
||||||
|
let author = (status.reblog ?? status).author
|
||||||
|
configureAuthor(author: author)
|
||||||
|
let timestamp = (status.reblog ?? status).publisher(for: \.createdAt)
|
||||||
|
configureTimestamp(timestamp: timestamp.eraseToAnyPublisher())
|
||||||
|
configureApplicationName(status.application?.name)
|
||||||
|
configureMedia(status: status)
|
||||||
|
configurePollHistory(statusEdit: statusEdit)
|
||||||
|
configureCard(status: status)
|
||||||
|
configureToolbar(status: status)
|
||||||
|
configureFilter(status: status)
|
||||||
|
configureContent(statusEdit: statusEdit, status: status)
|
||||||
|
configureMedia(status: statusEdit)
|
||||||
|
actionToolbarAdaptiveMarginContainerView.isHidden = true
|
||||||
|
authorView.menuButton.isHidden = true
|
||||||
|
headerAdaptiveMarginContainerView.isHidden = true
|
||||||
|
viewModel.isSensitiveToggled = true
|
||||||
|
viewModel.isContentReveal = true
|
||||||
|
}
|
||||||
|
|
||||||
public func configure(status: Status) {
|
public func configure(status: Status) {
|
||||||
viewModel.objects.insert(status)
|
viewModel.objects.insert(status)
|
||||||
if let reblog = status.reblog {
|
if let reblog = status.reblog {
|
||||||
|
@ -50,6 +77,7 @@ extension StatusView {
|
||||||
configureAuthor(author: author)
|
configureAuthor(author: author)
|
||||||
let timestamp = (status.reblog ?? status).publisher(for: \.createdAt)
|
let timestamp = (status.reblog ?? status).publisher(for: \.createdAt)
|
||||||
configureTimestamp(timestamp: timestamp.eraseToAnyPublisher())
|
configureTimestamp(timestamp: timestamp.eraseToAnyPublisher())
|
||||||
|
configureApplicationName(status.application?.name)
|
||||||
configureContent(status: status)
|
configureContent(status: status)
|
||||||
configureMedia(status: status)
|
configureMedia(status: status)
|
||||||
configurePoll(status: status)
|
configurePoll(status: status)
|
||||||
|
@ -113,7 +141,7 @@ extension StatusView {
|
||||||
let header = ViewModel.Header.reply(info: .init(header: metaContent))
|
let header = ViewModel.Header.reply(info: .init(header: metaContent))
|
||||||
return header
|
return header
|
||||||
}
|
}
|
||||||
|
|
||||||
if let replyTo = status.replyTo {
|
if let replyTo = status.replyTo {
|
||||||
// A. replyTo status exist
|
// A. replyTo status exist
|
||||||
let header = createHeader(name: replyTo.author.displayNameWithFallback, emojis: replyTo.author.emojis.asDictionary)
|
let header = createHeader(name: replyTo.author.displayNameWithFallback, emojis: replyTo.author.emojis.asDictionary)
|
||||||
|
@ -234,14 +262,21 @@ extension StatusView {
|
||||||
|
|
||||||
private func configureTimestamp(timestamp: AnyPublisher<Date, Never>) {
|
private func configureTimestamp(timestamp: AnyPublisher<Date, Never>) {
|
||||||
// timestamp
|
// timestamp
|
||||||
viewModel.timestampFormatter = { (date: Date) in
|
viewModel.timestampFormatter = { (date: Date, isEdited: Bool) in
|
||||||
date.localizedSlowedTimeAgoSinceNow
|
if isEdited {
|
||||||
|
return L10n.Common.Controls.Status.editedAtTimestampPrefix(date.localizedSlowedTimeAgoSinceNow)
|
||||||
|
}
|
||||||
|
return date.localizedSlowedTimeAgoSinceNow
|
||||||
}
|
}
|
||||||
timestamp
|
timestamp
|
||||||
.map { $0 as Date? }
|
.map { $0 as Date? }
|
||||||
.assign(to: \.timestamp, on: viewModel)
|
.assign(to: \.timestamp, on: viewModel)
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func configureApplicationName(_ applicationName: String?) {
|
||||||
|
viewModel.applicationName = applicationName
|
||||||
|
}
|
||||||
|
|
||||||
func revertTranslation() {
|
func revertTranslation() {
|
||||||
guard let originalStatus = viewModel.originalStatus else { return }
|
guard let originalStatus = viewModel.originalStatus else { return }
|
||||||
|
@ -281,7 +316,27 @@ extension StatusView {
|
||||||
viewModel.content = PlaintextMetaContent(string: "")
|
viewModel.content = PlaintextMetaContent(string: "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func configureContent(statusEdit: StatusEdit, status: Status) {
|
||||||
|
statusEdit.spoilerText.map {
|
||||||
|
viewModel.spoilerContent = PlaintextMetaContent(string: $0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// language
|
||||||
|
viewModel.language = (status.reblog ?? status).language
|
||||||
|
// content
|
||||||
|
do {
|
||||||
|
let content = MastodonContent(content: statusEdit.content, emojis: statusEdit.emojis.asDictionary)
|
||||||
|
let metaContent = try MastodonMetaContent.convert(document: content)
|
||||||
|
viewModel.content = metaContent
|
||||||
|
viewModel.translatedFromLanguage = nil
|
||||||
|
viewModel.isCurrentlyTranslating = false
|
||||||
|
} catch {
|
||||||
|
assertionFailure(error.localizedDescription)
|
||||||
|
viewModel.content = PlaintextMetaContent(string: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func configureContent(status: Status) {
|
private func configureContent(status: Status) {
|
||||||
guard status.translatedContent == nil else {
|
guard status.translatedContent == nil else {
|
||||||
return configureTranslated(status: status)
|
return configureTranslated(status: status)
|
||||||
|
@ -327,7 +382,7 @@ extension StatusView {
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func configureMedia(status: Status) {
|
private func configureMedia(status: StatusCompatible) {
|
||||||
let status = status.reblog ?? status
|
let status = status.reblog ?? status
|
||||||
|
|
||||||
viewModel.isMediaSensitive = status.isMediaSensitive
|
viewModel.isMediaSensitive = status.isMediaSensitive
|
||||||
|
@ -335,6 +390,19 @@ extension StatusView {
|
||||||
let configurations = MediaView.configuration(status: status)
|
let configurations = MediaView.configuration(status: status)
|
||||||
viewModel.mediaViewConfigurations = configurations
|
viewModel.mediaViewConfigurations = configurations
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func configurePollHistory(statusEdit: StatusEdit) {
|
||||||
|
guard let poll = statusEdit.poll else { return }
|
||||||
|
|
||||||
|
let pollItems = poll.options.map { PollItem.history(option: $0) }
|
||||||
|
self.viewModel.pollItems = pollItems
|
||||||
|
pollStatusStackView.isHidden = true
|
||||||
|
|
||||||
|
var _snapshot = NSDiffableDataSourceSnapshot<PollSection, PollItem>()
|
||||||
|
_snapshot.appendSections([.main])
|
||||||
|
_snapshot.appendItems(pollItems, toSection: .main)
|
||||||
|
pollTableViewDiffableDataSource?.applySnapshotUsingReloadData(_snapshot)
|
||||||
|
}
|
||||||
|
|
||||||
private func configurePoll(status: Status) {
|
private func configurePoll(status: Status) {
|
||||||
let status = status.reblog ?? status
|
let status = status.reblog ?? status
|
||||||
|
@ -433,6 +501,16 @@ extension StatusView {
|
||||||
.map(Int.init)
|
.map(Int.init)
|
||||||
.assign(to: \.favoriteCount, on: viewModel)
|
.assign(to: \.favoriteCount, on: viewModel)
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
status.publisher(for: \.editedAt)
|
||||||
|
.assign(to: \.editedAt, on: viewModel)
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
status.publisher(for: \.editHistory)
|
||||||
|
.compactMap({ guard let edits = $0 else { return nil }
|
||||||
|
return Array(edits)
|
||||||
|
})
|
||||||
|
.assign(to: \.statusEdits, on: viewModel)
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
// relationship
|
// relationship
|
||||||
status.publisher(for: \.rebloggedBy)
|
status.publisher(for: \.rebloggedBy)
|
||||||
|
|
|
@ -30,7 +30,7 @@ extension StatusView {
|
||||||
public var context: AppContext?
|
public var context: AppContext?
|
||||||
public var authContext: AuthContext?
|
public var authContext: AuthContext?
|
||||||
public var originalStatus: Status?
|
public var originalStatus: Status?
|
||||||
|
|
||||||
// Header
|
// Header
|
||||||
@Published public var header: Header = .none
|
@Published public var header: Header = .none
|
||||||
|
|
||||||
|
@ -52,8 +52,9 @@ extension StatusView {
|
||||||
@Published public var translatedUsingProvider: String?
|
@Published public var translatedUsingProvider: String?
|
||||||
|
|
||||||
@Published public var timestamp: Date?
|
@Published public var timestamp: Date?
|
||||||
public var timestampFormatter: ((_ date: Date) -> String)?
|
public var timestampFormatter: ((_ date: Date, _ isEdited: Bool) -> String)?
|
||||||
@Published public var timestampText = ""
|
@Published public var timestampText = ""
|
||||||
|
@Published public var applicationName: String? = nil
|
||||||
|
|
||||||
// Spoiler
|
// Spoiler
|
||||||
@Published public var spoilerContent: MetaContent?
|
@Published public var spoilerContent: MetaContent?
|
||||||
|
@ -101,6 +102,9 @@ extension StatusView {
|
||||||
@Published public var replyCount: Int = 0
|
@Published public var replyCount: Int = 0
|
||||||
@Published public var reblogCount: Int = 0
|
@Published public var reblogCount: Int = 0
|
||||||
@Published public var favoriteCount: Int = 0
|
@Published public var favoriteCount: Int = 0
|
||||||
|
|
||||||
|
@Published public var statusEdits: [StatusEdit] = []
|
||||||
|
@Published public var editedAt: Date? = nil
|
||||||
|
|
||||||
// Filter
|
// Filter
|
||||||
@Published public var activeFilters: [Mastodon.Entity.Filter] = []
|
@Published public var activeFilters: [Mastodon.Entity.Filter] = []
|
||||||
|
@ -264,16 +268,19 @@ extension StatusView.ViewModel {
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
// timestamp
|
// timestamp
|
||||||
Publishers.CombineLatest(
|
Publishers.CombineLatest3(
|
||||||
$timestamp,
|
$timestamp,
|
||||||
|
$editedAt,
|
||||||
timestampUpdatePublisher.prepend(Date()).eraseToAnyPublisher()
|
timestampUpdatePublisher.prepend(Date()).eraseToAnyPublisher()
|
||||||
)
|
)
|
||||||
.compactMap { [weak self] timestamp, _ -> String? in
|
.compactMap { [weak self] timestamp, editedAt, _ -> String? in
|
||||||
guard let self = self else { return nil }
|
guard let self = self else { return nil }
|
||||||
guard let timestamp = timestamp,
|
if let timestamp = editedAt, let text = self.timestampFormatter?(timestamp, true) {
|
||||||
let text = self.timestampFormatter?(timestamp)
|
return text
|
||||||
else { return "" }
|
} else if let timestamp = timestamp, let text = self.timestampFormatter?(timestamp, false) {
|
||||||
return text
|
return text
|
||||||
|
}
|
||||||
|
return ""
|
||||||
}
|
}
|
||||||
.removeDuplicates()
|
.removeDuplicates()
|
||||||
.assign(to: &$timestampText)
|
.assign(to: &$timestampText)
|
||||||
|
@ -304,6 +311,16 @@ extension StatusView.ViewModel {
|
||||||
// statusView.spoilerBannerView.label.reset()
|
// statusView.spoilerBannerView.label.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if statusView.style == .editHistory, let spoilerContent = spoilerContent, !spoilerContent.string.isEmpty {
|
||||||
|
statusView.historyContentWarningLabel.configure(content: spoilerContent)
|
||||||
|
statusView.historyContentWarningAdaptiveMarginContainerView.isHidden = statusView.style != .editHistory
|
||||||
|
statusView.setContentSensitiveeToggleButtonDisplay(isDisplay: false)
|
||||||
|
} else {
|
||||||
|
statusView.historyContentWarningLabel.reset()
|
||||||
|
statusView.historyContentWarningAdaptiveMarginContainerView.isHidden = true
|
||||||
|
statusView.setContentSensitiveeToggleButtonDisplay(isDisplay: false)
|
||||||
|
}
|
||||||
|
|
||||||
let paragraphStyle = statusView.contentMetaText.paragraphStyle
|
let paragraphStyle = statusView.contentMetaText.paragraphStyle
|
||||||
if let language = language {
|
if let language = language {
|
||||||
if #available(iOS 16, *) {
|
if #available(iOS 16, *) {
|
||||||
|
@ -571,12 +588,13 @@ extension StatusView.ViewModel {
|
||||||
favoriteButtonTitle
|
favoriteButtonTitle
|
||||||
).map { $0.count + $1.count }
|
).map { $0.count + $1.count }
|
||||||
|
|
||||||
Publishers.CombineLatest(
|
Publishers.CombineLatest3(
|
||||||
$timestamp,
|
$timestamp,
|
||||||
|
$applicationName,
|
||||||
metricButtonTitleLength
|
metricButtonTitleLength
|
||||||
)
|
)
|
||||||
.sink { timestamp, metricButtonTitleLength in
|
.sink { timestamp, applicationName, metricButtonTitleLength in
|
||||||
let text: String = {
|
let dateString: String = {
|
||||||
guard let timestamp = timestamp else { return " " }
|
guard let timestamp = timestamp else { return " " }
|
||||||
|
|
||||||
let formatter = DateFormatter()
|
let formatter = DateFormatter()
|
||||||
|
@ -591,20 +609,42 @@ extension StatusView.ViewModel {
|
||||||
}
|
}
|
||||||
return formatter.string(from: timestamp)
|
return formatter.string(from: timestamp)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
let text: String
|
||||||
|
if let applicationName {
|
||||||
|
text = L10n.Common.Controls.Status.postedViaApplication(dateString, applicationName)
|
||||||
|
} else {
|
||||||
|
text = dateString
|
||||||
|
}
|
||||||
|
|
||||||
statusView.statusMetricView.dateLabel.text = text
|
statusView.statusMetricView.dateLabel.text = text
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
reblogButtonTitle
|
$reblogCount
|
||||||
.sink { title in
|
.sink { count in
|
||||||
statusView.statusMetricView.reblogButton.setTitle(title, for: .normal)
|
statusView.statusMetricView.reblogButton.isHidden = count == 0
|
||||||
|
statusView.statusMetricView.reblogButton.detailLabel.text = count.formatted()
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
favoriteButtonTitle
|
$favoriteCount
|
||||||
.sink { title in
|
.sink { count in
|
||||||
statusView.statusMetricView.favoriteButton.setTitle(title, for: .normal)
|
statusView.statusMetricView.favoriteButton.isHidden = count == 0
|
||||||
|
statusView.statusMetricView.favoriteButton.detailLabel.text = count.formatted()
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
$editedAt
|
||||||
|
.sink { editedAt in
|
||||||
|
if let editedAt {
|
||||||
|
let relativeDateFormatter = RelativeDateTimeFormatter()
|
||||||
|
let relativeDate = relativeDateFormatter.localizedString(for: editedAt, relativeTo: Date())
|
||||||
|
statusView.statusMetricView.editHistoryButton.detailLabel.text = L10n.Common.Controls.Status.Buttons.editHistoryDetail(relativeDate)
|
||||||
|
statusView.statusMetricView.editHistoryButton.isHidden = false
|
||||||
|
} else {
|
||||||
|
statusView.statusMetricView.editHistoryButton.isHidden = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
}
|
}
|
||||||
|
@ -705,8 +745,10 @@ extension StatusView.ViewModel {
|
||||||
strings.append(info.header.string)
|
strings.append(info.header.string)
|
||||||
strings.append(authorName?.string)
|
strings.append(authorName?.string)
|
||||||
}
|
}
|
||||||
|
|
||||||
strings.append(timestamp)
|
if statusView.style != .editHistory {
|
||||||
|
strings.append(timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
return strings.compactMap { $0 }.joined(separator: ", ")
|
return strings.compactMap { $0 }.joined(separator: ", ")
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@ public protocol StatusViewDelegate: AnyObject {
|
||||||
func statusView(_ statusView: StatusView, mediaGridContainerView: MediaGridContainerView, mediaSensitiveButtonDidPressed button: UIButton)
|
func statusView(_ statusView: StatusView, mediaGridContainerView: MediaGridContainerView, mediaSensitiveButtonDidPressed button: UIButton)
|
||||||
func statusView(_ statusView: StatusView, statusMetricView: StatusMetricView, reblogButtonDidPressed button: UIButton)
|
func statusView(_ statusView: StatusView, statusMetricView: StatusMetricView, reblogButtonDidPressed button: UIButton)
|
||||||
func statusView(_ statusView: StatusView, statusMetricView: StatusMetricView, favoriteButtonDidPressed button: UIButton)
|
func statusView(_ statusView: StatusView, statusMetricView: StatusMetricView, favoriteButtonDidPressed button: UIButton)
|
||||||
|
func statusView(_ statusView: StatusView, statusMetricView: StatusMetricView, showEditHistory button: UIButton)
|
||||||
func statusView(_ statusView: StatusView, cardControl: StatusCardControl, didTapURL url: URL)
|
func statusView(_ statusView: StatusView, cardControl: StatusCardControl, didTapURL url: URL)
|
||||||
func statusView(_ statusView: StatusView, cardControlMenu: StatusCardControl) -> UIMenu?
|
func statusView(_ statusView: StatusView, cardControlMenu: StatusCardControl) -> UIMenu?
|
||||||
|
|
||||||
|
@ -87,6 +88,46 @@ public final class StatusView: UIView {
|
||||||
// author
|
// author
|
||||||
let authorAdaptiveMarginContainerView = AdaptiveMarginContainerView()
|
let authorAdaptiveMarginContainerView = AdaptiveMarginContainerView()
|
||||||
public let authorView = StatusAuthorView()
|
public let authorView = StatusAuthorView()
|
||||||
|
|
||||||
|
// edit history content warning
|
||||||
|
lazy var historyContentWarningAdaptiveMarginContainerView: AdaptiveMarginContainerView = {
|
||||||
|
let view = AdaptiveMarginContainerView()
|
||||||
|
view.contentView = historyContentWarningContainerView
|
||||||
|
view.margin = StatusView.containerLayoutMargin
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
let historyContentWarningLabel: MetaLabel = {
|
||||||
|
let label = MetaLabel(style: .statusSpoilerBanner)
|
||||||
|
label.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var historyContentWarningContainerView: UIView = {
|
||||||
|
let container = UIView()
|
||||||
|
container.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
let divider = UIView()
|
||||||
|
divider.backgroundColor = Asset.Colors.Label.secondary.color
|
||||||
|
divider.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
container.addSubview(historyContentWarningLabel)
|
||||||
|
container.addSubview(divider)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
historyContentWarningLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor),
|
||||||
|
historyContentWarningLabel.topAnchor.constraint(equalTo: container.topAnchor),
|
||||||
|
historyContentWarningLabel.trailingAnchor.constraint(equalTo: container.trailingAnchor),
|
||||||
|
|
||||||
|
divider.leadingAnchor.constraint(equalTo: container.leadingAnchor),
|
||||||
|
divider.topAnchor.constraint(equalTo: historyContentWarningLabel.bottomAnchor, constant: 16),
|
||||||
|
divider.trailingAnchor.constraint(equalTo: container.trailingAnchor),
|
||||||
|
divider.heightAnchor.constraint(equalToConstant: 2),
|
||||||
|
divider.bottomAnchor.constraint(equalTo: container.bottomAnchor)
|
||||||
|
])
|
||||||
|
|
||||||
|
return container
|
||||||
|
}()
|
||||||
|
|
||||||
// content
|
// content
|
||||||
let contentAdaptiveMarginContainerView = AdaptiveMarginContainerView()
|
let contentAdaptiveMarginContainerView = AdaptiveMarginContainerView()
|
||||||
|
@ -255,7 +296,6 @@ public final class StatusView: UIView {
|
||||||
public let actionToolbarContainer = ActionToolbarContainer()
|
public let actionToolbarContainer = ActionToolbarContainer()
|
||||||
|
|
||||||
// metric
|
// metric
|
||||||
let statusMetricViewAdaptiveMarginContainerView = AdaptiveMarginContainerView()
|
|
||||||
public let statusMetricView = StatusMetricView()
|
public let statusMetricView = StatusMetricView()
|
||||||
|
|
||||||
// filter hint
|
// filter hint
|
||||||
|
@ -397,6 +437,7 @@ extension StatusView {
|
||||||
case notificationQuote
|
case notificationQuote
|
||||||
case composeStatusReplica
|
case composeStatusReplica
|
||||||
case composeStatusAuthor
|
case composeStatusAuthor
|
||||||
|
case editHistory
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -411,6 +452,7 @@ extension StatusView.Style {
|
||||||
case .notificationQuote: notificationQuote(statusView: statusView)
|
case .notificationQuote: notificationQuote(statusView: statusView)
|
||||||
case .composeStatusReplica: composeStatusReplica(statusView: statusView)
|
case .composeStatusReplica: composeStatusReplica(statusView: statusView)
|
||||||
case .composeStatusAuthor: composeStatusAuthor(statusView: statusView)
|
case .composeStatusAuthor: composeStatusAuthor(statusView: statusView)
|
||||||
|
case .editHistory: editHistory(statusView: statusView)
|
||||||
}
|
}
|
||||||
|
|
||||||
statusView.authorView.layout(style: self)
|
statusView.authorView.layout(style: self)
|
||||||
|
@ -448,6 +490,9 @@ extension StatusView.Style {
|
||||||
statusView.authorAdaptiveMarginContainerView.contentView = statusView.authorView
|
statusView.authorAdaptiveMarginContainerView.contentView = statusView.authorView
|
||||||
statusView.authorAdaptiveMarginContainerView.margin = StatusView.containerLayoutMargin
|
statusView.authorAdaptiveMarginContainerView.margin = StatusView.containerLayoutMargin
|
||||||
statusView.containerStackView.addArrangedSubview(statusView.authorAdaptiveMarginContainerView)
|
statusView.containerStackView.addArrangedSubview(statusView.authorAdaptiveMarginContainerView)
|
||||||
|
|
||||||
|
// history content warning
|
||||||
|
statusView.containerStackView.addArrangedSubview(statusView.historyContentWarningAdaptiveMarginContainerView)
|
||||||
|
|
||||||
// content container: V - [ contentMetaText statusCardControl ]
|
// content container: V - [ contentMetaText statusCardControl ]
|
||||||
statusView.contentContainer.axis = .vertical
|
statusView.contentContainer.axis = .vertical
|
||||||
|
@ -534,16 +579,8 @@ extension StatusView.Style {
|
||||||
base(statusView: statusView) // override the base style
|
base(statusView: statusView) // override the base style
|
||||||
|
|
||||||
// statusMetricView
|
// statusMetricView
|
||||||
statusView.statusMetricViewAdaptiveMarginContainerView.contentView = statusView.statusMetricView
|
statusView.statusMetricView.margin = StatusView.containerLayoutMargin
|
||||||
statusView.statusMetricViewAdaptiveMarginContainerView.margin = StatusView.containerLayoutMargin
|
statusView.containerStackView.addArrangedSubview(statusView.statusMetricView)
|
||||||
statusView.containerStackView.addArrangedSubview(statusView.statusMetricViewAdaptiveMarginContainerView)
|
|
||||||
|
|
||||||
UIContentSizeCategory.publisher
|
|
||||||
.sink { category in
|
|
||||||
statusView.statusMetricView.containerStackView.axis = category > .accessibilityLarge ? .vertical : .horizontal
|
|
||||||
statusView.statusMetricView.containerStackView.alignment = category > .accessibilityLarge ? .leading : .fill
|
|
||||||
}
|
|
||||||
.store(in: &statusView._disposeBag)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func report(statusView: StatusView) {
|
func report(statusView: StatusView) {
|
||||||
|
@ -585,6 +622,9 @@ extension StatusView.Style {
|
||||||
statusView.actionToolbarAdaptiveMarginContainerView.removeFromSuperview()
|
statusView.actionToolbarAdaptiveMarginContainerView.removeFromSuperview()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func editHistory(statusView: StatusView) {
|
||||||
|
base(statusView: statusView)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension StatusView {
|
extension StatusView {
|
||||||
|
@ -653,7 +693,7 @@ extension StatusView: AdaptiveContainerView {
|
||||||
contentAdaptiveMarginContainerView.margin = margin
|
contentAdaptiveMarginContainerView.margin = margin
|
||||||
pollAdaptiveMarginContainerView.margin = margin
|
pollAdaptiveMarginContainerView.margin = margin
|
||||||
actionToolbarAdaptiveMarginContainerView.margin = margin
|
actionToolbarAdaptiveMarginContainerView.margin = margin
|
||||||
statusMetricViewAdaptiveMarginContainerView.margin = margin
|
statusMetricView.margin = margin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -744,6 +784,10 @@ extension StatusView: StatusMetricViewDelegate {
|
||||||
func statusMetricView(_ statusMetricView: StatusMetricView, favoriteButtonDidPressed button: UIButton) {
|
func statusMetricView(_ statusMetricView: StatusMetricView, favoriteButtonDidPressed button: UIButton) {
|
||||||
delegate?.statusView(self, statusMetricView: statusMetricView, favoriteButtonDidPressed: button)
|
delegate?.statusView(self, statusMetricView: statusMetricView, favoriteButtonDidPressed: button)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func statusMetricView(_ statusMetricView: StatusMetricView, didPressEditHistoryButton button: UIButton) {
|
||||||
|
delegate?.statusView(self, statusMetricView: statusMetricView, showEditHistory: button)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - MastodonMenuDelegate
|
// MARK: - MastodonMenuDelegate
|
||||||
|
|
|
@ -19,10 +19,18 @@ public enum MastodonMenu {
|
||||||
) -> UIMenu {
|
) -> UIMenu {
|
||||||
var children: [UIMenuElement] = []
|
var children: [UIMenuElement] = []
|
||||||
for action in actions {
|
for action in actions {
|
||||||
let element = action.build(delegate: delegate)
|
|
||||||
children.append(element.menuElement)
|
let element: UIMenuElement
|
||||||
|
|
||||||
|
if case let .deleteStatus = action {
|
||||||
|
let deleteAction = action.build(delegate: delegate).menuElement
|
||||||
|
element = UIMenu(options: .displayInline, children: [deleteAction])
|
||||||
|
} else {
|
||||||
|
element = action.build(delegate: delegate).menuElement
|
||||||
|
}
|
||||||
|
children.append(element)
|
||||||
}
|
}
|
||||||
return UIMenu(title: "", options: [], children: children)
|
return UIMenu(children: children)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func setupAccessibilityActions(
|
public static func setupAccessibilityActions(
|
||||||
|
@ -49,6 +57,7 @@ extension MastodonMenu {
|
||||||
case hideReblogs(HideReblogsActionContext)
|
case hideReblogs(HideReblogsActionContext)
|
||||||
case shareStatus
|
case shareStatus
|
||||||
case deleteStatus
|
case deleteStatus
|
||||||
|
case editStatus
|
||||||
|
|
||||||
func build(delegate: MastodonMenuDelegate) -> BuiltAction {
|
func build(delegate: MastodonMenuDelegate) -> BuiltAction {
|
||||||
switch self {
|
switch self {
|
||||||
|
@ -136,6 +145,14 @@ extension MastodonMenu {
|
||||||
delegate.menuAction(self)
|
delegate.menuAction(self)
|
||||||
}
|
}
|
||||||
return translateAction
|
return translateAction
|
||||||
|
case .editStatus:
|
||||||
|
let editStatusAction = BuiltAction(title: L10n.Common.Controls.Actions.editPost, image: UIImage(systemName: "pencil")) {
|
||||||
|
[weak delegate] in
|
||||||
|
guard let delegate else { return }
|
||||||
|
delegate.menuAction(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
return editStatusAction
|
||||||
} // end switch
|
} // end switch
|
||||||
} // end func build
|
} // end func build
|
||||||
} // end enum Action
|
} // end enum Action
|
||||||
|
|
|
@ -32,6 +32,7 @@ final class ShareViewController: UIViewController {
|
||||||
button.setTitle(L10n.Scene.Compose.composeAction, for: .normal)
|
button.setTitle(L10n.Scene.Compose.composeAction, for: .normal)
|
||||||
return button
|
return button
|
||||||
}()
|
}()
|
||||||
|
|
||||||
private func configurePublishButtonApperance() {
|
private func configurePublishButtonApperance() {
|
||||||
publishButton.adjustsImageWhenHighlighted = false
|
publishButton.adjustsImageWhenHighlighted = false
|
||||||
publishButton.setBackgroundImage(.placeholder(color: Asset.Colors.Label.primary.color), for: .normal)
|
publishButton.setBackgroundImage(.placeholder(color: Asset.Colors.Label.primary.color), for: .normal)
|
||||||
|
@ -99,6 +100,7 @@ extension ShareViewController {
|
||||||
let composeContentViewModel = ComposeContentViewModel(
|
let composeContentViewModel = ComposeContentViewModel(
|
||||||
context: context,
|
context: context,
|
||||||
authContext: authContext,
|
authContext: authContext,
|
||||||
|
composeContext: .composeStatus,
|
||||||
destination: .topLevel,
|
destination: .topLevel,
|
||||||
initialContent: ""
|
initialContent: ""
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue