mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-05 16:29:33 +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()
|
||||
}
|
||||
|
||||
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> {
|
||||
databaseWriter.writePublisher {
|
||||
try list.save($0)
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
"account.field.verified" = "Verified %@";
|
||||
"account.follow" = "Follow";
|
||||
"account.following" = "Following";
|
||||
"account.request" = "Request";
|
||||
"account.statuses" = "Posts";
|
||||
"account.statuses-and-replies" = "Posts & Replies";
|
||||
"account.media" = "Media";
|
||||
"account.unfollow-account" = "Unfollow %@";
|
||||
"add" = "Add";
|
||||
"apns-default-message" = "New notification";
|
||||
"add-identity.instance-url" = "Instance URL";
|
||||
|
@ -14,6 +18,7 @@
|
|||
"add-identity.unable-to-connect-to-instance" = "Unable to connect to instance";
|
||||
"attachment.sensitive-content" = "Sensitive content";
|
||||
"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.username" = "Username";
|
||||
"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
|
||||
}
|
||||
}
|
||||
|
||||
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 relationship: Relationship? { accountService.relationship }
|
||||
|
||||
var identityProofs: [IdentityProof] { accountService.identityProofs }
|
||||
|
||||
var fields: [Account.Field] { accountService.account.fields }
|
||||
|
@ -63,4 +65,12 @@ public extension AccountViewModel {
|
|||
.setFailureType(to: Error.self)
|
||||
.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 avatarImageView = UIImageView()
|
||||
let avatarButton = UIButton()
|
||||
let relationshipButtonsStackView = UIStackView()
|
||||
let followButton = UIButton(type: .system)
|
||||
let unfollowButton = UIButton(type: .system)
|
||||
let displayNameLabel = UILabel()
|
||||
let accountStackView = UIStackView()
|
||||
let accountLabel = UILabel()
|
||||
|
@ -25,6 +28,20 @@ final class AccountHeaderView: UIView {
|
|||
avatarImageView.kf.setImage(with: accountViewModel.avatarURL(profile: true))
|
||||
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 {
|
||||
displayNameLabel.isHidden = true
|
||||
} else {
|
||||
|
@ -99,6 +116,17 @@ final class AccountHeaderView: UIView {
|
|||
required init?(coder: NSCoder) {
|
||||
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 {
|
||||
|
@ -152,6 +180,50 @@ private extension AccountHeaderView {
|
|||
|
||||
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)
|
||||
baseStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
baseStackView.axis = .vertical
|
||||
|
@ -231,6 +303,12 @@ private extension AccountHeaderView {
|
|||
avatarButton.topAnchor.constraint(equalTo: avatarImageView.topAnchor),
|
||||
avatarButton.bottomAnchor.constraint(equalTo: avatarImageView.bottomAnchor),
|
||||
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.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor),
|
||||
baseStackView.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor),
|
||||
|
|
Loading…
Reference in a new issue