use crate::state::ChannelId;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PostChannelUpdate {
pub channel_id: ChannelId,
pub unread_delta: i64,
pub unread_post_id: Option<String>,
pub last_post: String,
pub has_schedule_post: bool,
pub msg_create_at: i64,
pub visible: bool,
pub sender_user_id: String,
pub post_id: String,
pub last_message: String,
pub mentions: Vec<String>,
pub urgent_user_ids: Vec<String>,
pub mention_hit: bool,
pub urgent_hit: bool,
}
pub fn post_updates(
channel_id: ChannelId,
data: &serde_json::Value,
auth_user_id: &str,
msg_create_at: i64,
) -> PostChannelUpdate {
let msg_type = data.get("type").and_then(|v| v.as_str()).unwrap_or("");
let sender = sender_user_id_from_value(data);
let viewers = string_array(data.get("viewers"));
let visible = visible_to_user(msg_type, &viewers, auth_user_id)
|| (!auth_user_id.is_empty() && sender == auth_user_id);
let should_increment = if auth_user_id.is_empty() {
true
} else {
!sender.is_empty() && sender.as_str() != auth_user_id
};
let post_id = data
.get("postId")
.and_then(|v| v.as_str())
.or_else(|| data.get("id").and_then(|v| v.as_str()))
.unwrap_or("")
.to_string();
let mentions = string_array(data.get("mentions"));
let (urgent_hit, urgent_user_ids) = urgent_from_value(data, auth_user_id);
let mention_hit = !auth_user_id.is_empty() && mentions.iter().any(|id| id == auth_user_id);
let last_post = data.to_string();
let last_message = data
.get("simpleMessage")
.or_else(|| data.get("simple_message"))
.or_else(|| data.get("message"))
.and_then(serde_json::Value::as_str)
.unwrap_or("")
.to_string();
let has_schedule_post = data
.get("isSchedule")
.and_then(serde_json::Value::as_bool)
.unwrap_or(false);
let unread_delta = if visible && should_increment { 1 } else { 0 };
let unread_post_id = if unread_delta > 0 && !post_id.is_empty() {
Some(post_id.clone())
} else {
None
};
PostChannelUpdate {
channel_id,
unread_delta,
unread_post_id,
last_post: if visible { last_post } else { String::new() },
has_schedule_post,
msg_create_at,
visible,
sender_user_id: sender,
post_id,
last_message,
mentions,
urgent_user_ids,
mention_hit: visible && mention_hit,
urgent_hit: visible && urgent_hit,
}
}
pub fn post_updates_from_fields(
channel_id: ChannelId,
fields: &crate::sync_session::PostFields,
auth_user_id: &str,
) -> PostChannelUpdate {
let visible = visible_to_user(&fields.msg_type, &fields.viewers, auth_user_id)
|| (!auth_user_id.is_empty() && fields.user_id == auth_user_id);
let should_increment = if auth_user_id.is_empty() {
true
} else {
!fields.user_id.is_empty() && fields.user_id != auth_user_id
};
let post_id = fields.id.clone();
let (urgent_hit, urgent_user_ids) = urgent_from_fields(fields, auth_user_id);
let mention_hit =
!auth_user_id.is_empty() && fields.mentions.iter().any(|id| id == auth_user_id);
let unread_delta = if visible && should_increment { 1 } else { 0 };
let unread_post_id = if unread_delta > 0 && !post_id.is_empty() {
Some(post_id.clone())
} else {
None
};
PostChannelUpdate {
channel_id,
unread_delta,
unread_post_id,
last_post: if visible {
sync_last_post_json(fields)
} else {
String::new()
},
has_schedule_post: false,
msg_create_at: fields.create_at,
visible,
sender_user_id: fields.user_id.clone(),
post_id,
last_message: if fields.simple_message.is_empty() {
fields.message.clone()
} else {
fields.simple_message.clone()
},
mentions: fields.mentions.clone(),
urgent_user_ids,
mention_hit: visible && mention_hit,
urgent_hit: visible && urgent_hit,
}
}
pub fn message_class(upd: &PostChannelUpdate) -> &'static str {
match (upd.mention_hit, upd.urgent_hit) {
(true, true) => "mention_urgent",
(true, false) => "mention",
(false, true) => "urgent",
(false, false) => "normal",
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum PostUpsertSource {
OnlineSparse,
SyncSnapshot,
}
pub(crate) fn message_v3_post_mutation_ops(
event: &crate::sync_session::EventEnvelope,
auth_user_id: &str,
last_post: String,
source: PostUpsertSource,
) -> Vec<helix_core::effect::StorageOp> {
use helix_core::effect::{
BatchUpdateSpec, ScopedGuardedBumpSpec, SqlValue, StorageOp, UpsertSpec,
};
let fields = &event.fields;
let Ok(event_seq) = i64::try_from(event.seq.0) else {
return Vec::new();
};
if auth_user_id.is_empty() {
return Vec::new();
}
let channel_id = event.channel_id.as_str().to_string();
let viewer_id = auth_user_id.to_string();
let sender_view = fields.user_id == auth_user_id;
let should_bump_unread = event
.unread_bump
.as_ref()
.map(|update| update.unread_delta > 0)
.unwrap_or(!sender_view);
let message_op = match source {
PostUpsertSource::OnlineSparse => crate::channel::event_to_online_upsert_op(event),
PostUpsertSource::SyncSnapshot => crate::channel::event_to_sync_upsert_op(event),
};
let mut ops = vec![
message_op,
StorageOp::BatchUpdate(BatchUpdateSpec {
table: "channel",
key_col: "id",
key_vals: vec![SqlValue::Text(channel_id.clone())],
patch: vec![
("last_post".to_string(), SqlValue::Text(last_post.clone())),
(
"last_post_at".to_string(),
SqlValue::Integer(fields.create_at),
),
(
"last_root_post_at".to_string(),
SqlValue::Integer(fields.create_at),
),
],
}),
];
if matches!(source, PostUpsertSource::SyncSnapshot) {
return ops;
}
let member_row = vec![
("channel_id".to_string(), SqlValue::Text(channel_id.clone())),
("user_id".to_string(), SqlValue::Text(viewer_id.clone())),
("unread_count".to_string(), SqlValue::Integer(0)),
("last_unread_event_seq".to_string(), SqlValue::Integer(0)),
];
ops.push(StorageOp::BatchUpsert(UpsertSpec {
version_column: None,
update_guard: None,
table: "channel_member",
rows: vec![member_row],
conflict_key: Some("channel_id,user_id"),
exclude_from_update: vec!["unread_count", "last_unread_event_seq"],
}));
ops.push(StorageOp::ScopedGuardedBump(ScopedGuardedBumpSpec {
table: "channel_member",
scope_col: "channel_id",
scope_val: SqlValue::Text(channel_id),
key_col: "user_id",
key_val: SqlValue::Text(viewer_id),
bump_col: "unread_count",
bump_delta: i64::from(should_bump_unread),
set_cols: vec![
(
"unread_post_id".to_string(),
SqlValue::Text(if should_bump_unread {
fields.id.clone()
} else {
String::new()
}),
),
("last_post".to_string(), SqlValue::Text(last_post)),
(
"last_post_at".to_string(),
SqlValue::Integer(fields.create_at),
),
(
"last_root_post_at".to_string(),
SqlValue::Integer(fields.create_at),
),
(
"last_unread_event_seq".to_string(),
SqlValue::Integer(event_seq),
),
],
guard_col: "last_unread_event_seq",
guard_val: event_seq,
}));
ops
}
pub fn message_v3_commit_ops(
event: &crate::sync_session::EventEnvelope,
auth_user_id: &str,
last_post: &serde_json::Value,
) -> Vec<helix_core::effect::StorageOp> {
let mut ops = message_v3_post_mutation_ops(
event,
auth_user_id,
last_post.to_string(),
PostUpsertSource::OnlineSparse,
);
if ops.is_empty() {
return ops;
}
ops.push(crate::acl::to_effect::advance_cursor_op(
event.channel_id,
event.seq,
));
ops
}
pub fn message_v3_member_read_op(
channel_id: ChannelId,
auth_user_id: &str,
) -> helix_core::effect::StorageOp {
use helix_core::effect::{ScopedGetSpec, SqlValue, StorageOp};
StorageOp::ScopedGet(ScopedGetSpec {
table: "channel_member",
scope_col: "channel_id",
scope_val: SqlValue::Text(channel_id.as_str().to_string()),
key_col: "user_id",
key_val: SqlValue::Text(auth_user_id.to_string()),
})
}
mod fields;
use fields::{
sender_user_id_from_value, string_array, sync_last_post_json, urgent_from_fields,
urgent_from_value, visible_to_user,
};
#[cfg(test)]
mod tests;