mirror of
https://github.com/youwen5/iamb.git
synced 2025-08-04 03:28:28 -07:00
Compare commits
7 commits
1f88ef6e02
...
1475d9ce50
Author | SHA1 | Date | |
---|---|---|---|
1475d9ce50 | |||
5029c6ee3b | |||
![]() |
e9cdb3371a | ||
![]() |
0ff8828a1c | ||
![]() |
331a6bca89 | ||
![]() |
963ce3c7c2 | ||
![]() |
ec88f4441e |
12 changed files with 171 additions and 27 deletions
10
Cargo.lock
generated
10
Cargo.lock
generated
|
@ -2274,6 +2274,7 @@ dependencies = [
|
|||
"image",
|
||||
"lazy_static 1.5.0",
|
||||
"libc",
|
||||
"linkify",
|
||||
"markup5ever_rcdom",
|
||||
"matrix-sdk",
|
||||
"mime",
|
||||
|
@ -2831,6 +2832,15 @@ dependencies = [
|
|||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linkify"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1dfa36d52c581e9ec783a7ce2a5e0143da6237be5811a0b3153fedfdbe9f780"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.3.8"
|
||||
|
|
|
@ -64,6 +64,7 @@ unicode-width = "0.1.10"
|
|||
url = {version = "^2.2.2", features = ["serde"]}
|
||||
edit = "0.1.4"
|
||||
humansize = "2.0.0"
|
||||
linkify = "0.10.0"
|
||||
|
||||
[dependencies.comrak]
|
||||
version = "0.22.0"
|
||||
|
|
|
@ -173,6 +173,9 @@ respective shortcodes.
|
|||
.It Sy message_user_color
|
||||
Defines whether or not the message body is colored like the username.
|
||||
|
||||
.It Sy normal_after_send
|
||||
Defines whether to reset input to Normal mode after sending a message.
|
||||
|
||||
.It Sy notifications
|
||||
When this subsection is present, you can enable and configure push notifications.
|
||||
See
|
||||
|
|
|
@ -92,6 +92,7 @@ use modalkit::{
|
|||
|
||||
use crate::config::ImagePreviewProtocolValues;
|
||||
use crate::message::ImageStatus;
|
||||
use crate::notifications::NotificationHandle;
|
||||
use crate::preview::{source_from_event, spawn_insert_preview};
|
||||
use crate::{
|
||||
message::{Message, MessageEvent, MessageKey, MessageTimeStamp, Messages},
|
||||
|
@ -1558,6 +1559,9 @@ pub struct ChatStore {
|
|||
|
||||
/// Collator for locale-aware text sorting.
|
||||
pub collator: feruca::Collator,
|
||||
|
||||
/// Notifications that should be dismissed when the user opens the room.
|
||||
pub open_notifications: HashMap<OwnedRoomId, Vec<NotificationHandle>>,
|
||||
}
|
||||
|
||||
impl ChatStore {
|
||||
|
@ -1582,6 +1586,7 @@ impl ChatStore {
|
|||
draw_curr: None,
|
||||
ring_bell: false,
|
||||
focused: true,
|
||||
open_notifications: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
use std::borrow::Cow;
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::env;
|
||||
use std::fmt;
|
||||
use std::fs::File;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
@ -399,6 +400,9 @@ pub enum UserDisplayStyle {
|
|||
// it can wind up being the Matrix username if there are display name collisions in the room,
|
||||
// in order to avoid any confusion.
|
||||
DisplayName,
|
||||
|
||||
// Acts like Username, except when the username matches given regex, then acts like DisplayName
|
||||
Regex,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
|
@ -557,6 +561,7 @@ impl SortOverrides {
|
|||
pub struct TunableValues {
|
||||
pub log_level: Level,
|
||||
pub message_shortcode_display: bool,
|
||||
pub normal_after_send: bool,
|
||||
pub reaction_display: bool,
|
||||
pub reaction_shortcode_display: bool,
|
||||
pub read_receipt_send: bool,
|
||||
|
@ -568,6 +573,7 @@ pub struct TunableValues {
|
|||
pub typing_notice_display: bool,
|
||||
pub users: UserOverrides,
|
||||
pub username_display: UserDisplayStyle,
|
||||
pub username_display_regex: Option<String>,
|
||||
pub message_user_color: bool,
|
||||
pub default_room: Option<String>,
|
||||
pub open_command: Option<Vec<String>>,
|
||||
|
@ -583,6 +589,7 @@ pub struct TunableValues {
|
|||
pub struct Tunables {
|
||||
pub log_level: Option<LogLevel>,
|
||||
pub message_shortcode_display: Option<bool>,
|
||||
pub normal_after_send: Option<bool>,
|
||||
pub reaction_display: Option<bool>,
|
||||
pub reaction_shortcode_display: Option<bool>,
|
||||
pub read_receipt_send: Option<bool>,
|
||||
|
@ -595,6 +602,7 @@ pub struct Tunables {
|
|||
pub typing_notice_display: Option<bool>,
|
||||
pub users: Option<UserOverrides>,
|
||||
pub username_display: Option<UserDisplayStyle>,
|
||||
pub username_display_regex: Option<String>,
|
||||
pub message_user_color: Option<bool>,
|
||||
pub default_room: Option<String>,
|
||||
pub open_command: Option<Vec<String>>,
|
||||
|
@ -613,6 +621,7 @@ impl Tunables {
|
|||
message_shortcode_display: self
|
||||
.message_shortcode_display
|
||||
.or(other.message_shortcode_display),
|
||||
normal_after_send: self.normal_after_send.or(other.normal_after_send),
|
||||
reaction_display: self.reaction_display.or(other.reaction_display),
|
||||
reaction_shortcode_display: self
|
||||
.reaction_shortcode_display
|
||||
|
@ -626,6 +635,7 @@ impl Tunables {
|
|||
typing_notice_display: self.typing_notice_display.or(other.typing_notice_display),
|
||||
users: merge_maps(self.users, other.users),
|
||||
username_display: self.username_display.or(other.username_display),
|
||||
username_display_regex: self.username_display_regex.or(other.username_display_regex),
|
||||
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),
|
||||
|
@ -644,6 +654,7 @@ impl Tunables {
|
|||
TunableValues {
|
||||
log_level: self.log_level.map(Level::from).unwrap_or(Level::INFO),
|
||||
message_shortcode_display: self.message_shortcode_display.unwrap_or(false),
|
||||
normal_after_send: self.normal_after_send.unwrap_or(false),
|
||||
reaction_display: self.reaction_display.unwrap_or(true),
|
||||
reaction_shortcode_display: self.reaction_shortcode_display.unwrap_or(false),
|
||||
read_receipt_send: self.read_receipt_send.unwrap_or(true),
|
||||
|
@ -655,6 +666,7 @@ impl Tunables {
|
|||
typing_notice_display: self.typing_notice_display.unwrap_or(true),
|
||||
users: self.users.unwrap_or_default(),
|
||||
username_display: self.username_display.unwrap_or_default(),
|
||||
username_display_regex: self.username_display_regex,
|
||||
message_user_color: self.message_user_color.unwrap_or(false),
|
||||
default_room: self.default_room,
|
||||
open_command: self.open_command,
|
||||
|
@ -837,8 +849,16 @@ pub struct ApplicationSettings {
|
|||
}
|
||||
|
||||
impl ApplicationSettings {
|
||||
fn get_xdg_config_home() -> Option<PathBuf> {
|
||||
env::var("XDG_CONFIG_HOME").ok().map(PathBuf::from)
|
||||
}
|
||||
|
||||
pub fn load(cli: Iamb) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let mut config_dir = cli.config_directory.or_else(dirs::config_dir).unwrap_or_else(|| {
|
||||
let mut config_dir = cli
|
||||
.config_directory
|
||||
.or_else(Self::get_xdg_config_home)
|
||||
.or_else(dirs::config_dir)
|
||||
.unwrap_or_else(|| {
|
||||
usage!(
|
||||
"No user configuration directory found;\
|
||||
please specify one via -C.\n\n
|
||||
|
@ -1048,6 +1068,20 @@ impl ApplicationSettings {
|
|||
Cow::Borrowed(user_id.as_str())
|
||||
}
|
||||
},
|
||||
(None, UserDisplayStyle::Regex) => {
|
||||
let re = regex::Regex::new(
|
||||
&self.tunables.username_display_regex.clone().unwrap_or("*".into()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
if !re.is_match(user_id.as_str()) {
|
||||
Cow::Borrowed(user_id.as_str())
|
||||
} else if let Some(display) = info.display_names.get(user_id) {
|
||||
Cow::Borrowed(display.as_str())
|
||||
} else {
|
||||
Cow::Borrowed(user_id.as_str())
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Span::styled(name, style)
|
||||
|
@ -1164,6 +1198,13 @@ mod tests {
|
|||
let res: Tunables =
|
||||
serde_json::from_str("{\"username_display\": \"displayname\"}").unwrap();
|
||||
assert_eq!(res.username_display, Some(UserDisplayStyle::DisplayName));
|
||||
|
||||
let res: Tunables = serde_json::from_str(
|
||||
"{\"username_display\": \"regex\",\n\"username_display_regex\": \"foo\"}",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(res.username_display, Some(UserDisplayStyle::Regex));
|
||||
assert_eq!(res.username_display_regex.unwrap_or("FAILED".into()), "foo".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -561,6 +561,9 @@ impl Application {
|
|||
IambAction::ClearUnreads => {
|
||||
let user_id = &store.application.settings.profile.user_id;
|
||||
|
||||
// Clear any notifications we displayed:
|
||||
store.application.open_notifications.clear();
|
||||
|
||||
for room_id in store.application.sync_info.chats() {
|
||||
if let Some(room) = store.application.rooms.get_mut(room_id) {
|
||||
room.fully_read(user_id);
|
||||
|
@ -596,6 +599,9 @@ impl Application {
|
|||
None
|
||||
},
|
||||
IambAction::Send(act) => {
|
||||
if store.application.settings.tunables.normal_after_send {
|
||||
self.bindings.reset_mode();
|
||||
}
|
||||
self.screen.current_window_mut()?.send_command(act, ctx, store).await?
|
||||
},
|
||||
|
||||
|
|
|
@ -41,6 +41,8 @@ use crate::{
|
|||
util::{join_cell_text, space_text},
|
||||
};
|
||||
|
||||
const QUOTE_COLOR: Color = Color::Indexed(236);
|
||||
|
||||
/// Generate bullet points from a [ListStyle].
|
||||
pub struct BulletIterator {
|
||||
style: ListStyle,
|
||||
|
@ -351,11 +353,14 @@ impl StyleTreeNode {
|
|||
printer.push_span_nobreak(span);
|
||||
},
|
||||
StyleTreeNode::Blockquote(child) => {
|
||||
let mut subp = printer.sub(4);
|
||||
let mut subp = printer.sub(3);
|
||||
child.print(&mut subp, style);
|
||||
|
||||
for mut line in subp.finish() {
|
||||
line.spans.insert(0, Span::styled(" ", style));
|
||||
line.spans
|
||||
.insert(0, Span::styled(line::THICK_VERTICAL, style.fg(QUOTE_COLOR)));
|
||||
line.spans.insert(0, Span::styled(" ", style));
|
||||
printer.push_line(line);
|
||||
}
|
||||
},
|
||||
|
@ -524,11 +529,11 @@ impl StyleTree {
|
|||
}
|
||||
|
||||
pub struct TreeGenState {
|
||||
link_num: u8,
|
||||
pub link_num: u8,
|
||||
}
|
||||
|
||||
impl TreeGenState {
|
||||
fn next_link_char(&mut self) -> Option<char> {
|
||||
pub fn next_link_char(&mut self) -> Option<char> {
|
||||
let num = self.link_num;
|
||||
|
||||
if num < 62 {
|
||||
|
@ -1075,14 +1080,29 @@ pub mod tests {
|
|||
let s = "<blockquote>Hello world!</blockquote>";
|
||||
let tree = parse_matrix_html(s);
|
||||
let text = tree.to_text(10, Style::default(), false, &settings);
|
||||
let style = Style::new().fg(QUOTE_COLOR);
|
||||
assert_eq!(text.lines.len(), 2);
|
||||
assert_eq!(
|
||||
text.lines[0],
|
||||
Line::from(vec![Span::raw(" "), Span::raw("Hello"), Span::raw(" ")])
|
||||
Line::from(vec![
|
||||
Span::raw(" "),
|
||||
Span::styled(line::THICK_VERTICAL, style),
|
||||
Span::raw(" "),
|
||||
Span::raw("Hello"),
|
||||
Span::raw(" "),
|
||||
Span::raw(" "),
|
||||
])
|
||||
);
|
||||
assert_eq!(
|
||||
text.lines[1],
|
||||
Line::from(vec![Span::raw(" "), Span::raw("world"), Span::raw("!")])
|
||||
Line::from(vec![
|
||||
Span::raw(" "),
|
||||
Span::styled(line::THICK_VERTICAL, style),
|
||||
Span::raw(" "),
|
||||
Span::raw("world"),
|
||||
Span::raw("!"),
|
||||
Span::raw(" "),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -72,6 +72,7 @@ mod state;
|
|||
|
||||
pub use self::compose::text_to_message;
|
||||
use self::state::{body_cow_state, html_state};
|
||||
pub use html::TreeGenState;
|
||||
|
||||
type ProtocolPreview<'a> = (&'a Protocol, u16, u16);
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ use matrix_sdk::{
|
|||
events::{room::message::MessageType, AnyMessageLikeEventContent, AnySyncTimelineEvent},
|
||||
serde::Raw,
|
||||
MilliSecondsSinceUnixEpoch,
|
||||
OwnedRoomId,
|
||||
RoomId,
|
||||
},
|
||||
Client,
|
||||
|
@ -24,6 +25,21 @@ const IAMB_XDG_NAME: &str = match option_env!("IAMB_XDG_NAME") {
|
|||
Some(iamb) => iamb,
|
||||
};
|
||||
|
||||
/// Handle for an open notification that should be closed when the user views it.
|
||||
pub struct NotificationHandle(
|
||||
#[cfg(all(feature = "desktop", unix, not(target_os = "macos")))]
|
||||
Option<notify_rust::NotificationHandle>,
|
||||
);
|
||||
|
||||
impl Drop for NotificationHandle {
|
||||
fn drop(&mut self) {
|
||||
#[cfg(all(feature = "desktop", unix, not(target_os = "macos")))]
|
||||
if let Some(handle) = self.0.take() {
|
||||
handle.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn register_notifications(
|
||||
client: &Client,
|
||||
settings: &ApplicationSettings,
|
||||
|
@ -54,6 +70,7 @@ pub async fn register_notifications(
|
|||
return;
|
||||
}
|
||||
|
||||
let room_id = room.room_id().to_owned();
|
||||
match notification.event {
|
||||
RawAnySyncOrStrippedTimelineEvent::Sync(e) => {
|
||||
match parse_full_notification(e, room, show_message).await {
|
||||
|
@ -66,7 +83,13 @@ pub async fn register_notifications(
|
|||
return;
|
||||
}
|
||||
|
||||
send_notification(¬ify_via, &store, &summary, body.as_deref())
|
||||
send_notification(
|
||||
¬ify_via,
|
||||
&summary,
|
||||
body.as_deref(),
|
||||
room_id,
|
||||
&store,
|
||||
)
|
||||
.await;
|
||||
},
|
||||
Err(err) => {
|
||||
|
@ -86,13 +109,14 @@ pub async fn register_notifications(
|
|||
|
||||
async fn send_notification(
|
||||
via: &NotifyVia,
|
||||
store: &AsyncProgramStore,
|
||||
summary: &str,
|
||||
body: Option<&str>,
|
||||
room_id: OwnedRoomId,
|
||||
store: &AsyncProgramStore,
|
||||
) {
|
||||
#[cfg(feature = "desktop")]
|
||||
if via.desktop {
|
||||
send_notification_desktop(summary, body);
|
||||
send_notification_desktop(summary, body, room_id, store).await;
|
||||
}
|
||||
#[cfg(not(feature = "desktop"))]
|
||||
{
|
||||
|
@ -110,7 +134,12 @@ async fn send_notification_bell(store: &AsyncProgramStore) {
|
|||
}
|
||||
|
||||
#[cfg(feature = "desktop")]
|
||||
fn send_notification_desktop(summary: &str, body: Option<&str>) {
|
||||
async fn send_notification_desktop(
|
||||
summary: &str,
|
||||
body: Option<&str>,
|
||||
room_id: OwnedRoomId,
|
||||
_store: &AsyncProgramStore,
|
||||
) {
|
||||
let mut desktop_notification = notify_rust::Notification::new();
|
||||
desktop_notification
|
||||
.summary(summary)
|
||||
|
@ -125,8 +154,19 @@ fn send_notification_desktop(summary: &str, body: Option<&str>) {
|
|||
desktop_notification.body(body);
|
||||
}
|
||||
|
||||
if let Err(err) = desktop_notification.show() {
|
||||
tracing::error!("Failed to send notification: {err}")
|
||||
match desktop_notification.show() {
|
||||
Err(err) => tracing::error!("Failed to send notification: {err}"),
|
||||
Ok(handle) => {
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
_store
|
||||
.lock()
|
||||
.await
|
||||
.application
|
||||
.open_notifications
|
||||
.entry(room_id)
|
||||
.or_default()
|
||||
.push(NotificationHandle(Some(handle)));
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -171,6 +171,7 @@ pub fn mock_tunables() -> TunableValues {
|
|||
default_room: None,
|
||||
log_level: Level::INFO,
|
||||
message_shortcode_display: false,
|
||||
normal_after_send: true,
|
||||
reaction_display: true,
|
||||
reaction_shortcode_display: false,
|
||||
read_receipt_send: true,
|
||||
|
@ -189,6 +190,7 @@ pub fn mock_tunables() -> TunableValues {
|
|||
open_command: None,
|
||||
external_edit_file_suffix: String::from(".md"),
|
||||
username_display: UserDisplayStyle::Username,
|
||||
username_display_regex: Some(String::from(".*")),
|
||||
message_user_color: false,
|
||||
mouse: Default::default(),
|
||||
notifications: Notifications {
|
||||
|
|
|
@ -86,7 +86,14 @@ use crate::base::{
|
|||
SendAction,
|
||||
};
|
||||
|
||||
use crate::message::{text_to_message, Message, MessageEvent, MessageKey, MessageTimeStamp};
|
||||
use crate::message::{
|
||||
text_to_message,
|
||||
Message,
|
||||
MessageEvent,
|
||||
MessageKey,
|
||||
MessageTimeStamp,
|
||||
TreeGenState,
|
||||
};
|
||||
use crate::worker::Requester;
|
||||
|
||||
use super::scrollback::{Scrollback, ScrollbackState};
|
||||
|
@ -226,10 +233,14 @@ impl ChatState {
|
|||
|
||||
let links = if let Some(html) = &msg.html {
|
||||
html.get_links()
|
||||
} else if let Ok(url) = Url::parse(&msg.event.body()) {
|
||||
vec![('0', url)]
|
||||
} else {
|
||||
vec![]
|
||||
linkify::LinkFinder::new()
|
||||
.links(&msg.event.body())
|
||||
.filter_map(|u| Url::parse(u.as_str()).ok())
|
||||
.scan(TreeGenState { link_num: 0 }, |state, u| {
|
||||
state.next_link_char().map(|c| (c, u))
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
if links.is_empty() {
|
||||
|
|
|
@ -502,19 +502,23 @@ async fn send_receipts_forever(client: &Client, store: &AsyncProgramStore) {
|
|||
loop {
|
||||
interval.tick().await;
|
||||
|
||||
let locked = store.lock().await;
|
||||
let user_id = &locked.application.settings.profile.user_id;
|
||||
let mut locked = store.lock().await;
|
||||
let ChatStore { settings, open_notifications, rooms, .. } = &mut locked.application;
|
||||
let user_id = &settings.profile.user_id;
|
||||
|
||||
let mut updates = Vec::new();
|
||||
for room in client.joined_rooms() {
|
||||
let room_id = room.room_id();
|
||||
let Some(info) = locked.application.rooms.get(&room_id) else {
|
||||
let Some(info) = rooms.get(room_id) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let changed = info.receipts(user_id).filter_map(|(thread, new_receipt)| {
|
||||
let old_receipt = sent.get(room_id).and_then(|ts| ts.get(thread));
|
||||
let changed = Some(new_receipt) != old_receipt;
|
||||
if changed {
|
||||
open_notifications.remove(room_id);
|
||||
}
|
||||
changed.then(|| (room_id.to_owned(), thread.to_owned(), new_receipt.to_owned()))
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue