Add support of follow/unfollow of tags
This commit is contained in:
parent
2c22651cac
commit
0daf234784
|
@ -25,3 +25,4 @@ public typealias Offset = Int
|
|||
public typealias Scope = String
|
||||
public typealias Scopes = [Scope]
|
||||
public typealias Token = String
|
||||
public typealias Hashtag = String
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
//
|
||||
// https://mczachurski.dev
|
||||
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension MastodonClientAuthenticated {
|
||||
|
||||
func tag(hashtag: String) async throws -> Tag {
|
||||
let request = try Self.request(
|
||||
for: baseURL,
|
||||
target: Mastodon.Tags.tag(hashtag),
|
||||
withBearerToken: token
|
||||
)
|
||||
|
||||
return try await downloadJson(Tag.self, request: request)
|
||||
}
|
||||
|
||||
func follow(hashtag: String) async throws -> Tag {
|
||||
let request = try Self.request(
|
||||
for: baseURL,
|
||||
target: Mastodon.Tags.follow(hashtag),
|
||||
withBearerToken: token
|
||||
)
|
||||
|
||||
return try await downloadJson(Tag.self, request: request)
|
||||
}
|
||||
|
||||
func unfollow(hashtag: String) async throws -> Tag {
|
||||
let request = try Self.request(
|
||||
for: baseURL,
|
||||
target: Mastodon.Tags.unfollow(hashtag),
|
||||
withBearerToken: token
|
||||
)
|
||||
|
||||
return try await downloadJson(Tag.self, request: request)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
//
|
||||
// https://mczachurski.dev
|
||||
// Copyright © 2022 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Mastodon {
|
||||
public enum Tags {
|
||||
case tag(Hashtag)
|
||||
case follow(Hashtag)
|
||||
case unfollow(Hashtag)
|
||||
}
|
||||
}
|
||||
|
||||
extension Mastodon.Tags: TargetType {
|
||||
fileprivate var apiPath: String { return "/api/v1/tags" }
|
||||
|
||||
/// The path to be appended to `baseURL` to form the full `URL`.
|
||||
public var path: String {
|
||||
switch self {
|
||||
case .tag(let hashtag):
|
||||
return "\(apiPath)/\(hashtag)"
|
||||
case .follow(let hashtag):
|
||||
return "\(apiPath)/\(hashtag)/follow"
|
||||
case .unfollow(let hashtag):
|
||||
return "\(apiPath)/\(hashtag)/unfollow"
|
||||
}
|
||||
}
|
||||
|
||||
/// The HTTP method used in the request.
|
||||
public var method: Method {
|
||||
switch self {
|
||||
case .tag:
|
||||
return .get
|
||||
case .follow, .unfollow:
|
||||
return .post
|
||||
}
|
||||
}
|
||||
|
||||
/// The parameters to be incoded in the request.
|
||||
public var queryItems: [(String, String)]? {
|
||||
return nil
|
||||
}
|
||||
|
||||
public var headers: [String: String]? {
|
||||
[:].contentTypeApplicationJson
|
||||
}
|
||||
|
||||
public var httpBody: Data? {
|
||||
nil
|
||||
}
|
||||
}
|
|
@ -111,6 +111,7 @@
|
|||
F8B1E6512973FB7E00EE0D10 /* ToastrService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8B1E6502973FB7E00EE0D10 /* ToastrService.swift */; };
|
||||
F8C14392296AF0B3001FE31D /* String+Exif.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8C14391296AF0B3001FE31D /* String+Exif.swift */; };
|
||||
F8C14394296AF21B001FE31D /* Double+Round.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8C14393296AF21B001FE31D /* Double+Round.swift */; };
|
||||
F8C7EDBF298169EE002843BC /* TagsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8C7EDBE298169EE002843BC /* TagsService.swift */; };
|
||||
F8CC95CE2970761D00C9C2AC /* TintColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8CC95CD2970761D00C9C2AC /* TintColor.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
|
@ -218,6 +219,7 @@
|
|||
F8B1E6502973FB7E00EE0D10 /* ToastrService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastrService.swift; sourceTree = "<group>"; };
|
||||
F8C14391296AF0B3001FE31D /* String+Exif.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Exif.swift"; sourceTree = "<group>"; };
|
||||
F8C14393296AF21B001FE31D /* Double+Round.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+Round.swift"; sourceTree = "<group>"; };
|
||||
F8C7EDBE298169EE002843BC /* TagsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagsService.swift; sourceTree = "<group>"; };
|
||||
F8CC95CD2970761D00C9C2AC /* TintColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TintColor.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
|
@ -448,6 +450,7 @@
|
|||
F88E4D45297E89DF0057491A /* TrendsService.swift */,
|
||||
F88E4D49297EA0490057491A /* RouterPath.swift */,
|
||||
F88E4D59297ECEE60057491A /* SearchService.swift */,
|
||||
F8C7EDBE298169EE002843BC /* TagsService.swift */,
|
||||
);
|
||||
path = Services;
|
||||
sourceTree = "<group>";
|
||||
|
@ -606,6 +609,7 @@
|
|||
F8984E4D296B648000A2610F /* UIImage+Blurhash.swift in Sources */,
|
||||
F897978A2968314A00B22335 /* LoadingIndicator.swift in Sources */,
|
||||
F8210DE52966E160001D9973 /* Color+SystemColors.swift in Sources */,
|
||||
F8C7EDBF298169EE002843BC /* TagsService.swift in Sources */,
|
||||
F88FAD2B295F43B8009B20C9 /* AccountData+CoreDataProperties.swift in Sources */,
|
||||
F85D4975296407F100751DF7 /* HomeTimelineService.swift in Sources */,
|
||||
F80048062961850500E6868A /* StatusData+CoreDataProperties.swift in Sources */,
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
//
|
||||
// https://mczachurski.dev
|
||||
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MastodonKit
|
||||
|
||||
public class TagsService {
|
||||
public static let shared = TagsService()
|
||||
private init() { }
|
||||
|
||||
public func tag(accountData: AccountData?, hashTag: String) async throws -> Tag? {
|
||||
guard let accessToken = accountData?.accessToken, let serverUrl = accountData?.serverUrl else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let client = MastodonClient(baseURL: serverUrl).getAuthenticated(token: accessToken)
|
||||
return try await client.tag(hashtag: hashTag)
|
||||
}
|
||||
|
||||
public func follow(accountData: AccountData?, hashTag: String) async throws -> Tag? {
|
||||
guard let accessToken = accountData?.accessToken, let serverUrl = accountData?.serverUrl else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let client = MastodonClient(baseURL: serverUrl).getAuthenticated(token: accessToken)
|
||||
return try await client.follow(hashtag: hashTag)
|
||||
}
|
||||
|
||||
public func unfollow(accountData: AccountData?, hashTag: String) async throws -> Tag? {
|
||||
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(hashtag: hashTag)
|
||||
}
|
||||
}
|
|
@ -17,7 +17,7 @@ struct SignInView: View {
|
|||
|
||||
var body: some View {
|
||||
VStack {
|
||||
// TODO: Rebild signin.
|
||||
// TODO: Rebuild signin page.
|
||||
HStack {
|
||||
TextField(
|
||||
"Server address",
|
||||
|
|
|
@ -24,6 +24,7 @@ struct StatusesView: View {
|
|||
@State private var allItemsLoaded = false
|
||||
@State private var firstLoadFinished = false
|
||||
|
||||
@State private var tag: Tag?
|
||||
@State private var statusViewModels: [StatusViewModel] = []
|
||||
private let defaultLimit = 20
|
||||
|
||||
|
@ -79,9 +80,17 @@ struct StatusesView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
// TODO: It seems like pixelfed is not supporting the endpoints.
|
||||
// self.getTrailingToolbar()
|
||||
}
|
||||
.task {
|
||||
do {
|
||||
try await self.loadStatuses()
|
||||
|
||||
if case .hashtag(let hashtag) = self.listType {
|
||||
await self.loadTag(hashtag: hashtag)
|
||||
}
|
||||
} catch {
|
||||
ErrorService.shared.handle(error, message: "Loading statuses failed.", showToastr: !Task.isCancelled)
|
||||
}
|
||||
|
@ -205,4 +214,50 @@ struct StatusesView: View {
|
|||
return "#\(tag)"
|
||||
}
|
||||
}
|
||||
|
||||
@ToolbarContentBuilder
|
||||
private func getTrailingToolbar() -> some ToolbarContent {
|
||||
if case .hashtag(let hashtag) = self.listType {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button {
|
||||
Task {
|
||||
if self.tag?.following == true {
|
||||
await self.follow(hashtag: hashtag)
|
||||
} else {
|
||||
await self.unfollow(hashtag: hashtag)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: self.tag?.following == true ? "number.square.fill" : "number.square")
|
||||
.tint(.mainTextColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func loadTag(hashtag: String) async {
|
||||
do {
|
||||
self.tag = try await TagsService.shared.tag(accountData: self.applicationState.accountData, hashTag: hashtag)
|
||||
} catch {
|
||||
ErrorService.shared.handle(error, message: "Error during loading tag from server.", showToastr: false)
|
||||
}
|
||||
}
|
||||
|
||||
private func follow(hashtag: String) async {
|
||||
do {
|
||||
self.tag = try await TagsService.shared.follow(accountData: self.applicationState.accountData, hashTag: hashtag)
|
||||
ToastrService.shared.showSuccess("You are following the tag.", imageSystemName: "number.square.fill")
|
||||
} catch {
|
||||
ErrorService.shared.handle(error, message: "Error during following tag.", showToastr: true)
|
||||
}
|
||||
}
|
||||
|
||||
private func unfollow(hashtag: String) async {
|
||||
do {
|
||||
self.tag = try await TagsService.shared.unfollow(accountData: self.applicationState.accountData, hashTag: hashtag)
|
||||
ToastrService.shared.showSuccess("Tag has been unfollowed.", imageSystemName: "number.square")
|
||||
} catch {
|
||||
ErrorService.shared.handle(error, message: "Error during unfollowing tag.", showToastr: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,20 +9,19 @@ import MastodonKit
|
|||
|
||||
struct CommentBody: View {
|
||||
@EnvironmentObject var applicationState: ApplicationState
|
||||
@EnvironmentObject var routerPath: RouterPath
|
||||
|
||||
@State var statusViewModel: StatusViewModel
|
||||
private let contentWidth = Int(UIScreen.main.bounds.width) - 60
|
||||
|
||||
var body: some View {
|
||||
HStack (alignment: .top) {
|
||||
|
||||
NavigationLink(value: RouteurDestinations.userProfile(
|
||||
accountId: self.statusViewModel.account.id,
|
||||
accountDisplayName: self.statusViewModel.account.displayName,
|
||||
accountUserName: self.statusViewModel.account.acct)
|
||||
) {
|
||||
UserAvatar(accountAvatar: self.statusViewModel.account.avatar, size: .comment)
|
||||
}
|
||||
UserAvatar(accountAvatar: self.statusViewModel.account.avatar, size: .comment)
|
||||
.onTapGesture {
|
||||
routerPath.navigate(to: .userProfile(accountId: self.statusViewModel.account.id,
|
||||
accountDisplayName: self.statusViewModel.account.displayNameWithoutEmojis,
|
||||
accountUserName: self.statusViewModel.account.acct))
|
||||
}
|
||||
|
||||
VStack (alignment: .leading, spacing: 0) {
|
||||
HStack (alignment: .top) {
|
||||
|
@ -71,6 +70,7 @@ struct CommentBody: View {
|
|||
.padding(.bottom, 8)
|
||||
}
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
withAnimation(.linear(duration: 0.3)) {
|
||||
if self.statusViewModel.id == self.applicationState.showInteractionStatusId {
|
||||
|
|
|
@ -34,33 +34,31 @@ struct UserAvatar: View {
|
|||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if let accountAvatar {
|
||||
if let cachedAvatar = CacheImageService.shared.getImage(for: accountAvatar) {
|
||||
cachedAvatar
|
||||
.resizable()
|
||||
.clipShape(applicationState.avatarShape.shape())
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: size.size.width, height: size.size.height)
|
||||
} else {
|
||||
LazyImage(url: accountAvatar) { state in
|
||||
if let image = state.image {
|
||||
image
|
||||
.clipShape(applicationState.avatarShape.shape())
|
||||
.aspectRatio(contentMode: .fit)
|
||||
} else if state.isLoading {
|
||||
placeholderView
|
||||
} else {
|
||||
placeholderView
|
||||
}
|
||||
}
|
||||
.priority(.high)
|
||||
if let accountAvatar {
|
||||
if let cachedAvatar = CacheImageService.shared.getImage(for: accountAvatar) {
|
||||
cachedAvatar
|
||||
.resizable()
|
||||
.clipShape(applicationState.avatarShape.shape())
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: size.size.width, height: size.size.height)
|
||||
}
|
||||
} else {
|
||||
placeholderView
|
||||
.frame(width: size.size.width, height: size.size.height)
|
||||
LazyImage(url: accountAvatar) { state in
|
||||
if let image = state.image {
|
||||
image
|
||||
.clipShape(applicationState.avatarShape.shape())
|
||||
.aspectRatio(contentMode: .fit)
|
||||
} else if state.isLoading {
|
||||
placeholderView
|
||||
} else {
|
||||
placeholderView
|
||||
}
|
||||
}
|
||||
.priority(.high)
|
||||
.frame(width: size.size.width, height: size.size.height)
|
||||
}
|
||||
} else {
|
||||
placeholderView
|
||||
.frame(width: size.size.width, height: size.size.height)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue