Add follow/unfollow tags

This commit is contained in:
Marcin Czachurski 2023-09-26 20:01:58 +02:00
parent 3be590e812
commit 221e128303
13 changed files with 128 additions and 44 deletions

View File

@ -20,5 +20,13 @@ extension Client {
public func unfollow(tag: String) async throws -> Tag? {
return try await pixelfedClient.unfollow(hashtag: tag)
}
public func followed(maxId: MaxId? = nil,
sinceId: SinceId? = nil,
minId: MinId? = nil,
limit: Int? = nil
) async throws -> Linkable<[Tag]> {
return try await pixelfedClient.followedTags(maxId: maxId, sinceId: sinceId, minId: minId, limit: limit)
}
}
}

View File

@ -78,10 +78,12 @@
"trendingStatuses.title.noPhotos" = "Unfortunately, there are no photos here.";
// Mark: Trending tags.
"trendingTags.navigationBar.title" = "Tags";
"trendingTags.title.noTags" = "Unfortunately, there are no tags here.";
"trendingTags.title.amountOfPosts" = "%d posts";
"trendingTags.error.loadingTagsFailed" = "Loading tags failed.";
"tags.navigationBar.trendingTitle" = "Tags";
"tags.navigationBar.searchTitle" = "Tags";
"tags.navigationBar.followedTitle" = "Followed tags";
"tags.title.noTags" = "Unfortunately, there are no tags here.";
"tags.title.amountOfPosts" = "%d posts";
"tags.error.loadingTagsFailed" = "Loading tags failed.";
// Mark: Trending accounts.
"trendingAccounts.navigationBar.title" = "Accounts";
@ -97,6 +99,7 @@
"userProfile.title.block" = "Block";
"userProfile.title.favourites" = "Favourites";
"userProfile.title.bookmarks" = "Bookmarks";
"userProfile.title.followedTags" = "Followed tags";
"userProfile.title.posts" = "Posts";
"userProfile.title.followers" = "Followers";
"userProfile.title.following" = "Following";

View File

@ -78,10 +78,12 @@
"trendingStatuses.title.noPhotos" = "Argazkirik ez.";
// Mark: Trending tags.
"trendingTags.navigationBar.title" = "Traolak";
"trendingTags.title.noTags" = "Traolarik ez.";
"trendingTags.title.amountOfPosts" = "%d bidalketa";
"trendingTags.error.loadingTagsFailed" = "Traolak kargatzeak huts egin du.";
"tags.navigationBar.trendingTitle" = "Traolak";
"tags.navigationBar.searchTitle" = "Traolak";
"tags.navigationBar.followedTitle" = "Traolak";
"tags.title.noTags" = "Traolarik ez.";
"tags.title.amountOfPosts" = "%d bidalketa";
"tags.error.loadingTagsFailed" = "Traolak kargatzeak huts egin du.";
// Mark: Trending accounts.
"trendingAccounts.navigationBar.title" = "Kontuak";
@ -97,6 +99,7 @@
"userProfile.title.block" = "Blokeatu";
"userProfile.title.favourites" = "Gogokoak";
"userProfile.title.bookmarks" = "Laster-markak";
"userProfile.title.followedTags" = "Traolak";
"userProfile.title.posts" = "Bidalketa";
"userProfile.title.followers" = "Jarraitzaile";
"userProfile.title.following" = "Jarraitzen";

View File

