Skip to content

Commit 0ba2574

Browse files
committed
task(ui): Add functions to help computing the count for Skip.
This patch adds functions like `compute_count`, `compute_count_when_paginating_backwards` and `compute_count_when_paginating_forwards`, which are necessary to correctly compute the `count` value for the `Skip` higher-order stream. This patch adds the associated test suite.
1 parent e7ee239 commit 0ba2574

File tree

2 files changed

+341
-1
lines changed

2 files changed

+341
-1
lines changed

crates/matrix-sdk-ui/src/timeline/controller/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ impl Default for TimelineSettings {
170170
}
171171

172172
#[derive(Debug, Clone, Copy)]
173-
enum TimelineFocusKind {
173+
pub(super) enum TimelineFocusKind {
174174
Live,
175175
Event,
176176
PinnedEvents,

crates/matrix-sdk-ui/src/timeline/subscriber.rs

+340
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,343 @@ where
4747
self.project().inner.poll_next(cx)
4848
}
4949
}
50+
51+
pub mod skip {
52+
use eyeball::SharedObservable;
53+
use futures_core::Stream;
54+
55+
use super::super::controller::TimelineFocusKind;
56+
57+
const MAXIMUM_NUMBER_OF_INITIAL_ITEMS: usize = 20;
58+
59+
#[derive(Clone)]
60+
pub struct SkipCount {
61+
count: SharedObservable<usize>,
62+
}
63+
64+
impl SkipCount {
65+
pub fn new() -> Self {
66+
Self { count: SharedObservable::new(0) }
67+
}
68+
69+
/// Compute the `count` value for [the `Skip` higher-order
70+
/// stream][`Skip`].
71+
///
72+
/// This is useful when new items are inserted, removed and so on.
73+
///
74+
/// [`Skip`]: eyeball_im_util::vector::Skip
75+
pub fn compute_next(
76+
&self,
77+
previous_number_of_items: usize,
78+
next_number_of_items: usize,
79+
) -> usize {
80+
let current_count = self.count.get();
81+
82+
// Initial states: no items is present.
83+
if previous_number_of_items == 0 {
84+
// Adjust the count to provide a maximum number of initial items. We want to
85+
// skip the first items until we get a certain number of items to display.
86+
//
87+
// | `next_number_of_items` | `MAX…` | output | will display |
88+
// |------------------------|--------|--------|--------------|
89+
// | 60 | 20 | 40 | 20 items |
90+
// | 10 | 20 | 0 | 10 items |
91+
// | 0 | 20 | 0 | 0 item |
92+
//
93+
next_number_of_items.saturating_sub(MAXIMUM_NUMBER_OF_INITIAL_ITEMS)
94+
}
95+
// Not the initial state: there are items.
96+
else {
97+
// There is less items than before. Shift to the left `count` by the difference
98+
// between `previous_number_of_items` and `next_number_of_items` to keep the
99+
// same number of items in the stream as much as possible.
100+
//
101+
// This is not a backwards pagination, it cannot “go below 0”, however this is
102+
// necessary to handle the case where the timeline is cleared and
103+
// the number of items becomes 0 for example.
104+
if next_number_of_items < previous_number_of_items {
105+
current_count.saturating_sub(previous_number_of_items - next_number_of_items)
106+
}
107+
// Return `current_count` with no modification, we don't want to adjust the
108+
// count, we want to see all initial items and new items.
109+
else {
110+
current_count
111+
}
112+
}
113+
}
114+
115+
/// Compute the `count` value for [the `Skip` higher-order
116+
/// stream][`Skip`] when a backwards pagination is happening.
117+
///
118+
/// It returns the new value for `count` in addition to
119+
/// `Some(number_of_items)` to fulfill the page up to `page_size`,
120+
/// `None` otherwise. For example, assuming a `page_size` of 15,
121+
/// if the `count` moves from 10 to 0, then 10 new items will
122+
/// appear in the stream, but 5 are missing because they aren't
123+
/// present in the stream: the stream has reached its beginning:
124+
/// `Some(5)` will be returned. This is useful
125+
/// for the pagination mechanism to fill the timeline with more items,
126+
/// either from a storage, or from the network.
127+
///
128+
/// [`Skip`]: eyeball_im_util::vector::Skip
129+
pub fn compute_next_when_paginating_backwards(
130+
&self,
131+
page_size: usize,
132+
) -> (usize, Option<usize>) {
133+
let current_count = self.count.get();
134+
135+
// We skip the values from the start of the timeline; paginating backwards means
136+
// we have to reduce the count until reaching 0.
137+
//
138+
// | `current_count` | `page_size` | output |
139+
// |-----------------|-------------|----------------|
140+
// | 50 | 20 | (30, None) |
141+
// | 30 | 20 | (10, None) |
142+
// | 10 | 20 | (0, Some(10)) |
143+
// | 0 | 20 | (0, Some(20)) |
144+
// ^ ^^^^^^^^
145+
// | |
146+
// | it needs 20 items to fulfill the
147+
// | page size
148+
// count becomes 0
149+
//
150+
if current_count >= page_size {
151+
(current_count - page_size, None)
152+
} else {
153+
(0, Some(page_size - current_count))
154+
}
155+
}
156+
157+
/// Compute the `count` value for [the `Skip` higher-order
158+
/// stream][`Skip`] when a backwards pagination is happening.
159+
///
160+
/// The `page_size` is present to mimic the
161+
/// [`compute_count_when_paginating_backwards`] function but it is
162+
/// actually useless for the current implementation.
163+
///
164+
/// [`Skip`]: eyeball_im_util::vector::Skip
165+
pub fn compute_next_when_paginating_forwards(&self, _page_size: usize) -> usize {
166+
let current_count = self.count.get();
167+
168+
// Nothing to do, the count remains unchanged as we skip the first values, not
169+
// the last values; paginating forwards will add items at the end, not at the
170+
// start of the timeline.
171+
current_count
172+
}
173+
174+
/// Subscribe to update of the count value.
175+
pub fn subscribe(&self) -> impl Stream<Item = usize> {
176+
self.count.subscribe()
177+
}
178+
179+
/// Update the skip count if and only if the timeline has a live focus
180+
/// ([`TimelineFocusKind::Live`]).
181+
pub fn update(&self, count: usize, focus_kind: &TimelineFocusKind) {
182+
if matches!(focus_kind, TimelineFocusKind::Live) {
183+
self.count.set_if_not_eq(count);
184+
}
185+
}
186+
}
187+
188+
#[cfg(test)]
189+
mod tests {
190+
use super::SkipCount;
191+
192+
#[test]
193+
fn test_compute_count_from_underflowing_initial_states() {
194+
let skip_count = SkipCount::new();
195+
196+
// Initial state with too few new items. None is skipped.
197+
let previous_number_of_items = 0;
198+
let next_number_of_items = previous_number_of_items + 10;
199+
let count = skip_count.compute_next(previous_number_of_items, next_number_of_items);
200+
assert_eq!(count, 0);
201+
skip_count.count.set(count);
202+
203+
// Add 5 new items. The count stays at 0 because we don't want to skip the
204+
// previous items.
205+
let previous_number_of_items = next_number_of_items;
206+
let next_number_of_items = previous_number_of_items + 5;
207+
let count = skip_count.compute_next(previous_number_of_items, next_number_of_items);
208+
assert_eq!(count, 0);
209+
skip_count.count.set(count);
210+
211+
// Add 20 new items. The count stays at 0 because we don't want to
212+
// skip the previous items.
213+
let previous_number_of_items = next_number_of_items;
214+
let next_number_of_items = previous_number_of_items + 20;
215+
let count = skip_count.compute_next(previous_number_of_items, next_number_of_items);
216+
assert_eq!(count, 0);
217+
skip_count.count.set(count);
218+
219+
// Remove a certain number of items. The count stays at 0 because it was
220+
// previously 0, no items are skipped, nothing to adjust.
221+
let previous_number_of_items = next_number_of_items;
222+
let next_number_of_items = previous_number_of_items - 4;
223+
let count = skip_count.compute_next(previous_number_of_items, next_number_of_items);
224+
assert_eq!(count, 0);
225+
skip_count.count.set(count);
226+
227+
// Remove all items. The count goes to 0 (regardless it was 0 before).
228+
let previous_number_of_items = next_number_of_items;
229+
let next_number_of_items = 0;
230+
let count = skip_count.compute_next(previous_number_of_items, next_number_of_items);
231+
assert_eq!(count, 0);
232+
}
233+
234+
#[test]
235+
fn test_compute_count_from_overflowing_initial_states() {
236+
let skip_count = SkipCount::new();
237+
238+
// Initial state with too much new items. Some are skipped.
239+
let previous_number_of_items = 0;
240+
let next_number_of_items = previous_number_of_items + 30;
241+
let count = skip_count.compute_next(previous_number_of_items, next_number_of_items);
242+
assert_eq!(count, 10);
243+
skip_count.count.set(count);
244+
245+
// Add 5 new items. The count stays at 10 because we don't want to skip the
246+
// previous items.
247+
let previous_number_of_items = next_number_of_items;
248+
let next_number_of_items = previous_number_of_items + 5;
249+
let count = skip_count.compute_next(previous_number_of_items, next_number_of_items);
250+
assert_eq!(count, 10);
251+
skip_count.count.set(count);
252+
253+
// Add 20 new items. The count stays at 10 because we don't want to
254+
// skip the previous items.
255+
let previous_number_of_items = next_number_of_items;
256+
let next_number_of_items = previous_number_of_items + 20;
257+
let count = skip_count.compute_next(previous_number_of_items, next_number_of_items);
258+
assert_eq!(count, 10);
259+
skip_count.count.set(count);
260+
261+
// Remove a certain number of items. The count is reduced by 5 so that the same
262+
// number of items are presented.
263+
let previous_number_of_items = next_number_of_items;
264+
let next_number_of_items = previous_number_of_items - 4;
265+
let count = skip_count.compute_next(previous_number_of_items, next_number_of_items);
266+
assert_eq!(count, 6);
267+
skip_count.count.set(count);
268+
269+
// Remove all items. The count goes to 0 (regardless it was 6 before).
270+
let previous_number_of_items = next_number_of_items;
271+
let next_number_of_items = 0;
272+
let count = skip_count.compute_next(previous_number_of_items, next_number_of_items);
273+
assert_eq!(count, 0);
274+
}
275+
276+
#[test]
277+
fn test_compute_count_when_paginating_backwards_from_underflowing_initial_states() {
278+
let skip_count = SkipCount::new();
279+
280+
// Initial state with too few new items. None is skipped.
281+
let previous_number_of_items = 0;
282+
let next_number_of_items = previous_number_of_items + 10;
283+
let count = skip_count.compute_next(previous_number_of_items, next_number_of_items);
284+
assert_eq!(count, 0);
285+
skip_count.count.set(count);
286+
287+
// Add 30 new items. The count stays at 0 because we don't want to skip the
288+
// previous items.
289+
let previous_number_of_items = next_number_of_items;
290+
let next_number_of_items = previous_number_of_items + 30;
291+
let count = skip_count.compute_next(previous_number_of_items, next_number_of_items);
292+
assert_eq!(count, 0);
293+
skip_count.count.set(count);
294+
295+
let page_size = 20;
296+
297+
// Paginate backwards.
298+
let (count, needs) = skip_count.compute_next_when_paginating_backwards(page_size);
299+
assert_eq!(count, 0);
300+
assert_eq!(needs, Some(20));
301+
}
302+
303+
#[test]
304+
fn test_compute_count_when_paginating_backwards_from_overflowing_initial_states() {
305+
let skip_count = SkipCount::new();
306+
307+
// Initial state with too much new items. Some are skipped.
308+
let previous_number_of_items = 0;
309+
let next_number_of_items = previous_number_of_items + 50;
310+
let count = skip_count.compute_next(previous_number_of_items, next_number_of_items);
311+
assert_eq!(count, 30);
312+
skip_count.count.set(count);
313+
314+
// Add 30 new items. The count stays at 30 because we don't want to
315+
// skip the previous items.
316+
let previous_number_of_items = next_number_of_items;
317+
let next_number_of_items = previous_number_of_items + 30;
318+
let count = skip_count.compute_next(previous_number_of_items, next_number_of_items);
319+
assert_eq!(count, 30);
320+
skip_count.count.set(count);
321+
322+
let page_size = 20;
323+
324+
// Paginate backwards. The count shifts by `page_size`, and the page is full.
325+
let (count, needs) = skip_count.compute_next_when_paginating_backwards(page_size);
326+
assert_eq!(count, 10);
327+
assert_eq!(needs, None);
328+
skip_count.count.set(count);
329+
330+
// Paginate backwards. The count shifts by `page_size` but reaches 0 before the
331+
// page becomes full. It needs 10 more items to fulfill the page.
332+
let (count, needs) = skip_count.compute_next_when_paginating_backwards(page_size);
333+
assert_eq!(count, 0);
334+
assert_eq!(needs, Some(10));
335+
}
336+
337+
#[test]
338+
fn test_compute_count_when_paginating_forwards_from_underflowing_initial_states() {
339+
let skip_count = SkipCount::new();
340+
341+
// Initial state with too few new items. None is skipped.
342+
let previous_number_of_items = 0;
343+
let next_number_of_items = previous_number_of_items + 10;
344+
let count = skip_count.compute_next(previous_number_of_items, next_number_of_items);
345+
assert_eq!(count, 0);
346+
skip_count.count.set(count);
347+
348+
// Add 30 new items. The count stays at 0 because we don't want to skip the
349+
// previous items.
350+
let previous_number_of_items = next_number_of_items;
351+
let next_number_of_items = previous_number_of_items + 30;
352+
let count = skip_count.compute_next(previous_number_of_items, next_number_of_items);
353+
assert_eq!(count, 0);
354+
skip_count.count.set(count);
355+
356+
let page_size = 20;
357+
358+
// Paginate forwards. The count remains unchanged.
359+
let count = skip_count.compute_next_when_paginating_forwards(page_size);
360+
assert_eq!(count, 0);
361+
}
362+
363+
#[test]
364+
fn test_compute_count_when_paginating_forwards_from_overflowing_initial_states() {
365+
let skip_count = SkipCount::new();
366+
367+
// Initial state with too much new items. Some are skipped.
368+
let previous_number_of_items = 0;
369+
let next_number_of_items = previous_number_of_items + 50;
370+
let count = skip_count.compute_next(previous_number_of_items, next_number_of_items);
371+
assert_eq!(count, 30);
372+
skip_count.count.set(count);
373+
374+
// Add 30 new items. The count stays at 30 because we don't want to
375+
// skip the previous items.
376+
let previous_number_of_items = next_number_of_items;
377+
let next_number_of_items = previous_number_of_items + 30;
378+
let count = skip_count.compute_next(previous_number_of_items, next_number_of_items);
379+
assert_eq!(count, 30);
380+
skip_count.count.set(count);
381+
382+
let page_size = 20;
383+
384+
// Paginate forwards. The count remains unchanged.
385+
let count = skip_count.compute_next_when_paginating_forwards(page_size);
386+
assert_eq!(count, 30);
387+
}
388+
}
389+
}

0 commit comments

Comments
 (0)