258 lines
8.3 KiB
Rust
258 lines
8.3 KiB
Rust
use chrono::Utc;
|
|
use uuid::Uuid;
|
|
|
|
use crate::event::{RoomInfo, UserInfo, conversation};
|
|
use crate::{ChannelBus, ChannelResult};
|
|
|
|
use super::WsHandler;
|
|
use super::WsOutEvent;
|
|
|
|
impl WsHandler {
|
|
pub(super) async fn conversation_pin(
|
|
bus: &ChannelBus,
|
|
user_id: Uuid,
|
|
room: Uuid,
|
|
pin: bool,
|
|
) -> ChannelResult<Option<WsOutEvent>> {
|
|
Self::ensure_room_access(bus, user_id, room).await?;
|
|
let now = Utc::now();
|
|
db::sqlx::query(
|
|
"INSERT INTO user_room_state (\"user\", room, is_pinned, updated_at) \
|
|
VALUES ($1, $2, $3, $4) \
|
|
ON CONFLICT (\"user\", room) DO UPDATE \
|
|
SET is_pinned = $3, updated_at = $4",
|
|
)
|
|
.bind(user_id)
|
|
.bind(room)
|
|
.bind(pin)
|
|
.bind(now)
|
|
.execute(bus.inner.db.writer())
|
|
.await?;
|
|
|
|
let room_info = bus
|
|
.lookup_room(room)
|
|
.await
|
|
.unwrap_or_else(|_| RoomInfo::unknown(room));
|
|
let user_info = bus
|
|
.lookup_user(user_id)
|
|
.await
|
|
.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
|
|
|
if pin {
|
|
let data = conversation::ConversationPinnedService {
|
|
user: user_info,
|
|
room: room_info.clone(),
|
|
pinned_at: now,
|
|
};
|
|
bus.emit_to_user(user_id, "conversation.pinned", &data)
|
|
.await?;
|
|
Ok(Some(WsOutEvent::ConversationPinned {
|
|
room: room_info,
|
|
data,
|
|
}))
|
|
} else {
|
|
let data = conversation::ConversationUnpinnedService {
|
|
user: user_info,
|
|
room: room_info.clone(),
|
|
unpinned_at: now,
|
|
};
|
|
bus.emit_to_user(user_id, "conversation.unpinned", &data)
|
|
.await?;
|
|
Ok(Some(WsOutEvent::ConversationUnpinned {
|
|
room: room_info,
|
|
data,
|
|
}))
|
|
}
|
|
}
|
|
pub(super) async fn conversation_mute(
|
|
bus: &ChannelBus,
|
|
user_id: Uuid,
|
|
room: Uuid,
|
|
mute: bool,
|
|
) -> ChannelResult<Option<WsOutEvent>> {
|
|
Self::ensure_room_access(bus, user_id, room).await?;
|
|
let now = Utc::now();
|
|
|
|
db::sqlx::query(
|
|
"INSERT INTO user_room_state (\"user\", room, is_muted, updated_at) \
|
|
VALUES ($1, $2, $3, $4) \
|
|
ON CONFLICT (\"user\", room) DO UPDATE \
|
|
SET is_muted = $3, updated_at = $4",
|
|
)
|
|
.bind(user_id)
|
|
.bind(room)
|
|
.bind(mute)
|
|
.bind(now)
|
|
.execute(bus.inner.db.writer())
|
|
.await?;
|
|
|
|
let room_info = bus
|
|
.lookup_room(room)
|
|
.await
|
|
.unwrap_or_else(|_| RoomInfo::unknown(room));
|
|
let user_info = bus
|
|
.lookup_user(user_id)
|
|
.await
|
|
.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
|
|
|
if mute {
|
|
let data = conversation::ConversationMutedService {
|
|
user: user_info,
|
|
room: room_info.clone(),
|
|
muted_at: now,
|
|
};
|
|
bus.emit_to_user(user_id, "conversation.muted", &data)
|
|
.await?;
|
|
Ok(Some(WsOutEvent::ConversationMuted {
|
|
room: room_info,
|
|
data,
|
|
}))
|
|
} else {
|
|
let data = conversation::ConversationUnmutedService {
|
|
user: user_info,
|
|
room: room_info.clone(),
|
|
unmuted_at: now,
|
|
};
|
|
bus.emit_to_user(user_id, "conversation.unmuted", &data)
|
|
.await?;
|
|
Ok(Some(WsOutEvent::ConversationUnmuted {
|
|
room: room_info,
|
|
data,
|
|
}))
|
|
}
|
|
}
|
|
pub(super) async fn conversation_notify_level(
|
|
bus: &ChannelBus,
|
|
user_id: Uuid,
|
|
room: Uuid,
|
|
notify_level: String,
|
|
) -> ChannelResult<Option<WsOutEvent>> {
|
|
Self::ensure_room_access(bus, user_id, room).await?;
|
|
let valid =
|
|
matches!(notify_level.as_str(), "all" | "mentions" | "none");
|
|
if !valid {
|
|
return Err(crate::ChannelError::Internal(
|
|
"notify_level must be 'all', 'mentions', or 'none'".to_string(),
|
|
));
|
|
}
|
|
|
|
let now = Utc::now();
|
|
let old_level: Option<(String,)> = db::sqlx::query_as(
|
|
"SELECT notify_level FROM user_room_state \
|
|
WHERE \"user\" = $1 AND room = $2",
|
|
)
|
|
.bind(user_id)
|
|
.bind(room)
|
|
.fetch_optional(bus.inner.db.reader())
|
|
.await?;
|
|
let old = old_level.map(|r| r.0).unwrap_or_else(|| "all".to_string());
|
|
|
|
db::sqlx::query(
|
|
"INSERT INTO user_room_state (\"user\", room, notify_level, updated_at) \
|
|
VALUES ($1, $2, $3, $4) \
|
|
ON CONFLICT (\"user\", room) DO UPDATE \
|
|
SET notify_level = $3, updated_at = $4",
|
|
)
|
|
.bind(user_id)
|
|
.bind(room)
|
|
.bind(¬ify_level)
|
|
.bind(now)
|
|
.execute(bus.inner.db.writer())
|
|
.await?;
|
|
|
|
let room_info = bus
|
|
.lookup_room(room)
|
|
.await
|
|
.unwrap_or_else(|_| RoomInfo::unknown(room));
|
|
let user_info = bus
|
|
.lookup_user(user_id)
|
|
.await
|
|
.unwrap_or_else(|_| UserInfo::unknown(user_id));
|
|
|
|
let data = conversation::ConversationNotifyLevelChangedService {
|
|
user: user_info,
|
|
room: room_info.clone(),
|
|
old_level: old,
|
|
new_level: notify_level,
|
|
updated_at: now,
|
|
};
|
|
bus.emit_to_user(user_id, "conversation.notify_level_changed", &data)
|
|
.await?;
|
|
Ok(None)
|
|
}
|
|
pub(super) async fn conversation_list(
|
|
bus: &ChannelBus,
|
|
user_id: Uuid,
|
|
) -> ChannelResult<Option<WsOutEvent>> {
|
|
let rooms = crate::rooms::user_rooms(
|
|
&bus.inner.db,
|
|
&bus.inner.cache,
|
|
&bus.inner.config,
|
|
user_id,
|
|
)
|
|
.await?;
|
|
|
|
if rooms.is_empty() {
|
|
return Ok(Some(WsOutEvent::ConversationList { data: vec![] }));
|
|
}
|
|
let rows = db::sqlx::query_as::<_, (
|
|
Uuid, // room id
|
|
String, // room name
|
|
String, // room type
|
|
bool, // is_pinned
|
|
bool, // is_muted
|
|
String, // notify_level
|
|
i64, // last_read_seq
|
|
i64, // max seq from room_message
|
|
)>(
|
|
"SELECT r.id, r.name, r.room_type, \
|
|
COALESCE(s.is_pinned, false), \
|
|
COALESCE(s.is_muted, false), \
|
|
COALESCE(s.notify_level, 'all'), \
|
|
COALESCE(s.last_read_seq, 0), \
|
|
COALESCE((SELECT MAX(seq) FROM room_message \
|
|
WHERE room = r.id AND deleted_at IS NULL), 0) \
|
|
FROM room r \
|
|
LEFT JOIN user_room_state s ON s.room = r.id AND s.\"user\" = $1 \
|
|
WHERE r.id = ANY($2) AND r.deleted_at IS NULL AND r.is_archived = false \
|
|
ORDER BY COALESCE(s.is_pinned, false) DESC, r.name",
|
|
)
|
|
.bind(user_id)
|
|
.bind(&rooms)
|
|
.fetch_all(bus.inner.db.reader())
|
|
.await?;
|
|
|
|
let summaries: Vec<conversation::ConversationSummary> = rows
|
|
.into_iter()
|
|
.map(
|
|
|(
|
|
id,
|
|
name,
|
|
room_type,
|
|
is_pinned,
|
|
is_muted,
|
|
notify_level,
|
|
last_read_seq,
|
|
max_seq,
|
|
)| {
|
|
let unread = (max_seq - last_read_seq).max(0);
|
|
conversation::ConversationSummary {
|
|
room: id,
|
|
room_name: name,
|
|
room_type,
|
|
is_pinned,
|
|
is_muted,
|
|
notify_level,
|
|
last_read_seq,
|
|
max_seq,
|
|
unread_count: unread,
|
|
last_read_at: None,
|
|
}
|
|
},
|
|
)
|
|
.collect();
|
|
|
|
Ok(Some(WsOutEvent::ConversationList { data: summaries }))
|
|
}
|
|
}
|