Skip to content

Commit

Permalink
Notifications
Browse files Browse the repository at this point in the history
Basic notifications feature.
  • Loading branch information
benjajaja committed Mar 21, 2024
1 parent c63f8d9 commit eb8fc49
Show file tree
Hide file tree
Showing 10 changed files with 837 additions and 29 deletions.
610 changes: 583 additions & 27 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 @@ -45,6 +45,7 @@ libc = "0.2"
markup5ever_rcdom = "0.2.0"
mime = "^0.3.16"
mime_guess = "^2.0.4"
notify-rust = { version = "4.10.0", default-features = false, features = ["zbus", "serde"] }
open = "3.2.0"
rand = "0.8.5"
ratatui = "0.23"
Expand Down
3 changes: 3 additions & 0 deletions docs/iamb.5.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ overridden as described in *PROFILES*.
**message_user_color** (type: boolean)
> Defines whether or not the message body is colored like the username.
**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
7 changes: 7 additions & 0 deletions src/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,9 @@ pub struct RoomInfo {

/// The display names for users in this room.
pub display_names: HashMap<OwnedUserId, String>,

/// The last time the room was rendered, used to detect if it is currently open.
pub draw_last: Option<Instant>,
}

impl RoomInfo {
Expand Down Expand Up @@ -1192,6 +1195,9 @@ pub struct ChatStore {

/// Image preview "protocol" picker.
pub picker: Option<Picker>,

/// Last draw time, used to match with RoomInfo's draw_last.
pub draw_curr: Option<Instant>,
}

impl ChatStore {
Expand All @@ -1212,6 +1218,7 @@ impl ChatStore {
verifications: Default::default(),
need_load: Default::default(),
sync_info: Default::default(),
draw_curr: None,
}
}

Expand Down
10 changes: 10 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,12 @@ pub enum UserDisplayStyle {
DisplayName,
}

#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
pub struct Notifications {
pub enabled: bool,
pub show_message: Option<bool>,
}

#[derive(Clone)]
pub struct ImagePreviewValues {
pub size: ImagePreviewSize,
Expand Down Expand Up @@ -476,6 +482,7 @@ pub struct TunableValues {
pub message_user_color: bool,
pub default_room: Option<String>,
pub open_command: Option<Vec<String>>,
pub notifications: Notifications,
pub image_preview: Option<ImagePreviewValues>,
}

Expand All @@ -496,6 +503,7 @@ pub struct Tunables {
pub message_user_color: Option<bool>,
pub default_room: Option<String>,
pub open_command: Option<Vec<String>>,
pub notifications: Option<Notifications>,
pub image_preview: Option<ImagePreview>,
}

Expand All @@ -518,6 +526,7 @@ impl Tunables {
message_user_color: self.message_user_color.or(other.message_user_color),
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 @@ -538,6 +547,7 @@ impl Tunables {
message_user_color: self.message_user_color.unwrap_or(false),
default_room: self.default_room,
open_command: self.open_command,
notifications: self.notifications.unwrap_or_default(),
image_preview: self.image_preview.map(ImagePreview::values),
}
}
Expand Down
4 changes: 3 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use std::ops::DerefMut;
use std::process;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;
use std::time::{Duration, Instant};

use clap::Parser;
use matrix_sdk::crypto::encrypt_room_key_export;
Expand Down Expand Up @@ -66,6 +66,7 @@ mod commands;
mod config;
mod keybindings;
mod message;
mod notifications;
mod preview;
mod sled_export;
mod util;
Expand Down Expand Up @@ -305,6 +306,7 @@ impl Application {
// Don't show terminal cursor when we show a dialog.
let hide_cursor = !dialogstr.is_empty();

store.application.draw_curr = Some(Instant::now());
let screen = Screen::new(store)
.show_dialog(dialogstr)
.show_mode(modestr)
Expand Down
221 changes: 221 additions & 0 deletions src/notifications.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
use std::time::SystemTime;

use matrix_sdk::{
notification_settings::{IsEncrypted, IsOneToOne, NotificationSettings, RoomNotificationMode},
room::Room as MatrixRoom,
ruma::{
api::client::push::get_notifications::v3::Notification,
events::{room::message::MessageType, AnyMessageLikeEventContent, AnySyncTimelineEvent},
MilliSecondsSinceUnixEpoch,
RoomId,
},
Client,
};
use unicode_segmentation::UnicodeSegmentation;

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

pub async fn register_notifications(
client: &Client,
settings: &ApplicationSettings,
store: &AsyncProgramStore,
) {
if !settings.tunables.notifications.enabled {
return;
}
let show_message = settings.tunables.notifications.show_message;
let server_settings = client.notification_settings().await;
let Some(startup_ts) = MilliSecondsSinceUnixEpoch::from_system_time(SystemTime::now()) else {
return;
};

let store = store.clone();
client
.register_notification_handler(move |notification, room: MatrixRoom, client: Client| {
let store = store.clone();
let server_settings = server_settings.clone();
async move {
let mode = global_or_room_mode(&server_settings, &room).await;
if mode == RoomNotificationMode::Mute {
return;
}

if is_open(&store, room.room_id()).await {
return;
}

match parse_notification(notification, room).await {
Ok((summary, body, server_ts)) => {
if server_ts < startup_ts {
return;
}

let mut desktop_notification = notify_rust::Notification::new();
desktop_notification
.summary(&summary)
.appname("iamb")
.timeout(notify_rust::Timeout::Milliseconds(3000))
.action("default", "default");

if is_missing_mention(&body, mode, &client) {
return;
}
if show_message != Some(false) {
if let Some(body) = body {
desktop_notification.body(&body);
}
}
if let Err(err) = desktop_notification.show() {
tracing::error!("Failed to send notification: {err}")
}
},
Err(err) => {
tracing::error!("Failed to extract notification data: {err}")
},
}
}
})
.await;
}

async fn global_or_room_mode(
settings: &NotificationSettings,
room: &MatrixRoom,
) -> RoomNotificationMode {
let room_mode = settings.get_user_defined_room_notification_mode(room.room_id()).await;
if let Some(mode) = room_mode {
return mode;
}
let is_one_to_one = match room.is_direct().await {
Ok(true) => IsOneToOne::Yes,
_ => IsOneToOne::No,
};
let is_encrypted = match room.is_encrypted().await {
Ok(true) => IsEncrypted::Yes,
_ => IsEncrypted::No,
};
settings
.get_default_room_notification_mode(is_encrypted, is_one_to_one)
.await
}

fn is_missing_mention(body: &Option<String>, mode: RoomNotificationMode, client: &Client) -> bool {
if let Some(body) = body {
if mode == RoomNotificationMode::MentionsAndKeywordsOnly {
let mentioned = match client.user_id() {
Some(user_id) => body.contains(user_id.localpart()),
_ => false,
};
return !mentioned;
}
}
false
}

async fn is_open(store: &AsyncProgramStore, room_id: &RoomId) -> bool {
let mut locked = store.lock().await;
if let Some(draw_curr) = locked.application.draw_curr {
let info = locked.application.get_room_info(room_id.to_owned());
if let Some(draw_last) = info.draw_last {
return draw_last == draw_curr;
}
}
false
}

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

let server_ts = event.origin_server_ts();

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().await.map_err(IambError::from)?,
)
.map(truncate);
return Ok((sender_name.to_string(), body, server_ts));
}

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,
}
}