@ -78,10 +78,12 @@
"trendingStatuses.title.noPhotos" = "Malheureusement, il n'y a pas de photos ici.";
// Mark: Trending tags.
"trendingTags.navigationBar.title" = "Tags";
"trendingTags.title.noTags" = "Malheureusement, il n'y a pas de tags ici.";
"trendingTags.title.amountOfPosts" = "%d posts";
"trendingTags.error.loadingTagsFailed" = "Chargement des tags échoué.";
"tags.navigationBar.trendingTitle" = "Tags";
"tags.navigationBar.searchTitle" = "Tags";
"tags.navigationBar.followedTitle" = "Tags";
"tags.title.noTags" = "Malheureusement, il n'y a pas de tags ici.";
"tags.title.amountOfPosts" = "%d posts";
"tags.error.loadingTagsFailed" = "Chargement des tags échoué.";
// Mark: Trending accounts.
"trendingAccounts.navigationBar.title" = "Utilisateurs";
@ -97,6 +99,7 @@
"userProfile.title.block" = "Bloquer";
"userProfile.title.favourites" = "Favoris";
"userProfile.title.bookmarks" = "Marque-pages";
"userProfile.title.followedTags" = "Tags";
"userProfile.title.posts" = "Posts";
"userProfile.title.followers" = "Abonnés";
"userProfile.title.following" = "Abonnements";

View File

@ -78,10 +78,12 @@
"trendingStatuses.title.noPhotos" = "Niestety nie ma jeszcze żadnych zdjęć.";
// Mark: Trending tags.
"trendingTags.navigationBar.title" = "Tags";
"trendingTags.title.noTags" = "Niestety nie ma jeszcze żadnych tagów.";
"trendingTags.title.amountOfPosts" = "%d statusów";
"trendingTags.error.loadingTagsFailed" = "Błąd podczas wczytywania tagów.";
"tags.navigationBar.trendingTitle" = "Tagi";
"tags.navigationBar.searchTitle" = "Tagi";
"tags.navigationBar.followedTitle" = "Obserwowane tagi";
"tags.title.noTags" = "Niestety nie ma jeszcze żadnych tagów.";
"tags.title.amountOfPosts" = "%d statusów";
"tags.error.loadingTagsFailed" = "Błąd podczas wczytywania tagów.";
// Mark: Trending accounts.
"trendingAccounts.navigationBar.title" = "Użytkownicy";
@ -97,6 +99,7 @@
"userProfile.title.block" = "Zablokuj";
"userProfile.title.favourites" = "Polubione";
"userProfile.title.bookmarks" = "Zakładki";
"userProfile.title.followedTags" = "Obserwowane tagi";
"userProfile.title.posts" = "Statusy";
"userProfile.title.followers" = "Obserwujący";
"userProfile.title.following" = "Obserwowani";

View File

@ -37,4 +37,16 @@ public extension PixelfedClientAuthenticated {
return try await downloadJson(Tag.self, request: request)
}
func followedTags(maxId: MaxId? = nil,
sinceId: SinceId? = nil,
minId: MinId? = nil,
limit: Int? = nil
) async throws -> Linkable<[Tag]> {
let request = try Self.request(for: baseURL,
target: Pixelfed.Tags.followed(maxId, sinceId, minId, limit),
withBearerToken: token)
return try await downloadJsonWithLink([Tag].self, request: request)
}
}

View File

