mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-12-22 23:26:37 +00:00
OAuth + Home timeline
This commit is contained in:
parent
eb92379ac7
commit
df2d383b8a
20 changed files with 528 additions and 73 deletions
|
@ -13,8 +13,12 @@
|
||||||
9F398AA62935FE8A00A889F2 /* AppRouteur.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F398AA52935FE8A00A889F2 /* AppRouteur.swift */; };
|
9F398AA62935FE8A00A889F2 /* AppRouteur.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F398AA52935FE8A00A889F2 /* AppRouteur.swift */; };
|
||||||
9F398AA92935FFDB00A889F2 /* Account in Frameworks */ = {isa = PBXBuildFile; productRef = 9F398AA82935FFDB00A889F2 /* Account */; };
|
9F398AA92935FFDB00A889F2 /* Account in Frameworks */ = {isa = PBXBuildFile; productRef = 9F398AA82935FFDB00A889F2 /* Account */; };
|
||||||
9F398AAB2935FFDB00A889F2 /* Models in Frameworks */ = {isa = PBXBuildFile; productRef = 9F398AAA2935FFDB00A889F2 /* Models */; };
|
9F398AAB2935FFDB00A889F2 /* Models in Frameworks */ = {isa = PBXBuildFile; productRef = 9F398AAA2935FFDB00A889F2 /* Models */; };
|
||||||
9F398AB329360A4C00A889F2 /* TimelineTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F398AB229360A4C00A889F2 /* TimelineTabView.swift */; };
|
9F398AB329360A4C00A889F2 /* TimelineTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F398AB229360A4C00A889F2 /* TimelineTab.swift */; };
|
||||||
9FBFE63D292A715500C250E9 /* IceCubesAppApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBFE63C292A715500C250E9 /* IceCubesAppApp.swift */; };
|
9FAE4ACB293783B000772766 /* SettingsTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FAE4ACA293783B000772766 /* SettingsTab.swift */; };
|
||||||
|
9FAE4ACE29379A5A00772766 /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 9FAE4ACD29379A5A00772766 /* KeychainSwift */; };
|
||||||
|
9FAE4AD129379AD600772766 /* AppAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FAE4AD029379AD600772766 /* AppAccount.swift */; };
|
||||||
|
9FAE4AD32937A0C600772766 /* AppAccountsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FAE4AD22937A0C600772766 /* AppAccountsManager.swift */; };
|
||||||
|
9FBFE63D292A715500C250E9 /* IceCubesApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBFE63C292A715500C250E9 /* IceCubesApp.swift */; };
|
||||||
9FBFE641292A715600C250E9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9FBFE640292A715600C250E9 /* Assets.xcassets */; };
|
9FBFE641292A715600C250E9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9FBFE640292A715600C250E9 /* Assets.xcassets */; };
|
||||||
9FBFE64E292A72BD00C250E9 /* Network in Frameworks */ = {isa = PBXBuildFile; productRef = 9FBFE64D292A72BD00C250E9 /* Network */; };
|
9FBFE64E292A72BD00C250E9 /* Network in Frameworks */ = {isa = PBXBuildFile; productRef = 9FBFE64D292A72BD00C250E9 /* Network */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
@ -27,9 +31,13 @@
|
||||||
9F398AA32935F90100A889F2 /* Models */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Models; path = Packages/Models; sourceTree = "<group>"; };
|
9F398AA32935F90100A889F2 /* Models */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Models; path = Packages/Models; sourceTree = "<group>"; };
|
||||||
9F398AA52935FE8A00A889F2 /* AppRouteur.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRouteur.swift; sourceTree = "<group>"; };
|
9F398AA52935FE8A00A889F2 /* AppRouteur.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRouteur.swift; sourceTree = "<group>"; };
|
||||||
9F398AAC2936005300A889F2 /* Account */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Account; path = Packages/Account; sourceTree = "<group>"; };
|
9F398AAC2936005300A889F2 /* Account */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Account; path = Packages/Account; sourceTree = "<group>"; };
|
||||||
9F398AB229360A4C00A889F2 /* TimelineTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTabView.swift; sourceTree = "<group>"; };
|
9F398AB229360A4C00A889F2 /* TimelineTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTab.swift; sourceTree = "<group>"; };
|
||||||
|
9FAE4AC8293774FF00772766 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
9FAE4ACA293783B000772766 /* SettingsTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTab.swift; sourceTree = "<group>"; };
|
||||||
|
9FAE4AD029379AD600772766 /* AppAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppAccount.swift; sourceTree = "<group>"; };
|
||||||
|
9FAE4AD22937A0C600772766 /* AppAccountsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppAccountsManager.swift; sourceTree = "<group>"; };
|
||||||
9FBFE639292A715500C250E9 /* IceCubesApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = IceCubesApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
9FBFE639292A715500C250E9 /* IceCubesApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = IceCubesApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
9FBFE63C292A715500C250E9 /* IceCubesAppApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IceCubesAppApp.swift; sourceTree = "<group>"; };
|
9FBFE63C292A715500C250E9 /* IceCubesApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IceCubesApp.swift; sourceTree = "<group>"; };
|
||||||
9FBFE640292A715600C250E9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
9FBFE640292A715600C250E9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; 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>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
@ -39,6 +47,7 @@
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
9FAE4ACE29379A5A00772766 /* KeychainSwift in Frameworks */,
|
||||||
9F398AA92935FFDB00A889F2 /* Account in Frameworks */,
|
9F398AA92935FFDB00A889F2 /* Account in Frameworks */,
|
||||||
9FBFE64E292A72BD00C250E9 /* Network in Frameworks */,
|
9FBFE64E292A72BD00C250E9 /* Network in Frameworks */,
|
||||||
9F398AAB2935FFDB00A889F2 /* Models in Frameworks */,
|
9F398AAB2935FFDB00A889F2 /* Models in Frameworks */,
|
||||||
|
@ -53,9 +62,10 @@
|
||||||
9F398AB429360A5800A889F2 /* App */ = {
|
9F398AB429360A5800A889F2 /* App */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
9FBFE63C292A715500C250E9 /* IceCubesAppApp.swift */,
|
9FAE4ACF29379ACA00772766 /* AppAccounts */,
|
||||||
|
9FAE4AC9293783A200772766 /* Tabs */,
|
||||||
|
9FBFE63C292A715500C250E9 /* IceCubesApp.swift */,
|
||||||
9F398AA52935FE8A00A889F2 /* AppRouteur.swift */,
|
9F398AA52935FE8A00A889F2 /* AppRouteur.swift */,
|
||||||
9F398AB229360A4C00A889F2 /* TimelineTabView.swift */,
|
|
||||||
);
|
);
|
||||||
path = App;
|
path = App;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -69,6 +79,24 @@
|
||||||
path = Resources;
|
path = Resources;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
9FAE4AC9293783A200772766 /* Tabs */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
9F398AB229360A4C00A889F2 /* TimelineTab.swift */,
|
||||||
|
9FAE4ACA293783B000772766 /* SettingsTab.swift */,
|
||||||
|
);
|
||||||
|
path = Tabs;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
9FAE4ACF29379ACA00772766 /* AppAccounts */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
9FAE4AD029379AD600772766 /* AppAccount.swift */,
|
||||||
|
9FAE4AD22937A0C600772766 /* AppAccountsManager.swift */,
|
||||||
|
);
|
||||||
|
path = AppAccounts;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
9FBFE630292A715500C250E9 = {
|
9FBFE630292A715500C250E9 = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -94,6 +122,7 @@
|
||||||
9FBFE63B292A715500C250E9 /* IceCubesApp */ = {
|
9FBFE63B292A715500C250E9 /* IceCubesApp */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
9FAE4AC8293774FF00772766 /* Info.plist */,
|
||||||
9F398AB429360A5800A889F2 /* App */,
|
9F398AB429360A5800A889F2 /* App */,
|
||||||
9FBFE642292A715600C250E9 /* IceCubesApp.entitlements */,
|
9FBFE642292A715600C250E9 /* IceCubesApp.entitlements */,
|
||||||
9F398AB529360A6100A889F2 /* Resources */,
|
9F398AB529360A6100A889F2 /* Resources */,
|
||||||
|
@ -130,6 +159,7 @@
|
||||||
9F398AA82935FFDB00A889F2 /* Account */,
|
9F398AA82935FFDB00A889F2 /* Account */,
|
||||||
9F398AAA2935FFDB00A889F2 /* Models */,
|
9F398AAA2935FFDB00A889F2 /* Models */,
|
||||||
9F24EEBA293619210042359D /* Routeur */,
|
9F24EEBA293619210042359D /* Routeur */,
|
||||||
|
9FAE4ACD29379A5A00772766 /* KeychainSwift */,
|
||||||
);
|
);
|
||||||
productName = IceCubesApp;
|
productName = IceCubesApp;
|
||||||
productReference = 9FBFE639292A715500C250E9 /* IceCubesApp.app */;
|
productReference = 9FBFE639292A715500C250E9 /* IceCubesApp.app */;
|
||||||
|
@ -159,6 +189,9 @@
|
||||||
Base,
|
Base,
|
||||||
);
|
);
|
||||||
mainGroup = 9FBFE630292A715500C250E9;
|
mainGroup = 9FBFE630292A715500C250E9;
|
||||||
|
packageReferences = (
|
||||||
|
9FAE4ACC29379A5A00772766 /* XCRemoteSwiftPackageReference "keychain-swift" */,
|
||||||
|
);
|
||||||
productRefGroup = 9FBFE63A292A715500C250E9 /* Products */;
|
productRefGroup = 9FBFE63A292A715500C250E9 /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
|
@ -185,9 +218,12 @@
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
9F398AB329360A4C00A889F2 /* TimelineTabView.swift in Sources */,
|
9FAE4ACB293783B000772766 /* SettingsTab.swift in Sources */,
|
||||||
|
9FAE4AD32937A0C600772766 /* AppAccountsManager.swift in Sources */,
|
||||||
|
9F398AB329360A4C00A889F2 /* TimelineTab.swift in Sources */,
|
||||||
9F398AA62935FE8A00A889F2 /* AppRouteur.swift in Sources */,
|
9F398AA62935FE8A00A889F2 /* AppRouteur.swift in Sources */,
|
||||||
9FBFE63D292A715500C250E9 /* IceCubesAppApp.swift in Sources */,
|
9FBFE63D292A715500C250E9 /* IceCubesApp.swift in Sources */,
|
||||||
|
9FAE4AD129379AD600772766 /* AppAccount.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -316,6 +352,7 @@
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_FILE = IceCubesApp/Info.plist;
|
||||||
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
|
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
|
||||||
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
|
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
|
||||||
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
|
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
|
||||||
|
@ -354,6 +391,7 @@
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_FILE = IceCubesApp/Info.plist;
|
||||||
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
|
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
|
||||||
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
|
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
|
||||||
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
|
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
|
||||||
|
@ -402,6 +440,17 @@
|
||||||
};
|
};
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
|
/* Begin XCRemoteSwiftPackageReference section */
|
||||||
|
9FAE4ACC29379A5A00772766 /* XCRemoteSwiftPackageReference "keychain-swift" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/evgenyneu/keychain-swift";
|
||||||
|
requirement = {
|
||||||
|
branch = master;
|
||||||
|
kind = branch;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
9F24EEBA293619210042359D /* Routeur */ = {
|
9F24EEBA293619210042359D /* Routeur */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
@ -419,6 +468,11 @@
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
productName = Models;
|
productName = Models;
|
||||||
};
|
};
|
||||||
|
9FAE4ACD29379A5A00772766 /* KeychainSwift */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 9FAE4ACC29379A5A00772766 /* XCRemoteSwiftPackageReference "keychain-swift" */;
|
||||||
|
productName = KeychainSwift;
|
||||||
|
};
|
||||||
9FBFE64D292A72BD00C250E9 /* Network */ = {
|
9FBFE64D292A72BD00C250E9 /* Network */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
productName = Network;
|
productName = Network;
|
||||||
|
|
|
@ -8,6 +8,15 @@
|
||||||
"revision" : "00d7a9744bbd1e7762c587bbd248775e16345a65",
|
"revision" : "00d7a9744bbd1e7762c587bbd248775e16345a65",
|
||||||
"version" : "1.0.0"
|
"version" : "1.0.0"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "keychain-swift",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/evgenyneu/keychain-swift",
|
||||||
|
"state" : {
|
||||||
|
"branch" : "master",
|
||||||
|
"revision" : "32a99b537d1c6f3529a08257c28a5feb70c0c5af"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"version" : 2
|
"version" : 2
|
||||||
|
|
51
IceCubesApp/App/AppAccounts/AppAccount.swift
Normal file
51
IceCubesApp/App/AppAccounts/AppAccount.swift
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import SwiftUI
|
||||||
|
import Timeline
|
||||||
|
import Network
|
||||||
|
import KeychainSwift
|
||||||
|
import Models
|
||||||
|
|
||||||
|
struct AppAccount: Codable {
|
||||||
|
let server: String
|
||||||
|
let oauthToken: OauthToken?
|
||||||
|
|
||||||
|
var key: String {
|
||||||
|
if let oauthToken {
|
||||||
|
return "\(server):\(oauthToken.createdAt)"
|
||||||
|
} else {
|
||||||
|
return "\(server):anonymous:\(Date().timeIntervalSince1970)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func save() throws {
|
||||||
|
let encoder = JSONEncoder()
|
||||||
|
let data = try encoder.encode(self)
|
||||||
|
let keychain = KeychainSwift()
|
||||||
|
keychain.set(data, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func delete() {
|
||||||
|
KeychainSwift().delete(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func retrieveAll() throws -> [AppAccount] {
|
||||||
|
let keychain = KeychainSwift()
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
let keys = keychain.allKeys
|
||||||
|
var accounts: [AppAccount] = []
|
||||||
|
for key in keys {
|
||||||
|
if let data = keychain.getData(key) {
|
||||||
|
let account = try decoder.decode(AppAccount.self, from: data)
|
||||||
|
accounts.append(account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return accounts
|
||||||
|
}
|
||||||
|
|
||||||
|
static func deleteAll() {
|
||||||
|
let keychain = KeychainSwift()
|
||||||
|
let keys = keychain.allKeys
|
||||||
|
for key in keys {
|
||||||
|
keychain.delete(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
34
IceCubesApp/App/AppAccounts/AppAccountsManager.swift
Normal file
34
IceCubesApp/App/AppAccounts/AppAccountsManager.swift
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import SwiftUI
|
||||||
|
import Network
|
||||||
|
|
||||||
|
class AppAccountsManager: ObservableObject {
|
||||||
|
@Published var currentAccount: AppAccount {
|
||||||
|
didSet {
|
||||||
|
currentClient = .init(server: currentAccount.server,
|
||||||
|
oauthToken: currentAccount.oauthToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Published var currentClient: Client
|
||||||
|
|
||||||
|
init() {
|
||||||
|
var defaultAccount = AppAccount(server: IceCubesApp.defaultServer, oauthToken: nil)
|
||||||
|
do {
|
||||||
|
let keychainAccounts = try AppAccount.retrieveAll()
|
||||||
|
defaultAccount = keychainAccounts.first ?? defaultAccount
|
||||||
|
} catch {}
|
||||||
|
currentAccount = defaultAccount
|
||||||
|
currentClient = .init(server: defaultAccount.server, oauthToken: defaultAccount.oauthToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
func add(account: AppAccount) {
|
||||||
|
do {
|
||||||
|
try account.save()
|
||||||
|
currentAccount = account
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
func delete(account: AppAccount) {
|
||||||
|
account.delete()
|
||||||
|
currentAccount = AppAccount(server: IceCubesApp.defaultServer, oauthToken: nil)
|
||||||
|
}
|
||||||
|
}
|
28
IceCubesApp/App/IceCubesApp.swift
Normal file
28
IceCubesApp/App/IceCubesApp.swift
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import SwiftUI
|
||||||
|
import Timeline
|
||||||
|
import Network
|
||||||
|
import KeychainSwift
|
||||||
|
|
||||||
|
@main
|
||||||
|
struct IceCubesApp: App {
|
||||||
|
public static let defaultServer = "mastodon.world"
|
||||||
|
|
||||||
|
@StateObject private var appAccountsManager = AppAccountsManager()
|
||||||
|
|
||||||
|
var body: some Scene {
|
||||||
|
WindowGroup {
|
||||||
|
TabView {
|
||||||
|
TimelineTab()
|
||||||
|
.tabItem {
|
||||||
|
Label("Home", systemImage: "globe")
|
||||||
|
}
|
||||||
|
SettingsTabs()
|
||||||
|
.tabItem {
|
||||||
|
Label("Settings", systemImage: "gear")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.environmentObject(appAccountsManager)
|
||||||
|
.environmentObject(appAccountsManager.currentClient)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,40 +0,0 @@
|
||||||
import SwiftUI
|
|
||||||
import Timeline
|
|
||||||
import Network
|
|
||||||
|
|
||||||
@main
|
|
||||||
struct IceCubesAppApp: App {
|
|
||||||
@State private var tabs: [String] = ["mastodon.social"]
|
|
||||||
@State private var isServerSelectDisplayed: Bool = false
|
|
||||||
@State private var newServerURL: String = ""
|
|
||||||
|
|
||||||
var body: some Scene {
|
|
||||||
WindowGroup {
|
|
||||||
TabView {
|
|
||||||
ForEach(tabs, id: \.self) { tab in
|
|
||||||
TimelineTabView(tab: tab)
|
|
||||||
.tabItem {
|
|
||||||
Label(tab, systemImage: "globe")
|
|
||||||
}
|
|
||||||
.toolbar {
|
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
|
||||||
Button {
|
|
||||||
isServerSelectDisplayed.toggle()
|
|
||||||
} label: {
|
|
||||||
Image(systemName: "globe")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.alert("Connect to another server", isPresented: $isServerSelectDisplayed) {
|
|
||||||
TextField(tab, text: $newServerURL)
|
|
||||||
Button("Connect", action: {
|
|
||||||
tabs.append(newServerURL)
|
|
||||||
newServerURL = ""
|
|
||||||
})
|
|
||||||
Button("Cancel", role: .cancel, action: {})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
103
IceCubesApp/App/Tabs/SettingsTab.swift
Normal file
103
IceCubesApp/App/Tabs/SettingsTab.swift
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
import SwiftUI
|
||||||
|
import Timeline
|
||||||
|
import Routeur
|
||||||
|
import Network
|
||||||
|
import Account
|
||||||
|
import Models
|
||||||
|
|
||||||
|
struct SettingsTabs: View {
|
||||||
|
@Environment(\.openURL) private var openURL
|
||||||
|
@EnvironmentObject private var client: Client
|
||||||
|
@EnvironmentObject private var appAccountsManager: AppAccountsManager
|
||||||
|
|
||||||
|
@State private var signInInProgress = false
|
||||||
|
@State private var accountData: Account?
|
||||||
|
@State private var signInServer = IceCubesApp.defaultServer
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationStack {
|
||||||
|
Form {
|
||||||
|
Section("Account") {
|
||||||
|
if let accountData {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text(appAccountsManager.currentAccount.server)
|
||||||
|
.font(.headline)
|
||||||
|
Text(accountData.displayName)
|
||||||
|
Text(accountData.username)
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
}
|
||||||
|
signOutButton
|
||||||
|
} else {
|
||||||
|
TextField("Mastodon server", text: $signInServer)
|
||||||
|
signInButton
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onOpenURL(perform: { url in
|
||||||
|
Task {
|
||||||
|
await continueSignIn(url: url)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.navigationTitle(Text("Settings"))
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
}
|
||||||
|
.task {
|
||||||
|
if appAccountsManager.currentAccount.oauthToken != nil {
|
||||||
|
signInInProgress = true
|
||||||
|
await refreshAccountInfo()
|
||||||
|
signInInProgress = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var signInButton: some View {
|
||||||
|
Button {
|
||||||
|
signInInProgress = true
|
||||||
|
Task {
|
||||||
|
await signIn()
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
if signInInProgress {
|
||||||
|
ProgressView()
|
||||||
|
} else {
|
||||||
|
Text("Sign in")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var signOutButton: some View {
|
||||||
|
Button {
|
||||||
|
accountData = nil
|
||||||
|
appAccountsManager.delete(account: appAccountsManager.currentAccount)
|
||||||
|
} label: {
|
||||||
|
Text("Sign out").foregroundColor(.red)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private func signIn() async {
|
||||||
|
do {
|
||||||
|
client.server = signInServer
|
||||||
|
let oauthURL = try await client.oauthURL()
|
||||||
|
openURL(oauthURL)
|
||||||
|
} catch {
|
||||||
|
signInInProgress = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func continueSignIn(url: URL) async {
|
||||||
|
do {
|
||||||
|
let oauthToken = try await client.continueOauthFlow(url: url)
|
||||||
|
appAccountsManager.add(account: AppAccount(server: client.server, oauthToken: oauthToken))
|
||||||
|
await refreshAccountInfo()
|
||||||
|
signInInProgress = false
|
||||||
|
} catch {
|
||||||
|
signInInProgress = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func refreshAccountInfo() async {
|
||||||
|
accountData = try? await client.get(endpoint: Accounts.verifyCredentials)
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,23 +3,14 @@ import Timeline
|
||||||
import Routeur
|
import Routeur
|
||||||
import Network
|
import Network
|
||||||
|
|
||||||
struct TimelineTabView: View {
|
struct TimelineTab: View {
|
||||||
let tab: String
|
|
||||||
|
|
||||||
private let client: Client
|
|
||||||
@StateObject private var routeurPath = RouterPath()
|
@StateObject private var routeurPath = RouterPath()
|
||||||
|
|
||||||
init(tab: String) {
|
|
||||||
self.tab = tab
|
|
||||||
self.client = .init(server: tab)
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack(path: $routeurPath.path) {
|
NavigationStack(path: $routeurPath.path) {
|
||||||
TimelineView()
|
TimelineView()
|
||||||
.withAppRouteur()
|
.withAppRouteur()
|
||||||
}
|
}
|
||||||
.environmentObject(routeurPath)
|
.environmentObject(routeurPath)
|
||||||
.environmentObject(client)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
19
IceCubesApp/Info.plist
Normal file
19
IceCubesApp/Info.plist
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleURLTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string>icecubesapp</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>icecubesapp</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
|
@ -8,7 +8,7 @@ class AccountDetailViewModel: ObservableObject {
|
||||||
var client: Client = .init(server: "")
|
var client: Client = .init(server: "")
|
||||||
|
|
||||||
enum State {
|
enum State {
|
||||||
case loading, data(account: Models.Account), error(error: Error)
|
case loading, data(account: Account), error(error: Error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Published var state: State = .loading
|
@Published var state: State = .loading
|
||||||
|
@ -19,7 +19,7 @@ class AccountDetailViewModel: ObservableObject {
|
||||||
|
|
||||||
func fetchAccount() async {
|
func fetchAccount() async {
|
||||||
do {
|
do {
|
||||||
state = .data(account: try await client.fetch(endpoint: Network.Account.accounts(id: accountId)))
|
state = .data(account: try await client.get(endpoint: Accounts.accounts(id: accountId)))
|
||||||
} catch {
|
} catch {
|
||||||
state = .error(error: error)
|
state = .error(error: error)
|
||||||
}
|
}
|
||||||
|
|
11
Packages/Models/Sources/Models/App.swift
Normal file
11
Packages/Models/Sources/Models/App.swift
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct App: Codable, Identifiable {
|
||||||
|
public let id: String
|
||||||
|
public let name: String
|
||||||
|
public let website: URL?
|
||||||
|
public let redirectUri: String
|
||||||
|
public let clientId: String
|
||||||
|
public let clientSecret: String
|
||||||
|
public let vapidKey: String
|
||||||
|
}
|
8
Packages/Models/Sources/Models/OauthToken.swift
Normal file
8
Packages/Models/Sources/Models/OauthToken.swift
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct OauthToken: Codable {
|
||||||
|
public let accessToken: String
|
||||||
|
public let tokenType: String
|
||||||
|
public let scope: String
|
||||||
|
public let createdAt: Double
|
||||||
|
}
|
|
@ -13,11 +13,15 @@ let package = Package(
|
||||||
name: "Network",
|
name: "Network",
|
||||||
targets: ["Network"]),
|
targets: ["Network"]),
|
||||||
],
|
],
|
||||||
dependencies: [],
|
dependencies: [
|
||||||
|
.package(name: "Models", path: "../Models"),
|
||||||
|
],
|
||||||
targets: [
|
targets: [
|
||||||
.target(
|
.target(
|
||||||
name: "Network",
|
name: "Network",
|
||||||
dependencies: []),
|
dependencies: [
|
||||||
|
.product(name: "Models", package: "Models")
|
||||||
|
]),
|
||||||
.testTarget(
|
.testTarget(
|
||||||
name: "NetworkTests",
|
name: "NetworkTests",
|
||||||
dependencies: ["Network"]),
|
dependencies: ["Network"]),
|
||||||
|
|
|
@ -1,34 +1,102 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import Models
|
||||||
|
|
||||||
public class Client: ObservableObject {
|
public class Client: ObservableObject {
|
||||||
public enum Version: String {
|
public enum Version: String {
|
||||||
case v1
|
case v1
|
||||||
}
|
}
|
||||||
|
|
||||||
public let server: String
|
public enum OauthError: Error {
|
||||||
|
case missingApp
|
||||||
|
case invalidRedirectURL
|
||||||
|
}
|
||||||
|
|
||||||
|
public var server: String
|
||||||
public let version: Version
|
public let version: Version
|
||||||
private let urlSession: URLSession
|
private let urlSession: URLSession
|
||||||
private let decoder = JSONDecoder()
|
private let decoder = JSONDecoder()
|
||||||
|
|
||||||
public init(server: String, version: Version = .v1) {
|
/// Only used as a transitionary app while in the oauth flow.
|
||||||
|
private var oauthApp: Models.App?
|
||||||
|
|
||||||
|
private var oauthToken: OauthToken?
|
||||||
|
|
||||||
|
public var isAuth: Bool {
|
||||||
|
oauthToken != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(server: String, version: Version = .v1, oauthToken: OauthToken? = nil) {
|
||||||
self.server = server
|
self.server = server
|
||||||
self.version = version
|
self.version = version
|
||||||
self.urlSession = URLSession.shared
|
self.urlSession = URLSession.shared
|
||||||
self.decoder.keyDecodingStrategy = .convertFromSnakeCase
|
self.decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||||
|
self.oauthToken = oauthToken
|
||||||
}
|
}
|
||||||
|
|
||||||
private func makeURL(endpoint: Endpoint) -> URL {
|
private func makeURL(endpoint: Endpoint) -> URL {
|
||||||
var components = URLComponents()
|
var components = URLComponents()
|
||||||
components.scheme = "https"
|
components.scheme = "https"
|
||||||
components.host = server
|
components.host = server
|
||||||
|
if type(of: endpoint) == Oauth.self {
|
||||||
|
components.path += "/\(endpoint.path())"
|
||||||
|
} else {
|
||||||
components.path += "/api/\(version.rawValue)/\(endpoint.path())"
|
components.path += "/api/\(version.rawValue)/\(endpoint.path())"
|
||||||
|
}
|
||||||
components.queryItems = endpoint.queryItems()
|
components.queryItems = endpoint.queryItems()
|
||||||
return components.url!
|
return components.url!
|
||||||
}
|
}
|
||||||
|
|
||||||
public func fetch<Entity: Codable>(endpoint: Endpoint) async throws -> Entity {
|
private func makeURLRequest(url: URL, httpMethod: String) -> URLRequest {
|
||||||
let (data, _) = try await urlSession.data(from: makeURL(endpoint: endpoint))
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = httpMethod
|
||||||
|
if let oauthToken {
|
||||||
|
request.setValue("Bearer \(oauthToken.accessToken)", forHTTPHeaderField: "Authorization")
|
||||||
|
}
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
||||||
|
public func get<Entity: Decodable>(endpoint: Endpoint) async throws -> Entity {
|
||||||
|
let url = makeURL(endpoint: endpoint)
|
||||||
|
let request = makeURLRequest(url: url, httpMethod: "GET")
|
||||||
|
let (data, httpResponse) = try await urlSession.data(for: request)
|
||||||
|
logResponseOnError(httpResponse: httpResponse, data: data)
|
||||||
return try decoder.decode(Entity.self, from: data)
|
return try decoder.decode(Entity.self, from: data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func post<Entity: Decodable>(endpoint: Endpoint) async throws -> Entity {
|
||||||
|
let url = makeURL(endpoint: endpoint)
|
||||||
|
let request = makeURLRequest(url: url, httpMethod: "POST")
|
||||||
|
let (data, httpResponse) = try await urlSession.data(for: request)
|
||||||
|
logResponseOnError(httpResponse: httpResponse, data: data)
|
||||||
|
return try decoder.decode(Entity.self, from: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func oauthURL() async throws -> URL {
|
||||||
|
let app: Models.App = try await post(endpoint: Apps.registerApp)
|
||||||
|
self.oauthApp = app
|
||||||
|
return makeURL(endpoint: Oauth.authorize(clientId: app.clientId))
|
||||||
|
}
|
||||||
|
|
||||||
|
public func continueOauthFlow(url: URL) async throws -> OauthToken {
|
||||||
|
guard let app = oauthApp else {
|
||||||
|
throw OauthError.missingApp
|
||||||
|
}
|
||||||
|
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
|
||||||
|
let code = components.queryItems?.first(where: { $0.name == "code"})?.value else {
|
||||||
|
throw OauthError.invalidRedirectURL
|
||||||
|
}
|
||||||
|
let token: OauthToken = try await post(endpoint: Oauth.token(code: code,
|
||||||
|
clientId: app.clientId,
|
||||||
|
clientSecret: app.clientSecret))
|
||||||
|
self.oauthToken = token
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
|
private func logResponseOnError(httpResponse: URLResponse, data: Data) {
|
||||||
|
if let httpResponse = httpResponse as? HTTPURLResponse, httpResponse.statusCode > 299 {
|
||||||
|
print(httpResponse)
|
||||||
|
print(String(data: data, encoding: .utf8) ?? "")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public enum Account: Endpoint {
|
public enum Accounts: Endpoint {
|
||||||
case accounts(id: String)
|
case accounts(id: String)
|
||||||
|
case verifyCredentials
|
||||||
|
|
||||||
public func path() -> String {
|
public func path() -> String {
|
||||||
switch self {
|
switch self {
|
||||||
case .accounts(let id):
|
case .accounts(let id):
|
||||||
return "accounts/\(id)"
|
return "accounts/\(id)"
|
||||||
|
case .verifyCredentials:
|
||||||
|
return "accounts/verify_credentials"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
24
Packages/Network/Sources/Network/Endpoint/Apps.swift
Normal file
24
Packages/Network/Sources/Network/Endpoint/Apps.swift
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public enum Apps: Endpoint {
|
||||||
|
case registerApp
|
||||||
|
|
||||||
|
public func path() -> String {
|
||||||
|
switch self {
|
||||||
|
case .registerApp:
|
||||||
|
return "apps"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func queryItems() -> [URLQueryItem]? {
|
||||||
|
switch self {
|
||||||
|
case .registerApp:
|
||||||
|
return [
|
||||||
|
.init(name: "client_name", value: "IceCubesApp"),
|
||||||
|
.init(name: "redirect_uris", value: "icecubesapp://"),
|
||||||
|
.init(name: "scopes", value: "read write follow push"),
|
||||||
|
.init(name: "website", value: "https://github.com/Dimillian/IceCubesApp")
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
36
Packages/Network/Sources/Network/Endpoint/Oauth.swift
Normal file
36
Packages/Network/Sources/Network/Endpoint/Oauth.swift
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public enum Oauth: Endpoint {
|
||||||
|
case authorize(clientId: String)
|
||||||
|
case token(code: String, clientId: String, clientSecret: String)
|
||||||
|
|
||||||
|
public func path() -> String {
|
||||||
|
switch self {
|
||||||
|
case .authorize:
|
||||||
|
return "oauth/authorize"
|
||||||
|
case .token:
|
||||||
|
return "oauth/token"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func queryItems() -> [URLQueryItem]? {
|
||||||
|
switch self {
|
||||||
|
case let .authorize(clientId):
|
||||||
|
return [
|
||||||
|
.init(name: "response_type", value: "code"),
|
||||||
|
.init(name: "client_id", value: clientId),
|
||||||
|
.init(name: "redirect_uri", value: "icecubesapp://"),
|
||||||
|
.init(name: "scope", value: "read write follow push")
|
||||||
|
]
|
||||||
|
case let .token(code, clientId, clientSecret):
|
||||||
|
return [
|
||||||
|
.init(name: "grant_type", value: "authorization_code"),
|
||||||
|
.init(name: "client_id", value: clientId),
|
||||||
|
.init(name: "client_secret", value: clientSecret),
|
||||||
|
.init(name: "redirect_uri", value: "icecubesapp://"),
|
||||||
|
.init(name: "code", value: code),
|
||||||
|
.init(name: "scope", value: "read write follow push")
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,12 +1,15 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public enum Timeline: Endpoint {
|
public enum Timelines: Endpoint {
|
||||||
case pub(sinceId: String?)
|
case pub(sinceId: String?)
|
||||||
|
case home(sinceId: String?)
|
||||||
|
|
||||||
public func path() -> String {
|
public func path() -> String {
|
||||||
switch self {
|
switch self {
|
||||||
case .pub:
|
case .pub:
|
||||||
return "timelines/public"
|
return "timelines/public"
|
||||||
|
case .home:
|
||||||
|
return "timelines/home"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,6 +17,8 @@ public enum Timeline: Endpoint {
|
||||||
switch self {
|
switch self {
|
||||||
case .pub(let sinceId):
|
case .pub(let sinceId):
|
||||||
return [.init(name: "max_id", value: sinceId)]
|
return [.init(name: "max_id", value: sinceId)]
|
||||||
|
case .home(let sinceId):
|
||||||
|
return [.init(name: "max_id", value: sinceId)]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -33,8 +33,13 @@ public struct TimelineView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.listStyle(.plain)
|
.listStyle(.plain)
|
||||||
.navigationTitle("Public Timeline: \(viewModel.serverName)")
|
.navigationTitle("\(viewModel.serverName)")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
|
timelineFilterButton
|
||||||
|
}
|
||||||
|
}
|
||||||
.task {
|
.task {
|
||||||
viewModel.client = client
|
viewModel.client = client
|
||||||
if !didAppear {
|
if !didAppear {
|
||||||
|
@ -54,4 +59,19 @@ public struct TimelineView: View {
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var timelineFilterButton: some View {
|
||||||
|
Menu {
|
||||||
|
ForEach(TimelineViewModel.TimelineFilter.allCases, id: \.self) { filter in
|
||||||
|
Button {
|
||||||
|
viewModel.timeline = filter
|
||||||
|
} label: {
|
||||||
|
Text(filter.rawValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "line.3.horizontal.decrease.circle")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,10 +13,36 @@ class TimelineViewModel: ObservableObject {
|
||||||
case error(error: Error)
|
case error(error: Error)
|
||||||
}
|
}
|
||||||
|
|
||||||
var client: Client = .init(server: "")
|
enum TimelineFilter: String, CaseIterable {
|
||||||
|
case pub = "Public"
|
||||||
|
case home = "Home"
|
||||||
|
|
||||||
|
func endpoint(sinceId: String?) -> Timelines {
|
||||||
|
switch self {
|
||||||
|
case .pub: return .pub(sinceId: sinceId)
|
||||||
|
case .home: return .home(sinceId: sinceId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var client: Client = .init(server: "") {
|
||||||
|
didSet {
|
||||||
|
timeline = client.isAuth ? .home : .pub
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var statuses: [Status] = []
|
private var statuses: [Status] = []
|
||||||
|
|
||||||
@Published var state: State = .loading
|
@Published var state: State = .loading
|
||||||
|
@Published var timeline: TimelineFilter = .pub {
|
||||||
|
didSet {
|
||||||
|
if oldValue != timeline {
|
||||||
|
Task {
|
||||||
|
await refreshTimeline()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var serverName: String {
|
var serverName: String {
|
||||||
client.server
|
client.server
|
||||||
|
@ -24,7 +50,8 @@ class TimelineViewModel: ObservableObject {
|
||||||
|
|
||||||
func refreshTimeline() async {
|
func refreshTimeline() async {
|
||||||
do {
|
do {
|
||||||
statuses = try await client.fetch(endpoint: Timeline.pub(sinceId: nil))
|
state = .loading
|
||||||
|
statuses = try await client.get(endpoint: timeline.endpoint(sinceId: nil))
|
||||||
state = .display(statuses: statuses, nextPageState: .hasNextPage)
|
state = .display(statuses: statuses, nextPageState: .hasNextPage)
|
||||||
} catch {
|
} catch {
|
||||||
state = .error(error: error)
|
state = .error(error: error)
|
||||||
|
@ -35,7 +62,7 @@ class TimelineViewModel: ObservableObject {
|
||||||
do {
|
do {
|
||||||
guard let lastId = statuses.last?.id else { return }
|
guard let lastId = statuses.last?.id else { return }
|
||||||
state = .display(statuses: statuses, nextPageState: .loadingNextPage)
|
state = .display(statuses: statuses, nextPageState: .loadingNextPage)
|
||||||
let newStatuses: [Status] = try await client.fetch(endpoint: Timeline.pub(sinceId: lastId))
|
let newStatuses: [Status] = try await client.get(endpoint: timeline.endpoint(sinceId: lastId))
|
||||||
statuses.append(contentsOf: newStatuses)
|
statuses.append(contentsOf: newStatuses)
|
||||||
state = .display(statuses: statuses, nextPageState: .hasNextPage)
|
state = .display(statuses: statuses, nextPageState: .hasNextPage)
|
||||||
} catch {
|
} catch {
|
||||||
|
|
Loading…
Reference in a new issue