Add unfollow and information about empty following/followers list.
This commit is contained in:
parent
fc3f00ca86
commit
8a5552b21a
|
@ -52,6 +52,7 @@
|
|||
F866F6B729608467002E8F88 /* MastodonSwift in Frameworks */ = {isa = PBXBuildFile; productRef = F866F6B629608467002E8F88 /* MastodonSwift */; };
|
||||
F86B7214296BFDCE00EE59EC /* UserProfileHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86B7213296BFDCE00EE59EC /* UserProfileHeader.swift */; };
|
||||
F86B7216296BFFDA00EE59EC /* UserProfileStatuses.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86B7215296BFFDA00EE59EC /* UserProfileStatuses.swift */; };
|
||||
F86B7218296C27C100EE59EC /* ActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86B7217296C27C100EE59EC /* ActionButton.swift */; };
|
||||
F88ABD9229686F1C004EF61E /* MemoryCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88ABD9129686F1C004EF61E /* MemoryCache.swift */; };
|
||||
F88ABD9429687CA4004EF61E /* ComposeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88ABD9329687CA4004EF61E /* ComposeView.swift */; };
|
||||
F88C246C295C37B80006098B /* VernissageApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C246B295C37B80006098B /* VernissageApp.swift */; };
|
||||
|
@ -127,6 +128,7 @@
|
|||
F866F6AD29606367002E8F88 /* ApplicationViewMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationViewMode.swift; sourceTree = "<group>"; };
|
||||
F86B7213296BFDCE00EE59EC /* UserProfileHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileHeader.swift; sourceTree = "<group>"; };
|
||||
F86B7215296BFFDA00EE59EC /* UserProfileStatuses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileStatuses.swift; sourceTree = "<group>"; };
|
||||
F86B7217296C27C100EE59EC /* ActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButton.swift; sourceTree = "<group>"; };
|
||||
F88ABD9129686F1C004EF61E /* MemoryCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryCache.swift; sourceTree = "<group>"; };
|
||||
F88ABD9329687CA4004EF61E /* ComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeView.swift; sourceTree = "<group>"; };
|
||||
F88ABD9529687D4D004EF61E /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
|
||||
|
@ -279,6 +281,7 @@
|
|||
F89797892968314A00B22335 /* LoadingIndicator.swift */,
|
||||
F86B7213296BFDCE00EE59EC /* UserProfileHeader.swift */,
|
||||
F86B7215296BFFDA00EE59EC /* UserProfileStatuses.swift */,
|
||||
F86B7217296C27C100EE59EC /* ActionButton.swift */,
|
||||
);
|
||||
path = Widgets;
|
||||
sourceTree = "<group>";
|
||||
|
@ -517,6 +520,7 @@
|
|||
F8A93D802965FED4001D8331 /* AccountService.swift in Sources */,
|
||||
F866F6AA29605AFA002E8F88 /* SceneDelegate.swift in Sources */,
|
||||
F85D4973296406E700751DF7 /* BottomRight.swift in Sources */,
|
||||
F86B7218296C27C100EE59EC /* ActionButton.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -652,8 +656,8 @@
|
|||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
|
@ -662,9 +666,12 @@
|
|||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.Vernissage;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
|
@ -683,8 +690,8 @@
|
|||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
|
@ -693,9 +700,12 @@
|
|||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.Vernissage;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
|
|
|
@ -75,6 +75,21 @@ extension MastodonClientAuthenticated {
|
|||
return try JSONDecoder().decode(Relationship.self, from: data)
|
||||
}
|
||||
|
||||
func unfollow(for accountId: String) async throws -> Relationship {
|
||||
let request = try Self.request(
|
||||
for: baseURL,
|
||||
target: Mastodon.Account.unfollow(accountId),
|
||||
withBearerToken: token
|
||||
)
|
||||
|
||||
let (data, response) = try await urlSession.data(for: request)
|
||||
guard (response as? HTTPURLResponse)?.status?.responseType == .success else {
|
||||
throw NetworkError.notSuccessResponse(response)
|
||||
}
|
||||
|
||||
return try JSONDecoder().decode(Relationship.self, from: data)
|
||||
}
|
||||
|
||||
func getFollowers(for accountId: String, page: Int = 1) async throws -> [Account] {
|
||||
let request = try Self.request(
|
||||
for: baseURL,
|
||||
|
|
|
@ -60,6 +60,15 @@ public class AccountService {
|
|||
return try await client.follow(for: accountId)
|
||||
}
|
||||
|
||||
public func unfollow(forAccountId accountId: String, andContext accountData: AccountData?) async throws -> Relationship? {
|
||||
guard let accessToken = accountData?.accessToken, let serverUrl = accountData?.serverUrl else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let client = MastodonClient(baseURL: serverUrl).getAuthenticated(token: accessToken)
|
||||
return try await client.unfollow(for: accountId)
|
||||
}
|
||||
|
||||
public func getFollowers(forAccountId accountId: String, andContext accountData: AccountData?, page: Int) async throws -> [Account] {
|
||||
guard let accessToken = accountData?.accessToken, let serverUrl = accountData?.serverUrl else {
|
||||
return []
|
||||
|
|
|
@ -44,6 +44,16 @@ struct FollowersView: View {
|
|||
}.overlay {
|
||||
if firstLoadFinished == false {
|
||||
LoadingIndicator()
|
||||
} else {
|
||||
if self.accounts.isEmpty {
|
||||
VStack {
|
||||
Image(systemName: "person.3.sequence")
|
||||
.font(.largeTitle)
|
||||
.padding(.bottom, 4)
|
||||
Text("Unfortunately, there is no one here.")
|
||||
.font(.title3)
|
||||
}.foregroundColor(.lightGrayColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarTitle("Followers")
|
||||
|
|
|
@ -44,6 +44,16 @@ struct FollowingView: View {
|
|||
}.overlay {
|
||||
if firstLoadFinished == false {
|
||||
LoadingIndicator()
|
||||
} else {
|
||||
if self.accounts.isEmpty {
|
||||
VStack {
|
||||
Image(systemName: "person.3.sequence")
|
||||
.font(.largeTitle)
|
||||
.padding(.bottom, 4)
|
||||
Text("Unfortunately, there is no one here.")
|
||||
.font(.title3)
|
||||
}.foregroundColor(.lightGrayColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarTitle("Following")
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
//
|
||||
// https://mczachurski.dev
|
||||
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ActionButton<Label> : View where Label : View {
|
||||
@State private var isDuringAction = false
|
||||
|
||||
private let action: () async -> Void
|
||||
private let label: () -> Label
|
||||
|
||||
public init(action: @escaping () async -> Void, @ViewBuilder label: @escaping () -> Label) {
|
||||
self.action = action
|
||||
self.label = label
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
Task {
|
||||
HapticService.shared.touch()
|
||||
defer {
|
||||
Task { @MainActor in
|
||||
withAnimation {
|
||||
self.isDuringAction = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
withAnimation {
|
||||
self.isDuringAction = true
|
||||
}
|
||||
|
||||
await action()
|
||||
}
|
||||
} label: {
|
||||
if isDuringAction {
|
||||
LoadingIndicator(withText: false)
|
||||
.transition(.opacity)
|
||||
} else {
|
||||
label()
|
||||
.transition(.opacity)
|
||||
}
|
||||
}.disabled(isDuringAction)
|
||||
}
|
||||
}
|
||||
|
||||
struct ActionButton_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ActionButton {
|
||||
|
||||
} label: {
|
||||
Text("Action")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -15,13 +15,12 @@ struct InteractionRow: View {
|
|||
@State var favourited = false
|
||||
@State var favouritesCount = 0
|
||||
@State var bookmarked = false
|
||||
|
||||
|
||||
var onNewStatus: (() -> Void)?
|
||||
|
||||
var body: some View {
|
||||
HStack (alignment: .top) {
|
||||
Button {
|
||||
HapticService.shared.touch()
|
||||
ActionButton {
|
||||
onNewStatus?()
|
||||
} label: {
|
||||
HStack(alignment: .center) {
|
||||
|
@ -33,25 +32,21 @@ struct InteractionRow: View {
|
|||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
Task {
|
||||
HapticService.shared.touch()
|
||||
ActionButton {
|
||||
do {
|
||||
let status = self.reblogged
|
||||
? try await StatusService.shared.unboost(statusId: self.statusId, accountData: self.applicationState.accountData)
|
||||
: try await StatusService.shared.boost(statusId: self.statusId, accountData: self.applicationState.accountData)
|
||||
|
||||
do {
|
||||
let status = self.reblogged
|
||||
? try await StatusService.shared.unboost(statusId: self.statusId, accountData: self.applicationState.accountData)
|
||||
: try await StatusService.shared.boost(statusId: self.statusId, accountData: self.applicationState.accountData)
|
||||
if let status {
|
||||
self.reblogsCount = status.reblogsCount == self.reblogsCount
|
||||
? status.reblogsCount + 1
|
||||
: status.reblogsCount
|
||||
|
||||
if let status {
|
||||
self.reblogsCount = status.reblogsCount == self.reblogsCount
|
||||
? status.reblogsCount + 1
|
||||
: status.reblogsCount
|
||||
|
||||
self.reblogged = status.reblogged
|
||||
}
|
||||
} catch {
|
||||
print("Error \(error.localizedDescription)")
|
||||
self.reblogged = status.reblogged
|
||||
}
|
||||
} catch {
|
||||
print("Error \(error.localizedDescription)")
|
||||
}
|
||||
} label: {
|
||||
HStack(alignment: .center) {
|
||||
|
@ -63,25 +58,21 @@ struct InteractionRow: View {
|
|||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
Task {
|
||||
HapticService.shared.touch()
|
||||
ActionButton {
|
||||
do {
|
||||
let status = self.favourited
|
||||
? try await StatusService.shared.unfavourite(statusId: self.statusId, accountData: self.applicationState.accountData)
|
||||
: try await StatusService.shared.favourite(statusId: self.statusId, accountData: self.applicationState.accountData)
|
||||
|
||||
do {
|
||||
let status = self.favourited
|
||||
? try await StatusService.shared.unfavourite(statusId: self.statusId, accountData: self.applicationState.accountData)
|
||||
: try await StatusService.shared.favourite(statusId: self.statusId, accountData: self.applicationState.accountData)
|
||||
if let status {
|
||||
self.favouritesCount = status.favouritesCount == self.favouritesCount
|
||||
? status.favouritesCount + 1
|
||||
: status.favouritesCount
|
||||
|
||||
if let status {
|
||||
self.favouritesCount = status.favouritesCount == self.favouritesCount
|
||||
? status.favouritesCount + 1
|
||||
: status.favouritesCount
|
||||
|
||||
self.favourited = status.favourited
|
||||
}
|
||||
} catch {
|
||||
print("Error \(error.localizedDescription)")
|
||||
self.favourited = status.favourited
|
||||
}
|
||||
} catch {
|
||||
print("Error \(error.localizedDescription)")
|
||||
}
|
||||
} label: {
|
||||
HStack(alignment: .center) {
|
||||
|
@ -93,19 +84,15 @@ struct InteractionRow: View {
|
|||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
Task {
|
||||
HapticService.shared.touch()
|
||||
|
||||
do {
|
||||
_ = self.bookmarked
|
||||
? try await StatusService.shared.unbookmark(statusId: self.statusId, accountData: self.applicationState.accountData)
|
||||
: try await StatusService.shared.bookmark(statusId: self.statusId, accountData: self.applicationState.accountData)
|
||||
ActionButton {
|
||||
do {
|
||||
_ = self.bookmarked
|
||||
? try await StatusService.shared.unbookmark(statusId: self.statusId, accountData: self.applicationState.accountData)
|
||||
: try await StatusService.shared.bookmark(statusId: self.statusId, accountData: self.applicationState.accountData)
|
||||
|
||||
self.bookmarked.toggle()
|
||||
} catch {
|
||||
print("Error \(error.localizedDescription)")
|
||||
}
|
||||
self.bookmarked.toggle()
|
||||
} catch {
|
||||
print("Error \(error.localizedDescription)")
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: self.bookmarked ? "bookmark.fill" : "bookmark")
|
||||
|
@ -113,9 +100,8 @@ struct InteractionRow: View {
|
|||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
ActionButton {
|
||||
// TODO: Share.
|
||||
HapticService.shared.touch()
|
||||
} label: {
|
||||
Image(systemName: "square.and.arrow.up")
|
||||
}
|
||||
|
|
|
@ -11,8 +11,6 @@ struct UserProfileHeader: View {
|
|||
@EnvironmentObject private var applicationState: ApplicationState
|
||||
@State var account: Account
|
||||
@State var relationship: Relationship? = nil
|
||||
|
||||
@State private var isDuringRelationshipAction = false
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
|
@ -72,45 +70,14 @@ struct UserProfileHeader: View {
|
|||
Spacer()
|
||||
|
||||
if self.applicationState.accountData?.id != self.account.id {
|
||||
Button {
|
||||
Task {
|
||||
defer {
|
||||
Task { @MainActor in
|
||||
withAnimation {
|
||||
self.isDuringRelationshipAction = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HapticService.shared.touch()
|
||||
withAnimation {
|
||||
self.isDuringRelationshipAction = true
|
||||
}
|
||||
|
||||
do {
|
||||
if let relationship = try await AccountService.shared.follow(
|
||||
forAccountId: self.account.id,
|
||||
andContext: self.applicationState.accountData
|
||||
) {
|
||||
self.relationship = relationship
|
||||
}
|
||||
} catch {
|
||||
print("Error \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
ActionButton {
|
||||
await onRelationshipButtonTap()
|
||||
} label: {
|
||||
if isDuringRelationshipAction {
|
||||
LoadingIndicator(withText: false)
|
||||
.transition(.opacity)
|
||||
} else {
|
||||
HStack {
|
||||
Image(systemName: relationship?.following == true ? "person.badge.minus" : "person.badge.plus")
|
||||
Text(relationship?.following == true ? "Unfollow" : (relationship?.followedBy == true ? "Follow back" : "Follow"))
|
||||
}
|
||||
.transition(.opacity)
|
||||
HStack {
|
||||
Image(systemName: relationship?.following == true ? "person.badge.minus" : "person.badge.plus")
|
||||
Text(relationship?.following == true ? "Unfollow" : (relationship?.followedBy == true ? "Follow back" : "Follow"))
|
||||
}
|
||||
}
|
||||
.disabled(isDuringRelationshipAction)
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(relationship?.following == true ? .dangerColor : .accentColor)
|
||||
}
|
||||
|
@ -129,6 +96,28 @@ struct UserProfileHeader: View {
|
|||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
private func onRelationshipButtonTap() async {
|
||||
do {
|
||||
if self.relationship?.following == true {
|
||||
if let relationship = try await AccountService.shared.unfollow(
|
||||
forAccountId: self.account.id,
|
||||
andContext: self.applicationState.accountData
|
||||
) {
|
||||
self.relationship = relationship
|
||||
}
|
||||
} else {
|
||||
if let relationship = try await AccountService.shared.follow(
|
||||
forAccountId: self.account.id,
|
||||
andContext: self.applicationState.accountData
|
||||
) {
|
||||
self.relationship = relationship
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
print("Error \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct UserProfileHeader_Previews: PreviewProvider {
|
||||
|
|
Loading…
Reference in New Issue