diff --git a/src/app.rs b/src/app.rs index 8737aa5a..d3ea88bd 100644 --- a/src/app.rs +++ b/src/app.rs @@ -4,7 +4,7 @@ use std::{cell::RefCell, collections::HashMap}; use makepad_widgets::*; -use matrix_sdk::{RoomState, ruma::{OwnedRoomId, RoomId}}; +use matrix_sdk::{RoomState, ruma::{OwnedEventId, OwnedRoomId, RoomId}}; use serde::{Deserialize, Serialize}; use crate::{ avatar_cache::clear_avatar_cache, home::{ @@ -374,18 +374,14 @@ impl MatchEvent for App { continue; } + // A new room has been selected, update the app state and navigate to the main content view. if let RoomsListAction::Selected(selected_room) = action.as_widget_action().cast() { - // A room has been selected, update the app state and navigate to the main content view. - let display_name = match &selected_room { - SelectedRoom::JoinedRoom { room_name_id } => room_name_id.to_string(), - SelectedRoom::InvitedRoom { room_name_id } => room_name_id.to_string(), - SelectedRoom::Space { space_name_id } => format!("[Space] {}", space_name_id), - }; - self.app_state.selected_room = Some(selected_room); // Set the Stack Navigation header to show the name of the newly-selected room. self.ui .label(ids!(main_content_view.header.content.title_container.title)) - .set_text(cx, &display_name); + .set_text(cx, &selected_room.display_name()); + + self.app_state.selected_room = Some(selected_room); // Navigate to the main content view cx.widget_action( @@ -852,12 +848,29 @@ pub struct SavedDockState { /// Represents a room currently or previously selected by the user. /// -/// One `SelectedRoom` is considered equal to another if their `room_id`s are equal. +/// ## PartialEq/Eq equality comparison behavior +/// Room/Space names are ignored for the purpose of equality comparison. +/// Two `SelectedRoom`s are considered equal if their `room_id`s are equal, +/// unless they are `Thread`s,` in which case their `thread_root_event_id`s +/// are also compared for equality. +/// A `Thread` is never considered equal to a non-`Thread`, even if their `room_id`s are equal. #[derive(Clone, Debug, Serialize, Deserialize)] pub enum SelectedRoom { - JoinedRoom { room_name_id: RoomNameId }, - InvitedRoom { room_name_id: RoomNameId }, - Space { space_name_id: RoomNameId }, + JoinedRoom { + room_name_id: RoomNameId, + }, + Thread { + room_name_id: RoomNameId, + /// The event ID of the root message of this thread, + /// which is used to distinguish this thread from the main room timeline. + thread_root_event_id: OwnedEventId, + }, + InvitedRoom { + room_name_id: RoomNameId, + }, + Space { + space_name_id: RoomNameId, + }, } impl SelectedRoom { @@ -866,6 +879,7 @@ impl SelectedRoom { SelectedRoom::JoinedRoom { room_name_id } => room_name_id.room_id(), SelectedRoom::InvitedRoom { room_name_id } => room_name_id.room_id(), SelectedRoom::Space { space_name_id } => space_name_id.room_id(), + SelectedRoom::Thread { room_name_id, .. } => room_name_id.room_id(), } } @@ -874,6 +888,7 @@ impl SelectedRoom { SelectedRoom::JoinedRoom { room_name_id } => room_name_id, SelectedRoom::InvitedRoom { room_name_id } => room_name_id, SelectedRoom::Space { space_name_id } => space_name_id, + SelectedRoom::Thread { room_name_id, .. } => room_name_id, } } @@ -895,10 +910,49 @@ impl SelectedRoom { _ => false, } } + + /// Returns the `LiveId` of the room tab corresponding to this `SelectedRoom`. + pub fn tab_id(&self) -> LiveId { + match self { + SelectedRoom::Thread { room_name_id, thread_root_event_id } => { + LiveId::from_str( + &format!("{}##{}", room_name_id.room_id(), thread_root_event_id) + ) + } + other => LiveId::from_str(other.room_id().as_str()), + } + } + + /// Returns the display name to be shown for this room in the UI. + pub fn display_name(&self) -> String { + match self { + SelectedRoom::JoinedRoom { room_name_id } => room_name_id.to_string(), + SelectedRoom::InvitedRoom { room_name_id } => room_name_id.to_string(), + SelectedRoom::Space { space_name_id } => format!("[Space] {space_name_id}"), + SelectedRoom::Thread { room_name_id, .. } => format!("[Thread] {room_name_id}"), + } + } } + impl PartialEq for SelectedRoom { fn eq(&self, other: &Self) -> bool { - self.room_id() == other.room_id() + match (self, other) { + ( + SelectedRoom::Thread { + room_name_id: lhs_room_name_id, + thread_root_event_id: lhs_thread_root_event_id, + }, + SelectedRoom::Thread { + room_name_id: rhs_room_name_id, + thread_root_event_id: rhs_thread_root_event_id, + }, + ) => { + lhs_room_name_id.room_id() == rhs_room_name_id.room_id() + && lhs_thread_root_event_id == rhs_thread_root_event_id + } + (SelectedRoom::Thread { .. }, _) | (_, SelectedRoom::Thread { .. }) => false, + _ => self.room_id() == other.room_id(), + } } } impl Eq for SelectedRoom {} diff --git a/src/event_preview.rs b/src/event_preview.rs index 75b9b0be..d4e0cde2 100644 --- a/src/event_preview.rs +++ b/src/event_preview.rs @@ -7,7 +7,7 @@ use std::borrow::Cow; -use matrix_sdk::{ruma::{events::{room::{guest_access::GuestAccess, history_visibility::HistoryVisibility, join_rules::JoinRule, message::{MessageFormat, MessageType}}, AnySyncMessageLikeEvent, AnySyncTimelineEvent, FullStateEventContent, SyncMessageLikeEvent}, serde::Raw, UserId}}; +use matrix_sdk::{ruma::{OwnedUserId, events::{room::{guest_access::GuestAccess, history_visibility::HistoryVisibility, join_rules::JoinRule, message::{MessageFormat, MessageType}}, AnySyncMessageLikeEvent, AnySyncTimelineEvent, FullStateEventContent, SyncMessageLikeEvent}, serde::Raw, UserId}}; use matrix_sdk_base::crypto::types::events::UtdCause; use matrix_sdk_ui::timeline::{self, AnyOtherFullStateEventContent, EncryptedMessage, EventTimelineItem, MemberProfileChange, MembershipChange, MsgLikeKind, OtherMessageLike, RoomMembershipChange, TimelineItemContent}; @@ -69,7 +69,7 @@ pub fn text_preview_of_timeline_item( match content { TimelineItemContent::MsgLike(msg_like_content) => { match &msg_like_content.kind { - MsgLikeKind::Message(msg) => text_preview_of_message(msg, sender_username), + MsgLikeKind::Message(msg) => text_preview_of_message(msg.msgtype(), sender_username), MsgLikeKind::Sticker(sticker) => TextPreview::from(( format!("[Sticker]: {}", htmlize::escape_text(&sticker.content().body)), BeforeText::UsernameWithColon, @@ -206,11 +206,11 @@ pub fn plaintext_body_of_timeline_item( /// Returns a text preview of the given message as an Html-formatted string. -pub fn text_preview_of_message( - message: &timeline::Message, +fn text_preview_of_message( + msg: &MessageType, sender_username: &str, ) -> TextPreview { - let text = match message.msgtype() { + let text = match msg { MessageType::Audio(audio) => format!( "[Audio]: {}", if let Some(formatted_body) = audio.formatted.as_ref() { @@ -300,6 +300,36 @@ pub fn text_preview_of_message( TextPreview::from((text, BeforeText::UsernameWithColon)) } +/// Returns a preview of the given raw timeline event. +pub fn text_preview_of_raw_timeline_event( + raw_event: &Raw, + sender_username: &str, +) -> Option { + match raw_event.deserialize().ok()? { + AnySyncTimelineEvent::MessageLike( + AnySyncMessageLikeEvent::RoomMessage( + SyncMessageLikeEvent::Original(ev) + ) + ) => Some(text_preview_of_message( + &ev.content.msgtype, + sender_username, + )), + AnySyncTimelineEvent::MessageLike( + AnySyncMessageLikeEvent::RoomMessage( + SyncMessageLikeEvent::Redacted(_) + ) + ) => { + let sender_user_id = raw_event.get_field::("sender").ok().flatten()?; + Some(text_preview_of_redacted_message( + Some(raw_event), + sender_user_id.as_ref(), + sender_username, + )) + } + _ => None, + } +} + /// Returns a plaintext preview of the given redacted message. /// diff --git a/src/home/editing_pane.rs b/src/home/editing_pane.rs index 42005192..842682ad 100644 --- a/src/home/editing_pane.rs +++ b/src/home/editing_pane.rs @@ -2,7 +2,6 @@ use makepad_widgets::{text::selection::Cursor, *}; use matrix_sdk::{ room::edit::EditedContent, ruma::{ - OwnedRoomId, events::{ poll::unstable_start::{UnstablePollAnswer, UnstablePollStartContentBlock}, room::message::{FormattedBody, MessageType, RoomMessageEventContentWithoutRelation}, @@ -13,7 +12,8 @@ use matrix_sdk_ui::timeline::{EventTimelineItem, MsgLikeKind, TimelineEventItemI use crate::shared::mentionable_text_input::MentionableTextInputWidgetExt; use crate::{ - shared::popup_list::{enqueue_popup_notification, PopupKind}, sliding_sync::{submit_async_request, MatrixRequest} + shared::popup_list::{enqueue_popup_notification, PopupKind}, + sliding_sync::{submit_async_request, MatrixRequest, TimelineKind}, }; live_design! { @@ -150,7 +150,7 @@ pub enum EditingPaneAction { /// The information maintained by the EditingPane widget. struct EditingPaneInfo { event_tl_item: EventTimelineItem, - room_id: OwnedRoomId, + timeline_kind: TimelineKind, } /// A view that slides in from the bottom of the screen to allow editing a message. @@ -369,7 +369,7 @@ impl Widget for EditingPane { }; submit_async_request(MatrixRequest::EditMessage { - room_id: info.room_id.clone(), + timeline_kind: info.timeline_kind.clone(), timeline_event_item_id: info.event_tl_item.identifier(), edited_content, }); @@ -426,7 +426,12 @@ impl EditingPane { } /// Shows the editing pane and sets it up to edit the given `event`'s content. - pub fn show(&mut self, cx: &mut Cx, event_tl_item: EventTimelineItem, room_id: OwnedRoomId) { + pub fn show( + &mut self, + cx: &mut Cx, + event_tl_item: EventTimelineItem, + timeline_kind: TimelineKind, + ) { if !event_tl_item.is_editable() { enqueue_popup_notification( "That message cannot be edited.", @@ -452,7 +457,10 @@ impl EditingPane { } - self.info = Some(EditingPaneInfo { event_tl_item, room_id: room_id.clone() }); + self.info = Some(EditingPaneInfo { + event_tl_item, + timeline_kind, + }); self.visible = true; self.button(ids!(accept_button)).reset_hover(cx); @@ -487,13 +495,16 @@ impl EditingPane { &mut self, cx: &mut Cx, editing_pane_state: EditingPaneState, - room_id: OwnedRoomId, + timeline_kind: TimelineKind, ) { let EditingPaneState { event_tl_item, text_input_state } = editing_pane_state; self.mentionable_text_input(ids!(editing_content.edit_text_input)) .text_input_ref() .restore_state(cx, text_input_state); - self.info = Some(EditingPaneInfo { event_tl_item, room_id: room_id.clone() }); + self.info = Some(EditingPaneInfo { + event_tl_item, + timeline_kind, + }); self.visible = true; self.button(ids!(accept_button)).reset_hover(cx); self.button(ids!(cancel_button)).reset_hover(cx); @@ -537,11 +548,16 @@ impl EditingPaneRef { } /// See [`EditingPane::show()`]. - pub fn show(&self, cx: &mut Cx, event_tl_item: EventTimelineItem, room_id: OwnedRoomId) { + pub fn show( + &self, + cx: &mut Cx, + event_tl_item: EventTimelineItem, + timeline_kind: TimelineKind, + ) { let Some(mut inner) = self.borrow_mut() else { return; }; - inner.show(cx, event_tl_item, room_id); + inner.show(cx, event_tl_item, timeline_kind); } /// See [`EditingPane::save_state()`]. @@ -556,10 +572,10 @@ impl EditingPaneRef { &self, cx: &mut Cx, editing_pane_state: EditingPaneState, - room_id: OwnedRoomId, + timeline_kind: TimelineKind, ) { let Some(mut inner) = self.borrow_mut() else { return }; - inner.restore_state(cx, editing_pane_state, room_id); + inner.restore_state(cx, editing_pane_state, timeline_kind); } /// Hides the editing pane immediately and clears its state without animating it out. diff --git a/src/home/event_reaction_list.rs b/src/home/event_reaction_list.rs index cf1656f4..94cd3741 100644 --- a/src/home/event_reaction_list.rs +++ b/src/home/event_reaction_list.rs @@ -1,6 +1,6 @@ use crate::home::room_screen::RoomScreenTooltipActions; use crate::profile::user_profile_cache; -use crate::sliding_sync::{current_user_id, submit_async_request, MatrixRequest}; +use crate::sliding_sync::{current_user_id, submit_async_request, MatrixRequest, TimelineKind}; use indexmap::IndexMap; use makepad_widgets::*; use matrix_sdk::ruma::{OwnedRoomId, OwnedUserId}; @@ -114,21 +114,14 @@ pub struct ReactionData { #[derive(Live, LiveHook, Widget)] pub struct ReactionList { - #[redraw] - #[rust] - area: Area, - #[live] - item: Option, - #[rust] - children: Vec<(ButtonRef, ReactionData)>, - #[layout] - layout: Layout, - #[walk] - walk: Walk, - #[rust] - room_id: Option, - #[rust] - timeline_event_id: Option, + #[redraw] #[rust] area: Area, + #[live] item: Option, + #[rust] children: Vec<(ButtonRef, ReactionData)>, + #[layout] layout: Layout, + #[walk] walk: Walk, + + #[rust] timeline_kind: Option, + #[rust] timeline_event_id: Option, } impl Widget for ReactionList { fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { @@ -170,12 +163,12 @@ impl Widget for ReactionList { } // Otherwise, a primary click/press over the button should toggle the reaction. else if fue.is_primary_hit() && fue.was_tap() { - let Some(room_id) = &self.room_id else { return }; + let Some(kind) = &self.timeline_kind else { return }; let Some(timeline_event_id) = &self.timeline_event_id else { return; }; submit_async_request(MatrixRequest::ToggleReaction { - room_id: room_id.clone(), + timeline_kind: kind.clone(), timeline_event_id: timeline_event_id.clone(), reaction: reaction_data.reaction.clone(), }); @@ -254,7 +247,7 @@ impl ReactionListRef { &mut self, cx: &mut Cx, event_tl_item_reactions: Option<&ReactionsByKeyBySender>, - room_id: OwnedRoomId, + timeline_kind: TimelineKind, timeline_event_item_id: TimelineEventItemId, _id: usize, ) { @@ -279,24 +272,25 @@ impl ReactionListRef { includes_user = true; } // Prefill each reactor's user profile into the cache so the tooltip will show their display name. - let _ = user_profile_cache::with_user_profile(cx, sender.clone(), Some(&room_id), true, |_, _| { }); + let _ = user_profile_cache::with_user_profile( + cx, + sender.clone(), + Some(timeline_kind.room_id()), + true, |_, _| { }, + ); } let reaction_data = ReactionData { reaction: reaction_text.to_string(), includes_user, reaction_senders: reaction_senders.clone(), - room_id: room_id.clone(), + room_id: timeline_kind.room_id().clone(), }; let button = WidgetRef::new_from_ptr(cx, inner.item).as_button(); - button.set_text( - cx, - &format!( - "{} {}", - reaction_data.reaction, - reaction_senders.len() - ), - ); + button.set_text(cx, &format!("{} {}", + reaction_data.reaction, + reaction_senders.len() + )); let (bg_color, border_color) = if reaction_data.includes_user { (EMOJI_BG_COLOR_INCLUDE_SELF, EMOJI_BORDER_COLOR_INCLUDE_SELF) } else { @@ -313,7 +307,7 @@ impl ReactionListRef { ); inner.children.push((button, reaction_data)); } - inner.room_id = Some(room_id); + inner.timeline_kind = Some(timeline_kind); inner.timeline_event_id = Some(timeline_event_item_id); } diff --git a/src/home/main_desktop_ui.rs b/src/home/main_desktop_ui.rs index 3b331da6..7a88771e 100644 --- a/src/home/main_desktop_ui.rs +++ b/src/home/main_desktop_ui.rs @@ -137,51 +137,40 @@ impl MainDesktopUI { /// Focuses on a room if it is already open, otherwise creates a new tab for the room. fn focus_or_create_tab(&mut self, cx: &mut Cx, room: SelectedRoom) { // Do nothing if the room to select is already created and focused. - if self.most_recently_selected_room.as_ref().is_some_and(|r| r == &room) { + if self.most_recently_selected_room.as_ref().is_some_and(|sr| sr == &room) { return; } let dock = self.view.dock(ids!(dock)); // If the room is already open, select (jump to) its existing tab - let room_id_as_live_id = LiveId::from_str(room.room_id().as_str()); - if self.open_rooms.contains_key(&room_id_as_live_id) { - dock.select_tab(cx, room_id_as_live_id); + let room_tab_id = room.tab_id(); + if self.open_rooms.contains_key(&room_tab_id) { + dock.select_tab(cx, room_tab_id); self.most_recently_selected_room = Some(room); return; } // Create a new tab for the room - let (kind, name) = match &room { - SelectedRoom::JoinedRoom { room_name_id } => ( - id!(room_screen), - room_name_id.to_string(), - ), - SelectedRoom::InvitedRoom { room_name_id } => ( - id!(invite_screen), - room_name_id.to_string(), - ), - SelectedRoom::Space { space_name_id } => ( - id!(space_lobby_screen), - format!("[Space] {}", space_name_id), - ), + let kind = match &room { + SelectedRoom::JoinedRoom { .. } + | SelectedRoom::Thread { .. } => id!(room_screen), + SelectedRoom::InvitedRoom { .. } => id!(invite_screen), + SelectedRoom::Space { .. } => id!(space_lobby_screen), }; // Insert the tab after the currently-selected room's tab, if possible. // Otherwise, insert it after the home tab, which should always exist. let (tab_bar, insert_after) = self.most_recently_selected_room.as_ref() - .and_then(|curr_room| { - let curr_room_id = LiveId::from_str(curr_room.room_id().as_str()); - dock.find_tab_bar_of_tab(curr_room_id) - }) + .and_then(|curr_room| dock.find_tab_bar_of_tab(curr_room.tab_id())) .unwrap_or_else(|| dock.find_tab_bar_of_tab(id!(home_tab)).unwrap()); let new_tab_widget = dock.create_and_select_tab( cx, tab_bar, - room_id_as_live_id, + room_tab_id, kind, - name, + room.display_name(), id!(CloseableTab), Some(insert_after), ); @@ -194,6 +183,14 @@ impl MainDesktopUI { new_widget.as_room_screen().set_displayed_room( cx, room_name_id, + None, + ); + } + SelectedRoom::Thread { room_name_id, thread_root_event_id } => { + new_widget.as_room_screen().set_displayed_room( + cx, + room_name_id, + Some(thread_root_event_id.clone()), ); } SelectedRoom::InvitedRoom { room_name_id } => { @@ -214,7 +211,7 @@ impl MainDesktopUI { error!("BUG: failed to create tab for {room:?}"); } - self.open_rooms.insert(room_id_as_live_id, room.clone()); + self.open_rooms.insert(room_tab_id, room.clone()); self.most_recently_selected_room = Some(room); } @@ -291,7 +288,7 @@ impl MainDesktopUI { // Set the info to be displayed in the newly-replaced RoomScreen.. new_widget .as_room_screen() - .set_displayed_room(cx, room_name_id); + .set_displayed_room(cx, room_name_id, None); // Go through all existing `SelectedRoom` instances and replace the // `SelectedRoom::InvitedRoom`s with `SelectedRoom::JoinedRoom`s. @@ -365,6 +362,7 @@ impl MainDesktopUI { widget.as_room_screen().set_displayed_room( cx, room_name_id, + None, ); } Some(SelectedRoom::InvitedRoom { room_name_id }) => { @@ -379,6 +377,13 @@ impl MainDesktopUI { space_name_id, ); } + Some(SelectedRoom::Thread { room_name_id, thread_root_event_id }) => { + widget.as_room_screen().set_displayed_room( + cx, + room_name_id, + Some(thread_root_event_id.clone()), + ); + } None => { } } } diff --git a/src/home/main_mobile_ui.rs b/src/home/main_mobile_ui.rs index 3709f1ff..dec5b3d3 100644 --- a/src/home/main_mobile_ui.rs +++ b/src/home/main_mobile_ui.rs @@ -89,7 +89,7 @@ impl Widget for MainMobileUI { // Get a reference to the `RoomScreen` widget and tell it which room's data to show. self.view .room_screen(ids!(room_screen)) - .set_displayed_room(cx, room_name_id); + .set_displayed_room(cx, room_name_id, None); } Some(SelectedRoom::InvitedRoom { room_name_id }) => { show_welcome = false; @@ -109,6 +109,15 @@ impl Widget for MainMobileUI { .space_lobby_screen(ids!(space_lobby_screen)) .set_displayed_space(cx, space_name_id); } + Some(SelectedRoom::Thread { room_name_id, thread_root_event_id }) => { + show_welcome = false; + show_room = true; + show_invite = false; + show_space_lobby = false; + self.view + .room_screen(ids!(room_screen)) + .set_displayed_room(cx, room_name_id, Some(thread_root_event_id.clone())); + } None => { show_welcome = true; show_room = false; diff --git a/src/home/new_message_context_menu.rs b/src/home/new_message_context_menu.rs index 1efd2ddc..d78fbc26 100644 --- a/src/home/new_message_context_menu.rs +++ b/src/home/new_message_context_menu.rs @@ -283,6 +283,9 @@ pub struct MessageDetails { /// The event ID of the message that this message is related to, if any, /// such as the replied-to message. pub related_event_id: Option, + /// The event ID of the thread root if this message is part of a thread + /// (or if this message is itself the thread root). + pub thread_root_event_id: Option, /// The widget ID of the RoomScreen that contains this message. pub room_screen_widget_uid: WidgetUid, /// Whether this message should be highlighted, i.e., diff --git a/src/home/room_read_receipt.rs b/src/home/room_read_receipt.rs index 3c7a2752..60fb7557 100644 --- a/src/home/room_read_receipt.rs +++ b/src/home/room_read_receipt.rs @@ -1,10 +1,11 @@ use crate::home::room_screen::RoomScreenTooltipActions; use crate::profile::user_profile_cache::get_user_display_name_for_room; use crate::shared::avatar::{AvatarRef, AvatarWidgetRefExt}; +use crate::sliding_sync::TimelineKind; use crate::utils::human_readable_list; use indexmap::IndexMap; use makepad_widgets::*; -use matrix_sdk::ruma::{events::receipt::Receipt, EventId, OwnedUserId, OwnedRoomId, RoomId}; +use matrix_sdk::ruma::{events::receipt::Receipt, EventId, OwnedUserId, OwnedRoomId}; use matrix_sdk_ui::timeline::EventTimelineItem; use std::cmp; @@ -159,7 +160,7 @@ impl AvatarRow { pub fn set_avatar_row( &mut self, cx: &mut Cx, - room_id: &RoomId, + timeline_kind: &TimelineKind, event_id: Option<&EventId>, receipts_map: &IndexMap, ) { @@ -180,7 +181,7 @@ impl AvatarRow { if !*drawn { let (_, drawn_status) = avatar_ref.set_avatar_and_get_username( cx, - room_id, + timeline_kind, user_id, None, event_id, @@ -212,12 +213,12 @@ impl AvatarRowRef { pub fn set_avatar_row( &mut self, cx: &mut Cx, - room_id: &RoomId, + timeline_kind: &TimelineKind, event_id: Option<&EventId>, receipts_map: &IndexMap, ) { if let Some(ref mut inner) = self.borrow_mut() { - inner.set_avatar_row(cx, room_id, event_id, receipts_map); + inner.set_avatar_row(cx, timeline_kind, event_id, receipts_map); } } } @@ -231,12 +232,12 @@ impl AvatarRowRef { pub fn populate_read_receipts( item: &WidgetRef, cx: &mut Cx, - room_id: &RoomId, + timeline_kind: &TimelineKind, event_tl_item: &EventTimelineItem, ) { item.avatar_row(ids!(avatar_row)).set_avatar_row( cx, - room_id, + timeline_kind, event_tl_item.event_id(), event_tl_item.read_receipts(), ); diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 75cb7a5a..08d88ab9 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -1,9 +1,10 @@ -//! A room screen is the UI view that displays a single Room's timeline of events/messages -//! along with a message input bar at the bottom. +//! The `RoomScreen` widget is the UI view that displays a single room or thread's timeline +//! of events (messages,state changes, etc.), along with an input bar at the bottom. -use std::{borrow::Cow, cell::RefCell, collections::BTreeMap, ops::{DerefMut, Range}, sync::Arc}; +use std::{borrow::Cow, cell::RefCell, ops::{DerefMut, Range}, sync::Arc}; use bytesize::ByteSize; +use hashbrown::{HashMap, HashSet}; use imbl::Vector; use makepad_widgets::{image_cache::ImageBuffer, *}; use matrix_sdk::{ @@ -22,10 +23,10 @@ use matrix_sdk::{ use matrix_sdk_ui::timeline::{ self, EmbeddedEvent, EncryptedMessage, EventTimelineItem, InReplyToDetails, MemberProfileChange, MembershipChange, MsgLikeContent, MsgLikeKind, OtherMessageLike, PollState, RoomMembershipChange, TimelineDetails, TimelineEventItemId, TimelineItem, TimelineItemContent, TimelineItemKind, VirtualTimelineItem }; -use ruma::{OwnedUserId, events::{AnySyncMessageLikeEvent, AnySyncTimelineEvent, SyncMessageLikeEvent}}; +use ruma::{OwnedUserId, api::client::receipt::create_receipt::v3::ReceiptType, events::{AnySyncMessageLikeEvent, AnySyncTimelineEvent, SyncMessageLikeEvent}, owned_room_id}; use crate::{ - app::{AppStateAction, ConfirmDeleteAction}, avatar_cache, event_preview::{plaintext_body_of_timeline_item, text_preview_of_encrypted_message, text_preview_of_member_profile_change, text_preview_of_other_message_like, text_preview_of_other_state, text_preview_of_room_membership_change, text_preview_of_timeline_item}, home::{edited_indicator::EditedIndicatorWidgetRefExt, link_preview::{LinkPreviewCache, LinkPreviewRef, LinkPreviewWidgetRefExt}, loading_pane::{LoadingPaneState, LoadingPaneWidgetExt}, room_image_viewer::{get_image_name_and_filesize, populate_matrix_image_modal}, rooms_list::RoomsListRef, tombstone_footer::SuccessorRoomDetails}, media_cache::{MediaCache, MediaCacheEntry}, profile::{ + app::{AppStateAction, ConfirmDeleteAction, SelectedRoom}, avatar_cache, event_preview::{plaintext_body_of_timeline_item, text_preview_of_encrypted_message, text_preview_of_member_profile_change, text_preview_of_other_message_like, text_preview_of_other_state, text_preview_of_room_membership_change, text_preview_of_timeline_item}, home::{edited_indicator::EditedIndicatorWidgetRefExt, link_preview::{LinkPreviewCache, LinkPreviewRef, LinkPreviewWidgetRefExt}, loading_pane::{LoadingPaneState, LoadingPaneWidgetExt}, room_image_viewer::{get_image_name_and_filesize, populate_matrix_image_modal}, rooms_list::{RoomsListAction, RoomsListRef}, tombstone_footer::SuccessorRoomDetails}, media_cache::{MediaCache, MediaCacheEntry}, profile::{ user_profile::{ShowUserProfileAction, UserProfile, UserProfileAndRoomId, UserProfilePaneInfo, UserProfileSlidingPaneRef, UserProfileSlidingPaneWidgetExt}, user_profile_cache, }, @@ -33,7 +34,7 @@ use crate::{ shared::{ avatar::{AvatarState, AvatarWidgetRefExt}, callout_tooltip::{CalloutTooltipOptions, TooltipAction, TooltipPosition}, confirmation_modal::ConfirmationModalContent, html_or_plaintext::{HtmlOrPlaintextRef, HtmlOrPlaintextWidgetRefExt, RobrixHtmlLinkAction}, image_viewer::{ImageViewerAction, ImageViewerMetaData, LoadState}, jump_to_bottom_button::{JumpToBottomButtonWidgetExt, UnreadMessageCount}, popup_list::{PopupKind, enqueue_popup_notification}, restore_status_view::RestoreStatusViewWidgetExt, styles::*, text_or_image::{TextOrImageAction, TextOrImageRef, TextOrImageWidgetRefExt}, timestamp::TimestampWidgetRefExt }, - sliding_sync::{BackwardsPaginateUntilEventRequest, MatrixRequest, PaginationDirection, TimelineEndpoints, TimelineRequestSender, UserPowerLevels, get_client, submit_async_request, take_timeline_endpoints}, utils::{self, ImageFormat, MEDIA_THUMBNAIL_FORMAT, RoomNameId, unix_time_millis_to_datetime} + sliding_sync::{BackwardsPaginateUntilEventRequest, MatrixRequest, PaginationDirection, TimelineEndpoints, TimelineKind, TimelineRequestSender, UserPowerLevels, get_client, submit_async_request, take_timeline_endpoints}, utils::{self, ImageFormat, MEDIA_THUMBNAIL_FORMAT, RoomNameId, unix_time_millis_to_datetime} }; use crate::home::event_reaction_list::ReactionListWidgetRefExt; use crate::home::room_read_receipt::AvatarRowWidgetRefExt; @@ -56,6 +57,12 @@ const BLURHASH_IMAGE_MAX_SIZE: u32 = 500; static UNNAMED_ROOM: &str = "Unnamed Room"; +/// #FFF4E5 +const COLOR_THREAD_SUMMARY_BG: Vec4 = vec4(1.0, 0.957, 0.898, 1.0); +/// #FFEACC +const COLOR_THREAD_SUMMARY_BG_HOVER: Vec4 = vec4(1.0, 0.918, 0.8, 1.0); + + live_design! { use link::theme::*; use link::shaders::*; @@ -88,10 +95,58 @@ live_design! { REACTION_TEXT_COLOR = #4c00b0 + COLOR_THREAD_SUMMARY_BG = #FFF4E5 + COLOR_THREAD_SUMMARY_BG_HOVER = #FFEACC + COLOR_THREAD_SUMMARY_BORDER = #E8C99A + COLOR_THREAD_SUMMARY_REPLY_COUNT = #A35A00 // An empty view that takes up no space in the portal list. Empty = { } + // A summary at the bottom of a message that is the root of a thread. + ThreadRootSummary = { + visible: false + width: Fill, + height: Fit + flow: Right, + align: {x: 0.0, y: 0.5} + spacing: 5.0 + margin: { top: 5.0 } + padding: 12, + cursor: Hand + + show_bg: true + draw_bg: { + color: (COLOR_THREAD_SUMMARY_BG) + border_radius: 4.0 + border_size: 1.5 + border_color: (COLOR_THREAD_SUMMARY_BORDER) + } + + thread_summary_count =