fix(leptos): resolve Phase 3 compilation errors
- Add PartialEq to MessageRecord and all nested types in shared-types - Fix String references in view! macro by cloning owned values - Fix match arm types with .into_any() in app.rs - Clone on_load_more before FnMut closure in message_feed.rs - Pre-compute class strings before passing to view! in message_card.rs - Fix lifetime issues with imgs/vids/cats by using owned Vec<AttachmentRef> - Use separate closures for different event types in mod.rs - Fix disabled property to use signal pattern - Fix filter chip closure signatures
This commit is contained in:
@@ -92,9 +92,9 @@ pub fn App() -> impl IntoView {
|
||||
// Content area
|
||||
<div style="flex: 1; overflow: auto; padding: 1.5rem;">
|
||||
{move || match ui.active_tab.get() {
|
||||
Tab::Messages => view! { <MessagesPanel /> }.into_view(),
|
||||
Tab::Live => view! { <div>"Live Panel"</div> }.into_view(),
|
||||
Tab::Dashboard => view! { <div>"Dashboard Panel"</div> }.into_view(),
|
||||
Tab::Messages => view! { <MessagesPanel /> }.into_any(),
|
||||
Tab::Live => view! { <div>"Live Panel"</div> }.into_any(),
|
||||
Tab::Dashboard => view! { <div>"Dashboard Panel"</div> }.into_any(),
|
||||
}}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
+115
-64
@@ -1,6 +1,6 @@
|
||||
use leptos::prelude::*;
|
||||
use regex::Regex;
|
||||
use shared_types::message::{AiSeverity, AiStatus, MessageRecord};
|
||||
use shared_types::message::{AiSeverity, AiStatus, AttachmentRef, MessageRecord};
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
@@ -84,13 +84,13 @@ fn get_cats(raw: &Option<Vec<String>>) -> Vec<String> {
|
||||
// ─── StatusBadgeInline ────────────────────────────────────
|
||||
#[component]
|
||||
fn StatusBadgeInline(status: AiStatus) -> impl IntoView {
|
||||
let (cl, icon_svg) = match &status {
|
||||
AiStatus::Clean => ("status-badge-clean", Some(view! { <svg class="h-3 w-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M10 15.586L6.707 12.293a1 1 0 00-1.414 1.414l4 4a1 1 0 001.414 0l8-8a1 1 0 10-1.414-1.414L10 15.586z"></path></svg> }).into_any()),
|
||||
AiStatus::Flagged => ("status-badge-flagged", Some(view! { <svg class="h-3 w-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line></svg> }).into_any()),
|
||||
AiStatus::Error => ("status-badge-error", Some(view! { <svg class="h-3 w-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line></svg> }).into_any()),
|
||||
AiStatus::Pending => ("status-badge-pending", None.into_any()),
|
||||
AiStatus::Processing => ("status-badge-processing", None.into_any()),
|
||||
AiStatus::Warn => ("status-badge-warn", None.into_any()),
|
||||
let (cl, icon_svg): (&'static str, AnyView) = match &status {
|
||||
AiStatus::Clean => ("status-badge-clean", view! { <svg class="h-3 w-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M10 15.586L6.707 12.293a1 1 0 00-1.414 1.414l4 4a1 1 0 001.414 0l8-8a1 1 0 10-1.414-1.414L10 15.586z"></path></svg> }.into_any()),
|
||||
AiStatus::Flagged => ("status-badge-flagged", view! { <svg class="h-3 w-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line></svg> }.into_any()),
|
||||
AiStatus::Error => ("status-badge-error", view! { <svg class="h-3 w-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line></svg> }.into_any()),
|
||||
AiStatus::Pending => ("status-badge-pending", view! { }.into_any()),
|
||||
AiStatus::Processing => ("status-badge-processing", view! { }.into_any()),
|
||||
AiStatus::Warn => ("status-badge-warn", view! { }.into_any()),
|
||||
};
|
||||
view! {
|
||||
<span class=format!("status-badge {}", cl)>
|
||||
@@ -125,20 +125,20 @@ pub fn MessageRow(
|
||||
// Attachments
|
||||
let all_atts = message.metadata.as_ref()
|
||||
.and_then(|m| m.attachments.as_ref()).cloned().unwrap_or_default();
|
||||
let imgs: Vec<_> = all_atts.iter().filter(|a| {
|
||||
let imgs: Vec<AttachmentRef> = all_atts.iter().filter(|a| {
|
||||
a.content_type.as_deref().map(|ct| ct.starts_with("image/")).unwrap_or(false)
|
||||
|| a.name.to_lowercase().ends_with(".png")
|
||||
|| a.name.to_lowercase().ends_with(".jpg")
|
||||
|| a.name.to_lowercase().ends_with(".jpeg")
|
||||
|| a.name.to_lowercase().ends_with(".gif")
|
||||
|| a.name.to_lowercase().ends_with(".webp")
|
||||
}).collect();
|
||||
let vids: Vec<_> = all_atts.iter().filter(|a| {
|
||||
}).cloned().collect();
|
||||
let vids: Vec<AttachmentRef> = all_atts.iter().filter(|a| {
|
||||
a.content_type.as_deref().map(|ct| ct.starts_with("video/")).unwrap_or(false)
|
||||
|| a.name.to_lowercase().ends_with(".mp4")
|
||||
|| a.name.to_lowercase().ends_with(".webm")
|
||||
|| a.name.to_lowercase().ends_with(".mov")
|
||||
}).collect();
|
||||
}).cloned().collect();
|
||||
|
||||
let stickers = message.metadata.as_ref()
|
||||
.and_then(|m| m.stickers.as_ref()).cloned().unwrap_or_default();
|
||||
@@ -180,9 +180,10 @@ pub fn MessageRow(
|
||||
{/* Content */}
|
||||
{show.then(|| {
|
||||
let rendered = render_emojis(display);
|
||||
let cls = if message.deleted_at.is_some() { "text-secondary/60" } else { "" };
|
||||
let cls_str = if message.deleted_at.is_some() { "text-secondary/60" } else { "" };
|
||||
let class_str = format!("whitespace-pre-wrap break-words text-sm leading-6 {}", cls_str);
|
||||
view! {
|
||||
<p class=format!("whitespace-pre-wrap break-words text-sm leading-6 {}", cls)>
|
||||
<p class=class_str>
|
||||
{rendered.into_iter().collect::<Vec<_>>()}
|
||||
</p>
|
||||
}
|
||||
@@ -192,17 +193,22 @@ pub fn MessageRow(
|
||||
{(!stickers.is_empty()).then(|| view! {
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{stickers.iter().map(|s| {
|
||||
let url = s.url.clone();
|
||||
let name = s.name.clone().unwrap_or_default();
|
||||
let url_owned = s.url.clone().unwrap_or_default();
|
||||
let name_owned = s.name.clone().unwrap_or_default();
|
||||
let has_url = !url_owned.is_empty();
|
||||
view! {
|
||||
<div>
|
||||
{url.as_ref().map(|u| view! {
|
||||
<img src=u alt=&name class="h-12 w-12 rounded-lg border border-border object-contain bg-surface/50" loading="lazy" />
|
||||
}).unwrap_or_else(|| view! {
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-lg border border-border bg-surface/50">
|
||||
"😊"
|
||||
</div>
|
||||
})}
|
||||
{if has_url {
|
||||
view! {
|
||||
<img src=url_owned alt=name_owned class="h-12 w-12 rounded-lg border border-border object-contain bg-surface/50" loading="lazy" />
|
||||
}.into_any()
|
||||
} else {
|
||||
view! {
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-lg border border-border bg-surface/50">
|
||||
"😊"
|
||||
</div>
|
||||
}.into_any()
|
||||
}}
|
||||
</div>
|
||||
}
|
||||
}).collect::<Vec<_>>()}
|
||||
@@ -210,55 +216,94 @@ pub fn MessageRow(
|
||||
})}
|
||||
|
||||
{/* Images */}
|
||||
{(!imgs.is_empty()).then(|| view! {
|
||||
<div class="flex gap-2 overflow-x-auto">
|
||||
{imgs.iter().take(4).map(|a| view! {
|
||||
<a href=&a.url target="_blank" class="shrink-0 overflow-hidden rounded-lg border border-border">
|
||||
<img src=&a.url alt=&a.name class="h-16 w-16 object-cover hover:scale-105 transition-transform" loading="lazy" />
|
||||
{if !imgs.is_empty() {
|
||||
let imgs_local = imgs.clone();
|
||||
let images_view = imgs_local.iter().take(4).map(|a| {
|
||||
let url1 = a.url.clone();
|
||||
let url2 = a.url.clone();
|
||||
let name1 = a.name.clone();
|
||||
view! {
|
||||
<a href=url1 target="_blank" class="shrink-0 overflow-hidden rounded-lg border border-border">
|
||||
<img src=url2 alt=name1 class="h-16 w-16 object-cover hover:scale-105 transition-transform" loading="lazy" />
|
||||
</a>
|
||||
}).collect::<Vec<_>>()}
|
||||
{(imgs.len() > 4).then(|| view! {
|
||||
}
|
||||
}).collect::<Vec<_>>();
|
||||
let overflow = if imgs.len() > 4 {
|
||||
let extra = imgs.len() - 4;
|
||||
view! {
|
||||
<div class="flex h-16 w-16 items-center justify-center rounded-lg border border-border bg-surface text-xs text-secondary">
|
||||
<span>{"+"} {imgs.len() - 4}</span> <span class="ml-0.5">"🖼"</span>
|
||||
<span>{"+"} {extra}</span> <span class="ml-0.5">"🖼"</span>
|
||||
</div>
|
||||
})}
|
||||
</div>
|
||||
})}
|
||||
}.into_any()
|
||||
} else {
|
||||
view! {}.into_any()
|
||||
};
|
||||
view! {
|
||||
<div class="flex gap-2 overflow-x-auto">
|
||||
{images_view}
|
||||
{overflow}
|
||||
</div>
|
||||
}.into_any()
|
||||
} else {
|
||||
view! {}.into_any()
|
||||
}}
|
||||
|
||||
{/* Videos */}
|
||||
{(!vids.is_empty()).then(|| view! {
|
||||
<div class="flex gap-2 overflow-x-auto">
|
||||
{vids.iter().take(4).map(|a| view! {
|
||||
<video src=&a.url controls class="h-28 w-48 shrink-0 rounded-lg border border-border object-cover bg-black" preload="metadata"></video>
|
||||
}).collect::<Vec<_>>()}
|
||||
{(vids.len() > 4).then(|| view! {
|
||||
{if !vids.is_empty() {
|
||||
let vids_local = vids.clone();
|
||||
let videos_view = vids_local.iter().take(4).map(|a| {
|
||||
let url = a.url.clone();
|
||||
view! {
|
||||
<video src=url controls class="h-28 w-48 shrink-0 rounded-lg border border-border object-cover bg-black" preload="metadata"></video>
|
||||
}
|
||||
}).collect::<Vec<_>>();
|
||||
let overflow = if vids.len() > 4 {
|
||||
let extra = vids.len() - 4;
|
||||
view! {
|
||||
<div class="flex h-28 w-16 items-center justify-center rounded-lg border border-border bg-surface text-xs text-secondary">
|
||||
<span>{"+"} {vids.len() - 4}</span> <span class="ml-0.5">"▶"</span>
|
||||
<span>{"+"} {extra}</span> <span class="ml-0.5">"▶"</span>
|
||||
</div>
|
||||
})}
|
||||
</div>
|
||||
})}
|
||||
}.into_any()
|
||||
} else {
|
||||
view! {}.into_any()
|
||||
};
|
||||
view! {
|
||||
<div class="flex gap-2 overflow-x-auto">
|
||||
{videos_view}
|
||||
{overflow}
|
||||
</div>
|
||||
}.into_any()
|
||||
} else {
|
||||
view! {}.into_any()
|
||||
}}
|
||||
|
||||
{/* Categories */}
|
||||
{(!cats.is_empty()).then(|| view! {
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{cats.iter().map(|c| view! {
|
||||
<span class="badge badge-secondary text-xs">{c.clone()}</span>
|
||||
}).collect::<Vec<_>>()}
|
||||
</div>
|
||||
})}
|
||||
{if !cats.is_empty() {
|
||||
let cats_local = cats.clone();
|
||||
view! {
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{cats_local.iter().map(|c| view! {
|
||||
<span class="badge badge-secondary text-xs">{c.clone()}</span>
|
||||
}).collect::<Vec<_>>()}
|
||||
</div>
|
||||
}.into_any()
|
||||
} else {
|
||||
view! {}.into_any()
|
||||
}}
|
||||
|
||||
{/* AI Analysis */}
|
||||
{message.ai_analysis.as_ref().map(|analysis| {
|
||||
let border = if ai_st == AiStatus::Flagged { "border-l-3 bg-warning/5" } else { "border-l-3 bg-success/5" };
|
||||
let border_str = if ai_st == AiStatus::Flagged { "border-l-3 bg-warning/5" } else { "border-l-3 bg-success/5" };
|
||||
let icon = if ai_st == AiStatus::Flagged { "🚨" } else { "ℹ️" };
|
||||
let analysis_summary_str = analysis_summary.clone();
|
||||
let analysis_str = analysis.clone();
|
||||
view! {
|
||||
<div class=format!("rounded-lg px-3 py-2 {}", border)>
|
||||
<div class=format!("rounded-lg px-3 py-2 {}", border_str)>
|
||||
<div class="flex items-start gap-2 text-xs">
|
||||
<span class="mt-0.5 shrink-0">{icon}</span>
|
||||
<div class="min-w-0 flex-1">
|
||||
<span class="block font-medium mb-1">{&analysis_summary}</span>
|
||||
<div class="text-xs leading-relaxed whitespace-pre-wrap">{analysis}</div>
|
||||
<span class="block font-medium mb-1">{analysis_summary_str}</span>
|
||||
<div class="text-xs leading-relaxed whitespace-pre-wrap">{analysis_str}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -266,10 +311,13 @@ pub fn MessageRow(
|
||||
})}
|
||||
|
||||
{/* Error */}
|
||||
{message.ai_error.as_ref().map(|e| view! {
|
||||
<div class="rounded-lg bg-warning/5 px-3 py-2 text-xs text-warning">
|
||||
<span>"AI error: "{e}</span>
|
||||
</div>
|
||||
{message.ai_error.as_ref().map(|e| {
|
||||
let error_str = e.clone();
|
||||
view! {
|
||||
<div class="rounded-lg bg-warning/5 px-3 py-2 text-xs text-warning">
|
||||
<span>"AI error: "{error_str}</span>
|
||||
</div>
|
||||
}
|
||||
})}
|
||||
|
||||
{/* Re-analyze */}
|
||||
@@ -316,11 +364,14 @@ pub fn MessageCard(
|
||||
<img src=avatar alt="" class="h-10 w-10 shrink-0 rounded-full object-cover ring-2 ring-primary/30" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-baseline gap-2 mb-2">
|
||||
<span class="font-semibold text-sm">{&first.username}</span>
|
||||
{loc_label.as_ref().map(|l| view! {
|
||||
<span class="flex items-center gap-1 text-xs text-secondary/50 bg-surface/50 px-1.5 py-0.5 rounded-full">
|
||||
"#" " " {l}
|
||||
</span>
|
||||
<span class="font-semibold text-sm">{first.username.clone()}</span>
|
||||
{loc_label.as_ref().map(|l| {
|
||||
let label_str = l.clone();
|
||||
view! {
|
||||
<span class="flex items-center gap-1 text-xs text-secondary/50 bg-surface/50 px-1.5 py-0.5 rounded-full">
|
||||
"#" " " {label_str}
|
||||
</span>
|
||||
}
|
||||
})}
|
||||
<span class="text-xs text-secondary/60">
|
||||
{time_ago(first.created_at)}
|
||||
|
||||
@@ -43,6 +43,7 @@ pub fn MessageFeed(
|
||||
create_effect(move |_| {
|
||||
let _ = intersecting.get(); // track signal
|
||||
if let Some(node) = sentinel_ref.get() {
|
||||
let on_load_more = on_load_more.clone();
|
||||
let cb = Closure::<dyn Fn(Vec<JsValue>)>::new(move |entries: Vec<JsValue>| {
|
||||
for entry in entries {
|
||||
if let Some(entry) = entry.dyn_ref::<web_sys::IntersectionObserverEntry>() {
|
||||
|
||||
@@ -53,10 +53,10 @@ pub fn MessagesPanel() -> impl IntoView {
|
||||
}).collect()
|
||||
});
|
||||
|
||||
// Search handler
|
||||
let handle_search = {
|
||||
// Search handler - takes any event type and triggers the search
|
||||
let do_search = {
|
||||
let q = search_query;
|
||||
move |_| {
|
||||
move || {
|
||||
let query = q.get();
|
||||
if query.trim().is_empty() {
|
||||
set_show_search.set(false);
|
||||
@@ -79,6 +79,9 @@ pub fn MessagesPanel() -> impl IntoView {
|
||||
});
|
||||
}
|
||||
};
|
||||
// Separate closures for different event types so on:click/on:keydown type-check
|
||||
let handle_search_click = move |_: web_sys::MouseEvent| do_search();
|
||||
let handle_search_keydown = move |_: web_sys::KeyboardEvent| do_search();
|
||||
|
||||
// Clear search
|
||||
let clear_search = move |_| {
|
||||
@@ -141,13 +144,22 @@ pub fn MessagesPanel() -> impl IntoView {
|
||||
create_effect(move |_| {
|
||||
if let Some(config) = use_context::<crate::app::AppConfig>() {
|
||||
if let Some(ref guild_id) = config.monitor_guild_id {
|
||||
state.fetch_messages(guild_id.clone());
|
||||
(state.fetch_messages)(guild_id.clone());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ─── View ────────────────────────────────────────────────
|
||||
let (total, clean, flagged, error, pending, deleted, edited) = move || stats.get();
|
||||
let get_stats = move || stats.get();
|
||||
let (total, clean, flagged, error, pending, deleted, edited) = (
|
||||
move || get_stats().0,
|
||||
move || get_stats().1,
|
||||
move || get_stats().2,
|
||||
move || get_stats().3,
|
||||
move || get_stats().4,
|
||||
move || get_stats().5,
|
||||
move || get_stats().6,
|
||||
);
|
||||
|
||||
view! {
|
||||
<div class="messages-panel">
|
||||
@@ -190,15 +202,15 @@ pub fn MessagesPanel() -> impl IntoView {
|
||||
prop:value=search_query
|
||||
on:input=move |ev| set_search_query.set(event_target_value(&ev))
|
||||
on:keydown=move |ev| {
|
||||
if ev.key() == "Enter" { handle_search(()); }
|
||||
if ev.key() == "Enter" { handle_search_keydown(ev); }
|
||||
}
|
||||
disabled=is_searching.get()
|
||||
disabled=move || is_searching.get()
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-primary btn-sm"
|
||||
on:click=handle_search
|
||||
disabled=is_searching.get() || search_query.get().trim().is_empty()
|
||||
on:click=handle_search_click
|
||||
disabled=move || is_searching.get() || search_query.get().trim().is_empty()
|
||||
>
|
||||
{move || if is_searching.get() { "Searching..." } else { "Search" }}
|
||||
</button>
|
||||
@@ -211,7 +223,7 @@ pub fn MessagesPanel() -> impl IntoView {
|
||||
<button
|
||||
class="btn btn-destructive btn-sm"
|
||||
on:click=handle_retry_all
|
||||
disabled=retrying_all.get()
|
||||
disabled=move || retrying_all.get()
|
||||
>
|
||||
{/* Rotate CCW icon as SVN */}
|
||||
<svg class=format!("mr-1.5 h-3.5 w-3.5{}", if retrying_all.get() { " animate-spin" } else { "" }) xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7v6h6"></path><path d="M21 17a9 9 0 00-9-9 9 9 0 00-6 2.3L3 13"></path></svg>
|
||||
@@ -271,18 +283,22 @@ pub fn MessagesPanel() -> impl IntoView {
|
||||
</div>
|
||||
|
||||
<div class="tab-content" style:display=move || if view_tab.get() == ViewTab::All { "block" } else { "none" }>
|
||||
<MessageFeed
|
||||
messages=filtered_messages.get()
|
||||
empty_text=if show_search.get() { "No messages found matching your search." } else { "No captures yet." }
|
||||
loading=state.loading.get()
|
||||
has_more=if show_search.get() { false } else { state.has_more.get() }
|
||||
loading_more=state.loading_more.get()
|
||||
on_load_more={
|
||||
let cb = state.load_more.clone();
|
||||
Some(Arc::new(move || cb()) as Arc<dyn Fn() + Send + Sync + 'static>)
|
||||
{
|
||||
let load_more_cb = state.load_more.clone();
|
||||
let empty_text: &'static str = if show_search.get() { "No messages found matching your search." } else { "No captures yet." };
|
||||
let has_more = if show_search.get() { false } else { state.has_more.get() };
|
||||
view! {
|
||||
<MessageFeed
|
||||
messages=filtered_messages.get()
|
||||
empty_text=empty_text
|
||||
loading=state.loading.get()
|
||||
has_more=has_more
|
||||
loading_more=state.loading_more.get()
|
||||
on_load_more=Arc::new(move || load_more_cb()) as Arc<dyn Fn() + Send + Sync + 'static>
|
||||
on_reanalyze=state.reanalyze.clone()
|
||||
/>
|
||||
}
|
||||
on_reanalyze=state.reanalyze.clone()
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
<div class="tab-content" style:display=move || if view_tab.get() == ViewTab::Images { "block" } else { "none" }>
|
||||
<ImageGrid messages=filtered_messages.get() />
|
||||
|
||||
@@ -34,7 +34,7 @@ pub enum AiRecommendedAction {
|
||||
}
|
||||
|
||||
// ── Message Metadata ──────────────────────────────────────
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct MessageMetadata {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub stickers: Option<Vec<StickerInfo>>,
|
||||
@@ -46,7 +46,7 @@ pub struct MessageMetadata {
|
||||
pub channel: Option<ChannelRef>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct StickerInfo {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
@@ -54,7 +54,7 @@ pub struct StickerInfo {
|
||||
pub url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct AttachmentRef {
|
||||
pub name: String,
|
||||
pub url: String,
|
||||
@@ -63,7 +63,7 @@ pub struct AttachmentRef {
|
||||
pub content_type: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct EmbedInfo {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub title: Option<String>,
|
||||
@@ -73,7 +73,7 @@ pub struct EmbedInfo {
|
||||
pub thumbnail: Option<EmbedMedia>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct EmbedMedia {
|
||||
pub url: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -82,7 +82,7 @@ pub struct EmbedMedia {
|
||||
pub height: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ChannelRef {
|
||||
pub channel_id: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -94,7 +94,7 @@ pub struct ChannelRef {
|
||||
}
|
||||
|
||||
// ── Message Record ────────────────────────────────────────
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct MessageRecord {
|
||||
pub id: String,
|
||||
pub guild_id: String,
|
||||
@@ -140,7 +140,7 @@ pub struct MessageRecord {
|
||||
}
|
||||
|
||||
// ── Pagination ────────────────────────────────────────────
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct PageResult<T> {
|
||||
pub data: Vec<T>,
|
||||
#[serde(rename = "nextCursor")]
|
||||
@@ -148,7 +148,7 @@ pub struct PageResult<T> {
|
||||
}
|
||||
|
||||
// ── Attachment ────────────────────────────────────────────
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct AttachmentRecord {
|
||||
pub id: String,
|
||||
pub message_id: String,
|
||||
|
||||
Reference in New Issue
Block a user