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