Moderation Warning Notifications (IOS-264) (#1287)

* Add moderation-warning-notification-type (IOS-264)

* Add entity for new AccountWarning (IOS-264)

Details see https://github.com/mastodon/mastodon/pull/30065 for now, as there's no documentation (yet)

* re-add file header (IOS-264)

* Add authorization to instance-calls (IOS-264)

This will improve using the app with `LIMITED_FEDERATION_INSTANCES`

* Add basic cell for account-warnings (IOS-264)

* Show some content for a warning (IOS-264)

* Open strike in browser (IOS-264)

* Add localization (IOS-264)

* Add missing localization (IOS-264)

* Cleanup (IOS-264)
This commit is contained in:
Nathan Mattes 2024-05-14 15:07:05 +02:00 committed by GitHub
parent 677670055e
commit 794ffc002f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 285 additions and 59 deletions

View File

@ -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": {

View File

@ -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": {

View File

@ -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 = "<group>"; };
D886FBD229DF710F00272017 /* WelcomeSeparatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeSeparatorView.swift; sourceTree = "<group>"; };
D8916DBF29211BE500124085 /* ContentSizedTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentSizedTableView.swift; sourceTree = "<group>"; };
D8A0729C2BEBA8D7001A4C7C /* AccountWarningNotificationCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountWarningNotificationCell.swift; sourceTree = "<group>"; };
D8A6AB6B291C5136003AB663 /* MastodonLoginViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonLoginViewController.swift; sourceTree = "<group>"; };
D8A6FE6129325F5900666A47 /* Intents.stringsdict */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; path = Intents.stringsdict; sourceTree = "<group>"; };
D8A6FE6229325F5900666A47 /* app.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = app.json; sourceTree = "<group>"; };
@ -1484,6 +1486,7 @@
DB023D2727A0FABD005AC798 /* NotificationTableViewCellDelegate.swift */,
DB63F76E279A7D1100455B82 /* NotificationTableViewCell.swift */,
DB63F774279A997D00455B82 /* NotificationTableViewCell+ViewModel.swift */,
D8A0729C2BEBA8D7001A4C7C /* AccountWarningNotificationCell.swift */,
);
path = Cell;
sourceTree = "<group>";
@ -1565,7 +1568,6 @@
DB0617F927855B460030EE79 /* Profile */,
DB4F097926A039C400D62E92 /* Status */,
DB65C63527A2AF52008BAC2E /* Report */,
DB0617F727855B010030EE79 /* Notification */,
DB4F097726A039A200D62E92 /* Search */,
);
path = Diffable;
@ -1921,15 +1923,6 @@
path = Onboarding;
sourceTree = "<group>";
};
DB0617F727855B010030EE79 /* Notification */ = {
isa = PBXGroup;
children = (
2D35237926256D920031AF25 /* NotificationSection.swift */,
2D7867182625B77500211898 /* NotificationItem.swift */,
);
path = Notification;
sourceTree = "<group>";
};
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 = "<group>";
@ -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 */,

View File

@ -0,0 +1,3 @@
{
FILEHEADER = " Copyright © ___YEAR___ Mastodon gGmbH. All rights reserved.";
}

View File

@ -131,7 +131,8 @@ final public class SceneCoordinator {
from: from,
transition: .show
)
case .moderationWarning:
break
case ._other:
assertionFailure()
break

View File

@ -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(

View File

@ -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,

View File

@ -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
}
}
}

View File

@ -37,20 +37,28 @@ extension NotificationSection {
configuration: Configuration
) -> UITableViewDiffableDataSource<NotificationSection, NotificationItem> {
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()

View File

@ -52,6 +52,9 @@ extension NotificationView {
quoteStatusView.configure(status: status)
setQuoteStatusViewDisplay()
}
case .moderationWarning:
// case handled in `AccountWarningNotificationCell.swift`
break
case ._other:
setAuthorContainerBottomPaddingViewDisplay()
assertionFailure()

View File

@ -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<MastodonPickServerViewModel.SignUpResponseFirst, Error>? in
guard let self = self else { return nil }
guard response.value.registrations != false else {

View File

@ -167,7 +167,7 @@ extension MastodonPickServerViewModel {
self.unindexedServers.value = nil
return self.context.apiService.webFinger(domain: domain)
.flatMap { domain -> AnyPublisher<Result<Mastodon.Response.Content<[Mastodon.Entity.Server]>, Error>, Never> in
return self.context.apiService.instance(domain: domain)
return self.context.apiService.instance(domain: domain, authenticationBox: nil)
.map { response -> Result<Mastodon.Response.Content<[Mastodon.Entity.Server]>, Error>in
let newResponse = response.map { [Mastodon.Entity.Server(domain: domain, instance: $0)] }
return Result.success(newResponse)

View File

@ -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,

View File

@ -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<MastodonPickServerViewModel.SignUpResponseFirst, Error>? in
guard let self = self else { return nil }
guard response.value.registrations != false else {

View File

@ -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 ?? []

View File

@ -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

View File

@ -14,18 +14,23 @@ import MastodonSDK
extension APIService {
public func instance(
domain: String
domain: String,
authenticationBox: MastodonAuthenticationBox?
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Instance>, 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<Mastodon.Response.Content<Mastodon.Entity.V2.Instance>, 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<Mastodon.Response.Content<Mastodon.Entity.ExtendedDescription>, Error> {
return Mastodon.API.Instance.extendedDescription(session: session, domain: domain)
public func extendedDescription(
domain: String,
authenticationBox: MastodonAuthenticationBox?
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.ExtendedDescription>, Error> {
return Mastodon.API.Instance.extendedDescription(session: session, authorization: authenticationBox?.userAuthorization, domain: domain)
}
}

View File

@ -60,6 +60,7 @@ extension APIService {
.favourite,
.poll,
.status,
.moderationWarning
]
case .mentions:
return [

View File

@ -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<Void, Error> 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 {

View File

@ -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 {

View File

@ -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";

View File

@ -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)")
}
}

View File

@ -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<Mastodon.Response.Content<Mastodon.Entity.Instance>, 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<Mastodon.Response.Content<Mastodon.Entity.ExtendedDescription>, 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)

View File

@ -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<Mastodon.Response.Content<Mastodon.Entity.V2.Instance>, 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

View File

@ -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 {

View File

@ -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 {

View File

@ -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
}
}

View File

@ -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)