Skip to content
Open
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
9 changes: 9 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,12 @@
[target.'cfg(all())']
rustflags = ["--cfg", "ruma_identifiers_storage=\"Arc\""]

## LiveKit/libwebrtc requires -ObjC linker flag on macOS
[target.'cfg(target_os = "macos")']
rustflags = ["-C", "link-args=-Wl,-ObjC"]

[target.x86_64-pc-windows-msvc]
rustflags = ["-C", "target-feature=-crt-static"]

[target.aarch64-pc-windows-msvc]
rustflags = ["-C", "target-feature=-crt-static"]
7 changes: 7 additions & 0 deletions .github/workflows/builds.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,16 @@ jobs:
with:
key: macos-${{ matrix.arch }}-build-${{ hashFiles('Cargo.lock') }}

- name: Install LLVM
run: brew install llvm

- name: Build
env:
RUSTFLAGS: "-D warnings"
# Use Homebrew LLVM for C++ compilation to avoid cxx contiguous_range
# static_assert issue with Apple's libc++
CXX: ${{ matrix.arch == 'arm64' && '/opt/homebrew/opt/llvm/bin/clang++' || '/usr/local/opt/llvm/bin/clang++' }}
CC: ${{ matrix.arch == 'arm64' && '/opt/homebrew/opt/llvm/bin/clang' || '/usr/local/opt/llvm/bin/clang' }}
run: |
cargo build --profile fast

Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ jobs:
runs-on: macos-14 ## avoids having to install Linux deps
steps:
- uses: actions/checkout@v4
- name: Select Xcode version
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '16.1.0'
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
Expand Down
9 changes: 9 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ matrix-sdk-ui = { git = "https://github.com/project-robius/matrix-rust-sdk", bra
ruma = { git = "https://github.com/ruma/ruma", rev = "a0acf4187a7c7557d145db54bcb23b01f6295ce7", features = [
"compat-optional",
"compat-unset-avatar",
"unstable-msc3401",
] }
rand = "0.8.5"
rangemap = "1.5.0"
Expand All @@ -85,6 +86,14 @@ url = "2.5.0"
rfd = "0.15"
cargo-packager-updater = "0.2"
semver = "1"
## nokhwa camera capture - only for desktop platforms (core-video-sys doesn't work on iOS)
nokhwa = { version = "0.10", features = ["input-native"] }

## LiveKit WebRTC SDK - only for desktop platforms (macOS, Linux)
## Windows is excluded due to MSVC runtime library mismatch with webrtc-sys
## rustls-tls-native-roots is required for WSS connections
[target.'cfg(all(not(any(target_os = "android", target_os = "ios", target_os = "windows"))))'.dependencies]
livekit = { version = "0.7", features = ["rustls-tls-native-roots"] }

## Dependencies for TSP support.
## Commit "f0bc4625dcd729e07e4a36257df2f1d94c81cef4" is the most recent one without the invalid change to pin serde to 1.0.219.
Expand Down
1 change: 1 addition & 0 deletions makepad
Submodule makepad added at 2cff94
6 changes: 6 additions & 0 deletions resources/icons/microphone.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions resources/icons/video.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
150 changes: 117 additions & 33 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use std::{cell::RefCell, collections::HashMap};
use makepad_widgets::*;
use matrix_sdk::{RoomState, ruma::{OwnedEventId, OwnedMxcUri, OwnedRoomId, OwnedUserId, RoomId, UserId, events::room::message::RoomMessageEventContent}};
use serde::{Deserialize, Serialize};
use makepad_widgets::makepad_platform::permission::Permission;
use crate::{
avatar_cache::{self, AvatarCacheEntry, clear_avatar_cache}, home::{
add_room::{CreateRoomModalAction, CreateRoomModalWidgetRefExt},
Expand All @@ -18,7 +19,8 @@ use crate::{
}, login::login_screen::LoginAction, logout::logout_confirm_modal::{LogoutAction, LogoutConfirmModalAction, LogoutConfirmModalWidgetRefExt}, persistence, profile::{user_profile::UserProfile, user_profile_cache::clear_user_profile_cache}, room::{BasicRoomDetails, FetchedRoomAvatar}, shared::{avatar::{AvatarState, AvatarWidgetRefExt}, confirmation_modal::{ConfirmationModalContent, ConfirmationModalWidgetRefExt}, file_upload_modal::{FilePreviewerAction, FileUploadModalWidgetRefExt}, image_viewer::{ImageViewerAction, LoadState}, popup_list::{PopupKind, enqueue_popup_notification}, room_filter_input_bar::FilterAction}, sliding_sync::{DirectMessageRoomAction, MatrixRequest, RemoteDirectorySearchKind, RemoteDirectorySearchResult, TimelineKind, AccountSwitchAction, current_user_id, submit_async_request, get_timeline_update_sender}, utils::RoomNameId, verification::VerificationAction, verification_modal::{
VerificationModalAction,
VerificationModalWidgetRefExt,
}
},
voip::{VoipGlobalState, VoipAction, PipVoipOverlayWidgetRefExt},
};

script_mod! {
Expand Down Expand Up @@ -311,6 +313,9 @@ script_mod! {
}
}

// PiP overlay for VoIP calls (shown when switching away from active call)
pip_voip_overlay := PipVoipOverlay {}

PopupList {}

// Tooltips must be shown in front of all other UI elements,
Expand Down Expand Up @@ -630,19 +635,9 @@ impl MatchEvent for App {
log!("App::Startup: initializing TSP (Trust Spanning Protocol) module.");
crate::tsp::tsp_init(_tokio_rt_handle).unwrap();
}
}

