mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-25 09:41:00 +00:00
Follow / unfollow
This commit is contained in:
parent
ad4b238883
commit
2c1c42ea71
6 changed files with 159 additions and 0 deletions
|
@ -218,6 +218,21 @@ public extension ContentDatabase {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func unfollow(id: Account.Id) -> AnyPublisher<Never, Error> {
|
||||||
|
databaseWriter.writePublisher {
|
||||||
|
let statusIds = try Status.Id.fetchAll(
|
||||||
|
$0,
|
||||||
|
StatusRecord.filter(StatusRecord.Columns.accountId == id).select(StatusRecord.Columns.id))
|
||||||
|
|
||||||
|
try TimelineStatusJoin.filter(
|
||||||
|
TimelineStatusJoin.Columns.timelineId == Timeline.home.id
|
||||||
|
&& statusIds.contains(TimelineStatusJoin.Columns.statusId))
|
||||||
|
.deleteAll($0)
|
||||||
|
}
|
||||||
|
.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,9 +1,13 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
"account.field.verified" = "Verified %@";
|
"account.field.verified" = "Verified %@";
|
||||||
|
"account.follow" = "Follow";
|
||||||
|
"account.following" = "Following";
|
||||||
|
"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.unfollow-account" = "Unfollow %@";
|
||||||
"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";
|
||||||
|
@ -14,6 +18,7 @@
|
||||||
"add-identity.unable-to-connect-to-instance" = "Unable to connect to instance";
|
"add-identity.unable-to-connect-to-instance" = "Unable to connect to instance";
|
||||||
"attachment.sensitive-content" = "Sensitive content";
|
"attachment.sensitive-content" = "Sensitive content";
|
||||||
"attachment.media-hidden" = "Media hidden";
|
"attachment.media-hidden" = "Media hidden";
|
||||||
|
"cancel" = "Cancel";
|
||||||
"registration.review-terms-of-use-and-privacy-policy-%@" = "Please review %@'s Terms of Use and Privacy Policy to continue";
|
"registration.review-terms-of-use-and-privacy-policy-%@" = "Please review %@'s Terms of Use and Privacy Policy to continue";
|
||||||
"registration.username" = "Username";
|
"registration.username" = "Username";
|
||||||
"registration.email" = "Email";
|
"registration.email" = "Email";
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import HTTP
|
||||||
|
import Mastodon
|
||||||
|
|
||||||
|
public enum RelationshipEndpoint {
|
||||||
|
case accountsFollow(id: Account.Id)
|
||||||
|
case accountsUnfollow(id: Account.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension RelationshipEndpoint: Endpoint {
|
||||||
|
public typealias ResultType = Relationship
|
||||||
|
|
||||||
|
public var context: [String] {
|
||||||
|
defaultContext + ["accounts"]
|
||||||
|
}
|
||||||
|
|
||||||
|
public var pathComponentsInContext: [String] {
|
||||||
|
switch self {
|
||||||
|
case let .accountsFollow(id):
|
||||||
|
return [id, "follow"]
|
||||||
|
case let .accountsUnfollow(id):
|
||||||
|
return [id, "unfollow"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var method: HTTPMethod {
|
||||||
|
switch self {
|
||||||
|
case .accountsFollow, .accountsUnfollow:
|
||||||
|
return .post
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,3 +28,20 @@ public struct AccountService {
|
||||||
self.contentDatabase = contentDatabase
|
self.contentDatabase = contentDatabase
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public extension AccountService {
|
||||||
|
func follow() -> AnyPublisher<Never, Error> {
|
||||||
|
mastodonAPIClient.request(RelationshipEndpoint.accountsFollow(id: account.id))
|
||||||
|
.flatMap { contentDatabase.insert(relationships: [$0]) }
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
func unfollow() -> AnyPublisher<Never, Error> {
|
||||||
|
mastodonAPIClient.request(RelationshipEndpoint.accountsUnfollow(id: account.id))
|
||||||
|
.flatMap {
|
||||||
|
contentDatabase.insert(relationships: [$0])
|
||||||
|
.merge(with: contentDatabase.unfollow(id: account.id))
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -36,6 +36,8 @@ public extension AccountViewModel {
|
||||||
|
|
||||||
var isLocked: Bool { accountService.account.locked }
|
var isLocked: Bool { accountService.account.locked }
|
||||||
|
|
||||||
|
var relationship: Relationship? { accountService.relationship }
|
||||||
|
|
||||||
var identityProofs: [IdentityProof] { accountService.identityProofs }
|
var identityProofs: [IdentityProof] { accountService.identityProofs }
|
||||||
|
|
||||||
var fields: [Account.Field] { accountService.account.fields }
|
var fields: [Account.Field] { accountService.account.fields }
|
||||||
|
@ -63,4 +65,12 @@ public extension AccountViewModel {
|
||||||
.setFailureType(to: Error.self)
|
.setFailureType(to: Error.self)
|
||||||
.eraseToAnyPublisher())
|
.eraseToAnyPublisher())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func follow() {
|
||||||
|
eventsSubject.send(accountService.follow().map { _ in .ignorableOutput }.eraseToAnyPublisher())
|
||||||
|
}
|
||||||
|
|
||||||
|
func unfollow() {
|
||||||
|
eventsSubject.send(accountService.unfollow().map { _ in .ignorableOutput }.eraseToAnyPublisher())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,9 @@ final class AccountHeaderView: UIView {
|
||||||
let headerButton = UIButton()
|
let headerButton = UIButton()
|
||||||
let avatarImageView = UIImageView()
|
let avatarImageView = UIImageView()
|
||||||
let avatarButton = UIButton()
|
let avatarButton = UIButton()
|
||||||
|
let relationshipButtonsStackView = UIStackView()
|
||||||
|
let followButton = UIButton(type: .system)
|
||||||
|
let unfollowButton = UIButton(type: .system)
|
||||||
let displayNameLabel = UILabel()
|
let displayNameLabel = UILabel()
|
||||||
let accountStackView = UIStackView()
|
let accountStackView = UIStackView()
|
||||||
let accountLabel = UILabel()
|
let accountLabel = UILabel()
|
||||||
|
@ -25,6 +28,20 @@ final class AccountHeaderView: UIView {
|
||||||
avatarImageView.kf.setImage(with: accountViewModel.avatarURL(profile: true))
|
avatarImageView.kf.setImage(with: accountViewModel.avatarURL(profile: true))
|
||||||
avatarImageView.tag = accountViewModel.avatarURL(profile: true).hashValue
|
avatarImageView.tag = accountViewModel.avatarURL(profile: true).hashValue
|
||||||
|
|
||||||
|
if !accountViewModel.isSelf, let relationship = accountViewModel.relationship {
|
||||||
|
followButton.setTitle(
|
||||||
|
NSLocalizedString(
|
||||||
|
accountViewModel.isLocked ? "account.request" : "account.follow",
|
||||||
|
comment: ""),
|
||||||
|
for: .normal)
|
||||||
|
followButton.isHidden = relationship.following
|
||||||
|
unfollowButton.isHidden = !relationship.following
|
||||||
|
|
||||||
|
relationshipButtonsStackView.isHidden = false
|
||||||
|
} else {
|
||||||
|
relationshipButtonsStackView.isHidden = true
|
||||||
|
}
|
||||||
|
|
||||||
if accountViewModel.displayName.isEmpty {
|
if accountViewModel.displayName.isEmpty {
|
||||||
displayNameLabel.isHidden = true
|
displayNameLabel.isHidden = true
|
||||||
} else {
|
} else {
|
||||||
|
@ -99,6 +116,17 @@ final class AccountHeaderView: UIView {
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
for button in [followButton, unfollowButton] {
|
||||||
|
let inset = (followButton.bounds.height - (button.titleLabel?.bounds.height ?? 0)) / 2
|
||||||
|
|
||||||
|
button.contentEdgeInsets = .init(top: 0, left: inset, bottom: 0, right: inset)
|
||||||
|
button.layer.cornerRadius = button.bounds.height / 2
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AccountHeaderView: UITextViewDelegate {
|
extension AccountHeaderView: UITextViewDelegate {
|
||||||
|
@ -152,6 +180,50 @@ private extension AccountHeaderView {
|
||||||
|
|
||||||
avatarButton.addAction(UIAction { [weak self] _ in self?.viewModel?.presentAvatar() }, for: .touchUpInside)
|
avatarButton.addAction(UIAction { [weak self] _ in self?.viewModel?.presentAvatar() }, for: .touchUpInside)
|
||||||
|
|
||||||
|
addSubview(relationshipButtonsStackView)
|
||||||
|
relationshipButtonsStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
relationshipButtonsStackView.spacing = .defaultSpacing
|
||||||
|
relationshipButtonsStackView.addArrangedSubview(UIView())
|
||||||
|
|
||||||
|
for button in [followButton, unfollowButton] {
|
||||||
|
relationshipButtonsStackView.addArrangedSubview(button)
|
||||||
|
button.titleLabel?.font = .preferredFont(forTextStyle: .headline)
|
||||||
|
button.titleLabel?.adjustsFontForContentSizeCategory = true
|
||||||
|
button.backgroundColor = .secondarySystemBackground
|
||||||
|
}
|
||||||
|
|
||||||
|
followButton.setImage(
|
||||||
|
UIImage(
|
||||||
|
systemName: "person.badge.plus",
|
||||||
|
withConfiguration: UIImage.SymbolConfiguration(scale: .small)),
|
||||||
|
for: .normal)
|
||||||
|
followButton.addAction(
|
||||||
|
UIAction { [weak self] _ in self?.viewModel?.accountViewModel?.follow() },
|
||||||
|
for: .touchUpInside)
|
||||||
|
|
||||||
|
unfollowButton.setImage(
|
||||||
|
UIImage(
|
||||||
|
systemName: "checkmark",
|
||||||
|
withConfiguration: UIImage.SymbolConfiguration(scale: .small)),
|
||||||
|
for: .normal)
|
||||||
|
unfollowButton.setTitle(NSLocalizedString("account.following", comment: ""), for: .normal)
|
||||||
|
unfollowButton.showsMenuAsPrimaryAction = true
|
||||||
|
unfollowButton.menu = UIMenu(children: [UIDeferredMenuElement { [weak self] completion in
|
||||||
|
guard let accountViewModel = self?.viewModel?.accountViewModel else { return }
|
||||||
|
|
||||||
|
let unfollowAction = UIAction(
|
||||||
|
title: String.localizedStringWithFormat(
|
||||||
|
NSLocalizedString("account.unfollow-account", comment: ""),
|
||||||
|
accountViewModel.accountName),
|
||||||
|
image: UIImage(systemName: "person.badge.minus"),
|
||||||
|
attributes: .destructive) { _ in
|
||||||
|
accountViewModel.unfollow()
|
||||||
|
}
|
||||||
|
|
||||||
|
completion([unfollowAction])
|
||||||
|
},
|
||||||
|
UIAction(title: NSLocalizedString("cancel", comment: "")) { _ in }])
|
||||||
|
|
||||||
addSubview(baseStackView)
|
addSubview(baseStackView)
|
||||||
baseStackView.translatesAutoresizingMaskIntoConstraints = false
|
baseStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
baseStackView.axis = .vertical
|
baseStackView.axis = .vertical
|
||||||
|
@ -231,6 +303,12 @@ private extension AccountHeaderView {
|
||||||
avatarButton.topAnchor.constraint(equalTo: avatarImageView.topAnchor),
|
avatarButton.topAnchor.constraint(equalTo: avatarImageView.topAnchor),
|
||||||
avatarButton.bottomAnchor.constraint(equalTo: avatarImageView.bottomAnchor),
|
avatarButton.bottomAnchor.constraint(equalTo: avatarImageView.bottomAnchor),
|
||||||
avatarButton.trailingAnchor.constraint(equalTo: avatarImageView.trailingAnchor),
|
avatarButton.trailingAnchor.constraint(equalTo: avatarImageView.trailingAnchor),
|
||||||
|
relationshipButtonsStackView.leadingAnchor.constraint(equalTo: avatarImageView.trailingAnchor),
|
||||||
|
relationshipButtonsStackView.topAnchor.constraint(
|
||||||
|
equalTo: headerImageView.bottomAnchor,
|
||||||
|
constant: .defaultSpacing),
|
||||||
|
relationshipButtonsStackView.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor),
|
||||||
|
relationshipButtonsStackView.bottomAnchor.constraint(equalTo: avatarImageView.bottomAnchor),
|
||||||
baseStackView.topAnchor.constraint(equalTo: avatarImageView.bottomAnchor, constant: .defaultSpacing),
|
baseStackView.topAnchor.constraint(equalTo: avatarImageView.bottomAnchor, constant: .defaultSpacing),
|
||||||
baseStackView.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor),
|
baseStackView.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor),
|
||||||
baseStackView.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor),
|
baseStackView.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor),
|
||||||
|
|
Loading…
Reference in a new issue