aa0a5fb356
- New WelcomeView gates first-run users with existing-account or signup paths - HostSignupClient posts to host.org-social.org /signup and auto-configures Settings - Show all relay feeds toggle (default off) restricts timeline to #+FOLLOW entries - Contextual empty-state in timeline routes to Discover or toggles relay-wide view - RootTab enum enables programmatic tab switching from child views - App icon background switched to cream for better contrast in iOS home screen - Remove pre-registered test account; fresh installs land on Welcome
146 lines
5.1 KiB
Swift
146 lines
5.1 KiB
Swift
import SwiftUI
|
|
import OrgSocialKit
|
|
|
|
/// First-run welcome gate. Shown when the user hasn't configured a publishing method yet.
|
|
/// Offers two paths: jump straight into Settings with an existing account, or create one
|
|
/// on host.org-social.org and auto-fill the config.
|
|
struct WelcomeView: View {
|
|
@State private var showSettings = false
|
|
@State private var showCreate = false
|
|
var onConfigured: () -> Void = {}
|
|
|
|
var body: some View {
|
|
VStack(spacing: 32) {
|
|
Spacer()
|
|
|
|
LogoView(size: 120)
|
|
.allowsHitTesting(false)
|
|
.accessibilityHidden(true)
|
|
|
|
VStack(spacing: 8) {
|
|
Text("Welcome to Org Social")
|
|
.font(.largeTitle.weight(.bold))
|
|
.multilineTextAlignment(.center)
|
|
Text("A decentralized social network based on plain text `social.org` files.")
|
|
.font(.subheadline)
|
|
.foregroundStyle(.secondary)
|
|
.multilineTextAlignment(.center)
|
|
.padding(.horizontal, 32)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
VStack(spacing: 12) {
|
|
Button {
|
|
showCreate = true
|
|
} label: {
|
|
Label("Create my social.org", systemImage: "sparkles")
|
|
.font(.body.weight(.semibold))
|
|
.frame(maxWidth: .infinity)
|
|
}
|
|
.buttonStyle(.borderedProminent)
|
|
.controlSize(.large)
|
|
|
|
Button {
|
|
showSettings = true
|
|
} label: {
|
|
Label("I already have a social.org", systemImage: "gearshape")
|
|
.frame(maxWidth: .infinity)
|
|
}
|
|
.buttonStyle(.bordered)
|
|
.controlSize(.large)
|
|
}
|
|
.padding(.horizontal, 32)
|
|
.padding(.bottom, 40)
|
|
}
|
|
.sheet(isPresented: $showSettings, onDismiss: onConfigured) {
|
|
SettingsView()
|
|
}
|
|
.sheet(isPresented: $showCreate, onDismiss: onConfigured) {
|
|
CreateAccountSheet()
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Sign-up form that creates an account on host.org-social.org and writes the
|
|
/// returned vfile token URL + public feed URL into UserDefaults.
|
|
struct CreateAccountSheet: View {
|
|
@Environment(\.dismiss) private var dismiss
|
|
@AppStorage("vfileURL") private var vfileURL = ""
|
|
@AppStorage("publicFeedURL") private var publicFeedURL = ""
|
|
@AppStorage("uploadMethod") private var uploadMethod = "vfile"
|
|
|
|
@State private var nickname = ""
|
|
@State private var isCreating = false
|
|
@State private var errorMessage: String?
|
|
@State private var success = false
|
|
|
|
private let client = HostSignupClient()
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
Form {
|
|
Section {
|
|
TextField("Nickname", text: $nickname)
|
|
.autocorrectionDisabled()
|
|
.textInputAutocapitalization(.never)
|
|
} header: {
|
|
Text("Choose a nickname")
|
|
} footer: {
|
|
Text("Your feed will be hosted at https://host.org-social.org/<nickname>/social.org. Pick something unique—no spaces.")
|
|
}
|
|
|
|
if let errorMessage {
|
|
Section {
|
|
Label(errorMessage, systemImage: "exclamationmark.triangle")
|
|
.font(.footnote)
|
|
.foregroundStyle(.red)
|
|
}
|
|
}
|
|
|
|
if success {
|
|
Section {
|
|
Label("Account created", systemImage: "checkmark.circle.fill")
|
|
.foregroundStyle(.green)
|
|
Text(publicFeedURL)
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("Create account")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .cancellationAction) {
|
|
Button("Cancel") { dismiss() }
|
|
}
|
|
ToolbarItem(placement: .confirmationAction) {
|
|
if isCreating {
|
|
ProgressView()
|
|
} else {
|
|
Button("Create") { Task { await create() } }
|
|
.disabled(nickname.trimmingCharacters(in: .whitespaces).isEmpty || success)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func create() async {
|
|
isCreating = true
|
|
errorMessage = nil
|
|
defer { isCreating = false }
|
|
do {
|
|
let result = try await client.signup(nick: nickname)
|
|
vfileURL = result.vfileURL.absoluteString
|
|
publicFeedURL = result.publicURL.absoluteString
|
|
uploadMethod = "vfile"
|
|
success = true
|
|
try? await Task.sleep(for: .seconds(1))
|
|
dismiss()
|
|
} catch {
|
|
errorMessage = error.localizedDescription
|
|
}
|
|
}
|
|
}
|