Commit fa5b4e09943a
tarball6 files changed+213-75
@@ -15,6 +15,7 @@ use matrix_sdk::{
1515
},1616
state::get_state_event_for_key,1717
},18
+ api::error::ErrorKind as ApiErrorKind,1819
events::{1920
AnyGlobalAccountDataEventContent, GlobalAccountDataEventType, MessageLikeEventType,2021
StateEventType,@@ -24,7 +25,7 @@ use matrix_sdk::{
2425
serde::{JsonObject, Raw},2526
},2627
};27
-use ruma::{OwnedRoomId, OwnedUserId};28
+use ruma::{EventId, OwnedRoomId, OwnedUserId};2829
use serde_json::Value as JsonValue;2930
use tokio::sync::mpsc;3031
@@ -184,6 +185,7 @@ fn key(mutation: &Mutation, server: Option<&ruma::ServerName>) -> Option<Key> {
184185
| RoomCommand::SetUserPowerLevel { room_id, .. }185186
| RoomCommand::MuteUser { room_id, .. }186187
| RoomCommand::SetFavourite { room_id, .. }188
+ | RoomCommand::SetPinned { room_id, .. }187189
| RoomCommand::SetOwnDisplayName { room_id, .. }188190
| RoomCommand::SetOwnAvatar { room_id, .. }189191
| RoomCommand::SetAccountData { room_id, .. },@@ -334,6 +336,13 @@ async fn execute(
334336
} => mute_user(client, &room(&room_id)?, &user_id, muted)335337
.await336338
.map(done),339
+ RoomCommand::SetPinned {340
+ room_id,341
+ event_id,342
+ pinned,343
+ } => set_pinned(client, &room(&room_id)?, &event_id, pinned)344
+ .await345
+ .map(done),337346
RoomCommand::SetFavourite { room_id, favourite } => room(&room_id)?338347
.set_is_favourite(favourite, None)339348
.await@@ -644,6 +653,48 @@ async fn set_own_member_field(
644653
.map_err(|source| Error::from_sdk("updating your member event failed", source))645654
}646655
656
+/// against the server's list, since the store's lags a sync and another client may have just pinned657
+async fn set_pinned(658
+ client: &Client,659
+ room: &Room,660
+ event_id: &EventId,661
+ pinned: bool,662
+) -> Result<(), Error> {663
+ let request = get_state_event_for_key::v3::Request::new(664
+ room.room_id().to_owned(),665
+ StateEventType::RoomPinnedEvents,666
+ String::new(),667
+ );668
+ let mut content: JsonObject = match client.send(request).await {669
+ Ok(response) => {670
+ serde_json::from_str(response.event_or_content.get()).map_err(|source| {671
+ Error::with_source(ErrorKind::Server, "the pinned list is malformed", source)672
+ })?673
+ }674
+ Err(error) if error.client_api_error_kind() == Some(&ApiErrorKind::NotFound) => {675
+ JsonObject::new()676
+ }677
+ Err(source) => return Err(Error::from_http("reading the pinned list failed", source)),678
+ };679
+ let list = content680
+ .entry("pinned")681
+ .or_insert_with(|| JsonValue::Array(Vec::new()));682
+ if !list.is_array() {683
+ *list = JsonValue::Array(Vec::new());684
+ }685
+ let list = list.as_array_mut().expect("just made it an array");686
+ let present = list.iter().any(|id| id.as_str() == Some(event_id.as_str()));687
+ match (pinned, present) {688
+ (true, true) | (false, false) => return Ok(()),689
+ (true, false) => list.push(event_id.as_str().into()),690
+ (false, true) => list.retain(|id| id.as_str() != Some(event_id.as_str())),691
+ }692
+ room.send_state_event_raw("m.room.pinned_events", "", JsonValue::Object(content))693
+ .await694
+ .map(drop)695
+ .map_err(|source| Error::from_sdk("updating the pinned list failed", source))696
+}697
+647698
fn done<T>(_: T) -> CommandOutput {648699
CommandOutput::Done649700
}@@ -2,7 +2,7 @@ use std::collections::{HashMap, VecDeque};
22
33
use matrix_sdk::{Room, RoomState};44
use matrix_sdk_ui::timeline::{RoomExt, Timeline, TimelineFocus, TimelineReadReceiptTracking};5
-use ruma::{OwnedEventId, OwnedRoomId};5
+use ruma::OwnedRoomId;66
use tokio::sync::mpsc;77
88
use super::executor::{AbortHandle, JoinError, TaskSet, is_panic, resume_unwind};@@ -222,28 +222,52 @@ struct Timelines {
222222
impl Timelines {223223
async fn execute(&mut self, command: TimelineCommand, reply: Responder, room: &Room) {224224
match command {225
- TimelineCommand::OpenThread { root_event_id, .. } => {226
- self.threads.open(root_event_id, reply, room);225
+ TimelineCommand::OpenThread {226
+ room_id,227
+ root_event_id,228
+ } => self229
+ .threads230
+ .open(TimelineId::thread(room_id, root_event_id), reply, room),231
+ TimelineCommand::CloseThread {232
+ room_id,233
+ root_event_id,234
+ } => {235
+ let id = TimelineId::thread(room_id, root_event_id);236
+ reply.done(self.threads.close_one(&id).await);227237
}228
- TimelineCommand::CloseThread { root_event_id, .. } => {229
- reply.done(self.threads.close_one(&root_event_id).await);238
+ TimelineCommand::OpenPinned(room_id) => {239
+ self.threads.open(TimelineId::Pinned(room_id), reply, room);240
+ }241
+ TimelineCommand::ClosePinned(room_id) => {242
+ reply.done(self.threads.close_one(&TimelineId::Pinned(room_id)).await);230243
}231244
TimelineCommand::Resync {232245
timeline_id: TimelineId::Room(_),233246
} => reply.done(self.room.resync().await),234247
command => {235
- let root = command248
+ let Some(id) = command236249
.timeline_id()237
- .and_then(TimelineId::thread_root)238
- .cloned();239
- let Some(root) = root else {250
+ .filter(|id| !matches!(id, TimelineId::Room(_)))251
+ .cloned()252
+ else {240253
return self.room.execute(command, reply, &mut self.work).await;241254
};255
+ if matches!(id, TimelineId::Pinned(_))256
+ && matches!(257
+ command,258
+ TimelineCommand::Send { .. } | TimelineCommand::Paginate { .. }259
+ )260
+ {261
+ return reply.done(Err(Error::new(262
+ ErrorKind::Invalid,263
+ "the pinned timeline can't send or paginate",264
+ )));265
+ }242266
if let TimelineCommand::MarkRead {243267
timeline_id,244268
event_id,245269
} = &command246
- && !self.threads.open.contains_key(&root)270
+ && !self.threads.open.contains_key(&id)247271
{248272
let client = room.client();249273
let (timeline_id, event_id) = (timeline_id.clone(), event_id.clone());@@ -254,7 +278,7 @@ impl Timelines {
254278
});255279
return;256280
}257
- match self.threads.route(root, command, reply) {281
+ match self.threads.route(id, command, reply) {258282
Routed::Open(session, TimelineCommand::Resync { .. }, reply) => {259283
reply.done(session.resync().await);260284
}@@ -273,31 +297,25 @@ impl Timelines {
273297
room_id: &OwnedRoomId,274298
context: &SessionContext,275299
) {276
- let Some((root, queued, timeline)) = self.threads.built(built) else {300
+ let Some((id, queued, timeline)) = self.threads.built(built) else {277301
return;278302
};279303
let timeline = match timeline {280304
Ok(timeline) => timeline,281305
Err(error) => {282
- tracing::warn!(%error, %room_id, %root, "thread timeline build failed");283
- let error =284
- Error::with_source(ErrorKind::Timeline, "thread timeline build failed", error);306
+ tracing::warn!(%error, %room_id, ?id, "timeline build failed");307
+ let error = Error::with_source(ErrorKind::Timeline, "timeline build failed", error);285308
for (_, reply) in queued {286309
reply.done(Err(error.clone()));287310
}288311
return;289312
}290313
};291
- let id = TimelineId::thread(room_id.clone(), root.clone());292
- let Ok(session) = TimelineSession::start(id, timeline, context.clone()).await else {314
+ let Ok(session) = TimelineSession::start(id.clone(), timeline, context.clone()).await315
+ else {293316
return;294317
};295
- let session = self296
- .threads297
- .open298
- .entry(root)299
- .insert_entry(session)300
- .into_mut();318
+ let session = self.threads.open.entry(id).insert_entry(session).into_mut();301319
for (command, reply) in queued {302320
match command {303321
None => reply.done(Ok(())),@@ -309,7 +327,7 @@ impl Timelines {
309327
}310328
311329
type Built = (312
- OwnedEventId,330
+ TimelineId,313331
u64,314332
Result<Timeline, matrix_sdk_ui::timeline::Error>,315333
);@@ -328,9 +346,10 @@ struct OpeningThread {
328346
queued: Queued,329347
}330348
349
+/// threads and the pinned timeline, which build off the actor and queue what arrives meanwhile331350
struct Threads {332
- open: HashMap<OwnedEventId, TimelineSession>,333
- opening: HashMap<OwnedEventId, OpeningThread>,351
+ open: HashMap<TimelineId, TimelineSession>,352
+ opening: HashMap<TimelineId, OpeningThread>,334353
builds: TaskSet<Built>,335354
next_generation: u64,336355
queued: usize,@@ -349,40 +368,43 @@ impl Threads {
349368
}350369
}351370
352
- fn enqueue(&mut self, root: &OwnedEventId, command: Option<TimelineCommand>, reply: Responder) {371
+ fn enqueue(&mut self, id: &TimelineId, command: Option<TimelineCommand>, reply: Responder) {353372
if self.queued >= self.capacity {354373
return reply.done(Err(Error::new(355374
ErrorKind::QueueFull,356375
"thread command queue is full",357376
)));358377
}359
- let Some(opening) = self.opening.get_mut(root) else {378
+ let Some(opening) = self.opening.get_mut(id) else {360379
return reply.done(Err(Error::new(361380
ErrorKind::TimelineNotOpen,362
- "thread timeline is not open",381
+ "timeline is not open",363382
)));364383
};365384
opening.queued.push_back((command, reply));366385
self.queued += 1;367386
}368387
369
- fn open(&mut self, root: OwnedEventId, reply: Responder, room: &Room) {370
- if self.open.contains_key(&root) {388
+ fn open(&mut self, id: TimelineId, reply: Responder, room: &Room) {389
+ if self.open.contains_key(&id) {371390
return reply.done(Ok(()));372391
}373
- if !self.opening.contains_key(&root) {392
+ if !self.opening.contains_key(&id) {374393
let generation = self.next_generation;375394
self.next_generation = self.next_generation.wrapping_add(1);376395
let room = room.clone();377
- let build_root = root.clone();396
+ let build_id = id.clone();378397
let build = self.builds.spawn(async move {379
- let focus = TimelineFocus::Thread {380
- root_event_id: build_root.clone(),398
+ let focus = match &build_id {399
+ TimelineId::Thread { root_event_id, .. } => TimelineFocus::Thread {400
+ root_event_id: root_event_id.clone(),401
+ },402
+ _ => TimelineFocus::PinnedEvents,381403
};382
- (build_root, generation, build_timeline(&room, focus).await)404
+ (build_id, generation, build_timeline(&room, focus).await)383405
});384406
self.opening.insert(385
- root.clone(),407
+ id.clone(),386408
OpeningThread {387409
generation,388410
build,@@ -390,20 +412,15 @@ impl Threads {
390412
},391413
);392414
}393
- self.enqueue(&root, None, reply);415
+ self.enqueue(&id, None, reply);394416
}395417
396
- fn route(397
- &mut self,398
- root: OwnedEventId,399
- command: TimelineCommand,400
- reply: Responder,401
- ) -> Routed<'_> {402
- if !self.open.contains_key(&root) {403
- self.enqueue(&root, Some(command), reply);418
+ fn route(&mut self, id: TimelineId, command: TimelineCommand, reply: Responder) -> Routed<'_> {419
+ if !self.open.contains_key(&id) {420
+ self.enqueue(&id, Some(command), reply);404421
return Routed::Queued;405422
}406
- let session = self.open.get_mut(&root).expect("thread was just checked");423
+ let session = self.open.get_mut(&id).expect("timeline was just checked");407424
Routed::Open(session, command, reply)408425
}409426
@@ -412,36 +429,36 @@ impl Threads {
412429
&mut self,413430
built: Result<Built, JoinError>,414431
) -> Option<(415
- OwnedEventId,432
+ TimelineId,416433
Queued,417434
Result<Timeline, matrix_sdk_ui::timeline::Error>,418435
)> {419
- let (root, generation, timeline) = match built {436
+ let (id, generation, timeline) = match built {420437
Ok(built) => built,421438
Err(error) if error.is_cancelled() => return None,422439
Err(error) => resume_unwind(error),423440
};424441
if self425442
.opening426
- .get(&root)443
+ .get(&id)427444
.is_none_or(|opening| opening.generation != generation)428445
{429446
return None;430447
}431
- let opening = self.opening.remove(&root)?;448
+ let opening = self.opening.remove(&id)?;432449
self.queued -= opening.queued.len();433
- Some((root, opening.queued, timeline))450
+ Some((id, opening.queued, timeline))434451
}435452
436
- async fn close_one(&mut self, root: &OwnedEventId) -> Result<(), Error> {437
- if let Some(session) = self.open.remove(root) {453
+ async fn close_one(&mut self, id: &TimelineId) -> Result<(), Error> {454
+ if let Some(session) = self.open.remove(id) {438455
session.close().await;439456
return Ok(());440457
}441
- let Some(opening) = self.opening.remove(root) else {458
+ let Some(opening) = self.opening.remove(id) else {442459
return Err(Error::new(443460
ErrorKind::TimelineNotOpen,444
- "thread timeline is not open",461
+ "timeline is not open",445462
));446463
};447464
opening.build.abort();@@ -401,22 +401,38 @@ pub(super) async fn encryption(client: Client, events: EventSender) {
401401
}402402
}403403
404
-/// space orders are room account data, which never touches room info404
+/// space orders, pins and power levels all feed summaries without raising a notable room info update405405
pub(super) async fn rooms(client: Client, events: EventSender) {406406
let (_, mut rooms) = client.rooms_stream();407407
let mut info = client.room_info_notable_update_receiver();408
- let order_changed = Arc::new(Notify::new());409
- let handler = client.add_event_handler({410
- let order_changed = order_changed.clone();411
- move |event: Raw<AnyRoomAccountDataEvent>| {412
- let order_changed = order_changed.clone();413
- async move {414
- if event_type(&event).as_deref() == Some(space_order::EVENT_TYPE) {415
- order_changed.notify_one();408
+ let changed = Arc::new(Notify::new());409
+ let handlers = [410
+ client.add_event_handler({411
+ let changed = changed.clone();412
+ move |event: Raw<AnyRoomAccountDataEvent>| {413
+ let changed = changed.clone();414
+ async move {415
+ if event_type(&event).as_deref() == Some(space_order::EVENT_TYPE) {416
+ changed.notify_one();417
+ }416418
}417419
}418
- }419
- });420
+ }),421
+ client.add_event_handler({422
+ let changed = changed.clone();423
+ move |event: Raw<AnySyncStateEvent>| {424
+ let changed = changed.clone();425
+ async move {426
+ if matches!(427
+ event_type(&event).as_deref(),428
+ Some("m.room.pinned_events" | "m.room.power_levels")429
+ ) {430
+ changed.notify_one();431
+ }432
+ }433
+ }434
+ }),435
+ ];420436
loop {421437
if events.send(Event::RoomsChanged).await.is_err() {422438
break;@@ -424,14 +440,16 @@ pub(super) async fn rooms(client: Client, events: EventSender) {
424440
tokio::select! {425441
update = rooms.next() => if update.is_none() { break },426442
update = info.recv() => if matches!(update, Err(RecvError::Closed)) { break },427
- () = order_changed.notified() => {}443
+ () = changed.notified() => {}428444
}429445
// a sync touching fifty rooms is one invalidation, not fifty430446
while let Ok(_) | Err(tokio::sync::broadcast::error::TryRecvError::Lagged(_)) =431447
info.try_recv()432448
{}433449
}434
- client.remove_event_handler(handler);450
+ for handler in handlers {451
+ client.remove_event_handler(handler);452
+ }435453
}436454
437455
/// packs live in room state and account data, and leaving a room takes its packs with it@@ -115,6 +115,12 @@ pub enum RoomCommand {
115115
user_id: OwnedUserId,116116
muted: bool,117117
},118
+ /// pinning appends, so the newest pin is last like everywhere else119
+ SetPinned {120
+ room_id: OwnedRoomId,121
+ event_id: OwnedEventId,122
+ pinned: bool,123
+ },118124
/// adding m.favourite drops m.lowpriority, the way Element does119125
SetFavourite {120126
room_id: OwnedRoomId,@@ -240,6 +246,10 @@ pub enum TimelineCommand {
240246
room_id: OwnedRoomId,241247
root_event_id: OwnedEventId,242248
},249
+ /// the room's pinned events as a timeline of their own, kept current as pins change.250
+ /// it can't send or paginate, the pins are all it holds251
+ OpenPinned(OwnedRoomId),252
+ ClosePinned(OwnedRoomId),243253
/// Re-emit an authoritative reset for an open room or thread without244254
/// closing it. The reset arrives before this command's terminal result.245255
Resync {@@ -282,7 +292,10 @@ impl TimelineCommand {
282292
#[must_use]283293
pub const fn room_id(&self) -> &OwnedRoomId {284294
match self {285
- Self::OpenThread { room_id, .. } | Self::CloseThread { room_id, .. } => room_id,295
+ Self::OpenThread { room_id, .. }296
+ | Self::CloseThread { room_id, .. }297
+ | Self::OpenPinned(room_id)298
+ | Self::ClosePinned(room_id) => room_id,286299
Self::Resync { timeline_id }287300
| Self::Paginate { timeline_id, .. }288301
| Self::Send { timeline_id, .. }@@ -296,7 +309,10 @@ impl TimelineCommand {
296309
#[must_use]297310
pub const fn timeline_id(&self) -> Option<&TimelineId> {298311
match self {299
- Self::OpenThread { .. } | Self::CloseThread { .. } => None,312
+ Self::OpenThread { .. }313
+ | Self::CloseThread { .. }314
+ | Self::OpenPinned(_)315
+ | Self::ClosePinned(_) => None,300316
Self::Resync { timeline_id }301317
| Self::Paginate { timeline_id, .. }302318
| Self::Send { timeline_id, .. }@@ -317,13 +333,14 @@ pub enum TimelineId {
317333
room_id: OwnedRoomId,318334
root_event_id: OwnedEventId,319335
},336
+ Pinned(OwnedRoomId),320337
}321338
322339
impl TimelineId {323340
#[must_use]324341
pub const fn room_id(&self) -> &OwnedRoomId {325342
match self {326
- Self::Room(room_id) | Self::Thread { room_id, .. } => room_id,343
+ Self::Room(room_id) | Self::Thread { room_id, .. } | Self::Pinned(room_id) => room_id,327344
}328345
}329346
@@ -338,7 +355,7 @@ impl TimelineId {
338355
#[must_use]339356
pub fn thread_root(&self) -> Option<&OwnedEventId> {340357
match self {341
- Self::Room(_) => None,358
+ Self::Room(_) | Self::Pinned(_) => None,342359
Self::Thread { root_event_id, .. } => Some(root_event_id),343360
}344361
}@@ -974,6 +991,9 @@ pub struct RoomSummary {
974991
pub space_order: Option<String>,975992
/// you can send im.ponies.room_emotes here, so you can make and edit its packs976993
pub can_edit_packs: bool,994
+ /// m.room.pinned_events in order, the newest pin last995
+ pub pinned_events: Vec<OwnedEventId>,996
+ pub can_pin: bool,977997
}978998
979999
#[derive(Clone, Debug, Eq, PartialEq)]@@ -4,7 +4,8 @@ use matrix_sdk::{
44
deserialized_responses::SyncOrStrippedState,55
latest_events::LatestEventValue,66
ruma::events::{7
- AnyMessageLikeEventContent, AnySyncMessageLikeEvent, AnySyncTimelineEvent, SyncStateEvent,7
+ AnyMessageLikeEventContent, AnySyncMessageLikeEvent, AnySyncTimelineEvent, StateEventType,8
+ SyncStateEvent,89
room::message::{Relation, RoomMessageEventContent},910
space::child::SpaceChildEventContent,1011
},@@ -108,6 +109,11 @@ async fn room_summary(room: &Room, spaces: &SpaceService) -> RoomSummary {
108109
None109110
},110111
can_edit_packs: super::can_edit_packs(room).await,112
+ pinned_events: room.pinned_event_ids().unwrap_or_default(),113
+ can_pin: room.state() == RoomState::Joined114
+ && room.power_levels().await.is_ok_and(|levels| {115
+ levels.user_can_send_state(room.own_user_id(), StateEventType::RoomPinnedEvents)116
+ }),111117
}112118
}113119
@@ -286,6 +286,32 @@ impl ClientHandle {
286286
.await287287
}288288
289
+ /// the room's pins as `TimelineId::Pinned`, kept current as they change. the room has to be open290
+ pub async fn open_pinned(&self, room_id: OwnedRoomId) -> Result<(), Error> {291
+ self.run(Command::Timeline(TimelineCommand::OpenPinned(room_id)))292
+ .await293
+ }294
+295
+ pub async fn close_pinned(&self, room_id: OwnedRoomId) -> Result<(), Error> {296
+ self.run(Command::Timeline(TimelineCommand::ClosePinned(room_id)))297
+ .await298
+ }299
+300
+ /// reads the server's list so a pin made elsewhere just before isn't dropped301
+ pub async fn set_pinned(302
+ &self,303
+ room_id: OwnedRoomId,304
+ event_id: OwnedEventId,305
+ pinned: bool,306
+ ) -> Result<(), Error> {307
+ self.run(Command::Room(RoomCommand::SetPinned {308
+ room_id,309
+ event_id,310
+ pinned,311
+ }))312
+ .await313
+ }314
+289315
pub async fn resync(&self, timeline_id: TimelineId) -> Result<(), Error> {290316
self.run(Command::Timeline(TimelineCommand::Resync { timeline_id }))291317
.await