Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize};
use crate::{
avatar_cache::clear_avatar_cache,
home::{
main_desktop_ui::MainDesktopUiAction, navigation_tab_bar::{NavigationBarAction, SelectedTab}, new_message_context_menu::NewMessageContextMenuWidgetRefExt, room_screen::{InviteAction, MessageAction, clear_timeline_states}, rooms_list::{RoomsListAction, RoomsListRef, RoomsListUpdate, clear_all_invited_rooms, enqueue_rooms_list_update}
main_desktop_ui::MainDesktopUiAction, navigation_tab_bar::{NavigationBarAction, SelectedTab}, new_message_context_menu::NewMessageContextMenuWidgetRefExt, room_screen::{InviteAction, MessageAction, clear_timeline_states}, rooms_list::{RoomsListAction, RoomsListRef, RoomsListUpdate, clear_all_invited_rooms, enqueue_rooms_list_update}, room_context_menu::RoomContextMenuWidgetRefExt
},
join_leave_room_modal::{
JoinLeaveModalKind, JoinLeaveRoomModalAction, JoinLeaveRoomModalWidgetRefExt
Expand Down Expand Up @@ -42,6 +42,7 @@ live_design! {
use crate::shared::confirmation_modal::*;
use crate::shared::popup_list::*;
use crate::home::new_message_context_menu::*;
use crate::home::room_context_menu::*;
use crate::shared::callout_tooltip::CalloutTooltip;
use crate::shared::image_viewer::ImageViewer;
use link::tsp_link::TspVerificationModal;
Expand Down Expand Up @@ -111,6 +112,7 @@ live_design! {
// Context menus should be shown in front of other UI elements,
// but behind verification modals.
new_message_context_menu = <NewMessageContextMenu> { }
room_context_menu = <RoomContextMenu> { }

// A modal to confirm sending out an invite to a room.
invite_confirmation_modal = <Modal> {
Expand Down Expand Up @@ -318,6 +320,21 @@ impl MatchEvent for App {
continue;
}

if let RoomsListAction::RightClicked { details, pos } = action.as_widget_action().cast() {
self.ui.callout_tooltip(ids!(app_tooltip)).hide(cx);
let room_context_menu = self.ui.room_context_menu(ids!(room_context_menu));
let expected_dimensions = room_context_menu.show(cx, details);
// Ensure the context menu does not spill over the window's bounds.
let rect = self.ui.window(ids!(main_window)).area().rect(cx);
let pos_x = min(pos.x, rect.size.x - expected_dimensions.x);
let pos_y = min(pos.y, rect.size.y - expected_dimensions.y);
room_context_menu.apply_over(cx, live! {
main_content = { margin: { left: (pos_x), top: (pos_y) } }
});
self.ui.redraw(cx);
continue;
}

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 = selected_room.room_name().to_string();
Expand Down
1 change: 1 addition & 0 deletions src/home/main_desktop_ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,7 @@ impl WidgetMatchEvent for MainDesktopUI {
RoomsListAction::InviteAccepted { room_name_id } => {
self.replace_invite_with_joined_room(cx, scope, room_name_id);
}
RoomsListAction::RightClicked { .. } => {}
RoomsListAction::None => { }
}

Expand Down
1 change: 1 addition & 0 deletions src/home/main_mobile_ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ impl Widget for MainMobileUI {
RoomsListAction::InviteAccepted { room_name_id: room_name } => {
cx.action(AppStateAction::UpgradedInviteToJoinedRoom(room_name.room_id().clone()));
}
RoomsListAction::RightClicked { .. } => {}
RoomsListAction::None => {}
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/home/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ pub mod navigation_tab_bar;
pub mod welcome_screen;
pub mod event_reaction_list;
pub mod new_message_context_menu;
pub mod room_context_menu;
pub mod link_preview;
pub mod room_image_viewer;

Expand All @@ -40,6 +41,7 @@ pub fn live_design(cx: &mut Cx) {
edited_indicator::live_design(cx);
editing_pane::live_design(cx);
new_message_context_menu::live_design(cx);
room_context_menu::live_design(cx);
invite_screen::live_design(cx);
tombstone_footer::live_design(cx);
room_screen::live_design(cx);
Expand Down
320 changes: 320 additions & 0 deletions src/home/room_context_menu.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,320 @@
//! A context menu that appears when the user right-clicks
//! or long-presses on a room in the room list.

use makepad_widgets::*;
use matrix_sdk::ruma::OwnedRoomId;

const BUTTON_HEIGHT: f64 = 35.0;
const MENU_WIDTH: f64 = 215.0;

live_design! {
use link::theme::*;
use link::shaders::*;
use link::widgets::*;

use crate::shared::helpers::*;
use crate::shared::styles::*;
use crate::shared::icon_button::*;

BUTTON_HEIGHT = 35
MENU_WIDTH = 215

ContextMenuButton = <RobrixIconButton> {
height: (BUTTON_HEIGHT)
width: Fill,
margin: 0,
icon_walk: {width: 16, height: 16, margin: {right: 3}}
}

pub RoomContextMenu = {{RoomContextMenu}} {
visible: false,
flow: Overlay,
width: Fill,
height: Fill,
cursor: Default,
align: {x: 0, y: 0}

show_bg: true
draw_bg: {
fn pixel(self) -> vec4 {
return vec4(0., 0., 0., 0.3)
}
}

main_content = <RoundedView> {
flow: Down
width: (MENU_WIDTH),
height: Fit,
padding: 5
spacing: 0,
align: {x: 0, y: 0}

show_bg: true
draw_bg: {
color: #fff
border_radius: 5.0
border_size: 0.5
border_color: #888
}

mark_unread_button = <ContextMenuButton> {
draw_icon: { svg_file: (ICON_CHECKMARK) }
text: "Mark as Read"
}

favorite_button = <ContextMenuButton> {
draw_icon: { svg_file: (ICON_PIN) }
text: "Favorite"
}

priority_button = <ContextMenuButton> {
draw_icon: { svg_file: (ICON_TOMBSTONE) }
text: "Set Low Priority"
}

share_button = <ContextMenuButton> {
draw_icon: { svg_file: (ICON_LINK) }
text: "Copy Link to Room"
}

divider1 = <LineH> {
margin: {top: 3, bottom: 3}
width: Fill,
}

settings_button = <ContextMenuButton> {
draw_icon: { svg_file: (ICON_SETTINGS) }
text: "Settings"
}

notifications_button = <ContextMenuButton> {
// TODO: use a proper bell icon
draw_icon: { svg_file: (ICON_INFO) }
text: "Notifications"
}

invite_button = <ContextMenuButton> {
draw_icon: { svg_file: (ICON_ADD_USER) }
text: "Invite"
}

divider2 = <LineH> {
margin: {top: 3, bottom: 3}
width: Fill,
}

leave_button = <ContextMenuButton> {
draw_icon: {
svg_file: (ICON_LOGOUT)
color: (COLOR_FG_DANGER_RED),
}
draw_bg: {
border_color: (COLOR_FG_DANGER_RED),
color: (COLOR_BG_DANGER_RED)
}
text: "Leave Room"
draw_text:{
color: (COLOR_FG_DANGER_RED),
}
}
}
}
}

use crate::{sliding_sync::{MatrixRequest, submit_async_request}, utils::RoomNameId};

#[derive(Clone, Debug)]
pub struct RoomContextMenuDetails {
pub room_id: OwnedRoomId,
pub room_name_id: RoomNameId,
pub is_favorite: bool,
pub is_low_priority: bool,
pub is_unread: bool,
}

#[derive(Clone, DefaultNone, Debug)]
pub enum RoomContextMenuAction {
SetFavorite(OwnedRoomId, bool),
SetLowPriority(OwnedRoomId, bool),
Notifications(OwnedRoomId),
Invite(OwnedRoomId),
CopyLink(OwnedRoomId),
// LeaveRoom is handled directly by emitting JoinLeaveRoomModalAction
OpenSettings(OwnedRoomId),
None,
}

#[derive(Live, LiveHook, Widget)]
pub struct RoomContextMenu {
#[deref] view: View,
#[rust] details: Option<RoomContextMenuDetails>,
}

impl Widget for RoomContextMenu {
fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep {
if self.details.is_none() {
self.visible = false;
};
self.view.draw_walk(cx, scope, walk)
}

fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
if !self.visible { return; }
self.view.handle_event(cx, event, scope);

// Close logic similar to NewMessageContextMenu
let area = self.view.area();
let close_menu = {
event.back_pressed()
|| match event.hits_with_capture_overload(cx, area, true) {
Hit::KeyUp(key) => key.key_code == KeyCode::Escape,
Hit::FingerUp(fue) if fue.is_over => {
!self.view(ids!(main_content)).area().rect(cx).contains(fue.abs)
}
Hit::FingerScroll(_) => true,
_ => false,
}
};

if close_menu {
self.close(cx);
return;
}

self.widget_match_event(cx, event, scope);
}
}

impl WidgetMatchEvent for RoomContextMenu {
fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions, scope: &mut Scope) {
let Some(details) = self.details.as_ref() else { return };
let mut action_to_dispatch = RoomContextMenuAction::None;
let mut close_menu = false;

if self.button(ids!(mark_unread_button)).clicked(actions) {
// Toggle unread status
submit_async_request(MatrixRequest::SetUnreadFlag {
room_id: details.room_id.clone(),
is_unread: !details.is_unread,
});
close_menu = true;
}
else if self.button(ids!(favorite_button)).clicked(actions) {
action_to_dispatch = RoomContextMenuAction::SetFavorite(details.room_id.clone(), !details.is_favorite);
close_menu = true;
}
else if self.button(ids!(priority_button)).clicked(actions) {
action_to_dispatch = RoomContextMenuAction::SetLowPriority(details.room_id.clone(), !details.is_low_priority);
close_menu = true;
}
else if self.button(ids!(share_button)).clicked(actions) {
action_to_dispatch = RoomContextMenuAction::CopyLink(details.room_id.clone());
close_menu = true;
}
else if self.button(ids!(settings_button)).clicked(actions) {
action_to_dispatch = RoomContextMenuAction::OpenSettings(details.room_id.clone());
close_menu = true;
}
else if self.button(ids!(notifications_button)).clicked(actions) {
action_to_dispatch = RoomContextMenuAction::Notifications(details.room_id.clone());
close_menu = true;
}
else if self.button(ids!(invite_button)).clicked(actions) {
action_to_dispatch = RoomContextMenuAction::Invite(details.room_id.clone());
close_menu = true;
}
else if self.button(ids!(leave_button)).clicked(actions) {
use crate::join_leave_room_modal::{JoinLeaveRoomModalAction, JoinLeaveModalKind};
use crate::room::BasicRoomDetails;
let room_details = BasicRoomDetails::Name(details.room_name_id.clone());
cx.action(JoinLeaveRoomModalAction::Open {
kind: JoinLeaveModalKind::LeaveRoom(room_details),
show_tip: false,
});
close_menu = true;
}

if !matches!(action_to_dispatch, RoomContextMenuAction::None) {
cx.widget_action(self.widget_uid(), &scope.path, action_to_dispatch);
}

if close_menu {
self.close(cx);
}
}
}

impl RoomContextMenu {
pub fn is_currently_shown(&self, _cx: &mut Cx) -> bool {
self.visible
}

pub fn show(&mut self, cx: &mut Cx, details: RoomContextMenuDetails) -> DVec2 {
self.details = Some(details.clone());
self.visible = true;
cx.set_key_focus(self.view.area());

let height = self.update_buttons(cx, &details);
dvec2(MENU_WIDTH, height)
}

fn update_buttons(&mut self, cx: &mut Cx, details: &RoomContextMenuDetails) -> f64 {
let mark_read_btn = self.button(ids!(mark_unread_button));
if details.is_unread {
mark_read_btn.set_text(cx, "Mark as Read");
// mark_read_btn.draw_icon.svg_file = ...; // Optional: change icon
} else {
mark_read_btn.set_text(cx, "Mark as Unread");
}

let fav_btn = self.button(ids!(favorite_button));
if details.is_favorite {
fav_btn.set_text(cx, "Unfavorite");
} else {
fav_btn.set_text(cx, "Favorite");
}

let priority_btn = self.button(ids!(priority_button));
if details.is_low_priority {
priority_btn.set_text(cx, "Set Standard Priority");
} else {
priority_btn.set_text(cx, "Set Low Priority");
}

// Reset hover states
mark_read_btn.reset_hover(cx);
fav_btn.reset_hover(cx);
priority_btn.reset_hover(cx);
self.button(ids!(share_button)).reset_hover(cx);
self.button(ids!(share_button)).reset_hover(cx);
self.button(ids!(settings_button)).reset_hover(cx);
self.button(ids!(notifications_button)).reset_hover(cx);
self.button(ids!(invite_button)).reset_hover(cx);
self.button(ids!(leave_button)).reset_hover(cx);

self.redraw(cx);

// Calculate height (rudimentary) - sum of visible buttons + padding
// 8 buttons * 35.0 + 2 dividers * ~10.0 + padding
(8.0 * BUTTON_HEIGHT) + 20.0 + 10.0 // approx
}

fn close(&mut self, cx: &mut Cx) {
self.visible = false;
self.details = None;
cx.revert_key_focus();
self.redraw(cx);
}
}

impl RoomContextMenuRef {
pub fn is_currently_shown(&self, cx: &mut Cx) -> bool {
let Some(inner) = self.borrow() else { return false };
inner.is_currently_shown(cx)
}

pub fn show(&self, cx: &mut Cx, details: RoomContextMenuDetails) -> DVec2 {
let Some(mut inner) = self.borrow_mut() else { return DVec2::default()};
inner.show(cx, details)
}
}
Loading