Implement post editing / edit history (#875)
Co-authored-by: Marcus Kida <marcus.kida@bearologics.com> Co-authored-by: Jed Fox <git@jedfox.com>
This commit is contained in:
parent
625ebb00fb
commit
0c224f47df
|
@ -101,7 +101,8 @@
|
|||
"translate_post": {
|
||||
"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",
|
||||
|
|
|
@ -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 */,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -652,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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) -> UIMenu?
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -112,8 +112,8 @@ extension StatusThreadRootTableViewCell {
|
|||
statusView.mediaGridContainerView,
|
||||
statusView.pollTableView,
|
||||
statusView.pollStatusStackView,
|
||||
statusView.actionToolbarContainer
|
||||
// statusMetricView is intentionally excluded
|
||||
statusView.actionToolbarContainer,
|
||||
statusView.statusMetricView,
|
||||
]
|
||||
|
||||
if statusView.viewModel.isContentReveal {
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import MastodonUI
|
||||
import MastodonSDK
|
||||
import CoreDataStack
|
||||
import MastodonAsset
|
||||
|
||||
class StatusEditHistoryTableViewCell: UITableViewCell {
|
||||
var containerViewLeadingLayoutConstraint: NSLayoutConstraint!
|
||||
var containerViewTrailingLayoutConstraint: NSLayoutConstraint!
|
||||
|
||||
static let identifier = "StatusEditHistoryTableViewCell"
|
||||
static let verticalMargin: CGFloat = 12
|
||||
static let horizontalMargin: CGFloat = 16
|
||||
|
||||
let dateLabel: UILabel
|
||||
let statusHistoryView: StatusHistoryView
|
||||
private let grayBackground: UIView
|
||||
var statusViewBottomConstraint: NSLayoutConstraint?
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
dateLabel = UILabel()
|
||||
dateLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
dateLabel.textColor = Asset.Colors.Label.secondary.color
|
||||
dateLabel.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular))
|
||||
|
||||
statusHistoryView = StatusHistoryView()
|
||||
statusHistoryView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
grayBackground = UIView()
|
||||
grayBackground.translatesAutoresizingMaskIntoConstraints = false
|
||||
grayBackground.backgroundColor = Asset.Scene.EditHistory.statusBackground.color
|
||||
grayBackground.layer.borderWidth = 1
|
||||
grayBackground.layer.borderColor = Asset.Scene.EditHistory.statusBackgroundBorder.color.cgColor
|
||||
grayBackground.applyCornerRadius(radius: 8)
|
||||
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
|
||||
isAccessibilityElement = true
|
||||
|
||||
selectionStyle = .none
|
||||
grayBackground.addSubview(statusHistoryView)
|
||||
contentView.addSubview(dateLabel)
|
||||
contentView.addSubview(grayBackground)
|
||||
|
||||
setupContainerViewMarginConstraints()
|
||||
setupConstraints()
|
||||
updateContainerViewMarginConstraints()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
private func setupConstraints() {
|
||||
statusViewBottomConstraint = statusHistoryView.bottomAnchor.constraint(equalTo: grayBackground.bottomAnchor, constant: -Self.verticalMargin)
|
||||
let constraints = [
|
||||
dateLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8),
|
||||
dateLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
|
||||
dateLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
|
||||
|
||||
grayBackground.topAnchor.constraint(equalTo: dateLabel.bottomAnchor, constant: Self.verticalMargin),
|
||||
grayBackground.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
|
||||
grayBackground.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
|
||||
grayBackground.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -Self.verticalMargin),
|
||||
|
||||
statusHistoryView.topAnchor.constraint(equalTo: grayBackground.topAnchor, constant: Self.verticalMargin),
|
||||
statusHistoryView.leadingAnchor.constraint(equalTo: grayBackground.leadingAnchor),
|
||||
statusHistoryView.trailingAnchor.constraint(equalTo: grayBackground.trailingAnchor),
|
||||
statusViewBottomConstraint,
|
||||
].compactMap { $0 }
|
||||
|
||||
NSLayoutConstraint.activate(constraints)
|
||||
}
|
||||
|
||||
func configure(status: Status, statusEdit: StatusEdit, dateText: String) {
|
||||
dateLabel.text = dateText
|
||||
statusHistoryView.statusView.configure(status: status, statusEdit: statusEdit)
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
statusHistoryView.prepareForReuse()
|
||||
super.prepareForReuse()
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
updateContainerViewMarginConstraints()
|
||||
}
|
||||
|
||||
override var accessibilityLabel: String? {
|
||||
get {
|
||||
(dateLabel.text ?? "") + ", " + (statusHistoryView.statusView.accessibilityLabel ?? "")
|
||||
}
|
||||
set {}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - AdaptiveContainerMarginTableViewCell
|
||||
extension StatusEditHistoryTableViewCell: AdaptiveContainerMarginTableViewCell {
|
||||
var containerView: StatusHistoryView {
|
||||
statusHistoryView
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import MastodonSDK
|
||||
import CoreDataStack
|
||||
import MastodonCore
|
||||
import MastodonLocalization
|
||||
|
||||
class StatusEditHistoryViewController: UIViewController {
|
||||
|
||||
private let tableView: UITableView
|
||||
|
||||
var tableViewDataSource: UITableViewDiffableDataSource<Int, StatusEdit>?
|
||||
var viewModel: StatusEditHistoryViewModel
|
||||
private let dateFormatter: DateFormatter
|
||||
|
||||
init(viewModel: StatusEditHistoryViewModel) {
|
||||
|
||||
self.viewModel = viewModel
|
||||
dateFormatter = DateFormatter()
|
||||
dateFormatter.dateStyle = .medium
|
||||
dateFormatter.timeStyle = .short
|
||||
|
||||
tableView = UITableView(frame: .zero)
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
tableView.separatorStyle = .none
|
||||
tableView.register(StatusEditHistoryTableViewCell.self, forCellReuseIdentifier: StatusEditHistoryTableViewCell.identifier)
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
let tableViewDataSource = UITableViewDiffableDataSource<Int, StatusEdit>(tableView: tableView) {tableView, indexPath, itemIdentifier in
|
||||
guard let cell = tableView.dequeueReusableCell(withIdentifier: StatusEditHistoryTableViewCell.identifier, for: indexPath) as? StatusEditHistoryTableViewCell else {
|
||||
fatalError("Wrong cell")
|
||||
}
|
||||
|
||||
let statusEdit = viewModel.edits[indexPath.row]
|
||||
let dateText: String
|
||||
|
||||
if statusEdit == viewModel.edits.last {
|
||||
dateText = L10n.Common.Controls.Status.EditHistory.originalPost(self.dateFormatter.string(from: statusEdit.createdAt))
|
||||
} else {
|
||||
dateText = self.dateFormatter.string(from: statusEdit.createdAt)
|
||||
}
|
||||
|
||||
viewModel.prepareCell(cell, in: tableView)
|
||||
cell.configure(status: viewModel.status, statusEdit: statusEdit, dateText: dateText)
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
tableView.dataSource = tableViewDataSource
|
||||
tableView.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor
|
||||
self.tableViewDataSource = tableViewDataSource
|
||||
|
||||
|
||||
view.addSubview(tableView)
|
||||
|
||||
view.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor
|
||||
setupConstraints()
|
||||
|
||||
title = L10n.Common.Controls.Status.EditHistory.title
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
private func setupConstraints() {
|
||||
let constraints = tableView.pinTo(to: view)
|
||||
NSLayoutConstraint.activate(constraints)
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Int, StatusEdit>()
|
||||
snapshot.appendSections([0])
|
||||
snapshot.appendItems(viewModel.edits)
|
||||
|
||||
tableViewDataSource?.apply(snapshot)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import CoreDataStack
|
||||
import MastodonCore
|
||||
import MastodonUI
|
||||
import UIKit
|
||||
|
||||
struct StatusEditHistoryViewModel {
|
||||
let status: Status
|
||||
let edits: [StatusEdit]
|
||||
|
||||
let appContext: AppContext
|
||||
let authContext: AuthContext
|
||||
|
||||
func prepareCell(_ cell: StatusEditHistoryTableViewCell, in tableView: UITableView) {
|
||||
StatusSection.setupStatusPollHistoryDataSource(
|
||||
context: appContext,
|
||||
authContext: authContext,
|
||||
statusView: cell.statusHistoryView.statusView
|
||||
)
|
||||
|
||||
cell.statusHistoryView.statusView.frame.size.width = tableView.frame.width - cell.containerViewHorizontalMargin
|
||||
cell.statusViewBottomConstraint?.constant = cell.statusHistoryView.statusView.mediaContainerView.isHidden ? -StatusEditHistoryTableViewCell.verticalMargin : 0
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import MastodonUI
|
||||
|
||||
class StatusHistoryView: UIView {
|
||||
let statusView = StatusView()
|
||||
|
||||
private var statusViewLeadingConstraint: NSLayoutConstraint!
|
||||
private var statusViewTrailingConstraint: NSLayoutConstraint!
|
||||
|
||||
init() {
|
||||
super.init(frame: .zero)
|
||||
statusView.translatesAutoresizingMaskIntoConstraints = false
|
||||
statusView.setup(style: .editHistory)
|
||||
addSubview(statusView)
|
||||
|
||||
statusViewLeadingConstraint = statusView.leadingAnchor.constraint(equalTo: leadingAnchor)
|
||||
statusViewTrailingConstraint = statusView.trailingAnchor.constraint(equalTo: trailingAnchor)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
statusView.topAnchor.constraint(equalTo: topAnchor),
|
||||
statusView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
statusViewLeadingConstraint,
|
||||
statusViewTrailingConstraint
|
||||
])
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func prepareForReuse() {
|
||||
statusView.prepareForReuse()
|
||||
}
|
||||
}
|
||||
|
||||
extension StatusHistoryView: AdaptiveContainerView {
|
||||
func updateContainerViewComponentsLayoutMarginsRelativeArrangementBehavior(isEnabled: Bool) {
|
||||
statusView.updateContainerViewComponentsLayoutMarginsRelativeArrangementBehavior(isEnabled: isEnabled)
|
||||
statusViewLeadingConstraint.constant = isEnabled ? 0 : StatusEditHistoryTableViewCell.horizontalMargin
|
||||
statusViewTrailingConstraint.constant = isEnabled ? 0 : -StatusEditHistoryTableViewCell.horizontalMargin
|
||||
}
|
||||
}
|
|
@ -117,6 +117,7 @@ extension ThreadViewController {
|
|||
let composeViewModel = ComposeViewModel(
|
||||
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))
|
||||
|
|
|
@ -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>
|
|
@ -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>)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -25,6 +25,7 @@ extension Mastodon.Entity {
|
|||
public let id: ID
|
||||
public let uri: String
|
||||
public let createdAt: Date
|
||||
public let editedAt: Date?
|
||||
public let account: Account
|
||||
public let content: String? // will be optional when delete status
|
||||
|
||||
|
@ -65,6 +66,7 @@ extension Mastodon.Entity {
|
|||
case id
|
||||
case uri
|
||||
case createdAt = "created_at"
|
||||
case editedAt = "edited_at"
|
||||
case account
|
||||
case content
|
||||
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Mastodon.Entity {
|
||||
|
||||
/// StatusEdit
|
||||
///
|
||||
/// - Since: 0.1.0
|
||||
/// - Version: 3.5.0
|
||||
/// # Last Update
|
||||
/// 2022/12/14
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/entities/statusedit/)
|
||||
public class StatusEdit: Codable {
|
||||
public class Poll: Codable {
|
||||
public class Option: Codable {
|
||||
public let title: String
|
||||
}
|
||||
public let options: [Option]
|
||||
public let title: String?
|
||||
}
|
||||
|
||||
public let content: String
|
||||
public let spoilerText: String?
|
||||
public let sensitive: Bool
|
||||
public let createdAt: Date
|
||||
public let account: Account
|
||||
public let poll: Poll?
|
||||
public let mediaAttachments: [Attachment]?
|
||||
public let emojis: [Emoji]?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case content
|
||||
case spoilerText = "spoiler_text"
|
||||
case sensitive
|
||||
case createdAt = "created_at"
|
||||
case account
|
||||
case poll
|
||||
case mediaAttachments = "media_attachments"
|
||||
case emojis
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Mastodon.Entity {
|
||||
public struct StatusSource: Codable {
|
||||
public let id: String
|
||||
public let text: String
|
||||
public let spoilerText: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case text
|
||||
case spoilerText = "spoiler_text"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -48,7 +48,7 @@ extension GetQuery {
|
|||
protocol PostQuery: RequestQuery { }
|
||||
|
||||
extension PostQuery {
|
||||
// By default a `PostQuery` does not has query items
|
||||
// By default a `PostQuery` does not have query items
|
||||
var queryItems: [URLQueryItem]? { nil }
|
||||
}
|
||||
|
||||
|
@ -58,10 +58,15 @@ protocol PatchQuery: RequestQuery { }
|
|||
// PUT
|
||||
protocol PutQuery: RequestQuery { }
|
||||
|
||||
extension PutQuery {
|
||||
// By default a `PutQuery` does not have query items
|
||||
var queryItems: [URLQueryItem]? { nil }
|
||||
}
|
||||
|
||||
// DELETE
|
||||
protocol DeleteQuery: RequestQuery { }
|
||||
|
||||
extension DeleteQuery {
|
||||
// By default a `DeleteQuery` does not has query items
|
||||
// By default a `DeleteQuery` does not have query items
|
||||
var queryItems: [URLQueryItem]? { nil }
|
||||
}
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import CoreDataStack
|
||||
|
||||
public protocol StatusCompatible {
|
||||
var reblog: Status? { get }
|
||||
var attachments: [MastodonAttachment] { get }
|
||||
var isMediaSensitive: Bool { get }
|
||||
var isSensitiveToggled: Bool { get }
|
||||
}
|
||||
|
||||
extension Status: StatusCompatible {}
|
||||
|
||||
extension StatusEdit: StatusCompatible {
|
||||
public var reblog: Status? {
|
||||
nil
|
||||
}
|
||||
|
||||
public var isMediaSensitive: Bool {
|
||||
sensitive
|
||||
}
|
||||
|
||||
public var isSensitiveToggled: Bool {
|
||||
true
|
||||
}
|
||||
}
|
|
@ -71,6 +71,7 @@ public struct AttachmentView: View {
|
|||
.lineLimit(1)
|
||||
}
|
||||
.padding(EdgeInsets(top: 6, leading: 0, bottom: 10, trailing: 0))
|
||||
.disabled(!viewModel.isCaptionEditable)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
@ -44,7 +44,7 @@ extension AttachmentViewModel: NSItemProviderWriting {
|
|||
switch input {
|
||||
case .image:
|
||||
typeIdentifiers.append(UTType.png.identifier)
|
||||
case .url(let url):
|
||||
case .url(let url), .mastodonAssetUrl(let url, _):
|
||||
let _uti = UTType(filenameExtension: url.pathExtension)
|
||||
if let uti = _uti {
|
||||
if uti.conforms(to: .image) {
|
||||
|
|
|
@ -28,6 +28,8 @@ extension AttachmentViewModel {
|
|||
} catch {
|
||||
throw error
|
||||
}
|
||||
case .mastodonAssetUrl(let url, _):
|
||||
return try await Self.loadMastodonAsset(url: url)
|
||||
case .pickerResult(let pickerResult):
|
||||
do {
|
||||
let output = try await AttachmentViewModel.load(itemProvider: pickerResult.itemProvider)
|
||||
|
@ -45,6 +47,14 @@ extension AttachmentViewModel {
|
|||
}
|
||||
}
|
||||
|
||||
private static func loadMastodonAsset(url: URL) async throws -> Output {
|
||||
guard !url.isFileURL else {
|
||||
throw AttachmentError.invalidAttachmentType
|
||||
}
|
||||
let (imageData, _) = try await URLSession.shared.data(from: url)
|
||||
return .image(imageData, imageKind: AssetType(imageData) == .png ? .png : .jpg)
|
||||
}
|
||||
|
||||
private static func load(url: URL) async throws -> Output {
|
||||
guard let uti = UTType(filenameExtension: url.pathExtension) else {
|
||||
throw AttachmentError.invalidAttachmentType
|
||||
|
|
|
@ -66,7 +66,10 @@ extension AttachmentViewModel {
|
|||
let authContext: AuthContext
|
||||
}
|
||||
|
||||
public typealias UploadResult = Mastodon.Entity.Attachment
|
||||
public enum UploadResult {
|
||||
case uploadedMastodonAttachment(Mastodon.Entity.Attachment)
|
||||
case exists
|
||||
}
|
||||
}
|
||||
|
||||
extension AttachmentViewModel {
|
||||
|
@ -194,7 +197,7 @@ extension AttachmentViewModel {
|
|||
|
||||
// escape here
|
||||
progress.completedUnitCount = progress.totalUnitCount
|
||||
return attachmentStatusResponse.value
|
||||
return .uploadedMastodonAttachment(attachmentStatusResponse.value)
|
||||
|
||||
} else {
|
||||
AttachmentViewModel.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): attachment processing. Retry \(waitProcessRetryCount)/\(waitProcessRetryLimit)")
|
||||
|
@ -207,7 +210,7 @@ extension AttachmentViewModel {
|
|||
} else {
|
||||
AttachmentViewModel.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): upload attachment success: \(attachmentUploadResponse.value.url ?? "<nil>")")
|
||||
|
||||
return attachmentUploadResponse.value
|
||||
return .uploadedMastodonAttachment(attachmentUploadResponse.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,7 +50,8 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable
|
|||
public let input: Input
|
||||
public let sizeLimit: SizeLimit
|
||||
@Published var caption = ""
|
||||
|
||||
@Published public private(set) var isCaptionEditable = true
|
||||
|
||||
// output
|
||||
@Published public private(set) var output: Output?
|
||||
@Published public private(set) var thumbnail: UIImage? // original size image thumbnail
|
||||
|
@ -137,6 +138,17 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable
|
|||
do {
|
||||
var output = try await load(input: input)
|
||||
|
||||
switch input {
|
||||
case .mastodonAssetUrl:
|
||||
self.isCaptionEditable = false
|
||||
self.uploadState = .finish
|
||||
self.output = output
|
||||
self.uploadResult = .exists
|
||||
return
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
switch output {
|
||||
case .image(let data, _):
|
||||
self.output = output
|
||||
|
@ -253,6 +265,7 @@ extension AttachmentViewModel {
|
|||
public enum Input: Hashable {
|
||||
case image(UIImage)
|
||||
case url(URL)
|
||||
case mastodonAssetUrl(URL, String)
|
||||
case pickerResult(PHPickerResult)
|
||||
case itemProvider(NSItemProvider)
|
||||
}
|
||||
|
|
|
@ -83,11 +83,6 @@ public final class ComposeContentViewController: UIViewController {
|
|||
)
|
||||
return view
|
||||
}()
|
||||
|
||||
deinit {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ComposeContentViewController {
|
||||
|
@ -331,6 +326,7 @@ extension ComposeContentViewController {
|
|||
viewModel.$isEmojiActive.assign(to: &composeContentToolbarViewModel.$isEmojiActive)
|
||||
viewModel.$isContentWarningActive.assign(to: &composeContentToolbarViewModel.$isContentWarningActive)
|
||||
viewModel.$visibility.assign(to: &composeContentToolbarViewModel.$visibility)
|
||||
viewModel.$isVisibilityButtonEnabled.assign(to: &composeContentToolbarViewModel.$isVisibilityButtonEnabled)
|
||||
viewModel.$maxTextInputLimit.assign(to: &composeContentToolbarViewModel.$maxTextInputLimit)
|
||||
viewModel.$contentWeightedLength.assign(to: &composeContentToolbarViewModel.$contentWeightedLength)
|
||||
viewModel.$contentWarningWeightedLength.assign(to: &composeContentToolbarViewModel.$contentWarningWeightedLength)
|
||||
|
|
|
@ -37,7 +37,6 @@ extension ComposeContentViewModel: UITextViewDelegate {
|
|||
return
|
||||
}
|
||||
let backedString = metaText.backedString
|
||||
logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(backedString)")
|
||||
|
||||
// configure auto completion
|
||||
setupAutoComplete(for: textView)
|
||||
|
|
|
@ -21,6 +21,11 @@ public protocol ComposeContentViewModelDelegate: AnyObject {
|
|||
}
|
||||
|
||||
public final class ComposeContentViewModel: NSObject, ObservableObject {
|
||||
|
||||
public enum ComposeContext {
|
||||
case composeStatus
|
||||
case editStatus(status: Status, statusSource: Mastodon.Entity.StatusSource)
|
||||
}
|
||||
|
||||
let logger = Logger(subsystem: "ComposeContentViewModel", category: "ViewModel")
|
||||
|
||||
|
@ -32,6 +37,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
|
|||
|
||||
// input
|
||||
let context: AppContext
|
||||
let composeContext: ComposeContext
|
||||
let destination: Destination
|
||||
weak var delegate: ComposeContentViewModelDelegate?
|
||||
|
||||
|
@ -111,7 +117,8 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
|
|||
|
||||
// visibility
|
||||
@Published public var visibility: Mastodon.Entity.Status.Visibility
|
||||
|
||||
@Published public var isVisibilityButtonEnabled = false
|
||||
|
||||
// language
|
||||
@Published public var language: String
|
||||
@Published public private(set) var recentLanguages: [String]
|
||||
|
@ -141,12 +148,14 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
|
|||
public init(
|
||||
context: AppContext,
|
||||
authContext: AuthContext,
|
||||
composeContext: ComposeContext,
|
||||
destination: Destination,
|
||||
initialContent: String
|
||||
) {
|
||||
self.context = context
|
||||
self.authContext = authContext
|
||||
self.destination = destination
|
||||
self.composeContext = composeContext
|
||||
self.visibility = {
|
||||
// default private when user locked
|
||||
var visibility: Mastodon.Entity.Status.Visibility = {
|
||||
|
@ -179,10 +188,30 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
|
|||
}
|
||||
return visibility
|
||||
}()
|
||||
|
||||
self.customEmojiViewModel = context.emojiService.dequeueCustomEmojiViewModel(
|
||||
for: authContext.mastodonAuthenticationBox.domain
|
||||
)
|
||||
|
||||
if case let ComposeContext.editStatus(status, _) = composeContext {
|
||||
if status.isContentSensitive {
|
||||
isContentWarningActive = true
|
||||
contentWarning = status.spoilerText ?? ""
|
||||
}
|
||||
if let poll = status.poll {
|
||||
isPollActive = !poll.expired
|
||||
pollMultipleConfigurationOption = poll.multiple
|
||||
if let pollExpiresAt = poll.expiresAt {
|
||||
pollExpireConfigurationOption = .init(closestDateToExpiry: pollExpiresAt)
|
||||
}
|
||||
pollOptions = poll.options.sortedByIndex().map {
|
||||
let option = PollComposeItem.Option()
|
||||
option.text = $0.title
|
||||
return option
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let recentLanguages = context.settingService.currentSetting.value?.recentLanguages ?? []
|
||||
self.recentLanguages = recentLanguages
|
||||
self.language = recentLanguages.first ?? Locale.current.languageCode ?? "en"
|
||||
|
@ -259,6 +288,28 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
|
|||
// TODO: more limit
|
||||
}
|
||||
|
||||
switch composeContext {
|
||||
case .composeStatus:
|
||||
self.isVisibilityButtonEnabled = true
|
||||
case let .editStatus(status, _):
|
||||
if let visibility = Mastodon.Entity.Status.Visibility(rawValue: status.visibility.rawValue) {
|
||||
self.visibility = visibility
|
||||
}
|
||||
self.isVisibilityButtonEnabled = false
|
||||
self.attachmentViewModels = status.attachments.compactMap {
|
||||
guard let assetURL = $0.assetURL, let url = URL(string: assetURL) else { return nil }
|
||||
let attachmentViewModel = AttachmentViewModel(
|
||||
api: context.apiService,
|
||||
authContext: authContext,
|
||||
input: .mastodonAssetUrl(url, $0.id),
|
||||
sizeLimit: sizeLimit,
|
||||
delegate: self
|
||||
)
|
||||
attachmentViewModel.caption = $0.altDescription ?? ""
|
||||
return attachmentViewModel
|
||||
}
|
||||
}
|
||||
|
||||
bind()
|
||||
}
|
||||
|
||||
|
@ -557,7 +608,57 @@ extension ComposeContentViewModel {
|
|||
visibility: visibility,
|
||||
language: language
|
||||
)
|
||||
} // end func publisher()
|
||||
}
|
||||
|
||||
|
||||
// MastodonEditStatusPublisher
|
||||
public func statusEditPublisher() throws -> StatusPublisher? {
|
||||
let authContext = self.authContext
|
||||
guard case let .editStatus(status, _) = composeContext else { return nil }
|
||||
|
||||
// author
|
||||
let managedObjectContext = self.context.managedObjectContext
|
||||
var _author: ManagedObjectRecord<MastodonUser>?
|
||||
managedObjectContext.performAndWait {
|
||||
_author = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: managedObjectContext)?.user.asRecord
|
||||
}
|
||||
guard let author = _author else {
|
||||
throw AppError.badAuthentication
|
||||
}
|
||||
|
||||
// poll
|
||||
_ = try {
|
||||
guard isPollActive else { return }
|
||||
let isAllNonEmpty = pollOptions
|
||||
.map { $0.text }
|
||||
.allSatisfy { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }
|
||||
guard isAllNonEmpty else {
|
||||
throw ComposeError.pollHasEmptyOption
|
||||
}
|
||||
}()
|
||||
|
||||
// save language to recent languages
|
||||
if let settings = context.settingService.currentSetting.value {
|
||||
settings.managedObjectContext?.performAndWait {
|
||||
settings.recentLanguages = [language] + settings.recentLanguages.filter { $0 != language }
|
||||
}
|
||||
}
|
||||
|
||||
return MastodonEditStatusPublisher(statusID: status.id,
|
||||
author: author,
|
||||
isContentWarningComposing: isContentWarningActive,
|
||||
contentWarning: contentWarning,
|
||||
content: content,
|
||||
isMediaSensitive: isContentWarningActive,
|
||||
attachmentViewModels: attachmentViewModels,
|
||||
isPollComposing: isPollActive,
|
||||
pollOptions: pollOptions,
|
||||
pollExpireConfigurationOption: pollExpireConfigurationOption,
|
||||
pollMultipleConfigurationOption: pollMultipleConfigurationOption,
|
||||
visibility: visibility,
|
||||
language: language)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ComposeContentViewModel {
|
||||
|
|
|
@ -0,0 +1,183 @@
|
|||
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonCore
|
||||
import MastodonSDK
|
||||
|
||||
public final class MastodonEditStatusPublisher: NSObject, ProgressReporting {
|
||||
|
||||
// Input
|
||||
public let statusID: Status.ID
|
||||
public let author: ManagedObjectRecord<MastodonUser>
|
||||
|
||||
// content warning
|
||||
public let isContentWarningComposing: Bool
|
||||
public let contentWarning: String
|
||||
// status content
|
||||
public let content: String
|
||||
// media
|
||||
public let isMediaSensitive: Bool
|
||||
public let attachmentViewModels: [AttachmentViewModel]
|
||||
// poll
|
||||
public let isPollComposing: Bool
|
||||
public let pollOptions: [PollComposeItem.Option]
|
||||
public let pollExpireConfigurationOption: PollComposeItem.ExpireConfiguration.Option
|
||||
public let pollMultipleConfigurationOption: PollComposeItem.MultipleConfiguration.Option
|
||||
// visibility
|
||||
public let visibility: Mastodon.Entity.Status.Visibility
|
||||
// language
|
||||
public let language: String
|
||||
|
||||
// Output
|
||||
let _progress = Progress()
|
||||
public var progress: Progress { _progress }
|
||||
@Published var _state: StatusPublisherState = .pending
|
||||
public var state: Published<StatusPublisherState>.Publisher { $_state }
|
||||
|
||||
public var reactor: StatusPublisherReactor?
|
||||
|
||||
public init(
|
||||
statusID: Status.ID,
|
||||
author: ManagedObjectRecord<MastodonUser>,
|
||||
isContentWarningComposing: Bool,
|
||||
contentWarning: String,
|
||||
content: String,
|
||||
isMediaSensitive: Bool,
|
||||
attachmentViewModels: [AttachmentViewModel],
|
||||
isPollComposing: Bool,
|
||||
pollOptions: [PollComposeItem.Option],
|
||||
pollExpireConfigurationOption: PollComposeItem.ExpireConfiguration.Option,
|
||||
pollMultipleConfigurationOption: PollComposeItem.MultipleConfiguration.Option,
|
||||
visibility: Mastodon.Entity.Status.Visibility,
|
||||
language: String
|
||||
) {
|
||||
self.author = author
|
||||
self.statusID = statusID
|
||||
self.isContentWarningComposing = isContentWarningComposing
|
||||
self.contentWarning = contentWarning
|
||||
self.content = content
|
||||
self.isMediaSensitive = isMediaSensitive
|
||||
self.attachmentViewModels = attachmentViewModels
|
||||
self.isPollComposing = isPollComposing
|
||||
self.pollOptions = pollOptions
|
||||
self.pollExpireConfigurationOption = pollExpireConfigurationOption
|
||||
self.pollMultipleConfigurationOption = pollMultipleConfigurationOption
|
||||
self.visibility = visibility
|
||||
self.language = language
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - StatusPublisher
|
||||
extension MastodonEditStatusPublisher: StatusPublisher {
|
||||
|
||||
public func publish(
|
||||
api: APIService,
|
||||
authContext: AuthContext
|
||||
) async throws -> StatusPublishResult {
|
||||
let idempotencyKey = UUID().uuidString
|
||||
|
||||
let publishStatusTaskStartDelayWeight: Int64 = 20
|
||||
let publishStatusTaskStartDelayCount: Int64 = publishStatusTaskStartDelayWeight
|
||||
|
||||
let publishAttachmentTaskWeight: Int64 = 100
|
||||
let publishAttachmentTaskCount: Int64 = Int64(attachmentViewModels.count) * publishAttachmentTaskWeight
|
||||
|
||||
let publishStatusTaskWeight: Int64 = 20
|
||||
let publishStatusTaskCount: Int64 = publishStatusTaskWeight
|
||||
|
||||
let taskCount = [
|
||||
publishStatusTaskStartDelayCount,
|
||||
publishAttachmentTaskCount,
|
||||
publishStatusTaskCount
|
||||
].reduce(0, +)
|
||||
progress.totalUnitCount = taskCount
|
||||
progress.completedUnitCount = 0
|
||||
|
||||
// start delay
|
||||
try? await Task.sleep(nanoseconds: 1 * .second)
|
||||
progress.completedUnitCount += publishStatusTaskStartDelayWeight
|
||||
|
||||
// Task: attachment
|
||||
|
||||
var attachmentIDs: [Mastodon.Entity.Attachment.ID] = []
|
||||
for attachmentViewModel in attachmentViewModels {
|
||||
// set progress
|
||||
progress.addChild(attachmentViewModel.progress, withPendingUnitCount: publishAttachmentTaskWeight)
|
||||
// upload media
|
||||
do {
|
||||
switch attachmentViewModel.uploadResult {
|
||||
case .none:
|
||||
// precondition: all media uploaded
|
||||
throw AppError.badRequest
|
||||
case .exists:
|
||||
guard case let AttachmentViewModel.Input.mastodonAssetUrl(_, attachmentId) = attachmentViewModel.input else {
|
||||
throw AppError.badRequest
|
||||
}
|
||||
attachmentIDs.append(attachmentId)
|
||||
break
|
||||
case let .uploadedMastodonAttachment(attachment):
|
||||
attachmentIDs.append(attachment.id)
|
||||
|
||||
let caption = attachmentViewModel.caption
|
||||
guard !caption.isEmpty else { continue }
|
||||
|
||||
_ = try await api.updateMedia(
|
||||
domain: authContext.mastodonAuthenticationBox.domain,
|
||||
attachmentID: attachment.id,
|
||||
query: .init(
|
||||
file: nil,
|
||||
thumbnail: nil,
|
||||
description: caption,
|
||||
focus: nil
|
||||
),
|
||||
mastodonAuthenticationBox: authContext.mastodonAuthenticationBox
|
||||
).singleOutput()
|
||||
|
||||
// TODO: allow background upload
|
||||
// let attachment = try await attachmentViewModel.upload(context: uploadContext)
|
||||
// let attachmentID = attachment.id
|
||||
// attachmentIDs.append(attachmentID)
|
||||
}
|
||||
} catch {
|
||||
_state = .failure(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
let pollOptions: [String]? = {
|
||||
guard self.isPollComposing else { return nil }
|
||||
let options = self.pollOptions.compactMap { $0.text.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||
return options.isEmpty ? nil : options
|
||||
}()
|
||||
let pollExpiresIn: Int? = {
|
||||
guard self.isPollComposing else { return nil }
|
||||
guard pollOptions != nil else { return nil }
|
||||
return self.pollExpireConfigurationOption.seconds
|
||||
}()
|
||||
|
||||
let query = Mastodon.API.Statuses.EditStatusQuery(
|
||||
status: content,
|
||||
mediaIDs: attachmentIDs.isEmpty ? nil : attachmentIDs,
|
||||
pollOptions: pollOptions,
|
||||
pollExpiresIn: pollExpiresIn,
|
||||
pollMultipleAnswers: pollMultipleConfigurationOption,
|
||||
sensitive: isMediaSensitive,
|
||||
spoilerText: isContentWarningComposing ? contentWarning : nil,
|
||||
visibility: visibility,
|
||||
language: language
|
||||
)
|
||||
|
||||
let editStatusResponse = try await api.publishStatusEdit(forStatusID: statusID,
|
||||
editStatusQuery: query,
|
||||
authenticationBox: authContext.mastodonAuthenticationBox)
|
||||
|
||||
progress.completedUnitCount += publishStatusTaskCount
|
||||
_state = .success
|
||||
|
||||
return .edit(editStatusResponse)
|
||||
}
|
||||
|
||||
}
|
|
@ -119,31 +119,35 @@ extension MastodonStatusPublisher: StatusPublisher {
|
|||
progress.addChild(attachmentViewModel.progress, withPendingUnitCount: publishAttachmentTaskWeight)
|
||||
// upload media
|
||||
do {
|
||||
guard let attachment = attachmentViewModel.uploadResult else {
|
||||
switch attachmentViewModel.uploadResult {
|
||||
case .none:
|
||||
// precondition: all media uploaded
|
||||
throw AppError.badRequest
|
||||
case .exists:
|
||||
break
|
||||
case let .uploadedMastodonAttachment(attachment):
|
||||
attachmentIDs.append(attachment.id)
|
||||
|
||||
let caption = attachmentViewModel.caption
|
||||
guard !caption.isEmpty else { continue }
|
||||
|
||||
_ = try await api.updateMedia(
|
||||
domain: authContext.mastodonAuthenticationBox.domain,
|
||||
attachmentID: attachment.id,
|
||||
query: .init(
|
||||
file: nil,
|
||||
thumbnail: nil,
|
||||
description: caption,
|
||||
focus: nil
|
||||
),
|
||||
mastodonAuthenticationBox: authContext.mastodonAuthenticationBox
|
||||
).singleOutput()
|
||||
|
||||
// TODO: allow background upload
|
||||
// let attachment = try await attachmentViewModel.upload(context: uploadContext)
|
||||
// let attachmentID = attachment.id
|
||||
// attachmentIDs.append(attachmentID)
|
||||
}
|
||||
attachmentIDs.append(attachment.id)
|
||||
|
||||
let caption = attachmentViewModel.caption
|
||||
guard !caption.isEmpty else { continue }
|
||||
|
||||
_ = try await api.updateMedia(
|
||||
domain: authContext.mastodonAuthenticationBox.domain,
|
||||
attachmentID: attachment.id,
|
||||
query: .init(
|
||||
file: nil,
|
||||
thumbnail: nil,
|
||||
description: caption,
|
||||
focus: nil
|
||||
),
|
||||
mastodonAuthenticationBox: authContext.mastodonAuthenticationBox
|
||||
).singleOutput()
|
||||
|
||||
// TODO: allow background upload
|
||||
// let attachment = try await attachmentViewModel.upload(context: uploadContext)
|
||||
// let attachmentID = attachment.id
|
||||
// attachmentIDs.append(attachmentID)
|
||||
} catch {
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): upload attachment fail: \(error.localizedDescription)")
|
||||
_state = .failure(error)
|
||||
|
@ -188,7 +192,7 @@ extension MastodonStatusPublisher: StatusPublisher {
|
|||
_state = .success
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): status published: \(publishResponse.value.id)")
|
||||
|
||||
return .mastodon(publishResponse)
|
||||
return .post(publishResponse)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ extension ComposeContentToolbarView {
|
|||
var allVisibilities: [Mastodon.Entity.Status.Visibility] {
|
||||
return [.public, .private, .direct]
|
||||
}
|
||||
|
||||
@Published var isVisibilityButtonEnabled = false
|
||||
@Published var isPollActive = false
|
||||
@Published var isEmojiActive = false
|
||||
@Published var isContentWarningActive = false
|
||||
|
|
|
@ -70,7 +70,9 @@ struct ComposeContentToolbarView: View {
|
|||
} label: {
|
||||
label(for: viewModel.visibility.image)
|
||||
.accessibilityLabel(L10n.Scene.Compose.Keyboard.selectVisibilityEntry(viewModel.visibility.title))
|
||||
.opacity(viewModel.isVisibilityButtonEnabled ? 1.0 : 0.5)
|
||||
}
|
||||
.disabled(!viewModel.isVisibilityButtonEnabled)
|
||||
.frame(width: 48, height: 48)
|
||||
case .poll:
|
||||
Button {
|
||||
|
|
|
@ -62,6 +62,10 @@ public struct MetaTextViewRepresentable: UIViewRepresentable {
|
|||
public func updateUIView(_ metaTextView: MetaTextView, context: Context) {
|
||||
// update layout
|
||||
context.coordinator.widthLayoutConstraint.constant = width
|
||||
|
||||
// trigger layout engine update to adjust to text height
|
||||
metaText.textView.setNeedsLayout()
|
||||
metaText.textView.layoutIfNeeded()
|
||||
}
|
||||
|
||||
public func makeCoordinator() -> Coordinator {
|
||||
|
|
|
@ -179,7 +179,7 @@ extension MediaView.Configuration {
|
|||
}
|
||||
|
||||
extension MediaView {
|
||||
public static func configuration(status: Status) -> [MediaView.Configuration] {
|
||||
public static func configuration(status: StatusCompatible) -> [MediaView.Configuration] {
|
||||
func videoInfo(from attachment: MastodonAttachment) -> MediaView.Configuration.VideoInfo {
|
||||
MediaView.Configuration.VideoInfo(
|
||||
aspectRadio: attachment.size,
|
||||
|
@ -190,7 +190,7 @@ extension MediaView {
|
|||
)
|
||||
}
|
||||
|
||||
let status = status.reblog ?? status
|
||||
let status: StatusCompatible = status.reblog ?? status
|
||||
let attachments = status.attachments
|
||||
let configurations = attachments.enumerated().map { (idx, attachment) -> MediaView.Configuration in
|
||||
let configuration: MediaView.Configuration = {
|
||||
|
|
|
@ -597,6 +597,10 @@ extension NotificationView: StatusViewDelegate {
|
|||
public func statusView(_ statusView: StatusView, statusMetricView: StatusMetricView, favoriteButtonDidPressed button: UIButton) {
|
||||
assertionFailure()
|
||||
}
|
||||
|
||||
public func statusView(_ statusView: StatusView, statusMetricView: StatusMetricView, showEditHistory button: UIButton) {
|
||||
assertionFailure()
|
||||
}
|
||||
|
||||
public func statusView(_ statusView: StatusView, accessibilityActivate: Void) {
|
||||
assertionFailure()
|
||||
|
|
|
@ -81,6 +81,7 @@ public class StatusAuthorView: UIStackView {
|
|||
case .notificationQuote: layoutNotificationQuote()
|
||||
case .composeStatusReplica: layoutComposeStatusReplica()
|
||||
case .composeStatusAuthor: layoutComposeStatusAuthor()
|
||||
case .editHistory: layoutBase()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -158,6 +159,10 @@ extension StatusAuthorView {
|
|||
public func setupAuthorMenu(menuContext: AuthorMenuContext) -> (UIMenu, [UIAccessibilityCustomAction]) {
|
||||
var actions = [MastodonMenu.Action]()
|
||||
|
||||
if menuContext.isMyself {
|
||||
actions.append(.editStatus)
|
||||
}
|
||||
|
||||
if !menuContext.isMyself {
|
||||
if let statusLanguage = menuContext.statusLanguage, menuContext.isTranslationEnabled, !menuContext.isTranslated {
|
||||
actions.append(
|
||||
|
|
|
@ -0,0 +1,149 @@
|
|||
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import MastodonAsset
|
||||
|
||||
public final class StatusMetricRowView: UIButton {
|
||||
let icon: UIImageView
|
||||
let textLabel: UILabel
|
||||
let detailLabel: UILabel
|
||||
let chevron: UIImageView
|
||||
|
||||
private var disposableConstraints: [NSLayoutConstraint] = []
|
||||
private var isVerticalAxis: Bool?
|
||||
|
||||
public init(iconImage: UIImage? = nil, text: String? = nil, detailText: String? = nil) {
|
||||
|
||||
icon = UIImageView(image: iconImage?.withRenderingMode(.alwaysTemplate))
|
||||
icon.tintColor = Asset.Colors.Label.secondary.color
|
||||
icon.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
textLabel = UILabel()
|
||||
textLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
textLabel.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular))
|
||||
textLabel.textColor = Asset.Colors.Label.primary.color
|
||||
textLabel.numberOfLines = 0
|
||||
textLabel.text = text
|
||||
|
||||
detailLabel = UILabel()
|
||||
detailLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
detailLabel.text = detailText
|
||||
detailLabel.textColor = Asset.Colors.Label.secondary.color
|
||||
detailLabel.numberOfLines = 0
|
||||
detailLabel.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular))
|
||||
|
||||
chevron = UIImageView(image: UIImage(systemName: "chevron.right"))
|
||||
chevron.translatesAutoresizingMaskIntoConstraints = false
|
||||
chevron.tintColor = Asset.Colors.Label.tertiary.color
|
||||
|
||||
super.init(frame: .zero)
|
||||
|
||||
addSubview(icon)
|
||||
addSubview(textLabel)
|
||||
addSubview(detailLabel)
|
||||
addSubview(chevron)
|
||||
|
||||
accessibilityTraits.insert(.button)
|
||||
|
||||
setupConstraints()
|
||||
traitCollectionDidChange(nil)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
let isVerticalAxis = traitCollection.preferredContentSizeCategory.isAccessibilityCategory
|
||||
|
||||
if isVerticalAxis {
|
||||
detailLabel.textAlignment = .natural
|
||||
} else {
|
||||
switch traitCollection.layoutDirection {
|
||||
case .leftToRight, .unspecified: detailLabel.textAlignment = .right
|
||||
case .rightToLeft: detailLabel.textAlignment = .left
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
guard isVerticalAxis != self.isVerticalAxis else { return }
|
||||
self.isVerticalAxis = isVerticalAxis
|
||||
NSLayoutConstraint.deactivate(disposableConstraints)
|
||||
|
||||
if isVerticalAxis {
|
||||
disposableConstraints = [
|
||||
textLabel.topAnchor.constraint(equalTo: topAnchor, constant: 11),
|
||||
|
||||
detailLabel.leadingAnchor.constraint(equalTo: textLabel.leadingAnchor),
|
||||
detailLabel.topAnchor.constraint(equalTo: textLabel.bottomAnchor, constant: 8),
|
||||
bottomAnchor.constraint(equalTo: detailLabel.bottomAnchor, constant: 11),
|
||||
|
||||
chevron.leadingAnchor.constraint(greaterThanOrEqualTo: textLabel.trailingAnchor, constant: 12),
|
||||
chevron.leadingAnchor.constraint(greaterThanOrEqualTo: detailLabel.trailingAnchor, constant: 12),
|
||||
]
|
||||
} else {
|
||||
disposableConstraints = [
|
||||
textLabel.topAnchor.constraint(greaterThanOrEqualTo: topAnchor, constant: 11),
|
||||
textLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||
bottomAnchor.constraint(greaterThanOrEqualTo: textLabel.bottomAnchor, constant: 11),
|
||||
|
||||
detailLabel.leadingAnchor.constraint(greaterThanOrEqualTo: textLabel.trailingAnchor, constant: 8),
|
||||
|
||||
detailLabel.topAnchor.constraint(greaterThanOrEqualTo: topAnchor, constant: 11),
|
||||
detailLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||
bottomAnchor.constraint(greaterThanOrEqualTo: detailLabel.bottomAnchor, constant: 11),
|
||||
|
||||
chevron.leadingAnchor.constraint(equalTo: detailLabel.trailingAnchor, constant: 12),
|
||||
]
|
||||
}
|
||||
NSLayoutConstraint.activate(disposableConstraints)
|
||||
}
|
||||
|
||||
var margin: CGFloat = 0 {
|
||||
didSet {
|
||||
layoutMargins = UIEdgeInsets(horizontal: margin, vertical: 0)
|
||||
}
|
||||
}
|
||||
|
||||
private func setupConstraints() {
|
||||
icon.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
||||
chevron.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
||||
chevron.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||
let constraints = [
|
||||
icon.topAnchor.constraint(greaterThanOrEqualTo: topAnchor, constant: 10),
|
||||
icon.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
|
||||
icon.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||
textLabel.leadingAnchor.constraint(equalTo: icon.trailingAnchor, constant: 16),
|
||||
icon.widthAnchor.constraint(greaterThanOrEqualToConstant: 24),
|
||||
icon.heightAnchor.constraint(greaterThanOrEqualToConstant: 24),
|
||||
bottomAnchor.constraint(greaterThanOrEqualTo: icon.bottomAnchor, constant: 10),
|
||||
|
||||
chevron.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||
layoutMarginsGuide.trailingAnchor.constraint(equalTo: chevron.trailingAnchor),
|
||||
]
|
||||
|
||||
NSLayoutConstraint.activate(constraints)
|
||||
}
|
||||
|
||||
public override var isHighlighted: Bool {
|
||||
get { super.isHighlighted }
|
||||
set {
|
||||
super.isHighlighted = newValue
|
||||
if newValue {
|
||||
backgroundColor = Asset.Colors.selectionHighlight.color
|
||||
} else {
|
||||
backgroundColor = .clear
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override var accessibilityLabel: String? {
|
||||
get { textLabel.text }
|
||||
set {}
|
||||
}
|
||||
|
||||
public override var accessibilityValue: String? {
|
||||
get { detailLabel.text }
|
||||
set {}
|
||||
}
|
||||
}
|
|
@ -5,29 +5,45 @@
|
|||
// Created by MainasuK on 2022-1-17.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
|
||||
protocol StatusMetricViewDelegate: AnyObject {
|
||||
func statusMetricView(_ statusMetricView: StatusMetricView, reblogButtonDidPressed button: UIButton)
|
||||
func statusMetricView(_ statusMetricView: StatusMetricView, favoriteButtonDidPressed button: UIButton)
|
||||
func statusMetricView(_ statusMetricView: StatusMetricView, didPressEditHistoryButton button: UIButton)
|
||||
}
|
||||
|
||||
public final class StatusMetricView: UIView {
|
||||
|
||||
let logger = Logger(subsystem: "StatusMetricView", category: "View")
|
||||
|
||||
|
||||
weak var delegate: StatusMetricViewDelegate?
|
||||
|
||||
var margin: CGFloat = 0 {
|
||||
didSet {
|
||||
dateAdaptiveMarginContainerView.margin = margin
|
||||
reblogButton.margin = margin
|
||||
favoriteButton.margin = margin
|
||||
editHistoryButton.margin = margin
|
||||
}
|
||||
}
|
||||
|
||||
// container
|
||||
public let containerStackView: UIStackView = {
|
||||
private let containerStackView: UIStackView = {
|
||||
let stackView = UIStackView()
|
||||
stackView.axis = .horizontal
|
||||
stackView.spacing = 4
|
||||
stackView.axis = .vertical
|
||||
stackView.alignment = .leading
|
||||
return stackView
|
||||
}()
|
||||
|
||||
private let separator: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = Asset.Theme.Mastodon.separator.color
|
||||
return view
|
||||
}()
|
||||
|
||||
// date
|
||||
let dateAdaptiveMarginContainerView = AdaptiveMarginContainerView()
|
||||
public let dateLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .systemFont(ofSize: 15, weight: .regular))
|
||||
|
@ -35,33 +51,34 @@ public final class StatusMetricView: UIView {
|
|||
label.adjustsFontSizeToFitWidth = true
|
||||
label.minimumScaleFactor = 0.5
|
||||
label.numberOfLines = 2
|
||||
label.textColor = Asset.Colors.Label.secondary.color
|
||||
return label
|
||||
}()
|
||||
|
||||
// meter
|
||||
public let meterContainer: UIStackView = {
|
||||
// reblog meter
|
||||
private let buttonStackView: UIStackView = {
|
||||
let stackView = UIStackView()
|
||||
stackView.axis = .horizontal
|
||||
stackView.spacing = 20
|
||||
stackView.axis = .vertical
|
||||
stackView.alignment = .leading
|
||||
return stackView
|
||||
}()
|
||||
|
||||
// reblog meter
|
||||
public let reblogButton: UIButton = {
|
||||
let button = UIButton(type: .system)
|
||||
button.titleLabel?.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold))
|
||||
button.setTitle("0 reblog", for: .normal)
|
||||
|
||||
public let reblogButton: StatusMetricRowView = {
|
||||
let button = StatusMetricRowView(iconImage: Asset.Arrow.repeat.image, text: L10n.Common.Controls.Status.Buttons.reblogsTitle, detailText: "")
|
||||
return button
|
||||
}()
|
||||
|
||||
// favorite meter
|
||||
public let favoriteButton: UIButton = {
|
||||
let button = UIButton(type: .system)
|
||||
button.titleLabel?.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold))
|
||||
button.setTitle("0 favorite", for: .normal)
|
||||
public let favoriteButton: StatusMetricRowView = {
|
||||
let button = StatusMetricRowView(iconImage: UIImage(systemName: "star"), text: L10n.Common.Controls.Status.Buttons.favoritesTitle, detailText: "")
|
||||
return button
|
||||
}()
|
||||
|
||||
|
||||
public let editHistoryButton: StatusMetricRowView = {
|
||||
let button = StatusMetricRowView(iconImage: Asset.Scene.EditHistory.edit.image, text: L10n.Common.Controls.Status.Buttons.editHistoryTitle)
|
||||
return button
|
||||
}()
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
_init()
|
||||
|
@ -71,7 +88,6 @@ public final class StatusMetricView: UIView {
|
|||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension StatusMetricView {
|
||||
|
@ -79,40 +95,63 @@ extension StatusMetricView {
|
|||
// container: H - [ dateLabel | meterContainer ]
|
||||
containerStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(containerStackView)
|
||||
|
||||
separator.translatesAutoresizingMaskIntoConstraints = false
|
||||
containerStackView.addArrangedSubview(separator)
|
||||
|
||||
reblogButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
buttonStackView.addArrangedSubview(reblogButton)
|
||||
|
||||
favoriteButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
buttonStackView.addArrangedSubview(favoriteButton)
|
||||
|
||||
editHistoryButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
buttonStackView.addArrangedSubview(editHistoryButton)
|
||||
|
||||
buttonStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
containerStackView.addArrangedSubview(buttonStackView)
|
||||
containerStackView.setCustomSpacing(11, after: buttonStackView)
|
||||
|
||||
dateLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
dateAdaptiveMarginContainerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
dateAdaptiveMarginContainerView.contentView = dateLabel
|
||||
containerStackView.addArrangedSubview(dateAdaptiveMarginContainerView)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
containerStackView.topAnchor.constraint(equalTo: topAnchor, constant: 8),
|
||||
containerStackView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
containerStackView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
bottomAnchor.constraint(equalTo: containerStackView.bottomAnchor, constant: 12),
|
||||
|
||||
separator.heightAnchor.constraint(equalToConstant: 0.5),
|
||||
|
||||
buttonStackView.widthAnchor.constraint(equalTo: containerStackView.widthAnchor),
|
||||
|
||||
reblogButton.widthAnchor.constraint(equalTo: buttonStackView.widthAnchor),
|
||||
favoriteButton.widthAnchor.constraint(equalTo: reblogButton.widthAnchor),
|
||||
editHistoryButton.widthAnchor.constraint(equalTo: reblogButton.widthAnchor),
|
||||
dateLabel.widthAnchor.constraint(equalTo: reblogButton.widthAnchor),
|
||||
])
|
||||
|
||||
containerStackView.addArrangedSubview(dateLabel)
|
||||
dateLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||
containerStackView.addArrangedSubview(meterContainer)
|
||||
|
||||
// meterContainer: H - [ reblogButton | favoriteButton ]
|
||||
meterContainer.addArrangedSubview(reblogButton)
|
||||
meterContainer.addArrangedSubview(favoriteButton)
|
||||
reblogButton.setContentHuggingPriority(.required - 2, for: .horizontal)
|
||||
reblogButton.setContentCompressionResistancePriority(.required - 2, for: .horizontal)
|
||||
favoriteButton.setContentHuggingPriority(.required - 1, for: .horizontal)
|
||||
favoriteButton.setContentCompressionResistancePriority(.required - 1, for: .horizontal)
|
||||
|
||||
reblogButton.addTarget(self, action: #selector(StatusMetricView.reblogButtonDidPressed(_:)), for: .touchUpInside)
|
||||
favoriteButton.addTarget(self, action: #selector(StatusMetricView.favoriteButtonDidPressed(_:)), for: .touchUpInside)
|
||||
|
||||
reblogButton.addTarget(self, action: #selector(StatusMetricView.didPressReblogButton(_:)), for: .touchUpInside)
|
||||
favoriteButton.addTarget(self, action: #selector(StatusMetricView.didPressFavoriteButton(_:)), for: .touchUpInside)
|
||||
editHistoryButton.addTarget(self, action: #selector(StatusMetricView.didPressEditHistoryButton(_:)), for: .touchUpInside)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
extension StatusMetricView {
|
||||
|
||||
@objc private func reblogButtonDidPressed(_ sender: UIButton) {
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
||||
@objc private func didPressReblogButton(_ sender: UIButton) {
|
||||
delegate?.statusMetricView(self, reblogButtonDidPressed: sender)
|
||||
}
|
||||
|
||||
@objc private func favoriteButtonDidPressed(_ sender: UIButton) {
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
||||
@objc private func didPressFavoriteButton(_ sender: UIButton) {
|
||||
delegate?.statusMetricView(self, favoriteButtonDidPressed: sender)
|
||||
}
|
||||
|
||||
|
||||
@objc private func didPressEditHistoryButton(_ sender: UIButton) {
|
||||
delegate?.statusMetricView(self, didPressEditHistoryButton: sender)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -39,6 +39,33 @@ extension StatusView {
|
|||
}
|
||||
|
||||
extension StatusView {
|
||||
|
||||
public func configure(status: Status, statusEdit: StatusEdit) {
|
||||
viewModel.objects.insert(status)
|
||||
if let reblog = status.reblog {
|
||||
viewModel.objects.insert(reblog)
|
||||
}
|
||||
|
||||
configureHeader(status: status)
|
||||
let author = (status.reblog ?? status).author
|
||||
configureAuthor(author: author)
|
||||
let timestamp = (status.reblog ?? status).publisher(for: \.createdAt)
|
||||
configureTimestamp(timestamp: timestamp.eraseToAnyPublisher())
|
||||
configureApplicationName(status.application?.name)
|
||||
configureMedia(status: status)
|
||||
configurePollHistory(statusEdit: statusEdit)
|
||||
configureCard(status: status)
|
||||
configureToolbar(status: status)
|
||||
configureFilter(status: status)
|
||||
configureContent(statusEdit: statusEdit, status: status)
|
||||
configureMedia(status: statusEdit)
|
||||
actionToolbarAdaptiveMarginContainerView.isHidden = true
|
||||
authorView.menuButton.isHidden = true
|
||||
headerAdaptiveMarginContainerView.isHidden = true
|
||||
viewModel.isSensitiveToggled = true
|
||||
viewModel.isContentReveal = true
|
||||
}
|
||||
|
||||
public func configure(status: Status) {
|
||||
viewModel.objects.insert(status)
|
||||
if let reblog = status.reblog {
|
||||
|
@ -50,6 +77,7 @@ extension StatusView {
|
|||
configureAuthor(author: author)
|
||||
let timestamp = (status.reblog ?? status).publisher(for: \.createdAt)
|
||||
configureTimestamp(timestamp: timestamp.eraseToAnyPublisher())
|
||||
configureApplicationName(status.application?.name)
|
||||
configureContent(status: status)
|
||||
configureMedia(status: status)
|
||||
configurePoll(status: status)
|
||||
|
@ -113,7 +141,7 @@ extension StatusView {
|
|||
let header = ViewModel.Header.reply(info: .init(header: metaContent))
|
||||
return header
|
||||
}
|
||||
|
||||
|
||||
if let replyTo = status.replyTo {
|
||||
// A. replyTo status exist
|
||||
let header = createHeader(name: replyTo.author.displayNameWithFallback, emojis: replyTo.author.emojis.asDictionary)
|
||||
|
@ -234,14 +262,21 @@ extension StatusView {
|
|||
|
||||
private func configureTimestamp(timestamp: AnyPublisher<Date, Never>) {
|
||||
// timestamp
|
||||
viewModel.timestampFormatter = { (date: Date) in
|
||||
date.localizedSlowedTimeAgoSinceNow
|
||||
viewModel.timestampFormatter = { (date: Date, isEdited: Bool) in
|
||||
if isEdited {
|
||||
return L10n.Common.Controls.Status.editedAtTimestampPrefix(date.localizedSlowedTimeAgoSinceNow)
|
||||
}
|
||||
return date.localizedSlowedTimeAgoSinceNow
|
||||
}
|
||||
timestamp
|
||||
.map { $0 as Date? }
|
||||
.assign(to: \.timestamp, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
private func configureApplicationName(_ applicationName: String?) {
|
||||
viewModel.applicationName = applicationName
|
||||
}
|
||||
|
||||
func revertTranslation() {
|
||||
guard let originalStatus = viewModel.originalStatus else { return }
|
||||
|
@ -281,7 +316,27 @@ extension StatusView {
|
|||
viewModel.content = PlaintextMetaContent(string: "")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func configureContent(statusEdit: StatusEdit, status: Status) {
|
||||
statusEdit.spoilerText.map {
|
||||
viewModel.spoilerContent = PlaintextMetaContent(string: $0)
|
||||
}
|
||||
|
||||
// language
|
||||
viewModel.language = (status.reblog ?? status).language
|
||||
// content
|
||||
do {
|
||||
let content = MastodonContent(content: statusEdit.content, emojis: statusEdit.emojis.asDictionary)
|
||||
let metaContent = try MastodonMetaContent.convert(document: content)
|
||||
viewModel.content = metaContent
|
||||
viewModel.translatedFromLanguage = nil
|
||||
viewModel.isCurrentlyTranslating = false
|
||||
} catch {
|
||||
assertionFailure(error.localizedDescription)
|
||||
viewModel.content = PlaintextMetaContent(string: "")
|
||||
}
|
||||
}
|
||||
|
||||
private func configureContent(status: Status) {
|
||||
guard status.translatedContent == nil else {
|
||||
return configureTranslated(status: status)
|
||||
|
@ -327,7 +382,7 @@ extension StatusView {
|
|||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
private func configureMedia(status: Status) {
|
||||
private func configureMedia(status: StatusCompatible) {
|
||||
let status = status.reblog ?? status
|
||||
|
||||
viewModel.isMediaSensitive = status.isMediaSensitive
|
||||
|
@ -335,6 +390,19 @@ extension StatusView {
|
|||
let configurations = MediaView.configuration(status: status)
|
||||
viewModel.mediaViewConfigurations = configurations
|
||||
}
|
||||
|
||||
private func configurePollHistory(statusEdit: StatusEdit) {
|
||||
guard let poll = statusEdit.poll else { return }
|
||||
|
||||
let pollItems = poll.options.map { PollItem.history(option: $0) }
|
||||
self.viewModel.pollItems = pollItems
|
||||
pollStatusStackView.isHidden = true
|
||||
|
||||
var _snapshot = NSDiffableDataSourceSnapshot<PollSection, PollItem>()
|
||||
_snapshot.appendSections([.main])
|
||||
_snapshot.appendItems(pollItems, toSection: .main)
|
||||
pollTableViewDiffableDataSource?.applySnapshotUsingReloadData(_snapshot)
|
||||
}
|
||||
|
||||
private func configurePoll(status: Status) {
|
||||
let status = status.reblog ?? status
|
||||
|
@ -433,6 +501,16 @@ extension StatusView {
|
|||
.map(Int.init)
|
||||
.assign(to: \.favoriteCount, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
status.publisher(for: \.editedAt)
|
||||
.assign(to: \.editedAt, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
status.publisher(for: \.editHistory)
|
||||
.compactMap({ guard let edits = $0 else { return nil }
|
||||
return Array(edits)
|
||||
})
|
||||
.assign(to: \.statusEdits, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// relationship
|
||||
status.publisher(for: \.rebloggedBy)
|
||||
|
|
|
@ -30,7 +30,7 @@ extension StatusView {
|
|||
public var context: AppContext?
|
||||
public var authContext: AuthContext?
|
||||
public var originalStatus: Status?
|
||||
|
||||
|
||||
// Header
|
||||
@Published public var header: Header = .none
|
||||
|
||||
|
@ -52,8 +52,9 @@ extension StatusView {
|
|||
@Published public var translatedUsingProvider: String?
|
||||
|
||||
@Published public var timestamp: Date?
|
||||
public var timestampFormatter: ((_ date: Date) -> String)?
|
||||
public var timestampFormatter: ((_ date: Date, _ isEdited: Bool) -> String)?
|
||||
@Published public var timestampText = ""
|
||||
@Published public var applicationName: String? = nil
|
||||
|
||||
// Spoiler
|
||||
@Published public var spoilerContent: MetaContent?
|
||||
|
@ -101,6 +102,9 @@ extension StatusView {
|
|||
@Published public var replyCount: Int = 0
|
||||
@Published public var reblogCount: Int = 0
|
||||
@Published public var favoriteCount: Int = 0
|
||||
|
||||
@Published public var statusEdits: [StatusEdit] = []
|
||||
@Published public var editedAt: Date? = nil
|
||||
|
||||
// Filter
|
||||
@Published public var activeFilters: [Mastodon.Entity.Filter] = []
|
||||
|
@ -264,16 +268,19 @@ extension StatusView.ViewModel {
|
|||
}
|
||||
.store(in: &disposeBag)
|
||||
// timestamp
|
||||
Publishers.CombineLatest(
|
||||
Publishers.CombineLatest3(
|
||||
$timestamp,
|
||||
$editedAt,
|
||||
timestampUpdatePublisher.prepend(Date()).eraseToAnyPublisher()
|
||||
)
|
||||
.compactMap { [weak self] timestamp, _ -> String? in
|
||||
.compactMap { [weak self] timestamp, editedAt, _ -> String? in
|
||||
guard let self = self else { return nil }
|
||||
guard let timestamp = timestamp,
|
||||
let text = self.timestampFormatter?(timestamp)
|
||||
else { return "" }
|
||||
return text
|
||||
if let timestamp = editedAt, let text = self.timestampFormatter?(timestamp, true) {
|
||||
return text
|
||||
} else if let timestamp = timestamp, let text = self.timestampFormatter?(timestamp, false) {
|
||||
return text
|
||||
}
|
||||
return ""
|
||||
}
|
||||
.removeDuplicates()
|
||||
.assign(to: &$timestampText)
|
||||
|
@ -304,6 +311,16 @@ extension StatusView.ViewModel {
|
|||
// statusView.spoilerBannerView.label.reset()
|
||||
}
|
||||
|
||||
if statusView.style == .editHistory, let spoilerContent = spoilerContent, !spoilerContent.string.isEmpty {
|
||||
statusView.historyContentWarningLabel.configure(content: spoilerContent)
|
||||
statusView.historyContentWarningAdaptiveMarginContainerView.isHidden = statusView.style != .editHistory
|
||||
statusView.setContentSensitiveeToggleButtonDisplay(isDisplay: false)
|
||||
} else {
|
||||
statusView.historyContentWarningLabel.reset()
|
||||
statusView.historyContentWarningAdaptiveMarginContainerView.isHidden = true
|
||||
statusView.setContentSensitiveeToggleButtonDisplay(isDisplay: false)
|
||||
}
|
||||
|
||||
let paragraphStyle = statusView.contentMetaText.paragraphStyle
|
||||
if let language = language {
|
||||
if #available(iOS 16, *) {
|
||||
|
@ -571,12 +588,13 @@ extension StatusView.ViewModel {
|
|||
favoriteButtonTitle
|
||||
).map { $0.count + $1.count }
|
||||
|
||||
Publishers.CombineLatest(
|
||||
Publishers.CombineLatest3(
|
||||
$timestamp,
|
||||
$applicationName,
|
||||
metricButtonTitleLength
|
||||
)
|
||||
.sink { timestamp, metricButtonTitleLength in
|
||||
let text: String = {
|
||||
.sink { timestamp, applicationName, metricButtonTitleLength in
|
||||
let dateString: String = {
|
||||
guard let timestamp = timestamp else { return " " }
|
||||
|
||||
let formatter = DateFormatter()
|
||||
|
@ -591,20 +609,42 @@ extension StatusView.ViewModel {
|
|||
}
|
||||
return formatter.string(from: timestamp)
|
||||
}()
|
||||
|
||||
|
||||
let text: String
|
||||
if let applicationName {
|
||||
text = L10n.Common.Controls.Status.postedViaApplication(dateString, applicationName)
|
||||
} else {
|
||||
text = dateString
|
||||
}
|
||||
|
||||
statusView.statusMetricView.dateLabel.text = text
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
reblogButtonTitle
|
||||
.sink { title in
|
||||
statusView.statusMetricView.reblogButton.setTitle(title, for: .normal)
|
||||
$reblogCount
|
||||
.sink { count in
|
||||
statusView.statusMetricView.reblogButton.isHidden = count == 0
|
||||
statusView.statusMetricView.reblogButton.detailLabel.text = count.formatted()
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
favoriteButtonTitle
|
||||
.sink { title in
|
||||
statusView.statusMetricView.favoriteButton.setTitle(title, for: .normal)
|
||||
$favoriteCount
|
||||
.sink { count in
|
||||
statusView.statusMetricView.favoriteButton.isHidden = count == 0
|
||||
statusView.statusMetricView.favoriteButton.detailLabel.text = count.formatted()
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
$editedAt
|
||||
.sink { editedAt in
|
||||
if let editedAt {
|
||||
let relativeDateFormatter = RelativeDateTimeFormatter()
|
||||
let relativeDate = relativeDateFormatter.localizedString(for: editedAt, relativeTo: Date())
|
||||
statusView.statusMetricView.editHistoryButton.detailLabel.text = L10n.Common.Controls.Status.Buttons.editHistoryDetail(relativeDate)
|
||||
statusView.statusMetricView.editHistoryButton.isHidden = false
|
||||
} else {
|
||||
statusView.statusMetricView.editHistoryButton.isHidden = true
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
@ -705,8 +745,10 @@ extension StatusView.ViewModel {
|
|||
strings.append(info.header.string)
|
||||
strings.append(authorName?.string)
|
||||
}
|
||||
|
||||
strings.append(timestamp)
|
||||
|
||||
if statusView.style != .editHistory {
|
||||
strings.append(timestamp)
|
||||
}
|
||||
|
||||
return strings.compactMap { $0 }.joined(separator: ", ")
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ public protocol StatusViewDelegate: AnyObject {
|
|||
func statusView(_ statusView: StatusView, mediaGridContainerView: MediaGridContainerView, mediaSensitiveButtonDidPressed button: UIButton)
|
||||
func statusView(_ statusView: StatusView, statusMetricView: StatusMetricView, reblogButtonDidPressed button: UIButton)
|
||||
func statusView(_ statusView: StatusView, statusMetricView: StatusMetricView, favoriteButtonDidPressed button: UIButton)
|
||||
func statusView(_ statusView: StatusView, statusMetricView: StatusMetricView, showEditHistory button: UIButton)
|
||||
func statusView(_ statusView: StatusView, cardControl: StatusCardControl, didTapURL url: URL)
|
||||
func statusView(_ statusView: StatusView, cardControlMenu: StatusCardControl) -> UIMenu?
|
||||
|
||||
|
@ -87,6 +88,46 @@ public final class StatusView: UIView {
|
|||
// author
|
||||
let authorAdaptiveMarginContainerView = AdaptiveMarginContainerView()
|
||||
public let authorView = StatusAuthorView()
|
||||
|
||||
// edit history content warning
|
||||
lazy var historyContentWarningAdaptiveMarginContainerView: AdaptiveMarginContainerView = {
|
||||
let view = AdaptiveMarginContainerView()
|
||||
view.contentView = historyContentWarningContainerView
|
||||
view.margin = StatusView.containerLayoutMargin
|
||||
return view
|
||||
}()
|
||||
|
||||
let historyContentWarningLabel: MetaLabel = {
|
||||
let label = MetaLabel(style: .statusSpoilerBanner)
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
return label
|
||||
}()
|
||||
|
||||
lazy var historyContentWarningContainerView: UIView = {
|
||||
let container = UIView()
|
||||
container.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let divider = UIView()
|
||||
divider.backgroundColor = Asset.Colors.Label.secondary.color
|
||||
divider.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
container.addSubview(historyContentWarningLabel)
|
||||
container.addSubview(divider)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
historyContentWarningLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor),
|
||||
historyContentWarningLabel.topAnchor.constraint(equalTo: container.topAnchor),
|
||||
historyContentWarningLabel.trailingAnchor.constraint(equalTo: container.trailingAnchor),
|
||||
|
||||
divider.leadingAnchor.constraint(equalTo: container.leadingAnchor),
|
||||
divider.topAnchor.constraint(equalTo: historyContentWarningLabel.bottomAnchor, constant: 16),
|
||||
divider.trailingAnchor.constraint(equalTo: container.trailingAnchor),
|
||||
divider.heightAnchor.constraint(equalToConstant: 2),
|
||||
divider.bottomAnchor.constraint(equalTo: container.bottomAnchor)
|
||||
])
|
||||
|
||||
return container
|
||||
}()
|
||||
|
||||
// content
|
||||
let contentAdaptiveMarginContainerView = AdaptiveMarginContainerView()
|
||||
|
@ -255,7 +296,6 @@ public final class StatusView: UIView {
|
|||
public let actionToolbarContainer = ActionToolbarContainer()
|
||||
|
||||
// metric
|
||||
let statusMetricViewAdaptiveMarginContainerView = AdaptiveMarginContainerView()
|
||||
public let statusMetricView = StatusMetricView()
|
||||
|
||||
// filter hint
|
||||
|
@ -397,6 +437,7 @@ extension StatusView {
|
|||
case notificationQuote
|
||||
case composeStatusReplica
|
||||
case composeStatusAuthor
|
||||
case editHistory
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -411,6 +452,7 @@ extension StatusView.Style {
|
|||
case .notificationQuote: notificationQuote(statusView: statusView)
|
||||
case .composeStatusReplica: composeStatusReplica(statusView: statusView)
|
||||
case .composeStatusAuthor: composeStatusAuthor(statusView: statusView)
|
||||
case .editHistory: editHistory(statusView: statusView)
|
||||
}
|
||||
|
||||
statusView.authorView.layout(style: self)
|
||||
|
@ -448,6 +490,9 @@ extension StatusView.Style {
|
|||
statusView.authorAdaptiveMarginContainerView.contentView = statusView.authorView
|
||||
statusView.authorAdaptiveMarginContainerView.margin = StatusView.containerLayoutMargin
|
||||
statusView.containerStackView.addArrangedSubview(statusView.authorAdaptiveMarginContainerView)
|
||||
|
||||
// history content warning
|
||||
statusView.containerStackView.addArrangedSubview(statusView.historyContentWarningAdaptiveMarginContainerView)
|
||||
|
||||
// content container: V - [ contentMetaText statusCardControl ]
|
||||
statusView.contentContainer.axis = .vertical
|
||||
|
@ -534,16 +579,8 @@ extension StatusView.Style {
|
|||
base(statusView: statusView) // override the base style
|
||||
|
||||
// statusMetricView
|
||||
statusView.statusMetricViewAdaptiveMarginContainerView.contentView = statusView.statusMetricView
|
||||
statusView.statusMetricViewAdaptiveMarginContainerView.margin = StatusView.containerLayoutMargin
|
||||
statusView.containerStackView.addArrangedSubview(statusView.statusMetricViewAdaptiveMarginContainerView)
|
||||
|
||||
UIContentSizeCategory.publisher
|
||||
.sink { category in
|
||||
statusView.statusMetricView.containerStackView.axis = category > .accessibilityLarge ? .vertical : .horizontal
|
||||
statusView.statusMetricView.containerStackView.alignment = category > .accessibilityLarge ? .leading : .fill
|
||||
}
|
||||
.store(in: &statusView._disposeBag)
|
||||
statusView.statusMetricView.margin = StatusView.containerLayoutMargin
|
||||
statusView.containerStackView.addArrangedSubview(statusView.statusMetricView)
|
||||
}
|
||||
|
||||
func report(statusView: StatusView) {
|
||||
|
@ -585,6 +622,9 @@ extension StatusView.Style {
|
|||
statusView.actionToolbarAdaptiveMarginContainerView.removeFromSuperview()
|
||||
}
|
||||
|
||||
func editHistory(statusView: StatusView) {
|
||||
base(statusView: statusView)
|
||||
}
|
||||
}
|
||||
|
||||
extension StatusView {
|
||||
|
@ -653,7 +693,7 @@ extension StatusView: AdaptiveContainerView {
|
|||
contentAdaptiveMarginContainerView.margin = margin
|
||||
pollAdaptiveMarginContainerView.margin = margin
|
||||
actionToolbarAdaptiveMarginContainerView.margin = margin
|
||||
statusMetricViewAdaptiveMarginContainerView.margin = margin
|
||||
statusMetricView.margin = margin
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -744,6 +784,10 @@ extension StatusView: StatusMetricViewDelegate {
|
|||
func statusMetricView(_ statusMetricView: StatusMetricView, favoriteButtonDidPressed button: UIButton) {
|
||||
delegate?.statusView(self, statusMetricView: statusMetricView, favoriteButtonDidPressed: button)
|
||||
}
|
||||
|
||||
func statusMetricView(_ statusMetricView: StatusMetricView, didPressEditHistoryButton button: UIButton) {
|
||||
delegate?.statusView(self, statusMetricView: statusMetricView, showEditHistory: button)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MastodonMenuDelegate
|
||||
|
|
|
@ -19,10 +19,18 @@ public enum MastodonMenu {
|
|||
) -> UIMenu {
|
||||
var children: [UIMenuElement] = []
|
||||
for action in actions {
|
||||
let element = action.build(delegate: delegate)
|
||||
children.append(element.menuElement)
|
||||
|
||||
let element: UIMenuElement
|
||||
|
||||
if case let .deleteStatus = action {
|
||||
let deleteAction = action.build(delegate: delegate).menuElement
|
||||
element = UIMenu(options: .displayInline, children: [deleteAction])
|
||||
} else {
|
||||
element = action.build(delegate: delegate).menuElement
|
||||
}
|
||||
children.append(element)
|
||||
}
|
||||
return UIMenu(title: "", options: [], children: children)
|
||||
return UIMenu(children: children)
|
||||
}
|
||||
|
||||
public static func setupAccessibilityActions(
|
||||
|
@ -49,6 +57,7 @@ extension MastodonMenu {
|
|||
case hideReblogs(HideReblogsActionContext)
|
||||
case shareStatus
|
||||
case deleteStatus
|
||||
case editStatus
|
||||
|
||||
func build(delegate: MastodonMenuDelegate) -> BuiltAction {
|
||||
switch self {
|
||||
|
@ -136,6 +145,14 @@ extension MastodonMenu {
|
|||
delegate.menuAction(self)
|
||||
}
|
||||
return translateAction
|
||||
case .editStatus:
|
||||
let editStatusAction = BuiltAction(title: L10n.Common.Controls.Actions.editPost, image: UIImage(systemName: "pencil")) {
|
||||
[weak delegate] in
|
||||
guard let delegate else { return }
|
||||
delegate.menuAction(self)
|
||||
}
|
||||
|
||||
return editStatusAction
|
||||
} // end switch
|
||||
} // end func build
|
||||
} // end enum Action
|
||||
|
|
|
@ -32,6 +32,7 @@ final class ShareViewController: UIViewController {
|
|||
button.setTitle(L10n.Scene.Compose.composeAction, for: .normal)
|
||||
return button
|
||||
}()
|
||||
|
||||
private func configurePublishButtonApperance() {
|
||||
publishButton.adjustsImageWhenHighlighted = false
|
||||
publishButton.setBackgroundImage(.placeholder(color: Asset.Colors.Label.primary.color), for: .normal)
|
||||
|
@ -99,6 +100,7 @@ extension ShareViewController {
|
|||
let composeContentViewModel = ComposeContentViewModel(
|
||||
context: context,
|
||||
authContext: authContext,
|
||||
composeContext: .composeStatus,
|
||||
destination: .topLevel,
|
||||
initialContent: ""
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue