#49 Approve/reject follow requests

This commit is contained in:
Marcin Czachurski 2023-04-25 15:47:21 +02:00
parent 71278400a8
commit b4f7b02d78
17 changed files with 473 additions and 54 deletions

View File

@ -0,0 +1,25 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import PixelfedKit
extension Client {
public class FollowRequests: BaseClient {
public func followRequests(limit: Int = 10,
page: Int? = nil) async throws -> [Account] {
return try await pixelfedClient.followRequests(limit: limit, page: page)
}
public func authorizeRequest(id: EntityId) async throws -> Relationship {
return try await pixelfedClient.authorizeRequest(id: id)
}
public func rejectRequest(id: EntityId) async throws -> Relationship {
return try await pixelfedClient.rejectRequest(id: id)
}
}
}

View File

@ -39,6 +39,7 @@ extension Client {
public var blocks: Blocks? { return Blocks(pixelfedClient: self.pixelfedClient) }
public var mutes: Mutes? { return Mutes(pixelfedClient: self.pixelfedClient) }
public var reports: Reports? { return Reports(pixelfedClient: self.pixelfedClient) }
public var followRequests: FollowRequests? { return FollowRequests(pixelfedClient: self.pixelfedClient) }
public var instances: Instances { return Instances() }
}

View File

@ -100,7 +100,6 @@
"userProfile.title.following" = "Following";
"userProfile.title.joined" = "Joined %@";
"userProfile.title.unfollow" = "Unfollow";
"userProfile.title.followBack" = "Follow back";
"userProfile.title.follow" = "Follow";
"userProfile.title.instance" = "Instance information";
"userProfile.title.blocks" = "Blocked accounts";
@ -110,6 +109,12 @@
"userProfile.title.blocked" = "Account blocked";
"userProfile.title.unblocked" = "Account unblocked";
"userProfile.title.report" = "Report";
"userProfile.title.followsYou" = "Follows you";
"userProfile.title.requestFollow" = "Request follow";
"userProfile.title.cancelRequestFollow" = "Cancel request";
"userProfile.title.followRequests" = "Follow requests";
"userProfile.title.privateProfileTitle" = "This profile is private.";
"userProfile.title.privateProfileSubtitle" = "Only approved followers can see photos.";
"userProfile.error.notExists" = "Account does not exists.";
"userProfile.error.loadingAccountFailed" = "Error during download account from server.";
"userProfile.error.muting" = "Muting/unmuting action failed.";
@ -350,3 +355,10 @@
"report.title.scam" = "Bullying or harassment";
"report.title.terrorism" = "Terrorism";
"report.error.notReported" = "Error during sending report.";
// Mark: Following requests.
"followingRequests.navigationBar.title" = "Following requests";
"followingRequests.title.approve" = "Approve";
"followingRequests.title.reject" = "Reject";
"followingRequests.error.approve" = "Error during approving request.";
"followingRequests.error.reject" = "Error during rejecting request.";

View File

@ -100,7 +100,6 @@
"userProfile.title.following" = "Jarraitzen";
"userProfile.title.joined" = "%@ egin zuen bat";
"userProfile.title.unfollow" = "Utzi jarraitzeari";
"userProfile.title.followBack" = "Jarraitu bera ere";
"userProfile.title.follow" = "Jarraitu";
"userProfile.title.instance" = "Instantziari buruzko informazioa";
"userProfile.title.blocks" = "Blokeatutako kontuak";
@ -110,6 +109,12 @@
"userProfile.title.blocked" = "Kontua blokeatu da";
"userProfile.title.unblocked" = "Kontua blokeatzeari utzi zaio";
"userProfile.title.report" = "Salatu";
"userProfile.title.followsYou" = "Jarraitzen dizu";
"userProfile.title.requestFollow" = "Request follow";
"userProfile.title.cancelRequestFollow" = "Cancel request";
"userProfile.title.followRequests" = "Follow requests";
"userProfile.title.privateProfileTitle" = "This profile is private.";
"userProfile.title.privateProfileSubtitle" = "Only approved followers can see photos.";
"userProfile.error.notExists" = "Kontua ez da existitzen.";
"userProfile.error.loadingAccountFailed" = "Errorea zerbitzaritik kontua eskuratzean.";
"userProfile.error.muting" = "Mututu/Mututzeari uzteak huts egin du.";
@ -350,3 +355,10 @@
"report.title.scam" = "Bullyinga edo jazarpena";
"report.title.terrorism" = "Terrorismoa";
"report.error.notReported" = "Errorea salaketa bidaltzerakoan.";
// Mark: Following requests.
"followingRequests.navigationBar.title" = "Following requests";
"followingRequests.title.approve" = "Approve";
"followingRequests.title.reject" = "Reject";
"followingRequests.error.approve" = "Error during approving request.";
"followingRequests.error.reject" = "Error during rejecting request.";

