Navigate to rooms using message links list
This commit is contained in:
parent
8040b82ff1
commit
bf9a9d640b
5 changed files with 123 additions and 52 deletions
|
|
@ -21,6 +21,7 @@ Procedure when bumping the version number:
|
|||
- Unicode-based grapheme width estimation method
|
||||
- `width_estimation_method` config option
|
||||
- `--width-estimation-method` option
|
||||
- Room links are now included in the `I` message links list
|
||||
|
||||
### Changed
|
||||
|
||||
|
|
|
|||
|
|
@ -7,16 +7,25 @@ use toss::{
|
|||
widgets::{Join2, Text},
|
||||
};
|
||||
|
||||
use crate::ui::{
|
||||
UiError, key_bindings, util,
|
||||
widgets::{ListBuilder, ListState, Popup},
|
||||
use crate::{
|
||||
euph::{self, SpanType},
|
||||
ui::{
|
||||
UiError, key_bindings, util,
|
||||
widgets::{ListBuilder, ListState, Popup},
|
||||
},
|
||||
};
|
||||
|
||||
use super::popup::PopupResult;
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
enum Link {
|
||||
Url(String),
|
||||
Room(String),
|
||||
}
|
||||
|
||||
pub struct LinksState {
|
||||
config: &'static Config,
|
||||
links: Vec<String>,
|
||||
links: Vec<Link>,
|
||||
list: ListState<usize>,
|
||||
}
|
||||
|
||||
|
|
@ -24,12 +33,34 @@ const NUMBER_KEYS: [char; 10] = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0
|
|||
|
||||
impl LinksState {
|
||||
pub fn new(config: &'static Config, content: &str) -> Self {
|
||||
let links = LinkFinder::new()
|
||||
let mut links = vec![];
|
||||
|
||||
// Collect URL-like links
|
||||
for link in LinkFinder::new()
|
||||
.url_must_have_scheme(false)
|
||||
.kinds(&[LinkKind::Url])
|
||||
.links(content)
|
||||
.map(|l| l.as_str().to_string())
|
||||
.collect();
|
||||
{
|
||||
links.push((
|
||||
link.start(),
|
||||
link.end(),
|
||||
Link::Url(link.as_str().to_string()),
|
||||
));
|
||||
}
|
||||
|
||||
// Collect room links
|
||||
for (span, range) in euph::find_spans(content) {
|
||||
if span == SpanType::Room {
|
||||
let name = &content[range.start + 1..range.end];
|
||||
links.push((range.start, range.end, Link::Room(name.to_string())));
|
||||
}
|
||||
}
|
||||
|
||||
links.sort();
|
||||
let links = links
|
||||
.into_iter()
|
||||
.map(|(_, _, link)| link)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Self {
|
||||
config,
|
||||
|
|
@ -49,29 +80,29 @@ impl LinksState {
|
|||
|
||||
for (id, link) in self.links.iter().enumerate() {
|
||||
let link = link.clone();
|
||||
if let Some(&number_key) = NUMBER_KEYS.get(id) {
|
||||
list_builder.add_sel(id, move |selected| {
|
||||
let text = if selected {
|
||||
Styled::new(format!("[{number_key}]"), style_selected.bold())
|
||||
.then(" ", style_selected)
|
||||
.then(link, style_selected)
|
||||
} else {
|
||||
Styled::new(format!("[{number_key}]"), Style::new().dark_grey().bold())
|
||||
.then_plain(" ")
|
||||
.then_plain(link)
|
||||
};
|
||||
Text::new(text)
|
||||
});
|
||||
} else {
|
||||
list_builder.add_sel(id, move |selected| {
|
||||
let text = if selected {
|
||||
Styled::new(format!(" {link}"), style_selected)
|
||||
} else {
|
||||
Styled::new_plain(format!(" {link}"))
|
||||
};
|
||||
Text::new(text)
|
||||
});
|
||||
}
|
||||
list_builder.add_sel(id, move |selected| {
|
||||
let mut text = Styled::default();
|
||||
|
||||
// Number key indicator
|
||||
text = match NUMBER_KEYS.get(id) {
|
||||
None if selected => text.then(" ", style_selected),
|
||||
None => text.then_plain(" "),
|
||||
Some(key) if selected => text.then(format!("[{key}] "), style_selected.bold()),
|
||||
Some(key) => text.then(format!("[{key}] "), Style::new().dark_grey().bold()),
|
||||
};
|
||||
|
||||
// The link itself
|
||||
text = match link {
|
||||
Link::Url(url) if selected => text.then(url, style_selected),
|
||||
Link::Url(url) => text.then_plain(url),
|
||||
Link::Room(name) if selected => {
|
||||
text.then(format!("&{name}"), style_selected.bold())
|
||||
}
|
||||
Link::Room(name) => text.then(format!("&{name}"), Style::new().blue().bold()),
|
||||
};
|
||||
|
||||
Text::new(text)
|
||||
});
|
||||
}
|
||||
|
||||
let hint_style = Style::new().grey().italic();
|
||||
|
|
@ -95,18 +126,24 @@ impl LinksState {
|
|||
}
|
||||
|
||||
fn open_link_by_id(&self, id: usize) -> PopupResult {
|
||||
if let Some(link) = self.links.get(id) {
|
||||
// The `http://` or `https://` schema is necessary for open::that to
|
||||
// successfully open the link in the browser.
|
||||
let link = if link.starts_with("http://") || link.starts_with("https://") {
|
||||
link.clone()
|
||||
} else {
|
||||
format!("https://{link}")
|
||||
};
|
||||
match self.links.get(id) {
|
||||
Some(Link::Url(url)) => {
|
||||
// The `http://` or `https://` schema is necessary for
|
||||
// open::that to successfully open the link in the browser.
|
||||
let link = if url.starts_with("http://") || url.starts_with("https://") {
|
||||
url.clone()
|
||||
} else {
|
||||
format!("https://{url}")
|
||||
};
|
||||
|
||||
if let Err(error) = open::that(&link) {
|
||||
return PopupResult::ErrorOpeningLink { link, error };
|
||||
if let Err(error) = open::that(&link) {
|
||||
return PopupResult::ErrorOpeningLink { link, error };
|
||||
}
|
||||
}
|
||||
|
||||
Some(Link::Room(name)) => return PopupResult::SwitchToRoom { name: name.clone() },
|
||||
|
||||
_ => {}
|
||||
}
|
||||
PopupResult::Handled
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,5 +35,6 @@ pub enum PopupResult {
|
|||
NotHandled,
|
||||
Handled,
|
||||
Close,
|
||||
SwitchToRoom { name: String },
|
||||
ErrorOpeningLink { link: String, error: io::Error },
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ use crate::{
|
|||
util,
|
||||
widgets::ListState,
|
||||
},
|
||||
vault::EuphRoomVault,
|
||||
vault::{EuphRoomVault, RoomIdentifier},
|
||||
};
|
||||
|
||||
use super::{
|
||||
|
|
@ -500,18 +500,22 @@ impl EuphRoom {
|
|||
false
|
||||
}
|
||||
|
||||
pub async fn handle_input_event(&mut self, event: &mut InputEvent<'_>, keys: &Keys) -> bool {
|
||||
pub async fn handle_input_event(
|
||||
&mut self,
|
||||
event: &mut InputEvent<'_>,
|
||||
keys: &Keys,
|
||||
) -> RoomResult {
|
||||
if !self.popups.is_empty() {
|
||||
if event.matches(&keys.general.abort) {
|
||||
self.popups.pop_back();
|
||||
return true;
|
||||
return RoomResult::Handled;
|
||||
}
|
||||
// Prevent event from reaching anything below the popup
|
||||
return false;
|
||||
return RoomResult::NotHandled;
|
||||
}
|
||||
|
||||
let result = match &mut self.state {
|
||||
State::Normal => return self.handle_normal_input_event(event, keys).await,
|
||||
State::Normal => return self.handle_normal_input_event(event, keys).await.into(),
|
||||
State::Auth(editor) => auth::handle_input_event(event, keys, &self.room, editor),
|
||||
State::Nick(editor) => nick::handle_input_event(event, keys, &self.room, editor),
|
||||
State::Account(account) => account.handle_input_event(event, keys, &self.room),
|
||||
|
|
@ -522,18 +526,24 @@ impl EuphRoom {
|
|||
};
|
||||
|
||||
match result {
|
||||
PopupResult::NotHandled => false,
|
||||
PopupResult::Handled => true,
|
||||
PopupResult::NotHandled => RoomResult::NotHandled,
|
||||
PopupResult::Handled => RoomResult::Handled,
|
||||
PopupResult::Close => {
|
||||
self.state = State::Normal;
|
||||
true
|
||||
RoomResult::Handled
|
||||
}
|
||||
PopupResult::SwitchToRoom { name } => RoomResult::SwitchToRoom {
|
||||
room: RoomIdentifier {
|
||||
domain: self.vault().room().domain.clone(),
|
||||
name,
|
||||
},
|
||||
},
|
||||
PopupResult::ErrorOpeningLink { link, error } => {
|
||||
self.popups.push_front(RoomPopup::Error {
|
||||
description: format!("Failed to open link: {link}"),
|
||||
reason: format!("{error}"),
|
||||
});
|
||||
true
|
||||
RoomResult::Handled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -638,3 +648,18 @@ impl EuphRoom {
|
|||
true
|
||||
}
|
||||
}
|
||||
|
||||
pub enum RoomResult {
|
||||
NotHandled,
|
||||
Handled,
|
||||
SwitchToRoom { room: RoomIdentifier },
|
||||
}
|
||||
|
||||
impl From<bool> for RoomResult {
|
||||
fn from(value: bool) -> Self {
|
||||
match value {
|
||||
true => Self::Handled,
|
||||
false => Self::NotHandled,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ use crate::{
|
|||
|
||||
use super::{
|
||||
UiError, UiEvent,
|
||||
euph::room::EuphRoom,
|
||||
euph::room::{EuphRoom, RoomResult},
|
||||
key_bindings, util,
|
||||
widgets::{ListBuilder, ListState},
|
||||
};
|
||||
|
|
@ -574,8 +574,15 @@ impl Rooms {
|
|||
}
|
||||
State::ShowRoom(name) => {
|
||||
if let Some(room) = self.euph_rooms.get_mut(name) {
|
||||
if room.handle_input_event(event, keys).await {
|
||||
return true;
|
||||
match room.handle_input_event(event, keys).await {
|
||||
RoomResult::NotHandled => {}
|
||||
RoomResult::Handled => return true,
|
||||
RoomResult::SwitchToRoom { room } => {
|
||||
self.list.move_cursor_to_id(&room);
|
||||
self.connect_to_room(room.clone()).await;
|
||||
self.state = State::ShowRoom(room);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if event.matches(&keys.general.abort) {
|
||||
self.state = State::ShowList;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue