|
|
@ -1,33 +1,49 @@ |
|
|
|
use crate::gui_events::GuiEvent; |
|
|
|
use crate::gui_events::GuiEvent; |
|
|
|
use crate::message::Message; |
|
|
|
use crate::message::{DegMessage, ServiceMsg}; |
|
|
|
use core::default::Default; |
|
|
|
use core::default::Default; |
|
|
|
|
|
|
|
use futures::Stream; |
|
|
|
use iced::{ |
|
|
|
use iced::{ |
|
|
|
button, Align, Button, Column, Element, HorizontalAlignment, Length, Row, Sandbox, Settings, |
|
|
|
button, Align, Application, Button, Column, Element, HorizontalAlignment, Length, Row, |
|
|
|
Text, TextInput, VerticalAlignment, |
|
|
|
Text, TextInput, VerticalAlignment, |
|
|
|
}; |
|
|
|
}; |
|
|
|
use ironforce::{Keys, PublicKey}; |
|
|
|
use ironforce::res::{IFError, IFResult}; |
|
|
|
use serde::{Deserialize, Serialize}; |
|
|
|
use ironforce::{IronForce, Keys, Message, MessageType, PublicKey}; |
|
|
|
|
|
|
|
use std::pin::Pin; |
|
|
|
|
|
|
|
use std::sync::{Arc, Mutex}; |
|
|
|
|
|
|
|
use std::task::{Context, Poll}; |
|
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Debug)] |
|
|
|
#[derive(Clone, Debug)] |
|
|
|
pub struct Chat { |
|
|
|
pub struct Chat { |
|
|
|
pkey: PublicKey, |
|
|
|
pkey: PublicKey, |
|
|
|
messages: Vec<(bool, Message)>, |
|
|
|
messages: Vec<(bool, DegMessage)>, |
|
|
|
name: String, |
|
|
|
name: String, |
|
|
|
scrolled: f32, |
|
|
|
scrolled: f32, |
|
|
|
pub input: String, |
|
|
|
pub input: String, |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
pub fn view_message(msg: &(bool, Message)) -> Option<Element<GuiEvent>> { |
|
|
|
impl Chat { |
|
|
|
|
|
|
|
pub fn new(pkey: PublicKey) -> Self { |
|
|
|
|
|
|
|
Self { |
|
|
|
|
|
|
|
pkey, |
|
|
|
|
|
|
|
messages: vec![], |
|
|
|
|
|
|
|
name: "".to_string(), |
|
|
|
|
|
|
|
scrolled: 0.0, |
|
|
|
|
|
|
|
input: "".to_string(), |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pub fn view_message(msg: &(bool, DegMessage)) -> Option<Element<GuiEvent>> { |
|
|
|
let msg = &msg.1; |
|
|
|
let msg = &msg.1; |
|
|
|
match msg { |
|
|
|
match msg { |
|
|
|
Message::Text(t) => Some( |
|
|
|
DegMessage::Text(t) => Some( |
|
|
|
iced::Container::new(Text::new(t.as_str())) |
|
|
|
iced::Container::new(Text::new(t.as_str())) |
|
|
|
.padding(10) |
|
|
|
.padding(10) |
|
|
|
.style(style::Container::Message) |
|
|
|
.style(style::Container::Message) |
|
|
|
.into(), |
|
|
|
.into(), |
|
|
|
), |
|
|
|
), |
|
|
|
Message::File(_) => None, |
|
|
|
DegMessage::File(_) => None, |
|
|
|
Message::Service(_) => None, |
|
|
|
DegMessage::Service(_) => None, |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
@ -54,7 +70,11 @@ mod style { |
|
|
|
})), |
|
|
|
})), |
|
|
|
border_radius: 5.0, |
|
|
|
border_radius: 5.0, |
|
|
|
shadow_offset: Vector::new(1.0, 1.0), |
|
|
|
shadow_offset: Vector::new(1.0, 1.0), |
|
|
|
text_color: if self != &Button::InactiveChat { Color::WHITE } else { Color::BLACK }, |
|
|
|
text_color: if self != &Button::InactiveChat { |
|
|
|
|
|
|
|
Color::WHITE |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
Color::BLACK |
|
|
|
|
|
|
|
}, |
|
|
|
..button::Style::default() |
|
|
|
..button::Style::default() |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
@ -118,11 +138,20 @@ impl Chat { |
|
|
|
.into() |
|
|
|
.into() |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
pub fn preview<'a>(&'a self, state: &'a mut button::State, i: usize, is_selected: bool) -> Element<'a, GuiEvent> { |
|
|
|
pub fn preview<'a>( |
|
|
|
|
|
|
|
&'a self, |
|
|
|
|
|
|
|
state: &'a mut button::State, |
|
|
|
|
|
|
|
i: usize, |
|
|
|
|
|
|
|
is_selected: bool, |
|
|
|
|
|
|
|
) -> Element<'a, GuiEvent> { |
|
|
|
Button::new(state, Text::new(self.name.as_str())) |
|
|
|
Button::new(state, Text::new(self.name.as_str())) |
|
|
|
.width(Length::Fill) |
|
|
|
.width(Length::Fill) |
|
|
|
.padding(10) |
|
|
|
.padding(10) |
|
|
|
.style(if is_selected { style::Button::Primary } else { style::Button::InactiveChat }) |
|
|
|
.style(if is_selected { |
|
|
|
|
|
|
|
style::Button::Primary |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
style::Button::InactiveChat |
|
|
|
|
|
|
|
}) |
|
|
|
.on_press(GuiEvent::ChatSelect(i)) |
|
|
|
.on_press(GuiEvent::ChatSelect(i)) |
|
|
|
.into() |
|
|
|
.into() |
|
|
|
} |
|
|
|
} |
|
|
@ -157,7 +186,7 @@ impl Chat { |
|
|
|
pub fn example(i: usize) -> Chat { |
|
|
|
pub fn example(i: usize) -> Chat { |
|
|
|
Self { |
|
|
|
Self { |
|
|
|
pkey: Keys::generate().get_public(), |
|
|
|
pkey: Keys::generate().get_public(), |
|
|
|
messages: vec![(false, Message::Text(format!("Example message {}", i)))], |
|
|
|
messages: vec![(false, DegMessage::Text(format!("Example message {}", i)))], |
|
|
|
name: format!("Example user ({})", i), |
|
|
|
name: format!("Example user ({})", i), |
|
|
|
scrolled: 0.0, |
|
|
|
scrolled: 0.0, |
|
|
|
input: "".to_string(), |
|
|
|
input: "".to_string(), |
|
|
@ -165,12 +194,135 @@ impl Chat { |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
#[derive(Default, Clone, Debug)] |
|
|
|
#[derive(Clone)] |
|
|
|
|
|
|
|
pub struct Degeon { |
|
|
|
|
|
|
|
pub chats: Vec<Chat>, |
|
|
|
|
|
|
|
pub my_name: String, |
|
|
|
|
|
|
|
pub keys: Keys, |
|
|
|
|
|
|
|
pub ironforce: Arc<Mutex<IronForce>>, |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
impl Default for Degeon { |
|
|
|
|
|
|
|
fn default() -> Self { |
|
|
|
|
|
|
|
let ironforce = IronForce::from_file("".to_string()).unwrap(); |
|
|
|
|
|
|
|
let keys = ironforce.keys.clone(); |
|
|
|
|
|
|
|
let (_thread, ironforce) = ironforce.launch_main_loop(500); |
|
|
|
|
|
|
|
Self { |
|
|
|
|
|
|
|
chats: vec![], |
|
|
|
|
|
|
|
my_name: "".to_string(), |
|
|
|
|
|
|
|
keys, |
|
|
|
|
|
|
|
ironforce, |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
impl Degeon { |
|
|
|
|
|
|
|
pub fn chat_with(&self, pkey: &PublicKey) -> Option<usize> { |
|
|
|
|
|
|
|
self.chats.iter().position(|chat| &chat.pkey == pkey) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pub fn process_message(&self, msg: ironforce::Message) -> IFResult<Option<GuiEvent>> { |
|
|
|
|
|
|
|
let deg_msg: DegMessage = |
|
|
|
|
|
|
|
serde_json::from_slice(msg.get_decrypted(&self.keys)?.as_slice())?; |
|
|
|
|
|
|
|
let sender = msg.get_sender(&self.keys).unwrap(); |
|
|
|
|
|
|
|
Ok(match °_msg { |
|
|
|
|
|
|
|
DegMessage::Text(_) | DegMessage::File(_) => { |
|
|
|
|
|
|
|
Some(GuiEvent::NewMessageInChat(sender, deg_msg)) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
DegMessage::Service(msg) => match msg { |
|
|
|
|
|
|
|
ServiceMsg::NameRequest => self |
|
|
|
|
|
|
|
.send_message( |
|
|
|
|
|
|
|
DegMessage::Service(ServiceMsg::NameStatement(self.my_name.clone())), |
|
|
|
|
|
|
|
&sender, |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
.map(|_| None)?, |
|
|
|
|
|
|
|
ServiceMsg::NameStatement(name) => { |
|
|
|
|
|
|
|
Some(GuiEvent::SetName(sender, name.to_string())) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
ServiceMsg::Ping => self |
|
|
|
|
|
|
|
.send_message(DegMessage::Service(ServiceMsg::HiThere), &sender) |
|
|
|
|
|
|
|
.map(|_| None)?, |
|
|
|
|
|
|
|
ServiceMsg::HiThere => Some(GuiEvent::NewChat(sender)), |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pub fn send_multicast(&self, msg: DegMessage) -> IFResult<()> { |
|
|
|
|
|
|
|
self.ironforce.lock().unwrap().send_to_all( |
|
|
|
|
|
|
|
Message::build() |
|
|
|
|
|
|
|
.message_type(MessageType::Broadcast) |
|
|
|
|
|
|
|
.content(serde_json::to_vec(&msg)?) |
|
|
|
|
|
|
|
.sign(&self.keys) |
|
|
|
|
|
|
|
.build()?, |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pub fn send_message(&self, msg: DegMessage, target: &PublicKey) -> IFResult<()> { |
|
|
|
|
|
|
|
if self.ironforce.lock().unwrap().get_tunnel(target).is_none() { |
|
|
|
|
|
|
|
println!("Creating a tunnel"); |
|
|
|
|
|
|
|
self.ironforce |
|
|
|
|
|
|
|
.lock() |
|
|
|
|
|
|
|
.unwrap() |
|
|
|
|
|
|
|
.initialize_tunnel_creation(target)?; |
|
|
|
|
|
|
|
let mut counter = 0; |
|
|
|
|
|
|
|
while self.ironforce.lock().unwrap().get_tunnel(target).is_none() { |
|
|
|
|
|
|
|
std::thread::sleep(std::time::Duration::from_millis(350)); |
|
|
|
|
|
|
|
counter += 1; |
|
|
|
|
|
|
|
if counter > 100 { |
|
|
|
|
|
|
|
return Err(IFError::TunnelNotFound); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
self.ironforce.lock().unwrap().send_to_all( |
|
|
|
|
|
|
|
Message::build() |
|
|
|
|
|
|
|
.message_type(MessageType::Broadcast) |
|
|
|
|
|
|
|
.content(serde_json::to_vec(&msg)?) |
|
|
|
|
|
|
|
.recipient(target) |
|
|
|
|
|
|
|
.sign(&self.keys) |
|
|
|
|
|
|
|
.build()?, |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
impl Stream for Degeon { |
|
|
|
|
|
|
|
type Item = GuiEvent; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { |
|
|
|
|
|
|
|
println!("Degeon worker is being polled"); |
|
|
|
|
|
|
|
let msg = self.ironforce.lock().unwrap().read_message(); |
|
|
|
|
|
|
|
match msg.map(|msg| self.process_message(msg).unwrap()) { |
|
|
|
|
|
|
|
None | Some(None) => Poll::Pending, |
|
|
|
|
|
|
|
Some(Some(msg)) => Poll::Ready(Some(msg)), |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
impl<H, I> iced_native::subscription::Recipe<H, I> for Degeon |
|
|
|
|
|
|
|
where |
|
|
|
|
|
|
|
H: std::hash::Hasher, |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
type Output = GuiEvent; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fn hash(&self, state: &mut H) { |
|
|
|
|
|
|
|
use std::hash::Hash; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
std::any::TypeId::of::<Self>().hash(state); |
|
|
|
|
|
|
|
self.ironforce.lock().unwrap().hash(state); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fn stream( |
|
|
|
|
|
|
|
self: Box<Self>, |
|
|
|
|
|
|
|
_input: futures::stream::BoxStream<'static, I>, |
|
|
|
|
|
|
|
) -> futures::stream::BoxStream<'static, Self::Output> { |
|
|
|
|
|
|
|
Box::pin(self) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Default)] |
|
|
|
pub struct State { |
|
|
|
pub struct State { |
|
|
|
chats: Vec<Chat>, |
|
|
|
pub data: Degeon, |
|
|
|
my_name: String, |
|
|
|
|
|
|
|
selected_chat: usize, |
|
|
|
selected_chat: usize, |
|
|
|
pub send_button_state: iced::button::State, |
|
|
|
send_button_state: iced::button::State, |
|
|
|
text_input_state: iced::text_input::State, |
|
|
|
text_input_state: iced::text_input::State, |
|
|
|
preview_button_states: Vec<button::State>, |
|
|
|
preview_button_states: Vec<button::State>, |
|
|
|
} |
|
|
|
} |
|
|
@ -179,8 +331,11 @@ impl State { |
|
|
|
fn chat_list<'a>( |
|
|
|
fn chat_list<'a>( |
|
|
|
chats: &'a Vec<Chat>, |
|
|
|
chats: &'a Vec<Chat>, |
|
|
|
preview_button_states: &'a mut Vec<button::State>, |
|
|
|
preview_button_states: &'a mut Vec<button::State>, |
|
|
|
selected: usize |
|
|
|
selected: usize, |
|
|
|
) -> Element<'a, GuiEvent> { |
|
|
|
) -> Element<'a, GuiEvent> { |
|
|
|
|
|
|
|
while preview_button_states.len() < chats.len() { |
|
|
|
|
|
|
|
preview_button_states.push(Default::default()) |
|
|
|
|
|
|
|
} |
|
|
|
Column::with_children( |
|
|
|
Column::with_children( |
|
|
|
chats |
|
|
|
chats |
|
|
|
.iter() |
|
|
|
.iter() |
|
|
@ -214,41 +369,87 @@ impl State { |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
impl Sandbox for State { |
|
|
|
impl Application for State { |
|
|
|
|
|
|
|
type Executor = iced::executor::Default; |
|
|
|
type Message = GuiEvent; |
|
|
|
type Message = GuiEvent; |
|
|
|
|
|
|
|
type Flags = (); |
|
|
|
|
|
|
|
|
|
|
|
fn new() -> Self { |
|
|
|
fn new(_: ()) -> (Self, iced::Command<GuiEvent>) { |
|
|
|
let mut st = Self::default(); |
|
|
|
let mut st = Self::default(); |
|
|
|
st.chats = vec![Chat::example(1), Chat::example(2)]; |
|
|
|
st.data.chats = vec![Chat::example(1), Chat::example(2)]; |
|
|
|
st.preview_button_states = vec![Default::default(), Default::default()]; |
|
|
|
st.preview_button_states = vec![Default::default(), Default::default()]; |
|
|
|
st |
|
|
|
st.data.my_name = "John".to_string(); |
|
|
|
|
|
|
|
st.data |
|
|
|
|
|
|
|
.send_multicast(DegMessage::Service(ServiceMsg::Ping)) |
|
|
|
|
|
|
|
.unwrap(); |
|
|
|
|
|
|
|
let data_clone = st.data.clone(); |
|
|
|
|
|
|
|
std::thread::spawn(move || { |
|
|
|
|
|
|
|
std::thread::sleep(std::time::Duration::from_secs(10)); |
|
|
|
|
|
|
|
loop { |
|
|
|
|
|
|
|
data_clone |
|
|
|
|
|
|
|
.send_multicast(DegMessage::Service(ServiceMsg::Ping)) |
|
|
|
|
|
|
|
.unwrap(); |
|
|
|
|
|
|
|
std::thread::sleep(std::time::Duration::from_secs(120)); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
(st, iced::Command::none()) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
fn title(&self) -> String { |
|
|
|
fn title(&self) -> String { |
|
|
|
String::from("Degeon") |
|
|
|
String::from("Degeon") |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
fn update(&mut self, message: GuiEvent) { |
|
|
|
fn update(&mut self, message: GuiEvent, _: &mut iced::Clipboard) -> iced::Command<GuiEvent> { |
|
|
|
match message { |
|
|
|
match message { |
|
|
|
GuiEvent::ChatSelect(i) => self.selected_chat = i, |
|
|
|
GuiEvent::ChatSelect(i) => self.selected_chat = i, |
|
|
|
GuiEvent::Typed(st) => self.chats[self.selected_chat].input = st, |
|
|
|
GuiEvent::Typed(st) => self.data.chats[self.selected_chat].input = st, |
|
|
|
GuiEvent::SendClick => { |
|
|
|
GuiEvent::SendClick => { |
|
|
|
if self.chats[self.selected_chat].input.is_empty() { |
|
|
|
if self.data.chats[self.selected_chat].input.is_empty() { |
|
|
|
return; |
|
|
|
return iced::Command::none(); |
|
|
|
} |
|
|
|
} |
|
|
|
let new_msg = Message::Text(self.chats[self.selected_chat].input.clone()); |
|
|
|
let new_msg = DegMessage::Text(self.data.chats[self.selected_chat].input.clone()); |
|
|
|
self.chats[self.selected_chat].input = String::new(); |
|
|
|
self.data.chats[self.selected_chat].input = String::new(); |
|
|
|
self.chats[self.selected_chat] |
|
|
|
self.data.chats[self.selected_chat] |
|
|
|
.messages |
|
|
|
.messages |
|
|
|
.push((true, new_msg)); |
|
|
|
.push((true, new_msg.clone())); |
|
|
|
// todo
|
|
|
|
let data_cloned = self.data.clone(); |
|
|
|
|
|
|
|
let target = self.data.chats[self.selected_chat].pkey.clone(); |
|
|
|
|
|
|
|
std::thread::spawn(move || { |
|
|
|
|
|
|
|
data_cloned |
|
|
|
|
|
|
|
.send_message(new_msg, &target) |
|
|
|
|
|
|
|
.unwrap() |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
GuiEvent::NewChat(pkey) => { |
|
|
|
|
|
|
|
if self.data.chat_with(&pkey).is_none() { |
|
|
|
|
|
|
|
self.data.chats.push(Chat::new(pkey)) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
GuiEvent::NewMessageInChat(pkey, msg) => { |
|
|
|
|
|
|
|
if self.data.chat_with(&pkey).is_none() { |
|
|
|
|
|
|
|
self.data.chats.push(Chat::new(pkey.clone())) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
let ind = self.data.chat_with(&pkey).unwrap(); |
|
|
|
|
|
|
|
self.data.chats[ind].messages.push((false, msg)) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
GuiEvent::SetName(pkey, name) => { |
|
|
|
|
|
|
|
if self.data.chat_with(&pkey).is_none() { |
|
|
|
|
|
|
|
self.data.chats.push(Chat::new(pkey.clone())) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
let ind = self.data.chat_with(&pkey).unwrap(); |
|
|
|
|
|
|
|
self.data.chats[ind].name = name; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
iced::Command::none() |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fn subscription(&self) -> iced::Subscription<GuiEvent> { |
|
|
|
|
|
|
|
iced::Subscription::from_recipe(self.data.clone()) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
fn view(&mut self) -> Element<GuiEvent> { |
|
|
|
fn view(&mut self) -> Element<GuiEvent> { |
|
|
|
let Self { |
|
|
|
let Self { |
|
|
|
chats, |
|
|
|
data: Degeon { chats, .. }, |
|
|
|
selected_chat, |
|
|
|
selected_chat, |
|
|
|
send_button_state, |
|
|
|
send_button_state, |
|
|
|
text_input_state, |
|
|
|
text_input_state, |
|
|
@ -257,7 +458,11 @@ impl Sandbox for State { |
|
|
|
} = self; |
|
|
|
} = self; |
|
|
|
Row::new() |
|
|
|
Row::new() |
|
|
|
.padding(20) |
|
|
|
.padding(20) |
|
|
|
.push(Self::chat_list(chats, preview_button_states, *selected_chat)) |
|
|
|
.push(Self::chat_list( |
|
|
|
|
|
|
|
chats, |
|
|
|
|
|
|
|
preview_button_states, |
|
|
|
|
|
|
|
*selected_chat, |
|
|
|
|
|
|
|
)) |
|
|
|
.push(Self::active_chat( |
|
|
|
.push(Self::active_chat( |
|
|
|
chats, |
|
|
|
chats, |
|
|
|
*selected_chat, |
|
|
|
*selected_chat, |
|
|
|