View File

@ -100,7 +100,6 @@
"userProfile.title.following" = "Suivis";
"userProfile.title.joined" = "Joint %@";
"userProfile.title.unfollow" = "Ne plus suivre";
"userProfile.title.followBack" = "Suivre en retour";
"userProfile.title.follow" = "Suivre";
"userProfile.title.instance" = "Information sur l'instance";
"userProfile.title.blocks" = "Comptes bloqués";
@ -110,6 +109,12 @@
"userProfile.title.blocked" = "Compte bloqué";
"userProfile.title.unblocked" = "Compte débloqué";
"userProfile.title.report" = "Rapport";
"userProfile.title.followsYou" = "Vous suit";
"userProfile.title.requestFollow" = "Request follow";
"userProfile.title.cancelRequestFollow" = "Cancel request";
"userProfile.title.followRequests" = "Follow requests";
"userProfile.title.privateProfileTitle" = "This profile is private.";
"userProfile.title.privateProfileSubtitle" = "Only approved followers can see photos.";
"userProfile.error.notExists" = "Le compte n'existe pas.";
"userProfile.error.loadingAccountFailed" = "Erreur pendant le téléchargement du compte depuis le serveur.";
"userProfile.error.muting" = "L'action sourdine / réactivation a échoué.";
@ -350,3 +355,10 @@
"report.title.scam" = "Intimidation ou harcèlement";
"report.title.terrorism" = "Le terrorisme";
"report.error.notReported" = "Erreur lors de l'envoi du rapport.";
// Mark: Following requests.
"followingRequests.navigationBar.title" = "Following requests";
"followingRequests.title.approve" = "Approve";
"followingRequests.title.reject" = "Reject";
"followingRequests.error.approve" = "Error during approving request.";
"followingRequests.error.reject" = "Error during rejecting request.";

View File

