diff --git a/Localization/StringsConvertor/input/Base.lproj/app.json b/Localization/StringsConvertor/input/Base.lproj/app.json index 7b102d5a0..4be5fb01b 100644 --- a/Localization/StringsConvertor/input/Base.lproj/app.json +++ b/Localization/StringsConvertor/input/Base.lproj/app.json @@ -729,6 +729,16 @@ "accepted": "Accepted", "reject": "reject", "rejected": "Rejected" + }, + "warning": { + "none": "Your account has received a moderation warning.", + "disable": "Your account has been disabled.", + "mark_statuses_as_sensitive": "Some of your posts have been marked as sensitive.", + "delete_statuses": "Some of your posts have been removed.", + "sensitive": "Your posts will be marked as sensitive from now on.", + "silence": "Your account has been limited.", + "suspend": "Your account has been suspended.", + "learn_more": "Learn More" } }, "thread": { diff --git a/Localization/app.json b/Localization/app.json index 7b102d5a0..4be5fb01b 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -729,6 +729,16 @@ "accepted": "Accepted", "reject": "reject", "rejected": "Rejected" + }, + "warning": { + "none": "Your account has received a moderation warning.", + "disable": "Your account has been disabled.", + "mark_statuses_as_sensitive": "Some of your posts have been marked as sensitive.", + "delete_statuses": "Some of your posts have been removed.", + "sensitive": "Your posts will be marked as sensitive from now on.", + "silence": "Your account has been limited.", + "suspend": "Your account has been suspended.", + "learn_more": "Learn More" } }, "thread": { diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 1c7cbf890..db1d5ebc9 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -162,6 +162,7 @@ D87BFC8F291EC26A00FEE264 /* MastodonLoginServerTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87BFC8E291EC26A00FEE264 /* MastodonLoginServerTableViewCell.swift */; }; D886FBD329DF710F00272017 /* WelcomeSeparatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D886FBD229DF710F00272017 /* WelcomeSeparatorView.swift */; }; D8916DC029211BE500124085 /* ContentSizedTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8916DBF29211BE500124085 /* ContentSizedTableView.swift */; }; + D8A0729D2BEBA8D7001A4C7C /* AccountWarningNotificationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A0729C2BEBA8D7001A4C7C /* AccountWarningNotificationCell.swift */; }; D8A6AB6C291C5136003AB663 /* MastodonLoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A6AB6B291C5136003AB663 /* MastodonLoginViewController.swift */; }; D8B5E4EE2A4EB8930008970C /* NotificationSettingTableViewToggleCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B5E4ED2A4EB8920008970C /* NotificationSettingTableViewToggleCell.swift */; }; D8B5E4F02A4EB8A00008970C /* NotificationSettingTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B5E4EF2A4EB8A00008970C /* NotificationSettingTableViewCell.swift */; }; @@ -796,6 +797,7 @@ D87DC50E2A17C32F00219C5F /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = el; path = el.lproj/Intents.stringsdict; sourceTree = ""; }; D886FBD229DF710F00272017 /* WelcomeSeparatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeSeparatorView.swift; sourceTree = ""; }; D8916DBF29211BE500124085 /* ContentSizedTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentSizedTableView.swift; sourceTree = ""; }; + D8A0729C2BEBA8D7001A4C7C /* AccountWarningNotificationCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountWarningNotificationCell.swift; sourceTree = ""; }; D8A6AB6B291C5136003AB663 /* MastodonLoginViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonLoginViewController.swift; sourceTree = ""; }; D8A6FE6129325F5900666A47 /* Intents.stringsdict */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; path = Intents.stringsdict; sourceTree = ""; }; D8A6FE6229325F5900666A47 /* app.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = app.json; sourceTree = ""; }; @@ -1484,6 +1486,7 @@ DB023D2727A0FABD005AC798 /* NotificationTableViewCellDelegate.swift */, DB63F76E279A7D1100455B82 /* NotificationTableViewCell.swift */, DB63F774279A997D00455B82 /* NotificationTableViewCell+ViewModel.swift */, + D8A0729C2BEBA8D7001A4C7C /* AccountWarningNotificationCell.swift */, ); path = Cell; sourceTree = ""; @@ -1565,7 +1568,6 @@ DB0617F927855B460030EE79 /* Profile */, DB4F097926A039C400D62E92 /* Status */, DB65C63527A2AF52008BAC2E /* Report */, - DB0617F727855B010030EE79 /* Notification */, DB4F097726A039A200D62E92 /* Search */, ); path = Diffable; @@ -1921,15 +1923,6 @@ path = Onboarding; sourceTree = ""; }; - DB0617F727855B010030EE79 /* Notification */ = { - isa = PBXGroup; - children = ( - 2D35237926256D920031AF25 /* NotificationSection.swift */, - 2D7867182625B77500211898 /* NotificationItem.swift */, - ); - path = Notification; - sourceTree = ""; - }; DB0617F827855B170030EE79 /* User */ = { isa = PBXGroup; children = ( @@ -2654,6 +2647,8 @@ D80F627E2B5C32E400877059 /* NotificationView */, DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */, 2D607AD726242FC500B70763 /* NotificationViewModel.swift */, + 2D35237926256D920031AF25 /* NotificationSection.swift */, + 2D7867182625B77500211898 /* NotificationItem.swift */, ); path = Notification; sourceTree = ""; @@ -3445,6 +3440,7 @@ DB5B54AE2833C15F00DEF8B2 /* UserListViewModel+Diffable.swift in Sources */, DB482A3F261331E8008AE74C /* UserTimelineViewModel+State.swift in Sources */, DB3E6FE02806A4ED00B035AE /* DiscoveryHashtagsViewModel.swift in Sources */, + D8A0729D2BEBA8D7001A4C7C /* AccountWarningNotificationCell.swift in Sources */, 2D38F1F725CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift in Sources */, DB0FCB7427956939006C02E2 /* DataSourceFacade+Status.swift in Sources */, D81A22782AB4782400905D71 /* SearchResultOverviewSection.swift in Sources */, diff --git a/Mastodon.xcodeproj/xcshareddata/IDETemplateMacros.plist b/Mastodon.xcodeproj/xcshareddata/IDETemplateMacros.plist new file mode 100644 index 000000000..120490ff8 --- /dev/null +++ b/Mastodon.xcodeproj/xcshareddata/IDETemplateMacros.plist @@ -0,0 +1,3 @@ +{ + FILEHEADER = " Copyright © ___YEAR___ Mastodon gGmbH. All rights reserved."; +} \ No newline at end of file diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index 7259f6540..acf985fee 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -131,7 +131,8 @@ final public class SceneCoordinator { from: from, transition: .show ) - + case .moderationWarning: + break case ._other: assertionFailure() break diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift index 89fd2547a..12285d871 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift @@ -143,7 +143,7 @@ extension DataSourceFacade { else { return } - let mentions = status.entity.mentions ?? [] + let mentions = status.entity.mentions guard let mention = mentions.first(where: { $0.url == href }) else { _ = provider.coordinator.present( diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift index dc2efdeeb..d5e654dca 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift @@ -43,6 +43,14 @@ extension UITableViewDelegate where Self: DataSourceProvider & AuthContextProvid target: .status, // remove reblog wrapper status: status ) + } else if let accountWarning = notification.entity.accountWarning { + let url = Mastodon.API.disputesEndpoint(domain: authContext.mastodonAuthenticationBox.domain, strikeId: accountWarning.id) + _ = coordinator.present( + scene: .safari(url: url), + from: self, + transition: .safariPresent(animated: true, completion: nil) + ) + } else { await DataSourceFacade.coordinateToProfileScene( provider: self, diff --git a/Mastodon/Scene/Notification/Cell/AccountWarningNotificationCell.swift b/Mastodon/Scene/Notification/Cell/AccountWarningNotificationCell.swift new file mode 100644 index 000000000..f6096e2d9 --- /dev/null +++ b/Mastodon/Scene/Notification/Cell/AccountWarningNotificationCell.swift @@ -0,0 +1,88 @@ +// Copyright © 2024 Mastodon gGmbH. All rights reserved. + +import UIKit +import MastodonSDK +import MastodonAsset +import MastodonLocalization + +class AccountWarningNotificationCell: UITableViewCell { + public static let reuseIdentifier = "AccountWarningNotificationCell" + + let iconImageView: UIImageView + let warningLabel: UILabel + let learnMoreLabel: UILabel + + private let contentStackView: UIStackView + private let labelStackView: UIStackView + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + let icon = UIImage(systemName: "exclamationmark.triangle.fill")? + .withConfiguration(UIImage.SymbolConfiguration(font: .systemFont(ofSize: 17))) + iconImageView = UIImageView(image: icon) + iconImageView.tintColor = Asset.Colors.Brand.blurple.color + + warningLabel = UILabel() + warningLabel.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17)) + warningLabel.numberOfLines = 0 + + learnMoreLabel = UILabel() + learnMoreLabel.text = L10n.Scene.Notification.Warning.learnMore + learnMoreLabel.textColor = Asset.Colors.Brand.blurple.color + learnMoreLabel.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17)) + learnMoreLabel.numberOfLines = 0 + + labelStackView = UIStackView(arrangedSubviews: [warningLabel, learnMoreLabel]) + labelStackView.axis = .vertical + labelStackView.alignment = .leading + labelStackView.spacing = 7 + + contentStackView = UIStackView(arrangedSubviews: [iconImageView, labelStackView]) + contentStackView.translatesAutoresizingMaskIntoConstraints = false + contentStackView.axis = .horizontal + contentStackView.alignment = .top + contentStackView.spacing = 16 + + super.init(style: style, reuseIdentifier: reuseIdentifier) + + contentView.addSubview(contentStackView) + setupConstraints() + } + + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + private func setupConstraints() { + let constraints = [ + contentStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16), + contentStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), + contentView.trailingAnchor.constraint(equalTo: contentStackView.trailingAnchor, constant: 16), + contentView.bottomAnchor.constraint(equalTo: contentStackView.bottomAnchor, constant: 16), + ] + + NSLayoutConstraint.activate(constraints) + } + + public func configure(with accountWarning: Mastodon.Entity.AccountWarning) { + warningLabel.text = accountWarning.action.description + } +} + +extension Mastodon.Entity.AccountWarning.Action { + var description: String { + switch self { + case .none: + return L10n.Scene.Notification.Warning.none + case .disable: + return L10n.Scene.Notification.Warning.disable + case .markStatusesAsSensitive: + return L10n.Scene.Notification.Warning.markStatusesAsSensitive + case .deleteStatuses: + return L10n.Scene.Notification.Warning.deleteStatuses + case .sensitive: + return L10n.Scene.Notification.Warning.sensitive + case .silence: + return L10n.Scene.Notification.Warning.silence + case .suspend: + return L10n.Scene.Notification.Warning.suspend + } + } +} diff --git a/Mastodon/Diffable/Notification/NotificationItem.swift b/Mastodon/Scene/Notification/NotificationItem.swift similarity index 100% rename from Mastodon/Diffable/Notification/NotificationItem.swift rename to Mastodon/Scene/Notification/NotificationItem.swift diff --git a/Mastodon/Diffable/Notification/NotificationSection.swift b/Mastodon/Scene/Notification/NotificationSection.swift similarity index 76% rename from Mastodon/Diffable/Notification/NotificationSection.swift rename to Mastodon/Scene/Notification/NotificationSection.swift index 7965ddc0a..ac80faf97 100644 --- a/Mastodon/Diffable/Notification/NotificationSection.swift +++ b/Mastodon/Scene/Notification/NotificationSection.swift @@ -37,20 +37,28 @@ extension NotificationSection { configuration: Configuration ) -> UITableViewDiffableDataSource { tableView.register(NotificationTableViewCell.self, forCellReuseIdentifier: String(describing: NotificationTableViewCell.self)) + tableView.register(AccountWarningNotificationCell.self, forCellReuseIdentifier: AccountWarningNotificationCell.reuseIdentifier) tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self)) - + return UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in switch item { case .feed(let feed): - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: NotificationTableViewCell.self), for: indexPath) as! NotificationTableViewCell - configure( - context: context, - tableView: tableView, - cell: cell, - viewModel: NotificationTableViewCell.ViewModel(value: .feed(feed)), - configuration: configuration - ) - return cell + if let notification = feed.notification, let accountWarning = notification.accountWarning { + let cell = tableView.dequeueReusableCell(withIdentifier: AccountWarningNotificationCell.reuseIdentifier, for: indexPath) as! AccountWarningNotificationCell + cell.configure(with: accountWarning) + return cell + } else { + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: NotificationTableViewCell.self), for: indexPath) as! NotificationTableViewCell + configure( + context: context, + tableView: tableView, + cell: cell, + viewModel: NotificationTableViewCell.ViewModel(value: .feed(feed)), + configuration: configuration + ) + return cell + } + case .feedLoader: let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell cell.activityIndicatorView.startAnimating() diff --git a/Mastodon/Scene/Notification/NotificationView/NotificationView+Configuration.swift b/Mastodon/Scene/Notification/NotificationView/NotificationView+Configuration.swift index 90c7a255e..353f309db 100644 --- a/Mastodon/Scene/Notification/NotificationView/NotificationView+Configuration.swift +++ b/Mastodon/Scene/Notification/NotificationView/NotificationView+Configuration.swift @@ -52,6 +52,9 @@ extension NotificationView { quoteStatusView.configure(status: status) setQuoteStatusViewDisplay() } + case .moderationWarning: + // case handled in `AccountWarningNotificationCell.swift` + break case ._other: setAuthorContainerBottomPaddingViewDisplay() assertionFailure() diff --git a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift index 886b138b1..4395d1104 100644 --- a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift +++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift @@ -272,7 +272,7 @@ extension MastodonPickServerViewController { authenticationViewModel.isAuthenticating.send(true) - context.apiService.instance(domain: server.domain) + context.apiService.instance(domain: server.domain, authenticationBox: nil) .compactMap { [weak self] response -> AnyPublisher? in guard let self = self else { return nil } guard response.value.registrations != false else { diff --git a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift index a9f760a52..f0278bb3e 100644 --- a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift +++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift @@ -167,7 +167,7 @@ extension MastodonPickServerViewModel { self.unindexedServers.value = nil return self.context.apiService.webFinger(domain: domain) .flatMap { domain -> AnyPublisher, Error>, Never> in - return self.context.apiService.instance(domain: domain) + return self.context.apiService.instance(domain: domain, authenticationBox: nil) .map { response -> Result, Error>in let newResponse = response.map { [Mastodon.Entity.Server(domain: domain, instance: $0)] } return Result.success(newResponse) diff --git a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController+Debug.swift b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController+Debug.swift index 7477d3bc5..0cfde37dd 100644 --- a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController+Debug.swift +++ b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController+Debug.swift @@ -22,7 +22,7 @@ extension MastodonRegisterViewController { viewController.context = context viewController.coordinator = coordinator - let instanceResponse = try await context.apiService.instance(domain: domain).singleOutput() + let instanceResponse = try await context.apiService.instance(domain: domain, authenticationBox: nil).singleOutput() let applicationResponse = try await context.apiService.createApplication(domain: domain).singleOutput() let accessTokenResponse = try await context.apiService.applicationAccessToken( domain: domain, diff --git a/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift b/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift index da97a7888..498e99d53 100644 --- a/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift +++ b/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift @@ -283,7 +283,7 @@ extension WelcomeViewController { authenticationViewModel.isAuthenticating.send(true) - context.apiService.instance(domain: server.domain) + context.apiService.instance(domain: server.domain, authenticationBox: nil) .compactMap { [weak self] response -> AnyPublisher? in guard let self = self else { return nil } guard response.value.registrations != false else { diff --git a/Mastodon/Scene/Report/Report/ReportViewModel.swift b/Mastodon/Scene/Report/Report/ReportViewModel.swift index cff16063a..c8896ef4f 100644 --- a/Mastodon/Scene/Report/Report/ReportViewModel.swift +++ b/Mastodon/Scene/Report/Report/ReportViewModel.swift @@ -67,7 +67,7 @@ class ReportViewModel { // bind server rules Task { @MainActor in do { - let response = try await context.apiService.instance(domain: authContext.mastodonAuthenticationBox.domain) + let response = try await context.apiService.instance(domain: authContext.mastodonAuthenticationBox.domain, authenticationBox: authContext.mastodonAuthenticationBox) .timeout(3, scheduler: DispatchQueue.main) .singleOutput() let rules = response.value.rules ?? [] diff --git a/Mastodon/Scene/Settings/SettingsCoordinator.swift b/Mastodon/Scene/Settings/SettingsCoordinator.swift index c465ca139..502598a2f 100644 --- a/Mastodon/Scene/Settings/SettingsCoordinator.swift +++ b/Mastodon/Scene/Settings/SettingsCoordinator.swift @@ -75,7 +75,7 @@ extension SettingsCoordinator: SettingsViewControllerDelegate { let serverDetailsViewController = ServerDetailsViewController(domain: domain, appContext: appContext, authContext: authContext, sceneCoordinator: sceneCoordinator) serverDetailsViewController.delegate = self - appContext.apiService.instanceV2(domain: domain) + appContext.apiService.instanceV2(domain: domain, authenticationBox: authContext.mastodonAuthenticationBox) .sink { _ in } receiveValue: { content in @@ -83,7 +83,7 @@ extension SettingsCoordinator: SettingsViewControllerDelegate { } .store(in: &disposeBag) - appContext.apiService.extendedDescription(domain: domain) + appContext.apiService.extendedDescription(domain: domain, authenticationBox: authContext.mastodonAuthenticationBox) .sink { _ in } receiveValue: { content in diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Instance.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Instance.swift index 03a63461e..140c37157 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Instance.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Instance.swift @@ -14,18 +14,23 @@ import MastodonSDK extension APIService { public func instance( - domain: String + domain: String, + authenticationBox: MastodonAuthenticationBox? ) -> AnyPublisher, Error> { - return Mastodon.API.Instance.instance(session: session, domain: domain) + return Mastodon.API.Instance.instance(session: session, authorization: authenticationBox?.userAuthorization, domain: domain) } public func instanceV2( - domain: String + domain: String, + authenticationBox: MastodonAuthenticationBox? ) -> AnyPublisher, Error> { - return Mastodon.API.V2.Instance.instance(session: session, domain: domain) + return Mastodon.API.V2.Instance.instance(session: session, authorization: authenticationBox?.userAuthorization, domain: domain) } - public func extendedDescription(domain: String) -> AnyPublisher, Error> { - return Mastodon.API.Instance.extendedDescription(session: session, domain: domain) + public func extendedDescription( + domain: String, + authenticationBox: MastodonAuthenticationBox? + ) -> AnyPublisher, Error> { + return Mastodon.API.Instance.extendedDescription(session: session, authorization: authenticationBox?.userAuthorization, domain: domain) } } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Notification.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Notification.swift index 2a30c2b7c..c6dab4cb2 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Notification.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Notification.swift @@ -60,6 +60,7 @@ extension APIService { .favourite, .poll, .status, + .moderationWarning ] case .mentions: return [ diff --git a/MastodonSDK/Sources/MastodonCore/Service/InstanceService.swift b/MastodonSDK/Sources/MastodonCore/Service/InstanceService.swift index fb4335cc3..5088d8124 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/InstanceService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/InstanceService.swift @@ -45,11 +45,11 @@ public final class InstanceService { extension InstanceService { func updateInstance(domain: String) { - guard let apiService = self.apiService else { return } - apiService.instance(domain: domain) + guard let apiService else { return } + apiService.instance(domain: domain, authenticationBox: authenticationService?.mastodonAuthenticationBoxes.first) .flatMap { [unowned self] response -> AnyPublisher in if response.value.version?.majorServerVersion(greaterThanOrEquals: 4) == true { - return apiService.instanceV2(domain: domain) + return apiService.instanceV2(domain: domain, authenticationBox: authenticationService?.mastodonAuthenticationBoxes.first) .flatMap { return self.updateInstanceV2(domain: domain, response: $0) } .eraseToAnyPublisher() } else { diff --git a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift index d861fccb4..ccf92024e 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift +++ b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift @@ -909,6 +909,24 @@ public enum L10n { /// Mentions public static let mentions = L10n.tr("Localizable", "Scene.Notification.Title.Mentions", fallback: "Mentions") } + public enum Warning { + /// Some of your posts have been removed. + public static let deleteStatuses = L10n.tr("Localizable", "Scene.Notification.Warning.DeleteStatuses", fallback: "Some of your posts have been removed.") + /// Your account has been disabled. + public static let disable = L10n.tr("Localizable", "Scene.Notification.Warning.Disable", fallback: "Your account has been disabled.") + /// Learn More + public static let learnMore = L10n.tr("Localizable", "Scene.Notification.Warning.LearnMore", fallback: "Learn More") + /// Some of your posts have been marked as sensitive. + public static let markStatusesAsSensitive = L10n.tr("Localizable", "Scene.Notification.Warning.MarkStatusesAsSensitive", fallback: "Some of your posts have been marked as sensitive.") + /// Your account has received a moderation warning. + public static let `none` = L10n.tr("Localizable", "Scene.Notification.Warning.None", fallback: "Your account has received a moderation warning.") + /// Your posts will be marked as sensitive from now on. + public static let sensitive = L10n.tr("Localizable", "Scene.Notification.Warning.Sensitive", fallback: "Your posts will be marked as sensitive from now on.") + /// Your account has been limited. + public static let silence = L10n.tr("Localizable", "Scene.Notification.Warning.Silence", fallback: "Your account has been limited.") + /// Your account has been suspended. + public static let suspend = L10n.tr("Localizable", "Scene.Notification.Warning.Suspend", fallback: "Your account has been suspended.") + } } public enum Preview { public enum Keyboard { diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings index 0c9d5f3a7..7ed8427ee 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings @@ -323,6 +323,14 @@ uploaded to Mastodon."; "Scene.Notification.NotificationDescription.RequestToFollowYou" = "request to follow you"; "Scene.Notification.Title.Everything" = "Everything"; "Scene.Notification.Title.Mentions" = "Mentions"; +"Scene.Notification.Warning.DeleteStatuses" = "Some of your posts have been removed."; +"Scene.Notification.Warning.Disable" = "Your account has been disabled."; +"Scene.Notification.Warning.LearnMore" = "Learn More"; +"Scene.Notification.Warning.MarkStatusesAsSensitive" = "Some of your posts have been marked as sensitive."; +"Scene.Notification.Warning.None" = "Your account has received a moderation warning."; +"Scene.Notification.Warning.Sensitive" = "Your posts will be marked as sensitive from now on."; +"Scene.Notification.Warning.Silence" = "Your account has been limited."; +"Scene.Notification.Warning.Suspend" = "Your account has been suspended."; "Scene.Preview.Keyboard.ClosePreview" = "Close Preview"; "Scene.Preview.Keyboard.ShowNext" = "Show Next"; "Scene.Preview.Keyboard.ShowPrevious" = "Show Previous"; diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Disputes.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Disputes.swift new file mode 100644 index 000000000..21e22dad8 --- /dev/null +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Disputes.swift @@ -0,0 +1,9 @@ +// Copyright © 2024 Mastodon gGmbH. All rights reserved. + +import Foundation + +extension Mastodon.API { + public static func disputesEndpoint(domain: String, strikeId: String) -> URL { + return Mastodon.API.webURL(domain: domain).appendingPathComponent("disputes/strikes/\(strikeId)") + } +} diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Instance.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Instance.swift index 871f43632..8cf9430cd 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Instance.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Instance.swift @@ -28,9 +28,10 @@ extension Mastodon.API.Instance { /// - Returns: `AnyPublisher` contains `Instance` nested in the response public static func instance( session: URLSession, + authorization: Mastodon.API.OAuth.Authorization?, domain: String ) -> AnyPublisher, Error> { - let request = Mastodon.API.get(url: instanceEndpointURL(domain: domain)) + let request = Mastodon.API.get(url: instanceEndpointURL(domain: domain), authorization: authorization) return session.dataTaskPublisher(for: request) .tryMap { data, response in let value: Mastodon.Entity.Instance @@ -62,9 +63,10 @@ extension Mastodon.API.Instance { /// [Document](https://docs.joinmastodon.org/methods/instance/#extended_description) public static func extendedDescription( session: URLSession, + authorization: Mastodon.API.OAuth.Authorization?, domain: String ) -> AnyPublisher, Error> { - let request = Mastodon.API.get(url: extendedDescriptionEndpointURL(domain: domain)) + let request = Mastodon.API.get(url: extendedDescriptionEndpointURL(domain: domain), authorization: authorization) return session.dataTaskPublisher(for: request) .tryMap { data, response in let value = try Mastodon.API.decode(type: Mastodon.Entity.ExtendedDescription.self, from: data, response: response) diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+V2+Instance.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+V2+Instance.swift index e276fddba..1077b285e 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+V2+Instance.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+V2+Instance.swift @@ -21,12 +21,13 @@ extension Mastodon.API.V2.Instance { /// - Returns: `AnyPublisher` contains `Instance` nested in the response public static func instance( session: URLSession, + authorization: Mastodon.API.OAuth.Authorization?, domain: String ) -> AnyPublisher, Error> { let request = Mastodon.API.get( url: instanceEndpointURL(domain: domain), query: nil, - authorization: nil + authorization: authorization ) return session.dataTaskPublisher(for: request) .tryMap { data, response in diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift index 8005ebfa1..60123608f 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift @@ -99,6 +99,10 @@ extension Mastodon.API { public static func profileSettingsURL(domain: String) -> URL { return URL(string: "\(URL.httpScheme(domain: domain))://" + domain + "/auth/edit")! } + + public static func webURL(domain: String) -> URL { + return URL(string: "\(URL.httpScheme(domain: domain))://" + domain + "/")! + } } extension Mastodon.API { @@ -211,7 +215,7 @@ extension Mastodon.API { return try Mastodon.API.decoder.decode(type, from: data) } catch let decodeError { #if DEBUG - debugPrint("\(response.url), Data: \(String(data: data, encoding: .utf8)), \(decodeError)") + debugPrint("URL: \(String(describing: response.url))\nData: \(String(data: data, encoding: .utf8) ?? "-")\nError:\(decodeError)\n----\n") #endif guard let httpURLResponse = response as? HTTPURLResponse else { diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+ExtendedDescription.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+ExtendedDescription.swift index c42fe6ab1..337e54af9 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+ExtendedDescription.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+ExtendedDescription.swift @@ -8,7 +8,7 @@ extension Mastodon.Entity { /// ## Reference: /// [Document](https://docs.joinmastodon.org/entities/ExtendedDescription/) public struct ExtendedDescription: Codable { - public let updatedAt: Date + public let updatedAt: Date? public let content: String enum CodingKeys: String, CodingKey { diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Notification.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Notification.swift index d6a2f038d..f29a5fff4 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Notification.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Notification.swift @@ -23,15 +23,69 @@ extension Mastodon.Entity { public let type: Type public let createdAt: Date public let account: Account - public let status: Status? - + public let accountWarning: AccountWarning? + enum CodingKeys: String, CodingKey { case id case type case createdAt = "created_at" case account case status + case accountWarning = "moderation_warning" + } + } +} + +extension Mastodon.Entity { + public struct AccountWarning: Codable { + public typealias ID = String + + public let id: ID + public let action: Action + public let text: String? + public let targetAccount: Account + public let appeal: Appeal? + public let statusIds: [Mastodon.Entity.Status.ID]? + + public enum CodingKeys: String, CodingKey { + case id + case action + case text + case targetAccount = "target_account" + case appeal + case statusIds = "status_ids" + } + + public enum Action: String, Codable { + case none + case disable + case markStatusesAsSensitive + case deleteStatuses + case sensitive + case silence + case suspend + + public enum CodingKeys: String, CodingKey { + case none + case disable + case markStatusesAsSensitive = "mark_statuses_as_sensitive" + case deleteStatuses = "delete_statuses" + case sensitive + case silence + case suspend + } + } + + public struct Appeal: Codable { + public let text: String + public let state: State + + public enum State: String, Codable { + case approved + case rejected + case pending + } } } } @@ -46,21 +100,10 @@ extension Mastodon.Entity.Notification { case favourite case poll case status - + case moderationWarning + case _other(String) - public static var knownCases: [NotificationType] { - return [ - .follow, - .followRequest, - .mention, - .reblog, - .favourite, - .poll, - .status - ] - } - public init?(rawValue: String) { switch rawValue { case "follow": self = .follow @@ -70,6 +113,7 @@ extension Mastodon.Entity.Notification { case "favourite": self = .favourite case "poll": self = .poll case "status": self = .status + case "moderation_warning": self = .moderationWarning default: self = ._other(rawValue) } } @@ -83,6 +127,7 @@ extension Mastodon.Entity.Notification { case .favourite: return "favourite" case .poll: return "poll" case .status: return "status" + case .moderationWarning: return "moderation_warning" case ._other(let value): return value } } diff --git a/OpenInActionExtension/ActionRequestHandler.swift b/OpenInActionExtension/ActionRequestHandler.swift index 69215dcb2..813913027 100644 --- a/OpenInActionExtension/ActionRequestHandler.swift +++ b/OpenInActionExtension/ActionRequestHandler.swift @@ -110,7 +110,12 @@ private extension ActionRequestHandler { func continueWithSearch(_ query: String) { guard let url = URL(string: query), - let host = url.host + let host = url.host, + let activeAuthenticationBox = Self.appContext + .authenticationService + .mastodonAuthenticationBoxes + .first + else { return doneWithInvalidLink() } @@ -119,6 +124,7 @@ private extension ActionRequestHandler { .Instance .instance( session: .shared, + authorization: activeAuthenticationBox.userAuthorization, domain: host ) .receive(on: DispatchQueue.main)