@ -11,6 +11,7 @@ extension Pixelfed {
case tag(Hashtag)
case follow(Hashtag)
case unfollow(Hashtag)
case followed(MaxId?, SinceId?, MinId?, Limit?)
}
}
@ -26,13 +27,15 @@ extension Pixelfed.Tags: TargetType {
return "\(apiPath)/\(hashtag)/follow"
case .unfollow(let hashtag):
return "\(apiPath)/\(hashtag)/unfollow"
case .followed:
return "/api/v1/followed_tags"
}
}
/// The HTTP method used in the request.
public var method: Method {
switch self {
case .tag:
case .tag, .followed(_, _, _, _):
return .get
case .follow, .unfollow:
return .post
@ -41,7 +44,36 @@ extension Pixelfed.Tags: TargetType {
/// The parameters to be incoded in the request.
public var queryItems: [(String, String)]? {
return nil
var params: [(String, String)] = []
var maxId: MaxId?
var sinceId: SinceId?
var minId: MinId?
var limit: Limit?
switch self {
case .followed(let paramMaxId, let paramSinceId, let paramMinId, let paramLimit):
maxId = paramMaxId
sinceId = paramSinceId
minId = paramMinId
limit = paramLimit
default: break
}
if let maxId {
params.append(("max_id", maxId))
}
if let sinceId {
params.append(("since_id", sinceId))
}
if let minId {
params.append(("min_id", minId))
}
if let limit {
params.append(("limit", "\(limit)"))
}
return params
}
public var headers: [String: String]? {

View File

@ -1321,7 +1321,7 @@
CODE_SIGN_ENTITLEMENTS = VernissageWidget/VernissageWidgetExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 211;
CURRENT_PROJECT_VERSION = 213;
DEVELOPMENT_TEAM = B2U9FEKYP8;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = VernissageWidget/Info.plist;
@ -1333,7 +1333,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.10.0;
MARKETING_VERSION = 1.11.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage.widget;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@ -1352,7 +1352,7 @@
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CODE_SIGN_ENTITLEMENTS = VernissageWidget/VernissageWidgetExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 211;
CURRENT_PROJECT_VERSION = 213;
DEVELOPMENT_TEAM = B2U9FEKYP8;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = VernissageWidget/Info.plist;
@ -1364,7 +1364,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.10.0;
MARKETING_VERSION = 1.11.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage.widget;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@ -1382,7 +1382,7 @@
CODE_SIGN_ENTITLEMENTS = VernissageShare/VernissageShareExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 211;
CURRENT_PROJECT_VERSION = 213;
DEVELOPMENT_TEAM = B2U9FEKYP8;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = VernissageShare/Info.plist;
@ -1394,7 +1394,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.10.0;
MARKETING_VERSION = 1.11.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage.share;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@ -1411,7 +1411,7 @@
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = NO;
CODE_SIGN_ENTITLEMENTS = VernissageShare/VernissageShareExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 211;
CURRENT_PROJECT_VERSION = 213;
DEVELOPMENT_TEAM = B2U9FEKYP8;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = VernissageShare/Info.plist;
@ -1423,7 +1423,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.10.0;
MARKETING_VERSION = 1.11.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage.share;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@ -1565,7 +1565,7 @@
CODE_SIGN_ENTITLEMENTS = Vernissage/Vernissage.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 211;
CURRENT_PROJECT_VERSION = 213;
DEVELOPMENT_ASSET_PATHS = "\"Vernissage/Preview Content\"";
DEVELOPMENT_TEAM = B2U9FEKYP8;
ENABLE_PREVIEWS = YES;
@ -1584,7 +1584,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.10.0;
MARKETING_VERSION = 1.11.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -1608,7 +1608,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = Vernissage/Vernissage.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 211;
CURRENT_PROJECT_VERSION = 213;
DEVELOPMENT_ASSET_PATHS = "\"Vernissage/Preview Content\"";
DEVELOPMENT_TEAM = B2U9FEKYP8;
ENABLE_PREVIEWS = YES;
@ -1627,7 +1627,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.10.0;
MARKETING_VERSION = 1.11.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";

View File

@ -15,14 +15,17 @@ import WidgetsKit
struct HashtagsView: View {
public enum ListType: Hashable {
case trending
case followed
case search(query: String)
public var title: LocalizedStringKey {
switch self {
case .trending:
return "trendingTags.navigationBar.title"
return "tags.navigationBar.trendingTitle"
case .search:
return "trendingTags.navigationBar.title"
return "tags.navigationBar.searchTitle"
case .followed:
return "tags.navigationBar.followedTitle"
}
}
}
@ -51,7 +54,7 @@ struct HashtagsView: View {
}
case .loaded:
if self.tags.isEmpty {
NoDataView(imageSystemName: "person.3.sequence", text: "trendingTags.title.noTags")
NoDataView(imageSystemName: "person.3.sequence", text: "tags.title.noTags")
} else {
self.list()
}
@ -77,7 +80,7 @@ struct HashtagsView: View {
Text(tag.name).font(.headline)
Spacer()
if let total = tag.total {
Text(String(format: NSLocalizedString("trendingTags.title.amountOfPosts", comment: "Amount of posts"), total))
Text(String(format: NSLocalizedString("tags.title.amountOfPosts", comment: "Amount of posts"), total))
.font(.caption)
}
}
@ -87,6 +90,9 @@ struct HashtagsView: View {
}
}
}
.refreshable {
await self.loadData()
}
}
private func loadData() async {
@ -95,10 +101,10 @@ struct HashtagsView: View {
self.state = .loaded
} catch {
if !Task.isCancelled {
ErrorService.shared.handle(error, message: "trendingTags.error.loadingTagsFailed", showToastr: true)
ErrorService.shared.handle(error, message: "tags.error.loadingTagsFailed", showToastr: true)
self.state = .error(error)
} else {
ErrorService.shared.handle(error, message: "trendingTags.error.loadingTagsFailed", showToastr: false)
ErrorService.shared.handle(error, message: "tags.error.loadingTagsFailed", showToastr: false)
}
}
}
@ -111,6 +117,9 @@ struct HashtagsView: View {
case .search(let query):
let results = try await self.client.search?.search(query: query, resultsType: .hashtags, limit: 40)
return results?.hashtags.map({ tag in HashtagModel(tag: tag) }) ?? []
case .followed:
let tagsFromApi = try await self.client.tags?.followed(limit: 200)
return tagsFromApi?.data.map({ tag in HashtagModel(tag: tag) }) ?? []
}
}
}

