atagen

macltok

public
Star0
Watch0

Commit 5d2974de5826

tarball

atagen <boss@atagen.co>2026-10-08 13:08 UTC

8 files changed+74-57
media: hand avatars out as media ids
Modified file: docs/design-decisions.md+9 additions−2 deletions
@@ -164,8 +164,8 @@ sdk's own patcher is private
164164
165165 ## media
166166
167-media is a read. `ClientHandle::media` takes a `MediaId` a retained timeline
168-handed you, `mxc_media` takes any plain mxc uri. both share one bounded
167+media is a read. `ClientHandle::media` takes a `MediaId`, from a retained
168+timeline or an avatar, through one bounded
169169 download path with the byte cap the sdk's own download lacks, an hour's total
170170 deadline instead of the sdk client's thirty seconds, and a minute's patience
171171 for a stalled stream. concurrent asks for the same bytes share one flight.
@@ -181,6 +181,13 @@ checks the cached bytes against the file's hash and decrypts them if they match
181181 media ids are registered as timelines render and forgotten when their timeline
182182 closes, least recently seen first once the cap is hit
183183
184+avatars are `MediaId`s too, so a host never handles an mxc uri. they aren't
185+registered, since they live in the room list and member lists for the whole
186+session and there can be thousands, and the cap would evict ones still on
187+screen. a plain avatar carries no keys, so its id is its mxc uri and resolves
188+without the registry. it never expires, and an mxc uri a host got some other way
189+parses into one. `set_avatar` still takes an mxc uri
190+
184191 streaming decryption inside macltok was declined. in a browser the service
185192 worker answering range requests is a separate context from the worker running
186193 macltok, and streaming through macltok would carry every chunk through wasm
Modified file: src/engine/media.rs+20 additions−19 deletions
@@ -35,7 +35,7 @@ use super::executor::EngineBoxFuture;
3535 use crate::{
3636 AttachmentEncryption, AttachmentUpload, Error, ErrorKind, Media, MediaFile, MediaId, MediaKind,
3737 MediaLocation, MediaSize, TimelineId,
38- normalise::{MediaRegistration, MediaScope},
38+ normalise::{MediaRegistration, MediaScope, MediaSourceInfo},
3939 upload::prepare_upload,
4040 };
4141
@@ -137,6 +137,9 @@ impl MediaService {
137137 }
138138
139139 async fn registered(&self, media_id: &MediaId) -> Result<MediaRegistration, Error> {
140+ if let Some(uri) = media_id.as_plain() {
141+ return plain_registration(media_id, uri);
142+ }
140143 self.registry
141144 .lock()
142145 .await
@@ -150,24 +153,6 @@ impl MediaService {
150153 })
151154 }
152155
153- pub(crate) async fn mxc_media(&self, uri: &MxcUri, size: MediaSize) -> Result<Media, Error> {
154- if !uri.is_valid() {
155- return Err(Error::new(ErrorKind::Invalid, "mxc uri is malformed"));
156- }
157- let format = match size {
158- MediaSize::Full => MediaFormat::File,
159- MediaSize::Thumbnail { width, height } => thumbnail_format(width, height),
160- };
161- self.fetch_or_file(
162- MediaRequestParameters {
163- source: MediaSource::Plain(uri.to_owned()),
164- format,
165- },
166- None,
167- )
168- .await
169- }
170-
171156 /// plenty of servers won't thumbnail everything, and the file itself is the next best
172157 /// thing. the byte cap still holds either way
173158 async fn fetch_or_file(
@@ -233,6 +218,22 @@ struct ChosenSource {
233218 declared_size: Option<u64>,
234219 }
235220
221+fn plain_registration(media_id: &MediaId, uri: &MxcUri) -> Result<MediaRegistration, Error> {
222+ if !uri.is_valid() {
223+ return Err(Error::new(ErrorKind::Invalid, "mxc uri is malformed"));
224+ }
225+ Ok(MediaRegistration {
226+ id: media_id.clone(),
227+ kind: MediaKind::Image,
228+ full: MediaSourceInfo {
229+ source: MediaSource::Plain(uri.to_owned()),
230+ mimetype: None,
231+ declared_size: None,
232+ },
233+ thumbnail: None,
234+ })
235+}
236+
236237 fn choose_source(media: MediaRegistration, size: MediaSize) -> Result<ChosenSource, Error> {
237238 let (source, format, mimetype, declared_size) = match size {
238239 MediaSize::Full => (
Modified file: src/model.rs+21 additions−12 deletions
@@ -2,8 +2,8 @@ use std::{fmt, sync::Arc};
22
33 use imbl::Vector;
44 use ruma::{
5- OwnedDeviceId, OwnedEventId, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedRoomOrAliasId,
6- OwnedServerName, OwnedTransactionId, OwnedUserId,
5+ MxcUri, OwnedDeviceId, OwnedEventId, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId,
6+ OwnedRoomOrAliasId, OwnedServerName, OwnedTransactionId, OwnedUserId,
77 events::{
88 AnyGlobalAccountDataEventContent, AnyRoomAccountDataEventContent, AnySyncTimelineEvent,
99 GlobalAccountDataEventType, RoomAccountDataEventType,
@@ -786,7 +786,7 @@ pub struct RoomSummary {
786786 pub direct: bool,
787787 pub favourite: bool,
788788 pub low_priority: bool,
789- pub avatar: Option<OwnedMxcUri>,
789+ pub avatar: Option<MediaId>,
790790 pub latest_event_timestamp_ms: Option<u64>,
791791 pub latest_event: Option<LatestEvent>,
792792 pub notification_mode: Option<RoomNotificationMode>,
@@ -812,7 +812,7 @@ pub struct RoomSummary {
812812 pub struct RoomHero {
813813 pub user_id: OwnedUserId,
814814 pub display_name: Option<String>,
815- pub avatar: Option<OwnedMxcUri>,
815+ pub avatar: Option<MediaId>,
816816 }
817817
818818 /// enough for a room list preview line. `kind` and `body` are only set for messages,
@@ -834,7 +834,7 @@ pub struct LatestEvent {
834834 pub struct Profile {
835835 pub user_id: OwnedUserId,
836836 pub display_name: Option<String>,
837- pub avatar: Option<OwnedMxcUri>,
837+ pub avatar: Option<MediaId>,
838838 }
839839
840840 /// one room in a space's hierarchy, as the server describes it, which may be a room you're not in
@@ -845,7 +845,7 @@ pub struct SpaceChild {
845845 pub room_id: OwnedRoomId,
846846 pub name: String,
847847 pub topic: Option<String>,
848- pub avatar: Option<OwnedMxcUri>,
848+ pub avatar: Option<MediaId>,
849849 pub canonical_alias: Option<OwnedRoomAliasId>,
850850 pub joined_members: u64,
851851 pub space: bool,
@@ -903,7 +903,7 @@ pub struct PushNotification {
903903 pub struct PushNotificationSender {
904904 pub user_id: OwnedUserId,
905905 pub display_name: Option<String>,
906- pub avatar_url: Option<OwnedMxcUri>,
906+ pub avatar_url: Option<MediaId>,
907907 pub name_ambiguous: bool,
908908 }
909909
@@ -913,7 +913,7 @@ pub struct PushNotificationSender {
913913 pub struct PushNotificationRoom {
914914 pub room_id: OwnedRoomId,
915915 pub name: String,
916- pub avatar_url: Option<OwnedMxcUri>,
916+ pub avatar_url: Option<MediaId>,
917917 pub canonical_alias: Option<OwnedRoomAliasId>,
918918 pub topic: Option<String>,
919919 pub encrypted: Option<bool>,
@@ -1107,7 +1107,7 @@ pub struct MemberSummary {
11071107 pub user_id: OwnedUserId,
11081108 pub name: String,
11091109 pub display_name: Option<String>,
1110- pub avatar: Option<OwnedMxcUri>,
1110+ pub avatar: Option<MediaId>,
11111111 pub membership: MemberMembership,
11121112 pub own: bool,
11131113 pub name_ambiguous: bool,
@@ -1422,7 +1422,7 @@ impl From<OwnedEventId> for EventRef {
14221422 pub struct Sender {
14231423 pub user_id: OwnedUserId,
14241424 pub display_name: Option<String>,
1425- pub avatar: Option<OwnedMxcUri>,
1425+ pub avatar: Option<MediaId>,
14261426 /// someone else in the room goes by the same name, so show the user id too
14271427 pub name_ambiguous: bool,
14281428 }
@@ -1458,12 +1458,12 @@ pub enum TimelineContent {
14581458 Profile {
14591459 user_id: OwnedUserId,
14601460 display_name: Option<Change<Option<String>>>,
1461- avatar: Option<Change<Option<OwnedMxcUri>>>,
1461+ avatar: Option<Change<Option<MediaId>>>,
14621462 },
14631463 /// `None` means it was cleared, or the event was redacted
14641464 RoomName(Option<String>),
14651465 RoomTopic(Option<String>),
1466- RoomAvatar(Option<OwnedMxcUri>),
1466+ RoomAvatar(Option<MediaId>),
14671467 EncryptionEnabled,
14681468 RoomCreated,
14691469 RoomUpgraded {
@@ -1635,6 +1635,15 @@ impl MediaId {
16351635 Self(value)
16361636 }
16371637
1638+ /// plain media carries no keys, so it needs no registering and never expires
1639+ pub(crate) fn plain<U: AsRef<str> + ?Sized>(uri: &U) -> Self {
1640+ Self(uri.as_ref().to_owned())
1641+ }
1642+
1643+ pub(crate) fn as_plain(&self) -> Option<&MxcUri> {
1644+ self.0.starts_with("mxc://").then(|| self.0.as_str().into())
1645+ }
1646+
16381647 #[must_use]
16391648 pub fn as_str(&self) -> &str {
16401649 &self.0
Modified file: src/normalise/member.rs+2 additions−2 deletions
@@ -1,14 +1,14 @@
11 use matrix_sdk::room::RoomMember;
22 use ruma::events::room::member::MembershipState;
33
4-use crate::{MemberMembership, MemberSummary};
4+use crate::{MediaId, MemberMembership, MemberSummary};
55
66 pub(crate) fn member_summary(member: RoomMember) -> MemberSummary {
77 MemberSummary {
88 user_id: member.user_id().to_owned(),
99 name: member.name().to_owned(),
1010 display_name: member.display_name().map(ToOwned::to_owned),
11- avatar: member.avatar_url().map(ToOwned::to_owned),
11+ avatar: member.avatar_url().map(MediaId::plain),
1212 membership: match member.membership() {
1313 MembershipState::Join => MemberMembership::Joined,
1414 MembershipState::Invite => MemberMembership::Invited,
Modified file: src/normalise/mod.rs+1 additions−1 deletions
@@ -3,7 +3,7 @@ mod member;
33 mod room;
44 mod timeline;
55
6-pub(crate) use media::{MediaRegistration, MediaScope};
6+pub(crate) use media::{MediaRegistration, MediaScope, MediaSourceInfo};
77 pub(crate) use member::member_summary;
88 pub(crate) use room::notification_mode;
99 pub(crate) use room::{room_summaries, space_child};
Modified file: src/normalise/room.rs+4 additions−4 deletions
@@ -14,7 +14,7 @@ use ruma::OwnedRoomId;
1414
1515 use super::message_kind;
1616 use crate::{
17- LatestEvent, MessageKind, RoomHero, RoomMembership, RoomNotificationMode, RoomSummary,
17+ LatestEvent, MediaId, MessageKind, RoomHero, RoomMembership, RoomNotificationMode, RoomSummary,
1818 SendState, SpaceChild,
1919 };
2020
@@ -70,7 +70,7 @@ async fn room_summary(room: &Room, spaces: &SpaceService) -> RoomSummary {
7070 direct: room.is_direct().await.unwrap_or(false),
7171 favourite: room.is_favourite(),
7272 low_priority: room.is_low_priority(),
73- avatar: room.avatar_url(),
73+ avatar: room.avatar_url().as_deref().map(MediaId::plain),
7474 latest_event_timestamp_ms: room
7575 .latest_event_timestamp()
7676 .map(|value| value.get().into()),
@@ -87,7 +87,7 @@ async fn room_summary(room: &Room, spaces: &SpaceService) -> RoomSummary {
8787 .map(|hero| RoomHero {
8888 user_id: hero.user_id,
8989 display_name: hero.display_name,
90- avatar: hero.avatar_url,
90+ avatar: hero.avatar_url.as_deref().map(MediaId::plain),
9191 })
9292 .collect(),
9393 successor: room.successor_room().map(|successor| successor.room_id),
@@ -197,7 +197,7 @@ pub(crate) fn space_child(room: SpaceRoom) -> SpaceChild {
197197 name: room.display_name,
198198 room_id: room.room_id,
199199 topic: room.topic,
200- avatar: room.avatar_url,
200+ avatar: room.avatar_url.as_deref().map(MediaId::plain),
201201 canonical_alias: room.canonical_alias,
202202 joined_members: room.num_joined_members,
203203 space: room.room_type == Some(ruma::room::RoomType::Space),
Modified file: src/normalise/timeline.rs+7 additions−7 deletions
@@ -19,9 +19,9 @@ use ruma::{
1919 };
2020
2121 use crate::{
22- Change, MembershipChange, Message, MessageKind, MessageMentions, Reaction, ReplyDetails,
23- ReplyReference, SendState, Sender, ThreadSummary, TimelineContent, TimelineDiff, TimelineEvent,
24- TimelineItem, TimelineItemId, UtdCause,
22+ Change, MediaId, MembershipChange, Message, MessageKind, MessageMentions, Reaction,
23+ ReplyDetails, ReplyReference, SendState, Sender, ThreadSummary, TimelineContent, TimelineDiff,
24+ TimelineEvent, TimelineItem, TimelineItemId, UtdCause,
2525 };
2626
2727 use super::media::{
@@ -198,8 +198,8 @@ fn content(content: &TimelineItemContent, media_identity: Option<&str>) -> Timel
198198 new: change.new.clone(),
199199 }),
200200 avatar: change.avatar_url_change().map(|change| Change {
201- old: change.old.clone(),
202- new: change.new.clone(),
201+ old: change.old.as_deref().map(MediaId::plain),
202+ new: change.new.as_deref().map(MediaId::plain),
203203 }),
204204 },
205205 TimelineItemContent::OtherState(state) => match state.content() {
@@ -210,7 +210,7 @@ fn content(content: &TimelineItemContent, media_identity: Option<&str>) -> Timel
210210 original(change).and_then(|content| non_empty(&content.topic)),
211211 ),
212212 AnyOtherStateEventContentChange::RoomAvatar(change) => TimelineContent::RoomAvatar(
213- original(change).and_then(|content| content.url.clone()),
213+ original(change).and_then(|content| content.url.as_deref().map(MediaId::plain)),
214214 ),
215215 AnyOtherStateEventContentChange::RoomEncryption(_) => {
216216 TimelineContent::EncryptionEnabled
@@ -380,7 +380,7 @@ fn sender(user_id: &UserId, profile: &TimelineDetails<Profile>) -> Sender {
380380 Sender {
381381 user_id: user_id.to_owned(),
382382 display_name: profile.and_then(|profile| profile.display_name.clone()),
383- avatar: profile.and_then(|profile| profile.avatar_url.clone()),
383+ avatar: profile.and_then(|profile| profile.avatar_url.as_deref().map(MediaId::plain)),
384384 name_ambiguous: profile.is_some_and(|profile| profile.display_name_ambiguous),
385385 }
386386 }
Modified file: src/transport.rs+10 additions−10 deletions
@@ -15,7 +15,7 @@ use matrix_sdk_ui::{
1515 sync_service::SyncService,
1616 };
1717 use ruma::{
18- EventId, MxcUri, OwnedDeviceId, OwnedEventId, OwnedMxcUri, OwnedRoomId, OwnedRoomOrAliasId,
18+ EventId, OwnedDeviceId, OwnedEventId, OwnedMxcUri, OwnedRoomId, OwnedRoomOrAliasId,
1919 OwnedServerName, OwnedTransactionId, OwnedUserId, RoomId, UserId,
2020 events::{
2121 AnyGlobalAccountDataEventContent, AnyRoomAccountDataEvent, AnyRoomAccountDataEventContent,
@@ -555,7 +555,8 @@ impl ClientHandle {
555555 .await
556556 }
557557
558- /// bytes for media a retained timeline showed you. concurrent asks share one download
558+ /// bytes for an avatar, or for media a retained timeline showed you. concurrent asks share
559+ /// one download
559560 pub async fn media(&self, media_id: &MediaId, size: MediaSize) -> Result<Media, Error> {
560561 self.media.timeline_media(media_id, size).await
561562 }
@@ -570,11 +571,6 @@ impl ClientHandle {
570571 self.media.media_location(media_id, size).await
571572 }
572573
573- /// bytes for any plain mxc uri, avatars mostly. same byte cap as everything else
574- pub async fn mxc_media(&self, uri: &MxcUri, size: MediaSize) -> Result<Media, Error> {
575- self.media.mxc_media(uri, size).await
576- }
577-
578574 /// the space graph builds on first use and then keeps itself current
579575 async fn spaces(&self) -> &SpaceService {
580576 self.spaces
@@ -908,7 +904,11 @@ fn profile(
908904 Ok(Profile {
909905 user_id,
910906 display_name: response.get_static::<DisplayName>().map_err(malformed)?,
911- avatar: response.get_static::<AvatarUrl>().map_err(malformed)?,
907+ avatar: response
908+ .get_static::<AvatarUrl>()
909+ .map_err(malformed)?
910+ .as_deref()
911+ .map(MediaId::plain),
912912 })
913913 }
914914
@@ -1069,13 +1069,13 @@ fn normalise_push_notification(
10691069 sender: PushNotificationSender {
10701070 user_id: item.event.sender().to_owned(),
10711071 display_name: item.sender_display_name,
1072- avatar_url: item.sender_avatar_url.map(Into::into),
1072+ avatar_url: item.sender_avatar_url.as_deref().map(MediaId::plain),
10731073 name_ambiguous: item.is_sender_name_ambiguous,
10741074 },
10751075 room: PushNotificationRoom {
10761076 room_id: room_id.to_owned(),
10771077 name: item.room_computed_display_name,
1078- avatar_url: item.room_avatar_url.map(Into::into),
1078+ avatar_url: item.room_avatar_url.as_deref().map(MediaId::plain),
10791079 canonical_alias: item
10801080 .room_canonical_alias
10811081 .as_deref()