field_ios

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

commit 9beecc9c6a5218a47efd6820c79af8465013feb1
parent 8c8084df6f9fa981b448795237ee9c2ab18b2714
Author: triesap <triesap@radroots.dev>
Date:   Sun,  5 Oct 2025 22:20:22 +0100

Add Nostr post feed and detail views with post display and sharing, introduce copy and toast UI components, and update home and profile views for new metadata integration.

Diffstat:
MRadroots.xcodeproj/project.pbxproj | 36++++++++++++++++++++++++++++++++----
ARadroots/Shared/Components/CopyButton.swift | 40++++++++++++++++++++++++++++++++++++++++
ARadroots/Shared/Components/CopyRow.swift | 26++++++++++++++++++++++++++
ARadroots/Shared/Modifiers/ToastModifier.swift | 41+++++++++++++++++++++++++++++++++++++++++
MRadroots/Views/HomeView.swift | 24++++++++++++++++--------
ARadroots/Views/PostCreateView.swift | 104+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ARadroots/Views/PostDetailView.swift | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ARadroots/Views/PostFeedView.swift | 107+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
DRadroots/Views/PostView.swift | 104-------------------------------------------------------------------------------
MRadroots/Views/ProfileView.swift | 14+++++++-------
10 files changed, 431 insertions(+), 123 deletions(-)

