use super::*;
pub(super) const CHAT_MUTE_KIND: u32 = 10449;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub(crate) struct ChatMuteState {
pub chat_id: String,
pub until_secs: Option<u64>,
pub updated_at_ms: u64,
}
impl ChatMuteState {
fn version(&self) -> (u64, Option<u64>) {
(self.updated_at_ms, self.until_secs)
}
}
impl AppCore {
pub(super) fn chat_mute_snapshot(&self) -> Vec<ChatMuteState> {
let mut states = self.chat_mute_states.clone();
for (chat_id, until_secs) in self
.preferences
.muted_chat_ids
.iter()
.map(|id| (id, 0))
.chain(
self.preferences
.timed_chat_mutes
.iter()
.map(|m| (&m.chat_id, m.until_secs)),
)
{
states
.entry(chat_id.clone())
.or_insert_with(|| ChatMuteState {
chat_id: chat_id.clone(),
until_secs: Some(until_secs),
updated_at_ms: 1,
});
}
states.into_values().collect()
}
pub(super) fn restore_chat_mute_projection(&mut self) {
self.chat_mute_states = self
.chat_mute_snapshot()
.into_iter()
.map(|state| (state.chat_id.clone(), state))
.collect();
for state in self.chat_mute_states.values().cloned().collect::<Vec<_>>() {
self.project_chat_mute(&state);
}
}
fn project_chat_mute(&mut self, state: &ChatMuteState) {
self.preferences
.muted_chat_ids
.retain(|id| id != &state.chat_id);
self.preferences
.timed_chat_mutes
.retain(|mute| mute.chat_id != state.chat_id);
match state.until_secs {
Some(0) => self.preferences.muted_chat_ids.push(state.chat_id.clone()),
Some(until_secs) if until_secs > unix_now().get() => {
self.preferences.timed_chat_mutes.push(ChatMuteDeadline {
chat_id: state.chat_id.clone(),
until_secs,
});
}
_ => {}
}
self.preferences.muted_chat_ids.sort();
self.preferences
.timed_chat_mutes
.sort_by(|a, b| a.chat_id.cmp(&b.chat_id));
}
pub(super) fn merge_chat_mute(&mut self, state: ChatMuteState) -> anyhow::Result<bool> {
if self
.normalize_local_chat_setting_id(&state.chat_id)
.as_deref()
!= Some(&state.chat_id)
|| state.chat_id.len() > 134
|| state
.chat_id
.chars()
.any(|c| c.is_ascii_control() || c == ' ')
|| state.updated_at_ms == 0
|| state.updated_at_ms > unix_now_ms().saturating_add(300_000)
|| state
.until_secs
.is_some_and(|until| until > 253_402_300_799)
|| self
.chat_mute_states
.get(&state.chat_id)
.is_some_and(|old| old.version() >= state.version())
{
return Ok(false);
}
self.app_store.save_chat_mute_state(&state)?;
self.project_chat_mute(&state);
self.chat_mute_states.insert(state.chat_id.clone(), state);
self.schedule_chat_mute_expiry();
self.mark_mobile_push_dirty();
Ok(true)
}
pub(super) fn set_synced_chat_mute(&mut self, chat_id: &str, until_secs: Option<u64>) {
let Some(chat_id) = self.normalize_local_chat_setting_id(chat_id) else {
return;
};
let updated_at_ms = unix_now_ms().max(
self.chat_mute_states
.get(&chat_id)
.map_or(1, |old| old.updated_at_ms.saturating_add(1)),
);
let state = ChatMuteState {
chat_id,
until_secs,
updated_at_ms,
};
match self.merge_chat_mute(state.clone()) {
Ok(true) => {}
Ok(false) => return,
Err(error) => {
self.push_debug_log("chat_mute.save_failed", error.to_string());
self.state.toast = Some("Could not save notification setting. Try again.".into());
self.emit_state();
return;
}
}
if let Some(owner) = self.logged_in.as_ref().map(|account| account.owner_pubkey) {
let content =
serde_json::json!({ "type": "chat-mute", "v": 1, "mute": state }).to_string();
let unsigned =
nostr::EventBuilder::new(nostr::Kind::Custom(CHAT_MUTE_KIND as u16), content)
.tag(nostr::Tag::public_key(owner))
.allow_self_tagging()
.build(owner);
self.send_protocol_engine_unsigned_event_to_local_siblings(
owner,
&owner.to_hex(),
unsigned,
"chat_mute.self_sync",
);
}
self.broadcast_device_sync_snapshot();
self.rebuild_persist_and_emit_state();
}
pub(super) fn receive_chat_mute_control(
&mut self,
sender_owner: PublicKey,
sender_device: Option<PublicKey>,
content: &str,
) -> bool {
let Some(account) = self.logged_in.as_ref() else {
return true;
};
if sender_owner != account.owner_pubkey
|| !sender_device
.is_some_and(|device| self.device_sync_peer_is_authorized(&device.to_hex()))
{
return true;
}
let Ok(value) = serde_json::from_str::<serde_json::Value>(content) else {
return true;
};
if value.get("type").and_then(serde_json::Value::as_str) != Some("chat-mute")
|| value.get("v").and_then(serde_json::Value::as_u64) != Some(1)
{
return true;
}
let Some(payload) = value.get("mute") else {
return true;
};
let Ok(state) = serde_json::from_value::<ChatMuteState>(payload.clone()) else {
return true;
};
match self.merge_chat_mute(state) {
Ok(true) => {
self.rebuild_persist_and_emit_state();
true
}
Ok(false) => true,
Err(error) => {
self.push_debug_log("chat_mute.save_failed", error.to_string());
false }
}
}
}