Indicate and sort on rooms with unread messages (#205)

Fixes #83
This commit is contained in:
Ulyssa 2024-02-28 09:03:28 -08:00 committed by GitHub
parent 3ed87aae05
commit a2a708f1ae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 271 additions and 64 deletions

View file

@ -79,9 +79,11 @@ use crate::base::{
SortFieldRoom,
SortFieldUser,
SortOrder,
UnreadInfo,
};
use self::{room::RoomState, welcome::WelcomeState};
use crate::message::MessageTimeStamp;
pub mod room;
pub mod welcome;
@ -124,11 +126,31 @@ fn selected_text(s: &str, selected: bool) -> Text {
Text::from(selected_span(s, selected))
}
fn name_and_labels(name: &str, unread: bool, style: Style) -> (Span<'_>, Vec<Vec<Span<'_>>>) {
let name_style = if unread {
style.add_modifier(StyleModifier::BOLD)
} else {
style
};
let name = Span::styled(name, name_style);
let labels = if unread {
vec![vec![Span::styled("Unread", style)]]
} else {
vec![]
};
(name, labels)
}
/// Sort `Some` to be less than `None` so that list items with values come before those without.
#[inline]
fn some_cmp<T: Ord>(a: Option<T>, b: Option<T>) -> Ordering {
fn some_cmp<T, F>(a: Option<T>, b: Option<T>, f: F) -> Ordering
where
F: Fn(&T, &T) -> Ordering,
{
match (a, b) {
(Some(a), Some(b)) => a.cmp(&b),
(Some(a), Some(b)) => f(&a, &b),
(None, None) => Ordering::Equal,
(None, Some(_)) => Ordering::Greater,
(Some(_), None) => Ordering::Less,
@ -167,8 +189,16 @@ fn room_cmp<T: RoomLikeItem>(a: &T, b: &T, field: &SortFieldRoom) -> Ordering {
lowa.cmp(&lowb)
},
SortFieldRoom::Name => a.name().cmp(b.name()),
SortFieldRoom::Alias => some_cmp(a.alias(), b.alias()),
SortFieldRoom::Alias => some_cmp(a.alias(), b.alias(), Ord::cmp),
SortFieldRoom::RoomId => a.room_id().cmp(b.room_id()),
SortFieldRoom::Unread => {
// Sort true (unread) before false (read)
b.is_unread().cmp(&a.is_unread())
},
SortFieldRoom::Recent => {
// sort larger timestamps towards the top.
some_cmp(a.recent_ts(), b.recent_ts(), |a, b| b.cmp(a))
},
}
}
@ -243,6 +273,8 @@ fn append_tags<'a>(tags: Vec<Vec<Span<'a>>>, spans: &mut Vec<Span<'a>>, style: S
trait RoomLikeItem {
fn room_id(&self) -> &RoomId;
fn has_tag(&self, tag: TagName) -> bool;
fn is_unread(&self) -> bool;
fn recent_ts(&self) -> Option<&MessageTimeStamp>;
fn alias(&self) -> Option<&RoomAliasId>;
fn name(&self) -> &str;
}
@ -780,6 +812,7 @@ pub struct GenericChatItem {
room_info: MatrixRoomInfo,
name: String,
alias: Option<OwnedRoomAliasId>,
unread: UnreadInfo,
is_dm: bool,
}
@ -788,17 +821,17 @@ impl GenericChatItem {
let room = &room_info.deref().0;
let room_id = room.room_id();
let info = store.application.get_room_info(room_id.to_owned());
let info = store.application.rooms.get_or_default(room_id.to_owned());
let name = info.name.clone().unwrap_or_default();
let alias = room.canonical_alias();
let unread = info.unreads(&store.application.settings);
info.tags = room_info.deref().1.clone();
if let Some(alias) = &alias {
store.application.names.insert(alias.to_string(), room_id.to_owned());
}
GenericChatItem { room_info, name, alias, is_dm }
GenericChatItem { room_info, name, alias, is_dm, unread }
}
#[inline]
@ -832,6 +865,14 @@ impl RoomLikeItem for GenericChatItem {
false
}
}
fn recent_ts(&self) -> Option<&MessageTimeStamp> {
self.unread.latest()
}
fn is_unread(&self) -> bool {
self.unread.is_unread()
}
}
impl ToString for GenericChatItem {
@ -842,13 +883,16 @@ impl ToString for GenericChatItem {
impl ListItem<IambInfo> for GenericChatItem {
fn show(&self, selected: bool, _: &ViewportContext<ListCursor>, _: &mut ProgramStore) -> Text {
let unread = self.unread.is_unread();
let style = selected_style(selected);
let mut spans = vec![Span::styled(self.name.as_str(), style)];
let mut labels = if self.is_dm {
vec![vec![Span::styled("DM", style)]]
let (name, mut labels) = name_and_labels(&self.name, unread, style);
let mut spans = vec![name];
labels.push(if self.is_dm {
vec![Span::styled("DM", style)]
} else {
vec![vec![Span::styled("Room", style)]]
};
vec![Span::styled("Room", style)]
});
if let Some(tags) = &self.tags() {
labels.extend(tags.keys().map(|t| tag_to_span(t, style)));
@ -879,6 +923,7 @@ pub struct RoomItem {
room_info: MatrixRoomInfo,
name: String,
alias: Option<OwnedRoomAliasId>,
unread: UnreadInfo,
}
impl RoomItem {
@ -886,16 +931,17 @@ impl RoomItem {
let room = &room_info.deref().0;
let room_id = room.room_id();
let info = store.application.get_room_info(room_id.to_owned());
let info = store.application.rooms.get_or_default(room_id.to_owned());
let name = info.name.clone().unwrap_or_default();
let alias = room.canonical_alias();
let unread = info.unreads(&store.application.settings);
info.tags = room_info.deref().1.clone();
if let Some(alias) = &alias {
store.application.names.insert(alias.to_string(), room_id.to_owned());
}
RoomItem { room_info, name, alias }
RoomItem { room_info, name, alias, unread }
}
#[inline]
@ -929,6 +975,14 @@ impl RoomLikeItem for RoomItem {
false
}
}
fn recent_ts(&self) -> Option<&MessageTimeStamp> {
self.unread.latest()
}
fn is_unread(&self) -> bool {
self.unread.is_unread()
}
}
impl ToString for RoomItem {
@ -939,17 +993,18 @@ impl ToString for RoomItem {
impl ListItem<IambInfo> for RoomItem {
fn show(&self, selected: bool, _: &ViewportContext<ListCursor>, _: &mut ProgramStore) -> Text {
let unread = self.unread.is_unread();
let style = selected_style(selected);
let (name, mut labels) = name_and_labels(&self.name, unread, style);
let mut spans = vec![name];
if let Some(tags) = &self.tags() {
let style = selected_style(selected);
let mut spans = vec![Span::styled(self.name.as_str(), style)];
let tags = tags.keys().map(|t| tag_to_span(t, style)).collect();
append_tags(tags, &mut spans, style);
Text::from(Line::from(spans))
} else {
selected_text(self.name.as_str(), selected)
labels.extend(tags.keys().map(|t| tag_to_span(t, style)));
}
append_tags(labels, &mut spans, style);
Text::from(Line::from(spans))
}
fn get_word(&self) -> Option<String> {
@ -973,15 +1028,20 @@ pub struct DirectItem {
room_info: MatrixRoomInfo,
name: String,
alias: Option<OwnedRoomAliasId>,
unread: UnreadInfo,
}
impl DirectItem {
fn new(room_info: MatrixRoomInfo, store: &mut ProgramStore) -> Self {
let room_id = room_info.0.room_id().to_owned();
let name = store.application.get_room_info(room_id).name.clone().unwrap_or_default();
let alias = room_info.0.canonical_alias();
DirectItem { room_info, name, alias }
let info = store.application.rooms.get_or_default(room_id);
let name = info.name.clone().unwrap_or_default();
let unread = info.unreads(&store.application.settings);
info.tags = room_info.deref().1.clone();
DirectItem { room_info, name, alias, unread }
}
#[inline]
@ -1015,6 +1075,14 @@ impl RoomLikeItem for DirectItem {
fn room_id(&self) -> &RoomId {
self.room().room_id()
}
fn recent_ts(&self) -> Option<&MessageTimeStamp> {
self.unread.latest()
}
fn is_unread(&self) -> bool {
self.unread.is_unread()
}
}
impl ToString for DirectItem {
@ -1025,17 +1093,18 @@ impl ToString for DirectItem {
impl ListItem<IambInfo> for DirectItem {
fn show(&self, selected: bool, _: &ViewportContext<ListCursor>, _: &mut ProgramStore) -> Text {
let unread = self.unread.is_unread();
let style = selected_style(selected);
let (name, mut labels) = name_and_labels(&self.name, unread, style);
let mut spans = vec![name];
if let Some(tags) = &self.tags() {
let style = selected_style(selected);
let mut spans = vec![Span::styled(self.name.as_str(), style)];
let tags = tags.keys().map(|t| tag_to_span(t, style)).collect();
append_tags(tags, &mut spans, style);
Text::from(Line::from(spans))
} else {
selected_text(self.name.as_str(), selected)
labels.extend(tags.keys().map(|t| tag_to_span(t, style)));
}
append_tags(labels, &mut spans, style);
Text::from(Line::from(spans))
}
fn get_word(&self) -> Option<String> {
@ -1103,6 +1172,16 @@ impl RoomLikeItem for SpaceItem {
// exposes them, so we'll just always return false here for now.
false
}
fn recent_ts(&self) -> Option<&MessageTimeStamp> {
// XXX: this needs to determine the room with most recent message and return its timestamp.
None
}
fn is_unread(&self) -> bool {
// XXX: this needs to check whether the space contains rooms with unread messages
false
}
}
impl ToString for SpaceItem {
@ -1433,6 +1512,7 @@ mod tests {
tags: Vec<TagName>,
alias: Option<OwnedRoomAliasId>,
name: &'static str,
unread: UnreadInfo,
}
impl RoomLikeItem for &TestRoomItem {
@ -1451,6 +1531,14 @@ mod tests {
fn name(&self) -> &str {
self.name
}
fn recent_ts(&self) -> Option<&MessageTimeStamp> {
self.unread.latest()
}
fn is_unread(&self) -> bool {
self.unread.is_unread()
}
}
#[test]
@ -1462,6 +1550,7 @@ mod tests {
tags: vec![TagName::Favorite],
alias: Some(room_alias_id!("#room1:example.com").to_owned()),
name: "Z",
unread: UnreadInfo::default(),
};
let room2 = TestRoomItem {
@ -1469,6 +1558,7 @@ mod tests {
tags: vec![],
alias: Some(room_alias_id!("#a:example.com").to_owned()),
name: "Unnamed Room",
unread: UnreadInfo::default(),
};
let room3 = TestRoomItem {
@ -1476,6 +1566,7 @@ mod tests {
tags: vec![],
alias: None,
name: "Cool Room",
unread: UnreadInfo::default(),
};
// Sort by Name ascending.
@ -1510,4 +1601,51 @@ mod tests {
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields));
assert_eq!(rooms, vec![&room2, &room3, &room1]);
}
#[test]
fn test_sort_room_recents() {
let server = server_name!("example.com");
let room1 = TestRoomItem {
room_id: RoomId::new(server).to_owned(),
tags: vec![],
alias: None,
name: "Room 1",
unread: UnreadInfo { unread: false, latest: None },
};
let room2 = TestRoomItem {
room_id: RoomId::new(server).to_owned(),
tags: vec![],
alias: None,
name: "Room 2",
unread: UnreadInfo {
unread: false,
latest: Some(MessageTimeStamp::OriginServer(40u32.into())),
},
};
let room3 = TestRoomItem {
room_id: RoomId::new(server).to_owned(),
tags: vec![],
alias: None,
name: "Room 3",
unread: UnreadInfo {
unread: false,
latest: Some(MessageTimeStamp::OriginServer(20u32.into())),
},
};
// Sort by Recent ascending.
let mut rooms = vec![&room1, &room2, &room3];
let fields = &[SortColumn(SortFieldRoom::Recent, SortOrder::Ascending)];
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields));
assert_eq!(rooms, vec![&room2, &room3, &room1]);
// Sort by Recent descending.
let mut rooms = vec![&room1, &room2, &room3];
let fields = &[SortColumn(SortFieldRoom::Recent, SortOrder::Descending)];
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields));
assert_eq!(rooms, vec![&room1, &room3, &room2]);
}
}

View file

@ -1342,7 +1342,9 @@ impl<'a> StatefulWidget for Scrollback<'a> {
state.cursor.timestamp.is_none()
{
// If the cursor is at the last message, then update the read marker.
info.read_till = info.messages.last_key_value().map(|(k, _)| k.1.clone());
if let Some((k, _)) = info.messages.last_key_value() {
info.set_receipt(settings.profile.user_id.clone(), k.1.clone());
}
}
// Check whether we should load older messages for this room.