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:
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 ?? ""
)
}
}