mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-10-31 22:28:58 +00:00
Added lists support + bunch of bug fixes
This commit is contained in:
parent
a6da64ce14
commit
e0253fb439
28 changed files with 556 additions and 12 deletions
|
@ -32,6 +32,7 @@
|
||||||
9FBFE63D292A715500C250E9 /* IceCubesApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBFE63C292A715500C250E9 /* IceCubesApp.swift */; };
|
9FBFE63D292A715500C250E9 /* IceCubesApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBFE63C292A715500C250E9 /* IceCubesApp.swift */; };
|
||||||
9FBFE64E292A72BD00C250E9 /* Network in Frameworks */ = {isa = PBXBuildFile; productRef = 9FBFE64D292A72BD00C250E9 /* Network */; };
|
9FBFE64E292A72BD00C250E9 /* Network in Frameworks */ = {isa = PBXBuildFile; productRef = 9FBFE64D292A72BD00C250E9 /* Network */; };
|
||||||
9FD34823293D06E800DB0EE9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9FD34822293D06E800DB0EE9 /* Assets.xcassets */; };
|
9FD34823293D06E800DB0EE9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9FD34822293D06E800DB0EE9 /* Assets.xcassets */; };
|
||||||
|
9FD542E72962D2FF0045321A /* Lists in Frameworks */ = {isa = PBXBuildFile; productRef = 9FD542E62962D2FF0045321A /* Lists */; };
|
||||||
9FE151A6293C90F900E9683D /* IconSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FE151A5293C90F900E9683D /* IconSelectorView.swift */; };
|
9FE151A6293C90F900E9683D /* IconSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FE151A5293C90F900E9683D /* IconSelectorView.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
|
@ -64,6 +65,7 @@
|
||||||
9FBFE63C292A715500C250E9 /* IceCubesApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IceCubesApp.swift; sourceTree = "<group>"; };
|
9FBFE63C292A715500C250E9 /* IceCubesApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IceCubesApp.swift; sourceTree = "<group>"; };
|
||||||
9FBFE642292A715600C250E9 /* IceCubesApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = IceCubesApp.entitlements; sourceTree = "<group>"; };
|
9FBFE642292A715600C250E9 /* IceCubesApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = IceCubesApp.entitlements; sourceTree = "<group>"; };
|
||||||
9FD34822293D06E800DB0EE9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
9FD34822293D06E800DB0EE9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
|
9FD542E52962D2CE0045321A /* Lists */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Lists; path = Packages/Lists; sourceTree = "<group>"; };
|
||||||
9FE151A5293C90F900E9683D /* IconSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconSelectorView.swift; sourceTree = "<group>"; };
|
9FE151A5293C90F900E9683D /* IconSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconSelectorView.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
@ -76,6 +78,7 @@
|
||||||
9FAE4ACE29379A5A00772766 /* KeychainSwift in Frameworks */,
|
9FAE4ACE29379A5A00772766 /* KeychainSwift in Frameworks */,
|
||||||
9F398AA92935FFDB00A889F2 /* Account in Frameworks */,
|
9F398AA92935FFDB00A889F2 /* Account in Frameworks */,
|
||||||
9FBFE64E292A72BD00C250E9 /* Network in Frameworks */,
|
9FBFE64E292A72BD00C250E9 /* Network in Frameworks */,
|
||||||
|
9FD542E72962D2FF0045321A /* Lists in Frameworks */,
|
||||||
9F398AAB2935FFDB00A889F2 /* Models in Frameworks */,
|
9F398AAB2935FFDB00A889F2 /* Models in Frameworks */,
|
||||||
9F5E581929545BE700A53960 /* Env in Frameworks */,
|
9F5E581929545BE700A53960 /* Env in Frameworks */,
|
||||||
9F35DB44294F9A7D00B3281A /* Status in Frameworks */,
|
9F35DB44294F9A7D00B3281A /* Status in Frameworks */,
|
||||||
|
@ -142,6 +145,7 @@
|
||||||
9F5E581729545B5500A53960 /* Env */,
|
9F5E581729545B5500A53960 /* Env */,
|
||||||
9F398AA32935F90100A889F2 /* Models */,
|
9F398AA32935F90100A889F2 /* Models */,
|
||||||
9F29553D292B67B600E0E81B /* Network */,
|
9F29553D292B67B600E0E81B /* Network */,
|
||||||
|
9FD542E52962D2CE0045321A /* Lists */,
|
||||||
9F35DB4829506F7F00B3281A /* Notifications */,
|
9F35DB4829506F7F00B3281A /* Notifications */,
|
||||||
9F29553E292B6AF600E0E81B /* Timeline */,
|
9F29553E292B6AF600E0E81B /* Timeline */,
|
||||||
9F35DB42294F9A2900B3281A /* Status */,
|
9F35DB42294F9A2900B3281A /* Status */,
|
||||||
|
@ -212,6 +216,7 @@
|
||||||
9F35DB4929506FA100B3281A /* Notifications */,
|
9F35DB4929506FA100B3281A /* Notifications */,
|
||||||
9F5E581829545BE700A53960 /* Env */,
|
9F5E581829545BE700A53960 /* Env */,
|
||||||
9F55C68F2955993C00F94077 /* Explore */,
|
9F55C68F2955993C00F94077 /* Explore */,
|
||||||
|
9FD542E62962D2FF0045321A /* Lists */,
|
||||||
);
|
);
|
||||||
productName = IceCubesApp;
|
productName = IceCubesApp;
|
||||||
productReference = 9FBFE639292A715500C250E9 /* IceCubesApp.app */;
|
productReference = 9FBFE639292A715500C250E9 /* IceCubesApp.app */;
|
||||||
|
@ -566,6 +571,10 @@
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
productName = Network;
|
productName = Network;
|
||||||
};
|
};
|
||||||
|
9FD542E62962D2FF0045321A /* Lists */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
productName = Lists;
|
||||||
|
};
|
||||||
/* End XCSwiftPackageProductDependency section */
|
/* End XCSwiftPackageProductDependency section */
|
||||||
};
|
};
|
||||||
rootObject = 9FBFE631292A715500C250E9 /* Project object */;
|
rootObject = 9FBFE631292A715500C250E9 /* Project object */;
|
||||||
|
|
|
@ -4,6 +4,7 @@ import Account
|
||||||
import Env
|
import Env
|
||||||
import Status
|
import Status
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
|
import Lists
|
||||||
|
|
||||||
extension View {
|
extension View {
|
||||||
func withAppRouteur() -> some View {
|
func withAppRouteur() -> some View {
|
||||||
|
@ -17,6 +18,8 @@ extension View {
|
||||||
StatusDetailView(statusId: id)
|
StatusDetailView(statusId: id)
|
||||||
case let .hashTag(tag, accountId):
|
case let .hashTag(tag, accountId):
|
||||||
TimelineView(timeline: .constant(.hashtag(tag: tag, accountId: accountId)), scrollToTopSignal: .constant(0))
|
TimelineView(timeline: .constant(.hashtag(tag: tag, accountId: accountId)), scrollToTopSignal: .constant(0))
|
||||||
|
case let .list(list):
|
||||||
|
TimelineView(timeline: .constant(.list(list: list)), scrollToTopSignal: .constant(0))
|
||||||
case let .following(id):
|
case let .following(id):
|
||||||
AccountsListView(mode: .following(accountId: id))
|
AccountsListView(mode: .following(accountId: id))
|
||||||
case let .followers(id):
|
case let .followers(id):
|
||||||
|
@ -40,6 +43,10 @@ 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 .listEdit(list):
|
||||||
|
ListEditView(list: list)
|
||||||
|
case let .listAddAccount(account):
|
||||||
|
ListAddAccountView(account: account)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,9 @@ struct AccountTab: View {
|
||||||
routeurPath.path = []
|
routeurPath.path = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onChange(of: currentAccount.account?.id) { _ in
|
||||||
|
routeurPath.path = []
|
||||||
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
routeurPath.client = client
|
routeurPath.client = client
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,9 @@ struct ExploreTab: View {
|
||||||
routeurPath.path = []
|
routeurPath.path = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onChange(of: currentAccount.account?.id) { _ in
|
||||||
|
routeurPath.path = []
|
||||||
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
routeurPath.client = client
|
routeurPath.client = client
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,5 +31,8 @@ struct NotificationsTab: View {
|
||||||
routeurPath.path = []
|
routeurPath.path = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onChange(of: currentAccount.account?.id) { _ in
|
||||||
|
routeurPath.path = []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import Env
|
||||||
import Network
|
import Network
|
||||||
import Combine
|
import Combine
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
|
import Models
|
||||||
|
|
||||||
struct TimelineTab: View {
|
struct TimelineTab: View {
|
||||||
@EnvironmentObject private var appAccounts: AppAccountsManager
|
@EnvironmentObject private var appAccounts: AppAccountsManager
|
||||||
|
@ -12,6 +13,7 @@ struct TimelineTab: View {
|
||||||
@EnvironmentObject private var client: Client
|
@EnvironmentObject private var client: Client
|
||||||
@StateObject private var routeurPath = RouterPath()
|
@StateObject private var routeurPath = RouterPath()
|
||||||
@Binding var popToRootTab: Tab
|
@Binding var popToRootTab: Tab
|
||||||
|
|
||||||
@State private var timeline: TimelineFilter = .home
|
@State private var timeline: TimelineFilter = .home
|
||||||
@State private var scrollToTopSignal: Int = 0
|
@State private var scrollToTopSignal: Int = 0
|
||||||
@State private var isAddAccountSheetDisplayed = false
|
@State private var isAddAccountSheetDisplayed = false
|
||||||
|
@ -45,6 +47,9 @@ struct TimelineTab: View {
|
||||||
.onAppear {
|
.onAppear {
|
||||||
routeurPath.client = client
|
routeurPath.client = client
|
||||||
timeline = client.isAuth ? .home : .pub
|
timeline = client.isAuth ? .home : .pub
|
||||||
|
Task {
|
||||||
|
await currentAccount.fetchLists()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.environmentObject(routeurPath)
|
.environmentObject(routeurPath)
|
||||||
.onChange(of: $popToRootTab.wrappedValue) { popToRootTab in
|
.onChange(of: $popToRootTab.wrappedValue) { popToRootTab in
|
||||||
|
@ -56,9 +61,13 @@ struct TimelineTab: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onChange(of: currentAccount.account?.id) { _ in
|
||||||
|
routeurPath.path = []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
private var timelineFilterButton: some View {
|
private var timelineFilterButton: some View {
|
||||||
ForEach(TimelineFilter.availableTimeline(client: client), id: \.self) { timeline in
|
ForEach(TimelineFilter.availableTimeline(client: client), id: \.self) { timeline in
|
||||||
Button {
|
Button {
|
||||||
|
@ -67,6 +76,17 @@ struct TimelineTab: View {
|
||||||
Label(timeline.title(), systemImage: timeline.iconName() ?? "")
|
Label(timeline.title(), systemImage: timeline.iconName() ?? "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !currentAccount.lists.isEmpty {
|
||||||
|
Menu("Lists") {
|
||||||
|
ForEach(currentAccount.lists) { list in
|
||||||
|
Button {
|
||||||
|
timeline = .list(list: list)
|
||||||
|
} label: {
|
||||||
|
Label(list.title, systemImage: "list.bullet")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var accountButton: some View {
|
private var accountButton: some View {
|
||||||
|
@ -95,6 +115,7 @@ struct TimelineTab: View {
|
||||||
ForEach(accountsViewModel, id: \.appAccount.id) { viewModel in
|
ForEach(accountsViewModel, id: \.appAccount.id) { viewModel in
|
||||||
Button {
|
Button {
|
||||||
appAccounts.currentAccount = viewModel.appAccount
|
appAccounts.currentAccount = viewModel.appAccount
|
||||||
|
timeline = .home
|
||||||
} label: {
|
} label: {
|
||||||
HStack {
|
HStack {
|
||||||
if viewModel.account?.id == currentAccount.account?.id {
|
if viewModel.account?.id == currentAccount.account?.id {
|
||||||
|
|
|
@ -18,6 +18,8 @@ public struct AccountDetailView: View {
|
||||||
@State private var scrollOffset: CGFloat = 0
|
@State private var scrollOffset: CGFloat = 0
|
||||||
@State private var isFieldsSheetDisplayed: Bool = false
|
@State private var isFieldsSheetDisplayed: Bool = false
|
||||||
@State private var isCurrentUser: Bool = false
|
@State private var isCurrentUser: Bool = false
|
||||||
|
@State private var isCreateListAlertPresented: Bool = false
|
||||||
|
@State private var createListTitle: String = ""
|
||||||
|
|
||||||
/// When coming from a URL like a mention tap in a status.
|
/// When coming from a URL like a mention tap in a status.
|
||||||
public init(accountId: String) {
|
public init(accountId: String) {
|
||||||
|
@ -58,11 +60,28 @@ public struct AccountDetailView: View {
|
||||||
StatusesListView(fetcher: viewModel)
|
StatusesListView(fetcher: viewModel)
|
||||||
case let .followedTags(tags):
|
case let .followedTags(tags):
|
||||||
makeTagsListView(tags: tags)
|
makeTagsListView(tags: tags)
|
||||||
|
case .lists:
|
||||||
|
listsListView
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.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 {
|
||||||
|
@ -220,12 +239,62 @@ public struct AccountDetailView: View {
|
||||||
private func makeTagsListView(tags: [Tag]) -> some View {
|
private func makeTagsListView(tags: [Tag]) -> some View {
|
||||||
Group {
|
Group {
|
||||||
ForEach(tags) { tag in
|
ForEach(tags) { tag in
|
||||||
TagRowView(tag: tag)
|
HStack {
|
||||||
.padding(.horizontal, DS.Constants.layoutPadding)
|
TagRowView(tag: tag)
|
||||||
.padding(.vertical, 8)
|
Spacer()
|
||||||
|
Image(systemName: "chevron.right")
|
||||||
|
}
|
||||||
|
.padding(.horizontal, DS.Constants.layoutPadding)
|
||||||
|
.padding(.vertical, 8)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var listsListView: some View {
|
||||||
|
Group {
|
||||||
|
ForEach(currentAccount.lists) { list in
|
||||||
|
NavigationLink(value: RouteurDestinations.list(list: list)) {
|
||||||
|
HStack {
|
||||||
|
Text(list.title)
|
||||||
|
Spacer()
|
||||||
|
Image(systemName: "chevron.right")
|
||||||
|
}
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
.padding(.horizontal, DS.Constants.layoutPadding)
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
}
|
||||||
|
.contextMenu {
|
||||||
|
Button("Delete list", role: .destructive) {
|
||||||
|
Task {
|
||||||
|
await currentAccount.deleteList(list: list)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Button("Create a new list") {
|
||||||
|
isCreateListAlertPresented = true
|
||||||
|
}
|
||||||
|
.padding(.horizontal, DS.Constants.layoutPadding)
|
||||||
|
}
|
||||||
|
.alert("Create a new list", isPresented: $isCreateListAlertPresented) {
|
||||||
|
TextField("List name", text: $createListTitle)
|
||||||
|
Button("Cancel") {
|
||||||
|
isCreateListAlertPresented = false
|
||||||
|
createListTitle = ""
|
||||||
|
}
|
||||||
|
Button("Create List") {
|
||||||
|
guard !createListTitle.isEmpty else { return }
|
||||||
|
isCreateListAlertPresented = false
|
||||||
|
Task {
|
||||||
|
await currentAccount.createList(title: createListTitle)
|
||||||
|
createListTitle = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} message: {
|
||||||
|
Text("Enter the name for your list")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AccountDetailView_Previews: PreviewProvider {
|
struct AccountDetailView_Previews: PreviewProvider {
|
||||||
|
|
|
@ -15,10 +15,10 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Tab: Int {
|
enum Tab: Int {
|
||||||
case statuses, favourites, followedTags, postsAndReplies, media
|
case statuses, favourites, followedTags, postsAndReplies, media, lists
|
||||||
|
|
||||||
static var currentAccountTabs: [Tab] {
|
static var currentAccountTabs: [Tab] {
|
||||||
[.statuses, .favourites, .followedTags]
|
[.statuses, .favourites, .followedTags, .lists]
|
||||||
}
|
}
|
||||||
|
|
||||||
static var accountTabs: [Tab] {
|
static var accountTabs: [Tab] {
|
||||||
|
@ -28,10 +28,11 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
|
||||||
var title: String {
|
var title: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .statuses: return "Posts"
|
case .statuses: return "Posts"
|
||||||
case .favourites: return "Favourites"
|
case .favourites: return "Favorites"
|
||||||
case .followedTags: return "Followed Tags"
|
case .followedTags: return "Tags"
|
||||||
case .postsAndReplies: return "Posts & Replies"
|
case .postsAndReplies: return "Posts & Replies"
|
||||||
case .media: return "Media"
|
case .media: return "Media"
|
||||||
|
case .lists: return "Lists"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,6 +40,7 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
|
||||||
enum TabState {
|
enum TabState {
|
||||||
case followedTags(tags: [Tag])
|
case followedTags(tags: [Tag])
|
||||||
case statuses(statusesState: StatusesState)
|
case statuses(statusesState: StatusesState)
|
||||||
|
case lists
|
||||||
}
|
}
|
||||||
|
|
||||||
@Published var accountState: AccountState = .loading
|
@Published var accountState: AccountState = .loading
|
||||||
|
@ -77,7 +79,7 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var account: Account?
|
private(set) var account: Account?
|
||||||
private var tabTask: Task<Void, Never>?
|
private var tabTask: Task<Void, Never>?
|
||||||
|
|
||||||
private(set) var statuses: [Status] = []
|
private(set) var statuses: [Status] = []
|
||||||
|
@ -91,6 +93,7 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
|
||||||
/// When the account is already fetched by the parent caller.
|
/// When the account is already fetched by the parent caller.
|
||||||
init(account: Account) {
|
init(account: Account) {
|
||||||
self.accountId = account.id
|
self.accountId = account.id
|
||||||
|
self.account = account
|
||||||
self.accountState = .data(account: account)
|
self.accountState = .data(account: account)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -166,7 +169,7 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
|
||||||
(newFavourites, favouritesNextPage) = try await client.getWithLink(endpoint: Accounts.favourites(sinceId: nextPageId))
|
(newFavourites, favouritesNextPage) = try await client.getWithLink(endpoint: Accounts.favourites(sinceId: nextPageId))
|
||||||
favourites.append(contentsOf: newFavourites)
|
favourites.append(contentsOf: newFavourites)
|
||||||
tabState = .statuses(statusesState: .display(statuses: favourites, nextPageState: .hasNextPage))
|
tabState = .statuses(statusesState: .display(statuses: favourites, nextPageState: .hasNextPage))
|
||||||
case .followedTags:
|
case .followedTags, .lists:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
@ -201,6 +204,8 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
|
||||||
nextPageState: favouritesNextPage != nil ? .hasNextPage : .none))
|
nextPageState: favouritesNextPage != nil ? .hasNextPage : .none))
|
||||||
case .followedTags:
|
case .followedTags:
|
||||||
tabState = .followedTags(tags: followedTags)
|
tabState = .followedTags(tags: followedTags)
|
||||||
|
case .lists:
|
||||||
|
tabState = .lists
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,3 +13,19 @@ extension View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct StatusEditorToolbarItem: ToolbarContent {
|
||||||
|
@EnvironmentObject private var routerPath: RouterPath
|
||||||
|
|
||||||
|
public init() { }
|
||||||
|
|
||||||
|
public var body: some ToolbarContent {
|
||||||
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
|
Button {
|
||||||
|
routerPath.presentedSheet = .newStatusEditor
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "square.and.pencil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import Network
|
||||||
@MainActor
|
@MainActor
|
||||||
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] = []
|
||||||
|
|
||||||
private var client: Client?
|
private var client: Client?
|
||||||
|
|
||||||
|
@ -16,6 +17,7 @@ public class CurrentAccount: ObservableObject {
|
||||||
self.client = client
|
self.client = client
|
||||||
Task {
|
Task {
|
||||||
await fetchCurrentAccount()
|
await fetchCurrentAccount()
|
||||||
|
await fetchLists()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,4 +30,31 @@ public class CurrentAccount: ObservableObject {
|
||||||
account = try? await client.get(endpoint: Accounts.verifyCredentials)
|
account = try? await client.get(endpoint: Accounts.verifyCredentials)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func fetchLists() async {
|
||||||
|
guard let client, client.isAuth else { return }
|
||||||
|
do {
|
||||||
|
lists = try await client.get(endpoint: Lists.lists)
|
||||||
|
} catch {
|
||||||
|
lists = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func createList(title: String) async {
|
||||||
|
guard let client else { return }
|
||||||
|
do {
|
||||||
|
let list: Models.List = try await client.post(endpoint: Lists.createList(title: title))
|
||||||
|
lists.append(list)
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public func deleteList(list: Models.List) async {
|
||||||
|
guard let client else { return }
|
||||||
|
lists.removeAll(where: { $0.id == list.id })
|
||||||
|
let response = try? await client.delete(endpoint: Lists.list(id: list.id))
|
||||||
|
if response?.statusCode != 200 {
|
||||||
|
lists.append(list)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ public enum RouteurDestinations: Hashable {
|
||||||
case accountDetailWithAccount(account: Account)
|
case accountDetailWithAccount(account: Account)
|
||||||
case statusDetail(id: String)
|
case statusDetail(id: String)
|
||||||
case hashTag(tag: String, account: String?)
|
case hashTag(tag: String, account: String?)
|
||||||
|
case list(list: Models.List)
|
||||||
case followers(id: String)
|
case followers(id: String)
|
||||||
case following(id: String)
|
case following(id: String)
|
||||||
case favouritedBy(id: String)
|
case favouritedBy(id: String)
|
||||||
|
@ -19,11 +20,17 @@ 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 listEdit(list: Models.List)
|
||||||
|
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:
|
||||||
return "statusEditor"
|
return "statusEditor"
|
||||||
|
case .listEdit:
|
||||||
|
return "listEdit"
|
||||||
|
case .listAddAccount:
|
||||||
|
return "listAddAccount"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
9
Packages/Lists/.gitignore
vendored
Normal file
9
Packages/Lists/.gitignore
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
.DS_Store
|
||||||
|
/.build
|
||||||
|
/Packages
|
||||||
|
/*.xcodeproj
|
||||||
|
xcuserdata/
|
||||||
|
DerivedData/
|
||||||
|
.swiftpm/config/registries.json
|
||||||
|
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||||
|
.netrc
|
33
Packages/Lists/Package.swift
Normal file
33
Packages/Lists/Package.swift
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
// swift-tools-version: 5.7
|
||||||
|
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||||
|
|
||||||
|
import PackageDescription
|
||||||
|
|
||||||
|
let package = Package(
|
||||||
|
name: "Lists",
|
||||||
|
platforms: [
|
||||||
|
.iOS(.v16),
|
||||||
|
],
|
||||||
|
products: [
|
||||||
|
.library(
|
||||||
|
name: "Lists",
|
||||||
|
targets: ["Lists"]),
|
||||||
|
],
|
||||||
|
dependencies: [
|
||||||
|
.package(name: "Network", path: "../Network"),
|
||||||
|
.package(name: "Models", path: "../Models"),
|
||||||
|
.package(name: "Env", path: "../Env"),
|
||||||
|
.package(name: "DesignSystem", path: "../DesignSystem"),
|
||||||
|
],
|
||||||
|
targets: [
|
||||||
|
.target(
|
||||||
|
name: "Lists",
|
||||||
|
dependencies: [
|
||||||
|
.product(name: "Network", package: "Network"),
|
||||||
|
.product(name: "Models", package: "Models"),
|
||||||
|
.product(name: "Env", package: "Env"),
|
||||||
|
.product(name: "DesignSystem", package: "DesignSystem")
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
3
Packages/Lists/README.md
Normal file
3
Packages/Lists/README.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# Lists
|
||||||
|
|
||||||
|
A description of this package.
|
|
@ -0,0 +1,82 @@
|
||||||
|
import SwiftUI
|
||||||
|
import Network
|
||||||
|
import DesignSystem
|
||||||
|
import Env
|
||||||
|
import Models
|
||||||
|
|
||||||
|
public struct ListAddAccountView: View {
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
@EnvironmentObject private var client: Client
|
||||||
|
@EnvironmentObject private var theme: Theme
|
||||||
|
@EnvironmentObject private var currentAccount: CurrentAccount
|
||||||
|
@StateObject private var viewModel: ListAddAccountViewModel
|
||||||
|
|
||||||
|
@State private var isCreateListAlertPresented: Bool = false
|
||||||
|
@State private var createListTitle: String = ""
|
||||||
|
|
||||||
|
|
||||||
|
public init(account: Account) {
|
||||||
|
_viewModel = StateObject(wrappedValue: .init(account: account))
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
NavigationStack {
|
||||||
|
List {
|
||||||
|
ForEach(currentAccount.lists) { list in
|
||||||
|
HStack {
|
||||||
|
Toggle(list.title, isOn: .init(get: {
|
||||||
|
viewModel.inLists.contains(where: { $0.id == list.id })
|
||||||
|
}, set: { value in
|
||||||
|
Task {
|
||||||
|
if value {
|
||||||
|
await viewModel.addToList(list: list)
|
||||||
|
} else {
|
||||||
|
await viewModel.removeFromList(list: list)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.disabled(viewModel.isLoadingInfo)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
}
|
||||||
|
Button("Create a new list") {
|
||||||
|
isCreateListAlertPresented = true
|
||||||
|
}
|
||||||
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
}
|
||||||
|
.scrollContentBackground(.hidden)
|
||||||
|
.background(theme.secondaryBackgroundColor)
|
||||||
|
.navigationTitle("Add/Remove \(viewModel.account.displayName)")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem {
|
||||||
|
Button("Done") {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.alert("Create a new list", isPresented: $isCreateListAlertPresented) {
|
||||||
|
TextField("List name", text: $createListTitle)
|
||||||
|
Button("Cancel") {
|
||||||
|
isCreateListAlertPresented = false
|
||||||
|
createListTitle = ""
|
||||||
|
}
|
||||||
|
Button("Create List") {
|
||||||
|
guard !createListTitle.isEmpty else { return }
|
||||||
|
isCreateListAlertPresented = false
|
||||||
|
Task {
|
||||||
|
await currentAccount.createList(title: createListTitle)
|
||||||
|
createListTitle = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} message: {
|
||||||
|
Text("Enter the name for your list")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.task {
|
||||||
|
viewModel.client = client
|
||||||
|
await viewModel.fetchInfo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
import SwiftUI
|
||||||
|
import Models
|
||||||
|
import Network
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
class ListAddAccountViewModel: ObservableObject {
|
||||||
|
let account: Account
|
||||||
|
|
||||||
|
@Published var inLists: [Models.List] = []
|
||||||
|
@Published var isLoadingInfo: Bool = true
|
||||||
|
|
||||||
|
var client: Client?
|
||||||
|
|
||||||
|
init(account: Account) {
|
||||||
|
self.account = account
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchInfo() async {
|
||||||
|
guard let client else { return }
|
||||||
|
isLoadingInfo = true
|
||||||
|
do {
|
||||||
|
inLists = try await client.get(endpoint: Accounts.lists(id: account.id))
|
||||||
|
isLoadingInfo = false
|
||||||
|
} catch {
|
||||||
|
withAnimation {
|
||||||
|
isLoadingInfo = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func addToList(list: Models.List) async {
|
||||||
|
guard let client else { return }
|
||||||
|
let response = try? await client.post(endpoint: Lists.updateAccounts(listId: list.id, accounts: [account.id]))
|
||||||
|
if response?.statusCode == 200 {
|
||||||
|
inLists.append(list)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeFromList(list: Models.List) async {
|
||||||
|
guard let client else { return }
|
||||||
|
let response = try? await client.delete(endpoint: Lists.updateAccounts(listId: list.id, accounts: [account.id]))
|
||||||
|
if response?.statusCode == 200 {
|
||||||
|
inLists.removeAll(where: { $0.id == list.id })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
70
Packages/Lists/Sources/Lists/Edit/ListEditView.swift
Normal file
70
Packages/Lists/Sources/Lists/Edit/ListEditView.swift
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
import SwiftUI
|
||||||
|
import Models
|
||||||
|
import DesignSystem
|
||||||
|
import Network
|
||||||
|
|
||||||
|
public struct ListEditView: View {
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
@EnvironmentObject private var theme: Theme
|
||||||
|
@EnvironmentObject private var client: Client
|
||||||
|
|
||||||
|
@StateObject private var viewModel: ListEditViewModel
|
||||||
|
|
||||||
|
public init(list: Models.List) {
|
||||||
|
_viewModel = StateObject(wrappedValue: .init(list: list))
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
NavigationStack {
|
||||||
|
List {
|
||||||
|
Section("Users in this list") {
|
||||||
|
if viewModel.isLoadingAccounts {
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
ProgressView()
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
} else {
|
||||||
|
ForEach(viewModel.accounts) { account in
|
||||||
|
HStack {
|
||||||
|
AvatarView(url: account.avatar, size: .status)
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
account.displayNameWithEmojis
|
||||||
|
Text("@\(account.acct)")
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
.font(.footnote)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
}.onDelete { indexes in
|
||||||
|
if let index = indexes.first {
|
||||||
|
Task {
|
||||||
|
let account = viewModel.accounts[index]
|
||||||
|
await viewModel.delete(account: account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.scrollContentBackground(.hidden)
|
||||||
|
.background(theme.secondaryBackgroundColor)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem {
|
||||||
|
Button("Done") {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle(viewModel.list.title)
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.onAppear {
|
||||||
|
viewModel.client = client
|
||||||
|
Task {
|
||||||
|
await viewModel.fetchAccounts()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
38
Packages/Lists/Sources/Lists/Edit/ListEditViewModel.swift
Normal file
38
Packages/Lists/Sources/Lists/Edit/ListEditViewModel.swift
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import SwiftUI
|
||||||
|
import Models
|
||||||
|
import Network
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
public class ListEditViewModel: ObservableObject {
|
||||||
|
let list: Models.List
|
||||||
|
|
||||||
|
var client: Client?
|
||||||
|
|
||||||
|
@Published var isLoadingAccounts: Bool = true
|
||||||
|
@Published var accounts: [Account] = []
|
||||||
|
|
||||||
|
init(list: Models.List) {
|
||||||
|
self.list = list
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchAccounts() async {
|
||||||
|
guard let client else { return }
|
||||||
|
isLoadingAccounts = true
|
||||||
|
do {
|
||||||
|
accounts = try await client.get(endpoint: Lists.accounts(listId: list.id))
|
||||||
|
isLoadingAccounts = false
|
||||||
|
} catch {
|
||||||
|
isLoadingAccounts = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func delete(account: Account) async {
|
||||||
|
guard let client else { return }
|
||||||
|
do {
|
||||||
|
let response = try await client.delete(endpoint: Lists.updateAccounts(listId: list.id, accounts: [account.id]))
|
||||||
|
if response?.statusCode == 200 {
|
||||||
|
accounts.removeAll(where: { $0.id == account.id })
|
||||||
|
}
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
}
|
7
Packages/Models/Sources/Models/List.swift
Normal file
7
Packages/Models/Sources/Models/List.swift
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct List: Decodable, Identifiable, Equatable, Hashable {
|
||||||
|
public let id: String
|
||||||
|
public let title: String
|
||||||
|
public let repliesPolicy: String
|
||||||
|
}
|
|
@ -86,6 +86,13 @@ public class Client: ObservableObject, Equatable {
|
||||||
try await makeEntityRequest(endpoint: endpoint, method: "POST")
|
try await makeEntityRequest(endpoint: endpoint, method: "POST")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func post(endpoint: Endpoint) async throws -> HTTPURLResponse? {
|
||||||
|
let url = makeURL(endpoint: endpoint)
|
||||||
|
let request = makeURLRequest(url: url, httpMethod: "POST")
|
||||||
|
let (_, httpResponse) = try await urlSession.data(for: request)
|
||||||
|
return httpResponse as? HTTPURLResponse
|
||||||
|
}
|
||||||
|
|
||||||
public func put<Entity: Decodable>(endpoint: Endpoint) async throws -> Entity {
|
public func put<Entity: Decodable>(endpoint: Endpoint) async throws -> Entity {
|
||||||
try await makeEntityRequest(endpoint: endpoint, method: "PUT")
|
try await makeEntityRequest(endpoint: endpoint, method: "PUT")
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ public enum Accounts: Endpoint {
|
||||||
case suggestions
|
case suggestions
|
||||||
case followers(id: String, maxId: String?)
|
case followers(id: String, maxId: String?)
|
||||||
case following(id: String, maxId: String?)
|
case following(id: String, maxId: String?)
|
||||||
|
case lists(id: String)
|
||||||
|
|
||||||
public func path() -> String {
|
public func path() -> String {
|
||||||
switch self {
|
switch self {
|
||||||
|
@ -43,6 +44,8 @@ public enum Accounts: Endpoint {
|
||||||
return "accounts/\(id)/following"
|
return "accounts/\(id)/following"
|
||||||
case .followers(let id, _):
|
case .followers(let id, _):
|
||||||
return "accounts/\(id)/followers"
|
return "accounts/\(id)/followers"
|
||||||
|
case .lists(let id):
|
||||||
|
return "accounts/\(id)/lists"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
39
Packages/Network/Sources/Network/Endpoint/Lists.swift
Normal file
39
Packages/Network/Sources/Network/Endpoint/Lists.swift
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public enum Lists: Endpoint {
|
||||||
|
case lists
|
||||||
|
case list(id: String)
|
||||||
|
case createList(title: String)
|
||||||
|
case accounts(listId: String)
|
||||||
|
case updateAccounts(listId: String, accounts: [String])
|
||||||
|
|
||||||
|
public func path() -> String {
|
||||||
|
switch self {
|
||||||
|
case .lists, .createList:
|
||||||
|
return "lists"
|
||||||
|
case let .list(id):
|
||||||
|
return "lists/\(id)"
|
||||||
|
case let .accounts(listId):
|
||||||
|
return "lists/\(listId)/accounts"
|
||||||
|
case let .updateAccounts(listId, _):
|
||||||
|
return "lists/\(listId)/accounts"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func queryItems() -> [URLQueryItem]? {
|
||||||
|
switch self {
|
||||||
|
case .accounts(_):
|
||||||
|
return [.init(name: "limit", value: String(0))]
|
||||||
|
case let .createList(title):
|
||||||
|
return [.init(name: "title", value: title)]
|
||||||
|
case let .updateAccounts(_, accounts):
|
||||||
|
var params: [URLQueryItem] = []
|
||||||
|
for account in accounts {
|
||||||
|
params.append(.init(name: "account_ids[]", value: account))
|
||||||
|
}
|
||||||
|
return params
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ import Foundation
|
||||||
public enum Timelines: Endpoint {
|
public enum Timelines: Endpoint {
|
||||||
case pub(sinceId: String?, maxId: String?, minId: String?, local: Bool)
|
case pub(sinceId: String?, maxId: String?, minId: String?, local: Bool)
|
||||||
case home(sinceId: String?, maxId: String?, minId: String?)
|
case home(sinceId: String?, maxId: String?, minId: String?)
|
||||||
|
case list(listId: String, sinceId: String?, maxId: String?, minId: String?)
|
||||||
case hashtag(tag: String, maxId: String?)
|
case hashtag(tag: String, maxId: String?)
|
||||||
|
|
||||||
public func path() -> String {
|
public func path() -> String {
|
||||||
|
@ -11,6 +12,8 @@ public enum Timelines: Endpoint {
|
||||||
return "timelines/public"
|
return "timelines/public"
|
||||||
case .home:
|
case .home:
|
||||||
return "timelines/home"
|
return "timelines/home"
|
||||||
|
case let .list(listId, _, _, _):
|
||||||
|
return "timelines/list/\(listId)"
|
||||||
case let .hashtag(tag, _):
|
case let .hashtag(tag, _):
|
||||||
return "timelines/tag/\(tag)"
|
return "timelines/tag/\(tag)"
|
||||||
}
|
}
|
||||||
|
@ -24,6 +27,8 @@ public enum Timelines: Endpoint {
|
||||||
return params
|
return params
|
||||||
case .home(let sinceId, let maxId, let mindId):
|
case .home(let sinceId, let maxId, let mindId):
|
||||||
return makePaginationParam(sinceId: sinceId, maxId: maxId, mindId: mindId)
|
return makePaginationParam(sinceId: sinceId, maxId: maxId, mindId: mindId)
|
||||||
|
case .list(_, let sinceId, let maxId, let mindId):
|
||||||
|
return makePaginationParam(sinceId: sinceId, maxId: maxId, mindId: mindId)
|
||||||
case let .hashtag(_, maxId):
|
case let .hashtag(_, maxId):
|
||||||
return makePaginationParam(sinceId: nil, maxId: maxId, mindId: nil)
|
return makePaginationParam(sinceId: nil, maxId: maxId, mindId: nil)
|
||||||
}
|
}
|
||||||
|
|
|
@ -120,7 +120,7 @@ extension Models.Notification.NotificationType {
|
||||||
case .poll:
|
case .poll:
|
||||||
return "poll ended"
|
return "poll ended"
|
||||||
case .update:
|
case .update:
|
||||||
return "has been edited"
|
return "edited a post"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,9 @@ public struct StatusPollView: View {
|
||||||
|
|
||||||
private func percentForOption(option: Poll.Option) -> Int {
|
private func percentForOption(option: Poll.Option) -> Int {
|
||||||
let ratio = (Float(option.votesCount) / Float(viewModel.poll.votesCount)) * 100
|
let ratio = (Float(option.votesCount) / Float(viewModel.poll.votesCount)) * 100
|
||||||
|
if ratio.isNaN {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
return Int(round(ratio))
|
return Int(round(ratio))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import Network
|
||||||
public enum TimelineFilter: Hashable, Equatable {
|
public enum TimelineFilter: Hashable, Equatable {
|
||||||
case pub, local, home, trending
|
case pub, local, home, trending
|
||||||
case hashtag(tag: String, accountId: String?)
|
case hashtag(tag: String, accountId: String?)
|
||||||
|
case list(list: List)
|
||||||
|
|
||||||
public func hash(into hasher: inout Hasher) {
|
public func hash(into hasher: inout Hasher) {
|
||||||
hasher.combine(title())
|
hasher.combine(title())
|
||||||
|
@ -29,6 +30,8 @@ public enum TimelineFilter: Hashable, Equatable {
|
||||||
return "Home"
|
return "Home"
|
||||||
case let .hashtag(tag, _):
|
case let .hashtag(tag, _):
|
||||||
return "#\(tag)"
|
return "#\(tag)"
|
||||||
|
case let .list(list):
|
||||||
|
return list.title
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,6 +45,8 @@ public enum TimelineFilter: Hashable, Equatable {
|
||||||
return "chart.line.uptrend.xyaxis"
|
return "chart.line.uptrend.xyaxis"
|
||||||
case .home:
|
case .home:
|
||||||
return "house"
|
return "house"
|
||||||
|
case .list(_):
|
||||||
|
return "list.bullet"
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -53,7 +58,8 @@ public enum TimelineFilter: Hashable, Equatable {
|
||||||
case .local: return Timelines.pub(sinceId: sinceId, maxId: maxId, minId: minId, local: true)
|
case .local: return Timelines.pub(sinceId: sinceId, maxId: maxId, minId: minId, local: true)
|
||||||
case .home: return Timelines.home(sinceId: sinceId, maxId: maxId, minId: minId)
|
case .home: return Timelines.home(sinceId: sinceId, maxId: maxId, minId: minId)
|
||||||
case .trending: return Trends.statuses(offset: offset)
|
case .trending: return Trends.statuses(offset: offset)
|
||||||
case let .hashtag(tag, accountId):
|
case let .list(list): return Timelines.list(listId: list.id, sinceId: sinceId, maxId: maxId, minId: minId)
|
||||||
|
case let .hashtag(tag, accountId):
|
||||||
if let accountId {
|
if let accountId {
|
||||||
return Accounts.statuses(id: accountId, sinceId: nil, tag: tag, onlyMedia: nil, excludeReplies: nil)
|
return Accounts.statuses(id: accountId, sinceId: nil, tag: tag, onlyMedia: nil, excludeReplies: nil)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -16,6 +16,7 @@ public struct TimelineView: View {
|
||||||
@EnvironmentObject private var account: CurrentAccount
|
@EnvironmentObject private var account: CurrentAccount
|
||||||
@EnvironmentObject private var watcher: StreamWatcher
|
@EnvironmentObject private var watcher: StreamWatcher
|
||||||
@EnvironmentObject private var client: Client
|
@EnvironmentObject private var client: Client
|
||||||
|
@EnvironmentObject private var routerPath: RouterPath
|
||||||
|
|
||||||
@StateObject private var viewModel = TimelineViewModel()
|
@StateObject private var viewModel = TimelineViewModel()
|
||||||
|
|
||||||
|
@ -54,6 +55,22 @@ public struct TimelineView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle(timeline.title())
|
.navigationTitle(timeline.title())
|
||||||
|
.toolbar{
|
||||||
|
switch timeline {
|
||||||
|
case let .list(list):
|
||||||
|
ToolbarItem {
|
||||||
|
Button {
|
||||||
|
routerPath.presentedSheet = .listEdit(list: list)
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "pencil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
ToolbarItem {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.client = client
|
viewModel.client = client
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
# IceCubesApp
|
# IceCubesApp
|
||||||
|
|
||||||
|
[Public TestFlight beta](https://testflight.apple.com/join/tqI3dK1u) (An proper App Store release will come eventually)
|
||||||
|
|
||||||
<p float="left">
|
<p float="left">
|
||||||
<img src="Images/image1.png" width="300" />
|
<img src="Images/image1.png" width="300" />
|
||||||
<img src="Images/image2.png" width="300" />
|
<img src="Images/image2.png" width="300" />
|
||||||
|
@ -16,6 +18,7 @@
|
||||||
For contributors and myself, here is a todo list of features that could be added (while giving you a good idea of what's already done if not in this list, the app is quite complete already!)
|
For contributors and myself, here is a todo list of features that could be added (while giving you a good idea of what's already done if not in this list, the app is quite complete already!)
|
||||||
|
|
||||||
- [ ] DM / Conversations
|
- [ ] DM / Conversations
|
||||||
|
- [X] Lists support
|
||||||
- [ ] Display images alt
|
- [ ] Display images alt
|
||||||
- [ ] Editor: Post image alts
|
- [ ] Editor: Post image alts
|
||||||
- [ ] Editor: Add / Edit polls
|
- [ ] Editor: Add / Edit polls
|
||||||
|
@ -25,13 +28,14 @@ For contributors and myself, here is a todo list of features that could be added
|
||||||
- [ ] Better settings tab
|
- [ ] Better settings tab
|
||||||
- [ ] Edit profile
|
- [ ] Edit profile
|
||||||
- [X] Light theme
|
- [X] Light theme
|
||||||
- [ ] More themes
|
- [X] More themes
|
||||||
- [ ] Honor & display server side features (filter, default visibility, etc...)
|
- [ ] Honor & display server side features (filter, default visibility, etc...)
|
||||||
- [X] Open remote status locally
|
- [X] Open remote status locally
|
||||||
- [ ] More context menu everywhere
|
- [ ] More context menu everywhere
|
||||||
- [ ] Support pinned posts
|
- [ ] Support pinned posts
|
||||||
- [ ] Support IceCubesApp://any mastodon links
|
- [ ] Support IceCubesApp://any mastodon links
|
||||||
- [ ] Add a share sheet
|
- [ ] Add a share sheet
|
||||||
|
- [ ] Translate button
|
||||||
- [ ] Proper iPad support
|
- [ ] Proper iPad support
|
||||||
- [ ] macOS support
|
- [ ] macOS support
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue