Post boosted by / favourited by

This commit is contained in:
Thomas Ricouard 2022-12-24 13:41:25 +01:00
parent 569aedeaeb
commit 70ee6e0d27
14 changed files with 143 additions and 68 deletions

View file

@ -18,9 +18,13 @@ extension View {
case let .hashTag(tag, accountId): case let .hashTag(tag, accountId):
TimelineView(timeline: .hashtag(tag: tag, accountId: accountId)) TimelineView(timeline: .hashtag(tag: tag, accountId: accountId))
case let .following(id): case let .following(id):
AccountsListView(accountId: id, mode: .following) AccountsListView(mode: .followers(accountId: id))
case let .followers(id): case let .followers(id):
AccountsListView(accountId: id, mode: .followers) AccountsListView(mode: .followers(accountId: id))
case let .favouritedBy(id):
AccountsListView(mode: .favouritedBy(statusId: id))
case let .rebloggedBy(id):
AccountsListView(mode: .rebloggedBy(statusId: id))
} }
} }
} }

View file

@ -96,7 +96,7 @@ struct AccountDetailHeaderView: View {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
account.displayNameWithEmojis account.displayNameWithEmojis
.font(.headline) .font(.headline)
Text(account.acct) Text("@\(account.acct)")
.font(.callout) .font(.callout)
.foregroundColor(.gray) .foregroundColor(.gray)
} }

View file