@ -100,10 +100,8 @@
"userProfile.title.following" = "Obserwowani";
"userProfile.title.joined" = "Dołączył(a) %@";
"userProfile.title.unfollow" = "Przestań obserwować";
"userProfile.title.followBack" = "Również obserwuj";
"userProfile.title.follow" = "Obserwuj";
"userProfile.title.instance" = "Informacje o instancji";
"userProfile.error.notExists" = "Konto nie istnieje.";
"userProfile.title.blocks" = "Zablokowane konta";
"userProfile.title.mutes" = "Wyciszone konta";
"userProfile.title.muted" = "Konto wyciszone";
@ -111,6 +109,13 @@
"userProfile.title.blocked" = "Konto zablokowane";
"userProfile.title.unblocked" = "Konto odblokowane";
"userProfile.title.report" = "Zgłoś";
"userProfile.title.followsYou" = "Obserwuje ciebie";
"userProfile.title.requestFollow" = "Poproś o obserwowanie";
"userProfile.title.cancelRequestFollow" = "Anuluj prośbę";
"userProfile.title.followRequests" = "Prośby o obserwowanie";
"userProfile.title.privateProfileTitle" = "To konto jest prywatne.";
"userProfile.title.privateProfileSubtitle" = "Tylko zaakceptowani użytkownicy mogą przeglądać zdjęcia.";
"userProfile.error.notExists" = "Konto nie istnieje.";
"userProfile.error.notExists" = "Błąd podczas pobierania danych użytkownika.";
"userProfile.error.mute" = "Błąd podczas wyciszania użytkownika.";
"userProfile.error.block" = "Błąd podczas blokowania/odblokowywania użytkownika.";
@ -350,3 +355,10 @@
"report.title.scam" = "Znęcanie się lub nękanie";
"report.title.terrorism" = "Terroryzm";
"report.error.notReported" = "Błąd podczas wysyłania zgłoszenia.";
// Mark: Following requests.
"followingRequests.navigationBar.title" = "Prośby o obserwowanie";
"followingRequests.title.approve" = "Zaakceptuj";
"followingRequests.title.reject" = "Odrzuć";
"followingRequests.error.approve" = "Błąd podczas akceptowania prośby.";
"followingRequests.error.reject" = "Błąd podczas odrzucania prośby.";

View File

@ -0,0 +1,40 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
public extension PixelfedClientAuthenticated {
func followRequests(limit: Int? = nil,
page: Page? = nil) async throws -> [Account] {
let request = try Self.request(
for: baseURL,
target: Pixelfed.FollowRequests.followRequests(limit, page),
withBearerToken: token
)
return try await downloadJson([Account].self, request: request)
}
func authorizeRequest(id: EntityId) async throws -> Relationship {
let request = try Self.request(
for: baseURL,
target: Pixelfed.FollowRequests.authorize(id),
withBearerToken: token
)
return try await downloadJson(Relationship.self, request: request)
}
func rejectRequest(id: EntityId) async throws -> Relationship {
let request = try Self.request(
for: baseURL,
target: Pixelfed.FollowRequests.reject(id),
withBearerToken: token
)
return try await downloadJson(Relationship.self, request: request)
}
}

View File

@ -8,7 +8,7 @@ import Foundation
extension Pixelfed {
public enum FollowRequests {
case followRequests
case followRequests(Limit?, Page?)
case authorize(String)
case reject(String)
}
@ -22,10 +22,10 @@ extension Pixelfed.FollowRequests: TargetType {
switch self {
case .followRequests:
return "\(apiPath)"
case .authorize:
return "\(apiPath)/authorize"
case .reject:
return "\(apiPath)/reject"
case .authorize(let id):
return "\(apiPath)/\(id)/authorize"
case .reject(let id):
return "\(apiPath)/\(id)/reject"
}
}
@ -39,9 +39,29 @@ extension Pixelfed.FollowRequests: TargetType {
}
}
/// The parameters to be incoded in the request.
public var queryItems: [(String, String)]? {
nil
var params: [(String, String)] = []
var limit: Limit?
var page: Page?
switch self {
case .followRequests(let paramLimit, let paramPage):
limit = paramLimit
page = paramPage
default:
return nil
}
if let limit {
params.append(("limit", "\(limit)"))
}
if let page {
params.append(("page", "\(page)"))
}
return params
}
public var headers: [String: String]? {
@ -49,15 +69,6 @@ extension Pixelfed.FollowRequests: TargetType {
}
public var httpBody: Data? {
switch self {
case .followRequests:
return nil
case .authorize(let id):
return try? JSONEncoder().encode(
["id": id]
)
case .reject:
return nil
}
return nil
}
}

View File

@ -25,6 +25,8 @@
F8210DDD2966CF17001D9973 /* StatusData+Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DDC2966CF17001D9973 /* StatusData+Status.swift */; };
F8210DDF2966CFC7001D9973 /* AttachmentData+Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DDE2966CFC7001D9973 /* AttachmentData+Attachment.swift */; };
F8210DEA2966E4F9001D9973 /* AnimatePlaceholderModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DE92966E4F9001D9973 /* AnimatePlaceholderModifier.swift */; };
F825F0C929F7A562008BD204 /* UserProfilePrivateAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F825F0C829F7A562008BD204 /* UserProfilePrivateAccountView.swift */; };
F825F0CB29F7CFC4008BD204 /* FollowRequestsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F825F0CA29F7CFC4008BD204 /* FollowRequestsView.swift */; };
F835082329BEF9C400DE3247 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = F835082629BEF9C400DE3247 /* Localizable.strings */; };
F835082429BEF9C400DE3247 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = F835082629BEF9C400DE3247 /* Localizable.strings */; };
F83CBEFB298298A1002972C8 /* ImageCarouselPicture.swift in Sources */ = {isa = PBXBuildFile; fileRef = F83CBEFA298298A1002972C8 /* ImageCarouselPicture.swift */; };
@ -231,6 +233,8 @@
F8210DDC2966CF17001D9973 /* StatusData+Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusData+Status.swift"; sourceTree = "<group>"; };
F8210DDE2966CFC7001D9973 /* AttachmentData+Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttachmentData+Attachment.swift"; sourceTree = "<group>"; };
F8210DE92966E4F9001D9973 /* AnimatePlaceholderModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatePlaceholderModifier.swift; sourceTree = "<group>"; };
F825F0C829F7A562008BD204 /* UserProfilePrivateAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfilePrivateAccountView.swift; sourceTree = "<group>"; };
F825F0CA29F7CFC4008BD204 /* FollowRequestsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowRequestsView.swift; sourceTree = "<group>"; };
F835082529BEF9C400DE3247 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
F835082729BEFA1E00DE3247 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = "<group>"; };
F837269429A221420098D3C4 /* PixelfedKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = PixelfedKit; sourceTree = "<group>"; };
@ -477,6 +481,7 @@
F89B5CC129D01BF700549F2F /* InstanceView.swift */,
F805DCF029DBEF83006A1FD9 /* ReportView.swift */,
F86BC9EA29EBDA2E009415EC /* ActivityView.swift */,
F825F0CA29F7CFC4008BD204 /* FollowRequestsView.swift */,
);
path = Views;
sourceTree = "<group>";
@ -607,6 +612,7 @@
children = (
F86B7213296BFDCE00EE59EC /* UserProfileHeaderView.swift */,
F86B7215296BFFDA00EE59EC /* UserProfileStatusesView.swift */,
F825F0C829F7A562008BD204 /* UserProfilePrivateAccountView.swift */,
);
path = Subviews;
sourceTree = "<group>";
@ -1136,6 +1142,8 @@
F8FB8ABA29EB2ED400342C04 /* NavigationMenuButtons.swift in Sources */,
F88BC51D29E0377B00CE6141 /* AccountData+AccountModel.swift in Sources */,
F89B5CC229D01BF700549F2F /* InstanceView.swift in Sources */,
F825F0CB29F7CFC4008BD204 /* FollowRequestsView.swift in Sources */,
F825F0C929F7A562008BD204 /* UserProfilePrivateAccountView.swift in Sources */,
F89F57B029D1C11200001EE3 /* RelationshipModel.swift in Sources */,
F88AB05829B36B8200345EDE /* AccountsPhotoView.swift in Sources */,
F85D4971296402DC00751DF7 /* AuthorizationService.swift in Sources */,

View File

@ -50,6 +50,8 @@ extension View {
EditProfileView()
case .instance:
InstanceView()
case .followRequests:
FollowRequestsView()
}
}
}

View File

