add :emote and :list to chat

This commit is contained in:
Evan Carroll 2026-01-12 17:23:41 -06:00
parent 1ca300098f
commit bd28e201a2
7 changed files with 741 additions and 22 deletions

View file

@ -1695,3 +1695,30 @@ pub struct JoinChannelResponse {
pub member: ChannelMemberInfo,
pub members: Vec<ChannelMemberWithAvatar>,
}
// =============================================================================
// Emotion Availability
// =============================================================================
/// Emotion availability data for the emote command UI.
///
/// Indicates which of the 12 emotions have assets configured for the user's avatar,
/// and provides preview paths (position 4/center) for rendering in the emotion picker.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmotionAvailability {
/// Which emotions have at least one non-null asset slot (positions 0-8).
/// Index corresponds to emotion: 0=neutral, 1=happy, 2=sad, etc.
pub available: [bool; 12],
/// Center position (4) asset path for each emotion, used for preview rendering.
/// None if that emotion has no center asset.
pub preview_paths: [Option<String>; 12],
}
impl Default for EmotionAvailability {
fn default() -> Self {
Self {
available: [false; 12],
preview_paths: Default::default(),
}
}
}

View file

@ -3,7 +3,7 @@
use sqlx::PgExecutor;
use uuid::Uuid;
use crate::models::{ActiveAvatar, AvatarRenderData};
use crate::models::{ActiveAvatar, AvatarRenderData, EmotionAvailability};
use chattyness_error::AppError;
/// Get the active avatar for a user in a realm.
@ -35,8 +35,8 @@ pub async fn set_emotion<'e>(
realm_id: Uuid,
emotion: i16,
) -> Result<[Option<String>; 9], AppError> {
if emotion < 0 || emotion > 9 {
return Err(AppError::Validation("Emotion must be 0-9".to_string()));
if emotion < 0 || emotion > 11 {
return Err(AppError::Validation("Emotion must be 0-11".to_string()));
}
// Map emotion index to column prefix
@ -51,7 +51,9 @@ pub async fn set_emotion<'e>(
7 => "e_crying",
8 => "e_love",
9 => "e_confused",
_ => return Err(AppError::Validation("Emotion must be 0-9".to_string())),
10 => "e_sleeping",
11 => "e_wink",
_ => return Err(AppError::Validation("Emotion must be 0-11".to_string())),
};
// Build dynamic query for the specific emotion's 9 positions
@ -199,3 +201,167 @@ impl From<SimplifiedAvatarRow> for AvatarRenderData {
}
}
}
/// Get emotion availability for a user's avatar in a realm.
///
/// Returns which emotions have assets configured (any of positions 0-8 non-null)
/// and the center position (4) preview path for each emotion.
pub async fn get_emotion_availability<'e>(
executor: impl PgExecutor<'e>,
user_id: Uuid,
realm_id: Uuid,
) -> Result<EmotionAvailability, AppError> {
let row = sqlx::query_as::<_, EmotionAvailabilityRow>(
r#"
SELECT
-- Neutral (0): check if any position has asset
(a.e_neutral_0 IS NOT NULL OR a.e_neutral_1 IS NOT NULL OR a.e_neutral_2 IS NOT NULL OR
a.e_neutral_3 IS NOT NULL OR a.e_neutral_4 IS NOT NULL OR a.e_neutral_5 IS NOT NULL OR
a.e_neutral_6 IS NOT NULL OR a.e_neutral_7 IS NOT NULL OR a.e_neutral_8 IS NOT NULL) as avail_0,
(SELECT prop_asset_path FROM props.inventory WHERE id = a.e_neutral_4) as preview_0,
-- Happy (1)
(a.e_happy_0 IS NOT NULL OR a.e_happy_1 IS NOT NULL OR a.e_happy_2 IS NOT NULL OR
a.e_happy_3 IS NOT NULL OR a.e_happy_4 IS NOT NULL OR a.e_happy_5 IS NOT NULL OR
a.e_happy_6 IS NOT NULL OR a.e_happy_7 IS NOT NULL OR a.e_happy_8 IS NOT NULL) as avail_1,
(SELECT prop_asset_path FROM props.inventory WHERE id = a.e_happy_4) as preview_1,
-- Sad (2)
(a.e_sad_0 IS NOT NULL OR a.e_sad_1 IS NOT NULL OR a.e_sad_2 IS NOT NULL OR
a.e_sad_3 IS NOT NULL OR a.e_sad_4 IS NOT NULL OR a.e_sad_5 IS NOT NULL OR
a.e_sad_6 IS NOT NULL OR a.e_sad_7 IS NOT NULL OR a.e_sad_8 IS NOT NULL) as avail_2,
(SELECT prop_asset_path FROM props.inventory WHERE id = a.e_sad_4) as preview_2,
-- Angry (3)
(a.e_angry_0 IS NOT NULL OR a.e_angry_1 IS NOT NULL OR a.e_angry_2 IS NOT NULL OR
a.e_angry_3 IS NOT NULL OR a.e_angry_4 IS NOT NULL OR a.e_angry_5 IS NOT NULL OR
a.e_angry_6 IS NOT NULL OR a.e_angry_7 IS NOT NULL OR a.e_angry_8 IS NOT NULL) as avail_3,
(SELECT prop_asset_path FROM props.inventory WHERE id = a.e_angry_4) as preview_3,
-- Surprised (4)
(a.e_surprised_0 IS NOT NULL OR a.e_surprised_1 IS NOT NULL OR a.e_surprised_2 IS NOT NULL OR
a.e_surprised_3 IS NOT NULL OR a.e_surprised_4 IS NOT NULL OR a.e_surprised_5 IS NOT NULL OR
a.e_surprised_6 IS NOT NULL OR a.e_surprised_7 IS NOT NULL OR a.e_surprised_8 IS NOT NULL) as avail_4,
(SELECT prop_asset_path FROM props.inventory WHERE id = a.e_surprised_4) as preview_4,
-- Thinking (5)
(a.e_thinking_0 IS NOT NULL OR a.e_thinking_1 IS NOT NULL OR a.e_thinking_2 IS NOT NULL OR
a.e_thinking_3 IS NOT NULL OR a.e_thinking_4 IS NOT NULL OR a.e_thinking_5 IS NOT NULL OR
a.e_thinking_6 IS NOT NULL OR a.e_thinking_7 IS NOT NULL OR a.e_thinking_8 IS NOT NULL) as avail_5,
(SELECT prop_asset_path FROM props.inventory WHERE id = a.e_thinking_4) as preview_5,
-- Laughing (6)
(a.e_laughing_0 IS NOT NULL OR a.e_laughing_1 IS NOT NULL OR a.e_laughing_2 IS NOT NULL OR
a.e_laughing_3 IS NOT NULL OR a.e_laughing_4 IS NOT NULL OR a.e_laughing_5 IS NOT NULL OR
a.e_laughing_6 IS NOT NULL OR a.e_laughing_7 IS NOT NULL OR a.e_laughing_8 IS NOT NULL) as avail_6,
(SELECT prop_asset_path FROM props.inventory WHERE id = a.e_laughing_4) as preview_6,
-- Crying (7)
(a.e_crying_0 IS NOT NULL OR a.e_crying_1 IS NOT NULL OR a.e_crying_2 IS NOT NULL OR
a.e_crying_3 IS NOT NULL OR a.e_crying_4 IS NOT NULL OR a.e_crying_5 IS NOT NULL OR
a.e_crying_6 IS NOT NULL OR a.e_crying_7 IS NOT NULL OR a.e_crying_8 IS NOT NULL) as avail_7,
(SELECT prop_asset_path FROM props.inventory WHERE id = a.e_crying_4) as preview_7,
-- Love (8)
(a.e_love_0 IS NOT NULL OR a.e_love_1 IS NOT NULL OR a.e_love_2 IS NOT NULL OR
a.e_love_3 IS NOT NULL OR a.e_love_4 IS NOT NULL OR a.e_love_5 IS NOT NULL OR
a.e_love_6 IS NOT NULL OR a.e_love_7 IS NOT NULL OR a.e_love_8 IS NOT NULL) as avail_8,
(SELECT prop_asset_path FROM props.inventory WHERE id = a.e_love_4) as preview_8,
-- Confused (9)
(a.e_confused_0 IS NOT NULL OR a.e_confused_1 IS NOT NULL OR a.e_confused_2 IS NOT NULL OR
a.e_confused_3 IS NOT NULL OR a.e_confused_4 IS NOT NULL OR a.e_confused_5 IS NOT NULL OR
a.e_confused_6 IS NOT NULL OR a.e_confused_7 IS NOT NULL OR a.e_confused_8 IS NOT NULL) as avail_9,
(SELECT prop_asset_path FROM props.inventory WHERE id = a.e_confused_4) as preview_9,
-- Sleeping (10)
(a.e_sleeping_0 IS NOT NULL OR a.e_sleeping_1 IS NOT NULL OR a.e_sleeping_2 IS NOT NULL OR
a.e_sleeping_3 IS NOT NULL OR a.e_sleeping_4 IS NOT NULL OR a.e_sleeping_5 IS NOT NULL OR
a.e_sleeping_6 IS NOT NULL OR a.e_sleeping_7 IS NOT NULL OR a.e_sleeping_8 IS NOT NULL) as avail_10,
(SELECT prop_asset_path FROM props.inventory WHERE id = a.e_sleeping_4) as preview_10,
-- Wink (11)
(a.e_wink_0 IS NOT NULL OR a.e_wink_1 IS NOT NULL OR a.e_wink_2 IS NOT NULL OR
a.e_wink_3 IS NOT NULL OR a.e_wink_4 IS NOT NULL OR a.e_wink_5 IS NOT NULL OR
a.e_wink_6 IS NOT NULL OR a.e_wink_7 IS NOT NULL OR a.e_wink_8 IS NOT NULL) as avail_11,
(SELECT prop_asset_path FROM props.inventory WHERE id = a.e_wink_4) as preview_11
FROM props.active_avatars aa
JOIN props.avatars a ON aa.avatar_id = a.id
WHERE aa.user_id = $1 AND aa.realm_id = $2
"#,
)
.bind(user_id)
.bind(realm_id)
.fetch_optional(executor)
.await?;
match row {
Some(r) => Ok(r.into()),
None => Ok(EmotionAvailability::default()),
}
}
/// Row type for emotion availability query.
#[derive(Debug, sqlx::FromRow)]
struct EmotionAvailabilityRow {
avail_0: Option<bool>,
preview_0: Option<String>,
avail_1: Option<bool>,
preview_1: Option<String>,
avail_2: Option<bool>,
preview_2: Option<String>,
avail_3: Option<bool>,
preview_3: Option<String>,
avail_4: Option<bool>,
preview_4: Option<String>,
avail_5: Option<bool>,
preview_5: Option<String>,
avail_6: Option<bool>,
preview_6: Option<String>,
avail_7: Option<bool>,
preview_7: Option<String>,
avail_8: Option<bool>,
preview_8: Option<String>,
avail_9: Option<bool>,
preview_9: Option<String>,
avail_10: Option<bool>,
preview_10: Option<String>,
avail_11: Option<bool>,
preview_11: Option<String>,
}
impl From<EmotionAvailabilityRow> for EmotionAvailability {
fn from(row: EmotionAvailabilityRow) -> Self {
Self {
available: [
row.avail_0.unwrap_or(false),
row.avail_1.unwrap_or(false),
row.avail_2.unwrap_or(false),
row.avail_3.unwrap_or(false),
row.avail_4.unwrap_or(false),
row.avail_5.unwrap_or(false),
row.avail_6.unwrap_or(false),
row.avail_7.unwrap_or(false),
row.avail_8.unwrap_or(false),
row.avail_9.unwrap_or(false),
row.avail_10.unwrap_or(false),
row.avail_11.unwrap_or(false),
],
preview_paths: [
row.preview_0,
row.preview_1,
row.preview_2,
row.preview_3,
row.preview_4,
row.preview_5,
row.preview_6,
row.preview_7,
row.preview_8,
row.preview_9,
row.preview_10,
row.preview_11,
],
}
}
}