use bitflags::bitflags;
use matrix_sdk::ruma::{
OwnedRoomAliasId, RoomAliasId, RoomId,
events::tag::{TagName, Tags},
};
use std::{
borrow::Cow,
cmp::Ordering,
collections::{BTreeMap, HashSet},
fmt,
ops::Deref,
};
use crate::room::invited_room::InvitedRoomInfo;
use super::rooms_list::JoinedRoomInfo;
#[allow(dead_code)]
static EMPTY_TAGS: Tags = BTreeMap::new();
pub trait FilterableRoom {
fn room_id(&self) -> &RoomId;
fn room_name(&self) -> Cow<'_, str>;
fn _unread_mentions(&self) -> u64;
fn _unread_messages(&self) -> u64;
fn canonical_alias(&self) -> Option<Cow<'_, RoomAliasId>>;
fn alt_aliases(&self) -> Cow<'_, [OwnedRoomAliasId]>;
fn tags(&self) -> &Tags;
fn latest_ts(&self) -> u64;
}
impl FilterableRoom for JoinedRoomInfo {
fn room_id(&self) -> &RoomId {
&self.room_id
}
fn room_name(&self) -> Cow<'_, str> {
Cow::Owned(self.room_name.to_string())
}
fn _unread_mentions(&self) -> u64 {
self.num_unread_mentions
}
fn _unread_messages(&self) -> u64 {
self.num_unread_messages
}
fn canonical_alias(&self) -> Option<Cow<'_, RoomAliasId>> {
self.canonical_alias.as_deref().map(Cow::Borrowed)
}
fn alt_aliases(&self) -> Cow<'_, [OwnedRoomAliasId]> {
Cow::Borrowed(&self.alt_aliases)
}
fn tags(&self) -> &Tags {
&self.tags
}
fn latest_ts(&self) -> u64 {
self.latest
.as_ref()
.map_or(0, |latest| latest.0.get().into())
}
}
impl FilterableRoom for InvitedRoomInfo {
fn room_id(&self) -> &RoomId {
&self.room_id
}
fn room_name(&self) -> Cow<'_, str> {
Cow::Owned(self.room_name.to_string())
}
fn _unread_mentions(&self) -> u64 {
1
}
fn _unread_messages(&self) -> u64 {
0
}
fn canonical_alias(&self) -> Option<Cow<'_, RoomAliasId>> {
self.canonical_alias.as_deref().map(Cow::Borrowed)
}
fn alt_aliases(&self) -> Cow<'_, [OwnedRoomAliasId]> {
Cow::Borrowed(&self.alt_aliases)
}
fn tags(&self) -> &Tags {
&EMPTY_TAGS
}
fn latest_ts(&self) -> u64 {
self.latest
.as_ref()
.map_or(0, |latest| latest.0.get().into())
}
}
pub type RoomFilterFn = dyn Fn(&(dyn FilterableRoom + Send + Sync)) -> bool + Send + Sync;
#[allow(dead_code)]
pub type SortFn = dyn Fn(&(dyn FilterableRoom + Send + Sync), &(dyn FilterableRoom + Send + Sync)) -> Ordering
+ Send
+ Sync;
pub struct RoomDisplayFilter(Box<RoomFilterFn>);
impl Default for RoomDisplayFilter {
fn default() -> Self {
RoomDisplayFilter(Box::new(|_| true))
}
}
impl Deref for RoomDisplayFilter {
type Target = Box<RoomFilterFn>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl fmt::Debug for RoomDisplayFilter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RoomDisplayFilter")
.field("filter_fn", &"<function>")
.finish()
}
}
bitflags! {
#[derive(Copy, Clone, PartialEq, Eq)]
pub struct RoomFilterCriteria: u8 {
const RoomId = 0b0000_0001;
const RoomName = 0b0000_0010;
const RoomAlias = 0b0000_0100;
const RoomTags = 0b0000_1000;
const All = Self::RoomId.bits() | Self::RoomName.bits() | Self::RoomAlias.bits() | Self::RoomTags.bits();
}
}
impl Default for RoomFilterCriteria {
fn default() -> Self {
RoomFilterCriteria::All
}
}
#[allow(dead_code)]
pub struct RoomDisplayFilterBuilder {
keywords: String,
filter_criteria: RoomFilterCriteria,
sort_fn: Option<Box<SortFn>>,
}
impl RoomDisplayFilterBuilder {
#[allow(dead_code)]
pub fn new() -> Self {
Self {
keywords: String::new(),
filter_criteria: RoomFilterCriteria::default(),
sort_fn: None,
}
}
pub fn set_keywords(mut self, keywords: String) -> Self {
self.keywords = keywords;
self
}
pub fn set_filter_criteria(mut self, filter_criteria: RoomFilterCriteria) -> Self {
self.filter_criteria = filter_criteria;
self
}
pub fn _sort_by<F>(mut self, sort_fn: F) -> Self
where
F: Fn(&(dyn FilterableRoom + Send + Sync), &(dyn FilterableRoom + Send + Sync)) -> Ordering
+ Send
+ Sync
+ 'static,
{
self.sort_fn = Some(Box::new(sort_fn));
self
}
pub fn sort_by_latest_ts(mut self) -> Self {
self.sort_fn = Some(Box::new(|a, b| b.latest_ts().cmp(&a.latest_ts())));
self
}
fn matches_room_id(room: &dyn FilterableRoom, keywords: &str) -> bool {
room.room_id().as_str().eq_ignore_ascii_case(keywords)
}
fn matches_room_name(room: &dyn FilterableRoom, keywords: &str) -> bool {
room.room_name().to_lowercase().contains(keywords)
}
fn matches_room_alias(room: &dyn FilterableRoom, keywords: &str) -> bool {
room.canonical_alias()
.is_some_and(|alias| alias.as_str().eq_ignore_ascii_case(keywords))
|| room
.alt_aliases()
.iter()
.any(|alias| alias.as_str().eq_ignore_ascii_case(keywords))
}
fn matches_room_tags(room: &dyn FilterableRoom, keywords: &str) -> bool {
fn is_tag_match(search_tag: &str, tag_name: &TagName) -> bool {
match tag_name {
TagName::Favorite => ["favourite", "favorite", "fav"].contains(&search_tag),
TagName::LowPriority => {
["low_priority", "low-priority", "lowpriority", "lowPriority"]
.contains(&search_tag)
}
TagName::ServerNotice => [
"server_notice",
"server-notice",
"servernotice",
"serverNotice",
]
.contains(&search_tag),
TagName::User(user_tag) => user_tag.as_ref().eq_ignore_ascii_case(search_tag),
_ => false,
}
}
let search_tags: HashSet<&str> = keywords
.split_whitespace()
.map(|tag| tag.trim_start_matches(':'))
.collect();
let tags = room.tags();
search_tags.iter().all(|search_tag| {
tags.iter()
.any(|(tag_name, _)| is_tag_match(search_tag, tag_name))
})
}
fn pre_match_filter_check(keywords: &str) -> (RoomFilterCriteria, &str) {
match keywords.chars().next() {
Some('!') => (RoomFilterCriteria::RoomId, keywords),
Some('#') => (RoomFilterCriteria::RoomAlias, keywords),
Some(':') => (RoomFilterCriteria::RoomTags, keywords),
_ => (RoomFilterCriteria::All, keywords),
}
}
fn matches_filter(
room: &dyn FilterableRoom,
keywords: &str,
filter_criteria: RoomFilterCriteria,
) -> bool {
if filter_criteria.is_empty() {
return false;
}
let (specific_type, cleaned_keywords) = Self::pre_match_filter_check(keywords);
if specific_type != RoomFilterCriteria::All {
match specific_type {
RoomFilterCriteria::RoomId
if filter_criteria.contains(RoomFilterCriteria::RoomId) =>
{
Self::matches_room_id(room, cleaned_keywords)
}
RoomFilterCriteria::RoomAlias
if filter_criteria.contains(RoomFilterCriteria::RoomAlias) =>
{
Self::matches_room_alias(room, cleaned_keywords)
}
RoomFilterCriteria::RoomTags
if filter_criteria.contains(RoomFilterCriteria::RoomTags) =>
{
Self::matches_room_tags(room, cleaned_keywords)
}
_ => false,
}
} else {
let mut matches = false;
if filter_criteria.contains(RoomFilterCriteria::RoomId) {
matches |= Self::matches_room_id(room, cleaned_keywords);
}
if filter_criteria.contains(RoomFilterCriteria::RoomName) {
matches |= Self::matches_room_name(room, cleaned_keywords);
}
if filter_criteria.contains(RoomFilterCriteria::RoomAlias) {
matches |= Self::matches_room_alias(room, cleaned_keywords);
}
if filter_criteria.contains(RoomFilterCriteria::RoomTags) {
matches |= Self::matches_room_tags(room, cleaned_keywords);
}
matches
}
}
pub fn build(self) -> (RoomDisplayFilter, Option<Box<SortFn>>) {
let keywords = self.keywords;
let filter_criteria = self.filter_criteria;
let filter = RoomDisplayFilter(Box::new(
move |room: &(dyn FilterableRoom + Send + Sync)| {
if keywords.is_empty() || filter_criteria.is_empty() {
return true;
}
let keywords = keywords.trim().to_lowercase();
Self::matches_filter(room, &keywords, self.filter_criteria)
},
));
(filter, self.sort_fn)
}
}
impl Default for RoomDisplayFilterBuilder {
fn default() -> Self {
Self::new()
}
}