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:
asepharyana
2026-07-03 20:45:35 +07:00
parent 63ddc1e112
commit 3de7f8d823
5 changed files with 165 additions and 97 deletions
+3 -3
View File
@@ -92,9 +92,9 @@ pub fn App() -> impl IntoView {
// Content area // Content area
<div style="flex: 1; overflow: auto; padding: 1.5rem;"> <div style="flex: 1; overflow: auto; padding: 1.5rem;">
{move || match ui.active_tab.get() { {move || match ui.active_tab.get() {
Tab::Messages => view! { <MessagesPanel /> }.into_view(), Tab::Messages => view! { <MessagesPanel /> }.into_any(),
Tab::Live => view! { <div>"Live Panel"</div> }.into_view(), Tab::Live => view! { <div>"Live Panel"</div> }.into_any(),
Tab::Dashboard => view! { <div>"Dashboard Panel"</div> }.into_view(), Tab::Dashboard => view! { <div>"Dashboard Panel"</div> }.into_any(),
}} }}
</div> </div>
</main> </main>
@@ -1,6 +1,6 @@
use leptos::prelude::*; use leptos::prelude::*;
use regex::Regex; use regex::Regex;
use shared_types::message::{AiSeverity, AiStatus, MessageRecord}; use shared_types::message::{AiSeverity, AiStatus, AttachmentRef, MessageRecord};
use std::sync::{Arc, OnceLock}; use std::sync::{Arc, OnceLock};
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
@@ -84,13 +84,13 @@ fn get_cats(raw: &Option<Vec<String>>) -> Vec<String> {
// ─── StatusBadgeInline ──────────────────────────────────── // ─── StatusBadgeInline ────────────────────────────────────
#[component] #[component]
fn StatusBadgeInline(status: AiStatus) -> impl IntoView { fn StatusBadgeInline(status: AiStatus) -> impl IntoView {
let (cl, icon_svg) = match &status { let (cl, icon_svg): (&'static str, AnyView) = 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::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", 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::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", 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", 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::Pending => ("status-badge-pending", view! { }.into_any()),
AiStatus::Processing => ("status-badge-processing", None.into_any()), AiStatus::Processing => ("status-badge-processing", view! { }.into_any()),
AiStatus::Warn => ("status-badge-warn", None.into_any()), AiStatus::Warn => ("status-badge-warn", view! { }.into_any()),
}; };
view! { view! {
<span class=format!("status-badge {}", cl)> <span class=format!("status-badge {}", cl)>
@@ -125,20 +125,20 @@ pub fn MessageRow(
// Attachments // Attachments
let all_atts = message.metadata.as_ref() let all_atts = message.metadata.as_ref()
.and_then(|m| m.attachments.as_ref()).cloned().unwrap_or_default(); .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.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(".png")
|| a.name.to_lowercase().ends_with(".jpg") || a.name.to_lowercase().ends_with(".jpg")
|| a.name.to_lowercase().ends_with(".jpeg") || a.name.to_lowercase().ends_with(".jpeg")
|| a.name.to_lowercase().ends_with(".gif") || a.name.to_lowercase().ends_with(".gif")
|| a.name.to_lowercase().ends_with(".webp") || a.name.to_lowercase().ends_with(".webp")
}).collect(); }).cloned().collect();
let vids: Vec<_> = all_atts.iter().filter(|a| { let vids: Vec<AttachmentRef> = all_atts.iter().filter(|a| {
a.content_type.as_deref().map(|ct| ct.starts_with("video/")).unwrap_or(false) 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(".mp4")
|| a.name.to_lowercase().ends_with(".webm") || a.name.to_lowercase().ends_with(".webm")
|| a.name.to_lowercase().ends_with(".mov") || a.name.to_lowercase().ends_with(".mov")
}).collect(); }).cloned().collect();
let stickers = message.metadata.as_ref() let stickers = message.metadata.as_ref()
.and_then(|m| m.stickers.as_ref()).cloned().unwrap_or_default(); .and_then(|m| m.stickers.as_ref()).cloned().unwrap_or_default();
@@ -180,9 +180,10 @@ pub fn MessageRow(
{/* Content */} {/* Content */}
{show.then(|| { {show.then(|| {
let rendered = render_emojis(display); 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! { view! {
<p class=format!("whitespace-pre-wrap break-words text-sm leading-6 {}", cls)> <p class=class_str>
{rendered.into_iter().collect::<Vec<_>>()} {rendered.into_iter().collect::<Vec<_>>()}
</p> </p>
} }
@@ -192,17 +193,22 @@ pub fn MessageRow(
{(!stickers.is_empty()).then(|| view! { {(!stickers.is_empty()).then(|| view! {
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
{stickers.iter().map(|s| { {stickers.iter().map(|s| {
let url = s.url.clone(); let url_owned = s.url.clone().unwrap_or_default();
let name = s.name.clone().unwrap_or_default(); let name_owned = s.name.clone().unwrap_or_default();
let has_url = !url_owned.is_empty();
view! { view! {
<div> <div>
{url.as_ref().map(|u| view! { {if has_url {
<img src=u alt=&name class="h-12 w-12 rounded-lg border border-border object-contain bg-surface/50" loading="lazy" /> view! {
}).unwrap_or_else(|| 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 class="flex h-12 w-12 items-center justify-center rounded-lg border border-border bg-surface/50">
"😊" "😊"
</div> </div>
})} }.into_any()
}}
</div> </div>
} }
}).collect::<Vec<_>>()} }).collect::<Vec<_>>()}
@@ -210,55 +216,94 @@ pub fn MessageRow(
})} })}
{/* Images */} {/* Images */}
{(!imgs.is_empty()).then(|| view! { {if !imgs.is_empty() {
<div class="flex gap-2 overflow-x-auto"> let imgs_local = imgs.clone();
{imgs.iter().take(4).map(|a| view! { let images_view = imgs_local.iter().take(4).map(|a| {
<a href=&a.url target="_blank" class="shrink-0 overflow-hidden rounded-lg border border-border"> let url1 = a.url.clone();
<img src=&a.url alt=&a.name class="h-16 w-16 object-cover hover:scale-105 transition-transform" loading="lazy" /> 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> </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"> <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> </div>
})} }.into_any()
} else {
view! {}.into_any()
}}
{/* Videos */} {/* Videos */}
{(!vids.is_empty()).then(|| view! { {if !vids.is_empty() {
<div class="flex gap-2 overflow-x-auto"> let vids_local = vids.clone();
{vids.iter().take(4).map(|a| view! { let videos_view = vids_local.iter().take(4).map(|a| {
<video src=&a.url controls class="h-28 w-48 shrink-0 rounded-lg border border-border object-cover bg-black" preload="metadata"></video> let url = a.url.clone();
}).collect::<Vec<_>>()} view! {
{(vids.len() > 4).then(|| 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"> <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> </div>
})} }.into_any()
} else {
view! {}.into_any()
}}
{/* Categories */} {/* Categories */}
{(!cats.is_empty()).then(|| view! { {if !cats.is_empty() {
let cats_local = cats.clone();
view! {
<div class="flex flex-wrap gap-1"> <div class="flex flex-wrap gap-1">
{cats.iter().map(|c| view! { {cats_local.iter().map(|c| view! {
<span class="badge badge-secondary text-xs">{c.clone()}</span> <span class="badge badge-secondary text-xs">{c.clone()}</span>
}).collect::<Vec<_>>()} }).collect::<Vec<_>>()}
</div> </div>
})} }.into_any()
} else {
view! {}.into_any()
}}
{/* AI Analysis */} {/* AI Analysis */}
{message.ai_analysis.as_ref().map(|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 icon = if ai_st == AiStatus::Flagged { "🚨" } else { "" };
let analysis_summary_str = analysis_summary.clone();
let analysis_str = analysis.clone();
view! { 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"> <div class="flex items-start gap-2 text-xs">
<span class="mt-0.5 shrink-0">{icon}</span> <span class="mt-0.5 shrink-0">{icon}</span>
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<span class="block font-medium mb-1">{&analysis_summary}</span> <span class="block font-medium mb-1">{analysis_summary_str}</span>
<div class="text-xs leading-relaxed whitespace-pre-wrap">{analysis}</div> <div class="text-xs leading-relaxed whitespace-pre-wrap">{analysis_str}</div>
</div> </div>
</div> </div>
</div> </div>
@@ -266,10 +311,13 @@ pub fn MessageRow(
})} })}
{/* Error */} {/* Error */}
{message.ai_error.as_ref().map(|e| view! { {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"> <div class="rounded-lg bg-warning/5 px-3 py-2 text-xs text-warning">
<span>"AI error: "{e}</span> <span>"AI error: "{error_str}</span>
</div> </div>
}
})} })}
{/* Re-analyze */} {/* 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" /> <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="min-w-0 flex-1">
<div class="flex items-baseline gap-2 mb-2"> <div class="flex items-baseline gap-2 mb-2">
<span class="font-semibold text-sm">{&first.username}</span> <span class="font-semibold text-sm">{first.username.clone()}</span>
{loc_label.as_ref().map(|l| view! { {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"> <span class="flex items-center gap-1 text-xs text-secondary/50 bg-surface/50 px-1.5 py-0.5 rounded-full">
"#" " " {l} "#" " " {label_str}
</span> </span>
}
})} })}
<span class="text-xs text-secondary/60"> <span class="text-xs text-secondary/60">
{time_ago(first.created_at)} {time_ago(first.created_at)}
@@ -43,6 +43,7 @@ pub fn MessageFeed(
create_effect(move |_| { create_effect(move |_| {
let _ = intersecting.get(); // track signal let _ = intersecting.get(); // track signal
if let Some(node) = sentinel_ref.get() { 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>| { let cb = Closure::<dyn Fn(Vec<JsValue>)>::new(move |entries: Vec<JsValue>| {
for entry in entries { for entry in entries {
if let Some(entry) = entry.dyn_ref::<web_sys::IntersectionObserverEntry>() { if let Some(entry) = entry.dyn_ref::<web_sys::IntersectionObserverEntry>() {
@@ -53,10 +53,10 @@ pub fn MessagesPanel() -> impl IntoView {
}).collect() }).collect()
}); });
// Search handler // Search handler - takes any event type and triggers the search
let handle_search = { let do_search = {
let q = search_query; let q = search_query;
move |_| { move || {
let query = q.get(); let query = q.get();
if query.trim().is_empty() { if query.trim().is_empty() {
set_show_search.set(false); 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 // Clear search
let clear_search = move |_| { let clear_search = move |_| {
@@ -141,13 +144,22 @@ pub fn MessagesPanel() -> impl IntoView {
create_effect(move |_| { create_effect(move |_| {
if let Some(config) = use_context::<crate::app::AppConfig>() { if let Some(config) = use_context::<crate::app::AppConfig>() {
if let Some(ref guild_id) = config.monitor_guild_id { if let Some(ref guild_id) = config.monitor_guild_id {
state.fetch_messages(guild_id.clone()); (state.fetch_messages)(guild_id.clone());
} }
} }
}); });
// ─── View ──────────────────────────────────────────────── // ─── 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! { view! {
<div class="messages-panel"> <div class="messages-panel">
@@ -190,15 +202,15 @@ pub fn MessagesPanel() -> impl IntoView {
prop:value=search_query prop:value=search_query
on:input=move |ev| set_search_query.set(event_target_value(&ev)) on:input=move |ev| set_search_query.set(event_target_value(&ev))
on:keydown=move |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> </div>
<button <button
class="btn btn-primary btn-sm" class="btn btn-primary btn-sm"
on:click=handle_search on:click=handle_search_click
disabled=is_searching.get() || search_query.get().trim().is_empty() disabled=move || is_searching.get() || search_query.get().trim().is_empty()
> >
{move || if is_searching.get() { "Searching..." } else { "Search" }} {move || if is_searching.get() { "Searching..." } else { "Search" }}
</button> </button>
@@ -211,7 +223,7 @@ pub fn MessagesPanel() -> impl IntoView {
<button <button
class="btn btn-destructive btn-sm" class="btn btn-destructive btn-sm"
on:click=handle_retry_all on:click=handle_retry_all
disabled=retrying_all.get() disabled=move || retrying_all.get()
> >
{/* Rotate CCW icon as SVN */} {/* 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> <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>
<div class="tab-content" style:display=move || if view_tab.get() == ViewTab::All { "block" } else { "none" }> <div class="tab-content" style:display=move || if view_tab.get() == ViewTab::All { "block" } else { "none" }>
{
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 <MessageFeed
messages=filtered_messages.get() messages=filtered_messages.get()
empty_text=if show_search.get() { "No messages found matching your search." } else { "No captures yet." } empty_text=empty_text
loading=state.loading.get() loading=state.loading.get()
has_more=if show_search.get() { false } else { state.has_more.get() } has_more=has_more
loading_more=state.loading_more.get() loading_more=state.loading_more.get()
on_load_more={ on_load_more=Arc::new(move || load_more_cb()) as Arc<dyn Fn() + Send + Sync + 'static>
let cb = state.load_more.clone();
Some(Arc::new(move || cb()) as Arc<dyn Fn() + Send + Sync + 'static>)
}
on_reanalyze=state.reanalyze.clone() on_reanalyze=state.reanalyze.clone()
/> />
}
}
</div> </div>
<div class="tab-content" style:display=move || if view_tab.get() == ViewTab::Images { "block" } else { "none" }> <div class="tab-content" style:display=move || if view_tab.get() == ViewTab::Images { "block" } else { "none" }>
<ImageGrid messages=filtered_messages.get() /> <ImageGrid messages=filtered_messages.get() />
@@ -34,7 +34,7 @@ pub enum AiRecommendedAction {
} }
// ── Message Metadata ────────────────────────────────────── // ── Message Metadata ──────────────────────────────────────
#[derive(Debug, Clone, Serialize, Deserialize, Default)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct MessageMetadata { pub struct MessageMetadata {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub stickers: Option<Vec<StickerInfo>>, pub stickers: Option<Vec<StickerInfo>>,
@@ -46,7 +46,7 @@ pub struct MessageMetadata {
pub channel: Option<ChannelRef>, pub channel: Option<ChannelRef>,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct StickerInfo { pub struct StickerInfo {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>, pub name: Option<String>,
@@ -54,7 +54,7 @@ pub struct StickerInfo {
pub url: Option<String>, pub url: Option<String>,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AttachmentRef { pub struct AttachmentRef {
pub name: String, pub name: String,
pub url: String, pub url: String,
@@ -63,7 +63,7 @@ pub struct AttachmentRef {
pub content_type: Option<String>, pub content_type: Option<String>,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct EmbedInfo { pub struct EmbedInfo {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>, pub title: Option<String>,
@@ -73,7 +73,7 @@ pub struct EmbedInfo {
pub thumbnail: Option<EmbedMedia>, pub thumbnail: Option<EmbedMedia>,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct EmbedMedia { pub struct EmbedMedia {
pub url: String, pub url: String,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
@@ -82,7 +82,7 @@ pub struct EmbedMedia {
pub height: Option<u32>, pub height: Option<u32>,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ChannelRef { pub struct ChannelRef {
pub channel_id: String, pub channel_id: String,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
@@ -94,7 +94,7 @@ pub struct ChannelRef {
} }
// ── Message Record ──────────────────────────────────────── // ── Message Record ────────────────────────────────────────
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct MessageRecord { pub struct MessageRecord {
pub id: String, pub id: String,
pub guild_id: String, pub guild_id: String,
@@ -140,7 +140,7 @@ pub struct MessageRecord {
} }
// ── Pagination ──────────────────────────────────────────── // ── Pagination ────────────────────────────────────────────
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct PageResult<T> { pub struct PageResult<T> {
pub data: Vec<T>, pub data: Vec<T>,
#[serde(rename = "nextCursor")] #[serde(rename = "nextCursor")]
@@ -148,7 +148,7 @@ pub struct PageResult<T> {
} }
// ── Attachment ──────────────────────────────────────────── // ── Attachment ────────────────────────────────────────────
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AttachmentRecord { pub struct AttachmentRecord {
pub id: String, pub id: String,
pub message_id: String, pub message_id: String,