Merge branch 'develop' into status-card-a11y
This commit is contained in:
commit
b6b7eed5e2
|
@ -132,5 +132,6 @@ env/**/**
|
|||
vendor/
|
||||
.bundle/
|
||||
|
||||
## IntelliJ IDEA ##
|
||||
.idea
|
||||
# IDEs
|
||||
.idea
|
||||
.vscode
|
||||
|
|
|
@ -620,7 +620,7 @@
|
|||
"intro": "These are the posts gaining traction in your corner of Mastodon."
|
||||
},
|
||||
"favorite": {
|
||||
"title": "Your Favorites"
|
||||
"title": "Favorites"
|
||||
},
|
||||
"notification": {
|
||||
"title": {
|
||||
|
|
|
@ -584,7 +584,7 @@
|
|||
"intro": "These are the posts gaining traction in your corner of Mastodon."
|
||||
},
|
||||
"favorite": {
|
||||
"title": "Your Favorites"
|
||||
"title": "Favorites"
|
||||
},
|
||||
"notification": {
|
||||
"title": {
|
||||
|
|
|
@ -584,7 +584,7 @@
|
|||
"intro": "These are the posts gaining traction in your corner of Mastodon."
|
||||
},
|
||||
"favorite": {
|
||||
"title": "Your Favorites"
|
||||
"title": "Favorites"
|
||||
},
|
||||
"notification": {
|
||||
"title": {
|
||||
|
|
|
@ -584,7 +584,7 @@
|
|||
"intro": "These are the posts gaining traction in your corner of Mastodon."
|
||||
},
|
||||
"favorite": {
|
||||
"title": "Your Favorites"
|
||||
"title": "Favorites"
|
||||
},
|
||||
"notification": {
|
||||
"title": {
|
||||
|
|
|
@ -584,7 +584,7 @@
|
|||
"intro": "These are the posts gaining traction in your corner of Mastodon."
|
||||
},
|
||||
"favorite": {
|
||||
"title": "Your Favorites"
|
||||
"title": "Favorites"
|
||||
},
|
||||
"notification": {
|
||||
"title": {
|
||||
|
|
|
@ -584,7 +584,7 @@
|
|||
"intro": "These are the posts gaining traction in your corner of Mastodon."
|
||||
},
|
||||
"favorite": {
|
||||
"title": "Your Favorites"
|
||||
"title": "Favorites"
|
||||
},
|
||||
"notification": {
|
||||
"title": {
|
||||
|
|
|
@ -584,7 +584,7 @@
|
|||
"intro": "These are the posts gaining traction in your corner of Mastodon."
|
||||
},
|
||||
"favorite": {
|
||||
"title": "Your Favorites"
|
||||
"title": "Favorites"
|
||||
},
|
||||
"notification": {
|
||||
"title": {
|
||||
|
|
|
@ -584,7 +584,7 @@
|
|||
"intro": "These are the posts gaining traction in your corner of Mastodon."
|
||||
},
|
||||
"favorite": {
|
||||
"title": "Your Favorites"
|
||||
"title": "Favorites"
|
||||
},
|
||||
"notification": {
|
||||
"title": {
|
||||
|
|
|
@ -584,7 +584,7 @@
|
|||
"intro": "These are the posts gaining traction in your corner of Mastodon."
|
||||
},
|
||||
"favorite": {
|
||||
"title": "Your Favorites"
|
||||
"title": "Favorites"
|
||||
},
|
||||
"notification": {
|
||||
"title": {
|
||||
|
|
|
@ -584,7 +584,7 @@
|
|||
"intro": "These are the posts gaining traction in your corner of Mastodon."
|
||||
},
|
||||
"favorite": {
|
||||
"title": "Your Favorites"
|
||||
"title": "Favorites"
|
||||
},
|
||||
"notification": {
|
||||
"title": {
|
||||
|
|
|
@ -101,7 +101,8 @@
|
|||
"translate_post": {
|
||||
"title": "Translate from %s",
|
||||
"unknown_language": "Unknown"
|
||||
}
|
||||
},
|
||||
"edit_post": "Edit"
|
||||
},
|
||||
"tabs": {
|
||||
"home": "Home",
|
||||
|
@ -193,7 +194,20 @@
|
|||
"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_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": {
|
||||
"follow": "Follow",
|
||||
|
@ -437,7 +451,8 @@
|
|||
"compose": {
|
||||
"title": {
|
||||
"new_post": "New Post",
|
||||
"new_reply": "New Reply"
|
||||
"new_reply": "New Reply",
|
||||
"edit_post": "Edit Post"
|
||||
},
|
||||
"media_selection": {
|
||||
"camera": "Take Photo",
|
||||
|
@ -641,7 +656,7 @@
|
|||
"intro": "These are the posts gaining traction in your corner of Mastodon."
|
||||
},
|
||||
"favorite": {
|
||||
"title": "Your Favorites"
|
||||
"title": "Favorites"
|
||||
},
|
||||
"notification": {
|
||||
"title": {
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
2A1FE47E2938C11200784BF1 /* Collection+IsNotEmpty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A1FE47D2938C11200784BF1 /* Collection+IsNotEmpty.swift */; };
|
||||
2A33062D2987DBFA001D4C51 /* FollowersCountHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A33062C2987DBFA001D4C51 /* FollowersCountHistory.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 */; };
|
||||
2A3F6FE5292F6E44002E6DA7 /* FollowedTagsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3F6FE4292F6E44002E6DA7 /* FollowedTagsTableViewCell.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 */; };
|
||||
9E44C7222967AD17004B2A72 /* MastodonSDKDynamic in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 9E44C71F2967AD17004B2A72 /* MastodonSDKDynamic */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
||||
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 */; };
|
||||
D809907A294BC9390050219F /* PrivacyTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8099079294BC9390050219F /* PrivacyTableViewCell.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 */; };
|
||||
D8A6FE5B293244B500666A47 /* WelcomeContentPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A6FE5A293244B500666A47 /* WelcomeContentPage.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, ); }; };
|
||||
DB0009A726AEE5DC009B9D2D /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = DB0009A926AEE5DC009B9D2D /* Intents.intentdefinition */; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -786,6 +794,8 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -1824,6 +1834,17 @@
|
|||
path = Localization;
|
||||
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 */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -2378,6 +2399,7 @@
|
|||
DB603110279EB38500A935FE /* DataSourceFacade+Mute.swift */,
|
||||
DB603112279EBEBA00A935FE /* DataSourceFacade+Block.swift */,
|
||||
DB0FCB7327956939006C02E2 /* DataSourceFacade+Status.swift */,
|
||||
D8E5C345296DAB84007E76A7 /* DataSourceFacade+Status+History.swift */,
|
||||
DB63F778279ABF9C00455B82 /* DataSourceFacade+Reblog.swift */,
|
||||
DB63F77A279ACAE500455B82 /* DataSourceFacade+Favorite.swift */,
|
||||
DB0FCB67279507EF006C02E2 /* DataSourceFacade+Meta.swift */,
|
||||
|
@ -2566,6 +2588,7 @@
|
|||
DB938EEB2623F52600E5B6C1 /* Thread */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D8E5C347296DB896007E76A7 /* Edit History */,
|
||||
DB938EE52623F50700E5B6C1 /* ThreadViewController.swift */,
|
||||
DB0FCB75279571C5006C02E2 /* ThreadViewController+DataSourceProvider.swift */,
|
||||
DB938EEC2623F79B00E5B6C1 /* ThreadViewModel.swift */,
|
||||
|
@ -3583,6 +3606,7 @@
|
|||
DBDFF1902805543100557A48 /* DiscoveryPostsViewController.swift in Sources */,
|
||||
DB697DD9278F4CED004EF2F7 /* HomeTimelineViewController+DataSourceProvider.swift in Sources */,
|
||||
DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */,
|
||||
D808B94E296EFBBA0031EB1E /* StatusEditHistoryTableViewCell.swift in Sources */,
|
||||
2D8434F525FF465D00EECE90 /* HomeTimelineNavigationBarTitleViewModel.swift in Sources */,
|
||||
DB938F0F2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift in Sources */,
|
||||
DB6180F226391CF40018D199 /* MediaPreviewImageViewModel.swift in Sources */,
|
||||
|
@ -3619,6 +3643,7 @@
|
|||
DB938F0926240F3C00E5B6C1 /* RemoteThreadViewModel.swift in Sources */,
|
||||
DB5B54A62833BE0000DEF8B2 /* UserListViewModel+State.swift in Sources */,
|
||||
DB0617ED277F02C50030EE79 /* OnboardingNavigationController.swift in Sources */,
|
||||
D808B94C296ECFDC0031EB1E /* StatusEditHistoryViewModel.swift in Sources */,
|
||||
DB0617F527855AB90030EE79 /* ServerRuleSection.swift in Sources */,
|
||||
DB75BF1E263C1C1B00EDBF1F /* CustomScheduler.swift in Sources */,
|
||||
0FAA102725E1126A0017CCDE /* MastodonPickServerViewController.swift in Sources */,
|
||||
|
@ -3685,6 +3710,7 @@
|
|||
2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */,
|
||||
2DAC9E38262FC2320062E1A6 /* SuggestionAccountViewController.swift in Sources */,
|
||||
DB6180E02639194B0018D199 /* MediaPreviewPagingViewController.swift in Sources */,
|
||||
D8E5C349296DB8A3007E76A7 /* StatusEditHistoryViewController.swift in Sources */,
|
||||
DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */,
|
||||
5B90C45E262599800002E742 /* SettingsViewModel.swift in Sources */,
|
||||
DB5B54A12833A89600DEF8B2 /* FamiliarFollowersViewController+DataSourceProvider.swift in Sources */,
|
||||
|
@ -3795,11 +3821,13 @@
|
|||
DB6D9F8426358EEC008423CD /* SettingsItem.swift in Sources */,
|
||||
2D364F7825E66D8300204FDC /* MastodonResendEmailViewModel.swift in Sources */,
|
||||
DBEFCD7B282A162400C0ABEA /* ReportReasonView.swift in Sources */,
|
||||
D8E5C346296DAB84007E76A7 /* DataSourceFacade+Status+History.swift in Sources */,
|
||||
DB8AF54525C13647002E6C99 /* NeedsDependency.swift in Sources */,
|
||||
DB63F77B279ACAE500455B82 /* DataSourceFacade+Favorite.swift in Sources */,
|
||||
DB9D6BF825E4F5690051B173 /* NotificationViewController.swift in Sources */,
|
||||
2DAC9E46262FC9FD0062E1A6 /* SuggestionAccountTableViewCell.swift in Sources */,
|
||||
DB4FFC2C269EC39600D62E92 /* SearchTransitionController.swift in Sources */,
|
||||
2A3D9B7E29A8F33A00F30313 /* StatusHistoryView.swift in Sources */,
|
||||
6213AF5E2893A8B200BCADB6 /* DataSourceFacade+Bookmark.swift in Sources */,
|
||||
DBA5E7A9263BD3A4004598BB /* ContextMenuImagePreviewViewController.swift in Sources */,
|
||||
DBF156E22702DA6900EC00B7 /* UIStatusBarManager+HandleTapAction.m in Sources */,
|
||||
|
@ -4572,7 +4600,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.4.11;
|
||||
MARKETING_VERSION = 1.5;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
@ -4604,7 +4632,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.4.11;
|
||||
MARKETING_VERSION = 1.5;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
@ -4791,7 +4819,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.4.11;
|
||||
MARKETING_VERSION = 1.5;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
@ -5082,7 +5110,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.4.11;
|
||||
MARKETING_VERSION = 1.5;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
|
|
@ -157,9 +157,11 @@ extension SceneCoordinator {
|
|||
|
||||
// compose
|
||||
case compose(viewModel: ComposeViewModel)
|
||||
case editStatus(viewModel: ComposeViewModel)
|
||||
|
||||
// thread
|
||||
case thread(viewModel: ThreadViewModel)
|
||||
case editHistory(viewModel: StatusEditHistoryViewModel)
|
||||
|
||||
// Hashtag Timeline
|
||||
case hashtagTimeline(viewModel: HashtagTimelineViewModel)
|
||||
|
@ -273,7 +275,7 @@ extension SceneCoordinator {
|
|||
|
||||
@MainActor
|
||||
@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 {
|
||||
return nil
|
||||
}
|
||||
|
@ -430,13 +432,15 @@ private extension SceneCoordinator {
|
|||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .compose(let viewModel):
|
||||
let _viewController = ComposeViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
let _viewController = ComposeViewController(viewModel: viewModel)
|
||||
viewController = _viewController
|
||||
case .thread(let viewModel):
|
||||
let _viewController = ThreadViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .editHistory(let viewModel):
|
||||
let editHistoryViewController = StatusEditHistoryViewController(viewModel: viewModel)
|
||||
viewController = editHistoryViewController
|
||||
case .hashtagTimeline(let viewModel):
|
||||
let _viewController = HashtagTimelineViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
|
@ -536,6 +540,9 @@ private extension SceneCoordinator {
|
|||
let _viewController = SettingsViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .editStatus(let viewModel):
|
||||
let composeViewController = ComposeViewController(viewModel: viewModel)
|
||||
viewController = composeViewController
|
||||
}
|
||||
|
||||
setupDependency(for: viewController as? NeedsDependency)
|
||||
|
|
|
@ -167,6 +167,8 @@ extension StatusSection {
|
|||
let managedObjectContext = context.managedObjectContext
|
||||
statusView.pollTableViewDiffableDataSource = UITableViewDiffableDataSource<PollSection, PollItem>(tableView: statusView.pollTableView) { tableView, indexPath, item in
|
||||
switch item {
|
||||
case .history:
|
||||
return nil
|
||||
case .option(let record):
|
||||
// Fix cell reuse animation issue
|
||||
let cell: PollOptionTableViewCell = {
|
||||
|
@ -188,9 +190,11 @@ extension StatusSection {
|
|||
// trigger update if needs
|
||||
let needsUpdatePoll: Bool = {
|
||||
// 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 {
|
||||
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
|
||||
|
@ -213,7 +217,8 @@ extension StatusSection {
|
|||
}()
|
||||
|
||||
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
|
||||
guard let context = context else { return }
|
||||
_ = 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 {
|
||||
|
||||
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(
|
||||
context: provider.context,
|
||||
authContext: provider.authContext,
|
||||
composeContext: .composeStatus,
|
||||
destination: .reply(parent: status)
|
||||
)
|
||||
_ = provider.coordinator.present(
|
||||
|
@ -373,6 +374,21 @@ extension DataSourceFacade {
|
|||
alertController.addAction(UIAlertAction(title: L10n.Common.Alerts.TranslationFailed.button, style: .default))
|
||||
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
|
||||
}
|
||||
|
|
|
@ -522,6 +522,7 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut
|
|||
Task {
|
||||
let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil)
|
||||
guard let item = await item(from: source) else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
switch item {
|
||||
|
|
|
@ -233,6 +233,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte
|
|||
scene: .compose(viewModel: ComposeViewModel(
|
||||
context: self.context,
|
||||
authContext: self.authContext,
|
||||
composeContext: .composeStatus,
|
||||
destination: .topLevel,
|
||||
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 {
|
||||
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)
|
||||
|
||||
_isMultiple = poll.multiple
|
||||
|
@ -357,8 +358,10 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte
|
|||
|
||||
// restore voting state
|
||||
try await managedObjectContext.performChanges {
|
||||
guard let pollOption = pollOption.object(in: managedObjectContext) else { return }
|
||||
let poll = pollOption.poll
|
||||
guard
|
||||
let pollOption = pollOption.object(in: managedObjectContext),
|
||||
let poll = pollOption.poll
|
||||
else { return }
|
||||
poll.update(isVoting: false)
|
||||
}
|
||||
}
|
||||
|
@ -483,9 +486,14 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte
|
|||
return
|
||||
}
|
||||
|
||||
if let cell = cell as? StatusTableViewCell {
|
||||
if case .translateStatus = action {
|
||||
DispatchQueue.main.async {
|
||||
cell.statusView.viewModel.isCurrentlyTranslating = true
|
||||
if let cell = cell as? StatusTableViewCell {
|
||||
cell.statusView.viewModel.isCurrentlyTranslating = true
|
||||
} else if let cell = cell as? StatusThreadRootTableViewCell {
|
||||
cell.statusView.viewModel.isCurrentlyTranslating = true
|
||||
}
|
||||
cell.invalidateIntrinsicContentSize()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -647,6 +655,24 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte
|
|||
)
|
||||
} // 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
|
||||
|
@ -655,6 +681,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte
|
|||
Task {
|
||||
let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil)
|
||||
guard let item = await item(from: source) else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
switch item {
|
||||
|
|
|
@ -100,6 +100,7 @@ extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvid
|
|||
let composeViewModel = ComposeViewModel(
|
||||
context: self.context,
|
||||
authContext: authContext,
|
||||
composeContext: .composeStatus,
|
||||
destination: .reply(parent: status)
|
||||
)
|
||||
_ = self.coordinator.present(
|
||||
|
|
|
@ -193,7 +193,7 @@ uploaded to Mastodon.";
|
|||
"Scene.ConfirmEmail.Subtitle" = "We just sent an email to %@,
|
||||
tap the link to confirm your account.";
|
||||
"Scene.ConfirmEmail.Title" = "One last thing.";
|
||||
"Scene.Favorite.Title" = "Your Favorites";
|
||||
"Scene.Favorite.Title" = "Favorites";
|
||||
"Scene.Hashtag.Prompt" = "%@ people talking";
|
||||
"Scene.HomeTimeline.NavigationBarState.NewPosts" = "See new posts";
|
||||
"Scene.HomeTimeline.NavigationBarState.Offline" = "Offline";
|
||||
|
|
|
@ -19,7 +19,6 @@ import MastodonLocalization
|
|||
import MastodonSDK
|
||||
|
||||
final class ComposeViewController: UIViewController, NeedsDependency {
|
||||
|
||||
static let minAutoCompleteVisibleHeight: CGFloat = 100
|
||||
|
||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||
|
@ -29,13 +28,34 @@ final class ComposeViewController: UIViewController, NeedsDependency {
|
|||
var viewModel: ComposeViewModel!
|
||||
|
||||
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 = {
|
||||
|
||||
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(
|
||||
context: context,
|
||||
authContext: viewModel.authContext,
|
||||
composeContext: composeContext,
|
||||
destination: viewModel.destination,
|
||||
initialContent: viewModel.initialContent
|
||||
initialContent: initialContent
|
||||
)
|
||||
}()
|
||||
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(_:)))
|
||||
|
||||
let publishButton: UIButton = {
|
||||
private lazy var publishButton: 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.Scene.Compose.composeAction, for: .normal)
|
||||
button.addTarget(self, action: #selector(ComposeViewController.publishBarButtonItemPressed(_:)), for: .touchUpInside)
|
||||
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 = {
|
||||
configurePublishButtonApperance()
|
||||
configurePublishButtonApperance(button: publishButton)
|
||||
let shadowBackgroundContainer = ShadowBackgroundContainer()
|
||||
publishButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
shadowBackgroundContainer.addSubview(publishButton)
|
||||
|
@ -63,12 +105,13 @@ final class ComposeViewController: UIViewController, NeedsDependency {
|
|||
let barButtonItem = UIBarButtonItem(customView: shadowBackgroundContainer)
|
||||
return barButtonItem
|
||||
}()
|
||||
private func configurePublishButtonApperance() {
|
||||
publishButton.adjustsImageWhenHighlighted = false
|
||||
publishButton.setBackgroundImage(.placeholder(color: Asset.Colors.Label.primary.color), for: .normal)
|
||||
publishButton.setBackgroundImage(.placeholder(color: Asset.Colors.Label.primary.color.withAlphaComponent(0.5)), for: .highlighted)
|
||||
publishButton.setBackgroundImage(.placeholder(color: Asset.Colors.Button.disabled.color), for: .disabled)
|
||||
publishButton.setTitleColor(Asset.Colors.Label.primaryReverse.color, for: .normal)
|
||||
|
||||
private func configurePublishButtonApperance(button: UIButton) {
|
||||
button.adjustsImageWhenHighlighted = false
|
||||
button.setBackgroundImage(.placeholder(color: Asset.Colors.Label.primary.color), for: .normal)
|
||||
button.setBackgroundImage(.placeholder(color: Asset.Colors.Label.primary.color.withAlphaComponent(0.5)), for: .highlighted)
|
||||
button.setBackgroundImage(.placeholder(color: Asset.Colors.Button.disabled.color), for: .disabled)
|
||||
button.setTitleColor(Asset.Colors.Label.primaryReverse.color, for: .normal)
|
||||
}
|
||||
|
||||
deinit {
|
||||
|
@ -83,18 +126,17 @@ extension ComposeViewController {
|
|||
super.viewDidLoad()
|
||||
|
||||
navigationItem.leftBarButtonItem = cancelBarButtonItem
|
||||
navigationItem.rightBarButtonItem = publishBarButtonItem
|
||||
viewModel.traitCollectionDidChangePublisher
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
guard self.traitCollection.userInterfaceIdiom == .pad else { return }
|
||||
let items = [self.publishBarButtonItem]
|
||||
self.navigationItem.rightBarButtonItems = items
|
||||
self.navigationItem.rightBarButtonItem = self.rightBarButtonItemForCurrentContext
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
publishButton.addTarget(self, action: #selector(ComposeViewController.publishBarButtonItemPressed(_:)), for: .touchUpInside)
|
||||
|
||||
|
||||
navigationItem.rightBarButtonItem = rightBarButtonItemForCurrentContext
|
||||
|
||||
addChild(composeContentViewController)
|
||||
composeContentViewController.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(composeContentViewController.view)
|
||||
|
@ -119,8 +161,14 @@ extension ComposeViewController {
|
|||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
|
||||
configurePublishButtonApperance()
|
||||
|
||||
switch viewModel.composeContext {
|
||||
case .composeStatus:
|
||||
configurePublishButtonApperance(button: publishButton)
|
||||
case .editStatus:
|
||||
configurePublishButtonApperance(button: saveButton)
|
||||
}
|
||||
|
||||
viewModel.traitCollectionDidChangePublisher.send()
|
||||
}
|
||||
|
||||
|
@ -141,6 +189,14 @@ extension ComposeViewController {
|
|||
present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
private var rightBarButtonItemForCurrentContext: UIBarButtonItem {
|
||||
switch viewModel.composeContext {
|
||||
case .composeStatus:
|
||||
return publishBarButtonItem
|
||||
case .editStatus:
|
||||
return saveBarButtonItem
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ComposeViewController {
|
||||
|
@ -155,8 +211,7 @@ extension ComposeViewController {
|
|||
}
|
||||
|
||||
@objc private func publishBarButtonItemPressed(_ sender: UIBarButtonItem) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
|
||||
|
||||
do {
|
||||
try composeContentViewModel.checkAttachmentPrecondition()
|
||||
} catch {
|
||||
|
@ -185,7 +240,34 @@ extension ComposeViewController {
|
|||
|
||||
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 {
|
||||
|
|
|
@ -19,7 +19,12 @@ import MastodonMeta
|
|||
import MastodonUI
|
||||
|
||||
final class ComposeViewModel {
|
||||
|
||||
|
||||
enum Context {
|
||||
case composeStatus
|
||||
case editStatus(status: Status, statusSource: Mastodon.Entity.StatusSource)
|
||||
}
|
||||
|
||||
let logger = Logger(subsystem: "ComposeViewModel", category: "ViewModel")
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
@ -29,6 +34,7 @@ final class ComposeViewModel {
|
|||
// input
|
||||
let context: AppContext
|
||||
let authContext: AuthContext
|
||||
let composeContext: Context
|
||||
let destination: ComposeContentViewModel.Destination
|
||||
let initialContent: String
|
||||
|
||||
|
@ -42,6 +48,7 @@ final class ComposeViewModel {
|
|||
init(
|
||||
context: AppContext,
|
||||
authContext: AuthContext,
|
||||
composeContext: ComposeViewModel.Context,
|
||||
destination: ComposeContentViewModel.Destination,
|
||||
initialContent: String = ""
|
||||
) {
|
||||
|
@ -49,18 +56,23 @@ final class ComposeViewModel {
|
|||
self.authContext = authContext
|
||||
self.destination = destination
|
||||
self.initialContent = initialContent
|
||||
self.composeContext = composeContext
|
||||
// end init
|
||||
|
||||
self.title = {
|
||||
let title: String
|
||||
|
||||
switch composeContext {
|
||||
case .composeStatus:
|
||||
switch destination {
|
||||
case .topLevel: return L10n.Scene.Compose.Title.newPost
|
||||
case .reply: return L10n.Scene.Compose.Title.newReply
|
||||
case .topLevel:
|
||||
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(
|
||||
context: context,
|
||||
authContext: viewModel.authContext,
|
||||
composeContext: .composeStatus,
|
||||
destination: .topLevel,
|
||||
initialContent: hashtag
|
||||
)
|
||||
|
|
|
@ -200,6 +200,12 @@ extension HomeTimelineViewController {
|
|||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
context.publisherService.statusPublishResult.sink { result in
|
||||
if case .success(.edit) = result {
|
||||
self.viewModel.hasPendingStatusEditReload = true
|
||||
}
|
||||
}.store(in: &disposeBag)
|
||||
|
||||
context.publisherService.$currentPublishProgress
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] progress in
|
||||
|
|
|
@ -91,7 +91,7 @@ extension HomeTimelineViewModel {
|
|||
}
|
||||
|
||||
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.didLoadLatest.send()
|
||||
return
|
||||
|
@ -117,6 +117,7 @@ extension HomeTimelineViewModel {
|
|||
tableView.setContentOffset(contentOffset, animated: false)
|
||||
self.didLoadLatest.send()
|
||||
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
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
|
|
@ -51,7 +51,7 @@ extension HomeTimelineViewModel {
|
|||
extension HomeTimelineViewModel.LoadLatestState {
|
||||
class Initial: HomeTimelineViewModel.LoadLatestState {
|
||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||
return stateClass == Loading.self
|
||||
return stateClass == Loading.self || stateClass == LoadingManually.self
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -83,7 +83,7 @@ extension HomeTimelineViewModel.LoadLatestState {
|
|||
|
||||
class Idle: HomeTimelineViewModel.LoadLatestState {
|
||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||
return stateClass == Loading.self
|
||||
return stateClass == Loading.self || stateClass == LoadingManually.self
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -35,6 +35,7 @@ final class HomeTimelineViewModel: NSObject {
|
|||
@Published var lastAutomaticFetchTimestamp: Date? = nil
|
||||
@Published var scrollPositionRecord: ScrollPositionRecord? = nil
|
||||
@Published var displaySettingBarButtonItem = true
|
||||
@Published var hasPendingStatusEditReload = false
|
||||
|
||||
weak var tableView: UITableView?
|
||||
weak var timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate?
|
||||
|
|
|
@ -126,45 +126,20 @@ extension MastodonPickServerViewModel {
|
|||
}
|
||||
)
|
||||
.map { indexedServers, selectCategoryItem, searchText, filters -> [Mastodon.Entity.Server] in
|
||||
// ignore approval required servers when sign-up
|
||||
var indexedServers = indexedServers
|
||||
// Note:
|
||||
// sort by calculate last week users count
|
||||
// and make medium size (~800) server to top
|
||||
|
||||
// group by language user preferred language first
|
||||
var languageToServersMapping = OrderedDictionary<String, [Mastodon.Entity.Server]>()
|
||||
for language in Locale.preferredLanguages {
|
||||
let local = Locale(identifier: language)
|
||||
guard let languageCode = local.languageCode else { continue }
|
||||
// skip if key duplicate
|
||||
guard !languageToServersMapping.keys.contains(languageCode) else { continue }
|
||||
// append to dict
|
||||
languageToServersMapping[languageCode] = indexedServers
|
||||
.filter { $0.language.lowercased() == languageCode.lowercased() }
|
||||
.sorted(by: { lh, rh in
|
||||
let lhValue = abs(log2(800.0) - log2(Double(lh.lastWeekUsers)))
|
||||
let rhValue = abs(log2(800.0) - log2(Double(rh.lastWeekUsers)))
|
||||
return lhValue < rhValue
|
||||
})
|
||||
}
|
||||
// sort remains servers
|
||||
let remainsServers = indexedServers
|
||||
.filter { server in
|
||||
return !languageToServersMapping.contains { _, servers in servers.contains(server) }
|
||||
}
|
||||
.sorted(by: { lh, rh in
|
||||
let lhValue = abs(log2(800.0) - log2(Double(lh.lastWeekUsers)))
|
||||
let rhValue = abs(log2(800.0) - log2(Double(rh.lastWeekUsers)))
|
||||
return lhValue < rhValue
|
||||
})
|
||||
|
||||
|
||||
var _indexedServers: [Mastodon.Entity.Server] = []
|
||||
for key in languageToServersMapping.keys {
|
||||
_indexedServers.append(contentsOf: languageToServersMapping[key] ?? [])
|
||||
}
|
||||
_indexedServers.append(contentsOf: remainsServers)
|
||||
|
||||
|
||||
let sortedInstantSignupServers = indexedServers
|
||||
.filter { $0.approvalRequired == false }
|
||||
.sorted { $0.lastWeekUsers >= $1.lastWeekUsers }
|
||||
let sortedApprovalRequiredServers = indexedServers
|
||||
.filter { $0.approvalRequired }
|
||||
.sorted { $0.lastWeekUsers >= $1.lastWeekUsers }
|
||||
|
||||
_indexedServers.append(contentsOf: sortedInstantSignupServers)
|
||||
_indexedServers.append(contentsOf: sortedApprovalRequiredServers)
|
||||
|
||||
if _indexedServers.count == indexedServers.count {
|
||||
indexedServers = _indexedServers
|
||||
} else {
|
||||
|
|
|
@ -558,6 +558,7 @@ extension ProfileViewController {
|
|||
let composeViewModel = ComposeViewModel(
|
||||
context: context,
|
||||
authContext: viewModel.authContext,
|
||||
composeContext: .composeStatus,
|
||||
destination: .topLevel,
|
||||
initialContent: mention
|
||||
)
|
||||
|
|
|
@ -386,9 +386,10 @@ extension MainTabBarController {
|
|||
let composeViewModel = ComposeViewModel(
|
||||
context: context,
|
||||
authContext: authContext,
|
||||
composeContext: .composeStatus,
|
||||
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? {
|
||||
|
@ -815,6 +816,7 @@ extension MainTabBarController {
|
|||
let composeViewModel = ComposeViewModel(
|
||||
context: context,
|
||||
authContext: authContext,
|
||||
composeContext: .composeStatus,
|
||||
destination: .topLevel
|
||||
)
|
||||
_ = coordinator.present(scene: .compose(viewModel: composeViewModel), from: nil, transition: .modal(animated: true, completion: nil))
|
||||
|
|
|
@ -227,6 +227,7 @@ extension SidebarViewController: UICollectionViewDelegate {
|
|||
let composeViewModel = ComposeViewModel(
|
||||
context: context,
|
||||
authContext: authContext,
|
||||
composeContext: .composeStatus,
|
||||
destination: .topLevel
|
||||
)
|
||||
_ = coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil))
|
||||
|
|
|
@ -14,6 +14,11 @@ import MastodonUI
|
|||
|
||||
extension PollOptionView {
|
||||
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)
|
||||
|
||||
// background
|
||||
|
@ -33,7 +38,7 @@ extension PollOptionView {
|
|||
.store(in: &disposeBag)
|
||||
// percentage
|
||||
Publishers.CombineLatest(
|
||||
option.poll.publisher(for: \.votersCount),
|
||||
poll.publisher(for: \.votersCount),
|
||||
option.publisher(for: \.votesCount)
|
||||
)
|
||||
.map { pollVotersCount, optionVotesCount -> Double? in
|
||||
|
@ -43,15 +48,15 @@ extension PollOptionView {
|
|||
.assign(to: \.percentage, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
// $isExpire
|
||||
option.poll.publisher(for: \.expired)
|
||||
poll.publisher(for: \.expired)
|
||||
.assign(to: \.isExpire, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
// isMultiple
|
||||
viewModel.isMultiple = option.poll.multiple
|
||||
viewModel.isMultiple = poll.multiple
|
||||
|
||||
let optionIndex = option.index
|
||||
let authorDomain = option.poll.status.author.domain
|
||||
let authorID = option.poll.status.author.id
|
||||
let authorDomain = status.author.domain
|
||||
let authorID = status.author.id
|
||||
// isSelect, isPollVoted, isMyPoll
|
||||
Publishers.CombineLatest4(
|
||||
option.publisher(for: \.poll),
|
||||
|
@ -60,7 +65,7 @@ extension PollOptionView {
|
|||
viewModel.$authContext
|
||||
)
|
||||
.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 userID = authContext?.mastodonAuthenticationBox.userID ?? ""
|
||||
|
@ -109,3 +114,30 @@ extension PollOptionView {
|
|||
.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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -86,7 +86,16 @@ extension StatusTableViewCell {
|
|||
self.accessibilityLabel = accessibilityLabel
|
||||
}
|
||||
.store(in: &_disposeBag)
|
||||
|
||||
|
||||
statusView.viewModel.$contentAccessibilityLabel
|
||||
.combineLatest(statusView.viewModel.$groupedAccessibilityLabel)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] contentLabel, accessibilityLabel in
|
||||
guard let self = self else { return }
|
||||
self.accessibilityUserInputLabels = [contentLabel, accessibilityLabel]
|
||||
}
|
||||
.store(in: &_disposeBag)
|
||||
|
||||
statusView.viewModel
|
||||
.$translatedFromLanguage
|
||||
.receive(on: DispatchQueue.main)
|
||||
|
|
|
@ -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, statusMetricView: StatusMetricView, reblogButtonDidPressed 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, cardControlMenu: StatusCardControl) -> [LabeledAction]?
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
delegate?.tableViewCell(self, statusView: statusView, cardControl: cardControl, didTapURL: url)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -24,7 +24,6 @@ extension ThreadViewController: DataSourceProvider {
|
|||
case .thread(let thread):
|
||||
return .status(record: thread.record)
|
||||
default:
|
||||
assertionFailure()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
|
@ -117,6 +117,7 @@ extension ThreadViewController {
|
|||
let composeViewModel = ComposeViewModel(
|
||||
context: context,
|
||||
authContext: viewModel.authContext,
|
||||
composeContext: .composeStatus,
|
||||
destination: .reply(parent: threadContext.status)
|
||||
)
|
||||
_ = coordinator.present(
|
||||
|
|
|
@ -115,7 +115,7 @@ extension ThreadViewModel {
|
|||
}
|
||||
|
||||
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")
|
||||
return
|
||||
} else {
|
||||
|
@ -140,6 +140,7 @@ extension ThreadViewModel {
|
|||
newSnapshot: newSnapshot,
|
||||
difference: difference
|
||||
)
|
||||
self.hasPendingStatusEditReload = false
|
||||
} // end Task
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
|
|
@ -66,7 +66,7 @@ extension ThreadViewModel.LoadThreadState {
|
|||
override func didEnter(from previousState: GKState?) {
|
||||
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 {
|
||||
stateMachine.enter(Fail.self)
|
||||
|
@ -79,11 +79,14 @@ extension ThreadViewModel.LoadThreadState {
|
|||
statusID: threadContext.statusID,
|
||||
authenticationBox: viewModel.authContext.mastodonAuthenticationBox
|
||||
)
|
||||
|
||||
|
||||
await enter(state: NoMore.self)
|
||||
|
||||
// assert(!Thread.isMainThread)
|
||||
// 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(
|
||||
domain: threadContext.domain,
|
||||
|
|
|
@ -33,6 +33,7 @@ class ThreadViewModel {
|
|||
var diffableDataSource: UITableViewDiffableDataSource<StatusSection, StatusItem>?
|
||||
@Published var root: StatusItem.Thread?
|
||||
@Published var threadContext: ThreadContext?
|
||||
@Published var hasPendingStatusEditReload = false
|
||||
|
||||
private(set) lazy var loadThreadStateMachine: GKStateMachine = {
|
||||
let stateMachine = GKStateMachine(states: [
|
||||
|
@ -95,6 +96,15 @@ class ThreadViewModel {
|
|||
}()
|
||||
}
|
||||
.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 {
|
||||
|
|
|
@ -226,6 +226,7 @@ extension SceneDelegate {
|
|||
let composeViewModel = ComposeViewModel(
|
||||
context: AppContext.shared,
|
||||
authContext: authContext,
|
||||
composeContext: .composeStatus,
|
||||
destination: .topLevel
|
||||
)
|
||||
_ = coordinator?.present(scene: .compose(viewModel: composeViewModel), from: nil, transition: .modal(animated: true, completion: nil))
|
||||
|
|
|
@ -72,15 +72,6 @@
|
|||
"version" : "4.2.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "kingfisher",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/onevcat/Kingfisher.git",
|
||||
"state" : {
|
||||
"revision" : "44e891bdb61426a95e31492a67c7c0dfad1f87c5",
|
||||
"version" : "7.4.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "metatextkit",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
@ -216,15 +207,6 @@
|
|||
"version" : "2.13.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "thirdpartymailer",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/vtourraine/ThirdPartyMailer.git",
|
||||
"state" : {
|
||||
"revision" : "44c1cfaa6969963f22691aa67f88a69e3b6d651f",
|
||||
"version" : "2.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "tocropviewcontroller",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
|
|
@ -3,6 +3,6 @@
|
|||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>_XCCurrentVersionName</key>
|
||||
<string>CoreData 7.xcdatamodel</string>
|
||||
<string>CoreData 8.xcdatamodel</string>
|
||||
</dict>
|
||||
</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"?>
|
||||
<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">
|
||||
<attribute name="identifier" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
|
||||
<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="statuses" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="tags" inverseEntity="Status"/>
|
||||
</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>
|
|
@ -8,7 +8,7 @@
|
|||
import os
|
||||
import Foundation
|
||||
import Combine
|
||||
import CoreData
|
||||
@_exported import CoreData
|
||||
import MastodonCommon
|
||||
|
||||
public final class CoreDataStack {
|
||||
|
|
|
@ -38,7 +38,7 @@ public final class Poll: NSManagedObject {
|
|||
@NSManaged public private(set) var isVoting: Bool
|
||||
|
||||
// one-to-one relationship
|
||||
@NSManaged public private(set) var status: Status
|
||||
@NSManaged public private(set) var status: Status?
|
||||
|
||||
// one-to-many relationship
|
||||
@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
|
||||
|
||||
// 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
|
||||
@NSManaged public private(set) var votedBy: Set<MastodonUser>?
|
||||
|
@ -125,19 +126,22 @@ extension PollOption: AutoGenerateProperty {
|
|||
public let votesCount: Int64
|
||||
public let createdAt: Date
|
||||
public let updatedAt: Date
|
||||
public let poll: Poll?
|
||||
|
||||
public init(
|
||||
index: Int64,
|
||||
title: String,
|
||||
votesCount: Int64,
|
||||
createdAt: Date,
|
||||
updatedAt: Date
|
||||
updatedAt: Date,
|
||||
poll: Poll?
|
||||
) {
|
||||
self.index = index
|
||||
self.title = title
|
||||
self.votesCount = votesCount
|
||||
self.createdAt = createdAt
|
||||
self.updatedAt = updatedAt
|
||||
self.poll = poll
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -147,12 +151,14 @@ extension PollOption: AutoGenerateProperty {
|
|||
self.votesCount = property.votesCount
|
||||
self.createdAt = property.createdAt
|
||||
self.updatedAt = property.updatedAt
|
||||
self.poll = property.poll
|
||||
}
|
||||
|
||||
public func update(property: Property) {
|
||||
update(title: property.title)
|
||||
update(votesCount: property.votesCount)
|
||||
update(updatedAt: property.updatedAt)
|
||||
update(poll: property.poll)
|
||||
}
|
||||
// sourcery:end
|
||||
}
|
||||
|
@ -183,6 +189,11 @@ extension PollOption: AutoUpdatableObject {
|
|||
self.isSelected = isSelected
|
||||
}
|
||||
}
|
||||
public func update(poll: Poll?) {
|
||||
if self.poll != poll {
|
||||
self.poll = poll
|
||||
}
|
||||
}
|
||||
// sourcery:end
|
||||
|
||||
public func update(voted: Bool, by: MastodonUser) {
|
||||
|
|
|
@ -32,6 +32,10 @@ public final class Status: NSManagedObject {
|
|||
|
||||
// sourcery: autoUpdatableObject, autoGenerateProperty
|
||||
@NSManaged public private(set) var createdAt: Date
|
||||
|
||||
// sourcery: autoUpdatableObject, autoGenerateProperty
|
||||
@NSManaged public private(set) var editedAt: Date?
|
||||
|
||||
// sourcery: autoUpdatableObject, autoGenerateProperty
|
||||
@NSManaged public private(set) var content: String
|
||||
|
||||
|
@ -53,7 +57,8 @@ public final class Status: NSManagedObject {
|
|||
|
||||
// sourcery: autoUpdatableObject
|
||||
@NSManaged public private(set) var isSensitiveToggled: Bool
|
||||
|
||||
|
||||
// sourcery: autoGenerateRelationship
|
||||
@NSManaged public private(set) var application: Application?
|
||||
|
||||
// Informational
|
||||
|
@ -104,6 +109,8 @@ public final class Status: NSManagedObject {
|
|||
@NSManaged public private(set) var replyFrom: Set<Status>
|
||||
@NSManaged public private(set) var notifications: Set<Notification>
|
||||
@NSManaged public private(set) var searchHistories: Set<SearchHistory>
|
||||
|
||||
@NSManaged public private(set) var editHistory: Set<StatusEdit>?
|
||||
|
||||
// sourcery: autoUpdatableObject, autoGenerateProperty
|
||||
@NSManaged public private(set) var updatedAt: Date
|
||||
|
@ -176,8 +183,8 @@ extension Status {
|
|||
didAccessValue(forKey: keyPath)
|
||||
do {
|
||||
guard let data = _data else { return [] }
|
||||
let emojis = try JSONDecoder().decode([MastodonMention].self, from: data)
|
||||
return emojis
|
||||
let mentions = try JSONDecoder().decode([MastodonMention].self, from: data)
|
||||
return mentions
|
||||
} catch {
|
||||
assertionFailure(error.localizedDescription)
|
||||
return []
|
||||
|
@ -269,6 +276,7 @@ extension Status: AutoGenerateProperty {
|
|||
public let id: String
|
||||
public let uri: String
|
||||
public let createdAt: Date
|
||||
public let editedAt: Date?
|
||||
public let content: String
|
||||
public let visibility: MastodonVisibility
|
||||
public let sensitive: Bool
|
||||
|
@ -293,6 +301,7 @@ extension Status: AutoGenerateProperty {
|
|||
id: String,
|
||||
uri: String,
|
||||
createdAt: Date,
|
||||
editedAt: Date?,
|
||||
content: String,
|
||||
visibility: MastodonVisibility,
|
||||
sensitive: Bool,
|
||||
|
@ -316,6 +325,7 @@ extension Status: AutoGenerateProperty {
|
|||
self.id = id
|
||||
self.uri = uri
|
||||
self.createdAt = createdAt
|
||||
self.editedAt = editedAt
|
||||
self.content = content
|
||||
self.visibility = visibility
|
||||
self.sensitive = sensitive
|
||||
|
@ -342,6 +352,7 @@ extension Status: AutoGenerateProperty {
|
|||
self.id = property.id
|
||||
self.uri = property.uri
|
||||
self.createdAt = property.createdAt
|
||||
self.editedAt = property.editedAt
|
||||
self.content = property.content
|
||||
self.visibility = property.visibility
|
||||
self.sensitive = property.sensitive
|
||||
|
@ -363,6 +374,7 @@ extension Status: AutoGenerateProperty {
|
|||
|
||||
public func update(property: Property) {
|
||||
update(createdAt: property.createdAt)
|
||||
update(editedAt: property.editedAt)
|
||||
update(content: property.content)
|
||||
update(visibility: property.visibility)
|
||||
update(sensitive: property.sensitive)
|
||||
|
@ -391,17 +403,20 @@ extension Status: AutoGenerateRelationship {
|
|||
// Generated using Sourcery
|
||||
// DO NOT EDIT
|
||||
public struct Relationship {
|
||||
public let application: Application?
|
||||
public let author: MastodonUser
|
||||
public let reblog: Status?
|
||||
public let poll: Poll?
|
||||
public let card: Card?
|
||||
|
||||
public init(
|
||||
application: Application?,
|
||||
author: MastodonUser,
|
||||
reblog: Status?,
|
||||
poll: Poll?,
|
||||
card: Card?
|
||||
) {
|
||||
self.application = application
|
||||
self.author = author
|
||||
self.reblog = reblog
|
||||
self.poll = poll
|
||||
|
@ -410,6 +425,7 @@ extension Status: AutoGenerateRelationship {
|
|||
}
|
||||
|
||||
public func configure(relationship: Relationship) {
|
||||
self.application = relationship.application
|
||||
self.author = relationship.author
|
||||
self.reblog = relationship.reblog
|
||||
self.poll = relationship.poll
|
||||
|
@ -429,6 +445,11 @@ extension Status: AutoUpdatableObject {
|
|||
self.createdAt = createdAt
|
||||
}
|
||||
}
|
||||
public func update(editedAt: Date?) {
|
||||
if self.editedAt != editedAt {
|
||||
self.editedAt = editedAt
|
||||
}
|
||||
}
|
||||
public func update(content: String) {
|
||||
if self.content != content {
|
||||
self.content = content
|
||||
|
@ -587,6 +608,10 @@ extension Status: AutoUpdatableObject {
|
|||
public func update(isReveal: Bool) {
|
||||
revealedAt = isReveal ? Date() : nil
|
||||
}
|
||||
|
||||
public func update(editHistory: Set<StatusEdit>) {
|
||||
self.editHistory = editHistory
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
},
|
||||
{
|
||||
"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" : {
|
||||
|
|
|
@ -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 inactive = ColorAsset(name: "Colors/inactive")
|
||||
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 systemOrange = ColorAsset(name: "Colors/system.orange")
|
||||
}
|
||||
|
@ -164,6 +165,11 @@ public enum Asset {
|
|||
public enum Discovery {
|
||||
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 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")
|
||||
|
|
|
@ -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 {
|
||||
public init(
|
||||
poll: Poll,
|
||||
index: Int,
|
||||
entity: Mastodon.Entity.Poll.Option,
|
||||
networkDate: Date
|
||||
|
@ -20,7 +21,8 @@ extension PollOption.Property {
|
|||
title: entity.title,
|
||||
votesCount: Int64(entity.votesCount ?? 0),
|
||||
createdAt: networkDate,
|
||||
updatedAt: networkDate
|
||||
updatedAt: networkDate,
|
||||
poll: poll
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ extension Status.Property {
|
|||
id: entity.id,
|
||||
uri: entity.uri,
|
||||
createdAt: entity.createdAt,
|
||||
editedAt: entity.editedAt,
|
||||
content: entity.content ?? "",
|
||||
visibility: entity.mastodonVisibility,
|
||||
sensitive: entity.sensitive ?? false,
|
||||
|
@ -48,46 +49,7 @@ extension Mastodon.Entity.Status {
|
|||
|
||||
extension Mastodon.Entity.Status {
|
||||
public var mastodonAttachments: [MastodonAttachment] {
|
||||
guard let mediaAttachments = mediaAttachments 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
|
||||
mediaAttachments.mastodonAttachments
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
case option(record: ManagedObjectRecord<PollOption>)
|
||||
case history(option: StatusEdit.Poll.Option)
|
||||
}
|
||||
|
|
|
@ -59,30 +59,16 @@ extension Persistence.Poll {
|
|||
) -> PersistResult {
|
||||
|
||||
if let old = fetch(in: managedObjectContext, context: context) {
|
||||
merge(poll: old, context: context)
|
||||
merge(in: managedObjectContext, poll: old, context: context)
|
||||
return PersistResult(
|
||||
poll: old,
|
||||
isNewInsertion: false
|
||||
)
|
||||
} 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(
|
||||
in: managedObjectContext,
|
||||
context: context
|
||||
)
|
||||
poll.attach(options: options)
|
||||
|
||||
return PersistResult(
|
||||
poll: poll,
|
||||
|
@ -124,11 +110,12 @@ extension Persistence.Poll {
|
|||
into: managedObjectContext,
|
||||
property: property
|
||||
)
|
||||
update(poll: poll, context: context)
|
||||
update(in: managedObjectContext, poll: poll, context: context)
|
||||
return poll
|
||||
}
|
||||
|
||||
public static func merge(
|
||||
in managedObjectContext: NSManagedObjectContext,
|
||||
poll: Poll,
|
||||
context: PersistContext
|
||||
) {
|
||||
|
@ -139,10 +126,11 @@ extension Persistence.Poll {
|
|||
networkDate: context.networkDate
|
||||
)
|
||||
poll.update(property: property)
|
||||
update(poll: poll, context: context)
|
||||
update(in: managedObjectContext, poll: poll, context: context)
|
||||
}
|
||||
|
||||
public static func update(
|
||||
in managedObjectContext: NSManagedObjectContext,
|
||||
poll: Poll,
|
||||
context: PersistContext
|
||||
) {
|
||||
|
@ -153,6 +141,7 @@ extension Persistence.Poll {
|
|||
option: option,
|
||||
context: Persistence.PollOption.PersistContext(
|
||||
index: Int(option.index),
|
||||
poll: poll,
|
||||
entity: entity,
|
||||
me: context.me,
|
||||
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)
|
||||
}
|
||||
|
||||
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 let index: Int
|
||||
public let poll: Poll
|
||||
public let entity: Mastodon.Entity.Poll.Option
|
||||
public let me: MastodonUser?
|
||||
public let networkDate: Date
|
||||
|
@ -22,11 +23,13 @@ extension Persistence.PollOption {
|
|||
|
||||
public init(
|
||||
index: Int,
|
||||
poll: Poll,
|
||||
entity: Mastodon.Entity.Poll.Option,
|
||||
me: MastodonUser?,
|
||||
networkDate: Date
|
||||
) {
|
||||
self.index = index
|
||||
self.poll = poll
|
||||
self.entity = entity
|
||||
self.me = me
|
||||
self.networkDate = networkDate
|
||||
|
@ -66,6 +69,7 @@ extension Persistence.PollOption {
|
|||
context: PersistContext
|
||||
) -> PollOption {
|
||||
let property = PollOption.Property(
|
||||
poll: context.poll,
|
||||
index: context.index,
|
||||
entity: context.entity,
|
||||
networkDate: context.networkDate
|
||||
|
@ -81,6 +85,7 @@ extension Persistence.PollOption {
|
|||
) {
|
||||
guard context.networkDate > option.updatedAt else { return }
|
||||
let property = PollOption.Property(
|
||||
poll: context.poll,
|
||||
index: context.index,
|
||||
entity: context.entity,
|
||||
networkDate: context.networkDate
|
||||
|
|
|
@ -120,8 +120,10 @@ extension Persistence.Status {
|
|||
)
|
||||
)
|
||||
let author = authorResult.user
|
||||
let application: Application? = createApplication(in: managedObjectContext, context: .init(entity: context.entity))
|
||||
|
||||
let relationship = Status.Relationship(
|
||||
application: application,
|
||||
author: author,
|
||||
reblog: reblog,
|
||||
poll: poll,
|
||||
|
@ -197,7 +199,9 @@ extension Persistence.Status {
|
|||
)
|
||||
status.update(property: property)
|
||||
if let poll = status.poll, let entity = context.entity.poll {
|
||||
Persistence.Poll.merge(
|
||||
// update poll
|
||||
Persistence.Poll.update(
|
||||
in: managedObjectContext,
|
||||
poll: poll,
|
||||
context: Persistence.Poll.PersistContext(
|
||||
domain: context.domain,
|
||||
|
@ -206,6 +210,40 @@ extension Persistence.Status {
|
|||
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 {
|
||||
|
@ -243,5 +281,21 @@ extension Persistence.Status {
|
|||
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 SearchHistory { }
|
||||
public enum Notification { }
|
||||
public enum StatusEdit {}
|
||||
}
|
||||
|
||||
extension Persistence {
|
||||
|
|
|
@ -24,3 +24,4 @@ extension MastodonEmojiContainer {
|
|||
|
||||
extension Mastodon.Entity.Account: 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
|
||||
|
||||
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>)
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
public protocol StatusPublisher: ProgressReporting {
|
||||
var state: Published<StatusPublisherState>.Publisher { get }
|
||||
|
|
|
@ -8,10 +8,10 @@
|
|||
import Foundation
|
||||
|
||||
// https://gist.github.com/DougGregor/92a2e4f6e11f6d733fb5065e9d1c880f
|
||||
extension Collection {
|
||||
public func parallelMap<T>(
|
||||
extension Collection where Self: Sendable, Index: Sendable {
|
||||
public func parallelMap<T: Sendable>(
|
||||
parallelism requestedParallelism: Int? = nil,
|
||||
_ transform: @escaping (Element) async throws -> T
|
||||
_ transform: @escaping @Sendable (Element) async throws -> T
|
||||
) async rethrows -> [T] {
|
||||
let defaultParallelism = 2
|
||||
let parallelism = requestedParallelism ?? defaultParallelism
|
||||
|
@ -57,7 +57,7 @@ extension Collection {
|
|||
|
||||
func parallelEach(
|
||||
parallelism requestedParallelism: Int? = nil,
|
||||
_ work: @escaping (Element) async throws -> Void
|
||||
_ work: @escaping @Sendable (Element) async throws -> Void
|
||||
) async rethrows {
|
||||
_ = try await parallelMap {
|
||||
try await work($0)
|
||||
|
|
|
@ -132,6 +132,8 @@ public enum L10n {
|
|||
public static let done = L10n.tr("Localizable", "Common.Controls.Actions.Done", fallback: "Done")
|
||||
/// 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
|
||||
public static let findPeople = L10n.tr("Localizable", "Common.Controls.Actions.FindPeople", fallback: "Find people to follow")
|
||||
/// Manually search instead
|
||||
|
@ -290,6 +292,10 @@ public enum L10n {
|
|||
public enum Status {
|
||||
/// 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 %@
|
||||
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 %@")
|
||||
|
@ -298,6 +304,10 @@ public enum L10n {
|
|||
public static let loadEmbed = L10n.tr("Localizable", "Common.Controls.Status.LoadEmbed", fallback: "Load Embed")
|
||||
/// 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
|
||||
public static let sensitiveContent = L10n.tr("Localizable", "Common.Controls.Status.SensitiveContent", fallback: "Sensitive Content")
|
||||
/// Show Post
|
||||
|
@ -340,6 +350,26 @@ public enum L10n {
|
|||
/// 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 {
|
||||
/// %@, attachment %d of %d
|
||||
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 enum Title {
|
||||
/// Edit Post
|
||||
public static let editPost = L10n.tr("Localizable", "Scene.Compose.Title.EditPost", fallback: "Edit Post")
|
||||
/// New Post
|
||||
public static let newPost = L10n.tr("Localizable", "Scene.Compose.Title.NewPost", fallback: "New Post")
|
||||
/// New Reply
|
||||
|
@ -710,8 +742,8 @@ public enum L10n {
|
|||
public static let title = L10n.tr("Localizable", "Scene.Familiarfollowers.Title", fallback: "Followers you familiar")
|
||||
}
|
||||
public enum Favorite {
|
||||
/// Your Favorites
|
||||
public static let title = L10n.tr("Localizable", "Scene.Favorite.Title", fallback: "Your Favorites")
|
||||
/// Favorites
|
||||
public static let title = L10n.tr("Localizable", "Scene.Favorite.Title", fallback: "Favorites")
|
||||
}
|
||||
public enum FavoritedBy {
|
||||
/// Favorited By
|
||||
|
|
|
@ -65,6 +65,7 @@ Please check your internet connection.";
|
|||
"Common.Controls.Actions.TakePhoto" = "Take Photo";
|
||||
"Common.Controls.Actions.TranslatePost.Title" = "Translate from %@";
|
||||
"Common.Controls.Actions.TranslatePost.UnknownLanguage" = "Unknown";
|
||||
"Common.Controls.Actions.EditPost" = "Edit";
|
||||
"Common.Controls.Actions.TryAgain" = "Try Again";
|
||||
"Common.Controls.Actions.UnblockDomain" = "Unblock %@";
|
||||
"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.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.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.Notifications" = "Notifications";
|
||||
"Common.Controls.Tabs.Profile" = "Profile";
|
||||
|
@ -229,6 +238,7 @@ uploaded to Mastodon.";
|
|||
"Scene.Compose.ReplyingToUser" = "replying to %@";
|
||||
"Scene.Compose.Title.NewPost" = "New Post";
|
||||
"Scene.Compose.Title.NewReply" = "New Reply";
|
||||
"Scene.Compose.Title.EditPost" = "Edit Post";
|
||||
"Scene.Compose.Visibility.Direct" = "Only people I mention";
|
||||
"Scene.Compose.Visibility.Private" = "Followers only";
|
||||
"Scene.Compose.Visibility.Public" = "Public";
|
||||
|
@ -254,7 +264,7 @@ uploaded to Mastodon.";
|
|||
"Scene.Discovery.Tabs.Posts" = "Posts";
|
||||
"Scene.Familiarfollowers.FollowedByNames" = "Followed by %@";
|
||||
"Scene.Familiarfollowers.Title" = "Followers you familiar";
|
||||
"Scene.Favorite.Title" = "Your Favorites";
|
||||
"Scene.Favorite.Title" = "Favorites";
|
||||
"Scene.FavoritedBy.Title" = "Favorited By";
|
||||
"Scene.FollowedTags.Actions.Follow" = "Follow";
|
||||
"Scene.FollowedTags.Actions.Unfollow" = "Unfollow";
|
||||
|
|
|
@ -241,7 +241,7 @@ uploaded to Mastodon.";
|
|||
"Scene.Discovery.Tabs.Posts" = "Posts";
|
||||
"Scene.Familiarfollowers.FollowedByNames" = "Followed by %@";
|
||||
"Scene.Familiarfollowers.Title" = "Followers you familiar";
|
||||
"Scene.Favorite.Title" = "Your Favorites";
|
||||
"Scene.Favorite.Title" = "Favorites";
|
||||
"Scene.FavoritedBy.Title" = "Favorited By";
|
||||
"Scene.FollowedTags.Actions.Follow" = "Follow";
|
||||
"Scene.FollowedTags.Actions.Unfollow" = "Unfollow";
|
||||
|
|
|
@ -70,7 +70,7 @@ extension Mastodon.API.Media {
|
|||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
public struct UploadMediaQuery: PostQuery, PutQuery {
|
||||
public struct UploadMediaQuery: PostQuery {
|
||||
public let file: Mastodon.Query.MediaAttachment?
|
||||
public let thumbnail: Mastodon.Query.MediaAttachment?
|
||||
public let description: String?
|
||||
|
|
|
@ -36,7 +36,7 @@ extension Mastodon.API.Statuses {
|
|||
public static func favoriteBy(
|
||||
session: URLSession,
|
||||
domain: String,
|
||||
statusID: Mastodon.Entity.Poll.ID,
|
||||
statusID: Mastodon.Entity.Status.ID,
|
||||
query: FavoriteByQuery,
|
||||
authorization: Mastodon.API.OAuth.Authorization?
|
||||
) -> 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(
|
||||
url: URL,
|
||||
query: GetQuery?,
|
||||
query: GetQuery? = nil,
|
||||
authorization: OAuth.Authorization?
|
||||
) -> URLRequest {
|
||||
return buildRequest(url: url, method: .GET, query: query, authorization: authorization)
|
||||
|
@ -157,7 +157,7 @@ extension Mastodon.API {
|
|||
|
||||
static func put(
|
||||
url: URL,
|
||||
query: PutQuery?,
|
||||
query: PutQuery? = nil,
|
||||
authorization: OAuth.Authorization?
|
||||
) -> URLRequest {
|
||||
return buildRequest(url: url, method: .PUT, query: query, authorization: authorization)
|
||||
|
|
|
@ -17,7 +17,7 @@ extension Mastodon.Entity {
|
|||
/// 2021/1/28
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/entities/account/)
|
||||
public class Account: Codable {
|
||||
public final class Account: Codable, Sendable {
|
||||
|
||||
public typealias ID = String
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ extension Mastodon.Entity {
|
|||
/// 2021/1/28
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/entities/activity/)
|
||||
public struct Activity: Codable {
|
||||
public struct Activity: Codable, Sendable {
|
||||
public let week: Date
|
||||
public let statuses: Int
|
||||
public let logins: Int
|
||||
|
|
|
@ -16,7 +16,7 @@ extension Mastodon.Entity {
|
|||
/// 2021/1/28
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/entities/announcement/)
|
||||
public struct Announcement: Codable {
|
||||
public struct Announcement: Codable, Sendable {
|
||||
|
||||
public typealias ID = String
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ extension Mastodon.Entity {
|
|||
/// 2021/1/28
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/entities/announcementreaction/)
|
||||
public struct AnnouncementReaction: Codable {
|
||||
public struct AnnouncementReaction: Codable, Sendable {
|
||||
// Base
|
||||
public let name: String
|
||||
public let count: Int
|
||||
|
|
|
@ -16,7 +16,7 @@ extension Mastodon.Entity {
|
|||
/// 2021/1/28
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/entities/application/)
|
||||
public struct Application: Codable {
|
||||
public struct Application: Codable, Sendable {
|
||||
|
||||
public let name: String
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ extension Mastodon.Entity {
|
|||
/// 2021/1/28
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/entities/attachment/)
|
||||
public struct Attachment: Codable {
|
||||
public struct Attachment: Codable, Sendable {
|
||||
|
||||
public typealias ID = String
|
||||
|
||||
|
@ -48,7 +48,7 @@ extension Mastodon.Entity {
|
|||
|
||||
extension Mastodon.Entity.Attachment {
|
||||
public typealias AttachmentType = Type
|
||||
public enum `Type`: RawRepresentable, Codable {
|
||||
public enum `Type`: RawRepresentable, Codable, Sendable {
|
||||
case unknown
|
||||
case image
|
||||
case gifv
|
||||
|
@ -85,7 +85,7 @@ extension Mastodon.Entity.Attachment {
|
|||
extension Mastodon.Entity.Attachment {
|
||||
/// # Reference
|
||||
/// https://github.com/tootsuite/mastodon/blob/v3.3.0/app/models/media_attachment.rb
|
||||
public struct Meta: Codable {
|
||||
public struct Meta: Codable, Sendable {
|
||||
public let original: Format?
|
||||
public let small: Format?
|
||||
public let focus: Focus?
|
||||
|
@ -122,7 +122,7 @@ extension Mastodon.Entity.Attachment {
|
|||
}
|
||||
|
||||
extension Mastodon.Entity.Attachment.Meta {
|
||||
public struct Format: Codable {
|
||||
public struct Format: Codable, Sendable {
|
||||
public let width: Int?
|
||||
public let height: Int?
|
||||
public let size: String?
|
||||
|
@ -142,7 +142,7 @@ extension Mastodon.Entity.Attachment.Meta {
|
|||
}
|
||||
}
|
||||
|
||||
public struct Focus: Codable {
|
||||
public struct Focus: Codable, Sendable {
|
||||
public let x: Double
|
||||
public let y: Double
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ extension Mastodon.Entity {
|
|||
/// 2021/1/28
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/entities/card/)
|
||||
public struct Card: Codable {
|
||||
public struct Card: Codable, Sendable {
|
||||
// Base
|
||||
public let url: String
|
||||
public let title: String
|
||||
|
@ -54,7 +54,7 @@ extension Mastodon.Entity {
|
|||
}
|
||||
|
||||
extension Mastodon.Entity.Card {
|
||||
public enum `Type`: RawRepresentable, Codable {
|
||||
public enum `Type`: RawRepresentable, Codable, Sendable {
|
||||
case link
|
||||
case photo
|
||||
case video
|
||||
|
|
|
@ -9,7 +9,7 @@ import Foundation
|
|||
|
||||
extension Mastodon.Entity {
|
||||
|
||||
public struct Category: Codable {
|
||||
public struct Category: Codable, Sendable {
|
||||
public let category: Kind
|
||||
public let serversCount: Int
|
||||
|
||||
|
@ -25,7 +25,7 @@ extension Mastodon.Entity {
|
|||
|
||||
/// # Reference
|
||||
/// https://joinmastodon.org/communities
|
||||
public enum Kind: RawRepresentable, Codable {
|
||||
public enum Kind: RawRepresentable, Codable, Sendable {
|
||||
|
||||
case general
|
||||
case regional
|
||||
|
|
|
@ -16,7 +16,7 @@ extension Mastodon.Entity {
|
|||
/// 2021/1/28
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/entities/context/)
|
||||
public struct Context: Codable {
|
||||
public struct Context: Codable, Sendable {
|
||||
public let ancestors: [Status]
|
||||
public let descendants: [Status]
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ extension Mastodon.Entity {
|
|||
/// 2021/1/28
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/entities/emoji/)
|
||||
public struct Emoji: Codable {
|
||||
public struct Emoji: Codable, Sendable {
|
||||
public let shortcode: String
|
||||
public let url: String
|
||||
public let staticURL: String
|
||||
|
|
|
@ -17,7 +17,7 @@ extension Mastodon.Entity {
|
|||
/// 2022/5/16
|
||||
/// # Reference
|
||||
/// [Document](TBD)
|
||||
public class FamiliarFollowers: Codable {
|
||||
public class FamiliarFollowers: Codable, Sendable {
|
||||
public let id: Mastodon.Entity.Account.ID
|
||||
public let accounts: [Mastodon.Entity.Account]
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ extension Mastodon.Entity {
|
|||
/// 2021/1/28
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/entities/field/)
|
||||
public struct Field: Codable {
|
||||
public struct Field: Codable, Sendable {
|
||||
public let name: String
|
||||
public let value: String
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ extension Mastodon.Entity {
|
|||
/// 2021/1/28
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/entities/history/)
|
||||
public struct History: Codable {
|
||||
public struct History: Codable, Sendable {
|
||||
/// UNIX timestamp on midnight of the given day
|
||||
public let day: Date
|
||||
public let uses: String
|
||||
|
|
|
@ -16,7 +16,7 @@ extension Mastodon.Entity {
|
|||
/// 2022/4/13
|
||||
/// # Reference
|
||||
/// [Document](TBD)
|
||||
public struct Link: Codable {
|
||||
public struct Link: Codable, Sendable {
|
||||
public let url: String
|
||||
public let title: String
|
||||
public let description: String
|
||||
|
|
|
@ -16,7 +16,7 @@ extension Mastodon.Entity {
|
|||
/// 2021/1/28
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/entities/mention/)
|
||||
public struct Mention: Codable {
|
||||
public struct Mention: Codable, Sendable {
|
||||
|
||||
public typealias ID = String
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue