diff --git a/Bubble/Localizable.xcstrings b/Bubble/Localizable.xcstrings index 9c5309e..8125cff 100644 --- a/Bubble/Localizable.xcstrings +++ b/Bubble/Localizable.xcstrings @@ -82,13 +82,13 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Bubble is a very simple Mastodon client, that is meant to look clean and expurgated. It integrates perfectly with your Mastodon account, and matches your vibe, while having Mastodon-only features.\n\nBubble is mainly free, made in France using SwiftUI, [open-source](https://github.com/lumaa-dev/BubbleApp), and doesn't violate [your privacy](https://apps.lumaa.fr/legal/privacy).\n\nBubble is not related or affiliated to Mastodon gGmbH." + "value" : "Bubble is a very simple Mastodon client, that is meant to look clean and expurgated. It integrates perfectly with your Mastodon account, and matches your vibe, while having Mastodon-only features.\n\nBubble is mainly free, made in France using SwiftUI, [open-source](https://github.com/lumaa-dev/BubbleApp), and doesn't violate [your privacy](https://apps.lumaa.fr/legal/privacy?app=bubble).\n\nBubble is not related or affiliated to Mastodon gGmbH." } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Bubble is a very simple Mastodon client, that is meant to look clean and expurgated. It integrates perfectly with your Mastodon account, and matches your vibe, while having Mastodon-only features.\n\nBubble is mainly free, made in France using SwiftUI, [open-source](https://github.com/lumaa-dev/BubbleApp), and doesn't violate [your privacy](https://apps.lumaa.fr/legal/privacy).\n\nBubble is not related or affiliated to Mastodon gGmbH." + "value" : "Bubble is a very simple Mastodon client, that is meant to look clean and expurgated. It integrates perfectly with your Mastodon account, and matches your vibe, while having Mastodon-only features.\n\nBubble is mainly free, made in France using SwiftUI, [open-source](https://github.com/lumaa-dev/BubbleApp), and doesn't violate [your privacy](https://apps.lumaa.fr/legal/privacy?app=bubble).\n\nBubble is not related or affiliated to Mastodon gGmbH." } } } @@ -589,6 +589,22 @@ } } }, + "account.hide-reblog" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hide reblogs" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cacher les republications" + } + } + } + }, "account.mention" : { "localizations" : { "en" : { @@ -653,6 +669,39 @@ } } }, + "account.request-follow" : { + "comment" : "The action to request the selected user to follow their account", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Request Follow" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Demander pour suivre" + } + } + } + }, + "account.show-reblog" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Show reblogs" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Afficher les republications" + } + } + } + }, "account.subclub.subscribe" : { "localizations" : { "en" : { @@ -1669,6 +1718,23 @@ } } }, + "generic.more" : { + "comment" : "A generic word for \"More actions\"", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "More actions" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Plus d'actions" + } + } + } + }, "info.subclub.collaboration" : { "comment" : "sub.club is a COLLAB, precisely", "localizations" : { @@ -5216,4 +5282,4 @@ } }, "version" : "1.0" -} +} \ No newline at end of file diff --git a/Bubble/Views/Profile/ProfileView.swift b/Bubble/Views/Profile/ProfileView.swift index ee9a3d5..5e31ee4 100644 --- a/Bubble/Views/Profile/ProfileView.swift +++ b/Bubble/Views/Profile/ProfileView.swift @@ -15,13 +15,17 @@ struct ProfileView: View { @State private var canFollow: Bool? = nil @State private var initialFollowing: Bool = false + @State private var hasRequested: Bool = false @State private var isFollowing: Bool = false + @State private var accountFollows: Bool = false - + @State private var accountReblogging: Bool = false + @State private var accountNotify: Bool = false @State private var accountMuted: Bool = false @State private var accountBlocked: Bool = false + @State private var instanceBlocked: Bool = false - + @State private var loadingStatuses: Bool = false @State private var statuses: [Status]? = [] @State private var statusesPinned: [Status]? = [] @@ -58,70 +62,7 @@ struct ProfileView: View { .toolbar { if !isCurrent { ToolbarItem(placement: .primaryAction) { - Menu { - if accountMuted { - Button { - guard let client = accountManager.getClient() else { return } - - Task { - do { - _ = try await client.post(endpoint: Accounts.unmute(id: account.id)) - accountMuted = false - HapticManager.playHaptics(haptics: Haptic.success) - } catch { - HapticManager.playHaptics(haptics: Haptic.error) - print(error) - } - } - } label: { - Label("account.unmute", systemImage: "speaker.wave.2.fill") - } - } else { - Menu { - ForEach(MuteData.MuteDuration.allCases, id: \.self) { duration in - Button { - guard let client = accountManager.getClient() else { return } - - Task { - do { - _ = try await client.post(endpoint: Accounts.mute(id: account.id, json: .init(duration: duration.rawValue))) - accountMuted = true - HapticManager.playHaptics(haptics: Haptic.success) - } catch { - HapticManager.playHaptics(haptics: Haptic.error) - print(error) - } - } - } label: { - Text(duration.description) - } - } - } label: { - Label("account.mute", systemImage: "speaker.slash") - } - } - - Button(role: accountBlocked ? .cancel : .destructive) { - guard let client = accountManager.getClient() else { return } - - Task { - do { - let endp: Endpoint = accountBlocked ? Accounts.unblock(id: account.id) : Accounts.block(id: account.id) - _ = try await client.post(endpoint: endp) - accountBlocked.toggle() - HapticManager.playHaptics(haptics: Haptic.success) - } catch { - HapticManager.playHaptics(haptics: Haptic.error) - print(error) - } - } - } label: { - Label(accountBlocked ? "account.unblock" : "account.block", systemImage: accountBlocked ? "person.fill.badge.plus" : "person.slash.fill") - } - } label: { - Image(systemName: "shield.righthalf.filled") - .font(.title2) - } + accountMenu } } } @@ -173,7 +114,7 @@ struct ProfileView: View { .toolbarBackground(Color.appBackground, for: .navigationBar) .toolbarBackground(.automatic, for: .navigationBar) } - + // MARK: - Headers var wholeSmall: some View { @@ -261,6 +202,7 @@ struct ProfileView: View { .disabled(isSubscribed) } + // MARK: Follow button if (canFollow ?? true) == true { HStack (spacing: 5) { Button { @@ -269,33 +211,39 @@ struct ProfileView: View { } } label: { HStack { + let localized: LocalizedStringKey = account.locked ? hasRequested || isFollowing ? "account.unfollow" : "account.request-follow" : ( + isFollowing ? "account.unfollow" : accountFollows ? "account.follow-back" : "account.follow" + ) Spacer() - Text(isFollowing ? "account.unfollow" : accountFollows ? "account.follow-back" : "account.follow") + Text(localized) .font(.callout) Spacer() } } .buttonStyle(LargeButton(filled: true, height: 10)) - Button { - if let server = account.acct.split(separator: "@").last { - uniNav.presentedSheet = .post(content: "@\(account.username)@\(server)") - } else { - let client = accountManager.getClient() - uniNav.presentedSheet = .post(content: "@\(account.username)@\(client?.server ?? "???")") - } - } label: { - HStack { - Spacer() - Text("account.mention") - .font(.callout) - Spacer() + if !account.locked || isFollowing { + Button { + if let server = account.acct.split(separator: "@").last { + uniNav.presentedSheet = .post(content: "@\(account.username)@\(server)") + } else { + let client = accountManager.getClient() + uniNav.presentedSheet = .post(content: "@\(account.username)@\(client?.server ?? "???")") + } + } label: { + HStack { + Spacer() + Text("account.mention") + .font(.callout) + Spacer() + } } + .buttonStyle(LargeButton(filled: false, height: 10)) } - .buttonStyle(LargeButton(filled: false, height: 10)) } } } + .padding(.vertical, 3.5) if isCurrent { Button { @@ -326,7 +274,8 @@ struct ProfileView: View { .padding(.horizontal) } } - + + // MARK: Fields var fields: some View { VStack(alignment: .leading, spacing: 7.5) { ForEach(account.fields) { field in @@ -344,7 +293,8 @@ struct ProfileView: View { TextEmoji(field.value, emojis: account.emojis) } .onTapGesture { - if let url = URL(string: field.value.asRawText) { + if let url = URL(string: field.value.asRawText), url.absoluteString + .matches(of: #"\b((http|https):\/\/)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,6}(\/[a-zA-Z0-9\-._~:\/?#[\]@!$&'()*+,;=%]*)?\b"#).count > 0 { HapticManager.playHaptics(haptics: Haptic.success) openURL(url) } else { @@ -404,14 +354,25 @@ struct ProfileView: View { } } } - + + // MARK: - Functions & Other + func followAccount() async { if let client = accountManager.getClient() { Task { - let endpoint: Endpoint = isFollowing ? Accounts.unfollow(id: account.id) : Accounts.follow(id: account.id, notify: false, reblogs: true) + let endpoint: Endpoint = isFollowing || hasRequested ? Accounts.unfollow(id: account.id) : Accounts.follow( + id: account.id, + notify: false, + reblogs: true + ) HapticManager.playHaptics(haptics: Haptic.tap) _ = try await client.post(endpoint: endpoint) // Notify off until APNs? | Reblogs on by default (later changeable) - isFollowing = !isFollowing + + if !account.locked { + isFollowing = !isFollowing + } else { + hasRequested = !hasRequested + } } } } @@ -431,7 +392,6 @@ struct ProfileView: View { await updateRelationship() } - loadingStatuses = true statuses = try? await client.get(endpoint: Accounts.statuses(id: accId, sinceId: nil, tag: nil, onlyMedia: nil, excludeReplies: nil, pinned: nil)) statusesPinned = try? await client.get(endpoint: Accounts.statuses(id: accId, sinceId: nil, tag: nil, onlyMedia: nil, excludeReplies: nil, pinned: true)) @@ -439,7 +399,94 @@ struct ProfileView: View { } } } - + + var accountMenu: some View { + Menu { + Button { + guard let client = accountManager.getClient() else { return } + + Task { + do { + _ = try await client.post(endpoint: Accounts.follow(id: account.id, notify: accountNotify, reblogs: !accountReblogging)) + accountReblogging.toggle() + + HapticManager.playHaptics(haptics: Haptic.success) + } catch { + HapticManager.playHaptics(haptics: Haptic.error) + print(error) + } + } + } label: { + Label(accountReblogging ? "account.hide-reblog" : "account.show-reblog", systemImage: accountReblogging ? "bolt.horizontal.fill" : "bolt.horizontal") + } + .disabled(!isFollowing) + + Divider() + + if accountMuted { + Button { + guard let client = accountManager.getClient() else { return } + + Task { + do { + _ = try await client.post(endpoint: Accounts.unmute(id: account.id)) + accountMuted = false + HapticManager.playHaptics(haptics: Haptic.success) + } catch { + HapticManager.playHaptics(haptics: Haptic.error) + print(error) + } + } + } label: { + Label("account.unmute", systemImage: "speaker.wave.2.fill") + } + } else { + Menu { + ForEach(MuteData.MuteDuration.allCases, id: \.self) { duration in + Button { + guard let client = accountManager.getClient() else { return } + + Task { + do { + _ = try await client.post(endpoint: Accounts.mute(id: account.id, json: .init(duration: duration.rawValue))) + accountMuted = true + HapticManager.playHaptics(haptics: Haptic.success) + } catch { + HapticManager.playHaptics(haptics: Haptic.error) + print(error) + } + } + } label: { + Text(duration.description) + } + } + } label: { + Label("account.mute", systemImage: "speaker.slash") + } + } + + Button(role: accountBlocked ? .cancel : .destructive) { + guard let client = accountManager.getClient() else { return } + + Task { + do { + let endp: Endpoint = accountBlocked ? Accounts.unblock(id: account.id) : Accounts.block(id: account.id) + _ = try await client.post(endpoint: endp) + accountBlocked.toggle() + HapticManager.playHaptics(haptics: Haptic.success) + } catch { + HapticManager.playHaptics(haptics: Haptic.error) + print(error) + } + } + } label: { + Label(accountBlocked ? "account.unblock" : "account.block", systemImage: accountBlocked ? "person.fill.badge.plus" : "person.slash.fill") + } + } label: { + Label("generic.more", systemImage: "ellipsis") + } + } + func updateRelationship(with other: [String] = [], subClubId: String? = nil) async { if let client = accountManager.getClient() { if let currentAccount: Account = try? await client.get(endpoint: Accounts.verifyCredentials) { @@ -452,7 +499,10 @@ struct ProfileView: View { if let relationship: [Relationship] = try? await client.get(endpoint: Accounts.relationships(ids: relsId)) { let rel: Relationship = relationship.first! // the searched up account isFollowing = rel.following + hasRequested = rel.requested accountFollows = rel.followedBy + accountReblogging = rel.showingReblogs + accountNotify = rel.notifying accountMuted = rel.muting accountBlocked = rel.blocking