use std::collections::HashSet;
use std::sync::Arc;
use leptos::prelude::*;
use orbital_macros::component_doc;
use crate::{
locale_signal, resolve_discussion_locale, thread_has_streaming_reply, DiscussionAppearance,
DiscussionAttachmentValidation, DiscussionCitationAffordance, DiscussionCitationMenu,
DiscussionCitationMenuExtras, DiscussionEmptyView, DiscussionEvents, DiscussionFeatures,
DiscussionFocus, DiscussionLocale, DiscussionProvider, DiscussionRenderers,
DiscussionReplyFooterRegion, DiscussionReplyGraph, DiscussionReplyMenu,
DiscussionReplyMenuExtras, DiscussionReplyMeta, DiscussionReplyPart, DiscussionSlots,
DiscussionSort, DiscussionThreadToolbar, DiscussionViewMode, DEFAULT_MAX_VISIBLE_DEPTH,
};
use super::{
DiscussionComposerRoot, DiscussionDefaultEmptyView, DiscussionReplyList,
DiscussionThreadLoadingOverlay, DiscussionThreadRoot,
};
#[component_doc(
category = "Discussion",
preview_slug = "discussion",
preview_label = "Discussion Thread",
preview_icon = icondata::AiCommentOutlined,
)]
#[component]
pub fn DiscussionThread(
replies: Signal<Vec<crate::DiscussionReply>>,
#[prop(optional)]
focus: Option<Signal<DiscussionFocus>>,
#[prop(optional)]
set_focus: Option<WriteSignal<DiscussionFocus>>,
#[prop(optional)]
view_mode: Option<Signal<DiscussionViewMode>>,
#[prop(optional)]
set_view_mode: Option<WriteSignal<DiscussionViewMode>>,
#[prop(optional)]
sort: Option<Signal<DiscussionSort>>,
#[prop(optional)]
events: Option<DiscussionEvents>,
#[prop(default = DEFAULT_MAX_VISIBLE_DEPTH)]
max_visible_depth: u32,
#[prop(optional)]
renderers: Option<DiscussionRenderers>,
#[prop(optional)]
reply_to: Option<Signal<Option<String>>>,
#[prop(optional)]
set_reply_to: Option<WriteSignal<Option<String>>>,
#[prop(optional)]
collapsed: Option<Signal<HashSet<String>>>,
#[prop(optional)]
set_collapsed: Option<WriteSignal<HashSet<String>>>,
#[prop(optional)]
composer_disabled: Option<Signal<bool>>,
#[prop(default = DiscussionFeatures::default())]
features: DiscussionFeatures,
#[prop(default = None)]
loading: Option<Signal<bool>>,
#[prop(default = None)]
attachment_validation: Option<DiscussionAttachmentValidation>,
#[prop(optional)]
current_user_id: Option<Signal<Option<String>>>,
#[prop(optional)]
appearance: Option<Signal<DiscussionAppearance>>,
#[prop(optional)]
locale: Option<DiscussionLocale>,
#[prop(optional)]
locale_read: Option<ReadSignal<DiscussionLocale>>,
#[prop(optional, into)] class: MaybeProp<String>,
#[prop(optional)]
discussion_composer_slot: Option<crate::DiscussionComposerSlot>,
#[prop(optional)]
discussion_empty_view: Option<DiscussionEmptyView>,
#[prop(optional)]
discussion_thread_toolbar: Option<DiscussionThreadToolbar>,
#[prop(optional)]
discussion_composer_tools: Option<crate::DiscussionComposerTools>,
#[prop(optional)]
discussion_composer_hint: Option<crate::DiscussionComposerHint>,
#[prop(optional)]
discussion_reply_meta: Option<DiscussionReplyMeta>,
#[prop(optional)]
discussion_reply_footer_region: Option<DiscussionReplyFooterRegion>,
#[prop(optional)]
discussion_reply_part: Option<DiscussionReplyPart>,
#[prop(optional)]
discussion_reply_menu: Option<DiscussionReplyMenu>,
#[prop(optional)]
discussion_reply_menu_extras: Option<DiscussionReplyMenuExtras>,
#[prop(optional)]
discussion_citation_affordance: Option<DiscussionCitationAffordance>,
#[prop(optional)]
discussion_citation_menu_extras: Option<DiscussionCitationMenuExtras>,
#[prop(optional)]
discussion_citation_menu: Option<DiscussionCitationMenu>,
) -> impl IntoView {
let (focus_internal, set_focus_internal) = signal(DiscussionFocus::Root);
let focus = focus.unwrap_or_else(|| focus_internal.into());
let set_focus = set_focus.unwrap_or(set_focus_internal);
let (view_mode_internal, set_view_mode_internal) = signal(DiscussionViewMode::Tree);
let view_mode = view_mode.unwrap_or_else(|| view_mode_internal.into());
let set_view_mode = set_view_mode.unwrap_or(set_view_mode_internal);
let (collapsed_internal, set_collapsed_internal) = signal(HashSet::<String>::new());
let collapsed = collapsed.unwrap_or_else(|| collapsed_internal.into());
let set_collapsed = set_collapsed.unwrap_or(set_collapsed_internal);
let (reply_to_internal, set_reply_to_internal) = signal(None::<String>);
let reply_to = reply_to.unwrap_or_else(|| reply_to_internal.into());
let set_reply_to = set_reply_to.unwrap_or(set_reply_to_internal);
let sort = sort.unwrap_or_else(|| Signal::derive(|| DiscussionSort::OldestFirst));
let mut events = events.unwrap_or_default();
if events.on_reply_click.is_none() {
events.on_reply_click = Some(Callback::new(move |id| {
set_reply_to.set(Some(id));
}));
}
let events = StoredValue::new(events);
let slots = DiscussionSlots::from_slot_props(
discussion_composer_slot,
discussion_empty_view,
discussion_thread_toolbar,
discussion_composer_tools,
discussion_composer_hint,
discussion_reply_meta,
discussion_reply_footer_region,
discussion_reply_part,
discussion_reply_menu,
discussion_reply_menu_extras,
discussion_citation_affordance,
discussion_citation_menu_extras,
discussion_citation_menu,
);
#[allow(deprecated)]
let renderers = StoredValue::new(DiscussionRenderers::merge_with_slots(&slots, renderers));
let loading = loading.unwrap_or_else(|| Signal::derive(|| false));
let attachment_validation_for_composer = attachment_validation.clone();
let on_attachment_reject_for_composer =
Callback::new(move |rejections: Vec<(String, String)>| {
events.with_value(|e| e.notify_attachment_reject(rejections));
});
let host_composer_disabled = composer_disabled;
let streaming_blocked = Signal::derive(move || thread_has_streaming_reply(&replies.get()));
let composer_disabled_signal = Signal::derive(move || {
host_composer_disabled
.map(|signal| signal.get())
.unwrap_or(false)
|| streaming_blocked.get()
});
let mut slots = slots;
if slots.composer.is_none() {
let empty: ChildrenFn = Arc::new(|| view! { () }.into_any());
let composer_tools = slots
.composer_tools
.as_ref()
.map(|slot| slot.children.clone())
.unwrap_or_else(|| empty.clone());
let composer_hint = slots
.composer_hint
.as_ref()
.map(|slot| slot.children.clone())
.unwrap_or_else(|| empty);
slots.composer = Some(crate::DiscussionComposerSlot {
children: Arc::new(move || {
view! {
<DiscussionComposerRoot
reply_to=reply_to
set_reply_to=set_reply_to
disabled=Some(composer_disabled_signal)
attachment_validation=attachment_validation_for_composer.clone()
on_attachment_reject=Some(on_attachment_reject_for_composer)
composer_tools=composer_tools.clone()
composer_hint=composer_hint.clone()
/>
}
.into_any()
}),
});
}
let empty_slot = slots.empty.as_ref().map(|slot| slot.children.clone());
let graph = Memo::new(move |_| DiscussionReplyGraph::from_flat(&replies.get()));
let (_locale_internal_rw, locale_internal_read) =
locale_signal(resolve_discussion_locale(locale));
let locale_read = locale_read.unwrap_or(locale_internal_read);
let current_user_id = current_user_id.unwrap_or_else(|| Signal::derive(|| None::<String>));
let appearance = appearance.unwrap_or_else(|| Signal::derive(|| DiscussionAppearance::Surface));
let is_empty = Memo::new(move |_| replies.get().is_empty());
view! {
<DiscussionProvider
replies=replies
graph=graph
locale=locale_read
focus=focus
set_focus=set_focus
view_mode=view_mode
set_view_mode=set_view_mode
collapsed=collapsed
set_collapsed=set_collapsed
features=features
max_visible_depth=max_visible_depth
renderers=renderers
events=events
current_user_id=current_user_id
appearance=appearance
>
<DiscussionThreadRoot class=class slots=slots>
<Show when=move || loading.get()>
<DiscussionThreadLoadingOverlay />
</Show>
<Show
when=move || !is_empty.get() && !loading.get()
fallback=move || {
if loading.get() {
return None;
}
if let Some(children) = empty_slot.as_ref() {
Some(children())
} else {
Some(view! { <DiscussionDefaultEmptyView /> }.into_any())
}
}
>
<DiscussionReplyList
graph=graph
focus=focus
view_mode=view_mode
sort=sort
max_visible_depth=max_visible_depth
features=features
/>
</Show>
</DiscussionThreadRoot>
</DiscussionProvider>
}
}