Files
andros a190d507ed Add delete/edit post, search, notification actions, mood/client display, profile groups, group compose
Library:
- PostWriter.deletePost and editPost (by heading or :ID: timestamp)
- PostWriterError.postNotFound
- RelayClient.searchPostURLs for /search/?q= and /search/?tag=
- 15 new tests (132 total, all passing)

App:
- Swipe left on own posts reveals Delete (with confirmation) and Edit actions
- EditPostView sheet pre-filled with current body; preserves all properties
- PostActionsViewModel handles fetch→mutate→upload flow
- SearchView + SearchViewModel: full-text and tag search via relay
- Search accessible via magnifying glass in Timeline toolbar
- NotificationsView: summary count header, View Thread and Reply buttons per row
- PostRowView: mood emoji shown on regular posts, client name shown as "via …"
- ProfileView: groups section from #+GROUP: entries
- GroupsView: compose button for group posts
- ComposeView/ViewModel: group parameter support, "Group Post" title
2026-04-20 07:57:10 +02:00

103 lines
4.0 KiB
Swift

import SwiftUI
import OrgSocialKit
struct GroupsView: View {
@State private var viewModel = GroupsViewModel()
var body: some View {
NavigationStack {
Group {
if viewModel.isLoadingGroups {
ProgressView()
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else if let error = viewModel.groupsError {
ContentUnavailableView {
Label("Groups unavailable", systemImage: "person.3.slash")
} description: {
Text(error)
} actions: {
Button("Retry") { Task { await viewModel.loadGroups() } }
.buttonStyle(.bordered)
}
} else if viewModel.groups.isEmpty {
ContentUnavailableView(
"No groups",
systemImage: "person.3",
description: Text("No groups are registered on this relay.")
)
} else {
List(viewModel.groups) { group in
NavigationLink(value: group) {
Label(group.name, systemImage: "person.3.fill")
}
}
}
}
.navigationTitle("Groups")
.task { await viewModel.loadGroups() }
.navigationDestination(for: GroupsViewModel.GroupItem.self) { group in
GroupPostsView(group: group, viewModel: viewModel)
}
}
}
}
private struct GroupPostsView: View {
let group: GroupsViewModel.GroupItem
var viewModel: GroupsViewModel
@State private var showCompose = false
var body: some View {
Group {
if viewModel.isLoadingPosts {
ProgressView()
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else if let error = viewModel.postsError {
ContentUnavailableView {
Label("Could not load posts", systemImage: "exclamationmark.triangle")
} description: {
Text(error)
} actions: {
Button("Retry") { Task { await viewModel.loadPosts(for: group) } }
.buttonStyle(.bordered)
}
} else if viewModel.posts.isEmpty {
ContentUnavailableView(
"No posts",
systemImage: "bubble.left.and.bubble.right",
description: Text("This group has no posts yet.")
)
} else {
List {
ForEach(viewModel.posts, id: \.timestamp) { post in
PostRowView(post: post)
.listRowSeparator(.visible)
.listRowSeparatorTint(Color.secondary.opacity(0.2))
.listRowInsets(EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16))
.listRowBackground(Color.clear)
}
}
.listStyle(.plain)
.navigationDestination(for: URL.self) { ProfileView(feedURL: $0) }
.navigationDestination(for: ThreadRoute.self) {
ThreadView(postURL: $0.postURL, relayURL: $0.relayURL)
}
}
}
.navigationTitle(group.name)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button { showCompose = true } label: {
Image(systemName: "square.and.pencil")
}
}
}
.sheet(isPresented: $showCompose) {
let relayBase = UserDefaults.standard.string(forKey: "relayURL") ?? "https://relay.org-social.org"
ComposeView(group: "\(group.name) \(relayBase)")
}
.task { await viewModel.loadPosts(for: group) }
}
}