mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2025-09-01 23:53:48 +00:00
Timeline: Add filter for followed tags
This commit is contained in:
parent
62b96cac69
commit
dcdd8402e9
19 changed files with 259 additions and 153 deletions
|
@ -43,6 +43,8 @@ extension View {
|
||||||
StatusEditorView(mode: .edit(status: status))
|
StatusEditorView(mode: .edit(status: status))
|
||||||
case let .quoteStatusEditor(status):
|
case let .quoteStatusEditor(status):
|
||||||
StatusEditorView(mode: .quote(status: status))
|
StatusEditorView(mode: .quote(status: status))
|
||||||
|
case let .mentionStatusEditor(account, visibility):
|
||||||
|
StatusEditorView(mode: .mention(account: account, visibility: visibility))
|
||||||
case let .listEdit(list):
|
case let .listEdit(list):
|
||||||
ListEditView(list: list)
|
ListEditView(list: list)
|
||||||
case let .listAddAccount(account):
|
case let .listAddAccount(account):
|
||||||
|
|
|
@ -87,6 +87,18 @@ struct TimelineTab: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !currentAccount.tags.isEmpty {
|
||||||
|
Menu("Followed Tags") {
|
||||||
|
ForEach(currentAccount.tags) { tag in
|
||||||
|
Button {
|
||||||
|
timeline = .hashtag(tag: tag.name, accountId: nil)
|
||||||
|
} label: {
|
||||||
|
Label("#\(tag.name)", systemImage: "number")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var accountButton: some View {
|
private var accountButton: some View {
|
||||||
|
|
|
@ -61,8 +61,8 @@ public struct AccountDetailView: View {
|
||||||
pinnedPostsView
|
pinnedPostsView
|
||||||
}
|
}
|
||||||
StatusesListView(fetcher: viewModel)
|
StatusesListView(fetcher: viewModel)
|
||||||
case let .followedTags(tags):
|
case .followedTags:
|
||||||
makeTagsListView(tags: tags)
|
tagsListView
|
||||||
case .lists:
|
case .lists:
|
||||||
listsListView
|
listsListView
|
||||||
}
|
}
|
||||||
|
@ -70,21 +70,6 @@ public struct AccountDetailView: View {
|
||||||
}
|
}
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(theme.primaryBackgroundColor)
|
.background(theme.primaryBackgroundColor)
|
||||||
.toolbar {
|
|
||||||
if viewModel.relationship?.following == true, let account = viewModel.account {
|
|
||||||
ToolbarItem {
|
|
||||||
Menu {
|
|
||||||
Button {
|
|
||||||
routeurPath.presentedSheet = .listAddAccount(account: account)
|
|
||||||
} label: {
|
|
||||||
Label("Add/Remove from lists", systemImage: "list.bullet")
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
Image(systemName: "ellipsis")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
Task {
|
Task {
|
||||||
|
@ -113,16 +98,7 @@ public struct AccountDetailView: View {
|
||||||
.edgesIgnoringSafeArea(.top)
|
.edgesIgnoringSafeArea(.top)
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .principal) {
|
toolbarContent
|
||||||
if scrollOffset < -200 {
|
|
||||||
switch viewModel.accountState {
|
|
||||||
case let .data(account):
|
|
||||||
account.displayNameWithEmojis.font(.headline)
|
|
||||||
default:
|
|
||||||
EmptyView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -239,9 +215,9 @@ public struct AccountDetailView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func makeTagsListView(tags: [Tag]) -> some View {
|
private var tagsListView: some View {
|
||||||
Group {
|
Group {
|
||||||
ForEach(tags) { tag in
|
ForEach(currentAccount.tags) { tag in
|
||||||
HStack {
|
HStack {
|
||||||
TagRowView(tag: tag)
|
TagRowView(tag: tag)
|
||||||
Spacer()
|
Spacer()
|
||||||
|
@ -250,6 +226,8 @@ public struct AccountDetailView: View {
|
||||||
.padding(.horizontal, .layoutPadding)
|
.padding(.horizontal, .layoutPadding)
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
}
|
}
|
||||||
|
}.task {
|
||||||
|
await currentAccount.fetchFollowedTags()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -280,6 +258,9 @@ public struct AccountDetailView: View {
|
||||||
}
|
}
|
||||||
.padding(.horizontal, .layoutPadding)
|
.padding(.horizontal, .layoutPadding)
|
||||||
}
|
}
|
||||||
|
.task {
|
||||||
|
await currentAccount.fetchLists()
|
||||||
|
}
|
||||||
.alert("Create a new list", isPresented: $isCreateListAlertPresented) {
|
.alert("Create a new list", isPresented: $isCreateListAlertPresented) {
|
||||||
TextField("List name", text: $createListTitle)
|
TextField("List name", text: $createListTitle)
|
||||||
Button("Cancel") {
|
Button("Cancel") {
|
||||||
|
@ -316,6 +297,55 @@ public struct AccountDetailView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ToolbarContentBuilder
|
||||||
|
private var toolbarContent: some ToolbarContent {
|
||||||
|
ToolbarItem(placement: .principal) {
|
||||||
|
if scrollOffset < -200 {
|
||||||
|
switch viewModel.accountState {
|
||||||
|
case let .data(account):
|
||||||
|
account.displayNameWithEmojis.font(.headline)
|
||||||
|
default:
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
|
Menu {
|
||||||
|
if let account = viewModel.account {
|
||||||
|
Section(account.acct) {
|
||||||
|
if !viewModel.isCurrentUser {
|
||||||
|
Button {
|
||||||
|
routeurPath.presentedSheet = .mentionStatusEditor(account: account, visibility: .pub)
|
||||||
|
} label: {
|
||||||
|
Label("Mention", systemImage: "at")
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
routeurPath.presentedSheet = .mentionStatusEditor(account: account, visibility: .direct)
|
||||||
|
} label: {
|
||||||
|
Label("Message", systemImage: "tray.full")
|
||||||
|
}
|
||||||
|
Divider()
|
||||||
|
}
|
||||||
|
|
||||||
|
if viewModel.relationship?.following == true {
|
||||||
|
Button {
|
||||||
|
routeurPath.presentedSheet = .listAddAccount(account: account)
|
||||||
|
} label: {
|
||||||
|
Label("Add/Remove from lists", systemImage: "list.bullet")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let url = account.url {
|
||||||
|
ShareLink(item: url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "ellipsis")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AccountDetailView_Previews: PreviewProvider {
|
struct AccountDetailView_Previews: PreviewProvider {
|
||||||
|
|
|
@ -38,7 +38,7 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
enum TabState {
|
enum TabState {
|
||||||
case followedTags(tags: [Tag])
|
case followedTags
|
||||||
case statuses(statusesState: StatusesState)
|
case statuses(statusesState: StatusesState)
|
||||||
case lists
|
case lists
|
||||||
}
|
}
|
||||||
|
@ -62,7 +62,6 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
|
||||||
@Published var pinned: [Status] = []
|
@Published var pinned: [Status] = []
|
||||||
@Published var favourites: [Status] = []
|
@Published var favourites: [Status] = []
|
||||||
private var favouritesNextPage: LinkHandler?
|
private var favouritesNextPage: LinkHandler?
|
||||||
@Published var followedTags: [Tag] = []
|
|
||||||
@Published var featuredTags: [FeaturedTag] = []
|
@Published var featuredTags: [FeaturedTag] = []
|
||||||
@Published var fields: [Account.Field] = []
|
@Published var fields: [Account.Field] = []
|
||||||
@Published var familliarFollowers: [Account] = []
|
@Published var familliarFollowers: [Account] = []
|
||||||
|
@ -108,16 +107,11 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
|
||||||
self.featuredTags = try await featuredTags
|
self.featuredTags = try await featuredTags
|
||||||
self.featuredTags.sort { $0.statusesCountInt > $1.statusesCountInt }
|
self.featuredTags.sort { $0.statusesCountInt > $1.statusesCountInt }
|
||||||
self.fields = loadedAccount.fields
|
self.fields = loadedAccount.fields
|
||||||
if isCurrentUser {
|
if client.isAuth {
|
||||||
async let followedTags: [Tag] = client.get(endpoint: Accounts.followedTags)
|
async let relationships: [Relationshionship] = client.get(endpoint: Accounts.relationships(ids: [accountId]))
|
||||||
self.followedTags = try await followedTags
|
async let familliarFollowers: [FamilliarAccounts] = client.get(endpoint: Accounts.familiarFollowers(withAccount: accountId))
|
||||||
} else {
|
self.relationship = try await relationships.first
|
||||||
if client.isAuth {
|
self.familliarFollowers = try await familliarFollowers.first?.accounts ?? []
|
||||||
async let relationships: [Relationshionship] = client.get(endpoint: Accounts.relationships(ids: [accountId]))
|
|
||||||
async let familliarFollowers: [FamilliarAccounts] = client.get(endpoint: Accounts.familiarFollowers(withAccount: accountId))
|
|
||||||
self.relationship = try await relationships.first
|
|
||||||
self.familliarFollowers = try await familliarFollowers.first?.accounts ?? []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
accountState = .data(account: loadedAccount)
|
accountState = .data(account: loadedAccount)
|
||||||
} catch {
|
} catch {
|
||||||
|
@ -215,7 +209,7 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
|
||||||
tabState = .statuses(statusesState: .display(statuses: favourites,
|
tabState = .statuses(statusesState: .display(statuses: favourites,
|
||||||
nextPageState: favouritesNextPage != nil ? .hasNextPage : .none))
|
nextPageState: favouritesNextPage != nil ? .hasNextPage : .none))
|
||||||
case .followedTags:
|
case .followedTags:
|
||||||
tabState = .followedTags(tags: followedTags)
|
tabState = .followedTags
|
||||||
case .lists:
|
case .lists:
|
||||||
tabState = .lists
|
tabState = .lists
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,18 +6,25 @@ import Network
|
||||||
public class CurrentAccount: ObservableObject {
|
public class CurrentAccount: ObservableObject {
|
||||||
@Published public private(set) var account: Account?
|
@Published public private(set) var account: Account?
|
||||||
@Published public private(set) var lists: [List] = []
|
@Published public private(set) var lists: [List] = []
|
||||||
|
@Published public private(set) var tags: [Tag] = []
|
||||||
|
|
||||||
private var client: Client?
|
private var client: Client?
|
||||||
|
|
||||||
public init() {
|
public init() { }
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public func setClient(client: Client) {
|
public func setClient(client: Client) {
|
||||||
self.client = client
|
self.client = client
|
||||||
Task {
|
|
||||||
await fetchCurrentAccount()
|
Task(priority: .userInitiated) {
|
||||||
await fetchLists()
|
await fetchUserData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func fetchUserData() async {
|
||||||
|
await withTaskGroup(of: Void.self) { group in
|
||||||
|
group.addTask { await self.fetchCurrentAccount() }
|
||||||
|
group.addTask { await self.fetchLists() }
|
||||||
|
group.addTask { await self.fetchFollowedTags() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,9 +33,7 @@ public class CurrentAccount: ObservableObject {
|
||||||
account = nil
|
account = nil
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Task {
|
account = try? await client.get(endpoint: Accounts.verifyCredentials)
|
||||||
account = try? await client.get(endpoint: Accounts.verifyCredentials)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public func fetchLists() async {
|
public func fetchLists() async {
|
||||||
|
@ -40,6 +45,15 @@ public class CurrentAccount: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func fetchFollowedTags() async {
|
||||||
|
guard let client, client.isAuth else { return }
|
||||||
|
do {
|
||||||
|
tags = try await client.get(endpoint: Accounts.followedTags)
|
||||||
|
} catch {
|
||||||
|
tags = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public func createList(title: String) async {
|
public func createList(title: String) async {
|
||||||
guard let client else { return }
|
guard let client else { return }
|
||||||
do {
|
do {
|
||||||
|
@ -57,4 +71,26 @@ public class CurrentAccount: ObservableObject {
|
||||||
lists.append(list)
|
lists.append(list)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func followTag(id: String) async -> Tag? {
|
||||||
|
guard let client else { return nil }
|
||||||
|
do {
|
||||||
|
let tag: Tag = try await client.post(endpoint: Tags.follow(id: id))
|
||||||
|
tags.append(tag)
|
||||||
|
return tag
|
||||||
|
} catch {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func unfollowTag(id: String) async -> Tag? {
|
||||||
|
guard let client else { return nil }
|
||||||
|
do {
|
||||||
|
let tag: Tag = try await client.post(endpoint: Tags.unfollow(id: id))
|
||||||
|
tags.removeAll{ $0.id == tag.id }
|
||||||
|
return tag
|
||||||
|
} catch {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,12 +20,13 @@ public enum SheetDestinations: Identifiable {
|
||||||
case editStatusEditor(status: Status)
|
case editStatusEditor(status: Status)
|
||||||
case replyToStatusEditor(status: Status)
|
case replyToStatusEditor(status: Status)
|
||||||
case quoteStatusEditor(status: Status)
|
case quoteStatusEditor(status: Status)
|
||||||
|
case mentionStatusEditor(account: Account, visibility: Models.Visibility)
|
||||||
case listEdit(list: Models.List)
|
case listEdit(list: Models.List)
|
||||||
case listAddAccount(account: Account)
|
case listAddAccount(account: Account)
|
||||||
|
|
||||||
public var id: String {
|
public var id: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .editStatusEditor, .newStatusEditor, .replyToStatusEditor, .quoteStatusEditor:
|
case .editStatusEditor, .newStatusEditor, .replyToStatusEditor, .quoteStatusEditor, .mentionStatusEditor:
|
||||||
return "statusEditor"
|
return "statusEditor"
|
||||||
case .listEdit:
|
case .listEdit:
|
||||||
return "listEdit"
|
return "listEdit"
|
||||||
|
|
|
@ -83,19 +83,23 @@ public class StreamWatcher: ObservableObject {
|
||||||
} catch {
|
} catch {
|
||||||
print("Error decoding streaming event: \(error.localizedDescription)")
|
print("Error decoding streaming event: \(error.localizedDescription)")
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.receiveMessage()
|
||||||
|
|
||||||
case .failure:
|
case .failure:
|
||||||
self.stopWatching()
|
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(10)) { [weak self] in
|
||||||
self.connect()
|
guard let self = self else { return }
|
||||||
if let watchedStream = self.watchedStream {
|
self.stopWatching()
|
||||||
self.watch(stream: watchedStream)
|
self.connect()
|
||||||
|
if let watchedStream = self.watchedStream {
|
||||||
|
self.watch(stream: watchedStream)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.receiveMessage()
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,7 @@ public struct Account: Codable, Identifiable, Equatable, Hashable {
|
||||||
public let fields: [Field]
|
public let fields: [Field]
|
||||||
public let locked: Bool
|
public let locked: Bool
|
||||||
public let emojis: [Emoji]
|
public let emojis: [Emoji]
|
||||||
|
public let url: URL?
|
||||||
|
|
||||||
public static func placeholder() -> Account {
|
public static func placeholder() -> Account {
|
||||||
.init(id: UUID().uuidString,
|
.init(id: UUID().uuidString,
|
||||||
|
@ -46,7 +47,8 @@ public struct Account: Codable, Identifiable, Equatable, Hashable {
|
||||||
lastStatusAt: nil,
|
lastStatusAt: nil,
|
||||||
fields: [],
|
fields: [],
|
||||||
locked: false,
|
locked: false,
|
||||||
emojis: [])
|
emojis: [],
|
||||||
|
url: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func placeholders() -> [Account] {
|
public static func placeholders() -> [Account] {
|
||||||
|
|
8
Packages/Models/Sources/Models/App/App.swift
Normal file
8
Packages/Models/Sources/Models/App/App.swift
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct AppInfo {
|
||||||
|
public static let clientName = "IceCubesApp"
|
||||||
|
public static let scheme = "icecubesapp://"
|
||||||
|
public static let scopes = "read write follow push"
|
||||||
|
public static let weblink = "https://github.com/Dimillian/IceCubesApp"
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import Models
|
||||||
|
|
||||||
public enum Apps: Endpoint {
|
public enum Apps: Endpoint {
|
||||||
case registerApp
|
case registerApp
|
||||||
|
@ -14,10 +15,10 @@ public enum Apps: Endpoint {
|
||||||
switch self {
|
switch self {
|
||||||
case .registerApp:
|
case .registerApp:
|
||||||
return [
|
return [
|
||||||
.init(name: "client_name", value: "IceCubesApp"),
|
.init(name: "client_name", value: AppInfo.clientName),
|
||||||
.init(name: "redirect_uris", value: "icecubesapp://"),
|
.init(name: "redirect_uris", value: AppInfo.scheme),
|
||||||
.init(name: "scopes", value: "read write follow push"),
|
.init(name: "scopes", value: AppInfo.scopes),
|
||||||
.init(name: "website", value: "https://github.com/Dimillian/IceCubesApp")
|
.init(name: "website", value: AppInfo.weblink)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import Models
|
||||||
|
|
||||||
public enum Oauth: Endpoint {
|
public enum Oauth: Endpoint {
|
||||||
case authorize(clientId: String)
|
case authorize(clientId: String)
|
||||||
|
@ -19,17 +20,17 @@ public enum Oauth: Endpoint {
|
||||||
return [
|
return [
|
||||||
.init(name: "response_type", value: "code"),
|
.init(name: "response_type", value: "code"),
|
||||||
.init(name: "client_id", value: clientId),
|
.init(name: "client_id", value: clientId),
|
||||||
.init(name: "redirect_uri", value: "icecubesapp://"),
|
.init(name: "redirect_uri", value: AppInfo.scheme),
|
||||||
.init(name: "scope", value: "read write follow push")
|
.init(name: "scope", value: AppInfo.scopes)
|
||||||
]
|
]
|
||||||
case let .token(code, clientId, clientSecret):
|
case let .token(code, clientId, clientSecret):
|
||||||
return [
|
return [
|
||||||
.init(name: "grant_type", value: "authorization_code"),
|
.init(name: "grant_type", value: "authorization_code"),
|
||||||
.init(name: "client_id", value: clientId),
|
.init(name: "client_id", value: clientId),
|
||||||
.init(name: "client_secret", value: clientSecret),
|
.init(name: "client_secret", value: clientSecret),
|
||||||
.init(name: "redirect_uri", value: "icecubesapp://"),
|
.init(name: "redirect_uri", value: AppInfo.scheme),
|
||||||
.init(name: "code", value: code),
|
.init(name: "code", value: code),
|
||||||
.init(name: "scope", value: "read write follow push")
|
.init(name: "scope", value: AppInfo.scopes)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,11 +62,13 @@ struct StatusEditorAccessoryView: View {
|
||||||
|
|
||||||
private var visibilityMenu: some View {
|
private var visibilityMenu: some View {
|
||||||
Menu {
|
Menu {
|
||||||
ForEach(Models.Visibility.allCases, id: \.self) { visibility in
|
Section("Post visibility") {
|
||||||
Button {
|
ForEach(Models.Visibility.allCases, id: \.self) { visibility in
|
||||||
viewModel.visibility = visibility
|
Button {
|
||||||
} label: {
|
viewModel.visibility = visibility
|
||||||
Label(visibility.title, systemImage: visibility.iconName)
|
} label: {
|
||||||
|
Label(visibility.title, systemImage: visibility.iconName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
|
|
|
@ -82,7 +82,7 @@ public class StatusEditorViewModel: ObservableObject {
|
||||||
isPosting = true
|
isPosting = true
|
||||||
let postStatus: Status?
|
let postStatus: Status?
|
||||||
switch mode {
|
switch mode {
|
||||||
case .new, .replyTo, .quote:
|
case .new, .replyTo, .quote, .mention:
|
||||||
postStatus = try await client.post(endpoint: Statuses.postStatus(status: statusText.string,
|
postStatus = try await client.post(endpoint: Statuses.postStatus(status: statusText.string,
|
||||||
inReplyTo: mode.replyToStatus?.id,
|
inReplyTo: mode.replyToStatus?.id,
|
||||||
mediaIds: mediasImages.compactMap{ $0.mediaAttachement?.id },
|
mediaIds: mediasImages.compactMap{ $0.mediaAttachement?.id },
|
||||||
|
@ -117,6 +117,10 @@ public class StatusEditorViewModel: ObservableObject {
|
||||||
visibility = status.visibility
|
visibility = status.visibility
|
||||||
statusText = .init(string: mentionString)
|
statusText = .init(string: mentionString)
|
||||||
selectedRange = .init(location: mentionString.utf16.count, length: 0)
|
selectedRange = .init(location: mentionString.utf16.count, length: 0)
|
||||||
|
case let .mention(account, visibility):
|
||||||
|
statusText = .init(string: "@\(account.acct) ")
|
||||||
|
self.visibility = visibility
|
||||||
|
selectedRange = .init(location: statusText.string.utf16.count, length: 0)
|
||||||
case let .edit(status):
|
case let .edit(status):
|
||||||
statusText = .init(status.content.asSafeAttributedString)
|
statusText = .init(status.content.asSafeAttributedString)
|
||||||
selectedRange = .init(location: statusText.string.utf16.count, length: 0)
|
selectedRange = .init(location: statusText.string.utf16.count, length: 0)
|
||||||
|
|
|
@ -6,6 +6,7 @@ extension StatusEditorViewModel {
|
||||||
case new
|
case new
|
||||||
case edit(status: Status)
|
case edit(status: Status)
|
||||||
case quote(status: Status)
|
case quote(status: Status)
|
||||||
|
case mention(account: Account, visibility: Visibility)
|
||||||
|
|
||||||
var isEditing: Bool {
|
var isEditing: Bool {
|
||||||
switch self {
|
switch self {
|
||||||
|
@ -27,7 +28,7 @@ extension StatusEditorViewModel {
|
||||||
|
|
||||||
var title: String {
|
var title: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .new:
|
case .new, .mention:
|
||||||
return "New Post"
|
return "New Post"
|
||||||
case .edit:
|
case .edit:
|
||||||
return "Edit your post"
|
return "Edit your post"
|
||||||
|
|
|
@ -10,7 +10,7 @@ extension Visibility {
|
||||||
case .priv:
|
case .priv:
|
||||||
return "lock"
|
return "lock"
|
||||||
case .direct:
|
case .direct:
|
||||||
return "at.circle"
|
return "tray.full"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
import Env
|
||||||
|
|
||||||
|
struct StatusRowContextMenu: View {
|
||||||
|
@EnvironmentObject private var account: CurrentAccount
|
||||||
|
@EnvironmentObject private var routeurPath: RouterPath
|
||||||
|
@ObservedObject var viewModel: StatusRowViewModel
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button { Task {
|
||||||
|
if viewModel.isFavourited {
|
||||||
|
await viewModel.unFavourite()
|
||||||
|
} else {
|
||||||
|
await viewModel.favourite()
|
||||||
|
}
|
||||||
|
} } label: {
|
||||||
|
Label(viewModel.isFavourited ? "Unfavorite" : "Favorite", systemImage: "star")
|
||||||
|
}
|
||||||
|
Button { Task {
|
||||||
|
if viewModel.isReblogged {
|
||||||
|
await viewModel.unReblog()
|
||||||
|
} else {
|
||||||
|
await viewModel.reblog()
|
||||||
|
}
|
||||||
|
} } label: {
|
||||||
|
Label(viewModel.isReblogged ? "Unboost" : "Boost", systemImage: "arrow.left.arrow.right.circle")
|
||||||
|
}
|
||||||
|
|
||||||
|
if viewModel.status.visibility == .pub {
|
||||||
|
Button {
|
||||||
|
routeurPath.presentedSheet = .quoteStatusEditor(status: viewModel.status)
|
||||||
|
} label: {
|
||||||
|
Label("Quote this post", systemImage: "quote.bubble")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let url = viewModel.status.reblog?.url ?? viewModel.status.url {
|
||||||
|
Button { UIApplication.shared.open(url) } label: {
|
||||||
|
Label("View in Browser", systemImage: "safari")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if account.account?.id == viewModel.status.account.id {
|
||||||
|
Section("Your post") {
|
||||||
|
Button {
|
||||||
|
Task {
|
||||||
|
if viewModel.isPinned {
|
||||||
|
await viewModel.unPin()
|
||||||
|
} else {
|
||||||
|
await viewModel.pin()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Label(viewModel.isPinned ? "Unpin": "Pin", systemImage: viewModel.isPinned ? "pin.fill" : "pin")
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
routeurPath.presentedSheet = .editStatusEditor(status: viewModel.status)
|
||||||
|
} label: {
|
||||||
|
Label("Edit", systemImage: "pencil")
|
||||||
|
}
|
||||||
|
Button(role: .destructive) { Task { await viewModel.delete() } } label: {
|
||||||
|
Label("Delete", systemImage: "trash")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Section(viewModel.status.account.acct) {
|
||||||
|
Button {
|
||||||
|
routeurPath.presentedSheet = .mentionStatusEditor(account: viewModel.status.account, visibility: .pub)
|
||||||
|
} label: {
|
||||||
|
Label("Mention", systemImage: "at")
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
routeurPath.presentedSheet = .mentionStatusEditor(account: viewModel.status.account, visibility: .direct)
|
||||||
|
} label: {
|
||||||
|
Label("Message", systemImage: "tray.full")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -62,7 +62,7 @@ public struct StatusRowView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
contextMenu
|
StatusRowContextMenu(viewModel: viewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -209,7 +209,7 @@ public struct StatusRowView: View {
|
||||||
|
|
||||||
private var menuButton: some View {
|
private var menuButton: some View {
|
||||||
Menu {
|
Menu {
|
||||||
contextMenu
|
StatusRowContextMenu(viewModel: viewModel)
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "ellipsis")
|
Image(systemName: "ellipsis")
|
||||||
.frame(width: 30, height: 30)
|
.frame(width: 30, height: 30)
|
||||||
|
@ -217,64 +217,4 @@ public struct StatusRowView: View {
|
||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private var contextMenu: some View {
|
|
||||||
Button { Task {
|
|
||||||
if viewModel.isFavourited {
|
|
||||||
await viewModel.unFavourite()
|
|
||||||
} else {
|
|
||||||
await viewModel.favourite()
|
|
||||||
}
|
|
||||||
} } label: {
|
|
||||||
Label(viewModel.isFavourited ? "Unfavorite" : "Favorite", systemImage: "star")
|
|
||||||
}
|
|
||||||
Button { Task {
|
|
||||||
if viewModel.isReblogged {
|
|
||||||
await viewModel.unReblog()
|
|
||||||
} else {
|
|
||||||
await viewModel.reblog()
|
|
||||||
}
|
|
||||||
} } label: {
|
|
||||||
Label(viewModel.isReblogged ? "Unboost" : "Boost", systemImage: "arrow.left.arrow.right.circle")
|
|
||||||
}
|
|
||||||
|
|
||||||
if viewModel.status.visibility == .pub {
|
|
||||||
Button {
|
|
||||||
routeurPath.presentedSheet = .quoteStatusEditor(status: viewModel.status)
|
|
||||||
} label: {
|
|
||||||
Label("Quote this post", systemImage: "quote.bubble")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let url = viewModel.status.reblog?.url ?? viewModel.status.url {
|
|
||||||
Button { UIApplication.shared.open(url) } label: {
|
|
||||||
Label("View in Browser", systemImage: "safari")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if account.account?.id == viewModel.status.account.id {
|
|
||||||
Section("Your post") {
|
|
||||||
Button {
|
|
||||||
Task {
|
|
||||||
if viewModel.isPinned {
|
|
||||||
await viewModel.unPin()
|
|
||||||
} else {
|
|
||||||
await viewModel.pin()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
Label(viewModel.isPinned ? "Unpin": "Pin", systemImage: viewModel.isPinned ? "pin.fill" : "pin")
|
|
||||||
}
|
|
||||||
Button {
|
|
||||||
routeurPath.presentedSheet = .editStatusEditor(status: viewModel.status)
|
|
||||||
} label: {
|
|
||||||
Label("Edit", systemImage: "pencil")
|
|
||||||
}
|
|
||||||
Button(role: .destructive) { Task { await viewModel.delete() } } label: {
|
|
||||||
Label("Delete", systemImage: "trash")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -139,9 +139,9 @@ public struct TimelineView: View {
|
||||||
Button {
|
Button {
|
||||||
Task {
|
Task {
|
||||||
if tag.following {
|
if tag.following {
|
||||||
await viewModel.unfollowTag(id: tag.name)
|
viewModel.tag = await account.unfollowTag(id: tag.name)
|
||||||
} else {
|
} else {
|
||||||
await viewModel.followTag(id: tag.name)
|
viewModel.tag = await account.followTag(id: tag.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
|
|
|
@ -24,6 +24,7 @@ class TimelineViewModel: ObservableObject, StatusesFetcher {
|
||||||
if oldValue != timeline {
|
if oldValue != timeline {
|
||||||
statuses = []
|
statuses = []
|
||||||
pendingStatuses = []
|
pendingStatuses = []
|
||||||
|
tag = nil
|
||||||
}
|
}
|
||||||
await fetchStatuses(userIntent: false)
|
await fetchStatuses(userIntent: false)
|
||||||
switch timeline {
|
switch timeline {
|
||||||
|
@ -143,20 +144,6 @@ class TimelineViewModel: ObservableObject, StatusesFetcher {
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
func followTag(id: String) async {
|
|
||||||
guard let client else { return }
|
|
||||||
do {
|
|
||||||
tag = try await client.post(endpoint: Tags.follow(id: id))
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
func unfollowTag(id: String) async {
|
|
||||||
guard let client else { return }
|
|
||||||
do {
|
|
||||||
tag = try await client.post(endpoint: Tags.unfollow(id: id))
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleEvent(event: any StreamEvent, currentAccount: CurrentAccount) {
|
func handleEvent(event: any StreamEvent, currentAccount: CurrentAccount) {
|
||||||
if let event = event as? StreamEventUpdate {
|
if let event = event as? StreamEventUpdate {
|
||||||
if event.status.account.id == currentAccount.account?.id,
|
if event.status.account.id == currentAccount.account?.id,
|
||||||
|
|
Loading…
Reference in a new issue