use crate::app::App;
use crate::{config, hsauth, save_state};
use heatsync_core::block;
use heatsync_core::emote::{emotes_in, Emote, EmoteSet};
fn resolve(app: &App, name: &str) -> Option<Emote> {
app.emotes.get(app.focus)?.get(name).cloned()
}
fn sync(app: &App, key: String, on: bool) {
let (Some(client), Some(ch)) = (app.hs.clone(), app.channels.get(app.focus)) else {
return;
};
hsauth::spawn_note(app.hs_tx.clone(), ch.platform, ch.name.clone(), move || {
let o = client.set_emote_block(&key, on);
if o.ok {
o
} else {
hsauth::Outcome {
ok: false,
text: format!("saved here, but your account didn't get it: {}", o.text),
}
}
});
}
fn block_one(app: &mut App, e: &Emote) -> String {
let Some(key) = block::key(e) else {
return format!("can't block {} (its address is too long)", e.name);
};
if !block::block(e) {
return format!("{} is already blocked", e.name);
}
save_state(app);
sync(app, key, true);
format!("blocked {} — /unblock {} brings it back", e.name, e.name)
}
pub(crate) fn block_cmd(app: &mut App, name: &str) {
app.status = Some(match resolve(app, name) {
Some(e) => block_one(app, &e),
None => format!("no emote called {name} in this tab"),
});
}
pub(crate) fn unblock_cmd(app: &mut App, name: &str) {
let mut keys = block::keys_named(name);
if keys.is_empty() {
keys.extend(
resolve(app, name)
.and_then(|e| block::key(&e))
.filter(|k| block::list().iter().any(|(b, _)| b == k)),
);
}
if keys.is_empty() {
app.status = Some(format!("{name} isn't blocked — /blocked lists them"));
return;
}
for k in keys {
block::unblock_key(&k);
sync(app, k, false);
}
save_state(app);
app.status = Some(format!("unblocked {name}"));
}
pub(crate) fn list_cmd(app: &mut App) {
app.status = Some(summary(&block::list()));
}
pub(crate) fn summary(list: &[(String, String)]) -> String {
if list.is_empty() {
return "nothing blocked — /block <emote>, or b on a message in cursor mode".into();
}
let names: Vec<String> = list
.iter()
.map(|(k, n)| {
if n.is_empty() {
format!("({})", block::describe(k))
} else {
n.clone()
}
})
.collect();
let mut out = format!("blocked {}: ", list.len());
for (i, n) in names.iter().enumerate() {
if out.chars().count() + n.chars().count() > 70 {
out.push_str(&format!("… +{}", names.len() - i));
return out;
}
if i > 0 {
out.push_str(", ");
}
out.push_str(n);
}
out
}
pub(crate) fn cursor_block(app: &mut App, text: &str, refs: &EmoteSet) {
let Some(set) = app.emotes.get(app.focus) else {
return;
};
let pick = emotes_in(text, set, refs)
.into_iter()
.find(|e| !block::is_blocked(e))
.cloned();
app.status = Some(match pick {
Some(e) => block_one(app, &e),
None => "no emote left to block on that line".into(),
});
}
pub(crate) fn pull(app: &App) {
let Some(client) = app.hs.clone() else { return };
std::thread::spawn(move || {
if let Ok(keys) = client.emote_blocks() {
if block::merge_keys(keys) > 0 {
config::save_blocks(&block::list());
}
}
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn summary_names_them_and_truncates() {
assert!(summary(&[]).starts_with("nothing blocked"));
let two = vec![("k1".into(), "Kappa".into()), ("k2".into(), String::new())];
let s = summary(&two);
assert!(s.starts_with("blocked 2: Kappa, ("), "{s}");
let many: Vec<_> = (0..40)
.map(|i| (format!("k{i}"), format!("Emote{i}")))
.collect();
let s = summary(&many);
assert!(s.chars().count() < 90 && s.contains("… +"), "{s}");
}
}