Profile actions

This commit is contained in:
Justin Mazzocchi 2020-11-16 22:46:48 -08:00
parent 0b7b3d3dc4
commit 2397758456
No known key found for this signature in database
GPG key ID: E223E6937AAFB01C
8 changed files with 250 additions and 26 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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