diff --git a/Radroots.xcodeproj/project.pbxproj b/Radroots.xcodeproj/project.pbxproj @@ -13,32 +13,40 @@ 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 */; }; + 53694F65BFDDA2B5213063EE /* PostCreateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BADD960784613FCC0EA1D14 /* PostCreateView.swift */; }; 7FD8FB018DA09568303194B2 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADE61264E2C98E73828E8680 /* Strings.swift */; }; + 9121BD4A3E7C6EF2B21F540F /* CopyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C7DE4207398DE242519F9C /* CopyButton.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 */; }; C22DB0F3EB2E69A34DF941E0 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2818363B157125491FB84A1E /* App.swift */; }; C512BD267A3E2B8F10FABB3B /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE790CA1CD31208947913B9 /* Logger.swift */; }; C8AB3389F7430A5C79AD7DF8 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D12A016D1377CDFBFB0F9B /* SettingsView.swift */; }; + D3834AF9A4E1327B7DA557F3 /* CopyRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41A4289F43625DD65E6C4B25 /* CopyRow.swift */; }; + D62E9461833A0AA5E622A1E6 /* ToastModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 227028B4EBDC6703999FB9DA /* ToastModifier.swift */; }; DCE468F668A3C346E716B04C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CCF0F7B3C57D8D770F178329 /* Assets.xcassets */; }; E3864E34D67BAD0744B93180 /* Bundle+Build.swift in Sources */ = {isa = PBXBuildFile; fileRef = 138AA7BAA021EE13E829390B /* Bundle+Build.swift */; }; + EB7C19F62D7DAB9C044D53AA /* PostDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3C0EFACAD213A69C12D5064 /* PostDetailView.swift */; }; + F01E2EF766801B5D76A0852B /* FeedsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E07205F92CB5B757F7A985C /* FeedsView.swift */; }; F3E40E5A76B4EA19AC7603D2 /* RadrootsKit in Frameworks */ = {isa = PBXBuildFile; productRef = 2DAD90EBF8EB00ACDD7611CD /* RadrootsKit */; }; /* End PBXBuildFile section */ /* 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>"; }; + 0BADD960784613FCC0EA1D14 /* PostCreateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostCreateView.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>"; }; + 227028B4EBDC6703999FB9DA /* ToastModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastModifier.swift; sourceTree = "<group>"; }; 2818363B157125491FB84A1E /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; }; 2FE790CA1CD31208947913B9 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; }; 3FADD6E2563CC9AF9F935DCE /* RadrootsKit */ = {isa = PBXFileReference; lastKnownFileType = folder; name = RadrootsKit; path = RadrootsKit; sourceTree = SOURCE_ROOT; }; + 41A4289F43625DD65E6C4B25 /* CopyRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyRow.swift; sourceTree = "<group>"; }; 4BC4B7D0BB4C6D8E4B0AA4AD /* radroots.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = radroots.xcconfig; sourceTree = "<group>"; }; 54EE5A34FE2086899F5B7568 /* radroots.git.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = radroots.git.xcconfig; sourceTree = "<group>"; }; 676B89EB116B60AE8C2B4313 /* Base.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Base.xcconfig; sourceTree = "<group>"; }; 7BCA99336E305EC789152DDE /* radroots.local.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = radroots.local.xcconfig; sourceTree = "<group>"; }; 7C294E8EF50F5E1E73F5C135 /* Common.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Common.xcconfig; sourceTree = "<group>"; }; + 8E07205F92CB5B757F7A985C /* FeedsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedsView.swift; sourceTree = "<group>"; }; 93AA285819DD1269C3EAD80A /* Radroots.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = Radroots.app; sourceTree = BUILT_PRODUCTS_DIR; }; 93D729E070C32490545FA837 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; }; A0B0C9861CD86EAD3CAD549E /* View+Nav.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Nav.swift"; sourceTree = "<group>"; }; @@ -50,6 +58,8 @@ CCF0F7B3C57D8D770F178329 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; D448C9655B708CA3FA8712B9 /* AppEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppEntry.swift; sourceTree = "<group>"; }; E1D12A016D1377CDFBFB0F9B /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; }; + F3C0EFACAD213A69C12D5064 /* PostDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostDetailView.swift; sourceTree = "<group>"; }; + F4C7DE4207398DE242519F9C /* CopyButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyButton.swift; sourceTree = "<group>"; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -122,6 +132,7 @@ 9D22575D1FAD99FE8B6FCE6C /* Extensions */, 65EC1C4AF7DC676E78603D52 /* Localisation */, F16A19713274742D956C3A4D /* Logging */, + C5BAA3C6E2D410F0C8475D89 /* Modifiers */, ); path = Shared; sourceTree = "<group>"; @@ -146,8 +157,10 @@ BD0E20D32DF34D9E7C3EBCD2 /* Views */ = { isa = PBXGroup; children = ( + 8E07205F92CB5B757F7A985C /* FeedsView.swift */, 0A0274A0260D1C04F40C71AF /* HomeView.swift */, - 0BADD960784613FCC0EA1D14 /* PostView.swift */, + F3C0EFACAD213A69C12D5064 /* PostDetailView.swift */, + 0BADD960784613FCC0EA1D14 /* PostCreateView.swift */, C71A93F98C7B93188748B99B /* ProfileView.swift */, C1D9496F9F05A4E79E73A247 /* RelaysView.swift */, E1D12A016D1377CDFBFB0F9B /* SettingsView.swift */, @@ -165,6 +178,14 @@ ); sourceTree = "<group>"; }; + C5BAA3C6E2D410F0C8475D89 /* Modifiers */ = { + isa = PBXGroup; + children = ( + 227028B4EBDC6703999FB9DA /* ToastModifier.swift */, + ); + path = Modifiers; + sourceTree = "<group>"; + }; C5BC6846E71297290E1EAA29 /* Localizations */ = { isa = PBXGroup; children = ( @@ -176,6 +197,8 @@ D46F444AD1818932F03AC6B6 /* Components */ = { isa = PBXGroup; children = ( + F4C7DE4207398DE242519F9C /* CopyButton.swift */, + 41A4289F43625DD65E6C4B25 /* CopyRow.swift */, C17CA8F5611075F60F214A00 /* SectionWideButton.swift */, ); path = Components; @@ -272,15 +295,20 @@ C22DB0F3EB2E69A34DF941E0 /* App.swift in Sources */, 360F23EFE80FDBDC6983FB15 /* AppEntry.swift in Sources */, E3864E34D67BAD0744B93180 /* Bundle+Build.swift in Sources */, + 9121BD4A3E7C6EF2B21F540F /* CopyButton.swift in Sources */, + D3834AF9A4E1327B7DA557F3 /* CopyRow.swift in Sources */, + F01E2EF766801B5D76A0852B /* FeedsView.swift in Sources */, 1E5B41A3E1F9A7D68F63B079 /* HomeView.swift in Sources */, C512BD267A3E2B8F10FABB3B /* Logger.swift in Sources */, - 53694F65BFDDA2B5213063EE /* PostView.swift in Sources */, + EB7C19F62D7DAB9C044D53AA /* PostDetailView.swift in Sources */, + 53694F65BFDDA2B5213063EE /* PostCreateView.swift in Sources */, 275D4D574BF3B3C1DD746CE7 /* ProfileView.swift in Sources */, 022DA21729F49893319717AA /* RelaysView.swift in Sources */, A1B921027DA7ACD7343BE250 /* SectionWideButton.swift in Sources */, C8AB3389F7430A5C79AD7DF8 /* SettingsView.swift in Sources */, 2B6ACA26689B355CECBFFB57 /* SetupView.swift in Sources */, 7FD8FB018DA09568303194B2 /* Strings.swift in Sources */, + D62E9461833A0AA5E622A1E6 /* ToastModifier.swift in Sources */, B971351ABE8E79A472B4DC7D /* View+Nav.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Radroots/Shared/Components/CopyButton.swift b/Radroots/Shared/Components/CopyButton.swift @@ -0,0 +1,40 @@ +import SwiftUI + +public struct CopyButton: View { + private let value: String + private let onCopied: (() -> Void)? + @State private var copied = false + + public init(value: String, onCopied: (() -> Void)? = nil) { + self.value = value + self.onCopied = onCopied + } + + public var body: some View { + Button { + Task { @MainActor in + UIPasteboard.general.string = value + } + let gen = UINotificationFeedbackGenerator() + gen.notificationOccurred(.success) + withAnimation(.easeInOut(duration: 0.12)) { copied = true } + onCopied?() + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + withAnimation(.easeInOut(duration: 0.12)) { copied = false } + } + } label: { + ZStack { + Image(systemName: "doc.on.doc") + .opacity(copied ? 0 : 1) + Image(systemName: "checkmark.circle.fill") + .opacity(copied ? 1 : 0) + } + .frame(width: 24, height: 24) + .font(.system(size: 17, weight: .semibold)) + } + .buttonStyle(.plain) + .foregroundStyle(copied ? .green : .accentColor) + .contentTransition(.opacity) + .accessibilityLabel(copied ? "Copied" : "Copy") + } +} diff --git a/Radroots/Shared/Components/CopyRow.swift b/Radroots/Shared/Components/CopyRow.swift @@ -0,0 +1,26 @@ +import SwiftUI + +public struct CopyRow: View { + private let title: String + private let value: String + private let onCopied: (() -> Void)? + + public init(title: String, value: String, onCopied: (() -> Void)? = nil) { + self.title = title + self.value = value + self.onCopied = onCopied + } + + public var body: some View { + LabeledContent(title) { + HStack(spacing: 6) { + Text(value) + .font(.callout.monospaced()) + .lineLimit(1) + .truncationMode(.middle) + .textSelection(.enabled) + CopyButton(value: value, onCopied: onCopied) + } + } + } +} diff --git a/Radroots/Shared/Modifiers/ToastModifier.swift b/Radroots/Shared/Modifiers/ToastModifier.swift @@ -0,0 +1,41 @@ +import SwiftUI + +struct ToastModifier<Overlay: View>: ViewModifier { + @Binding var isPresented: Bool + let autoDismiss: Double + let overlay: () -> Overlay + + func body(content: Content) -> some View { + ZStack { + content + if isPresented { + overlay() + .transition(.opacity.combined(with: .scale(scale: 0.98))) + .zIndex(1) + .onAppear { + guard autoDismiss > 0 else { return } + DispatchQueue.main.asyncAfter(deadline: .now() + autoDismiss) { + withAnimation { isPresented = false } + } + } + } + } + .animation(.easeInOut(duration: 0.15), value: isPresented) + } +} + +extension View { + func toast<Overlay: View>( + isPresented: Binding<Bool>, + autoDismiss: Double = 1.2, + @ViewBuilder overlay: @escaping () -> Overlay + ) -> some View { + modifier( + ToastModifier( + isPresented: isPresented, + autoDismiss: autoDismiss, + overlay: overlay + ) + ) + } +} diff --git a/Radroots/Views/HomeView.swift b/Radroots/Views/HomeView.swift @@ -31,14 +31,6 @@ 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() @@ -70,6 +62,22 @@ private struct HomeDashboardView: View { } } } + + Section("Compose") { + NavigationLink { + PostCreateView() + } label: { + Label("New Post", systemImage: "square.and.pencil") + } + } + + Section("Explore") { + NavigationLink { + PostFeedView() + } label: { + Label("Public Feed", systemImage: "text.bubble.fill") + } + } } .listStyle(.insetGrouped) .inlineNavigationTitle("Home") diff --git a/Radroots/Views/PostCreateView.swift b/Radroots/Views/PostCreateView.swift @@ -0,0 +1,104 @@ +import SwiftUI +import RadrootsKit + +struct PostCreateView: 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 + } + } + } + } +} diff --git a/Radroots/Views/PostDetailView.swift b/Radroots/Views/PostDetailView.swift @@ -0,0 +1,58 @@ +import SwiftUI +import RadrootsKit + +struct PostDetailView: View { + let post: NostrPostEventMetadata + @State private var showCopied = false + + var body: some View { + List { + Section("Content") { + Text(post.post.content) + .frame(maxWidth: .infinity, alignment: .leading) + .textSelection(.enabled) + } + + Section("Event") { + CopyRow(title: "Author", value: post.author) { showCopied = true } + CopyRow(title: "Event ID", value: post.id) { showCopied = true } + + LabeledContent("Published") { + VStack(alignment: .trailing, spacing: 2) { + Text(absoluteDate) + Text(relativeDate) + .foregroundStyle(.secondary) + .font(.footnote) + } + } + } + } + .listStyle(.insetGrouped) + .inlineNavigationTitle("Post") + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + ShareLink(item: post.post.content) { + Image(systemName: "square.and.arrow.up") + } + } + } + .toast(isPresented: $showCopied) { + Text("Copied") + .padding(.horizontal, 12) + .padding(.vertical, 8) + .background(.thinMaterial, in: Capsule()) + } + } + + private var absoluteDate: String { + let d = Date(timeIntervalSince1970: TimeInterval(post.publishedAt)) + return d.formatted(date: .abbreviated, time: .shortened) + } + + private var relativeDate: String { + let d = Date(timeIntervalSince1970: TimeInterval(post.publishedAt)) + let f = RelativeDateTimeFormatter() + f.unitsStyle = .full + return f.localizedString(for: d, relativeTo: Date()) + } +} diff --git a/Radroots/Views/PostFeedView.swift b/Radroots/Views/PostFeedView.swift @@ -0,0 +1,107 @@ +import SwiftUI +import RadrootsKit + +struct PostFeedView: View { + @EnvironmentObject private var app: AppState + @State private var posts: [NostrPostEventMetadata] = [] + @State private var isLoading = false + @State private var errorMessage: String? + + var body: some View { + List { + if let e = errorMessage { + Section { + Text(e).foregroundStyle(.red).font(.footnote) + } + } + Section { + ForEach(posts, id: \.id) { item in + NavigationLink { + PostDetailView(post: item) + } label: { + PostRow(post: item) + } + } + } footer: { + if app.relayConnectedCount == 0 { + Text("No relays connected. Configure and connect to load posts.") + } + } + } + .listStyle(.insetGrouped) + .inlineNavigationTitle("Feed") + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + if isLoading { ProgressView() } + } + ToolbarItem(placement: .topBarTrailing) { + Button { + Task { await load() } + } label: { + Image(systemName: "arrow.clockwise") + } + .disabled(isLoading) + } + } + .task { if posts.isEmpty { await load() } } + .refreshable { await load() } + } + + private func load() async { + guard let rt = app.radroots.runtime else { return } + await MainActor.run { + isLoading = true + errorMessage = nil + } + do { + let fetched = try rt.nostrFetchTextNotes(limit: 50, sinceUnix: nil) + let sorted = fetched.sorted { $0.publishedAt > $1.publishedAt } + await MainActor.run { + posts = sorted + isLoading = false + } + } catch { + await MainActor.run { + errorMessage = String(describing: error) + isLoading = false + } + } + } +} + +private struct PostRow: View { + let post: NostrPostEventMetadata + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + Text(post.post.content) + .font(.body) + .multilineTextAlignment(.leading) + HStack(spacing: 8) { + Text(shortAuthor) + .font(.footnote.monospaced()) + .foregroundStyle(.secondary) + Text("·") + .foregroundStyle(.secondary) + Text(dateText) + .font(.footnote) + .foregroundStyle(.secondary) + } + } + .padding(.vertical, 4) + } + + private var dateText: String { + let d = Date(timeIntervalSince1970: TimeInterval(post.publishedAt)) + return d.formatted(date: .abbreviated, time: .shortened) + } + + private var shortAuthor: String { + let s = post.author + let n = s.count + if n <= 12 { return s } + let prefix = s.prefix(6) + let suffix = s.suffix(6) + return "\(prefix)…\(suffix)" + } +} diff --git a/Radroots/Views/PostView.swift b/Radroots/Views/PostView.swift @@ -1,104 +0,0 @@ -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 - } - } - } - } -} diff --git a/Radroots/Views/ProfileView.swift b/Radroots/Views/ProfileView.swift @@ -112,9 +112,9 @@ public struct ProfileView: View { guard let rt = app.radroots.runtime else { return } isLoading = true Task { - let prof = rt.nostrProfileForSelf() + let meta = rt.nostrProfileForSelf() await MainActor.run { - self.original = OriginalProfile.from(prof) + self.original = OriginalProfile.from(meta) self.name = original.name self.displayName = original.displayName self.nip05 = original.nip05 @@ -163,12 +163,12 @@ private struct OriginalProfile: Equatable { static let empty = OriginalProfile(name: "", displayName: "", nip05: "", about: "") - static func from(_ p: NostrProfile?) -> OriginalProfile { + static func from(_ p: NostrProfileEventMetadata?) -> OriginalProfile { OriginalProfile( - name: p?.name ?? "", - displayName: p?.displayName ?? "", - nip05: p?.nip05 ?? "", - about: p?.about ?? "" + name: p?.profile.name ?? "", + displayName: p?.profile.displayName ?? "", + nip05: p?.profile.nip05 ?? "", + about: p?.profile.about ?? "" ) } }