a190d507ed
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
103 lines
4.0 KiB
Swift
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) }
|
|
}
|
|
}
|