use crate::error::ImError;
use crate::module::ImModule;
use helix_core::effect::{GetSpec, SqlValue, StorageOp};
use helix_core::tick::PortOutcome;
use helix_core::{Correlation, Effect, EffectSink};
pub(crate) fn queue_commit(
state: &mut crate::state::ImState,
corr: Correlation,
event: crate::sync_session::EventEnvelope,
out: &mut EffectSink,
) {
let message_id = event
.msg_id
.as_deref()
.filter(|id| !id.is_empty())
.unwrap_or(event.fields.id.as_str());
let urgent_op = crate::channel::write::message_v3_urgent_op(
message_id,
event.fields.expedite_map.clone(),
event.seq.0,
event.fields.update_at,
);
let cursor_op = crate::acl::to_effect::advance_cursor_op(event.channel_id, event.seq);
state.corr_map.insert(
corr,
crate::state::CorrelationContext::MessageV3UrgentPersist {
event: Box::new(event),
},
);
out.push(Effect::PersistAtomic {
corr,
ops: vec![urgent_op, cursor_op],
});
}
impl ImModule {
pub(super) fn handle_message_v3_urgent_persist_reply(
&mut self,
event: crate::sync_session::EventEnvelope,
outcome: &PortOutcome,
out: &mut EffectSink,
) -> Result<(), ImError> {
if !matches!(outcome, PortOutcome::Ok(_)) {
if let Some(channel) = self.state.channels.get_mut(&event.channel_id) {
channel.restore_message_v3_post(event, out);
}
return Ok(());
}
let message_id = event
.msg_id
.as_deref()
.filter(|id| !id.is_empty())
.unwrap_or(event.fields.id.as_str())
.to_string();
let next = self
.state
.channels
.get_mut(&event.channel_id)
.and_then(|channel| channel.commit_message_v3_post(event.seq));
self.state
.invalidate_recent_message_coverage(event.channel_id);
let corr = self.alloc_corr_internal();
self.state.corr_map.insert(
corr,
crate::state::CorrelationContext::MessageV3UrgentReadback {
message_id: message_id.clone(),
},
);
out.push(Effect::Persist {
corr,
ops: vec![StorageOp::Get(GetSpec {
table: "message",
key_col: "id",
key_val: SqlValue::Text(message_id),
})],
});
if let Some(next_event) = next {
self.queue_next_message_v3_event(next_event, out)?;
}
Ok(())
}
pub(super) fn handle_message_v3_urgent_readback_reply(
&mut self,
message_id: String,
outcome: &PortOutcome,
out: &mut EffectSink,
) -> Result<(), ImError> {
let PortOutcome::Ok(reply) = outcome else {
return Ok(());
};
let rows = helix_core::port_codec::rows_from_reply_bytes(&reply.0)
.map_err(|error| ImError::Parse(format!("urgent readback: {error}")))?;
let Some(row) = rows.first() else {
return Ok(());
};
let Some(event) = crate::event::post::urgent_update_from_row(row)? else {
return Ok(());
};
out.push(event.into_effect());
if let Some(signal) =
urgent_channel_signal_from_row(row, self.config.auth_user_id.as_str())?
{
out.push(signal.into_effect());
}
let _confirmed_message_id = message_id;
Ok(())
}
}
fn urgent_channel_signal_from_row(
row: &helix_core::effect::Row,
viewer_user_id: &str,
) -> Result<Option<crate::event::MessageV3Event>, ImError> {
let (Some(post_id), Some(channel_id), Some(expedite_map)) = (
text_column(row, "id"),
text_column(row, "channel_id"),
text_column(row, "expedite_map")
.and_then(|raw| serde_json::from_str::<serde_json::Value>(raw).ok())
.filter(serde_json::Value::is_object),
) else {
return Ok(None);
};
let data = if has_pending_recipient(&expedite_map, viewer_user_id) {
serde_json::json!({
"channelId": channel_id,
"userId": viewer_user_id,
"projectionAuthority": "server-member-absolute",
"memberScope": "current_user",
"urgentPostList": [post_id],
"urgentCurrentName": requester_display_name(&expedite_map),
"hasUrgentPost": true,
})
} else {
serde_json::json!({
"channelId": channel_id,
"userId": viewer_user_id,
"projectionAuthority": "server-member-absolute",
"memberScope": "current_user",
"urgentPostList": [],
"urgentCurrentName": "",
"hasUrgentPost": false,
})
};
crate::event::channel::update(data).map(Some)
}
fn has_pending_recipient(expedite_map: &serde_json::Value, viewer_user_id: &str) -> bool {
if viewer_user_id.is_empty() {
return false;
}
expedite_map
.get("recipients")
.and_then(serde_json::Value::as_object)
.and_then(|recipients| recipients.get(viewer_user_id))
.is_some_and(|recipient| {
recipient
.get("status")
.and_then(serde_json::Value::as_i64)
.is_some_and(|status| status <= 0)
})
}
fn requester_display_name(expedite_map: &serde_json::Value) -> &str {
expedite_map
.get("sender")
.and_then(serde_json::Value::as_object)
.and_then(|sender| {
["name", "nickname", "userName", "displayName"]
.iter()
.find_map(|key| sender.get(*key))
})
.and_then(serde_json::Value::as_str)
.unwrap_or_default()
}
fn text_column<'a>(row: &'a helix_core::effect::Row, column: &str) -> Option<&'a str> {
row.iter().find_map(|(name, value)| {
(name == column)
.then_some(value)
.and_then(|value| match value {
helix_core::effect::SqlValue::Text(value) => Some(value.as_str()),
_ => None,
})
})
}
#[cfg(test)]
mod tests {
use super::has_pending_recipient;
use serde_json::json;
#[test]
fn sender_does_not_receive_recipient_urgent_badge() {
let expedite_map = json!({
"sender": { "id": "444", "name": "破坏者" },
"recipients": {
"447": { "status": 0 },
"678": { "status": 0 }
}
});
assert!(!has_pending_recipient(&expedite_map, "444"));
assert!(has_pending_recipient(&expedite_map, "447"));
}
#[test]
fn confirmed_recipient_does_not_keep_urgent_badge() {
let expedite_map = json!({
"recipients": {
"447": { "status": 1 }
}
});
assert!(!has_pending_recipient(&expedite_map, "447"));
assert!(!has_pending_recipient(&expedite_map, ""));
}
}