fn truncate(s: String) -> String {
static MAX_LENGTH: usize = 100;
if s.graphemes(true).count() > MAX_LENGTH {
let truncated: String = s.graphemes(true).take(MAX_LENGTH).collect();
truncated + "..."
} else {
s
}
}
3 changes: 3 additions & 0 deletions src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ use crate::{
user_style_from_color,
ApplicationSettings,
DirectoryValues,
Notifications,
ProfileConfig,
SortOverrides,
TunableValues,
Expand Down Expand Up @@ -164,6 +165,7 @@ pub fn mock_room() -> RoomInfo {
fetch_last: None,
users_typing: None,
display_names: HashMap::new(),
draw_last: None,
}
}

Expand Down Expand Up @@ -198,6 +200,7 @@ pub fn mock_tunables() -> TunableValues {
open_command: None,
username_display: UserDisplayStyle::Username,
message_user_color: false,
notifications: Notifications { enabled: false, show_message: None },
image_preview: None,
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/windows/room/scrollback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1437,6 +1437,8 @@ impl<'a> StatefulWidget for Scrollback<'a> {
.need_load
.insert(state.room_id.to_owned(), Need::MESSAGES);
}

info.draw_last = self.store.application.draw_curr;
}
}

Expand Down
5 changes: 4 additions & 1 deletion src/worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ use modalkit::errors::UIError;
use modalkit::prelude::{EditInfo, InfoMessage};

use crate::base::Need;
use crate::notifications::register_notifications;
use crate::{
base::{
AsyncProgramStore,
Expand Down Expand Up @@ -1242,12 +1243,14 @@ 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, &store);
let ((), (), (), ()) = tokio::join!(load, rcpt, room, notifications);
}
})
.into();
Expand Down

0 comments on commit eb8fc49

Please sign in to comment.