atagen

malice

public
Star0
Watch0

Commit 77eddb6b710f

tarball

atagen <boss@atagen.co>2026-10-24 10:05 UTC

3 files changed+66-0
core: expose the pinned timeline over the wire

Pinned items travel as their own Pinned and PinnedClosed events instead of riding the room Timeline events, so the frontend never mixes pins into the room timeline.
Modified file: crates/malice-core/src/feed.rs+34 additions
@@ -14,6 +14,7 @@ pub enum WireEvent {
1414 Snapshot {
1515 rooms: Vec<RoomSummary>,
1616 timelines: HashMap<String, Vector<TimelineItem>>,
17+ pinned: HashMap<String, Vector<TimelineItem>>,
1718 connection: String,
1819 encryption: Option<EncryptionState>,
1920 verification: Option<VerificationState>,
@@ -32,6 +33,13 @@ pub enum WireEvent {
3233 TimelineClosed {
3334 room_id: String,
3435 },
36+ Pinned {
37+ room_id: String,
38+ items: Vector<TimelineItem>,
39+ },
40+ PinnedClosed {
41+ room_id: String,
42+ },
3543 Connection {
3644 state: String,
3745 },
@@ -84,9 +92,20 @@ pub fn snapshot_event(state: &State) -> WireEvent {
8492 _ => None,
8593 })
8694 .collect();
95+ let pinned = state
96+ .timelines
97+ .iter()
98+ .filter_map(|(timeline_id, timeline)| match timeline_id {
99+ TimelineId::Pinned(room_id) => {
100+ Some((room_id.as_str().to_owned(), timeline.items.clone()))
101+ }
102+ _ => None,
103+ })
104+ .collect();
87105 WireEvent::Snapshot {
88106 rooms: state.rooms.iter().cloned().collect(),
89107 timelines,
108+ pinned,
90109 connection: connection_label(&state.connection),
91110 encryption: state.encryption.clone(),
92111 verification: state.verification.clone(),
@@ -129,6 +148,21 @@ pub fn wire_events(change: &StateChange) -> Vec<WireEvent> {
129148 }],
130149 _ => vec![],
131150 },
151+ StateChange::Timeline {
152+ timeline_id: TimelineId::Pinned(room_id),
153+ change,
154+ } => match change {
155+ TimelineChange::Reset { items } | TimelineChange::Changed { items, .. } => {
156+ vec![WireEvent::Pinned {
157+ room_id: room_id.as_str().to_owned(),
158+ items: items.clone(),
159+ }]
160+ }
161+ TimelineChange::Closed => vec![WireEvent::PinnedClosed {
162+ room_id: room_id.as_str().to_owned(),
163+ }],
164+ _ => vec![],
165+ },
132166 StateChange::Timeline { .. } => vec![],
133167 StateChange::Connection(state) => vec![WireEvent::Connection {
134168 state: connection_label(state),
Modified file: crates/malice-core/src/request.rs+29 additions
@@ -227,6 +227,17 @@ pub enum Request {
227227 PowerLevels {
228228 room_id: String,
229229 },
230+ OpenPinned {
231+ room_id: String,
232+ },
233+ ClosePinned {
234+ room_id: String,
235+ },
236+ SetPinned {
237+ room_id: String,
238+ event_id: String,
239+ pinned: bool,
240+ },
230241 UploadMedia {
231242 #[serde(skip)]
232243 bytes: Vec<u8>,
@@ -988,6 +999,24 @@ pub async fn dispatch(
988999 .await?;
9891000 Ok(Reply::Json(json!({ "skipped": skipped })))
9901001 }
1002+ Request::OpenPinned { room_id } => {
1003+ handle.open_pinned(parse_room_id(&room_id)?).await?;
1004+ Ok(Reply::Done)
1005+ }
1006+ Request::ClosePinned { room_id } => {
1007+ handle.close_pinned(parse_room_id(&room_id)?).await?;
1008+ Ok(Reply::Done)
1009+ }
1010+ Request::SetPinned {
1011+ room_id,
1012+ event_id,
1013+ pinned,
1014+ } => {
1015+ handle
1016+ .set_pinned(parse_room_id(&room_id)?, parse_event_id(&event_id)?, pinned)
1017+ .await?;
1018+ Ok(Reply::Done)
1019+ }
9911020 Request::PowerLevels { room_id } => {
9921021 let levels = handle.room_power_levels(&parse_room_id(&room_id)?).await?;
9931022 let value = serde_json::to_value(levels)
Modified file: crates/malice-server/src/routes.rs+3 additions
@@ -549,6 +549,9 @@ async fn post_command(
549549 | Request::Ignore { .. }
550550 | Request::Unignore { .. }
551551 | Request::PowerLevels { .. }
552+ | Request::OpenPinned { .. }
553+ | Request::ClosePinned { .. }
554+ | Request::SetPinned { .. }
552555 | Request::SendSticker { .. }
553556 | Request::CreateRoomPack { .. }
554557 | Request::DeletePack { .. }