use crate::app::{App, Feed, PinMsg};
use crate::emote::fb::FbEmotes;
use crate::emote::render::EmoteStore;
use crate::http;
use crate::layout::{each_stack, mentions_any, replies_to_me};
use crate::mentions::Mention;
use crate::net::ChatEvent;
use crate::special;
use crate::talk;
use heatsync_core::emote::EmoteSet;
use heatsync_core::proto::ChatLine;
use heatsync_core::{Extra, Message, Platform};
use std::sync::mpsc::Sender;
pub(crate) fn spawn_emote_fetch(
tx: &Sender<(Platform, String, EmoteSet)>,
platform: Platform,
name: String,
) {
let tx = tx.clone();
std::thread::spawn(move || {
let set = http::emote_set(&name, platform).unwrap_or_default();
let _ = tx.send((platform, name, set));
});
}
pub(crate) fn spawn_global_emotes(tx: &Sender<(Platform, String, EmoteSet)>) {
let tx = tx.clone();
std::thread::spawn(move || {
let set = http::global_emotes().unwrap_or_default();
if !set.is_empty() {
let _ = tx.send((Platform::Twitch, String::new(), set));
}
});
}
pub(crate) struct Hist {
pub(crate) platform: Platform,
pub(crate) name: String,
pub(crate) msgs: Vec<Message>,
pub(crate) live: Option<(u64, String)>,
}
pub(crate) fn spawn_history(tx: &Sender<Hist>, platform: Platform, name: String) {
let tx = tx.clone();
std::thread::spawn(move || {
let ask = (platform == Platform::Twitch).then(|| {
let n = name.clone();
std::thread::spawn(move || http::twitch_live(&n))
});
let msgs = http::recent(&name, platform, 60);
let live = ask.and_then(|h| h.join().ok()).flatten();
if !msgs.is_empty() {
let _ = tx.send(Hist {
platform,
name,
msgs,
live,
});
}
});
}
pub(crate) fn spawn_pin(tx: &Sender<PinMsg>, platform: Platform, name: String) {
let tx = tx.clone();
std::thread::spawn(move || {
if let Some(pin) = http::pin(platform, &name) {
let _ = tx.send((platform, name, Some(pin)));
}
});
}
pub(crate) fn drain_pins(app: &mut App) {
while let Ok((platform, name, pin)) = app.pin_rx.try_recv() {
set_pin(app, platform, &name, pin);
}
}
fn set_pin(app: &mut App, platform: Platform, name: &str, pin: Option<heatsync_core::Pin>) {
for c in app
.channels
.iter_mut()
.filter(|c| c.matches(platform, name))
{
c.set_pin(pin.clone());
}
}
pub(crate) fn drain_history(app: &mut App) {
let App {
channels,
emotes,
store,
fb,
hist_rx,
focus,
..
} = app;
while let Ok(Hist {
platform,
name,
msgs,
live,
}) = hist_rx.try_recv()
{
if let Some(i) = channels.iter().position(|c| c.matches(platform, &name)) {
let oldest = msgs
.iter()
.map(|m| m.extra.ts)
.filter(|t| *t > 0)
.min()
.unwrap_or(0);
channels[i].backfill(msgs);
if let Some((start, game)) = live {
crate::streamrow::start_row(
&mut channels[i],
&name,
oldest,
start,
&game,
crate::clock::now_secs(),
);
}
if i == *focus {
let from = channels[i].messages.len().saturating_sub(60);
for m in channels[i].messages.iter().skip(from) {
request_stacks(store, fb, &emotes[i], m.refs(), &m.text);
}
}
}
}
}
pub(crate) fn drain_emotes(app: &mut App) {
let App {
channels,
emotes,
globals,
store,
fb,
emote_rx,
focus,
..
} = app;
let mut dirty = false;
while let Ok((platform, name, set)) = emote_rx.try_recv() {
if name.is_empty() {
*globals = set;
for es in emotes.iter_mut() {
es.merge(globals.clone());
}
dirty = true;
continue;
}
if let Some(i) = channels.iter().position(|c| c.matches(platform, &name)) {
emotes[i].merge(set);
if i == *focus {
dirty = true;
}
}
}
if dirty && !channels.is_empty() {
let i = (*focus).min(channels.len() - 1);
let from = channels[i].messages.len().saturating_sub(60);
for m in channels[i].messages.iter().skip(from) {
request_stacks(store, fb, &emotes[i], m.refs(), &m.text);
}
}
}
pub(crate) fn request_stacks(
store: &mut Option<EmoteStore>,
fb: &Option<FbEmotes>,
set: &EmoteSet,
refs: &EmoteSet,
text: &str,
) {
if set.is_empty() && refs.is_empty() {
return;
}
if let Some(s) = store {
each_stack(text, set, refs, |k| s.request(k));
} else if let Some(f) = fb {
each_stack(text, set, refs, |k| f.request(k));
}
}
pub(crate) fn deliver(app: &mut App, l: ChatLine, now: u64) {
if let Some(i) = app
.channels
.iter()
.position(|c| c.matches(l.platform, &l.channel))
{
deliver_at(app, i, l, now, true);
}
}
fn fan_out(app: &mut App, l: &ChatLine, now: u64) {
for i in 0..app.channels.len() {
if app.channels[i].kind == heatsync_core::TabKind::Channel {
deliver_at(app, i, l.clone(), now, false);
}
}
}
fn deliver_at(app: &mut App, i: usize, l: ChatLine, now: u64, own: bool) {
let App {
channels,
emotes,
store,
fb,
focus,
me,
paints,
seen,
..
} = app;
if l.note.is_some()
&& channels[i].messages.iter().rev().take(16).any(|e| {
e.platform == l.platform
&& e.user == l.user
&& e.text == l.content
&& e.note.as_ref() == l.note.as_ref()
})
{
return;
}
if let Some(u) = &l.uid {
paints.want(u);
}
if l.note.is_none() {
seen.note(l.platform, &l.user, l.color.as_deref(), l.uid.as_deref());
}
let refs = (!l.emotes.is_empty())
.then(|| std::sync::Arc::new(heatsync_core::emote::ref_set(&l.emotes)));
if i == *focus {
let r = refs.as_deref().unwrap_or(EmoteSet::none());
request_stacks(store, fb, &emotes[i], r, &l.content);
}
let pinged = l.note.is_none()
&& !me.iter().any(|n| l.user.eq_ignore_ascii_case(n))
&& (mentions_any(&l.content, me) || replies_to_me(l.reply_to.as_deref(), me));
let evented = l.stream.is_some();
let chan = l.channel.clone();
let msg = Message {
platform: l.platform,
user: l.user,
text: l.content,
color: l.color,
badges: l.badges,
id: l.id,
gone: None,
reply_to: l.reply_to,
note: l.note,
extra: Extra {
ts: crate::clock::now_secs(),
..l.extra
},
emotes: refs,
tags: Vec::new(),
heat: 0.0,
uid: l.uid,
};
if !own {
channels[i].system(msg);
return;
}
channels[i].record(msg, now);
let mention = pinged
.then(|| channels[i].messages.back())
.flatten()
.map(|m| Mention {
platform: m.platform,
channel: chan,
user: m.user.clone(),
color: m.color.clone(),
text: m.text.clone(),
id: m.id.clone(),
at: crate::clock::now_secs() as i64,
});
if pinged {
channels[i].ping();
}
if evented {
channels[i].flag_event();
}
if let Some(m) = mention {
special::mention(app, m);
}
}
pub(crate) fn route(app: &mut App, l: ChatLine, now: u64) -> Vec<ChatLine> {
let follow = l.stream.as_ref().is_some_and(|s| s.follow);
if follow {
app.pick_cache.stale();
if app
.channels
.iter()
.any(|c| c.matches(l.platform, &l.channel))
{
return Vec::new();
}
}
let out = app.streams.push(l, now);
if follow {
for l in &out {
if let Some(n) = &l.note {
app.status = Some(format!("{} {}", l.channel, n.what));
}
special::follow_event(app, l);
fan_out(app, l, now);
}
return Vec::new();
}
out
}
pub(crate) fn drain_feed(app: &mut App) -> bool {
let Feed::Live { rx, start, .. } = &app.feed else {
return false; };
let now = start.elapsed().as_millis() as u64;
let mut got = false;
let mut evs = Vec::new();
let mut specials: Vec<ChatEvent> = Vec::new();
while let Ok(ev) = rx.try_recv() {
evs.push(ev);
}
for ev in evs {
got = true;
match ev {
ChatEvent::Line(l) => {
for l in route(app, l, now) {
deliver(app, l, now);
}
}
ChatEvent::FollowLive(live) => app.streams.seed_live(live),
ChatEvent::FollowHistory(h) => special::follow_history(app, h),
ChatEvent::FollowColors(c) => special::follow_colors(app, c),
ChatEvent::Pin(p, c, pin) => set_pin(app, p, &c, pin),
ChatEvent::Room(c, r) => {
for l in app.rooms.update(&c, r) {
deliver(app, l, now);
}
}
ChatEvent::Delete(d) => {
for c in app.channels.iter_mut() {
let here = if d.channel.is_empty() {
c.subs().any(|(p, _)| p == d.platform)
} else {
c.matches(d.platform, &d.channel)
};
if here {
c.apply(&d);
}
}
}
ev @ (ChatEvent::Special(_)
| ChatEvent::FeedHeat { .. }
| ChatEvent::FeedHidden { .. }
| ChatEvent::FeedEdit { .. }) => specials.push(ev),
ChatEvent::Account => special::account_changed(app),
ChatEvent::Follows => talk::refresh_follows(app),
ChatEvent::Color { id, names, color } => {
app.paints.set_color(id.as_deref(), &names, &color)
}
ChatEvent::Cosmetic(ids) => app.paints.invalidate(&ids),
ChatEvent::Connected | ChatEvent::Disconnected => {
if let Feed::Live { connected, .. } = &mut app.feed {
*connected = matches!(ev, ChatEvent::Connected);
}
}
ChatEvent::Auth(ok) => special::auth(app, ok),
ChatEvent::SendResult { ok, error } => {
if !ok {
app.status = Some(format!("send failed: {}", error.unwrap_or_default()));
}
}
}
}
for l in app.streams.tick(now) {
got = true;
deliver(app, l, now);
}
for ev in specials {
special::event(app, ev);
}
special::drain_backlog(app);
talk::drain(app);
got
}
pub(crate) fn drain_twitch(app: &mut App) {
let Some(rx) = &app.twitch_notes else { return };
while let Ok(n) = rx.try_recv() {
if let Some(name) = &n.channel {
if let Some(ch) = app
.channels
.iter_mut()
.find(|c| c.matches(Platform::Twitch, name))
{
ch.system(Message {
platform: Platform::Twitch,
user: String::new(),
text: String::new(),
color: None,
badges: Vec::new(),
id: None,
gone: None,
reply_to: None,
note: Some(heatsync_core::Note {
kind: heatsync_core::NoteKind::Notice,
what: n.text.clone(),
}),
extra: Default::default(),
emotes: None,
tags: Vec::new(),
heat: 0.0,
uid: None,
});
}
}
app.status = Some(n.text);
}
}
pub(crate) fn drain_hs(app: &mut App) {
while let Ok(names) = app.names_rx.try_recv() {
for n in names {
if !app.me.iter().any(|m| m.eq_ignore_ascii_case(&n)) {
app.me.push(n);
}
}
}
while let Ok(n) = app.hs_notes.try_recv() {
if n.logged_out {
app.hs = None; }
if n.sent {
crate::temp::sent(app, n.platform, &n.channel);
}
if n.text.is_empty() {
continue;
}
if let Some(ch) = app
.channels
.iter_mut()
.find(|c| c.matches(n.platform, &n.channel))
{
ch.system(Message {
platform: n.platform,
user: String::new(),
text: String::new(),
color: None,
badges: Vec::new(),
id: None,
gone: None,
reply_to: None,
note: Some(heatsync_core::Note {
kind: heatsync_core::NoteKind::Notice,
what: n.text.clone(),
}),
extra: Default::default(),
emotes: None,
tags: Vec::new(),
heat: 0.0,
uid: None,
});
}
app.status = Some(n.text);
}
}
pub(crate) fn pump_loads(app: &mut App) {
app.paints.pump();
if let Some(store) = &mut app.store {
store.pump();
} else if let Some(fb) = &mut app.fb {
fb.pump();
}
}
pub(crate) fn tick_advance(app: &mut App) {
let App {
channels,
emotes,
feed,
store,
fb,
focus,
..
} = app;
match feed {
Feed::Mock(driver) => {
driver.tick(channels);
}
Feed::Live { start, .. } => {
let now = start.elapsed().as_millis() as u64;
for ch in channels.iter_mut() {
ch.cool(now);
}
}
}
if let Some(ch) = channels.get(*focus) {
let from = ch.messages.len().saturating_sub(60);
for m in ch.messages.iter().skip(from) {
request_stacks(store, fb, &emotes[*focus], m.refs(), &m.text);
}
}
}