Skip to content

Commit 6088f5a

Browse files
committed
added populate_small_state_group_header
1 parent cd3fe5e commit 6088f5a

File tree

3 files changed

+210
-50
lines changed

3 files changed

+210
-50
lines changed

src/home/event_group.rs

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -14,54 +14,6 @@ const MIN_GROUP_SIZE_FOR_COLLAPSE: usize = 3;
1414
// Maximum number of user names to display before coalescing
1515
const SUMMARY_LENGTH: usize = 4;
1616

17-
script_mod! {
18-
use mod.prelude.widgets.*
19-
use mod.widgets.*
20-
mod.widgets.SmallStateGroupHeader = FoldHeader{
21-
header:= View{
22-
width: Fill,
23-
height: Fit,
24-
flow: Flow.Down{wrap: true},
25-
margin: Inset{top: 5.0}
26-
spacing: 8.0,
27-
padding: Inset{
28-
left: 7.0,
29-
top: 2.0,
30-
bottom: 2.0
31-
}
32-
View {
33-
width: Fill, height: Fit
34-
user_event_avatar_row := AvatarRow {
35-
margin: Inset{
36-
left: 10.0,
37-
top: 0.0
38-
}
39-
}
40-
summary_text := Label {
41-
width: Fill, height: Fit
42-
flow: Flow.RightWrap,
43-
padding: 0,
44-
draw_text: {
45-
wrap: WrapMode::Word,
46-
text_style: THEME_FONT_REGULAR{
47-
font_size: SMALL_STATE_FONT_SIZE
48-
},
49-
color: SMALL_STATE_TEXT_COLOR
50-
}
51-
}
52-
}
53-
View {
54-
width: Fill, height: Fit,
55-
flow: Flow.Right,
56-
align: Align{ x: 0.5, y: 0.5 },
57-
padding: Inset{top: 4.0},
58-
fold_button := FoldButton {}
59-
}
60-
}
61-
body:= View{}
62-
}
63-
}
64-
6517
/// Represents a group of adjacent small state events that can be collapsed/expanded in the UI.
6618
///
6719
/// This struct encapsulates the grouping logic for small state events (membership changes,

src/home/mod.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,5 +60,4 @@ pub fn script_mod(vm: &mut ScriptVm) {
6060
navigation_tab_bar::script_mod(vm);
6161
// Keep HomeScreen last, it references many widgets registered above.
6262
home_screen::script_mod(vm);
63-
event_group::script_mod(vm);
6463
}

src/home/room_screen.rs

