mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-25 17:50:59 +00:00
Profile actions
This commit is contained in:
parent
0b7b3d3dc4
commit
2397758456
8 changed files with 250 additions and 26 deletions
|
@ -27,7 +27,7 @@ extension ContentDatabase {
|
||||||
t.column("emojis", .blob).notNull()
|
t.column("emojis", .blob).notNull()
|
||||||
t.column("bot", .boolean).notNull()
|
t.column("bot", .boolean).notNull()
|
||||||
t.column("discoverable", .boolean)
|
t.column("discoverable", .boolean)
|
||||||
t.column("movedId", .text).references("accountRecord")
|
t.column("movedId", .text).references("accountRecord", onDelete: .cascade)
|
||||||
}
|
}
|
||||||
|
|
||||||
try db.create(table: "relationship") { t in
|
try db.create(table: "relationship") { t in
|
||||||
|
@ -61,7 +61,7 @@ extension ContentDatabase {
|
||||||
t.column("id", .text).primaryKey(onConflict: .replace)
|
t.column("id", .text).primaryKey(onConflict: .replace)
|
||||||
t.column("uri", .text).notNull()
|
t.column("uri", .text).notNull()
|
||||||
t.column("createdAt", .datetime).notNull()
|
t.column("createdAt", .datetime).notNull()
|
||||||
t.column("accountId", .text).notNull().references("accountRecord")
|
t.column("accountId", .text).notNull().references("accountRecord", onDelete: .cascade)
|
||||||
t.column("content", .text).notNull()
|
t.column("content", .text).notNull()
|
||||||
t.column("visibility", .text).notNull()
|
t.column("visibility", .text).notNull()
|
||||||
t.column("sensitive", .boolean).notNull()
|
t.column("sensitive", .boolean).notNull()
|
||||||
|
@ -77,7 +77,7 @@ extension ContentDatabase {
|
||||||
t.column("url", .text)
|
t.column("url", .text)
|
||||||
t.column("inReplyToId", .text)
|
t.column("inReplyToId", .text)
|
||||||
t.column("inReplyToAccountId", .text)
|
t.column("inReplyToAccountId", .text)
|
||||||
t.column("reblogId", .text).references("statusRecord")
|
t.column("reblogId", .text).references("statusRecord", onDelete: .cascade)
|
||||||
t.column("poll", .blob)
|
t.column("poll", .blob)
|
||||||
t.column("card", .blob)
|
t.column("card", .blob)
|
||||||
t.column("language", .text)
|
t.column("language", .text)
|
||||||
|
@ -135,7 +135,7 @@ extension ContentDatabase {
|
||||||
try db.create(table: "conversationRecord") { t in
|
try db.create(table: "conversationRecord") { t in
|
||||||
t.column("id", .text).primaryKey(onConflict: .replace)
|
t.column("id", .text).primaryKey(onConflict: .replace)
|
||||||
t.column("unread", .boolean).notNull()
|
t.column("unread", .boolean).notNull()
|
||||||
t.column("lastStatusId", .text).references("statusRecord")
|
t.column("lastStatusId", .text).references("statusRecord", onDelete: .cascade)
|
||||||
}
|
}
|
||||||
|
|
||||||
try db.create(table: "conversationAccountJoin") { t in
|
try db.create(table: "conversationAccountJoin") { t in
|
||||||
|
@ -155,8 +155,8 @@ extension ContentDatabase {
|
||||||
try db.create(table: "notificationRecord") { t in
|
try db.create(table: "notificationRecord") { t in
|
||||||
t.column("id", .text).primaryKey(onConflict: .replace)
|
t.column("id", .text).primaryKey(onConflict: .replace)
|
||||||
t.column("type", .text).notNull()
|
t.column("type", .text).notNull()
|
||||||
t.column("accountId", .text).notNull().references("accountRecord")
|
t.column("accountId", .text).notNull().references("accountRecord", onDelete: .cascade)
|
||||||
t.column("statusId").references("statusRecord")
|
t.column("statusId").references("statusRecord", onDelete: .cascade)
|
||||||
}
|
}
|
||||||
|
|
||||||
try db.create(table: "statusAncestorJoin") { t in
|
try db.create(table: "statusAncestorJoin") { t in
|
||||||
|
|
|
@ -233,6 +233,21 @@ public extension ContentDatabase {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mute(id: Account.Id) -> AnyPublisher<Never, Error> {
|
||||||
|
databaseWriter.writePublisher {
|
||||||
|
try StatusRecord.filter(StatusRecord.Columns.accountId == id).deleteAll($0)
|
||||||
|
try NotificationRecord.filter(NotificationRecord.Columns.accountId == id).deleteAll($0)
|
||||||
|
}
|
||||||
|
.ignoreOutput()
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
func block(id: Account.Id) -> AnyPublisher<Never, Error> {
|
||||||
|
databaseWriter.writePublisher(updates: AccountRecord.filter(AccountRecord.Columns.id == id).deleteAll)
|
||||||
|
.ignoreOutput()
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
func append(accounts: [Account], toList list: AccountList) -> AnyPublisher<Never, Error> {
|
func append(accounts: [Account], toList list: AccountList) -> AnyPublisher<Never, Error> {
|
||||||
databaseWriter.writePublisher {
|
databaseWriter.writePublisher {
|
||||||
try list.save($0)
|
try list.save($0)
|
||||||
|
|
|
@ -1,13 +1,19 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
"account.block-account" = "Block %@";
|
||||||
"account.field.verified" = "Verified %@";
|
"account.field.verified" = "Verified %@";
|
||||||
"account.follow" = "Follow";
|
"account.follow" = "Follow";
|
||||||
"account.following" = "Following";
|
"account.following" = "Following";
|
||||||
|
"account.hide-reblogs-account" = "Hide reblogs from %@";
|
||||||
|
"account.mute-account" = "Mute %@";
|
||||||
"account.request" = "Request";
|
"account.request" = "Request";
|
||||||
"account.statuses" = "Posts";
|
"account.statuses" = "Posts";
|
||||||
"account.statuses-and-replies" = "Posts & Replies";
|
"account.statuses-and-replies" = "Posts & Replies";
|
||||||
"account.media" = "Media";
|
"account.media" = "Media";
|
||||||
|
"account.show-reblogs-account" = "Show reblogs from %@";
|
||||||
|
"account.unblock-account" = "Unblock %@";
|
||||||
"account.unfollow-account" = "Unfollow %@";
|
"account.unfollow-account" = "Unfollow %@";
|
||||||
|
"account.unmute-account" = "Unmute %@";
|
||||||
"add" = "Add";
|
"add" = "Add";
|
||||||
"apns-default-message" = "New notification";
|
"apns-default-message" = "New notification";
|
||||||
"add-identity.instance-url" = "Instance URL";
|
"add-identity.instance-url" = "Instance URL";
|
||||||
|
|
|
@ -5,8 +5,15 @@ import HTTP
|
||||||
import Mastodon
|
import Mastodon
|
||||||
|
|
||||||
public enum RelationshipEndpoint {
|
public enum RelationshipEndpoint {
|
||||||
case accountsFollow(id: Account.Id)
|
case accountsFollow(id: Account.Id, showReblogs: Bool? = nil)
|
||||||
case accountsUnfollow(id: Account.Id)
|
case accountsUnfollow(id: Account.Id)
|
||||||
|
case accountsBlock(id: Account.Id)
|
||||||
|
case accountsUnblock(id: Account.Id)
|
||||||
|
case accountsMute(id: Account.Id)
|
||||||
|
case accountsUnmute(id: Account.Id)
|
||||||
|
case accountsPin(id: Account.Id)
|
||||||
|
case accountsUnpin(id: Account.Id)
|
||||||
|
case note(String, id: Account.Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
extension RelationshipEndpoint: Endpoint {
|
extension RelationshipEndpoint: Endpoint {
|
||||||
|
@ -18,17 +25,50 @@ extension RelationshipEndpoint: Endpoint {
|
||||||
|
|
||||||
public var pathComponentsInContext: [String] {
|
public var pathComponentsInContext: [String] {
|
||||||
switch self {
|
switch self {
|
||||||
case let .accountsFollow(id):
|
case let .accountsFollow(id, _):
|
||||||
return [id, "follow"]
|
return [id, "follow"]
|
||||||
case let .accountsUnfollow(id):
|
case let .accountsUnfollow(id):
|
||||||
return [id, "unfollow"]
|
return [id, "unfollow"]
|
||||||
|
case let .accountsBlock(id):
|
||||||
|
return [id, "block"]
|
||||||
|
case let .accountsUnblock(id):
|
||||||
|
return [id, "unblock"]
|
||||||
|
case let .accountsMute(id):
|
||||||
|
return [id, "mute"]
|
||||||
|
case let .accountsUnmute(id):
|
||||||
|
return [id, "unmute"]
|
||||||
|
case let .accountsPin(id):
|
||||||
|
return [id, "pin"]
|
||||||
|
case let .accountsUnpin(id):
|
||||||
|
return [id, "unpin"]
|
||||||
|
case let .note(_, id):
|
||||||
|
return [id, "note"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var queryParameters: [URLQueryItem] {
|
||||||
|
switch self {
|
||||||
|
case let .accountsFollow(_, showReblogs):
|
||||||
|
if let showReblogs = showReblogs {
|
||||||
|
return [URLQueryItem(name: "reblogs", value: String(showReblogs))]
|
||||||
|
} else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var jsonBody: [String: Any]? {
|
||||||
|
switch self {
|
||||||
|
case let .note(note, _):
|
||||||
|
return ["comment": note]
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var method: HTTPMethod {
|
public var method: HTTPMethod {
|
||||||
switch self {
|
.post
|
||||||
case .accountsFollow, .accountsUnfollow:
|
|
||||||
return .post
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,17 +31,63 @@ public struct AccountService {
|
||||||
|
|
||||||
public extension AccountService {
|
public extension AccountService {
|
||||||
func follow() -> AnyPublisher<Never, Error> {
|
func follow() -> AnyPublisher<Never, Error> {
|
||||||
mastodonAPIClient.request(RelationshipEndpoint.accountsFollow(id: account.id))
|
relationshipAction(.accountsFollow(id: account.id))
|
||||||
.flatMap { contentDatabase.insert(relationships: [$0]) }
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func unfollow() -> AnyPublisher<Never, Error> {
|
func unfollow() -> AnyPublisher<Never, Error> {
|
||||||
mastodonAPIClient.request(RelationshipEndpoint.accountsUnfollow(id: account.id))
|
relationshipAction(.accountsUnfollow(id: account.id))
|
||||||
.flatMap {
|
.collect()
|
||||||
contentDatabase.insert(relationships: [$0])
|
.flatMap { _ in contentDatabase.unfollow(id: account.id) }
|
||||||
.merge(with: contentDatabase.unfollow(id: account.id))
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func hideReblogs() -> AnyPublisher<Never, Error> {
|
||||||
|
relationshipAction(.accountsFollow(id: account.id, showReblogs: false))
|
||||||
|
}
|
||||||
|
|
||||||
|
func showReblogs() -> AnyPublisher<Never, Error> {
|
||||||
|
relationshipAction(.accountsFollow(id: account.id, showReblogs: true))
|
||||||
|
}
|
||||||
|
|
||||||
|
func block() -> AnyPublisher<Never, Error> {
|
||||||
|
relationshipAction(.accountsBlock(id: account.id))
|
||||||
|
.collect()
|
||||||
|
.flatMap { _ in contentDatabase.block(id: account.id) }
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
func unblock() -> AnyPublisher<Never, Error> {
|
||||||
|
relationshipAction(.accountsUnblock(id: account.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
func mute() -> AnyPublisher<Never, Error> {
|
||||||
|
relationshipAction(.accountsMute(id: account.id))
|
||||||
|
.collect()
|
||||||
|
.flatMap { _ in contentDatabase.mute(id: account.id) }
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
func unmute() -> AnyPublisher<Never, Error> {
|
||||||
|
relationshipAction(.accountsUnmute(id: account.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
func pin() -> AnyPublisher<Never, Error> {
|
||||||
|
relationshipAction(.accountsPin(id: account.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
func unpin() -> AnyPublisher<Never, Error> {
|
||||||
|
relationshipAction(.accountsUnpin(id: account.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
func set(note: String) -> AnyPublisher<Never, Error> {
|
||||||
|
relationshipAction(.note(note, id: account.id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension AccountService {
|
||||||
|
func relationshipAction(_ endpoint: RelationshipEndpoint) -> AnyPublisher<Never, Error> {
|
||||||
|
mastodonAPIClient.request(endpoint)
|
||||||
|
.flatMap { contentDatabase.insert(relationships: [$0]) }
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
import Combine
|
import Combine
|
||||||
|
import Mastodon
|
||||||
import UIKit
|
import UIKit
|
||||||
import ViewModels
|
import ViewModels
|
||||||
|
|
||||||
|
@ -24,9 +25,18 @@ final class ProfileViewController: TableViewController {
|
||||||
|
|
||||||
viewModel.$accountViewModel
|
viewModel.$accountViewModel
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] _ in
|
.sink { [weak self] in
|
||||||
accountHeaderView.viewModel = self?.viewModel
|
guard let self = self else { return }
|
||||||
self?.sizeTableHeaderFooterViews()
|
|
||||||
|
accountHeaderView.viewModel = self.viewModel
|
||||||
|
self.sizeTableHeaderFooterViews()
|
||||||
|
|
||||||
|
if let accountViewModel = $0,
|
||||||
|
let relationship = accountViewModel.relationship {
|
||||||
|
self.navigationItem.rightBarButtonItem = UIBarButtonItem(
|
||||||
|
image: UIImage(systemName: "ellipsis.circle"),
|
||||||
|
menu: self.menu(accountViewModel: accountViewModel, relationship: relationship))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
@ -54,3 +64,68 @@ final class ProfileViewController: TableViewController {
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extension ProfileViewController {
|
||||||
|
// swiftlint:disable:next function_body_length
|
||||||
|
func menu(accountViewModel: AccountViewModel, relationship: Relationship) -> UIMenu {
|
||||||
|
var actions = [UIAction]()
|
||||||
|
|
||||||
|
if relationship.following {
|
||||||
|
if relationship.showingReblogs {
|
||||||
|
actions.append(UIAction(
|
||||||
|
title: String.localizedStringWithFormat(
|
||||||
|
NSLocalizedString("account.hide-reblogs-account", comment: ""),
|
||||||
|
accountViewModel.accountName),
|
||||||
|
image: UIImage(systemName: "arrow.2.squarepath")) { _ in
|
||||||
|
accountViewModel.hideReblogs()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
actions.append(UIAction(
|
||||||
|
title: String.localizedStringWithFormat(
|
||||||
|
NSLocalizedString("account.show-reblogs-account", comment: ""),
|
||||||
|
accountViewModel.accountName),
|
||||||
|
image: UIImage(systemName: "arrow.2.squarepath")) { _ in
|
||||||
|
accountViewModel.showReblogs()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if relationship.muting {
|
||||||
|
actions.append(UIAction(
|
||||||
|
title: String.localizedStringWithFormat(
|
||||||
|
NSLocalizedString("account.unmute-account", comment: ""),
|
||||||
|
accountViewModel.accountName),
|
||||||
|
image: UIImage(systemName: "speaker")) { _ in
|
||||||
|
accountViewModel.unmute()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
actions.append(UIAction(
|
||||||
|
title: String.localizedStringWithFormat(
|
||||||
|
NSLocalizedString("account.mute-account", comment: ""),
|
||||||
|
accountViewModel.accountName),
|
||||||
|
image: UIImage(systemName: "speaker.slash")) { _ in
|
||||||
|
accountViewModel.mute()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if relationship.blocking {
|
||||||
|
actions.append(UIAction(
|
||||||
|
title: String.localizedStringWithFormat(
|
||||||
|
NSLocalizedString("account.unblock-account", comment: ""),
|
||||||
|
accountViewModel.accountName),
|
||||||
|
image: UIImage(systemName: "slash.circle")) { _ in
|
||||||
|
accountViewModel.unblock()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
actions.append(UIAction(
|
||||||
|
title: String.localizedStringWithFormat(
|
||||||
|
NSLocalizedString("account.block-account", comment: ""),
|
||||||
|
accountViewModel.accountName),
|
||||||
|
image: UIImage(systemName: "slash.circle")) { _ in
|
||||||
|
accountViewModel.block()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return UIMenu(children: actions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -67,10 +67,52 @@ public extension AccountViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
func follow() {
|
func follow() {
|
||||||
eventsSubject.send(accountService.follow().map { _ in .ignorableOutput }.eraseToAnyPublisher())
|
ignorableOutputEvent(accountService.follow())
|
||||||
}
|
}
|
||||||
|
|
||||||
func unfollow() {
|
func unfollow() {
|
||||||
eventsSubject.send(accountService.unfollow().map { _ in .ignorableOutput }.eraseToAnyPublisher())
|
ignorableOutputEvent(accountService.unfollow())
|
||||||
|
}
|
||||||
|
|
||||||
|
func hideReblogs() {
|
||||||
|
ignorableOutputEvent(accountService.hideReblogs())
|
||||||
|
}
|
||||||
|
|
||||||
|
func showReblogs() {
|
||||||
|
ignorableOutputEvent(accountService.showReblogs())
|
||||||
|
}
|
||||||
|
|
||||||
|
func block() {
|
||||||
|
ignorableOutputEvent(accountService.block())
|
||||||
|
}
|
||||||
|
|
||||||
|
func unblock() {
|
||||||
|
ignorableOutputEvent(accountService.unblock())
|
||||||
|
}
|
||||||
|
|
||||||
|
func mute() {
|
||||||
|
ignorableOutputEvent(accountService.mute())
|
||||||
|
}
|
||||||
|
|
||||||
|
func unmute() {
|
||||||
|
ignorableOutputEvent(accountService.unmute())
|
||||||
|
}
|
||||||
|
|
||||||
|
func pin() {
|
||||||
|
ignorableOutputEvent(accountService.pin())
|
||||||
|
}
|
||||||
|
|
||||||
|
func unpin() {
|
||||||
|
ignorableOutputEvent(accountService.unpin())
|
||||||
|
}
|
||||||
|
|
||||||
|
func set(note: String) {
|
||||||
|
ignorableOutputEvent(accountService.set(note: note))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension AccountViewModel {
|
||||||
|
func ignorableOutputEvent(_ action: AnyPublisher<Never, Error>) {
|
||||||
|
eventsSubject.send(action.map { _ in .ignorableOutput }.eraseToAnyPublisher())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,7 +71,7 @@ extension ProfileViewModel: CollectionViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
public var expandAll: AnyPublisher<ExpandAllState, Never> {
|
public var expandAll: AnyPublisher<ExpandAllState, Never> {
|
||||||
collectionViewModel.flatMap(\.expandAll).eraseToAnyPublisher()
|
Empty().eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
public var alertItems: AnyPublisher<AlertItem, Never> {
|
public var alertItems: AnyPublisher<AlertItem, Never> {
|
||||||
|
|
Loading…
Reference in a new issue