use std::collections::{HashMap, VecDeque};
use std::sync::Arc;
use std::time::Instant;
use super::keys::{Ask, Prompt};
use super::plural;
use crate::clock;
use crate::event::{Pointer, Reacted, Said};
use crate::live::{Command, Hub, REACTIONS};
use crate::notes::Feedback;
use crate::visitors::{Presence, Roster};
const PAGES_KEEP: usize = 20;
const SUGGESTIONS: usize = 5;
const CHAT_KEEP: usize = 60;
pub struct Message {
pub mine: bool,
pub who: String,
pub page: String,
pub text: String,
pub clock: String,
}
pub struct Audience {
pub roster: Roster,
pub pointers: HashMap<String, (Pointer, Instant)>,
pub chat: VecDeque<Message>,
pages: Vec<String>,
pub reactions: [u32; REACTIONS.len()],
pub feedback: u32,
pub selected: Option<String>,
hub: Arc<Hub>,
}
impl Audience {
pub fn new(hub: Arc<Hub>) -> Self {
Self {
roster: Roster::default(),
pointers: HashMap::new(),
chat: VecDeque::new(),
pages: Vec::new(),
reactions: [0; REACTIONS.len()],
feedback: 0,
selected: None,
hub,
}
}
pub fn presence(&mut self, presence: Presence, now: Instant) -> bool {
self.remember(&presence.page);
self.roster.update(presence, now)
}
pub fn pointer(&mut self, pointer: Pointer, now: Instant) {
if self.roster.get(&pointer.sid).is_some() {
self.pointers.insert(pointer.sid.clone(), (pointer, now));
}
}
pub fn note(&mut self, note: Feedback) -> (String, String) {
self.feedback += 1;
let excerpt: String = note.text().replace('\n', " ยท ").chars().take(40).collect();
let title = format!("โ {} on {}", note.device, note.page);
let mut text = format!("โ {}", note.message).trim_end().to_string();
for (index, point) in note.points.iter().enumerate() {
text.push_str(&format!("\n๐ {}", point.line(index + 1)));
}
if note.shot.is_some() {
text.push_str("\n๐ท screenshot");
}
self.message(Message {
mine: false,
who: note.device,
page: note.page,
text,
clock: clock::clock(),
});
(title, excerpt)
}
pub fn said(&mut self, said: Said) -> (String, String) {
let excerpt: String = said.text.chars().take(40).collect();
let title = format!("โ {} on {}", said.device, said.page);
self.message(Message {
mine: false,
who: said.device,
page: said.page,
text: said.text,
clock: clock::clock(),
});
(title, excerpt)
}
pub fn reacted(&mut self, reaction: &Reacted) -> (String, &'static str) {
self.reactions[reaction.kind] += 1;
let (emoji, label) = REACTIONS[reaction.kind];
(format!("{emoji} from {}", reaction.device), label)
}
pub fn select(&mut self, sid: Option<String>) {
self.hub.follow(sid.as_deref());
self.selected = sid;
}
pub fn send(&self, command: &Command) -> usize {
self.hub.send(self.selected.as_deref(), command)
}
pub fn react(&self, kind: usize) -> usize {
self.hub
.react(self.selected.as_deref(), &self.target(), REACTIONS[kind].0)
}
pub fn say(&mut self, text: &str) -> usize {
let to = self.target();
let reached = self.hub.say(self.selected.as_deref(), &to, text);
if reached > 0 {
self.message(Message {
mine: true,
who: to,
page: String::new(),
text: text.to_string(),
clock: clock::clock(),
});
}
reached
}
pub fn suggestions(&self, prompt: &Prompt) -> Vec<String> {
match prompt.ask {
Ask::Chat | Ask::Filter => return Vec::new(),
Ask::React => {
return REACTIONS
.iter()
.enumerate()
.map(|(i, (emoji, label))| format!("{} {emoji} {label}", i + 1))
.collect();
}
Ask::Go => {}
}
let text = prompt.text.trim();
self.pages
.iter()
.filter(|page| page.contains(text))
.take(SUGGESTIONS)
.cloned()
.collect()
}
pub fn target(&self) -> String {
self.selected
.as_ref()
.and_then(|sid| self.roster.get(sid))
.map_or_else(|| "everyone".into(), |v| v.presence.device.clone())
}
pub fn reached(&self, pages: usize) -> String {
match pages {
0 => "No page is connected live.".to_string(),
n => format!("{}, {}.", self.target(), plural(n, "page")),
}
}
fn remember(&mut self, page: &str) {
self.pages.retain(|seen| seen != page);
self.pages.insert(0, page.to_string());
self.pages.truncate(PAGES_KEEP);
}
fn message(&mut self, message: Message) {
self.chat.push_back(message);
if self.chat.len() > CHAT_KEEP {
self.chat.pop_front();
}
}
}