* avatar

* refactor avatar display: unify rendering and resolve at use time

  - Extract buildAvatarWidget() in common.dart to share avatar rendering
    logic across desktop settings, desktop CM and mobile CM
  - Add resolve_avatar_url() in Rust, exposed via FFI (SyncReturn),
    to resolve relative avatar paths (e.g. "/avatar/xxx") to absolute URLs
  - Store avatar as-is in local config, only resolve when displaying
    (settings page) or sending (LoginRequest)
  - Resolve avatar in LoginRequest before sending to remote peer
  - Add error handling for network image load failures
  - Guard against empty client.name[0] crash
  - Show avatar in mobile settings page account tile

Signed-off-by: 21pages <sunboeasy@gmail.com>

* web: implement mainResolveAvatarUrl via js getByName

Signed-off-by: 21pages <sunboeasy@gmail.com>

* increase ipc Data enum size limit to 120 bytes

Signed-off-by: 21pages <sunboeasy@gmail.com>

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
Co-authored-by: 21pages <sunboeasy@gmail.com>
This commit is contained in:
RustDesk
2026-03-04 21:43:19 +08:00
committed by GitHub
parent 52b66e71d1
commit ab64a32f30
20 changed files with 225 additions and 49 deletions

View File

@@ -33,7 +33,7 @@ use crate::{
create_symmetric_key_msg, decode_id_pk, get_rs_pk, is_keyboard_mode_supported,
kcp_stream::KcpStream,
secure_tcp,
ui_interface::{get_builtin_option, use_texture_render},
ui_interface::{get_builtin_option, resolve_avatar_url, use_texture_render},
ui_session_interface::{InvokeUiSession, Session},
};
#[cfg(feature = "unix-file-copy-paste")]
@@ -2625,6 +2625,20 @@ impl LoginConfigHandler {
} else {
(my_id, self.id.clone())
};
let mut avatar = get_builtin_option(keys::OPTION_AVATAR);
if avatar.is_empty() {
avatar = serde_json::from_str::<serde_json::Value>(&LocalConfig::get_option(
"user_info",
))
.ok()
.and_then(|x| {
x.get("avatar")
.and_then(|x| x.as_str())
.map(|x| x.trim().to_owned())
})
.unwrap_or_default();
}
avatar = resolve_avatar_url(avatar);
let mut display_name = get_builtin_option(keys::OPTION_DISPLAY_NAME);
if display_name.is_empty() {
display_name =
@@ -2684,6 +2698,7 @@ impl LoginConfigHandler {
})
.into(),
hwid,
avatar,
..Default::default()
};
match self.conn_type {

View File

@@ -1101,6 +1101,10 @@ pub fn main_get_api_server() -> String {
get_api_server()
}
pub fn main_resolve_avatar_url(avatar: String) -> SyncReturn<String> {
SyncReturn(resolve_avatar_url(avatar))
}
pub fn main_http_request(url: String, method: String, body: Option<String>, header: String) {
http_request(url, method, body, header)
}

View File

@@ -17,6 +17,7 @@ lazy_static::lazy_static! {
const QUERY_INTERVAL_SECS: f32 = 1.0;
const QUERY_TIMEOUT_SECS: u64 = 60 * 3;
const REQUESTING_ACCOUNT_AUTH: &str = "Requesting account auth";
const WAITING_ACCOUNT_AUTH: &str = "Waiting account auth";
const LOGIN_ACCOUNT_AUTH: &str = "Login account auth";
@@ -82,6 +83,8 @@ pub struct UserPayload {
#[serde(default)]
pub display_name: Option<String>,
#[serde(default)]
pub avatar: Option<String>,
#[serde(default)]
pub email: Option<String>,
#[serde(default)]
pub note: Option<String>,
@@ -273,6 +276,7 @@ impl OidcSession {
serde_json::json!({
"name": auth_body.user.name,
"display_name": auth_body.user.display_name,
"avatar": auth_body.user.avatar,
"status": auth_body.user.status
})
.to_string(),

View File

@@ -226,6 +226,7 @@ pub enum Data {
is_terminal: bool,
peer_id: String,
name: String,
avatar: String,
authorized: bool,
port_forward: String,
keyboard: bool,
@@ -1583,6 +1584,6 @@ mod test {
#[test]
fn verify_ffi_enum_data_size() {
println!("{}", std::mem::size_of::<Data>());
assert!(std::mem::size_of::<Data>() <= 96);
assert!(std::mem::size_of::<Data>() <= 120);
}
}

View File

@@ -1877,6 +1877,7 @@ impl Connection {
port_forward: self.port_forward_address.clone(),
peer_id,
name,
avatar: self.lr.avatar.clone(),
authorized,
keyboard: self.keyboard,
clipboard: self.clipboard,

View File

@@ -57,6 +57,11 @@ div.icon {
font-weight: bold;
}
img.icon {
size: 96px;
border-radius: 8px;
}
div.id {
@ELLIPSIS;
color: color(green-blue);

View File

@@ -28,6 +28,7 @@ impl InvokeUiCM for SciterHandler {
client.port_forward.clone(),
client.peer_id.clone(),
client.name.clone(),
client.avatar.clone(),
client.authorized,
client.keyboard,
client.clipboard,

View File

@@ -42,9 +42,11 @@ class Body: Reactor.Component
return <div .content style="size:*">
<div .left-panel>
<div .icon-and-id>
{c.avatar ?
<img .icon src={c.avatar} /> :
<div .icon style={"background: " + string2RGB(c.name, 1)}>
{c.name[0].toUpperCase()}
</div>
</div>}
<div>
<div .id style="font-weight: bold; font-size: 1.2em;">{c.name}</div>
<div .id>({c.peer_id})</div>
@@ -366,7 +368,7 @@ function bring_to_top(idx=-1) {
}
}
handler.addConnection = function(id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording, block_input) {
handler.addConnection = function(id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, avatar, authorized, keyboard, clipboard, audio, file, restart, recording, block_input) {
stdout.println("new connection #" + id + ": " + peer_id);
var conn;
connections.map(function(c) {
@@ -385,6 +387,7 @@ handler.addConnection = function(id, is_file_transfer, is_view_camera, is_termin
conn = {
id: id, is_file_transfer: is_file_transfer, is_view_camera: is_view_camera, is_terminal: is_terminal, peer_id: peer_id,
port_forward: port_forward,
avatar: avatar,
name: name, authorized: authorized, time: new Date(), now: new Date(),
keyboard: keyboard, clipboard: clipboard, msgs: [], unreaded: 0,
audio: audio, file: file, restart: restart, recording: recording,

View File

@@ -1451,6 +1451,9 @@ function set_local_user_info(user) {
if (user.display_name) {
user_info.display_name = user.display_name;
}
if (user.avatar) {
user_info.avatar = user.avatar;
}
if (user.status) {
user_info.status = user.status;
}

View File

@@ -134,6 +134,7 @@ pub struct Client {
pub is_terminal: bool,
pub port_forward: String,
pub name: String,
pub avatar: String,
pub peer_id: String,
pub keyboard: bool,
pub clipboard: bool,
@@ -220,6 +221,7 @@ impl<T: InvokeUiCM> ConnectionManager<T> {
port_forward: String,
peer_id: String,
name: String,
avatar: String,
authorized: bool,
keyboard: bool,
clipboard: bool,
@@ -240,6 +242,7 @@ impl<T: InvokeUiCM> ConnectionManager<T> {
is_terminal,
port_forward,
name: name.clone(),
avatar,
peer_id: peer_id.clone(),
keyboard,
clipboard,
@@ -500,9 +503,9 @@ impl<T: InvokeUiCM> IpcTaskRunner<T> {
}
Ok(Some(data)) => {
match data {
Data::Login{id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, file_transfer_enabled: _file_transfer_enabled, restart, recording, block_input, from_switch} => {
Data::Login{id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, avatar, authorized, keyboard, clipboard, audio, file, file_transfer_enabled: _file_transfer_enabled, restart, recording, block_input, from_switch} => {
log::debug!("conn_id: {}", id);
self.cm.add_connection(id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording, block_input, from_switch, self.tx.clone());
self.cm.add_connection(id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, avatar, authorized, keyboard, clipboard, audio, file, restart, recording, block_input, from_switch, self.tx.clone());
self.conn_id = id;
#[cfg(target_os = "windows")]
{
@@ -823,6 +826,7 @@ pub async fn start_listen<T: InvokeUiCM>(
port_forward,
peer_id,
name,
avatar,
authorized,
keyboard,
clipboard,
@@ -843,6 +847,7 @@ pub async fn start_listen<T: InvokeUiCM>(
port_forward,
peer_id,
name,
avatar,
authorized,
keyboard,
clipboard,

View File

@@ -245,7 +245,20 @@ pub fn get_builtin_option(key: &str) -> String {
#[inline]
pub fn set_local_option(key: String, value: String) {
LocalConfig::set_option(key.clone(), value.clone());
LocalConfig::set_option(key.clone(), value);
}
/// Resolve relative avatar path (e.g. "/avatar/xxx") to absolute URL
/// by prepending the API server address.
pub fn resolve_avatar_url(avatar: String) -> String {
let avatar = avatar.trim().to_owned();
if avatar.starts_with('/') {
let api_server = get_api_server();
if !api_server.is_empty() {
return format!("{}{}", api_server.trim_end_matches('/'), avatar);
}
}
avatar
}
#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))]