field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | LICENSE

commit 8c8084df6f9fa981b448795237ee9c2ab18b2714
parent 8550af403b49a80aab0324eecd441376b341fe97
Author: triesap <triesap@radroots.dev>
Date:   Sat,  4 Oct 2025 22:12:04 +0100

Add `PostView` for composing and posting text notes.

Diffstat:
MRadroots.xcodeproj/project.pbxproj | 4++++
MRadroots/Views/HomeView.swift | 8++++++++
ARadroots/Views/PostView.swift | 104+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 116 insertions(+), 0 deletions(-)

diff --git a/Radroots.xcodeproj/project.pbxproj b/Radroots.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 2B3886FD26434A54F3726591 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 8D19AA8D515FC8F5D2407378 /* Localizable.strings */; }; 2B6ACA26689B355CECBFFB57 /* SetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16A7641E5C643B4B36CFEDA8 /* SetupView.swift */; }; 360F23EFE80FDBDC6983FB15 /* AppEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = D448C9655B708CA3FA8712B9 /* AppEntry.swift */; }; + 53694F65BFDDA2B5213063EE /* PostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BADD960784613FCC0EA1D14 /* PostView.swift */; }; 7FD8FB018DA09568303194B2 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADE61264E2C98E73828E8680 /* Strings.swift */; }; A1B921027DA7ACD7343BE250 /* SectionWideButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C17CA8F5611075F60F214A00 /* SectionWideButton.swift */; }; B971351ABE8E79A472B4DC7D /* View+Nav.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0B0C9861CD86EAD3CAD549E /* View+Nav.swift */; }; @@ -27,6 +28,7 @@ /* Begin PBXFileReference section */ 08FA88664E5E3ED3A24D56CC /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; }; 0A0274A0260D1C04F40C71AF /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; }; + 0BADD960784613FCC0EA1D14 /* PostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostView.swift; sourceTree = "<group>"; }; 138AA7BAA021EE13E829390B /* Bundle+Build.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+Build.swift"; sourceTree = "<group>"; }; 16A7641E5C643B4B36CFEDA8 /* SetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupView.swift; sourceTree = "<group>"; }; 2818363B157125491FB84A1E /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; }; @@ -145,6 +147,7 @@ isa = PBXGroup; children = ( 0A0274A0260D1C04F40C71AF /* HomeView.swift */, + 0BADD960784613FCC0EA1D14 /* PostView.swift */, C71A93F98C7B93188748B99B /* ProfileView.swift */, C1D9496F9F05A4E79E73A247 /* RelaysView.swift */, E1D12A016D1377CDFBFB0F9B /* SettingsView.swift */, @@ -271,6 +274,7 @@ E3864E34D67BAD0744B93180 /* Bundle+Build.swift in Sources */, 1E5B41A3E1F9A7D68F63B079 /* HomeView.swift in Sources */, C512BD267A3E2B8F10FABB3B /* Logger.swift in Sources */, + 53694F65BFDDA2B5213063EE /* PostView.swift in Sources */, 275D4D574BF3B3C1DD746CE7 /* ProfileView.swift in Sources */, 022DA21729F49893319717AA /* RelaysView.swift in Sources */, A1B921027DA7ACD7343BE250 /* SectionWideButton.swift in Sources */, diff --git a/Radroots/Views/HomeView.swift b/Radroots/Views/HomeView.swift @@ -31,6 +31,14 @@ private struct HomeDashboardView: View { var body: some View { List { + Section("Compose") { + NavigationLink { + PostView() + } label: { + Label("New Post", systemImage: "square.and.pencil") + } + } + Section("Your Identity") { NavigationLink { ProfileView() diff --git a/Radroots/Views/PostView.swift b/Radroots/Views/PostView.swift @@ -0,0 +1,104 @@ +import SwiftUI +import RadrootsKit + +struct PostView: View { + @EnvironmentObject private var app: AppState + @State private var text: String = "" + @State private var isPosting = false + @State private var resultMessage: String? + @State private var showResult = false + @FocusState private var focused: Bool + + private var isConnected: Bool { app.relayConnectedCount > 0 } + private var canPost: Bool { + isConnected && !isPosting && !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + } + + var body: some View { + Form { + Section { + ZStack(alignment: .topLeading) { + TextEditor(text: $text) + .frame(minHeight: 160) + .focused($focused) + .submitLabel(.send) + if text.isEmpty { + Text("What's happening?") + .foregroundStyle(.secondary) + .padding(.top, 8) + .padding(.leading, 5) + .allowsHitTesting(false) + } + } + HStack { + Text("\(text.count)") + .font(.footnote) + .foregroundStyle(.secondary) + Spacer() + Button { + post() + } label: { + if isPosting { + ProgressView() + } else { + Text("Post") + .fontWeight(.semibold) + } + } + .buttonStyle(.borderedProminent) + .disabled(!canPost) + } + } footer: { + VStack(alignment: .leading, spacing: 4) { + if !isConnected { + Text("No relays connected. Configure and connect to post.") + .foregroundStyle(.red) + } + if let e = app.relayLastError { + Text(e).foregroundStyle(.secondary) + } + } + } + } + .listStyle(.insetGrouped) + .inlineNavigationTitle("Compose") + .toolbar { + ToolbarItemGroup(placement: .keyboard) { + Spacer() + Button("Done") { focused = false } + } + } + .alert("Post Result", isPresented: $showResult) { + Button("OK", role: .cancel) { } + } message: { + Text(resultMessage ?? "") + } + .onAppear { focused = true } + } + + private func post() { + guard let rt = app.radroots.runtime else { return } + let content = text.trimmingCharacters(in: .whitespacesAndNewlines) + guard !content.isEmpty else { return } + isPosting = true + resultMessage = nil + Task { + do { + let id = try rt.nostrPostTextNote(content: content) + await MainActor.run { + resultMessage = "Posted kind:1 event: \(id)" + showResult = true + text = "" + isPosting = false + app.refresh() + } + } catch { + await MainActor.run { + resultMessage = "Failed to post: \(error)" + showResult = true + isPosting = false + } + } + } + } +}