mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-11-26 10:11:00 +00:00
Explore: Added suggested accounts to follow
This commit is contained in:
parent
c598a4ab1d
commit
6e8ed998d4
8 changed files with 233 additions and 72 deletions
|
@ -24,12 +24,10 @@ struct IceCubesApp: App {
|
||||||
.tabItem {
|
.tabItem {
|
||||||
Label("Notifications", systemImage: "bell")
|
Label("Notifications", systemImage: "bell")
|
||||||
}
|
}
|
||||||
}
|
ExploreTab()
|
||||||
ExploreTab()
|
.tabItem {
|
||||||
.tabItem {
|
Label("Explore", systemImage: "magnifyingglass")
|
||||||
Label("Explore", systemImage: "magnifyingglass")
|
}
|
||||||
}
|
|
||||||
if appAccountsManager.currentClient.isAuth {
|
|
||||||
AccountTab()
|
AccountTab()
|
||||||
.tabItem {
|
.tabItem {
|
||||||
Label("Profile", systemImage: "person.circle")
|
Label("Profile", systemImage: "person.circle")
|
||||||
|
|
|
@ -112,6 +112,9 @@ struct AccountDetailHeaderView: View {
|
||||||
Text(account.note.asSafeAttributedString)
|
Text(account.note.asSafeAttributedString)
|
||||||
.font(.body)
|
.font(.body)
|
||||||
.padding(.top, 8)
|
.padding(.top, 8)
|
||||||
|
.environment(\.openURL, OpenURLAction { url in
|
||||||
|
routeurPath.handle(url: url)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
.padding(.horizontal, DS.Constants.layoutPadding)
|
.padding(.horizontal, DS.Constants.layoutPadding)
|
||||||
.offset(y: -40)
|
.offset(y: -40)
|
||||||
|
|
|
@ -80,7 +80,7 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
|
||||||
do {
|
do {
|
||||||
async let account: Account = client.get(endpoint: Accounts.accounts(id: accountId))
|
async let account: Account = client.get(endpoint: Accounts.accounts(id: accountId))
|
||||||
async let followedTags: [Tag] = client.get(endpoint: Accounts.followedTags)
|
async let followedTags: [Tag] = client.get(endpoint: Accounts.followedTags)
|
||||||
async let relationships: [Relationshionship] = client.get(endpoint: Accounts.relationships(id: accountId))
|
async let relationships: [Relationshionship] = client.get(endpoint: Accounts.relationships(ids: [accountId]))
|
||||||
async let featuredTags: [FeaturedTag] = client.get(endpoint: Accounts.featuredTags(id: accountId))
|
async let featuredTags: [FeaturedTag] = client.get(endpoint: Accounts.featuredTags(id: accountId))
|
||||||
async let familliarFollowers: [FamilliarAccounts] = client.get(endpoint: Accounts.familiarFollowers(withAccount: accountId))
|
async let familliarFollowers: [FamilliarAccounts] = client.get(endpoint: Accounts.familiarFollowers(withAccount: accountId))
|
||||||
let loadedAccount = try await account
|
let loadedAccount = try await account
|
||||||
|
|
|
@ -41,4 +41,13 @@ public class RouterPath: ObservableObject {
|
||||||
}
|
}
|
||||||
return .systemAction
|
return .systemAction
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func handle(url: URL) -> OpenURLAction.Result {
|
||||||
|
if url.pathComponents.contains(where: { $0 == "tags" }),
|
||||||
|
let tag = url.pathComponents.last {
|
||||||
|
navigate(to: .hashTag(tag: tag, account: nil))
|
||||||
|
return .handled
|
||||||
|
}
|
||||||
|
return .systemAction
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import Network
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
import Models
|
import Models
|
||||||
import Status
|
import Status
|
||||||
|
import Shimmer
|
||||||
|
|
||||||
public struct ExploreView: View {
|
public struct ExploreView: View {
|
||||||
@EnvironmentObject private var client: Client
|
@EnvironmentObject private var client: Client
|
||||||
|
@ -16,80 +17,132 @@ public struct ExploreView: View {
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
List {
|
List {
|
||||||
Section("Trending Tags") {
|
if !viewModel.isLoaded {
|
||||||
ForEach(viewModel.trendingTags
|
ForEach(Status.placeholders()) { status in
|
||||||
.prefix(upTo: viewModel.trendingTags.count > 5 ? 5 : viewModel.trendingTags.count)) { tag in
|
|
||||||
TagRowView(tag: tag)
|
|
||||||
.padding(.vertical, 4)
|
|
||||||
}
|
|
||||||
NavigationLink {
|
|
||||||
List {
|
|
||||||
ForEach(viewModel.trendingTags) { tag in
|
|
||||||
TagRowView(tag: tag)
|
|
||||||
.padding(.vertical, 4)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.listStyle(.plain)
|
|
||||||
.navigationTitle("Trending Tags")
|
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
|
||||||
} label: {
|
|
||||||
Text("See more")
|
|
||||||
.foregroundColor(.brand)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Section("Trending Posts") {
|
|
||||||
ForEach(viewModel.trendingStatuses
|
|
||||||
.prefix(upTo: viewModel.trendingStatuses.count > 3 ? 3 : viewModel.trendingStatuses.count)) { status in
|
|
||||||
StatusRowView(viewModel: .init(status: status, isEmbed: false))
|
StatusRowView(viewModel: .init(status: status, isEmbed: false))
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
|
.redacted(reason: .placeholder)
|
||||||
|
.shimmering()
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
NavigationLink {
|
trendingTagsSection
|
||||||
List {
|
suggestedAccountsSection
|
||||||
ForEach(viewModel.trendingStatuses) { status in
|
trendingPostsSection
|
||||||
StatusRowView(viewModel: .init(status: status, isEmbed: false))
|
trendingLinksSection
|
||||||
.padding(.vertical, 8)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.listStyle(.plain)
|
|
||||||
.navigationTitle("Trending Posts")
|
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
|
||||||
} label: {
|
|
||||||
Text("See more")
|
|
||||||
.foregroundColor(.brand)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Section("Trending Links") {
|
|
||||||
ForEach(viewModel.trendingLinks
|
|
||||||
.prefix(upTo: viewModel.trendingLinks.count > 3 ? 3 : viewModel.trendingLinks.count)) { card in
|
|
||||||
StatusCardView(card: card)
|
|
||||||
.padding(.vertical, 8)
|
|
||||||
}
|
|
||||||
NavigationLink {
|
|
||||||
List {
|
|
||||||
ForEach(viewModel.trendingLinks) { card in
|
|
||||||
StatusCardView(card: card)
|
|
||||||
.padding(.vertical, 8)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.listStyle(.plain)
|
|
||||||
.navigationTitle("Trending Links")
|
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
|
||||||
} label: {
|
|
||||||
Text("See more")
|
|
||||||
.foregroundColor(.brand)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.task {
|
.task {
|
||||||
viewModel.client = client
|
viewModel.client = client
|
||||||
|
guard !viewModel.isLoaded else { return }
|
||||||
await viewModel.fetchTrending()
|
await viewModel.fetchTrending()
|
||||||
}
|
}
|
||||||
|
.refreshable {
|
||||||
|
Task {
|
||||||
|
await viewModel.fetchTrending()
|
||||||
|
}
|
||||||
|
}
|
||||||
.listStyle(.grouped)
|
.listStyle(.grouped)
|
||||||
.navigationTitle("Explore")
|
.navigationTitle("Explore")
|
||||||
.searchable(text: $searchQuery)
|
.searchable(text: $searchQuery)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var suggestedAccountsSection: some View {
|
||||||
|
Section("Suggested Users") {
|
||||||
|
ForEach(viewModel.suggestedAccounts
|
||||||
|
.prefix(upTo: viewModel.suggestedAccounts.count > 3 ? 3 : viewModel.suggestedAccounts.count)) { account in
|
||||||
|
if let relationship = viewModel.suggestedAccountsRelationShips.first(where: { $0.id == account.id }) {
|
||||||
|
SuggestedAccountRow(viewModel: .init(account: account, relationShip: relationship))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NavigationLink {
|
||||||
|
List {
|
||||||
|
ForEach(viewModel.suggestedAccounts) { account in
|
||||||
|
if let relationship = viewModel.suggestedAccountsRelationShips.first(where: { $0.id == account.id }) {
|
||||||
|
SuggestedAccountRow(viewModel: .init(account: account, relationShip: relationship))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.listStyle(.plain)
|
||||||
|
.navigationTitle("Suggested Users")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
} label: {
|
||||||
|
Text("See more")
|
||||||
|
.foregroundColor(.brand)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var trendingTagsSection: some View {
|
||||||
|
Section("Trending Tags") {
|
||||||
|
ForEach(viewModel.trendingTags
|
||||||
|
.prefix(upTo: viewModel.trendingTags.count > 5 ? 5 : viewModel.trendingTags.count)) { tag in
|
||||||
|
TagRowView(tag: tag)
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
}
|
||||||
|
NavigationLink {
|
||||||
|
List {
|
||||||
|
ForEach(viewModel.trendingTags) { tag in
|
||||||
|
TagRowView(tag: tag)
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.listStyle(.plain)
|
||||||
|
.navigationTitle("Trending Tags")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
} label: {
|
||||||
|
Text("See more")
|
||||||
|
.foregroundColor(.brand)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var trendingPostsSection: some View {
|
||||||
|
Section("Trending Posts") {
|
||||||
|
ForEach(viewModel.trendingStatuses
|
||||||
|
.prefix(upTo: viewModel.trendingStatuses.count > 3 ? 3 : viewModel.trendingStatuses.count)) { status in
|
||||||
|
StatusRowView(viewModel: .init(status: status, isEmbed: false))
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
NavigationLink {
|
||||||
|
List {
|
||||||
|
ForEach(viewModel.trendingStatuses) { status in
|
||||||
|
StatusRowView(viewModel: .init(status: status, isEmbed: false))
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.listStyle(.plain)
|
||||||
|
.navigationTitle("Trending Posts")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
} label: {
|
||||||
|
Text("See more")
|
||||||
|
.foregroundColor(.brand)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var trendingLinksSection: some View {
|
||||||
|
Section("Trending Links") {
|
||||||
|
ForEach(viewModel.trendingLinks
|
||||||
|
.prefix(upTo: viewModel.trendingLinks.count > 3 ? 3 : viewModel.trendingLinks.count)) { card in
|
||||||
|
StatusCardView(card: card)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
}
|
||||||
|
NavigationLink {
|
||||||
|
List {
|
||||||
|
ForEach(viewModel.trendingLinks) { card in
|
||||||
|
StatusCardView(card: card)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.listStyle(.plain)
|
||||||
|
.navigationTitle("Trending Links")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
} label: {
|
||||||
|
Text("See more")
|
||||||
|
.foregroundColor(.brand)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,9 @@ import Network
|
||||||
class ExploreViewModel: ObservableObject {
|
class ExploreViewModel: ObservableObject {
|
||||||
var client: Client?
|
var client: Client?
|
||||||
|
|
||||||
|
@Published var isLoaded = false
|
||||||
|
@Published var suggestedAccounts: [Account] = []
|
||||||
|
@Published var suggestedAccountsRelationShips: [Relationshionship] = []
|
||||||
@Published var trendingTags: [Tag] = []
|
@Published var trendingTags: [Tag] = []
|
||||||
@Published var trendingStatuses: [Status] = []
|
@Published var trendingStatuses: [Status] = []
|
||||||
@Published var trendingLinks: [Card] = []
|
@Published var trendingLinks: [Card] = []
|
||||||
|
@ -13,13 +16,20 @@ class ExploreViewModel: ObservableObject {
|
||||||
func fetchTrending() async {
|
func fetchTrending() async {
|
||||||
guard let client else { return }
|
guard let client else { return }
|
||||||
do {
|
do {
|
||||||
|
isLoaded = false
|
||||||
|
async let suggestedAccounts: [Account] = client.get(endpoint: Accounts.suggestions)
|
||||||
async let trendingTags: [Tag] = client.get(endpoint: Trends.tags)
|
async let trendingTags: [Tag] = client.get(endpoint: Trends.tags)
|
||||||
async let trendingStatuses: [Status] = client.get(endpoint: Trends.statuses)
|
async let trendingStatuses: [Status] = client.get(endpoint: Trends.statuses)
|
||||||
async let trendingLinks: [Card] = client.get(endpoint: Trends.links)
|
async let trendingLinks: [Card] = client.get(endpoint: Trends.links)
|
||||||
|
|
||||||
|
self.suggestedAccounts = try await suggestedAccounts
|
||||||
self.trendingTags = try await trendingTags
|
self.trendingTags = try await trendingTags
|
||||||
self.trendingStatuses = try await trendingStatuses
|
self.trendingStatuses = try await trendingStatuses
|
||||||
self.trendingLinks = try await trendingLinks
|
self.trendingLinks = try await trendingLinks
|
||||||
|
|
||||||
|
self.suggestedAccountsRelationShips = try await client.get(endpoint: Accounts.relationships(ids: self.suggestedAccounts.map{ $0.id }))
|
||||||
|
|
||||||
|
isLoaded = true
|
||||||
} catch { }
|
} catch { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
83
Packages/Explore/Sources/Explore/SuggestedAccountRow.swift
Normal file
83
Packages/Explore/Sources/Explore/SuggestedAccountRow.swift
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
import SwiftUI
|
||||||
|
import Models
|
||||||
|
import Network
|
||||||
|
import DesignSystem
|
||||||
|
import Env
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
class SuggestedAccountViewModel: ObservableObject {
|
||||||
|
var client: Client?
|
||||||
|
|
||||||
|
@Published var account: Account
|
||||||
|
@Published var relationShip: Relationshionship
|
||||||
|
|
||||||
|
init(account: Account, relationShip: Relationshionship) {
|
||||||
|
self.account = account
|
||||||
|
self.relationShip = relationShip
|
||||||
|
}
|
||||||
|
|
||||||
|
func follow() async {
|
||||||
|
guard let client else { return }
|
||||||
|
do {
|
||||||
|
self.relationShip = try await client.post(endpoint: Accounts.follow(id: account.id))
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
func unfollow() async {
|
||||||
|
guard let client else { return }
|
||||||
|
do {
|
||||||
|
self.relationShip = try await client.post(endpoint: Accounts.unfollow(id: account.id))
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SuggestedAccountRow: View {
|
||||||
|
@EnvironmentObject private var routeurPath: RouterPath
|
||||||
|
@EnvironmentObject private var client: Client
|
||||||
|
|
||||||
|
@StateObject var viewModel: SuggestedAccountViewModel
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(alignment: .top) {
|
||||||
|
AvatarView(url: viewModel.account.avatar, size: .status)
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
viewModel.account.displayNameWithEmojis
|
||||||
|
.font(.subheadline)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
Text("@\(viewModel.account.acct)")
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
Text(viewModel.account.note.asSafeAttributedString)
|
||||||
|
.font(.callout)
|
||||||
|
.environment(\.openURL, OpenURLAction { url in
|
||||||
|
routeurPath.handle(url: url)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
Button {
|
||||||
|
Task {
|
||||||
|
if viewModel.relationShip.following {
|
||||||
|
await viewModel.unfollow()
|
||||||
|
} else {
|
||||||
|
await viewModel.follow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
if viewModel.relationShip.requested {
|
||||||
|
Text("Requested")
|
||||||
|
.font(.callout)
|
||||||
|
} else {
|
||||||
|
Text(viewModel.relationShip.following ? "Unfollow" : "Follow")
|
||||||
|
.font(.callout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
viewModel.client = client
|
||||||
|
}
|
||||||
|
.onTapGesture {
|
||||||
|
routeurPath.navigate(to: .accountDetailWithAccount(account: viewModel.account))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,10 +7,11 @@ public enum Accounts: Endpoint {
|
||||||
case featuredTags(id: String)
|
case featuredTags(id: String)
|
||||||
case verifyCredentials
|
case verifyCredentials
|
||||||
case statuses(id: String, sinceId: String?, tag: String?)
|
case statuses(id: String, sinceId: String?, tag: String?)
|
||||||
case relationships(id: String)
|
case relationships(ids: [String])
|
||||||
case follow(id: String)
|
case follow(id: String)
|
||||||
case unfollow(id: String)
|
case unfollow(id: String)
|
||||||
case familiarFollowers(withAccount: String)
|
case familiarFollowers(withAccount: String)
|
||||||
|
case suggestions
|
||||||
|
|
||||||
public func path() -> String {
|
public func path() -> String {
|
||||||
switch self {
|
switch self {
|
||||||
|
@ -34,6 +35,8 @@ public enum Accounts: Endpoint {
|
||||||
return "accounts/\(id)/unfollow"
|
return "accounts/\(id)/unfollow"
|
||||||
case .familiarFollowers:
|
case .familiarFollowers:
|
||||||
return "accounts/familiar_followers"
|
return "accounts/familiar_followers"
|
||||||
|
case .suggestions:
|
||||||
|
return "suggestions"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,8 +51,10 @@ public enum Accounts: Endpoint {
|
||||||
params.append(.init(name: "max_id", value: sinceId))
|
params.append(.init(name: "max_id", value: sinceId))
|
||||||
}
|
}
|
||||||
return params
|
return params
|
||||||
case let .relationships(id):
|
case let .relationships(ids):
|
||||||
return [.init(name: "id", value: id)]
|
return ids.map {
|
||||||
|
URLQueryItem(name: "id[]", value: $0)
|
||||||
|
}
|
||||||
case let .familiarFollowers(withAccount):
|
case let .familiarFollowers(withAccount):
|
||||||
return [.init(name: "id[]", value: withAccount)]
|
return [.init(name: "id[]", value: withAccount)]
|
||||||
default:
|
default:
|
||||||
|
|
Loading…
Reference in a new issue