yggdrasil/components/comments/
section.rs1use dioxus::prelude::*;
7
8use crate::api::comments::{check_pending_status, get_comments, CommentTreeResponse};
9use crate::components::comments::form::CommentForm;
10use crate::components::comments::list::CommentList;
11use crate::components::skeletons::comment_skeleton::CommentListSkeleton;
12use crate::components::skeletons::delayed_skeleton::DelayedSkeleton;
13use crate::utils::comment_storage::{self, PendingComment};
14use crate::utils::time::sleep_ms;
15
16const PENDING_POLL_INTERVAL_MS: u32 = 30_000;
21
22#[derive(Clone, Copy)]
29pub struct CommentContext {
30 pub active_reply: Signal<Option<i64>>,
32 pub refresh_trigger: Signal<bool>,
34 pub pending_comments: Signal<Vec<PendingComment>>,
36}
37
38#[component]
49pub fn CommentSection(post_id: i32) -> Element {
50 let mut ctx = use_context_provider(|| CommentContext {
51 active_reply: Signal::new(None),
52 refresh_trigger: Signal::new(false),
53 pending_comments: Signal::new(Vec::new()),
54 });
55
56 use_effect(move || {
58 let pending = comment_storage::load_pending_comments(post_id);
59 comment_storage::prune_all_expired();
60 ctx.pending_comments.set(pending);
61 });
62
63 let _pending_poll = use_resource(move || {
76 let mut pending_comments = ctx.pending_comments;
77 let mut refresh_trigger = ctx.refresh_trigger;
78 async move {
79 loop {
80 let ids: Vec<i64> = pending_comments.read().iter().map(|c| c.id).collect();
81 if ids.is_empty() {
82 return;
84 }
85
86 if let Ok(statuses) = check_pending_status(ids).await {
87 let to_remove: Vec<i64> = statuses
88 .into_iter()
89 .filter(|s| s.status != "pending")
90 .map(|s| s.id)
91 .collect();
92 if !to_remove.is_empty() {
93 comment_storage::remove_pending_ids(post_id, &to_remove);
94 let next = !*refresh_trigger.peek();
97 refresh_trigger.set(next);
98 pending_comments
99 .write()
100 .retain(|c| !to_remove.contains(&c.id));
101 }
102 }
103 sleep_ms(PENDING_POLL_INTERVAL_MS).await;
106 }
107 }
108 });
109
110 let comments_resource = use_resource(move || {
112 let _ = (ctx.refresh_trigger)();
113 async move { get_comments(post_id).await }
114 });
115
116 use_effect(move || {
121 let data = comments_resource.read();
122 if let Some(Ok(CommentTreeResponse { comments, .. })) = &*data {
123 let approved_ids: std::collections::HashSet<i64> =
124 comments.iter().map(|c| c.id).collect();
125 let to_remove: Vec<i64> = ctx
126 .pending_comments
127 .read()
128 .iter()
129 .filter(|p| approved_ids.contains(&p.id))
130 .map(|p| p.id)
131 .collect();
132 if !to_remove.is_empty() {
133 comment_storage::remove_pending_ids(post_id, &to_remove);
134 ctx.pending_comments
135 .write()
136 .retain(|p| !to_remove.contains(&p.id));
137 }
138 }
139 });
140
141 let data = comments_resource.read();
142
143 let total_count = if let Some(Ok(CommentTreeResponse { count, .. })) = &*data {
145 let approved_count = *count;
146 let pending_count = ctx.pending_comments.read().len() as i64;
147 Some(approved_count + pending_count)
148 } else {
149 None
150 };
151
152 rsx! {
153 div { class: "space-y-8",
154 if let Some(count) = total_count {
156 h2 { class: "text-xl font-bold text-paper-primary", "评论区 ({count})" }
157 } else {
158 h2 { class: "text-xl font-bold text-paper-primary", "评论区" }
159 }
160
161 CommentForm { post_id, parent_id: None, parent_indent: None }
163
164 match &*data {
166 Some(Ok(CommentTreeResponse { comments, .. })) => {
167 let approved_count = comments.len();
168 let pending_count = ctx.pending_comments.read().len();
169 let has_any = approved_count > 0 || pending_count > 0;
170 if !has_any {
171 rsx! {
172 p { class: "text-paper-tertiary text-center py-8", "暂无评论,成为第一个评论的人吧!" }
173 }
174 } else {
175 rsx! {
176 CommentList {
177 comments: comments.clone(),
178 pending: ctx.pending_comments.read().clone(),
179 post_id,
180 }
181 }
182 }
183 }
184 Some(Err(_)) => rsx! {
185 div { class: "text-center text-red-500 dark:text-red-400 py-8", "评论加载失败" }
186 },
187 None => rsx! {
188 DelayedSkeleton { CommentListSkeleton {} }
189 },
190 }
191 }
192 }
193}