@ -38,7 +38,8 @@ public struct AccountsListRow: View {
.font(.footnote) .font(.footnote)
.foregroundColor(.gray) .foregroundColor(.gray)
Text(viewModel.account.note.asSafeAttributedString) Text(viewModel.account.note.asSafeAttributedString)
.font(.callout) .font(.footnote)
.lineLimit(3)
.environment(\.openURL, OpenURLAction { url in .environment(\.openURL, OpenURLAction { url in
routeurPath.handle(url: url) routeurPath.handle(url: url)
}) })

View file

@ -9,8 +9,8 @@ public struct AccountsListView: View {
@StateObject private var viewModel: AccountsListViewModel @StateObject private var viewModel: AccountsListViewModel
@State private var didAppear: Bool = false @State private var didAppear: Bool = false
public init(accountId: String, mode: AccountsListMode) { public init(mode: AccountsListMode) {
_viewModel = StateObject(wrappedValue: .init(accountId: accountId, mode: mode)) _viewModel = StateObject(wrappedValue: .init(mode: mode))
} }
public var body: some View { public var body: some View {
@ -50,7 +50,7 @@ public struct AccountsListView: View {
} }
} }
.listStyle(.plain) .listStyle(.plain)
.navigationTitle(viewModel.mode.rawValue.capitalized) .navigationTitle(viewModel.mode.title)
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.task { .task {
viewModel.client = client viewModel.client = client

View file

@ -2,15 +2,28 @@ import SwiftUI
import Models import Models
import Network import Network
public enum AccountsListMode: String { public enum AccountsListMode {
case following, followers case following(accountId: String), followers(accountId: String)
case favouritedBy(statusId: String), rebloggedBy(statusId: String)
var title: String {
switch self {
case .following:
return "Following"
case .followers:
return "Followers"
case .favouritedBy:
return "Favourited by"
case .rebloggedBy:
return "Boosted by"
}
}
} }
@MainActor @MainActor
class AccountsListViewModel: ObservableObject { class AccountsListViewModel: ObservableObject {
var client: Client? var client: Client?
let accountId: String
let mode: AccountsListMode let mode: AccountsListMode
public enum State { public enum State {
@ -31,8 +44,7 @@ class AccountsListViewModel: ObservableObject {
private var nextPageId: String? private var nextPageId: String?
init(accountId: String, mode: AccountsListMode) { init(mode: AccountsListMode) {
self.accountId = accountId
self.mode = mode self.mode = mode
} }
@ -42,12 +54,18 @@ class AccountsListViewModel: ObservableObject {
state = .loading state = .loading
let link: LinkHandler? let link: LinkHandler?
switch mode { switch mode {
case .followers: case let .followers(accountId):
(accounts, link) = try await client.getWithLink(endpoint: Accounts.followers(id: accountId, (accounts, link) = try await client.getWithLink(endpoint: Accounts.followers(id: accountId,
sinceId: nil)) maxId: nil))
case .following: case let .following(accountId):
(accounts, link) = try await client.getWithLink(endpoint: Accounts.following(id: accountId, (accounts, link) = try await client.getWithLink(endpoint: Accounts.following(id: accountId,
sinceId: nil)) maxId: nil))
case let .rebloggedBy(statusId):
(accounts, link) = try await client.getWithLink(endpoint: Statuses.rebloggedBy(id: statusId,
maxId: nil))
case let .favouritedBy(statusId):
(accounts, link) = try await client.getWithLink(endpoint: Statuses.favouritedBy(id: statusId,
maxId: nil))
} }
nextPageId = link?.maxId nextPageId = link?.maxId
relationships = try await client.get(endpoint: relationships = try await client.get(endpoint:
@ -65,12 +83,18 @@ class AccountsListViewModel: ObservableObject {
let newAccounts: [Account] let newAccounts: [Account]
let link: LinkHandler? let link: LinkHandler?
switch mode { switch mode {
case .followers: case let .followers(accountId):
(newAccounts, link) = try await client.getWithLink(endpoint: Accounts.followers(id: accountId, (newAccounts, link) = try await client.getWithLink(endpoint: Accounts.followers(id: accountId,
sinceId: nextPageId)) maxId: nextPageId))
case .following: case let .following(accountId):
(newAccounts, link) = try await client.getWithLink(endpoint: Accounts.following(id: accountId, (newAccounts, link) = try await client.getWithLink(endpoint: Accounts.following(id: accountId,
sinceId: nextPageId)) maxId: nextPageId))
case let .rebloggedBy(statusId):
(newAccounts, link) = try await client.getWithLink(endpoint: Statuses.rebloggedBy(id: statusId,
maxId: nextPageId))
case let .favouritedBy(statusId):
(newAccounts, link) = try await client.getWithLink(endpoint: Statuses.favouritedBy(id: statusId,
maxId: nextPageId))
} }
accounts.append(contentsOf: newAccounts) accounts.append(contentsOf: newAccounts)
let newRelationships: [Relationshionship] = let newRelationships: [Relationshionship] =

View file

@ -9,6 +9,8 @@ public enum RouteurDestinations: Hashable {
case hashTag(tag: String, account: String?) case hashTag(tag: String, account: String?)
case followers(id: String) case followers(id: String)
case following(id: String) case following(id: String)
case favouritedBy(id: String)
case rebloggedBy(id: String)
} }
public enum SheetDestinations: Identifiable { public enum SheetDestinations: Identifiable {

View file

@ -12,8 +12,8 @@ public enum Accounts: Endpoint {
case unfollow(id: String) case unfollow(id: String)
case familiarFollowers(withAccount: String) case familiarFollowers(withAccount: String)
case suggestions case suggestions
case followers(id: String, sinceId: String?) case followers(id: String, maxId: String?)
case following(id: String, sinceId: String?) case following(id: String, maxId: String?)
public func path() -> String { public func path() -> String {
switch self { switch self {
@ -63,12 +63,10 @@ public enum Accounts: Endpoint {
} }
case let .familiarFollowers(withAccount): case let .familiarFollowers(withAccount):
return [.init(name: "id[]", value: withAccount)] return [.init(name: "id[]", value: withAccount)]
case let .followers(_, sinceId): case let .followers(_, maxId):
guard let sinceId else { return nil } return makePaginationParam(sinceId: nil, maxId: maxId)
return [.init(name: "max_id", value: sinceId)] case let .following(_, maxId):
case let .following(_, sinceId): return makePaginationParam(sinceId: nil, maxId: maxId)
guard let sinceId else { return nil }
return [.init(name: "max_id", value: sinceId)]
case let .favourites(sinceId): case let .favourites(sinceId):
guard let sinceId else { return nil } guard let sinceId else { return nil }
return [.init(name: "max_id", value: sinceId)] return [.init(name: "max_id", value: sinceId)]

View file

@ -7,6 +7,8 @@ public enum Statuses: Endpoint {
case unfavourite(id: String) case unfavourite(id: String)
case reblog(id: String) case reblog(id: String)
case unreblog(id: String) case unreblog(id: String)
case rebloggedBy(id: String, maxId: String?)
case favouritedBy(id: String, maxId: String?)
public func path() -> String { public func path() -> String {
switch self { switch self {
@ -22,11 +24,19 @@ public enum Statuses: Endpoint {
return "statuses/\(id)/reblog" return "statuses/\(id)/reblog"
case .unreblog(let id): case .unreblog(let id):
return "statuses/\(id)/unreblog" return "statuses/\(id)/unreblog"
case .rebloggedBy(let id, _):
return "statuses/\(id)/reblogged_by"
case .favouritedBy(let id, _):
return "statuses/\(id)/favourited_by"
} }
} }
public func queryItems() -> [URLQueryItem]? { public func queryItems() -> [URLQueryItem]? {
switch self { switch self {
case let .rebloggedBy(_, maxId):
return makePaginationParam(sinceId: nil, maxId: maxId)
case let .favouritedBy(_, maxId):
return makePaginationParam(sinceId: nil, maxId: maxId)
default: default:
return nil return nil
} }

View file

@ -14,7 +14,7 @@ struct NotificationRowView: View {
if let type = notification.supportedType { if let type = notification.supportedType {
HStack(alignment: .top, spacing: 8) { HStack(alignment: .top, spacing: 8) {
makeAvatarView(type: type) makeAvatarView(type: type)
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 2) {
makeMainLabel(type: type) makeMainLabel(type: type)
makeContent(type: type) makeContent(type: type)
} }
@ -77,14 +77,14 @@ struct NotificationRowView: View {
) )
.padding(.top, 8) .padding(.top, 8)
} else { } else {
Text(notification.account.acct) Text("@\(notification.account.acct)")
.font(.callout) .font(.callout)
.foregroundColor(.gray) .foregroundColor(.gray)
if type == .follow { if type == .follow {
Text(notification.account.note.asSafeAttributedString) Text(notification.account.note.asSafeAttributedString)
.lineLimit(3) .lineLimit(3)
.font(.body) .font(.callout)
.foregroundColor(.gray) .foregroundColor(.gray)
.environment(\.openURL, OpenURLAction { url in .environment(\.openURL, OpenURLAction { url in
routeurPath.handle(url: url) routeurPath.handle(url: url)

View file

@ -34,11 +34,12 @@ public struct StatusDetailView: View {
.padding(.vertical, DS.Constants.dividerPadding) .padding(.vertical, DS.Constants.dividerPadding)
} }
} }
StatusRowView(viewModel: .init(status: status, isEmbed: false)) StatusRowView(viewModel: .init(status: status,
isEmbed: false,
isFocused: true))
.id(status.id) .id(status.id)
makeStatusInfoDetailView(status: status)
Divider() Divider()
.padding(.vertical, DS.Constants.dividerPadding * 2) .padding(.bottom, DS.Constants.dividerPadding * 2)
if !context.descendants.isEmpty { if !context.descendants.isEmpty {
ForEach(context.descendants) { descendant in ForEach(context.descendants) { descendant in
StatusRowView(viewModel: .init(status: descendant, isEmbed: false)) StatusRowView(viewModel: .init(status: descendant, isEmbed: false))
@ -66,15 +67,4 @@ public struct StatusDetailView: View {
.navigationTitle(viewModel.title) .navigationTitle(viewModel.title)
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
} }
@ViewBuilder
private func makeStatusInfoDetailView(status: Status) -> some View {
HStack {
Text(status.createdAt.asDate, style: .date)
Text(status.createdAt.asDate, style: .time)
Spacer()
Text(status.application?.name ?? "")
}
.font(.caption)
}
} }

View file

@ -6,7 +6,7 @@ import Network
struct StatusActionsView: View { struct StatusActionsView: View {
@EnvironmentObject private var routeurPath: RouterPath @EnvironmentObject private var routeurPath: RouterPath
@ObservedObject var viewModel: StatusRowViewModel @ObservedObject var viewModel: StatusRowViewModel
@MainActor @MainActor
enum Actions: CaseIterable { enum Actions: CaseIterable {
case respond, boost, favourite, share case respond, boost, favourite, share
@ -39,32 +39,73 @@ struct StatusActionsView: View {
} }
var body: some View { var body: some View {
HStack { VStack(spacing: 12) {
ForEach(Actions.allCases, id: \.self) { action in HStack {
if action == .share { ForEach(Actions.allCases, id: \.self) { action in
if let url = viewModel.status.reblog?.url ?? viewModel.status.url { if action == .share {
ShareLink(item: url) { if let url = viewModel.status.reblog?.url ?? viewModel.status.url {
Image(systemName: action.iconName(viewModel: viewModel)) ShareLink(item: url) {
} Image(systemName: action.iconName(viewModel: viewModel))
}
} else {
Button {
handleAction(action: action)
} label: {
HStack(spacing: 2) {
Image(systemName: action.iconName(viewModel: viewModel))
if let count = action.count(viewModel: viewModel) {
Text("\(count)")
.font(.footnote)
} }
} }
} else {
Button {
handleAction(action: action)
} label: {
HStack(spacing: 2) {
Image(systemName: action.iconName(viewModel: viewModel))
if let count = action.count(viewModel: viewModel) {
Text("\(count)")
.font(.footnote)
}
}
}
.buttonStyle(.borderless)
Spacer()
} }
.buttonStyle(.borderless)
Spacer()
} }
} }
if viewModel.isFocused {
summaryView
}
}
}
@ViewBuilder
private var summaryView: some View {
HStack {
Text(viewModel.status.createdAt.asDate, style: .date)
Text(viewModel.status.createdAt.asDate, style: .time)
Spacer()
Text(viewModel.status.application?.name ?? "")
}
.font(.caption)
if viewModel.favouritesCount > 0 {
Divider()
Button {
routeurPath.navigate(to: .favouritedBy(id: viewModel.status.id))
} label: {
HStack {
Text("\(viewModel.favouritesCount) favorites")
Spacer()
Image(systemName: "chevron.right")
}
.font(.callout)
}
}
if viewModel.reblogsCount > 0 {
Divider()
Button {
routeurPath.navigate(to: .rebloggedBy(id: viewModel.status.id))
} label: {
HStack {
Text("\(viewModel.reblogsCount) boosts")
Spacer()
Image(systemName: "chevron.right")
}
.font(.callout)
}
} }
.tint(.gray)
} }
private func handleAction(action: Actions) { private func handleAction(action: Actions) {

View file

@ -24,6 +24,7 @@ public struct StatusRowView: View {
if !viewModel.isEmbed { if !viewModel.isEmbed {
StatusActionsView(viewModel: viewModel) StatusActionsView(viewModel: viewModel)
.padding(.vertical, 8) .padding(.vertical, 8)
.tint(viewModel.isFocused ? .brand : .gray)
} }
} }
.onAppear { .onAppear {

View file

@ -6,6 +6,7 @@ import Network
public class StatusRowViewModel: ObservableObject { public class StatusRowViewModel: ObservableObject {
let status: Status let status: Status
let isEmbed: Bool let isEmbed: Bool
let isFocused: Bool
@Published var favouritesCount: Int @Published var favouritesCount: Int
@Published var isFavourited: Bool @Published var isFavourited: Bool
@ -15,9 +16,12 @@ public class StatusRowViewModel: ObservableObject {
var client: Client? var client: Client?
public init(status: Status, isEmbed: Bool) { public init(status: Status,
isEmbed: Bool = false,
isFocused: Bool = false) {
self.status = status self.status = status
self.isEmbed = isEmbed self.isEmbed = isEmbed
self.isFocused = isFocused
if let reblog = status.reblog { if let reblog = status.reblog {
self.isFavourited = reblog.favourited == true self.isFavourited = reblog.favourited == true
self.isReblogged = reblog.reblogged == true self.isReblogged = reblog.reblogged == true

View file

@ -53,7 +53,7 @@ public struct TimelineView: View {
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
Text("#\(tag.name)") Text("#\(tag.name)")
.font(.headline) .font(.headline)
Text("\(tag.totalUses) posts from \(tag.totalAccounts) participants") Text("\(tag.totalUses) recent posts from \(tag.totalAccounts) participants")
.font(.footnote) .font(.footnote)
.foregroundColor(.gray) .foregroundColor(.gray)
} }