fn handle_signal(&mut self, cx: &mut Cx) {
avatar_cache::process_avatar_updates(cx);
self.refresh_room_filter_modal_result_buttons(cx);
}

fn handle_timer(&mut self, cx: &mut Cx, event: &TimerEvent) {
if self.room_filter_debounce_timer.is_timer(event).is_some() {
self.room_filter_debounce_timer = Timer::empty();
let keywords = std::mem::take(&mut self.pending_room_filter_keywords);
self.update_room_filter_modal_results(cx, &keywords);
}
// Initialize VoIP global state (camera permissions, video inputs)
VoipGlobalState::initialize(cx);
}

fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) {
Expand Down Expand Up @@ -1007,6 +1002,42 @@ impl MatchEvent for App {
_ => {}
}

// Handle VoIP PiP overlay actions
match action.downcast_ref() {
Some(VoipAction::ShowPip { room_id }) => {
log!("App: VoipAction::ShowPip received for room {}", room_id);
self.ui.pip_voip_overlay(cx, ids!(pip_voip_overlay)).show(cx, room_id.clone());
continue;
}
Some(VoipAction::HidePip) => {
log!("App: VoipAction::HidePip received");
self.ui.pip_voip_overlay(cx, ids!(pip_voip_overlay)).hide(cx);
continue;
}
Some(VoipAction::ReturnToVoipTab { room_id }) => {
log!("App: VoipAction::ReturnToVoipTab received for room {}", room_id);
// Hide the PiP overlay
self.ui.pip_voip_overlay(cx, ids!(pip_voip_overlay)).hide(cx);
// Navigate back to the VoIP tab by emitting a RoomsListAction::Selected
// We need to look up the room name from RoomsList
if let Some(room_name_id) = cx.get_global::<RoomsListRef>().get_room_name(room_id) {
cx.widget_action(
self.ui.widget_uid(),
RoomsListAction::Selected(SelectedRoom::Voip { room_name_id }),
);
}
self.ui.redraw(cx);
continue;
}
Some(VoipAction::PipHangup { room_id }) => {
log!("App: VoipAction::PipHangup received for room {}", room_id);
// Hide the PiP overlay - the VoipScreen will handle the actual hangup
self.ui.pip_voip_overlay(cx, ids!(pip_voip_overlay)).hide(cx);
// The action will continue to propagate to VoipScreen
}
_ => {}
}

// When a stack navigation pop is initiated (back button pressed),
// pop the mobile nav stack so it stays in sync with StackNavigation.
if let StackNavigationAction::Pop = action.as_widget_action().cast() {
Expand Down Expand Up @@ -1044,6 +1075,10 @@ impl MatchEvent for App {
self.app_state.logged_in = logged_in_actual;
// Initialize the global translation config so RoomInputBar can access it.
crate::room::translation::set_global_config(&self.app_state.translation);

// Restore VoIP token state to global state for caching
VoipGlobalState::restore_token_state(cx, self.app_state.voip_tokens.clone());

cx.action(MainDesktopUiAction::LoadDockFromAppState);
continue;
}
Expand Down Expand Up @@ -1458,6 +1493,8 @@ impl AppMain for App {
crate::join_leave_room_modal::script_mod(vm);
crate::verification_modal::script_mod(vm);
crate::profile::script_mod(vm);
crate::voip::voip_screen::script_mod(vm);
crate::voip::pip_overlay::script_mod(vm);
crate::home::script_mod(vm);
crate::login::script_mod(vm);
crate::logout::script_mod(vm);
Expand All @@ -1472,6 +1509,8 @@ impl AppMain for App {
error!("Failed to save window state. Error: {e}");
}
if let Some(user_id) = current_user_id() {
// Get the latest VoIP token state from global state before saving
self.app_state.voip_tokens = VoipGlobalState::get_token_state(cx);
let app_state = self.app_state.clone();
if let Err(e) = persistence::save_app_state(app_state, user_id) {
error!("Failed to save app state. Error: {e}");
Expand All @@ -1498,6 +1537,17 @@ impl AppMain for App {
}
}

// Handle VoIP-related events at app level (before VoipScreen is shown)
match event {
Event::PermissionResult(result) if result.permission == Permission::Camera => {
VoipGlobalState::handle_permission_result(cx, result.status);
}
Event::VideoInputs(ev) => {
VoipGlobalState::handle_video_inputs(cx, ev);
}
_ => {}
}

// Forward events to the MatchEvent trait implementation.
self.match_event(cx, event);
let scope = &mut Scope::with_data(&mut self.app_state);
Expand Down Expand Up @@ -1961,6 +2011,12 @@ impl App {
.set_displayed_space(cx, space_name_id);
id!(space_lobby_view)
}
SelectedRoom::Voip { room_name_id } => {
// VoIP uses RoomScreen with VoIP as main content (no timeline)
let room_screen = self.ui.room_screen(cx, ids!(room_screen_0));
room_screen.set_voip_visible(cx, true, Some(room_name_id.room_id().clone()));
id!(room_view_0)
}
};

// Set the header title for the view being pushed.
Expand Down Expand Up @@ -2025,6 +2081,12 @@ pub struct AppState {
pub adding_account: bool,
/// Local configuration and UI state for bot-assisted room binding.
pub bot_settings: BotSettingsState,
/// The room ID for VoIP calls, set when navigating to VoIP screen from a call notification.
#[serde(skip)]
pub voip_room_id: Option<OwnedRoomId>,
/// Cached VoIP tokens (OpenID and LiveKit JWT) for faster reconnection.
#[serde(default)]
pub voip_tokens: crate::voip::VoipTokenState,
/// Translation API configuration.
#[serde(default)]
pub translation: crate::room::translation::TranslationConfig,
Expand Down Expand Up @@ -2316,6 +2378,9 @@ pub enum SelectedRoom {
Space {
space_name_id: RoomNameId,
},
Voip {
room_name_id: RoomNameId,
},
}

impl SelectedRoom {
Expand All @@ -2325,6 +2390,8 @@ impl SelectedRoom {
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(),
SelectedRoom::Voip { room_name_id } => room_name_id.room_id(),

}
}

Expand All @@ -2334,6 +2401,26 @@ impl SelectedRoom {
SelectedRoom::InvitedRoom { room_name_id } => room_name_id,
SelectedRoom::Space { space_name_id } => space_name_id,
SelectedRoom::Thread { room_name_id, .. } => room_name_id,
SelectedRoom::Voip { room_name_id } => room_name_id,
}
}

/// Returns the `TimelineKind` for this room, if applicable.
/// Returns `None` for invited rooms, spaces, and VoIP rooms which don't have timelines.
pub fn timeline_kind(&self) -> Option<TimelineKind> {
match self {
SelectedRoom::JoinedRoom { room_name_id } => {
Some(TimelineKind::MainRoom { room_id: room_name_id.room_id().clone() })
}
SelectedRoom::Thread { room_name_id, thread_root_event_id } => {
Some(TimelineKind::Thread {
room_id: room_name_id.room_id().clone(),
thread_root_event_id: thread_root_event_id.clone(),
})
}
SelectedRoom::InvitedRoom { .. } => None,
SelectedRoom::Space { .. } => None,
SelectedRoom::Voip { .. } => None,
}
}

Expand Down Expand Up @@ -2364,6 +2451,12 @@ impl SelectedRoom {
&format!("{}##{}", room_name_id.room_id(), thread_root_event_id)
)
}
SelectedRoom::Voip { room_name_id } => {
// VoIP tabs get a distinct ID to differentiate from normal room tabs
LiveId::from_str(
&format!("{}##voip", room_name_id.room_id())
)
}
other => LiveId::from_str(other.room_id().as_str()),
}
}
Expand All @@ -2375,33 +2468,15 @@ impl SelectedRoom {
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}"),
}
}