View File

@ -65,8 +65,7 @@ struct StatusesView: View {
self.mainBody()
.navigationTitle(self.listType.title)
.toolbar {
// TODO: It seems like pixelfed is not supporting the endpoints.
// self.getTrailingToolbar()
self.getTrailingToolbar()
}
}
@ -292,15 +291,16 @@ struct StatusesView: View {
Button {
Task {
if self.tag?.following == true {
await self.follow(hashtag: hashtag)
} else {
await self.unfollow(hashtag: hashtag)
} else {
await self.follow(hashtag: hashtag)
}
}
} label: {
Image(systemName: self.tag?.following == true ? "number.square.fill" : "number.square")
.tint(.mainTextColor)
}
.disabled(self.tag == nil)
}
}
}

View File

@ -215,6 +215,10 @@ struct UserProfileView: View {
NavigationLink(value: RouteurDestinations.bookmarks) {
Label(NSLocalizedString("userProfile.title.bookmarks", comment: "Bookmarks"), systemImage: "bookmark")
}
NavigationLink(value: RouteurDestinations.hashtags(listType: .followed)) {
Label(NSLocalizedString("userProfile.title.followedTags", comment: "Followed tags"), systemImage: "number.square")
}
Divider()

View File

@ -163,9 +163,16 @@ struct ImageRowItemAsync: View {
private func imageView(image: Image) -> some View {
image
.resizable()
.aspectRatio(contentMode: self.clipToRectangle ? .fill : .fit)
.if(self.clipToRectangle) {
$0.frame(width: self.containerWidth, height: self.containerWidth).clipped()
.if(self.clipToRectangle == true) {
$0
.aspectRatio(contentMode: .fill)
.frame(width: self.containerWidth, height: self.containerWidth)
.clipped()
// Fix issue with clickable content area outside of the visible image: https://developer.apple.com/forums/thread/123717.
.contentShape(Rectangle())
}
.if(self.clipToRectangle == false) {
$0.aspectRatio(contentMode: .fit)
}
.onTapGesture(count: 2) {
Task {

View File

@ -20,7 +20,7 @@ struct ImagesGrid: View {
@EnvironmentObject var routerPath: RouterPath
private let maxImages = 5
private let maxHeight = 120.0
private let maxHeight = UIDevice.isIPad ? 240.0 : 120.0
@State public var gridType: GridType