1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
use *;
use component_doc;
/// Controlled state, [`DiscussionAdapter`] trait, and host wiring patterns.
///
/// # When to use
///
/// - Connecting a discussion thread to server actions or a custom backend adapter.
/// - Verifying [`use_discussion`] hooks against controlled reply and focus signals.
///
/// # Usage
///
/// 1. Hold replies in a controlled `Signal<Vec<DiscussionReply>>`.
/// 2. Implement [`DiscussionAdapter::submit_reply`] in your app (HTTP, server fn, etc.).
/// 3. Bridge composer submit via [`DiscussionEvents::on_submit`] — call the adapter
/// asynchronously and push the returned reply into your signal.
/// 4. Optionally pass [`DiscussionAttachmentValidation`] and wire
/// `events.on_attachment_reject` on [`DiscussionThread`] for client-side attach rules; re-validate
/// attachments in [`DiscussionAdapter::submit_reply`] before persisting.
///
/// Only `submit_reply` is required. Override [`DiscussionAdapter::fetch_branch`] when
/// your backend supports lazy branch loading (`hidden_child_count`, show-more drill-in).
/// Override [`DiscussionAdapter::upload_attachment`] when the composer should persist
/// attachment drafts to host storage before submit.
///
/// # Composer content flow
///
/// 1. User composes markdown in the composer (`DiscussionComposerFormatToolbar` optional).
/// 2. Attachment and citation drafts live in [`ComposerContext`] until submit.
/// 3. [`DiscussionEvents::on_submit`] receives [`DiscussionComposerSubmit`] with
/// `body_markdown`, `attachments`, and `citations`.
/// 4. Host adapter uploads files (if needed) and builds `DiscussionPart::File` entries
/// with real URLs — not `blob:` placeholders.
/// 5. Host adapter persists the citations array on the reply record.
/// 6. Host pushes the normalized [`DiscussionReply`] into the controlled `replies` signal.
///
/// The crate does not upload files or invent citation URLs; that remains host-owned
/// (same contract as Phase 3b adapter wiring).
///
/// # Performance
///
/// * [`DiscussionThread`](crate::DiscussionThread) builds [`DiscussionReplyGraph`] once per
/// reply-list change via a Leptos `Memo` and stores it on [`DiscussionContext::graph`](crate::DiscussionContext::graph).
/// * Custom descendants should call `ctx.graph.get()` — avoid `DiscussionReplyGraph::from_flat`
/// inside derived signals or per-row render paths.
/// * Reply lists iterate with keyed `<For>` over stable reply ids. For very large threads,
/// prefer granular subscriptions over cloning the full `replies` vector in leaf components.
///
/// # See also
///
/// * [`DiscussionLocalizationDoc`](crate::products::discussion::docs::localization::DiscussionLocalizationDoc)
/// * [`DiscussionComposerDoc`](crate::products::discussion::docs::composer::DiscussionComposerDoc)
/// * [`DiscussionEventsDoc`](crate::products::discussion::docs::events::DiscussionEventsDoc)
///
/// # Best Practices
///
/// ## Do's
///
/// * Store the adapter as `Arc<dyn DiscussionAdapter>` and clone it into async callbacks.
/// * Use `leptos::task::spawn_local` for adapter futures in CSR previews and host apps.
/// * Inspect [`DiscussionError::retryable`] before re-submitting a failed draft.
///
/// ## Don'ts
///
/// * Do not expect the crate to call your adapter automatically from the composer.
/// * Do not add HTTP or WebSocket clients inside `orbital-discussion`.
///
/// # Examples
///
/// ## Hooks + mock adapter submit
/// Controlled replies signal, `use_discussion` hook count, and in-memory adapter bridge.
/// <!-- preview -->
/// ```rust,ignore
/// use std::sync::Arc;
/// use crate::preview::fixtures::{sample_thread, PREVIEW_VIEWER_AUTHOR_ID};
/// use crate::preview::mock_adapter::MockDiscussionAdapter;
/// use crate::{DiscussionAdapter, DiscussionEvents, DiscussionFeatures, DiscussionFocus, DiscussionThread, DiscussionViewMode};
/// use leptos::prelude::*;
/// use leptos::task::spawn_local;
/// use std::collections::HashSet;
/// let (replies, set_replies) = signal(sample_thread());
/// let (focus, set_focus) = signal(DiscussionFocus::Root);
/// let (view_mode, set_view_mode) = signal(DiscussionViewMode::Tree);
/// let (collapsed, set_collapsed) = signal(HashSet::<String>::new());
/// let (reply_to, set_reply_to) = signal(None::<String>);
/// let (last_error, set_last_error) = signal(String::new());
/// let adapter: Arc<dyn DiscussionAdapter> = Arc::new(MockDiscussionAdapter::preview_viewer());
/// let current_user_id = Signal::derive(|| Some(PREVIEW_VIEWER_AUTHOR_ID.to_string()));
/// let events = DiscussionEvents {
/// on_reply_click: Some(Callback::new(move |id| set_reply_to.set(Some(id)))),
/// on_submit: Some(Callback::new({
/// let adapter = Arc::clone(&adapter);
/// move |draft| {
/// let adapter = Arc::clone(&adapter);
/// spawn_local(async move {
/// match adapter.submit_reply(draft).await {
/// Ok(reply) => set_replies.update(|list| list.push(reply)),
/// Err(err) => set_last_error.set(err.message),
/// }
/// });
/// }
/// })),
/// ..Default::default()
/// };
/// let features = DiscussionFeatures::MARKDOWN | DiscussionFeatures::FOCUS_NAVIGATION;
/// view! {
/// <div data-testid="discussion-integration-preview">
/// <span data-testid="discussion-hook-reply-count">{move || replies.get().len()}</span>
/// <pre data-testid="discussion-integration-error">{move || last_error.get()}</pre>
/// <DiscussionThread
/// replies=Signal::derive(move || replies.get())
/// focus=Signal::derive(move || focus.get())
/// set_focus=set_focus
/// view_mode=Signal::derive(move || view_mode.get())
/// set_view_mode=set_view_mode
/// collapsed=Signal::derive(move || collapsed.get())
/// set_collapsed=set_collapsed
/// reply_to=Signal::derive(move || reply_to.get())
/// set_reply_to=set_reply_to
/// current_user_id=current_user_id
/// events=events
/// features=features
/// />
/// </div>
/// }
/// ```