Skip to content

Commit

Permalink
Notifications
Browse files Browse the repository at this point in the history
Basic notifications feature.

Does NOT check if the room of the notification is currently open to skip
showing. Thus, the feature is very annoying in its current state for most users.

Can be enabled in config.json, disabled by default because of the above
missing check.

There are no checks against sizes of message/body, usernames, or room
names.
  • Loading branch information
benjajaja committed Jan 3, 2024
1 parent 999399a commit b62815a
Show file tree
Hide file tree
Showing 8 changed files with 844 additions and 36 deletions.
737 changes: 702 additions & 35 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ libc = "0.2"
markup5ever_rcdom = "0.2.0"
mime = "^0.3.16"
mime_guess = "^2.0.4"
notify-rust = { version = "4.9.0" }
open = "3.2.0"
ratatui-image = { version = "0.4.3", features = ["serde"] }
regex = "^1.5"
Expand Down
3 changes: 3 additions & 0 deletions docs/iamb.5.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ overridden as described in *PROFILES*.
**default_room** (type: string)
> The room to show by default instead of a welcome-screen.
**notifications** (type: boolean)
> Defines wether notifications are enabled or not.
**image_preview** (type: image_preview object)
> Enable image previews and configure it. An empty object will enable the
> feature with default settings, omitting it will disable the feature.
Expand Down
4 changes: 4 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ pub struct TunableValues {
pub username_display: UserDisplayStyle,
pub default_room: Option<String>,
pub open_command: Option<Vec<String>>,
pub notifications: bool,
pub image_preview: Option<ImagePreviewValues>,
}

Expand All @@ -367,6 +368,7 @@ pub struct Tunables {
pub username_display: Option<UserDisplayStyle>,
pub default_room: Option<String>,
pub open_command: Option<Vec<String>>,
pub notifications: Option<bool>,
pub image_preview: Option<ImagePreview>,
}

Expand All @@ -388,6 +390,7 @@ impl Tunables {
username_display: self.username_display.or(other.username_display),
default_room: self.default_room.or(other.default_room),
open_command: self.open_command.or(other.open_command),
notifications: self.notifications.or(other.notifications),
image_preview: self.image_preview.or(other.image_preview),
}
}
Expand All @@ -407,6 +410,7 @@ impl Tunables {
username_display: self.username_display.unwrap_or_default(),
default_room: self.default_room,
open_command: self.open_command,
notifications: self.notifications.unwrap_or(false),
image_preview: self.image_preview.map(ImagePreview::values),
}
}
Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ mod commands;
mod config;
mod keybindings;
mod message;
mod notifications;
mod preview;
mod util;
mod windows;
Expand Down
127 changes: 127 additions & 0 deletions src/notifications.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
use matrix_sdk::{
room::Room as MatrixRoom,
ruma::{
api::client::push::get_notifications::v3::Notification,
events::{room::message::MessageType, AnyMessageLikeEventContent, AnySyncTimelineEvent},
},
Client,
};

use crate::{
base::{IambError, IambResult},
config::ApplicationSettings,
};

pub async fn register_notifications(client: &Client, settings: &ApplicationSettings) {
if !settings.tunables.notifications {
return;
}
client
.register_notification_handler(|notification, room: MatrixRoom, _: Client| {
async move {
match parse_notification(notification, room).await {
Ok((summary, body)) => {
let Some(body) = body else {
// Never show without a body.
return;
};

// TODO: never show if room is currently open.

if let Err(err) = notify_rust::Notification::new()
.summary(&summary)
.body(&body)
.appname("iamb")
.timeout(notify_rust::Timeout::Milliseconds(3000))
.action("default", "default")
.show()
{
tracing::error!("Failed to send notification: {err}")
}
},
Err(err) => {
tracing::error!("Failed to extract notification data: {err}")
},
}
}
})
.await;
return;
}

pub async fn parse_notification(
notification: Notification,
room: MatrixRoom,
) -> IambResult<(String, Option<String>)> {
let event = notification.event.deserialize().map_err(IambError::from)?;

let sender_id = event.sender();
let sender = room.get_member_no_sync(sender_id).await.map_err(IambError::from)?;

let sender_name = sender
.as_ref()
.and_then(|m| m.display_name())
.unwrap_or_else(|| sender_id.localpart());

let body = event_notification_body(&event, sender_name, room.is_direct());
return Ok((sender_name.to_string(), body));
}

pub fn event_notification_body(
event: &AnySyncTimelineEvent,
sender_name: &str,
is_direct: bool,
) -> Option<String> {
let AnySyncTimelineEvent::MessageLike(event) = event else {
return None;
};

match event.original_content()? {
AnyMessageLikeEventContent::RoomMessage(message) => {
let body = match message.msgtype {
MessageType::Audio(_) => {
format!("{sender_name} sent an audio file.")
},
MessageType::Emote(content) => {
let message = &content.body;
format!("{sender_name}: {message}")
},
MessageType::File(_) => {
format!("{sender_name} sent a file.")
},
MessageType::Image(_) => {
format!("{sender_name} sent an image.")
},
MessageType::Location(_) => {
format!("{sender_name} sent their location.")
},
MessageType::Notice(content) => {
let message = &content.body;
format!("{sender_name}: {message}")
},
MessageType::ServerNotice(content) => {
let message = &content.body;
format!("{sender_name}: {message}")
},
MessageType::Text(content) => {
if is_direct {
content.body
} else {
let message = &content.body;
format!("{sender_name}: {message}")
}
},
MessageType::Video(_) => {
format!("{sender_name} sent a video.")
},
MessageType::VerificationRequest(_) => {
format!("{sender_name} sent a verification request.")
},
_ => unimplemented!(),
};
Some(body)
},
AnyMessageLikeEventContent::Sticker(_) => Some(format!("{sender_name} sent a sticker.")),
_ => None,
}
}
1 change: 1 addition & 0 deletions src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ pub fn mock_tunables() -> TunableValues {
.collect::<HashMap<_, _>>(),
open_command: None,
username_display: UserDisplayStyle::Username,
notifications: false,
image_preview: None,
}
}
Expand Down
6 changes: 5 additions & 1 deletion src/worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ use matrix_sdk::{
use modalkit::editing::action::{EditInfo, InfoMessage, UIError};

use crate::base::Need;
use crate::notifications::register_notifications;
use crate::{
base::{
AsyncProgramStore,
Expand Down Expand Up @@ -1103,12 +1104,15 @@ impl ClientWorker {

self.load_handle = tokio::spawn({
let client = self.client.clone();
let settings = self.settings.clone();

async move {
let load = load_older_forever(&client, &store);
let rcpt = send_receipts_forever(&client, &store);
let room = refresh_rooms_forever(&client, &store);
let ((), (), ()) = tokio::join!(load, rcpt, room);
let notifications = register_notifications(&client, &settings);

let ((), (), (), _) = tokio::join!(load, rcpt, room, notifications);
}
})
.into();
Expand Down

0 comments on commit b62815a

Please sign in to comment.