Lines changed: 210 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ use matrix_sdk_ui::timeline::{
2626
use ruma::{OwnedUserId, api::client::receipt::create_receipt::v3::ReceiptType, events::{AnySyncMessageLikeEvent, AnySyncTimelineEvent, SyncMessageLikeEvent}, owned_room_id};
2727

2828
use crate::{
29-
app::{AppStateAction, ConfirmDeleteAction, SelectedRoom}, avatar_cache, event_preview::{plaintext_body_of_timeline_item, text_preview_of_encrypted_message, text_preview_of_member_profile_change, text_preview_of_other_message_like, text_preview_of_other_state, text_preview_of_room_membership_change, text_preview_of_timeline_item}, home::{edited_indicator::EditedIndicatorWidgetRefExt, link_preview::{LinkPreviewCache, LinkPreviewRef, LinkPreviewWidgetRefExt}, loading_pane::{LoadingPaneState, LoadingPaneWidgetExt}, room_image_viewer::{get_image_name_and_filesize, populate_matrix_image_modal}, rooms_list::{RoomsListAction, RoomsListRef}, tombstone_footer::SuccessorRoomDetails}, media_cache::{MediaCache, MediaCacheEntry}, profile::{
29+
app::{AppStateAction, ConfirmDeleteAction, SelectedRoom}, avatar_cache, event_preview::{plaintext_body_of_timeline_item, text_preview_of_encrypted_message, text_preview_of_member_profile_change, text_preview_of_other_message_like, text_preview_of_other_state, text_preview_of_room_membership_change, text_preview_of_timeline_item}, home::{edited_indicator::EditedIndicatorWidgetRefExt, event_group::extract_small_state_events, link_preview::{LinkPreviewCache, LinkPreviewRef, LinkPreviewWidgetRefExt}, loading_pane::{LoadingPaneState, LoadingPaneWidgetExt}, room_image_viewer::{get_image_name_and_filesize, populate_matrix_image_modal}, rooms_list::{RoomsListAction, RoomsListRef}, tombstone_footer::SuccessorRoomDetails}, media_cache::{MediaCache, MediaCacheEntry}, profile::{
3030
user_profile::{ShowUserProfileAction, UserProfile, UserProfileAndRoomId, UserProfilePaneInfo, UserProfileSlidingPaneRef, UserProfileSlidingPaneWidgetExt},
3131
user_profile_cache,
3232
},
@@ -535,9 +535,84 @@ script_mod! {
535535
ImageMessage := mod.widgets.ImageMessage {}
536536
CondensedImageMessage := mod.widgets.CondensedImageMessage {}
537537
SmallStateEvent := mod.widgets.SmallStateEvent {}
538+
SmallStateGroupHeader := mod.prelude.widgets.FoldHeader {
539+
header: View{
540+
width: Fill,
541+
height: Fit,
542+
flow: Right,
543+
align: Align{ x: 0.0, y: 0.5 },
544+
margin: Inset{top: 5.0, bottom: 5.0}
545+
spacing: 8.0,
546+
padding: Inset{
547+
left: 7.0,
548+
top: 2.0,
549+
bottom: 2.0,
550+
right: 10.0
551+
}
552+
fold_button := FoldButton {
553+
width: 25, height: 25
554+
draw_bg +: {
555+
color: uniform(#555)
556+
color_hover: uniform(#333)
557+
color_active: uniform(#444)
558+
// Make triangle larger
559+
pixel: fn() {
560+
let sz = 5.0
561+
let c = vec2(self.rect_size.x * 0.5, self.rect_size.y * 0.5)
562+
let sdf = Sdf2d.viewport(self.pos * self.rect_size)
563+
sdf.clear(vec4(0.))
564+
sdf.rotate(self.active * 0.5 * PI + 0.5 * PI, c.x, c.y)
565+
sdf.move_to(c.x - sz, c.y + sz)
566+
sdf.line_to(c.x, c.y - sz)
567+
sdf.line_to(c.x + sz, c.y + sz)
568+
sdf.close_path()
569+
sdf.fill(
570+
mix(
571+
mix(self.color, self.color_hover, self.hover)
572+
mix(self.color_active, self.color_hover, self.hover)
573+
self.active
574+
)
575+
)
576+
return sdf.result * self.fade
577+
}
578+
}
579+
}
580+
user_event_avatar_row := mod.widgets.AvatarRow {
581+
margin: Inset{
582+
left: 5.0,
583+
top: 0.0
584+
}
585+
}
586+
summary_text := Label {
587+
width: Fill, height: Fit
588+
flow: Flow.Right{wrap: true},
589+
draw_text+: {
590+
wrap: Line,
591+
color: (SMALL_STATE_TEXT_COLOR),
592+
text_style: SMALL_STATE_TEXT_COLOR { font_size: 11 },
593+
}
594+
}
595+
}
596+
body: View{
597+
width: Fill,
598+
height: Fit
599+
flow: Down
600+
padding: Inset{ left: 30.0 }
601+
PortalList{
602+
height: Fit, width: Fill
603+
SmallStateEvent := mod.widgets.SmallStateEvent {}
604+
CondensedMessage := mod.widgets.CondensedMessage {}
605+
Message := mod.widgets.Message {}
606+
}
607+
}
608+
}
538609
Empty := mod.widgets.Empty {}
539610
DateDivider := mod.widgets.DateDivider {}
540611
ReadMarker := mod.widgets.ReadMarker {}
612+
BottomSpace := View {
613+
height: 100.0
614+
width: Fill
615+
}
541616
}
542617

543618
// A jump to bottom button (with an unread message badge) that is shown
@@ -1074,6 +1149,46 @@ impl Widget for RoomScreen {
10741149
while let Some(item_id) = list.next_visible_item(cx) {
10751150
let item = {
10761151
let tl_idx = item_id;
1152+
if let Some(group_range) = tl_state.small_state_group_manager.check_group_range(item_id) {
1153+
if group_range.start == item_id {
1154+
// This is the first item of a group - render FoldHeader
1155+
if let (Some(group), Some(timeline_kind)) = (tl_state.small_state_group_manager.get_group_at_item_id(item_id), &self.timeline_kind) {
1156+
populate_small_state_group_header(
1157+
cx,
1158+
scope,
1159+
walk,
1160+
list,
1161+
item_id,
1162+
timeline_kind,
1163+
&group_range,
1164+
group,
1165+
tl_items,
1166+
&mut tl_state.content_drawn_since_last_update,
1167+
&mut tl_state.profile_drawn_since_last_update,
1168+
&mut tl_state.media_cache,
1169+
&mut tl_state.link_preview_cache,
1170+
&tl_state.fetched_thread_summaries,
1171+
&mut tl_state.pending_thread_summary_fetches,
1172+
&tl_state.user_power,
1173+
&self.pinned_events,
1174+
room_screen_widget_uid,
1175+
);
1176+
} else {
1177+
let item = list.item(cx, item_id, id!(Empty));
1178+
item.draw_all(cx, scope);
1179+
}
1180+
continue;
1181+
} else if group_range.contains(&item_id) {
1182+
let item = list.item(cx, item_id, id!(Empty));
1183+
item.draw_all(cx, scope);
1184+
continue;
1185+
}
1186+
}
1187+
if item_id > tl_items.len() {
1188+
let item = list.item(cx, item_id, id!(BottomSpace));
1189+
item.draw_all(cx, scope);
1190+
continue;
1191+
}
10771192
let Some(timeline_item) = tl_items.get(tl_idx) else {
10781193
// This shouldn't happen (unless the timeline gets corrupted or some other weird error),
10791194
// but we can always safely fill the item with an empty widget that takes up no space.
@@ -1263,6 +1378,8 @@ impl RoomScreen {
12631378

12641379
tl.items = initial_items;
12651380
done_loading = true;
1381+
let small_state_events = extract_small_state_events(tl.items.iter().cloned());
1382+
tl.small_state_group_manager.compute_group_state(small_state_events);
12661383
}
12671384
TimelineUpdate::NewItems { new_items, changed_indices, is_append, clear_cache } => {
12681385
if new_items.is_empty() {
@@ -1380,6 +1497,8 @@ impl RoomScreen {
13801497
// log!("process_timeline_updates(): changed_indices: {changed_indices:?}, items len: {}\ncontent drawn: {:#?}\nprofile drawn: {:#?}", items.len(), tl.content_drawn_since_last_update, tl.profile_drawn_since_last_update);
13811498
}
13821499
tl.items = new_items;
1500+
let small_state_events = extract_small_state_events(tl.items.iter().cloned());
1501+
tl.small_state_group_manager.compute_group_state(small_state_events);
13831502
done_loading = true;
13841503
}
13851504
TimelineUpdate::NewUnreadMessagesCount(unread_messages_count) => {
@@ -2254,6 +2373,7 @@ impl RoomScreen {
22542373
scrolled_past_read_marker: false,
22552374
latest_own_user_receipt: None,
22562375
tombstone_info,
2376+
small_state_group_manager: crate::home::event_group::SmallStateGroupManager::default(),
22572377
};
22582378
(tl_state, true)
22592379
};
@@ -2846,6 +2966,8 @@ struct TimelineUiState {
28462966
/// If `Some`, this room has been tombstoned and the details of its successor room
28472967
/// are contained within. If `None`, the room has not been tombstoned.
28482968
tombstone_info: Option<SuccessorRoomDetails>,
2969+
/// Manager for small state event groups that can be collapsed/expanded.
2970+
small_state_group_manager: crate::home::event_group::SmallStateGroupManager,
28492971
}
28502972

28512973
#[derive(Default, Debug)]
@@ -4172,6 +4294,93 @@ pub fn populate_preview_of_timeline_item(
41724294
widget_out.show_html(cx, html);
41734295
}
41744296

4297+
/// Creates and populates a SmallStateGroupHeader (FoldHeader) for a group of small state events.
4298+
///
4299+
/// This follows the portal_list_auto_grouping pattern:
4300+
/// - The header shows a summary and fold button
4301+
/// - The body contains a ViewList with all grouped items (excluding the first one)
4302+
///
4303+
/// # Arguments
4304+
/// * `group_range` - The range of timeline indices covered by this group
4305+
/// * `group` - The SmallStateGroup metadata containing cached summary and user IDs
4306+
/// * `tl_items` - The full list of timeline items
4307+
fn populate_small_state_group_header(
4308+
cx: &mut Cx2d,
4309+
scope: &mut Scope,
4310+
walk: Walk,
4311+
list: &mut PortalList,
4312+
item_id: usize,
4313+
timeline_kind: &TimelineKind,
4314+
group_range: &std::ops::Range<usize>,
4315+
group: &crate::home::event_group::SmallStateGroup,
4316+
tl_items: &imbl::Vector<Arc<TimelineItem>>,
4317+
content_drawn_since_last_update: &mut RangeSet<usize>,
4318+
profile_drawn_since_last_update: &mut RangeSet<usize>,
4319+
media_cache: &mut MediaCache,
4320+
link_preview_cache: &mut LinkPreviewCache,
4321+
fetched_thread_summaries: &HashMap<OwnedEventId, FetchedThreadSummary>,
4322+
pending_thread_summary_fetches: &mut HashSet<OwnedEventId>,
4323+
user_power_levels: &UserPowerLevels,
4324+
pinned_events: &[OwnedEventId],
4325+
room_screen_widget_uid: WidgetUid,
4326+
) {
4327+
let (fold_item, _existed) = list.item_with_existed(cx, item_id, id!(SmallStateGroupHeader));
4328+
// Set the header summary text
4329+
if let Some(summary) = &group.cached_summary {
4330+
fold_item.label(cx, ids!(summary_text)).set_text(cx, summary);
4331+
}
4332+
4333+
// Set the avatars in the header
4334+
if let Some(user_ids) = &group.cached_avatar_user_ids {
4335+
crate::home::event_group::populate_avatar_row_from_user_ids(
4336+
cx,
4337+
&fold_item,
4338+
timeline_kind,
4339+
user_ids,
4340+
);
4341+
}
4342+
let mut walk = walk;
4343+
walk.height = Size::fit();
4344+
while let Some(item) = fold_item.draw_walk(cx, scope, walk).step() {
4345+
if let Some(mut list_ref) = item.as_portal_list().borrow_mut() {
4346+
let list = list_ref.deref_mut();
4347+
for tl_idx in (group_range.start)..group_range.end {
4348+
if let Some(timeline_item) = tl_items.get(tl_idx) {
4349+
let item_drawn_status = ItemDrawnStatus {
4350+
content_drawn: content_drawn_since_last_update.contains(&tl_idx),
4351+
profile_drawn: profile_drawn_since_last_update.contains(&tl_idx),
4352+
};
4353+
if let TimelineItemKind::Event(event_tl_item) = timeline_item.kind() {
4354+
// Create a new SmallStateEvent view from the template
4355+
let (item, item_drawn_status) = match event_tl_item.content() {
4356+
TimelineItemContent::MsgLike(msg_like_content) => match &msg_like_content.kind {
4357+
MsgLikeKind::Redacted => {
4358+
let prev_event = tl_idx.checked_sub(1).and_then(|i| tl_items.get(i));
4359+
populate_message_view(cx, list, tl_idx, timeline_kind, event_tl_item, msg_like_content, prev_event, media_cache, link_preview_cache, fetched_thread_summaries, pending_thread_summary_fetches, user_power_levels, pinned_events, item_drawn_status, room_screen_widget_uid)
4360+
}
4361+
MsgLikeKind::Poll(poll_state) => populate_small_state_event(cx, list, tl_idx, timeline_kind, event_tl_item, poll_state, item_drawn_status),
4362+
MsgLikeKind::UnableToDecrypt(utd) => populate_small_state_event(cx, list, tl_idx, timeline_kind, event_tl_item, utd, item_drawn_status),
4363+
MsgLikeKind::Other(other) => populate_small_state_event(cx, list, tl_idx, timeline_kind, event_tl_item, other, item_drawn_status),
4364+
_ => (list.item_with_existed(cx, tl_idx, id!(Empty)).0, item_drawn_status)
4365+
},
4366+
TimelineItemContent::MembershipChange(membership_change) => populate_small_state_event(cx, list, tl_idx, timeline_kind, event_tl_item, membership_change, item_drawn_status),
4367+
TimelineItemContent::ProfileChange(profile_change) => populate_small_state_event(cx, list, tl_idx, timeline_kind, event_tl_item, profile_change, item_drawn_status),
4368+
TimelineItemContent::OtherState(other_state) => populate_small_state_event(cx, list, tl_idx, timeline_kind, event_tl_item, other_state, item_drawn_status),
4369+
_=> (list.item_with_existed(cx, tl_idx, id!(Empty)).0, item_drawn_status)
4370+
};
4371+
if item_drawn_status.content_drawn {
4372+
content_drawn_since_last_update.insert(tl_idx..tl_idx + 1);
4373+
}
4374+
if item_drawn_status.profile_drawn {
4375+
profile_drawn_since_last_update.insert(tl_idx..tl_idx + 1);
4376+
}
4377+
item.draw_all(cx, scope);
4378+
}
4379+
}
4380+
}
4381+
}
4382+
}
4383+
}
41754384

41764385
/// A trait for abstracting over the different types of timeline events
41774386
/// that can be displayed in a `SmallStateEvent` widget.

0 commit comments

Comments
 (0)