@ -9,6 +9,12 @@ import Foundation
import PixelfedKit
public class RelationshipModel: ObservableObject {
enum RelationshipAction {
case follow
case unfollow
case requestFollow
case cancelRequestFollow
}
/// The account ID.
@Published public var id: EntityId
@ -122,3 +128,25 @@ extension RelationshipModel {
self.note = relationship.note
}
}
extension RelationshipModel {
func haveAccessToPhotos(account: Account) -> Bool {
return !account.locked || (account.locked && self.following)
}
func getRelationshipAction(account: Account) -> RelationshipAction {
if self.following {
return .unfollow
}
if self.requested {
return .cancelRequestFollow
}
if account.locked {
return .requestFollow
}
return .follow
}
}

View File

@ -26,6 +26,7 @@ enum RouteurDestinations: Hashable {
case search
case editProfile
case instance
case followRequests
}
enum SheetDestinations: Identifiable {

View File

@ -0,0 +1,179 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import SwiftUI
import PixelfedKit
import ClientKit
import Foundation
import ServicesKit
import EnvironmentKit
import WidgetsKit
struct FollowRequestsView: View {
@EnvironmentObject var applicationState: ApplicationState
@EnvironmentObject var routerPath: RouterPath
@EnvironmentObject var client: Client
@State private var accounts: [Account] = []
@State private var downloadedPage = 1
@State private var allItemsLoaded = false
@State private var state: ViewState = .loading
var body: some View {
self.mainBody()
.navigationTitle("followingRequests.navigationBar.title")
}
@ViewBuilder
private func mainBody() -> some View {
switch state {
case .loading:
LoadingIndicator()
.task {
await self.loadData(page: self.downloadedPage)
}
case .loaded:
if self.accounts.isEmpty {
NoDataView(imageSystemName: "person.3.sequence", text: "accounts.title.noAccounts")
} else {
self.list()
}
case .error(let error):
ErrorView(error: error) {
self.state = .loading
self.downloadedPage = 1
self.allItemsLoaded = false
self.accounts = []
await self.loadData(page: self.downloadedPage)
}
.padding()
}
}
@ViewBuilder
private func list() -> some View {
List {
ForEach(accounts, id: \.id) { account in
NavigationLink(value: RouteurDestinations.userProfile(
accountId: account.id,
accountDisplayName: account.displayNameWithoutEmojis,
accountUserName: account.acct)
) {
VStack(alignment: .leading) {
HStack(alignment: .center) {
UserAvatar(accountAvatar: account.avatar, size: .list)
VStack(alignment: .leading) {
Text(account.displayName ?? account.username)
.foregroundColor(.mainTextColor)
Text("@\(account.acct)")
.foregroundColor(.lightGrayColor)
.font(.footnote)
}
.padding(.leading, 8)
}
if let note = account.note, !note.asMarkdown.isEmpty {
MarkdownFormattedText(note.asMarkdown)
.font(.footnote)
.environment(\.openURL, OpenURLAction { url in
routerPath.handle(url: url)
})
.padding(.vertical, 4)
}
}
.swipeActions {
Button {
} label: {
Label("followingRequests.title.approve", systemImage: "checkmark")
}
Button(role: .destructive) {
} label: {
Label("followingRequests.title.reject", systemImage: "xmark")
}
.tint(.dangerColor)
}
}
}
if allItemsLoaded == false {
HStack {
Spacer()
LoadingIndicator()
.task {
self.downloadedPage = self.downloadedPage + 1
await self.loadData(page: self.downloadedPage)
}
Spacer()
}
.listRowSeparator(.hidden)
}
}
.listStyle(.plain)
}
private func loadData(page: Int) async {
do {
try await self.loadAccounts(page: page)
self.state = .loaded
} catch {
if !Task.isCancelled {
ErrorService.shared.handle(error, message: "accounts.error.loadingAccountsFailed", showToastr: true)
self.state = .error(error)
} else {
ErrorService.shared.handle(error, message: "accounts.error.loadingAccountsFailed", showToastr: false)
}
}
}
private func loadAccounts(page: Int) async throws {
let accountsFromApi = try await self.loadFromApi(page: page)
if accountsFromApi.isEmpty {
self.allItemsLoaded = true
return
}
await self.downloadAvatars(accounts: accountsFromApi)
self.accounts.append(contentsOf: accountsFromApi)
}
private func loadFromApi(page: Int) async throws -> [Account] {
// TODO: Workaround for not working paging for favourites/reblogged issues: https://github.com/pixelfed/pixelfed/issues/4182.
if page == 1 {
let results = try await self.client.followRequests?.followRequests(limit: 100, page: page)
return results ?? []
} else {
return []
}
}
private func approve(account: Account) async {
do {
_ = try await self.client.followRequests?.authorizeRequest(id: account.id)
} catch {
ErrorService.shared.handle(error, message: "followingRequests.error.approve", showToastr: true)
}
}
private func reject(account: Account) async {
do {
_ = try await self.client.followRequests?.rejectRequest(id: account.id)
} catch {
ErrorService.shared.handle(error, message: "followingRequests.error.reject", showToastr: true)
}
}
private func downloadAvatars(accounts: [Account]) async {
await withTaskGroup(of: Void.self) { group in
for account in accounts {
group.addTask { await CacheImageService.shared.download(url: account.avatar) }
}
}
}
}

View File

@ -21,18 +21,6 @@ struct UserProfileHeaderView: View {
var body: some View {
VStack(alignment: .leading) {
HStack(alignment: .top) {
Spacer()
if self.relationship.muting == true {
TagWidget(value: "userProfile.title.muted", color: .accentColor, systemImage: "message.and.waveform.fill")
}
if self.relationship.blocking == true {
TagWidget(value: "userProfile.title.blocked", color: .dangerColor, systemImage: "hand.raised.fill")
}
}
HStack(alignment: .center) {
UserAvatar(accountAvatar: account.avatar, size: .profile)
@ -108,6 +96,8 @@ struct UserProfileHeaderView: View {
.font(.footnote)
}
self.accountRelationshipPanel()
Text(String(format: NSLocalizedString("userProfile.title.joined", comment: "Joined"), account.createdAt.toRelative(.isoDateTimeMilliSec)))
.foregroundColor(.lightGrayColor.opacity(0.5))
.font(.footnote)
@ -115,6 +105,27 @@ struct UserProfileHeaderView: View {
.padding()
}
@ViewBuilder
private func accountRelationshipPanel() -> some View {
if self.relationship.followedBy || self.relationship.muting || self.relationship.blocking {
HStack(alignment: .top) {
if self.relationship.followedBy {
TagWidget(value: "userProfile.title.followsYou", color: .secondary, systemImage: "person.crop.circle.badge.checkmark")
}
if self.relationship.muting {
TagWidget(value: "userProfile.title.muted", color: .accentColor, systemImage: "message.and.waveform.fill")
}
if self.relationship.blocking {
TagWidget(value: "userProfile.title.blocked", color: .dangerColor, systemImage: "hand.raised.fill")
}
Spacer()
}
}
}
@ViewBuilder
private func otherAccountActionButtons() -> some View {
ActionButton {
@ -122,24 +133,51 @@ struct UserProfileHeaderView: View {
} label: {
HStack {
Image(systemName: relationship.following == true ? "person.badge.minus" : "person.badge.plus")
Text(relationship.following == true
? "userProfile.title.unfollow"
: (relationship.followedBy == true ? "userProfile.title.followBack" : "userProfile.title.follow"), comment: "Follow/unfollow actions")
Text(self.getRelationshipActionText(), comment: "Follow/unfollow actions")
}
}
.buttonStyle(.borderedProminent)
.tint(relationship.following == true ? .dangerColor : .accentColor)
.tint(self.getTintColor())
}
private func getRelationshipActionText() -> LocalizedStringKey {
let relationshipAction = self.relationship.getRelationshipAction(account: self.account)
switch relationshipAction {
case .follow:
return "userProfile.title.follow"
case .cancelRequestFollow:
return "userProfile.title.cancelRequestFollow"
case .requestFollow:
return "userProfile.title.requestFollow"
case .unfollow:
return "userProfile.title.unfollow"
}
}
private func getTintColor() -> Color {
let relationshipAction = self.relationship.getRelationshipAction(account: self.account)
switch relationshipAction {
case .follow, .requestFollow:
return .accentColor
case .cancelRequestFollow, .unfollow:
return .dangerColor
}
}
private func onRelationshipButtonTap() async {
do {
if self.relationship.following == true {
if let relationship = try await self.client.accounts?.unfollow(account: self.account.id) {
self.relationship.following = relationship.following
}
} else {
let relationshipAction = self.relationship.getRelationshipAction(account: self.account)
switch relationshipAction {
case .follow, .requestFollow:
if let relationship = try await self.client.accounts?.follow(account: self.account.id) {
self.relationship.following = relationship.following
self.relationship.update(relationship: relationship)
}
case .cancelRequestFollow, .unfollow:
if let relationship = try await self.client.accounts?.unfollow(account: self.account.id) {
self.relationship.update(relationship: relationship)
}
}
} catch {

View File

@ -0,0 +1,22 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import SwiftUI
struct UserProfilePrivateAccountView: View {
var body: some View {
VStack(alignment: .center) {
Spacer()
Text("userProfile.title.privateProfileTitle", comment: "This profile is private.")
.fontWeight(.bold)
.font(.headline)
Text("userProfile.title.privateProfileSubtitle", comment: "Only approved followers can see photos.")
.fontWeight(.light)
.font(.subheadline)
Spacer()
}.padding(.top, 60)
}
}

View File

@ -66,7 +66,12 @@ struct UserProfileView: View {
ScrollView {
UserProfileHeaderView(account: account, relationship: relationship)
.id(self.viewId)
UserProfileStatusesView(accountId: account.id)
if self.applicationState.account?.id == account.id || self.relationship.haveAccessToPhotos(account: account) {
UserProfileStatusesView(accountId: account.id)
} else {
UserProfilePrivateAccountView()
}
}
.onAppear {
if let updatedProfile = self.applicationState.updatedProfile {
@ -189,13 +194,7 @@ struct UserProfileView: View {
Divider()
NavigationLink(value: RouteurDestinations.accounts(listType: .blocks)) {
Label(NSLocalizedString("userProfile.title.blocks", comment: "Blocked accounts"), systemImage: "hand.raised.fill")
}
NavigationLink(value: RouteurDestinations.accounts(listType: .mutes)) {
Label(NSLocalizedString("userProfile.title.mutes", comment: "Muted accounts"), systemImage: "message.and.waveform.fill")
}
self.accountsMenuView(account: account)
Divider()
@ -220,6 +219,23 @@ struct UserProfileView: View {
}
}
@ViewBuilder
private func accountsMenuView(account: Account) -> some View {
NavigationLink(value: RouteurDestinations.accounts(listType: .blocks)) {
Label(NSLocalizedString("userProfile.title.blocks", comment: "Blocked accounts"), systemImage: "hand.raised.fill")
}
NavigationLink(value: RouteurDestinations.accounts(listType: .mutes)) {
Label(NSLocalizedString("userProfile.title.mutes", comment: "Muted accounts"), systemImage: "message.and.waveform.fill")
}
if account.locked {
NavigationLink(value: RouteurDestinations.followRequests) {
Label(NSLocalizedString("userProfile.title.followRequests", comment: "FollowRequests"), systemImage: "person.crop.circle.badge.checkmark")
}
}
}
private func onMuteAccount(account: Account) async {
do {
if self.relationship.muting == true {

View File

@ -30,8 +30,8 @@ public struct TagWidget: View {
.font(.footnote)
.fontWeight(.semibold)
}
.padding(.horizontal, 8)
.padding(.vertical, 2)
.padding(.horizontal, 10)
.padding(.vertical, 6)
.background(Capsule().foregroundColor(self.color))
}
}