/// Returns the `TimelineKind` for this selected room.
///
/// Returns `None` for `InvitedRoom` and `Space` variants, as they don't have timelines.
pub fn timeline_kind(&self) -> Option<TimelineKind> {
match self {
SelectedRoom::JoinedRoom { room_name_id } => {
Some(TimelineKind::MainRoom {
room_id: room_name_id.room_id().clone(),
})
}
SelectedRoom::Thread { room_name_id, thread_root_event_id } => {
Some(TimelineKind::Thread {
room_id: room_name_id.room_id().clone(),
thread_root_event_id: thread_root_event_id.clone(),
})
}
SelectedRoom::InvitedRoom { .. } | SelectedRoom::Space { .. } => None,
SelectedRoom::Voip { room_name_id } => format!("[VoIP] {room_name_id}"),
}
}
}

impl PartialEq for SelectedRoom {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
// Threads are equal if room_id and thread_root_event_id match
(
SelectedRoom::Thread {
room_name_id: lhs_room_name_id,
Expand All @@ -2415,7 +2490,16 @@ impl PartialEq for SelectedRoom {
lhs_room_name_id.room_id() == rhs_room_name_id.room_id()
&& lhs_thread_root_event_id == rhs_thread_root_event_id
}
// Thread is never equal to non-Thread
(SelectedRoom::Thread { .. }, _) | (_, SelectedRoom::Thread { .. }) => false,
// VoIP rooms are equal only to other VoIP rooms with same room_id
(
SelectedRoom::Voip { room_name_id: lhs },
SelectedRoom::Voip { room_name_id: rhs },
) => lhs.room_id() == rhs.room_id(),
// VoIP is never equal to non-VoIP (even if same room_id)
(SelectedRoom::Voip { .. }, _) | (_, SelectedRoom::Voip { .. }) => false,
// All other variants compare by room_id only
_ => self.room_id() == other.room_id(),
}
}
Expand Down
1 change: 1 addition & 0 deletions src/home/home_screen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,7 @@ impl HomeScreen {
| SelectedTab::Home => id!(home_page),
SelectedTab::Settings => id!(settings_page),
SelectedTab::AddRoom => id!(add_room_page),
SelectedTab::VoIP => id!(voip_page),
},
)
}
Expand Down
Loading
Loading