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 =