mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-11-26 02:01:02 +00:00
Post boosted by / favourited by
This commit is contained in:
parent
569aedeaeb
commit
70ee6e0d27
14 changed files with 143 additions and 68 deletions
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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] =
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue