Add unfollow and information about empty following/followers list.

This commit is contained in:
Marcin Czachursk 2023-01-09 12:14:33 +01:00
parent fc3f00ca86
commit 8a5552b21a
8 changed files with 181 additions and 93 deletions

View File

@ -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;
};

View File

@ -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,

View File

@ -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 []

View File

@ -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")

View File

@ -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")

View File

@ -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")
}
}
}

View File

@ -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")
}

View File

@ -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 {