feat: Show dehydrated devices in account panel

This commit is contained in:
Ginger committed 2026-07-27 14:36:01 -04:00
1 parent 7920127391
commit 226a3917af
6 files changed
+64 -35

No files matched your search

+3 -1
View File
@@ -70,6 +70,7 @@ enum AccountBody {
email_requirement: EmailRequirement,
email: Option<String>,
devices: Vec<DeviceCard>,
dehydrated_device_id: Option<OwnedDeviceId>,
},
Locked,
}
@@ -132,7 +133,8 @@ async fn get_account(
oidc_enabled: services.oidc.enabled(),
email_requirement,
email,
devices: device_cards
devices: device_cards,
dehydrated_device_id,
}))
}
+16 -2
View File
@@ -67,12 +67,15 @@ impl Avatar {
pub(super) fn for_device(
client_metadata: Option<&ClientMetadata>,
display_name: Option<&str>,
dehydrated: bool,
) -> Self {
let avatar_src = client_metadata
.and_then(|metadata| metadata.logo_uri.as_ref())
.map(|uri| uri.as_str().to_owned());
let avatar_type = if let Some(avatar_src) = avatar_src {
let avatar_type = if dehydrated {
AvatarType::Initial('⊡')
} else if let Some(avatar_src) = avatar_src {
AvatarType::Image(avatar_src)
} else if let Some(initial) = display_name.and_then(|name| name.chars().next()) {
if client_metadata.is_some() {
@@ -130,6 +133,7 @@ pub(super) struct DeviceCard {
pub last_active: String,
pub oauth_metadata: Option<ClientMetadata>,
pub style: DeviceCardStyle,
pub dehydrated: bool,
}
impl HtmlSafe for DeviceCard {}
@@ -163,12 +167,21 @@ impl DeviceCard {
}
.await;
let dehydrated_device_id = services
.users
.get_dehydrated_device(user_id)
.await
.ok()
.map(|device| device.device_id);
let dehydrated = dehydrated_device_id.as_ref() == Some(&device.device_id);
let display_name = oauth_metadata
.as_ref()
.and_then(|metadata| metadata.client_name.clone())
.or_else(|| device.display_name.clone());
let avatar = Avatar::for_device(oauth_metadata.as_ref(), display_name.as_deref());
let avatar =
Avatar::for_device(oauth_metadata.as_ref(), display_name.as_deref(), dehydrated);
let last_active = device.last_seen_ts.map_or_else(
|| "unknown".to_owned(),
@@ -190,6 +203,7 @@ impl DeviceCard {
last_active,
oauth_metadata,
style,
dehydrated,
}
}
}
@@ -1,37 +1,41 @@
<div class="card">
{{ avatar }}
<div class="info">
<div class="name">
<span>
{% if let Some(display_name) = display_name %}
{{ display_name }}
{% else %}
Unknown device
{% endif %}
</span>
{% if style == DeviceCardStyle::Detailed %}
<span class="id">
<span class="mobile-hidden">•</span>
<ul class="bullet-separated">
<li>{{ device_id }}</li>
<li>
{% if let Some(metadata) = oauth_metadata %}
<a href="{{ metadata.client_uri }}">Client website</a>
{% else %}
legacy
{% endif %}
</li>
</span>
{% if dehydrated %}
<div class="name">Dehydrated device</div>
{% else %}
<div class="name">
<span>
{% if let Some(display_name) = display_name %}
{{ display_name }}
{% else %}
Unknown device
{% endif %}
</span>
{% if style == DeviceCardStyle::Detailed %}
<span class="id">
<span class="mobile-hidden">•</span>
<ul class="bullet-separated">
<li>{{ device_id }}</li>
<li>
{% if let Some(metadata) = oauth_metadata %}
<a href="{{ metadata.client_uri }}">Client website</a>
{% else %}
legacy
{% endif %}
</li>
</span>
</span>
{% endif %}
</div>
<div>
Last active: {{ last_active }}
</div>
<div>
{% if style != DeviceCardStyle::Detailed %}
<a href="{{ crate::ROUTE_PREFIX }}/account/device/{{ device_id }}/">Details</a>
{% endif %}
</div>
<div>
Last active: {{ last_active }}
</div>
<div>
{% if style != DeviceCardStyle::Detailed %}
<a href="{{ crate::ROUTE_PREFIX }}/account/device/{{ device_id }}/">Details</a>
</div>
{% endif %}
</div>
</div>
</div>
+4 -1
View File
@@ -9,7 +9,7 @@ Your account
<h1>Manage your account</h1>
{{ user_card }}
{% match body %}
{% when AccountBody::Unlocked { suspended, email_requirement, email, devices, oidc_enabled } %}
{% when AccountBody::Unlocked { suspended, email_requirement, email, devices, oidc_enabled, dehydrated_device_id } %}
{% if suspended %}
<p class="card danger">
⚠️ Your account has been suspended by your homeserver's administrator.
@@ -52,6 +52,9 @@ Your account
and sign in to start chatting on Matrix.
</span>
{% endfor %}
{% if let Some(dehydrated_device_id) = dehydrated_device_id %}
<small>⊡ Your account has a dehydrated device. <a href="device/{{ dehydrated_device_id | urlencode_strict }}/">View details</a></small>
{% endif %}
</div>
</details>
</section>
@@ -27,6 +27,8 @@ Device information
{% if let Some((_, session_info)) = client_metadata %}
This device has permission to:
{{ ClientScopes { scopes: session_info.scopes.clone() } }}
{% else if device_card.dehydrated %}
This is your dehydrated device. It saves encryption keys for you while you're offline.
{% else %}
This device can access and control all features of your Matrix account.
<br>
+6 -2
View File
@@ -4,6 +4,7 @@ use std::{
time::{Duration, SystemTime},
};
use askama::filters::urlencode_strict;
use axum::{
extract::FromRequestParts,
http::request::Parts,
@@ -63,8 +64,11 @@ impl LoginTarget {
| Self::DeviceCode(code) =>
format!("oauth2/grant/device_code?{}", serde_urlencoded::to_string(code).unwrap())
.into(),
| Self::DeviceInfo(path) => format!("account/device/{}/", path.device).into(),
| Self::RemoveDevice(path) => format!("account/device/{}/remove", path.device).into(),
| Self::DeviceInfo(path) =>
format!("account/device/{}/", urlencode_strict(&path.device).unwrap()).into(),
| Self::RemoveDevice(path) =>
format!("account/device/{}/remove", urlencode_strict(&path.device).unwrap())
.into(),
};
format!("{ROUTE_PREFIX}/{path}")