Activity, rework of Navigator and KeychainSwift
This commit is contained in:
parent
6ea7d3a620
commit
0c000dbc9d
@ -14,6 +14,7 @@ Threaded is a 100% free, made in SwiftUI, [#OpenSource](https://github.com/luma
|
||||
- [SwiftSoup](https://github.com/scinfu/SwiftSoup)
|
||||
- [Nuke](https://github.com/kean/Nuke)
|
||||
- [EmojiText](https://github.com/divadretlaw/EmojiText)
|
||||
- [KeychainSwift](https://github.com/evgenyneu/keychain-swift)
|
||||
|
||||
## To-do list
|
||||
|
||||
|
@ -15,6 +15,7 @@
|
||||
B93B67782B42E8F0000892E9 /* TextEmoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = B93B67772B42E8F0000892E9 /* TextEmoji.swift */; };
|
||||
B93B677A2B42EC51000892E9 /* MetaPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B93B67792B42EC51000892E9 /* MetaPicker.swift */; };
|
||||
B93B677C2B433A6E000892E9 /* PostingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B93B677B2B433A6E000892E9 /* PostingView.swift */; };
|
||||
B97491E32B6E96700098BC48 /* SymbolWidth.swift in Sources */ = {isa = PBXBuildFile; fileRef = B97491E22B6E96700098BC48 /* SymbolWidth.swift */; };
|
||||
B97BCE242B3DD8400044756D /* HapticManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B97BCE232B3DD8400044756D /* HapticManager.swift */; };
|
||||
B97BCE262B3DE5A10044756D /* AccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B97BCE252B3DE5A10044756D /* AccountView.swift */; };
|
||||
B97BCE282B3ED2A80044756D /* .gitignore in Resources */ = {isa = PBXBuildFile; fileRef = B97BCE272B3ED2A80044756D /* .gitignore */; };
|
||||
@ -39,6 +40,7 @@
|
||||
B9BED5162B5D5E6500C9B715 /* PostInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9BED5152B5D5E6500C9B715 /* PostInteractor.swift */; };
|
||||
B9BED5182B5D649C00C9B715 /* PostMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9BED5172B5D649C00C9B715 /* PostMenu.swift */; };
|
||||
B9BED51A2B5D662D00C9B715 /* ShareSheetController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9BED5192B5D662D00C9B715 /* ShareSheetController.swift */; };
|
||||
B9BF54072B6B6823004B24E7 /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = B9BF54062B6B6823004B24E7 /* KeychainSwift */; };
|
||||
B9CC45B82B40A2D6001E4FA5 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9CC45B72B40A2D6001E4FA5 /* AboutView.swift */; };
|
||||
B9CFC43B2B4F08C9004CFCB7 /* LaunchStoryboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B9CFC43A2B4F08C9004CFCB7 /* LaunchStoryboard.storyboard */; };
|
||||
B9D9C6C12B6A56E000C26A41 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9D9C6C02B6A56E000C26A41 /* Notification.swift */; };
|
||||
@ -107,6 +109,7 @@
|
||||
B93B67772B42E8F0000892E9 /* TextEmoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextEmoji.swift; sourceTree = "<group>"; };
|
||||
B93B67792B42EC51000892E9 /* MetaPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetaPicker.swift; sourceTree = "<group>"; };
|
||||
B93B677B2B433A6E000892E9 /* PostingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostingView.swift; sourceTree = "<group>"; };
|
||||
B97491E22B6E96700098BC48 /* SymbolWidth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SymbolWidth.swift; sourceTree = "<group>"; };
|
||||
B97BCE232B3DD8400044756D /* HapticManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticManager.swift; sourceTree = "<group>"; };
|
||||
B97BCE252B3DE5A10044756D /* AccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountView.swift; sourceTree = "<group>"; };
|
||||
B97BCE272B3ED2A80044756D /* .gitignore */ = {isa = PBXFileReference; lastKnownFileType = text; path = .gitignore; sourceTree = "<group>"; };
|
||||
@ -182,6 +185,7 @@
|
||||
B93B67762B42E8AB000892E9 /* EmojiText in Frameworks */,
|
||||
B9FB94842B2E20AF00D81C07 /* SwiftSoup in Frameworks */,
|
||||
B93B676D2B42C94F000892E9 /* Nuke in Frameworks */,
|
||||
B9BF54072B6B6823004B24E7 /* KeychainSwift in Frameworks */,
|
||||
B93B67732B42C94F000892E9 /* NukeVideo in Frameworks */,
|
||||
B93B676F2B42C94F000892E9 /* NukeExtensions in Frameworks */,
|
||||
B93B67712B42C94F000892E9 /* NukeUI in Frameworks */,
|
||||
@ -332,6 +336,7 @@
|
||||
B9BED5192B5D662D00C9B715 /* ShareSheetController.swift */,
|
||||
B9D9C6C42B6A587700C26A41 /* NotificationRow.swift */,
|
||||
B9D9C6C62B6A590F00C26A41 /* ProfilePicture.swift */,
|
||||
B97491E22B6E96700098BC48 /* SymbolWidth.swift */,
|
||||
);
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
@ -402,6 +407,7 @@
|
||||
B93B67702B42C94F000892E9 /* NukeUI */,
|
||||
B93B67722B42C94F000892E9 /* NukeVideo */,
|
||||
B93B67752B42E8AB000892E9 /* EmojiText */,
|
||||
B9BF54062B6B6823004B24E7 /* KeychainSwift */,
|
||||
);
|
||||
productName = Threaded;
|
||||
productReference = B9FB94572B2DEECE00D81C07 /* Threaded.app */;
|
||||
@ -456,6 +462,7 @@
|
||||
B9FB94822B2E20AF00D81C07 /* XCRemoteSwiftPackageReference "SwiftSoup" */,
|
||||
B93B676B2B42C94F000892E9 /* XCRemoteSwiftPackageReference "Nuke" */,
|
||||
B93B67742B42E8AB000892E9 /* XCRemoteSwiftPackageReference "EmojiText" */,
|
||||
B9BF54052B6B6823004B24E7 /* XCRemoteSwiftPackageReference "keychain-swift" */,
|
||||
);
|
||||
productRefGroup = B9FB94582B2DEECE00D81C07 /* Products */;
|
||||
projectDirPath = "";
|
||||
@ -525,6 +532,7 @@
|
||||
B9FB94722B2DF49700D81C07 /* ConnectView.swift in Sources */,
|
||||
B9FB945B2B2DEECE00D81C07 /* ThreadedApp.swift in Sources */,
|
||||
B9FB94862B2E211200D81C07 /* Account+Elms.swift in Sources */,
|
||||
B97491E32B6E96700098BC48 /* SymbolWidth.swift in Sources */,
|
||||
B9FB94BC2B2F035500D81C07 /* Tag.swift in Sources */,
|
||||
B9BED51A2B5D662D00C9B715 /* ShareSheetController.swift in Sources */,
|
||||
B9BED5162B5D5E6500C9B715 /* PostInteractor.swift in Sources */,
|
||||
@ -887,6 +895,14 @@
|
||||
minimumVersion = 3.3.0;
|
||||
};
|
||||
};
|
||||
B9BF54052B6B6823004B24E7 /* XCRemoteSwiftPackageReference "keychain-swift" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/evgenyneu/keychain-swift?tab=readme-ov-file#usage";
|
||||
requirement = {
|
||||
kind = upToNextMinorVersion;
|
||||
minimumVersion = 21.0.0;
|
||||
};
|
||||
};
|
||||
B9FB94822B2E20AF00D81C07 /* XCRemoteSwiftPackageReference "SwiftSoup" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/scinfu/SwiftSoup";
|
||||
@ -923,6 +939,11 @@
|
||||
package = B93B67742B42E8AB000892E9 /* XCRemoteSwiftPackageReference "EmojiText" */;
|
||||
productName = EmojiText;
|
||||
};
|
||||
B9BF54062B6B6823004B24E7 /* KeychainSwift */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = B9BF54052B6B6823004B24E7 /* XCRemoteSwiftPackageReference "keychain-swift" */;
|
||||
productName = KeychainSwift;
|
||||
};
|
||||
B9FB94832B2E20AF00D81C07 /* SwiftSoup */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = B9FB94822B2E20AF00D81C07 /* XCRemoteSwiftPackageReference "SwiftSoup" */;
|
||||
|
@ -9,6 +9,15 @@
|
||||
"version" : "3.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "keychain-swift?tab=readme-ov-file#usage",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/evgenyneu/keychain-swift?tab=readme-ov-file#usage",
|
||||
"state" : {
|
||||
"revision" : "265806607b45687a3d646e4c9837c31c90f202e8",
|
||||
"version" : "21.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "nuke",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
@ -3,38 +3,68 @@
|
||||
import SwiftUI
|
||||
|
||||
struct NotificationRow: View {
|
||||
@Environment(Navigator.self) private var navigator: Navigator
|
||||
|
||||
var notif: Notification = .placeholder()
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
HStack(spacing: 5) {
|
||||
ProfilePicture(url: notif.account.avatar)
|
||||
ProfilePicture(url: notif.account.avatar, size: 60)
|
||||
.padding(.trailing)
|
||||
.overlay(alignment: .bottomTrailing) {
|
||||
notifIcon()
|
||||
.offset(x: -5, y: 5)
|
||||
}
|
||||
.padding()
|
||||
Text(localizedString())
|
||||
.padding(.horizontal, 10)
|
||||
.onTapGesture {
|
||||
navigator.navigate(to: .account(acc: notif.account))
|
||||
}
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text(localizedString())
|
||||
.multilineTextAlignment(.leading)
|
||||
.font(.subheadline)
|
||||
.lineLimit(2)
|
||||
|
||||
if notif.status != nil {
|
||||
TextEmoji(notif.status!.content, emojis: notif.status!.emojis)
|
||||
.multilineTextAlignment(.leading)
|
||||
.font(.caption)
|
||||
.foregroundStyle(Color.gray)
|
||||
.lineLimit(3, reservesSpace: true)
|
||||
} else {
|
||||
TextEmoji(notif.account.note, emojis: notif.account.emojis)
|
||||
.multilineTextAlignment(.leading)
|
||||
.font(.caption)
|
||||
.foregroundStyle(Color.gray)
|
||||
.lineLimit(3, reservesSpace: true)
|
||||
}
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
navigator.navigate(to: notif.status == nil ? .account(acc: notif.account) : .post(status: notif.status!))
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
}
|
||||
|
||||
private func localizedString() -> String {
|
||||
private func localizedString() -> LocalizedStringKey {
|
||||
let nameStr = "@\(notif.account.username)"
|
||||
switch (notif.supportedType) {
|
||||
case .favourite:
|
||||
return String(localized: "activity.favorite.%@").replacingOccurrences(of: "%@", with: "@\(notif.account.username)")
|
||||
return "activity.favorite.\(nameStr)"
|
||||
case .follow:
|
||||
return String(localized: "activity.followed.%@").replacingOccurrences(of: "%@", with: "@\(notif.account.username)")
|
||||
return "activity.followed.\(nameStr)"
|
||||
case .mention:
|
||||
return String(localized: "activity.mentionned.%@").replacingOccurrences(of: "%@", with: "@\(notif.account.username)")
|
||||
return "activity.mentionned.\(nameStr)"
|
||||
case .reblog:
|
||||
return String(localized: "activity.reblogged.%@").replacingOccurrences(of: "%@", with: "@\(notif.account.username)")
|
||||
return "activity.reblogged.\(nameStr)"
|
||||
case .status:
|
||||
return String(localized: "activity.status.%@").replacingOccurrences(of: "%@", with: "@\(notif.account.username)")
|
||||
return "activity.status.\(nameStr)"
|
||||
default:
|
||||
return String(localized: "activity.unknown")
|
||||
return "activity.unknown" // follow requests & polls
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,29 +87,32 @@ struct NotificationRow: View {
|
||||
|
||||
@ViewBuilder
|
||||
private func notifIcon() -> some View {
|
||||
let font: Font = .system(size: 12, weight: .regular, design: .monospaced)
|
||||
|
||||
ZStack {
|
||||
switch (notif.supportedType) {
|
||||
case .favourite:
|
||||
Image(systemName: "heart.fill")
|
||||
.font(.caption)
|
||||
.font(font)
|
||||
case .follow:
|
||||
Image(systemName: "person.fill.badge.plus")
|
||||
.font(.caption)
|
||||
.font(font)
|
||||
case .mention:
|
||||
Image(systemName: "tag.fill")
|
||||
.font(.caption)
|
||||
.font(font)
|
||||
case .reblog:
|
||||
Image(systemName: "bolt.horizontal.fill")
|
||||
.font(.caption)
|
||||
.font(font)
|
||||
case .status:
|
||||
Image(systemName: "text.badge.plus")
|
||||
.font(.caption)
|
||||
.font(font)
|
||||
default:
|
||||
Image(systemName: "questionmark")
|
||||
.font(.caption)
|
||||
.font(font)
|
||||
}
|
||||
}
|
||||
.padding(5)
|
||||
.frame(minWidth: 30)
|
||||
.padding(7)
|
||||
.background(notifColor())
|
||||
.clipShape(.circle)
|
||||
.overlay {
|
||||
|
@ -4,8 +4,8 @@ import SwiftUI
|
||||
|
||||
struct CompactPostView: View {
|
||||
@Environment(AccountManager.self) private var accountManager: AccountManager
|
||||
@Environment(Navigator.self) private var navigator: Navigator
|
||||
@State var status: Status
|
||||
@ObservedObject var navigator: Navigator
|
||||
|
||||
var pinned: Bool = false
|
||||
var detailed: Bool = false
|
||||
@ -23,7 +23,7 @@ struct CompactPostView: View {
|
||||
@State private var quoteStatus: Status? = nil
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
VStack(alignment: .leading) {
|
||||
notices
|
||||
|
||||
statusPost(status.reblogAsAsStatus ?? status)
|
||||
@ -35,6 +35,7 @@ struct CompactPostView: View {
|
||||
.padding(.bottom, 3)
|
||||
}
|
||||
}
|
||||
.environment(navigator)
|
||||
.onAppear {
|
||||
do {
|
||||
preferences = try UserPreferences.loadAsCurrent() ?? UserPreferences.defaultPreferences
|
||||
@ -205,17 +206,8 @@ struct CompactPostView: View {
|
||||
}
|
||||
|
||||
var profilePicture: some View {
|
||||
if status.reblog != nil {
|
||||
OnlineImage(url: status.reblog!.account.avatar, size: 50, useNuke: true)
|
||||
.frame(width: 40, height: 40)
|
||||
.padding(.horizontal)
|
||||
.clipShape(.circle)
|
||||
} else {
|
||||
OnlineImage(url: status.account.avatar, size: 50, useNuke: true)
|
||||
.frame(width: 40, height: 40)
|
||||
.padding(.horizontal)
|
||||
.clipShape(.circle)
|
||||
}
|
||||
ProfilePicture(url: status.reblog?.account.avatar ?? status.account.avatar)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
|
||||
var stats: some View {
|
||||
|
@ -4,7 +4,7 @@ import SwiftUI
|
||||
|
||||
struct PostInteractor: View {
|
||||
@Environment(AccountManager.self) private var accountManager
|
||||
@Environment(Navigator.self) private var navigator
|
||||
@Environment(UniversalNavigator.self) private var navigator
|
||||
|
||||
var status: Status
|
||||
|
||||
@ -56,12 +56,16 @@ struct PostInteractor: View {
|
||||
}
|
||||
.padding(.top)
|
||||
.onAppear {
|
||||
isLiked = status.reblog != nil ? status.reblog!.favourited ?? false : status.favourited ?? false
|
||||
isReposted = status.reblog != nil ? status.reblog!.reblogged ?? false : status.reblogged ?? false
|
||||
isBookmarked = status.reblog != nil ? status.reblog!.bookmarked ?? false : status.bookmarked ?? false
|
||||
syncInteractors(status: status)
|
||||
}
|
||||
}
|
||||
|
||||
func syncInteractors(status: Status) {
|
||||
isLiked = status.reblog != nil ? status.reblog!.favourited ?? false : status.favourited ?? false
|
||||
isReposted = status.reblog != nil ? status.reblog!.reblogged ?? false : status.reblogged ?? false
|
||||
isBookmarked = status.reblog != nil ? status.reblog!.bookmarked ?? false : status.bookmarked ?? false
|
||||
}
|
||||
|
||||
func likePost() async throws {
|
||||
if let client = accountManager.getClient() {
|
||||
guard client.isAuth else { fatalError("Client is not authenticated") }
|
||||
@ -70,9 +74,7 @@ struct PostInteractor: View {
|
||||
|
||||
isLiked = !isLiked
|
||||
let newStatus: Status = try await client.post(endpoint: endpoint)
|
||||
if isLiked != newStatus.favourited {
|
||||
isLiked = newStatus.favourited ?? !isLiked
|
||||
}
|
||||
syncInteractors(status: status)
|
||||
}
|
||||
}
|
||||
|
||||
@ -84,9 +86,7 @@ struct PostInteractor: View {
|
||||
|
||||
isReposted = !isReposted
|
||||
let newStatus: Status = try await client.post(endpoint: endpoint)
|
||||
if isReposted != newStatus.reblogged {
|
||||
isReposted = newStatus.reblogged ?? !isReposted
|
||||
}
|
||||
syncInteractors(status: status)
|
||||
}
|
||||
}
|
||||
|
||||
@ -98,9 +98,7 @@ struct PostInteractor: View {
|
||||
|
||||
isBookmarked = !isBookmarked
|
||||
let newStatus: Status = try await client.post(endpoint: endpoint)
|
||||
if isBookmarked != newStatus.bookmarked {
|
||||
isBookmarked = newStatus.bookmarked ?? !isBookmarked
|
||||
}
|
||||
syncInteractors(status: status)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -66,13 +66,15 @@ struct PostMenu: View {
|
||||
Image(systemName: "ellipsis")
|
||||
.foregroundStyle(Color.white.opacity(0.3))
|
||||
.font(.body)
|
||||
.contentShape(Rectangle())
|
||||
.padding(7.5)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func createImage() {
|
||||
let view = HStack {
|
||||
CompactPostView(status: status, navigator: Navigator(), imaging: true)
|
||||
CompactPostView(status: status, imaging: true)
|
||||
.padding(15)
|
||||
.background(Color.appBackground)
|
||||
}
|
||||
|
@ -7,7 +7,8 @@ struct QuotePostView: View {
|
||||
var status: Status
|
||||
|
||||
var body: some View {
|
||||
CompactPostView(status: status, navigator: navigator, quoted: true)
|
||||
CompactPostView(status: status, quoted: true)
|
||||
.environment(navigator)
|
||||
.frame(maxWidth: 250, maxHeight: 200)
|
||||
.padding(15)
|
||||
.padding([.horizontal], 20)
|
||||
|
@ -3,10 +3,17 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ProfilePicture: View {
|
||||
@Environment(UserPreferences.self) private var pref
|
||||
@EnvironmentObject private var pref: UserPreferences
|
||||
var url: URL
|
||||
var size: CGFloat = 50.0
|
||||
|
||||
init(url: URL, size: CGFloat = 50.0) {
|
||||
self.url = url
|
||||
self.size = size
|
||||
}
|
||||
|
||||
var cornerRadius: CGFloat {
|
||||
return pref.profilePictureShape == .circle ? (50 / 2) : 15.0
|
||||
return pref.profilePictureShape == .circle ? (50 / 2) : 10.0
|
||||
}
|
||||
|
||||
init(url: URL) {
|
||||
@ -18,8 +25,8 @@ struct ProfilePicture: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
OnlineImage(url: url, size: 50, useNuke: true)
|
||||
.frame(width: 40, height: 40)
|
||||
OnlineImage(url: url, size: size, useNuke: true)
|
||||
.frame(width: size - 10, height: size - 10)
|
||||
.clipShape(RoundedRectangle(cornerRadius: cornerRadius))
|
||||
}
|
||||
}
|
||||
|
31
Threaded/Components/SymbolWidth.swift
Normal file
31
Threaded/Components/SymbolWidth.swift
Normal file
@ -0,0 +1,31 @@
|
||||
//Made by Lumaa
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct SymbolWidthPreferenceKey: PreferenceKey {
|
||||
static var defaultValue: Double = 0
|
||||
|
||||
static func reduce(value: inout Double, nextValue: () -> Double) {
|
||||
value = max(value, nextValue())
|
||||
}
|
||||
}
|
||||
|
||||
struct SymbolWidthModifier: ViewModifier {
|
||||
@Binding var width: Double
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.background(GeometryReader { geo in
|
||||
Color
|
||||
.clear
|
||||
.preference(key: SymbolWidthPreferenceKey.self, value: geo.size.width)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
extension Image {
|
||||
func sync(with width: Binding<Double>) -> some View {
|
||||
modifier(SymbolWidthModifier(width: width))
|
||||
}
|
||||
}
|
@ -3,14 +3,15 @@
|
||||
import SwiftUI
|
||||
|
||||
struct TabsView: View {
|
||||
@State var navigator: Navigator
|
||||
@Binding var selectedTab: TabDestination
|
||||
var postButton: () -> Void
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center) {
|
||||
Button {
|
||||
navigator.selectedTab = .timeline
|
||||
selectedTab = .timeline
|
||||
} label: {
|
||||
if navigator.selectedTab == .timeline {
|
||||
if selectedTab == .timeline {
|
||||
Tabs.timeline.imageFill
|
||||
} else {
|
||||
Tabs.timeline.image
|
||||
@ -21,9 +22,9 @@ struct TabsView: View {
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
navigator.selectedTab = .search
|
||||
selectedTab = .search
|
||||
} label: {
|
||||
if navigator.selectedTab == .search {
|
||||
if selectedTab == .search {
|
||||
Tabs.search.imageFill
|
||||
} else {
|
||||
Tabs.search.image
|
||||
@ -34,7 +35,7 @@ struct TabsView: View {
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
navigator.presentedSheet = .post()
|
||||
postButton()
|
||||
} label: {
|
||||
Tabs.post.image
|
||||
}
|
||||
@ -43,9 +44,9 @@ struct TabsView: View {
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
navigator.selectedTab = .activity
|
||||
selectedTab = .activity
|
||||
} label: {
|
||||
if navigator.selectedTab == .activity {
|
||||
if selectedTab == .activity {
|
||||
Tabs.activity.imageFill
|
||||
} else {
|
||||
Tabs.activity.image
|
||||
@ -56,9 +57,9 @@ struct TabsView: View {
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
navigator.selectedTab = .profile
|
||||
selectedTab = .profile
|
||||
} label: {
|
||||
if navigator.selectedTab == .profile {
|
||||
if selectedTab == .profile {
|
||||
Tabs.profile.imageFill
|
||||
} else {
|
||||
Tabs.profile.image
|
||||
@ -130,8 +131,3 @@ extension Image {
|
||||
.opacity(neutral ? 0.3 : 1)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
TabsView(navigator: Navigator())
|
||||
.previewLayout(.sizeThatFits)
|
||||
}
|
||||
|
@ -1,8 +1,7 @@
|
||||
//Made by Lumaa
|
||||
|
||||
import Foundation
|
||||
|
||||
//TODO: Change this to SwiftData
|
||||
import KeychainSwift
|
||||
|
||||
@Observable
|
||||
public class AccountManager {
|
||||
@ -52,7 +51,13 @@ public struct AppAccount: Codable, Identifiable, Hashable {
|
||||
public let server: String
|
||||
public var accountName: String?
|
||||
public let oauthToken: OauthToken?
|
||||
|
||||
private static let saveKey: String = "threaded-appaccount.current"
|
||||
private static var keychain: KeychainSwift {
|
||||
let kc = KeychainSwift()
|
||||
// synchronise later
|
||||
return kc
|
||||
}
|
||||
|
||||
public var key: String {
|
||||
if let oauthToken {
|
||||
@ -66,32 +71,40 @@ public struct AppAccount: Codable, Identifiable, Hashable {
|
||||
key
|
||||
}
|
||||
|
||||
public init(server: String,
|
||||
accountName: String?,
|
||||
oauthToken: OauthToken? = nil)
|
||||
{
|
||||
public init(server: String, accountName: String?, oauthToken: OauthToken? = nil) {
|
||||
self.server = server
|
||||
self.accountName = accountName
|
||||
self.oauthToken = oauthToken
|
||||
}
|
||||
|
||||
func saveAsCurrent() throws {
|
||||
public static func clear() {
|
||||
Self.keychain.delete(Self.saveKey)
|
||||
}
|
||||
|
||||
public func clear() {
|
||||
Self.clear()
|
||||
}
|
||||
|
||||
public func saveAsCurrent(_ appAccount: AppAccount? = nil) {
|
||||
let encoder = JSONEncoder()
|
||||
let json = try encoder.encode(self)
|
||||
UserDefaults.standard.setValue(json, forKey: AppAccount.saveKey)
|
||||
}
|
||||
|
||||
static func loadAsCurrent() throws -> AppAccount? {
|
||||
let decoder = JSONDecoder()
|
||||
if let data = UserDefaults.standard.data(forKey: AppAccount.saveKey) {
|
||||
let account = try decoder.decode(AppAccount.self, from: data)
|
||||
return account
|
||||
if let data = try? encoder.encode(appAccount ?? self) {
|
||||
Self.keychain.set(data, forKey: Self.saveKey, withAccess: .accessibleWhenUnlocked)
|
||||
} else {
|
||||
fatalError("Couldn't encode AppAccount correctly to save")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
static func clear() {
|
||||
UserDefaults.standard.removeObject(forKey: AppAccount.saveKey)
|
||||
public static func loadAsCurrent(_ data: Data? = nil) -> Self? {
|
||||
let decoder = JSONDecoder()
|
||||
if let newData = data ?? keychain.getData(Self.saveKey) {
|
||||
if let decoded = try? decoder.decode(Self.self, from: newData) {
|
||||
return decoded
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
import Foundation
|
||||
|
||||
public enum AppInfo {
|
||||
public static let scopes = "read write follow"
|
||||
public static let scopes = "read write follow push"
|
||||
public static let scheme = "threaded://"
|
||||
public static let clientName = "ThreadedApp"
|
||||
public static let defaultServer = "mastodon.social"
|
||||
|
@ -92,10 +92,7 @@ public final class Client: Equatable, Identifiable, Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
private func makeURL(scheme: String = "https",
|
||||
endpoint: Endpoint,
|
||||
forceVersion: Version? = nil,
|
||||
forceServer: String? = nil) throws -> URL
|
||||
private func makeURL(scheme: String = "https", endpoint: Endpoint, forceVersion: Version? = nil, forceServer: String? = nil) throws -> URL
|
||||
{
|
||||
var components = URLComponents()
|
||||
components.scheme = scheme
|
||||
@ -220,9 +217,7 @@ public final class Client: Equatable, Identifiable, Hashable {
|
||||
else {
|
||||
throw OauthError.invalidRedirectURL
|
||||
}
|
||||
let token: OauthToken = try await post(endpoint: Oauth.token(code: code,
|
||||
clientId: app.clientId,
|
||||
clientSecret: app.clientSecret))
|
||||
let token: OauthToken = try await post(endpoint: Oauth.token(code: code, clientId: app.clientId, clientSecret: app.clientSecret))
|
||||
critical.withLock { $0.oauthToken = token }
|
||||
return token
|
||||
}
|
||||
@ -236,12 +231,7 @@ public final class Client: Equatable, Identifiable, Hashable {
|
||||
return urlSession.webSocketTask(with: url, protocols: subprotocols)
|
||||
}
|
||||
|
||||
public func mediaUpload<Entity: Decodable>(endpoint: Endpoint,
|
||||
version: Version,
|
||||
method: String,
|
||||
mimeType: String,
|
||||
filename: String,
|
||||
data: Data) async throws -> Entity
|
||||
public func mediaUpload<Entity: Decodable>(endpoint: Endpoint, version: Version, method: String, mimeType: String, filename: String, data: Data) async throws -> Entity
|
||||
{
|
||||
let url = try makeURL(endpoint: endpoint, forceVersion: version)
|
||||
var request = makeURLRequest(url: url, endpoint: endpoint, httpMethod: method)
|
||||
|
@ -7,18 +7,37 @@ import SwiftUI
|
||||
public class Navigator: ObservableObject {
|
||||
public var path: [RouterDestination] = []
|
||||
public var presentedSheet: SheetDestination?
|
||||
public var presentedCover: SheetDestination?
|
||||
public var selectedTab: TabDestination = .timeline
|
||||
|
||||
public var showTabbar: Bool = true
|
||||
|
||||
public func navigate(to: RouterDestination) {
|
||||
path.append(to)
|
||||
print("appended view")
|
||||
if path.contains(where: { $0 == .settings }) {
|
||||
toggleTabbar(false)
|
||||
} else {
|
||||
toggleTabbar(true)
|
||||
}
|
||||
}
|
||||
|
||||
public func removeSettingsOfPath() {
|
||||
self.path = self.path.filter({ !RouterDestination.allSettings.contains($0) })
|
||||
}
|
||||
|
||||
|
||||
/// Defines the visibility of the main tab bar in from `ContentView`
|
||||
/// - Parameter bool: `true` shows the tab bar and `false` hides the tab bar
|
||||
public func toggleTabbar(_ bool: Bool? = nil) {
|
||||
print("\((bool ?? !self.showTabbar) ? "shown" : "hide") the tab bar")
|
||||
withAnimation(.easeInOut(duration: 0.4)) {
|
||||
self.showTabbar = bool ?? !self.showTabbar
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class UniversalNavigator: Navigator {}
|
||||
|
||||
public enum TabDestination: Identifiable {
|
||||
case timeline
|
||||
case search
|
||||
@ -120,16 +139,24 @@ extension View {
|
||||
|
||||
func withSheets(sheetDestination: Binding<SheetDestination?>) -> some View {
|
||||
sheet(item: sheetDestination) { destination in
|
||||
viewRepresentation(destination: destination, isCover: false)
|
||||
viewSheet(destination: destination)
|
||||
}
|
||||
}
|
||||
|
||||
func withCovers(sheetDestination: Binding<SheetDestination?>) -> some View {
|
||||
fullScreenCover(item: sheetDestination) { destination in
|
||||
viewRepresentation(destination: destination, isCover: true)
|
||||
viewCover(destination: destination)
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, deprecated, renamed: "withSheets", message: "These two cannot support themselves")
|
||||
func withOver(sheetDestination: Binding<SheetDestination?>) -> some View {
|
||||
self
|
||||
.withCovers(sheetDestination: sheetDestination)
|
||||
.withSheets(sheetDestination: sheetDestination)
|
||||
}
|
||||
|
||||
@available(*, deprecated, message: "Causes bugs with sheets to display as covers")
|
||||
private func viewRepresentation(destination: SheetDestination, isCover: Bool) -> some View {
|
||||
Group {
|
||||
if destination.isCover {
|
||||
@ -162,18 +189,66 @@ extension View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func viewCover(destination: SheetDestination) -> some View {
|
||||
Group {
|
||||
switch destination {
|
||||
case .welcome:
|
||||
ConnectView()
|
||||
case .shop:
|
||||
ShopView()
|
||||
default:
|
||||
EmptySheetView(destId: destination.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func viewSheet(destination: SheetDestination) -> some View {
|
||||
Group {
|
||||
switch destination {
|
||||
case .post(let content, let replyId, let editId):
|
||||
NavigationStack {
|
||||
PostingView(initialString: content, replyId: replyId, editId: editId)
|
||||
.tint(Color(uiColor: UIColor.label))
|
||||
}
|
||||
case let .mastodonLogin(logged):
|
||||
AddInstanceView(logged: logged)
|
||||
.tint(Color.accentColor)
|
||||
case let .safari(url):
|
||||
SfSafariView(url: url)
|
||||
.ignoresSafeArea()
|
||||
case let .shareImage(image, status):
|
||||
ShareSheet(image: image, status: status)
|
||||
default:
|
||||
EmptySheetView(destId: destination.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This view is visible when the `viewRepresentation(destination: SheetDestination)` doesn't support the given `SheetDestination`
|
||||
private struct EmptySheetView: View {
|
||||
var destId: String = "???"
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
var destId: String = ""
|
||||
let str: String = .init(localized: "about.version-\(AppInfo.appVersion)")
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
ContentUnavailableView(String("Missing view for \"\(destId.isEmpty ? "[EMPTY_DEST_ID]" : destId)\""), systemImage: "exclamationmark.triangle.fill", description: Text(String("Please notify Lumaa as soon as possible!\n\n\(str)")))
|
||||
Rectangle()
|
||||
.fill(Color.red.gradient)
|
||||
.ignoresSafeArea()
|
||||
.background(Color.red.gradient)
|
||||
.foregroundStyle(.white)
|
||||
|
||||
VStack {
|
||||
ContentUnavailableView(String("Missing view for \"\(destId.isEmpty ? "[EMPTY_DEST_ID]" : destId)\""), systemImage: "exclamationmark.triangle.fill", description: Text(String("Please notify Lumaa as soon as possible!\n\n\(str)")))
|
||||
.foregroundStyle(.white)
|
||||
|
||||
Button {
|
||||
dismiss()
|
||||
} label: {
|
||||
Text(String("Dismiss"))
|
||||
}
|
||||
.buttonStyle(LargeButton(filled: true))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -60,7 +60,7 @@
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Threaded uses third-party open-source libraries and code:\n- [IceCubesApp](https://github.com/dimillian/IceCubesApp)\n- [SwiftSoup](https://github.com/scinfu/SwiftSoup)\n- [Nuke](https://github.com/kean/Nuke)\n- [EmojiText](https://github.com/divadretlaw/EmojiText)"
|
||||
"value" : "Threaded uses third-party open-source libraries and code:\n- [IceCubesApp](https://github.com/dimillian/IceCubesApp)\n- [SwiftSoup](https://github.com/scinfu/SwiftSoup)\n- [Nuke](https://github.com/kean/Nuke)\n- [EmojiText](https://github.com/divadretlaw/EmojiText)\n- [KeychainSwift](https://github.com/evgenyneu/keychain-swift)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -72,6 +72,12 @@
|
||||
"state" : "translated",
|
||||
"value" : "ThreadedApp v%@"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "ThreadedApp v%@"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -194,6 +200,24 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"variations" : {
|
||||
"plural" : {
|
||||
"one" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%lld follower"
|
||||
}
|
||||
},
|
||||
"other" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "%lld followers"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -213,6 +237,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"account.no-statuses" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "No posts"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Aucunes publications"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"account.unfollow" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -229,12 +269,34 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"activity" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Activity"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Activité"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"activity.favorite.%@" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%@ liked your post"
|
||||
"value" : "**%@** liked your post"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "**%@** a aimé votre publication"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -244,13 +306,13 @@
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%@ followed you"
|
||||
"value" : "**%@** followed you"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%@ vous a suivi(e)"
|
||||
"value" : "**%@** vous a suivi(e)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -260,13 +322,13 @@
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%@ mentionned you "
|
||||
"value" : "**%@** mentionned you "
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%@ vous a mentionné(e)"
|
||||
"value" : "**%@** vous a mentionné(e)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -292,13 +354,13 @@
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%@ reposted your post"
|
||||
"value" : "**%@** reposted your post"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%@ a republié votre publication"
|
||||
"value" : "**%@** a republié votre publication"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -308,13 +370,13 @@
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%@ posted"
|
||||
"value" : "**%@** posted"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%@ a publié une publication"
|
||||
"value" : "**%@** a publié une publication"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -324,7 +386,7 @@
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Unknown activity"
|
||||
"value" : "Unknown Activity"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
@ -346,7 +408,7 @@
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Erreur Image"
|
||||
"value" : "Erreur d'Image"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1290,4 +1352,4 @@
|
||||
}
|
||||
},
|
||||
"version" : "1.0"
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import SwiftUI
|
||||
|
||||
struct AccountView: View {
|
||||
@Environment(AccountManager.self) private var accountManager: AccountManager
|
||||
@Environment(UniversalNavigator.self) private var uniNav: UniversalNavigator
|
||||
|
||||
@Namespace var accountAnims
|
||||
@Namespace var animPicture
|
||||
@ -19,32 +20,25 @@ struct AccountView: View {
|
||||
@State private var accountFollows: Bool = false
|
||||
|
||||
@State private var loadingStatuses: Bool = false
|
||||
@State private var statuses: [Status]?
|
||||
@State private var statusesPinned: [Status]?
|
||||
@State private var statuses: [Status]? = []
|
||||
@State private var statusesPinned: [Status]? = []
|
||||
@State private var lastSeen: Int?
|
||||
|
||||
private let animPicCurve = Animation.smooth(duration: 0.25, extraBounce: 0.0)
|
||||
|
||||
var body: some View {
|
||||
if isCurrent {
|
||||
if accountManager.getClient() != nil {
|
||||
NavigationStack(path: $navigator.path) {
|
||||
accountView
|
||||
.withSheets(sheetDestination: $navigator.presentedSheet)
|
||||
.onAppear {
|
||||
account = accountManager.forceAccount()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ZStack {
|
||||
Color.appBackground
|
||||
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
}
|
||||
NavigationStack(path: $navigator.path) {
|
||||
accountView
|
||||
.environment(navigator)
|
||||
.withAppRouter(navigator)
|
||||
.onAppear {
|
||||
account = accountManager.forceAccount()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
accountView
|
||||
.environment(navigator)
|
||||
}
|
||||
}
|
||||
|
||||
@ -88,7 +82,7 @@ struct AccountView: View {
|
||||
}
|
||||
}
|
||||
.task {
|
||||
await updateRelationship()
|
||||
await reloadUser()
|
||||
initialFollowing = isFollowing
|
||||
}
|
||||
.refreshable {
|
||||
@ -144,10 +138,10 @@ struct AccountView: View {
|
||||
|
||||
Button {
|
||||
if let server = account.acct.split(separator: "@").last {
|
||||
navigator.presentedSheet = .post(content: "@\(account.username)@\(server)")
|
||||
uniNav.presentedSheet = .post(content: "@\(account.username)@\(server)")
|
||||
} else {
|
||||
let client = accountManager.getClient()
|
||||
navigator.presentedSheet = .post(content: "@\(account.username)@\(client?.server ?? "???")")
|
||||
uniNav.presentedSheet = .post(content: "@\(account.username)@\(client?.server ?? "???")")
|
||||
}
|
||||
} label: {
|
||||
HStack {
|
||||
@ -175,24 +169,26 @@ struct AccountView: View {
|
||||
.safeAreaPadding(.vertical)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.withAppRouter(navigator)
|
||||
.environment(navigator)
|
||||
}
|
||||
|
||||
var statusesList: some View {
|
||||
LazyVStack {
|
||||
if statuses != nil {
|
||||
if loadingStatuses == false || statuses == nil {
|
||||
if !(statusesPinned?.isEmpty ?? true) {
|
||||
ForEach(statusesPinned!, id: \.id) { status in
|
||||
CompactPostView(status: status, navigator: navigator, pinned: true)
|
||||
CompactPostView(status: status, pinned: true)
|
||||
}
|
||||
}
|
||||
if !statuses!.isEmpty {
|
||||
if !(statuses?.isEmpty ?? true) {
|
||||
ForEach(statuses!, id: \.id) { status in
|
||||
CompactPostView(status: status, navigator: navigator)
|
||||
CompactPostView(status: status)
|
||||
.onDisappear() {
|
||||
lastSeen = statuses!.firstIndex(where: { $0.id == status.id })
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ContentUnavailableView("account.no-statuses", systemImage: "pencil.slash")
|
||||
}
|
||||
} else {
|
||||
ProgressView()
|
||||
|
@ -146,7 +146,7 @@ struct AddInstanceView: View {
|
||||
let client = Client(server: client.server, oauthToken: oauthToken)
|
||||
let account: Account = try await client.get(endpoint: Accounts.verifyCredentials)
|
||||
let appAcc = AppAccount(server: client.server, accountName: "\(account.acct)@\(client.server)", oauthToken: oauthToken)
|
||||
try appAcc.saveAsCurrent()
|
||||
appAcc.saveAsCurrent()
|
||||
|
||||
signInClient = client
|
||||
logged = true
|
||||
|
@ -2,67 +2,63 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// TODO: Make some sort of "Universal Navigation"?
|
||||
/// Details: Fix bugs about `navigator.path` when tapping on any element that adds to it.
|
||||
/// Possibility 1: Put a `NavigationStack` in the parent view of the tabs' view with no title
|
||||
/// Possibility 2: Make another `Navigator` but universally
|
||||
|
||||
///
|
||||
struct ContentView: View {
|
||||
@UIApplicationDelegateAdaptor private var appDelegate: AppDelegate
|
||||
|
||||
@State private var preferences: UserPreferences = .defaultPreferences
|
||||
@State private var navigator = Navigator()
|
||||
@State private var sheet: SheetDestination?
|
||||
@StateObject private var navigator = UniversalNavigator() // "Universal Path" (POSS 1)
|
||||
@State private var accountManager: AccountManager = AccountManager()
|
||||
|
||||
var body: some View {
|
||||
TabView(selection: $navigator.selectedTab, content: {
|
||||
ZStack {
|
||||
if accountManager.getClient() != nil {
|
||||
ZStack {
|
||||
TabView(selection: $navigator.selectedTab, content: {
|
||||
if accountManager.getAccount() != nil {
|
||||
TimelineView(navigator: navigator, timelineModel: FetchTimeline(client: accountManager.forceClient()))
|
||||
.background(Color.appBackground)
|
||||
.safeAreaPadding()
|
||||
} else {
|
||||
ZStack {
|
||||
Color.appBackground
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
}
|
||||
}
|
||||
.background(Color.appBackground)
|
||||
.tag(TabDestination.timeline)
|
||||
|
||||
Text(String("Search"))
|
||||
.background(Color.appBackground)
|
||||
.tag(TabDestination.search)
|
||||
|
||||
//TODO: Messaging UI in Activity tab
|
||||
NotificationsView()
|
||||
.background(Color.appBackground)
|
||||
.tag(TabDestination.activity)
|
||||
|
||||
ZStack {
|
||||
if accountManager.getAccount() != nil {
|
||||
AccountView(isCurrent: true, account: accountManager.forceAccount())
|
||||
.environment(navigator)
|
||||
.tag(TabDestination.timeline)
|
||||
|
||||
Text(String("Search"))
|
||||
.background(Color.appBackground)
|
||||
.tag(TabDestination.search)
|
||||
|
||||
//TODO: Messaging UI in Activity tab
|
||||
NotificationsView()
|
||||
.background(Color.appBackground)
|
||||
.tag(TabDestination.activity)
|
||||
|
||||
AccountView(isCurrent: true, account: accountManager.forceAccount())
|
||||
.background(Color.appBackground)
|
||||
.tag(TabDestination.profile)
|
||||
} else {
|
||||
ZStack {
|
||||
Color.appBackground
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
}
|
||||
}
|
||||
.background(Color.appBackground)
|
||||
.tag(TabDestination.profile)
|
||||
|
||||
})
|
||||
.overlay(alignment: .bottom) {
|
||||
TabsView(navigator: navigator)
|
||||
.safeAreaPadding(.vertical)
|
||||
.zIndex(10)
|
||||
})
|
||||
}
|
||||
.overlay(alignment: .bottom) {
|
||||
if navigator.showTabbar {
|
||||
TabsView(selectedTab: $navigator.selectedTab) {
|
||||
navigator.presentedSheet = .post(content: "", replyId: nil, editId: nil)
|
||||
}
|
||||
.safeAreaPadding(.vertical)
|
||||
.offset(y: navigator.showTabbar ? 0 : -20)
|
||||
.zIndex(10)
|
||||
}
|
||||
}
|
||||
.withCovers(sheetDestination: $sheet)
|
||||
.withSheets(sheetDestination: $navigator.presentedSheet)
|
||||
.environment(accountManager)
|
||||
.withCovers(sheetDestination: $navigator.presentedCover)
|
||||
.environment(navigator)
|
||||
.environment(accountManager)
|
||||
.environment(appDelegate)
|
||||
.environment(preferences)
|
||||
.environmentObject(preferences)
|
||||
.onAppear {
|
||||
do {
|
||||
preferences = try UserPreferences.loadAsCurrent() ?? .defaultPreferences
|
||||
@ -76,9 +72,6 @@ struct ContentView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.task {
|
||||
await recognizeAccount()
|
||||
}
|
||||
.environment(\.openURL, OpenURLAction { url in
|
||||
// Open internal URL.
|
||||
guard preferences.browserType == .inApp else { return .systemAction }
|
||||
@ -92,20 +85,19 @@ struct ContentView: View {
|
||||
}
|
||||
|
||||
func recognizeAccount() async {
|
||||
let acc = try? AppAccount.loadAsCurrent()
|
||||
if acc == nil {
|
||||
sheet = .welcome
|
||||
let appAccount: AppAccount? = AppAccount.loadAsCurrent()
|
||||
if appAccount == nil {
|
||||
navigator.presentedSheet = .welcome
|
||||
} else {
|
||||
Task {
|
||||
accountManager.setClient(.init(server: acc!.server, oauthToken: acc!.oauthToken))
|
||||
|
||||
// check if token is still working
|
||||
let fetched: Account? = await accountManager.fetchAccount()
|
||||
if fetched == nil {
|
||||
accountManager.clear()
|
||||
AppAccount.clear()
|
||||
sheet = .welcome
|
||||
}
|
||||
//TODO: Fix this? (Fatal error: calling into SwiftUI on a non-main thread is not supported)
|
||||
accountManager.setClient(.init(server: appAccount!.server, oauthToken: appAccount!.oauthToken))
|
||||
|
||||
// Check if token is still working
|
||||
let fetched: Account? = await accountManager.fetchAccount()
|
||||
if fetched == nil {
|
||||
accountManager.clear()
|
||||
appAccount!.clear()
|
||||
navigator.presentedSheet = .welcome
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,18 +16,21 @@ struct NotificationsView: View {
|
||||
NavigationStack(path: $navigator.path) {
|
||||
if !notifications.isEmpty {
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
LazyVStack(alignment: .leading) {
|
||||
LazyVStack(alignment: .leading, spacing: 15) {
|
||||
ForEach(notifications) { notif in
|
||||
NotificationRow(notif: notif)
|
||||
.environment(navigator)
|
||||
.onDisappear() {
|
||||
guard !notifications.isEmpty else { return }
|
||||
lastId = notifications.firstIndex(where: { $0.id == notif.id })
|
||||
}
|
||||
}
|
||||
}
|
||||
.refreshable {
|
||||
notifications = []
|
||||
await fetchNotifications(lastId: nil)
|
||||
|
||||
if loadingNotifs {
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
.padding(.vertical)
|
||||
}
|
||||
}
|
||||
.onChange(of: lastId ?? 0) { _, new in
|
||||
guard !loadingNotifs else { return }
|
||||
@ -38,6 +41,12 @@ struct NotificationsView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.withAppRouter(navigator)
|
||||
.background(Color.appBackground)
|
||||
.refreshable {
|
||||
await fetchNotifications(lastId: nil)
|
||||
}
|
||||
.navigationTitle(String(localized: "activity"))
|
||||
} else if loadingNotifs == false && notifications.isEmpty {
|
||||
ZStack {
|
||||
Color.appBackground
|
||||
@ -70,7 +79,6 @@ struct NotificationsView: View {
|
||||
}
|
||||
|
||||
do {
|
||||
let allCases = Notification.NotificationType.allCases.map({ $0.rawValue })
|
||||
let notifs: [Notification] = try await client.get(endpoint: Notifications.notifications(minId: nil, maxId: nil, types: nil, limit: lastId != nil ? notifLimit : 30))
|
||||
guard !notifs.isEmpty else { return }
|
||||
|
||||
|
@ -29,19 +29,16 @@ struct PostDetailsView: View {
|
||||
VStack(alignment: .leading) {
|
||||
if statuses.isEmpty {
|
||||
statusPost(detailedStatus)
|
||||
|
||||
// Spacer()
|
||||
} else {
|
||||
ForEach(statuses) { status in
|
||||
if status.id == detailedStatus.id {
|
||||
statusPost(detailedStatus)
|
||||
.padding(.horizontal, 15)
|
||||
.padding(statuses.first!.id == detailedStatus.id ? .bottom : .vertical)
|
||||
.onAppear {
|
||||
proxy.scrollTo("\(detailedStatus.id)@\(detailedStatus.account.id)", anchor: .bottom)
|
||||
}
|
||||
} else {
|
||||
CompactPostView(status: status, navigator: navigator)
|
||||
CompactPostView(status: status)
|
||||
.environment(navigator)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -118,6 +115,9 @@ struct PostDetailsView: View {
|
||||
stats.padding(.top, 5)
|
||||
}
|
||||
}
|
||||
.environment(navigator)
|
||||
.padding(.horizontal, 15)
|
||||
.padding(statuses.first!.id == detailedStatus.id ? .bottom : .vertical)
|
||||
}
|
||||
|
||||
private func fetchStatusDetail() async {
|
||||
|
@ -8,7 +8,6 @@ struct PostingView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@Environment(AccountManager.self) private var accountManager: AccountManager
|
||||
@Environment(Navigator.self) private var navigator: Navigator
|
||||
|
||||
public var initialString: String = ""
|
||||
public var replyId: String? = nil
|
||||
@ -37,14 +36,27 @@ struct PostingView: View {
|
||||
|
||||
var body: some View {
|
||||
if accountManager.getAccount() != nil {
|
||||
posting
|
||||
.background(Color.appBackground)
|
||||
.sheet(isPresented: $selectingEmoji) {
|
||||
EmojiSelector(viewModel: $viewModel)
|
||||
.presentationDetents([.height(200), .medium])
|
||||
.presentationDragIndicator(.visible)
|
||||
.presentationBackgroundInteraction(.enabled(upThrough: .height(200))) // Allow users to move the cursor while adding emojis
|
||||
ViewThatFits {
|
||||
posting
|
||||
.background(Color.appBackground)
|
||||
.sheet(isPresented: $selectingEmoji) {
|
||||
EmojiSelector(viewModel: $viewModel)
|
||||
.presentationDetents([.height(200), .medium])
|
||||
.presentationDragIndicator(.visible)
|
||||
.presentationBackgroundInteraction(.enabled(upThrough: .height(200))) // Allow users to move the cursor while adding emojis
|
||||
}
|
||||
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
posting
|
||||
.background(Color.appBackground)
|
||||
.sheet(isPresented: $selectingEmoji) {
|
||||
EmojiSelector(viewModel: $viewModel)
|
||||
.presentationDetents([.height(200), .medium])
|
||||
.presentationDragIndicator(.visible)
|
||||
.presentationBackgroundInteraction(.enabled(upThrough: .height(200))) // Allow users to move the cursor while adding emojis
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
loading
|
||||
.background(Color.appBackground)
|
||||
@ -175,13 +187,15 @@ struct PostingView: View {
|
||||
let isEdit: Bool = editId != nil
|
||||
let endp: Endpoint = isEdit ? Statuses.editStatus(id: editId!, json: json) : Statuses.postStatus(json: json)
|
||||
|
||||
let newStatus: Status = isEdit ? try await client.put(endpoint: endp) : try await client.post(endpoint: endp)
|
||||
let _: Status = isEdit ? try await client.put(endpoint: endp) : try await client.post(endpoint: endp)
|
||||
|
||||
postingStatus = false
|
||||
HapticManager.playHaptics(haptics: Haptic.success)
|
||||
dismiss()
|
||||
navigator.removeSettingsOfPath()
|
||||
navigator.navigate(to: .post(status: newStatus))
|
||||
|
||||
if isEdit {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -205,7 +219,7 @@ struct PostingView: View {
|
||||
Image(uiImage: img)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(minWidth: mediaContainers.count == 1 ? nil : containerWidth, maxWidth: 500)
|
||||
.frame(minWidth: mediaContainers.count == 1 ? nil : containerWidth, maxWidth: 450)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 15)
|
||||
.stroke(.gray.opacity(0.3), lineWidth: 1)
|
||||
|
@ -57,7 +57,7 @@ struct TimelineView: View {
|
||||
|
||||
ForEach(statuses!, id: \.id) { status in
|
||||
LazyVStack(alignment: .leading, spacing: 2) {
|
||||
CompactPostView(status: status, navigator: navigator)
|
||||
CompactPostView(status: status)
|
||||
.onDisappear {
|
||||
guard statuses != nil else { return }
|
||||
lastSeen = statuses!.firstIndex(where: { $0.id == status.id })
|
||||
@ -84,6 +84,7 @@ struct TimelineView: View {
|
||||
}
|
||||
.padding(.top)
|
||||
.background(Color.appBackground)
|
||||
.environment(navigator)
|
||||
.withAppRouter(navigator)
|
||||
} else {
|
||||
ZStack {
|
||||
|
Loading…
x
Reference in New Issue
Block a user