minor refactors, add OutgoingMessage
This commit is contained in:
14
ntfy-daemon/src/actor_utils.rs
Normal file
14
ntfy-daemon/src/actor_utils.rs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
macro_rules! send_command {
|
||||||
|
($self:expr, $command:expr) => {{
|
||||||
|
let (resp_tx, rx) = oneshot::channel();
|
||||||
|
$self
|
||||||
|
.command_tx
|
||||||
|
.send($command(resp_tx))
|
||||||
|
.await
|
||||||
|
.map_err(|_| anyhow::anyhow!("Actor mailbox error"))?;
|
||||||
|
rx.await
|
||||||
|
.map_err(|_| anyhow::anyhow!("Actor response error"))?
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) use send_command;
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
mod actor_utils;
|
||||||
pub mod credentials;
|
pub mod credentials;
|
||||||
mod http_client;
|
mod http_client;
|
||||||
mod listener;
|
mod listener;
|
||||||
@ -31,6 +32,8 @@ pub enum Error {
|
|||||||
InvalidTopic(String),
|
InvalidTopic(String),
|
||||||
#[error("invalid server base url {0:?}")]
|
#[error("invalid server base url {0:?}")]
|
||||||
InvalidServer(#[from] url::ParseError),
|
InvalidServer(#[from] url::ParseError),
|
||||||
|
#[error("multiple errors in subscription model: {0:?}")]
|
||||||
|
InvalidSubscription(Vec<Error>),
|
||||||
#[error("duplicate message")]
|
#[error("duplicate message")]
|
||||||
DuplicateMessage,
|
DuplicateMessage,
|
||||||
#[error("can't parse the minimum set of required fields from the message {0}")]
|
#[error("can't parse the minimum set of required fields from the message {0}")]
|
||||||
|
|||||||
@ -36,7 +36,7 @@ pub enum ServerEvent {
|
|||||||
topic: String,
|
topic: String,
|
||||||
},
|
},
|
||||||
#[serde(rename = "message")]
|
#[serde(rename = "message")]
|
||||||
Message(models::Message),
|
Message(models::ReceivedMessage),
|
||||||
#[serde(rename = "keepalive")]
|
#[serde(rename = "keepalive")]
|
||||||
KeepAlive {
|
KeepAlive {
|
||||||
id: String,
|
id: String,
|
||||||
@ -48,7 +48,7 @@ pub enum ServerEvent {
|
|||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum ListenerEvent {
|
pub enum ListenerEvent {
|
||||||
Message(models::Message),
|
Message(models::ReceivedMessage),
|
||||||
ConnectionStateChanged(ConnectionState),
|
ConnectionStateChanged(ConnectionState),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -281,7 +281,7 @@ impl ListenerHandle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// the response will be sent as an event in self.events
|
// the response will be sent as an event in self.events
|
||||||
pub async fn request_state(&self) -> ConnectionState {
|
pub async fn state(&self) -> ConnectionState {
|
||||||
let (tx, rx) = oneshot::channel();
|
let (tx, rx) = oneshot::channel();
|
||||||
self.commands
|
self.commands
|
||||||
.send(ListenerCommand::GetState(tx))
|
.send(ListenerCommand::GetState(tx))
|
||||||
@ -300,20 +300,6 @@ mod tests {
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
// takes a list of pattern matches. It recvs events and then matches them
|
|
||||||
// against the macro parameters
|
|
||||||
macro_rules! assert_event_matches {
|
|
||||||
($listener:expr, $( $pattern:pat_param ),+ $(,)?) => {
|
|
||||||
$(
|
|
||||||
$listener.events.changed().await.unwrap();
|
|
||||||
let event = $listener.events.borrow().clone();
|
|
||||||
|
|
||||||
panic!("{:?}", &event);
|
|
||||||
assert!(matches!(event, $pattern));
|
|
||||||
)+
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_listener_reconnects_on_http_status_500() {
|
async fn test_listener_reconnects_on_http_status_500() {
|
||||||
let local_set = LocalSet::new();
|
let local_set = LocalSet::new();
|
||||||
|
|||||||
@ -27,7 +27,7 @@ pub fn validate_topic(topic: &str) -> Result<&str, Error> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Clone, Debug, Serialize, Deserialize)]
|
#[derive(Default, Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct Message {
|
pub struct ReceivedMessage {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub topic: String,
|
pub topic: String,
|
||||||
pub expires: Option<u64>,
|
pub expires: Option<u64>,
|
||||||
@ -59,7 +59,7 @@ pub struct Message {
|
|||||||
pub actions: Vec<Action>,
|
pub actions: Vec<Action>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Message {
|
impl ReceivedMessage {
|
||||||
fn extend_with_emojis(&self, text: &mut String) {
|
fn extend_with_emojis(&self, text: &mut String) {
|
||||||
// Add emojis
|
// Add emojis
|
||||||
for t in &self.tags {
|
for t in &self.tags {
|
||||||
@ -107,6 +107,37 @@ impl Message {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct OutgoingMessage {
|
||||||
|
pub topic: String,
|
||||||
|
pub message: Option<String>,
|
||||||
|
#[serde(default = "Default::default")]
|
||||||
|
pub time: u64,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub title: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
|
pub tags: Vec<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub priority: Option<i8>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[serde(default)]
|
||||||
|
pub attachment: Option<Attachment>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub icon: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub filename: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub delay: Option<usize>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub email: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub call: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
|
pub actions: Vec<Action>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct MinMessage {
|
pub struct MinMessage {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
@ -167,7 +198,7 @@ impl Subscription {
|
|||||||
.push("auth");
|
.push("auth");
|
||||||
Ok(url)
|
Ok(url)
|
||||||
}
|
}
|
||||||
pub fn validate(self) -> Result<Self, Vec<crate::Error>> {
|
pub fn validate(self) -> Result<Self, crate::Error> {
|
||||||
let mut errs = vec![];
|
let mut errs = vec![];
|
||||||
if let Err(e) = validate_topic(&self.topic) {
|
if let Err(e) = validate_topic(&self.topic) {
|
||||||
errs.push(e);
|
errs.push(e);
|
||||||
@ -176,7 +207,7 @@ impl Subscription {
|
|||||||
errs.push(e);
|
errs.push(e);
|
||||||
};
|
};
|
||||||
if !errs.is_empty() {
|
if !errs.is_empty() {
|
||||||
return Err(errs);
|
return Err(Error::InvalidSubscription(errs));
|
||||||
}
|
}
|
||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
@ -239,7 +270,7 @@ impl SubscriptionBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build(self) -> Result<Subscription, Vec<Error>> {
|
pub fn build(self) -> Result<Subscription, Error> {
|
||||||
let res = Subscription {
|
let res = Subscription {
|
||||||
server: self.server,
|
server: self.server,
|
||||||
topic: self.topic,
|
topic: self.topic,
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
use crate::actor_utils::send_command;
|
||||||
use crate::models::NullNetworkMonitor;
|
use crate::models::NullNetworkMonitor;
|
||||||
use crate::models::NullNotifier;
|
use crate::models::NullNotifier;
|
||||||
use anyhow::{anyhow, Context};
|
use anyhow::{anyhow, Context};
|
||||||
@ -36,40 +37,39 @@ pub fn build_client() -> anyhow::Result<reqwest::Client> {
|
|||||||
|
|
||||||
// Message types for the actor
|
// Message types for the actor
|
||||||
#[derive()]
|
#[derive()]
|
||||||
pub enum NtfyMessage {
|
pub enum NtfyCommand {
|
||||||
Subscribe {
|
Subscribe {
|
||||||
server: String,
|
server: String,
|
||||||
topic: String,
|
topic: String,
|
||||||
respond_to: oneshot::Sender<Result<SubscriptionHandle, Vec<anyhow::Error>>>,
|
resp_tx: oneshot::Sender<Result<SubscriptionHandle, anyhow::Error>>,
|
||||||
},
|
},
|
||||||
Unsubscribe {
|
Unsubscribe {
|
||||||
server: String,
|
server: String,
|
||||||
topic: String,
|
topic: String,
|
||||||
respond_to: oneshot::Sender<anyhow::Result<()>>,
|
resp_tx: oneshot::Sender<anyhow::Result<()>>,
|
||||||
},
|
},
|
||||||
RefreshAll {
|
RefreshAll {
|
||||||
respond_to: oneshot::Sender<anyhow::Result<()>>,
|
resp_tx: oneshot::Sender<anyhow::Result<()>>,
|
||||||
},
|
},
|
||||||
ListSubscriptions {
|
ListSubscriptions {
|
||||||
respond_to: oneshot::Sender<anyhow::Result<Vec<SubscriptionHandle>>>,
|
resp_tx: oneshot::Sender<anyhow::Result<Vec<SubscriptionHandle>>>,
|
||||||
},
|
},
|
||||||
ListAccounts {
|
ListAccounts {
|
||||||
respond_to: oneshot::Sender<anyhow::Result<Vec<Account>>>,
|
resp_tx: oneshot::Sender<anyhow::Result<Vec<Account>>>,
|
||||||
},
|
},
|
||||||
WatchSubscribed {
|
WatchSubscribed {
|
||||||
respond_to: oneshot::Sender<anyhow::Result<()>>,
|
resp_tx: oneshot::Sender<anyhow::Result<()>>,
|
||||||
},
|
},
|
||||||
AddAccount {
|
AddAccount {
|
||||||
server: String,
|
server: String,
|
||||||
username: String,
|
username: String,
|
||||||
password: String,
|
password: String,
|
||||||
respond_to: oneshot::Sender<anyhow::Result<()>>,
|
resp_tx: oneshot::Sender<anyhow::Result<()>>,
|
||||||
},
|
},
|
||||||
RemoveAccount {
|
RemoveAccount {
|
||||||
server: String,
|
server: String,
|
||||||
respond_to: oneshot::Sender<anyhow::Result<()>>,
|
resp_tx: oneshot::Sender<anyhow::Result<()>>,
|
||||||
},
|
},
|
||||||
Shutdown,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
||||||
@ -81,12 +81,12 @@ pub struct WatchKey {
|
|||||||
pub struct NtfyActor {
|
pub struct NtfyActor {
|
||||||
listener_handles: Arc<RwLock<HashMap<WatchKey, SubscriptionHandle>>>,
|
listener_handles: Arc<RwLock<HashMap<WatchKey, SubscriptionHandle>>>,
|
||||||
env: SharedEnv,
|
env: SharedEnv,
|
||||||
command_rx: mpsc::Receiver<NtfyMessage>,
|
command_rx: mpsc::Receiver<NtfyCommand>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct NtfyHandle {
|
pub struct NtfyHandle {
|
||||||
command_tx: mpsc::Sender<NtfyMessage>,
|
command_tx: mpsc::Sender<NtfyCommand>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NtfyActor {
|
impl NtfyActor {
|
||||||
@ -108,19 +108,15 @@ impl NtfyActor {
|
|||||||
&self,
|
&self,
|
||||||
server: String,
|
server: String,
|
||||||
topic: String,
|
topic: String,
|
||||||
) -> Result<SubscriptionHandle, Vec<anyhow::Error>> {
|
) -> Result<SubscriptionHandle, anyhow::Error> {
|
||||||
let subscription = models::Subscription::builder(topic.clone())
|
let subscription = models::Subscription::builder(topic.clone())
|
||||||
.server(server.clone())
|
.server(server.clone())
|
||||||
.build()
|
.build()?;
|
||||||
.map_err(|e| e.into_iter().map(|e| anyhow!(e)).collect::<Vec<_>>())?;
|
|
||||||
|
|
||||||
let mut db = self.env.db.clone();
|
let mut db = self.env.db.clone();
|
||||||
db.insert_subscription(subscription.clone())
|
db.insert_subscription(subscription.clone())?;
|
||||||
.map_err(|e| vec![anyhow!(e)])?;
|
|
||||||
|
|
||||||
self.listen(subscription)
|
self.listen(subscription).await
|
||||||
.await
|
|
||||||
.map_err(|e| vec![anyhow!(e)])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_unsubscribe(&mut self, server: String, topic: String) -> anyhow::Result<()> {
|
async fn handle_unsubscribe(&mut self, server: String, topic: String) -> anyhow::Result<()> {
|
||||||
@ -141,25 +137,25 @@ impl NtfyActor {
|
|||||||
pub async fn run(&mut self) {
|
pub async fn run(&mut self) {
|
||||||
while let Some(msg) = self.command_rx.recv().await {
|
while let Some(msg) = self.command_rx.recv().await {
|
||||||
match msg {
|
match msg {
|
||||||
NtfyMessage::Subscribe {
|
NtfyCommand::Subscribe {
|
||||||
server,
|
server,
|
||||||
topic,
|
topic,
|
||||||
respond_to,
|
resp_tx,
|
||||||
} => {
|
} => {
|
||||||
let result = self.handle_subscribe(server, topic).await;
|
let result = self.handle_subscribe(server, topic).await;
|
||||||
let _ = respond_to.send(result);
|
let _ = resp_tx.send(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
NtfyMessage::Unsubscribe {
|
NtfyCommand::Unsubscribe {
|
||||||
server,
|
server,
|
||||||
topic,
|
topic,
|
||||||
respond_to,
|
resp_tx,
|
||||||
} => {
|
} => {
|
||||||
let result = self.handle_unsubscribe(server, topic).await;
|
let result = self.handle_unsubscribe(server, topic).await;
|
||||||
let _ = respond_to.send(result);
|
let _ = resp_tx.send(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
NtfyMessage::RefreshAll { respond_to } => {
|
NtfyCommand::RefreshAll { resp_tx } => {
|
||||||
let mut res = Ok(());
|
let mut res = Ok(());
|
||||||
for sub in self.listener_handles.read().await.values() {
|
for sub in self.listener_handles.read().await.values() {
|
||||||
res = sub.restart().await;
|
res = sub.restart().await;
|
||||||
@ -167,10 +163,10 @@ impl NtfyActor {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let _ = respond_to.send(res);
|
let _ = resp_tx.send(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
NtfyMessage::ListSubscriptions { respond_to } => {
|
NtfyCommand::ListSubscriptions { resp_tx } => {
|
||||||
let subs = self
|
let subs = self
|
||||||
.listener_handles
|
.listener_handles
|
||||||
.read()
|
.read()
|
||||||
@ -178,10 +174,10 @@ impl NtfyActor {
|
|||||||
.values()
|
.values()
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect();
|
.collect();
|
||||||
let _ = respond_to.send(Ok(subs));
|
let _ = resp_tx.send(Ok(subs));
|
||||||
}
|
}
|
||||||
|
|
||||||
NtfyMessage::ListAccounts { respond_to } => {
|
NtfyCommand::ListAccounts { resp_tx } => {
|
||||||
let accounts = self
|
let accounts = self
|
||||||
.env
|
.env
|
||||||
.credentials
|
.credentials
|
||||||
@ -192,34 +188,32 @@ impl NtfyActor {
|
|||||||
username: credential.username,
|
username: credential.username,
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let _ = respond_to.send(Ok(accounts));
|
let _ = resp_tx.send(Ok(accounts));
|
||||||
}
|
}
|
||||||
|
|
||||||
NtfyMessage::WatchSubscribed { respond_to } => {
|
NtfyCommand::WatchSubscribed { resp_tx } => {
|
||||||
let result = self.handle_watch_subscribed().await;
|
let result = self.handle_watch_subscribed().await;
|
||||||
let _ = respond_to.send(result);
|
let _ = resp_tx.send(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
NtfyMessage::AddAccount {
|
NtfyCommand::AddAccount {
|
||||||
server,
|
server,
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
respond_to,
|
resp_tx,
|
||||||
} => {
|
} => {
|
||||||
let result = self
|
let result = self
|
||||||
.env
|
.env
|
||||||
.credentials
|
.credentials
|
||||||
.insert(&server, &username, &password)
|
.insert(&server, &username, &password)
|
||||||
.await;
|
.await;
|
||||||
let _ = respond_to.send(result);
|
let _ = resp_tx.send(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
NtfyMessage::RemoveAccount { server, respond_to } => {
|
NtfyCommand::RemoveAccount { server, resp_tx } => {
|
||||||
let result = self.env.credentials.delete(&server).await;
|
let result = self.env.credentials.delete(&server).await;
|
||||||
let _ = respond_to.send(result);
|
let _ = resp_tx.send(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
NtfyMessage::Shutdown => break,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -274,73 +268,36 @@ impl NtfyHandle {
|
|||||||
&self,
|
&self,
|
||||||
server: &str,
|
server: &str,
|
||||||
topic: &str,
|
topic: &str,
|
||||||
) -> Result<SubscriptionHandle, Vec<anyhow::Error>> {
|
) -> Result<SubscriptionHandle, anyhow::Error> {
|
||||||
let (tx, rx) = oneshot::channel();
|
send_command!(self, |resp_tx| NtfyCommand::Subscribe {
|
||||||
self.command_tx
|
server: server.to_string(),
|
||||||
.send(NtfyMessage::Subscribe {
|
topic: topic.to_string(),
|
||||||
server: server.to_string(),
|
resp_tx,
|
||||||
topic: topic.to_string(),
|
})
|
||||||
respond_to: tx,
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.map_err(|_| vec![anyhow!("Actor mailbox error")])?;
|
|
||||||
|
|
||||||
rx.await
|
|
||||||
.map_err(|_| vec![anyhow!("Actor response error")])?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn unsubscribe(&self, server: &str, topic: &str) -> anyhow::Result<()> {
|
pub async fn unsubscribe(&self, server: &str, topic: &str) -> anyhow::Result<()> {
|
||||||
let (tx, rx) = oneshot::channel();
|
send_command!(self, |resp_tx| NtfyCommand::Unsubscribe {
|
||||||
self.command_tx
|
server: server.to_string(),
|
||||||
.send(NtfyMessage::Unsubscribe {
|
topic: topic.to_string(),
|
||||||
server: server.to_string(),
|
resp_tx,
|
||||||
topic: topic.to_string(),
|
})
|
||||||
respond_to: tx,
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.map_err(|_| anyhow!("Actor mailbox error"))?;
|
|
||||||
|
|
||||||
rx.await.map_err(|_| anyhow!("Actor response error"))?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn refresh_all(&self) -> anyhow::Result<()> {
|
pub async fn refresh_all(&self) -> anyhow::Result<()> {
|
||||||
let (tx, rx) = oneshot::channel();
|
send_command!(self, |resp_tx| NtfyCommand::RefreshAll { resp_tx })
|
||||||
self.command_tx
|
|
||||||
.send(NtfyMessage::RefreshAll { respond_to: tx })
|
|
||||||
.await
|
|
||||||
.map_err(|_| anyhow!("Actor mailbox error"))?;
|
|
||||||
|
|
||||||
rx.await.map_err(|_| anyhow!("Actor response error"))?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_subscriptions(&self) -> anyhow::Result<Vec<SubscriptionHandle>> {
|
pub async fn list_subscriptions(&self) -> anyhow::Result<Vec<SubscriptionHandle>> {
|
||||||
let (tx, rx) = oneshot::channel();
|
send_command!(self, |resp_tx| NtfyCommand::ListSubscriptions { resp_tx })
|
||||||
self.command_tx
|
|
||||||
.send(NtfyMessage::ListSubscriptions { respond_to: tx })
|
|
||||||
.await
|
|
||||||
.map_err(|_| anyhow!("Actor mailbox error"))?;
|
|
||||||
|
|
||||||
rx.await.map_err(|_| anyhow!("Actor response error"))?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_accounts(&self) -> anyhow::Result<Vec<Account>> {
|
pub async fn list_accounts(&self) -> anyhow::Result<Vec<Account>> {
|
||||||
let (tx, rx) = oneshot::channel();
|
send_command!(self, |resp_tx| NtfyCommand::ListAccounts { resp_tx })
|
||||||
self.command_tx
|
|
||||||
.send(NtfyMessage::ListAccounts { respond_to: tx })
|
|
||||||
.await
|
|
||||||
.map_err(|_| anyhow!("Actor mailbox error"))?;
|
|
||||||
|
|
||||||
rx.await.map_err(|_| anyhow!("Actor response error"))?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn watch_subscribed(&self) -> anyhow::Result<()> {
|
pub async fn watch_subscribed(&self) -> anyhow::Result<()> {
|
||||||
let (tx, rx) = oneshot::channel();
|
send_command!(self, |resp_tx| NtfyCommand::WatchSubscribed { resp_tx })
|
||||||
self.command_tx
|
|
||||||
.send(NtfyMessage::WatchSubscribed { respond_to: tx })
|
|
||||||
.await
|
|
||||||
.map_err(|_| anyhow!("Actor mailbox error"))?;
|
|
||||||
|
|
||||||
rx.await.map_err(|_| anyhow!("Actor response error"))?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn add_account(
|
pub async fn add_account(
|
||||||
@ -349,31 +306,19 @@ impl NtfyHandle {
|
|||||||
username: &str,
|
username: &str,
|
||||||
password: &str,
|
password: &str,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let (tx, rx) = oneshot::channel();
|
send_command!(self, |resp_tx| NtfyCommand::AddAccount {
|
||||||
self.command_tx
|
server: server.to_string(),
|
||||||
.send(NtfyMessage::AddAccount {
|
username: username.to_string(),
|
||||||
server: server.to_string(),
|
password: password.to_string(),
|
||||||
username: username.to_string(),
|
resp_tx,
|
||||||
password: password.to_string(),
|
})
|
||||||
respond_to: tx,
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.map_err(|_| anyhow!("Actor mailbox error"))?;
|
|
||||||
|
|
||||||
rx.await.map_err(|_| anyhow!("Actor response error"))?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn remove_account(&self, server: &str) -> anyhow::Result<()> {
|
pub async fn remove_account(&self, server: &str) -> anyhow::Result<()> {
|
||||||
let (tx, rx) = oneshot::channel();
|
send_command!(self, |resp_tx| NtfyCommand::RemoveAccount {
|
||||||
self.command_tx
|
server: server.to_string(),
|
||||||
.send(NtfyMessage::RemoveAccount {
|
resp_tx,
|
||||||
server: server.to_string(),
|
})
|
||||||
respond_to: tx,
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.map_err(|_| anyhow!("Actor mailbox error"))?;
|
|
||||||
|
|
||||||
rx.await.map_err(|_| anyhow!("Actor response error"))?
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -438,7 +383,7 @@ pub fn start(
|
|||||||
mod tests {
|
mod tests {
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use models::Message;
|
use models::{OutgoingMessage, ReceivedMessage};
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
|
|
||||||
use crate::ListenerEvent;
|
use crate::ListenerEvent;
|
||||||
@ -466,7 +411,7 @@ mod tests {
|
|||||||
let subscription_handle = handle.subscribe(server, topic).await.unwrap();
|
let subscription_handle = handle.subscribe(server, topic).await.unwrap();
|
||||||
|
|
||||||
// Publish a message
|
// Publish a message
|
||||||
let message = serde_json::to_string(&Message {
|
let message = serde_json::to_string(&OutgoingMessage {
|
||||||
topic: topic.to_string(),
|
topic: topic.to_string(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
use crate::listener::{ListenerEvent, ListenerHandle};
|
use crate::listener::{ListenerEvent, ListenerHandle};
|
||||||
use crate::message_repo::Db;
|
use crate::message_repo::Db;
|
||||||
use crate::models::{self, Message, NotificationProxy};
|
use crate::models::{self, NotificationProxy, ReceivedMessage};
|
||||||
use crate::{Error, ServerEvent, SharedEnv};
|
use crate::{Error, ServerEvent, SharedEnv};
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@ -9,31 +9,58 @@ use tokio::sync::{broadcast, mpsc, oneshot, watch, RwLock};
|
|||||||
use tokio::task::spawn_local;
|
use tokio::task::spawn_local;
|
||||||
use tracing::{error, info, warn};
|
use tracing::{error, info, warn};
|
||||||
|
|
||||||
|
enum SubscriptionCommand {
|
||||||
|
GetModel {
|
||||||
|
resp_tx: oneshot::Sender<models::Subscription>,
|
||||||
|
},
|
||||||
|
UpdateInfo {
|
||||||
|
new_model: models::Subscription,
|
||||||
|
resp_tx: oneshot::Sender<anyhow::Result<()>>,
|
||||||
|
},
|
||||||
|
Attach {
|
||||||
|
resp_tx: oneshot::Sender<(Vec<ListenerEvent>, broadcast::Receiver<ListenerEvent>)>,
|
||||||
|
},
|
||||||
|
Publish {
|
||||||
|
msg: String,
|
||||||
|
resp_tx: oneshot::Sender<anyhow::Result<()>>,
|
||||||
|
},
|
||||||
|
ClearNotifications {
|
||||||
|
resp_tx: oneshot::Sender<anyhow::Result<()>>,
|
||||||
|
},
|
||||||
|
UpdateReadUntil {
|
||||||
|
timestamp: u64,
|
||||||
|
resp_tx: oneshot::Sender<anyhow::Result<()>>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct SubscriptionHandle {
|
pub struct SubscriptionHandle {
|
||||||
sender: mpsc::Sender<SubscriptionRequest>,
|
command_tx: mpsc::Sender<SubscriptionCommand>,
|
||||||
listener: ListenerHandle,
|
listener: ListenerHandle,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SubscriptionHandle {
|
impl SubscriptionHandle {
|
||||||
pub fn new(listener: ListenerHandle, model: models::Subscription, env: &SharedEnv) -> Self {
|
pub fn new(listener: ListenerHandle, model: models::Subscription, env: &SharedEnv) -> Self {
|
||||||
let (sender, receiver) = mpsc::channel(32);
|
let (command_tx, command_rx) = mpsc::channel(32);
|
||||||
let broadcast_tx = broadcast::channel(8).0;
|
let broadcast_tx = broadcast::channel(8).0;
|
||||||
let actor = SubscriptionActor {
|
let actor = SubscriptionActor {
|
||||||
listener: listener.clone(),
|
listener: listener.clone(),
|
||||||
model,
|
model,
|
||||||
receiver,
|
command_rx,
|
||||||
env: env.clone(),
|
env: env.clone(),
|
||||||
broadcast_tx: broadcast_tx.clone(),
|
broadcast_tx: broadcast_tx.clone(),
|
||||||
};
|
};
|
||||||
spawn_local(actor.run());
|
spawn_local(actor.run());
|
||||||
Self { sender, listener }
|
Self {
|
||||||
|
command_tx,
|
||||||
|
listener,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn model(&self) -> models::Subscription {
|
pub async fn model(&self) -> models::Subscription {
|
||||||
let (resp_tx, resp_rx) = oneshot::channel();
|
let (resp_tx, resp_rx) = oneshot::channel();
|
||||||
self.sender
|
self.command_tx
|
||||||
.send(SubscriptionRequest::GetModel { resp_tx })
|
.send(SubscriptionCommand::GetModel { resp_tx })
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
resp_rx.await.unwrap()
|
resp_rx.await.unwrap()
|
||||||
@ -41,8 +68,8 @@ impl SubscriptionHandle {
|
|||||||
|
|
||||||
pub async fn update_info(&self, new_model: models::Subscription) -> anyhow::Result<()> {
|
pub async fn update_info(&self, new_model: models::Subscription) -> anyhow::Result<()> {
|
||||||
let (resp_tx, resp_rx) = oneshot::channel();
|
let (resp_tx, resp_rx) = oneshot::channel();
|
||||||
self.sender
|
self.command_tx
|
||||||
.send(SubscriptionRequest::UpdateInfo { new_model, resp_tx })
|
.send(SubscriptionCommand::UpdateInfo { new_model, resp_tx })
|
||||||
.await?;
|
.await?;
|
||||||
resp_rx.await.unwrap()
|
resp_rx.await.unwrap()
|
||||||
}
|
}
|
||||||
@ -68,8 +95,8 @@ impl SubscriptionHandle {
|
|||||||
// The `ListenerHandle` is returned to receive new events.
|
// The `ListenerHandle` is returned to receive new events.
|
||||||
pub async fn attach(&self) -> (Vec<ListenerEvent>, broadcast::Receiver<ListenerEvent>) {
|
pub async fn attach(&self) -> (Vec<ListenerEvent>, broadcast::Receiver<ListenerEvent>) {
|
||||||
let (resp_tx, resp_rx) = oneshot::channel();
|
let (resp_tx, resp_rx) = oneshot::channel();
|
||||||
self.sender
|
self.command_tx
|
||||||
.send(SubscriptionRequest::Attach { resp_tx })
|
.send(SubscriptionCommand::Attach { resp_tx })
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
resp_rx.await.unwrap()
|
resp_rx.await.unwrap()
|
||||||
@ -77,8 +104,8 @@ impl SubscriptionHandle {
|
|||||||
|
|
||||||
pub async fn publish(&self, msg: String) -> anyhow::Result<()> {
|
pub async fn publish(&self, msg: String) -> anyhow::Result<()> {
|
||||||
let (resp_tx, resp_rx) = oneshot::channel();
|
let (resp_tx, resp_rx) = oneshot::channel();
|
||||||
self.sender
|
self.command_tx
|
||||||
.send(SubscriptionRequest::Publish { msg, resp_tx })
|
.send(SubscriptionCommand::Publish { msg, resp_tx })
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
resp_rx.await.unwrap()
|
resp_rx.await.unwrap()
|
||||||
@ -86,8 +113,8 @@ impl SubscriptionHandle {
|
|||||||
|
|
||||||
pub async fn clear_notifications(&self) -> anyhow::Result<()> {
|
pub async fn clear_notifications(&self) -> anyhow::Result<()> {
|
||||||
let (resp_tx, resp_rx) = oneshot::channel();
|
let (resp_tx, resp_rx) = oneshot::channel();
|
||||||
self.sender
|
self.command_tx
|
||||||
.send(SubscriptionRequest::ClearNotifications { resp_tx })
|
.send(SubscriptionCommand::ClearNotifications { resp_tx })
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
resp_rx.await.unwrap()
|
resp_rx.await.unwrap()
|
||||||
@ -95,8 +122,8 @@ impl SubscriptionHandle {
|
|||||||
|
|
||||||
pub async fn update_read_until(&self, timestamp: u64) -> anyhow::Result<()> {
|
pub async fn update_read_until(&self, timestamp: u64) -> anyhow::Result<()> {
|
||||||
let (resp_tx, resp_rx) = oneshot::channel();
|
let (resp_tx, resp_rx) = oneshot::channel();
|
||||||
self.sender
|
self.command_tx
|
||||||
.send(SubscriptionRequest::UpdateReadUntil { timestamp, resp_tx })
|
.send(SubscriptionCommand::UpdateReadUntil { timestamp, resp_tx })
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
resp_rx.await.unwrap()
|
resp_rx.await.unwrap()
|
||||||
@ -106,7 +133,7 @@ impl SubscriptionHandle {
|
|||||||
struct SubscriptionActor {
|
struct SubscriptionActor {
|
||||||
listener: ListenerHandle,
|
listener: ListenerHandle,
|
||||||
model: models::Subscription,
|
model: models::Subscription,
|
||||||
receiver: mpsc::Receiver<SubscriptionRequest>,
|
command_rx: mpsc::Receiver<SubscriptionCommand>,
|
||||||
env: SharedEnv,
|
env: SharedEnv,
|
||||||
broadcast_tx: broadcast::Sender<ListenerEvent>,
|
broadcast_tx: broadcast::Sender<ListenerEvent>,
|
||||||
}
|
}
|
||||||
@ -123,12 +150,12 @@ impl SubscriptionActor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(request) = self.receiver.recv() => {
|
Some(command) = self.command_rx.recv() => {
|
||||||
match request {
|
match command {
|
||||||
SubscriptionRequest::GetModel { resp_tx } => {
|
SubscriptionCommand::GetModel { resp_tx } => {
|
||||||
let _ = resp_tx.send(self.model.clone());
|
let _ = resp_tx.send(self.model.clone());
|
||||||
}
|
}
|
||||||
SubscriptionRequest::UpdateInfo {
|
SubscriptionCommand::UpdateInfo {
|
||||||
mut new_model,
|
mut new_model,
|
||||||
resp_tx,
|
resp_tx,
|
||||||
} => {
|
} => {
|
||||||
@ -140,10 +167,10 @@ impl SubscriptionActor {
|
|||||||
}
|
}
|
||||||
resp_tx.send(res.map_err(|e| e.into()));
|
resp_tx.send(res.map_err(|e| e.into()));
|
||||||
}
|
}
|
||||||
SubscriptionRequest::Publish {msg, resp_tx} => {
|
SubscriptionCommand::Publish {msg, resp_tx} => {
|
||||||
let _ = resp_tx.send(self.publish(msg).await);
|
let _ = resp_tx.send(self.publish(msg).await);
|
||||||
}
|
}
|
||||||
SubscriptionRequest::Attach { resp_tx } => {
|
SubscriptionCommand::Attach { resp_tx } => {
|
||||||
let messages = self
|
let messages = self
|
||||||
.env
|
.env
|
||||||
.db
|
.db
|
||||||
@ -163,13 +190,13 @@ impl SubscriptionActor {
|
|||||||
})
|
})
|
||||||
.map(ListenerEvent::Message)
|
.map(ListenerEvent::Message)
|
||||||
.collect();
|
.collect();
|
||||||
previous_events.push(ListenerEvent::ConnectionStateChanged(self.listener.request_state().await));
|
previous_events.push(ListenerEvent::ConnectionStateChanged(self.listener.state().await));
|
||||||
let _ = resp_tx.send((previous_events, self.broadcast_tx.subscribe()));
|
let _ = resp_tx.send((previous_events, self.broadcast_tx.subscribe()));
|
||||||
}
|
}
|
||||||
SubscriptionRequest::ClearNotifications {resp_tx} => {
|
SubscriptionCommand::ClearNotifications {resp_tx} => {
|
||||||
let _ = resp_tx.send(self.env.db.delete_messages(&self.model.server, &self.model.topic).map_err(|e| anyhow::anyhow!(e)));
|
let _ = resp_tx.send(self.env.db.delete_messages(&self.model.server, &self.model.topic).map_err(|e| anyhow::anyhow!(e)));
|
||||||
}
|
}
|
||||||
SubscriptionRequest::UpdateReadUntil { timestamp, resp_tx } => {
|
SubscriptionCommand::UpdateReadUntil { timestamp, resp_tx } => {
|
||||||
let res = self.env.db.update_read_until(&self.model.server, &self.model.topic, timestamp);
|
let res = self.env.db.update_read_until(&self.model.server, &self.model.topic, timestamp);
|
||||||
let _ = resp_tx.send(res.map_err(|e| anyhow::anyhow!(e)));
|
let _ = resp_tx.send(res.map_err(|e| anyhow::anyhow!(e)));
|
||||||
}
|
}
|
||||||
@ -192,7 +219,7 @@ impl SubscriptionActor {
|
|||||||
res.error_for_status()?;
|
res.error_for_status()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
fn handle_msg_event(&mut self, msg: Message) {
|
fn handle_msg_event(&mut self, msg: ReceivedMessage) {
|
||||||
// Store in database
|
// Store in database
|
||||||
let already_stored: bool = {
|
let already_stored: bool = {
|
||||||
let json_ev = &serde_json::to_string(&msg).unwrap();
|
let json_ev = &serde_json::to_string(&msg).unwrap();
|
||||||
@ -231,27 +258,3 @@ impl SubscriptionActor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum SubscriptionRequest {
|
|
||||||
GetModel {
|
|
||||||
resp_tx: oneshot::Sender<models::Subscription>,
|
|
||||||
},
|
|
||||||
UpdateInfo {
|
|
||||||
new_model: models::Subscription,
|
|
||||||
resp_tx: oneshot::Sender<anyhow::Result<()>>,
|
|
||||||
},
|
|
||||||
Attach {
|
|
||||||
resp_tx: oneshot::Sender<(Vec<ListenerEvent>, broadcast::Receiver<ListenerEvent>)>,
|
|
||||||
},
|
|
||||||
Publish {
|
|
||||||
msg: String,
|
|
||||||
resp_tx: oneshot::Sender<anyhow::Result<()>>,
|
|
||||||
},
|
|
||||||
ClearNotifications {
|
|
||||||
resp_tx: oneshot::Sender<anyhow::Result<()>>,
|
|
||||||
},
|
|
||||||
UpdateReadUntil {
|
|
||||||
timestamp: u64,
|
|
||||||
resp_tx: oneshot::Sender<anyhow::Result<()>>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|||||||
@ -227,12 +227,12 @@ impl Subscription {
|
|||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
fn last_message(list: &gio::ListStore) -> Option<models::Message> {
|
fn last_message(list: &gio::ListStore) -> Option<models::ReceivedMessage> {
|
||||||
let n = list.n_items();
|
let n = list.n_items();
|
||||||
let last = list
|
let last = list
|
||||||
.item(n.checked_sub(1)?)
|
.item(n.checked_sub(1)?)
|
||||||
.and_downcast::<glib::BoxedAnyObject>()?;
|
.and_downcast::<glib::BoxedAnyObject>()?;
|
||||||
let last = last.borrow::<models::Message>();
|
let last = last.borrow::<models::ReceivedMessage>();
|
||||||
Some(last.clone())
|
Some(last.clone())
|
||||||
}
|
}
|
||||||
fn update_unread_count(&self) {
|
fn update_unread_count(&self) {
|
||||||
@ -275,7 +275,7 @@ impl Subscription {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
pub async fn publish_msg(&self, mut msg: models::Message) -> anyhow::Result<()> {
|
pub async fn publish_msg(&self, mut msg: models::OutgoingMessage) -> anyhow::Result<()> {
|
||||||
let imp = self.imp();
|
let imp = self.imp();
|
||||||
let json = {
|
let json = {
|
||||||
msg.topic = self.topic();
|
msg.topic = self.topic();
|
||||||
|
|||||||
@ -166,7 +166,7 @@ impl AddSubscriptionDialog {
|
|||||||
obj.set_content_width(480);
|
obj.set_content_width(480);
|
||||||
obj.set_child(Some(&toolbar_view));
|
obj.set_child(Some(&toolbar_view));
|
||||||
}
|
}
|
||||||
pub fn subscription(&self) -> Result<models::Subscription, Vec<ntfy_daemon::Error>> {
|
pub fn subscription(&self) -> Result<models::Subscription, ntfy_daemon::Error> {
|
||||||
let w = { self.imp().widgets.borrow().clone() };
|
let w = { self.imp().widgets.borrow().clone() };
|
||||||
let mut sub = models::Subscription::builder(w.topic_entry.text().to_string());
|
let mut sub = models::Subscription::builder(w.topic_entry.text().to_string());
|
||||||
if w.server_expander.enables_expansion() {
|
if w.server_expander.enables_expansion() {
|
||||||
@ -183,7 +183,7 @@ impl AddSubscriptionDialog {
|
|||||||
w.topic_entry.remove_css_class("error");
|
w.topic_entry.remove_css_class("error");
|
||||||
w.sub_btn.set_sensitive(true);
|
w.sub_btn.set_sensitive(true);
|
||||||
|
|
||||||
if let Err(errs) = sub {
|
if let Err(ntfy_daemon::Error::InvalidSubscription(errs)) = sub {
|
||||||
w.sub_btn.set_sensitive(false);
|
w.sub_btn.set_sensitive(false);
|
||||||
for e in errs {
|
for e in errs {
|
||||||
match e {
|
match e {
|
||||||
|
|||||||
@ -34,12 +34,12 @@ glib::wrapper! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl MessageRow {
|
impl MessageRow {
|
||||||
pub fn new(msg: models::Message) -> Self {
|
pub fn new(msg: models::ReceivedMessage) -> Self {
|
||||||
let this: Self = glib::Object::new();
|
let this: Self = glib::Object::new();
|
||||||
this.build_ui(msg);
|
this.build_ui(msg);
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
fn build_ui(&self, msg: models::Message) {
|
fn build_ui(&self, msg: models::ReceivedMessage) {
|
||||||
self.set_margin_top(8);
|
self.set_margin_top(8);
|
||||||
self.set_margin_bottom(8);
|
self.set_margin_bottom(8);
|
||||||
self.set_margin_start(8);
|
self.set_margin_start(8);
|
||||||
|
|||||||
@ -226,9 +226,9 @@ impl NotifyWindow {
|
|||||||
entry.error_boundary().spawn(async move {
|
entry.error_boundary().spawn(async move {
|
||||||
this.selected_subscription()
|
this.selected_subscription()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.publish_msg(models::Message {
|
.publish_msg(models::OutgoingMessage {
|
||||||
message: Some(entry.text().as_str().to_string()),
|
message: Some(entry.text().as_str().to_string()),
|
||||||
..models::Message::default()
|
..models::OutgoingMessage::default()
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -266,13 +266,7 @@ impl NotifyWindow {
|
|||||||
fn add_subscription(&self, sub: models::Subscription) {
|
fn add_subscription(&self, sub: models::Subscription) {
|
||||||
let this = self.clone();
|
let this = self.clone();
|
||||||
self.error_boundary().spawn(async move {
|
self.error_boundary().spawn(async move {
|
||||||
let sub = this
|
let sub = this.notifier().subscribe(&sub.server, &sub.topic).await?;
|
||||||
.notifier()
|
|
||||||
.subscribe(&sub.server, &sub.topic)
|
|
||||||
.await
|
|
||||||
.map_err(|err| {
|
|
||||||
anyhow::anyhow!(err.into_iter().map(|x| x.to_string()).collect::<String>())
|
|
||||||
})?;
|
|
||||||
let imp = this.imp();
|
let imp = this.imp();
|
||||||
|
|
||||||
// Subscription::new will use the pipelined client to retrieve info about the subscription
|
// Subscription::new will use the pipelined client to retrieve info about the subscription
|
||||||
@ -371,7 +365,7 @@ impl NotifyWindow {
|
|||||||
imp.message_list
|
imp.message_list
|
||||||
.bind_model(Some(&sub.imp().messages), move |obj| {
|
.bind_model(Some(&sub.imp().messages), move |obj| {
|
||||||
let b = obj.downcast_ref::<glib::BoxedAnyObject>().unwrap();
|
let b = obj.downcast_ref::<glib::BoxedAnyObject>().unwrap();
|
||||||
let msg = b.borrow::<models::Message>();
|
let msg = b.borrow::<models::ReceivedMessage>();
|
||||||
|
|
||||||
MessageRow::new(msg.clone()).upcast()
|
MessageRow::new(msg.clone()).upcast()
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user