Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion apps/web/src/features/email-view/queries/use-email-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,9 @@ export function useEmailDataSource(
// bucketed by date.
const result: EmailDataSourceItem[] = search.isSearching()
? buildFlatSoupRows(listEntities())
: buildGroupedSoupRows(groupEmailEntitiesByDate(listEntities()));
: buildGroupedSoupRows(groupEmailEntitiesByDate(listEntities()), {
keyByEntity: true,
});

if (hasMore()) {
result.push(
Expand Down
39 changes: 38 additions & 1 deletion apps/web/src/features/soup/collection/rows.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { getSoupMenuEntities } from './rows';
import { buildGroupedSoupRows, getSoupMenuEntities } from './rows';

describe('getSoupMenuEntities', () => {
const rows = [
Expand All @@ -20,3 +20,40 @@ describe('getSoupMenuEntities', () => {
expect(getSoupMenuEntities(rows[0], [])).toEqual([rows[0]]);
});
});

describe('buildGroupedSoupRows keyed by entity', () => {
const thread = { id: 't1', type: 'email' };
const other = { id: 't2', type: 'email' };
const rowIdOf = (rows: { kind: string; id: string }[], index: number) =>
rows.filter((row) => row.kind === 'entity')[index]?.id;

it('keeps a row id when its entity moves to another group', () => {
const before = buildGroupedSoupRows(
[
{ id: 'today', label: 'Today', entities: [other] },
{ id: 'yesterday', label: 'Yesterday', entities: [thread] },
],
{ keyByEntity: true }
);
// A reply arrives: the thread is now today's.
const after = buildGroupedSoupRows(
[{ id: 'today', label: 'Today', entities: [thread, other] }],
{ keyByEntity: true }
);

expect(rowIdOf(after, 0)).toBe(rowIdOf(before, 1));
expect(rowIdOf(after, 1)).toBe(rowIdOf(before, 0));
});

it('still gives an entity listed in two groups two rows', () => {
const rows = buildGroupedSoupRows(
[
{ id: 'today', label: 'Today', entities: [thread] },
{ id: 'yesterday', label: 'Yesterday', entities: [thread] },
],
{ keyByEntity: true }
);

expect(rowIdOf(rows, 0)).not.toBe(rowIdOf(rows, 1));
});
});
17 changes: 15 additions & 2 deletions apps/web/src/features/soup/collection/rows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ const createSoupRowId = (
export type CreateSoupEntityRowOptions = {
groupId?: string;
occurrenceId?: string;
/** Leave the group out of the row id, so moving groups keeps the row. */
keyByEntity?: boolean;
};

export function createSoupEntityRow<TEntity extends SoupEntityIdentity>(
Expand All @@ -35,7 +37,7 @@ export function createSoupEntityRow<TEntity extends SoupEntityIdentity>(
'entity',
entity.type,
entity.id,
options.groupId,
options.keyByEntity ? undefined : options.groupId,
options.occurrenceId
),
entity,
Expand Down Expand Up @@ -125,6 +127,12 @@ export function buildFlatSoupRows<TEntity extends SoupEntityIdentity>(
}

export type BuildGroupedSoupRowsOptions<TEntity extends SoupEntityIdentity> = {
/**
* Identify rows by entity alone, for lists where each entity sits in one
* group (date buckets). A thread moving to another group then keeps its
* row, its DOM and the keyboard cursor instead of becoming a new row.
*/
keyByEntity?: boolean;
getOccurrenceId?: (
entity: TEntity,
index: number,
Expand All @@ -137,6 +145,8 @@ export function buildGroupedSoupRows<TEntity extends SoupEntityIdentity>(
options: BuildGroupedSoupRowsOptions<TEntity> = {}
): SoupRow<TEntity>[] {
const groupIds = new Set<string>();
// Keyed by entity, a repeat in another group still needs its own id.
const listOccurrences = new Map<string, number>();
const rows = groups.flatMap((group) => {
if (groupIds.has(group.id)) {
throw new Error(
Expand All @@ -146,11 +156,14 @@ export function buildGroupedSoupRows<TEntity extends SoupEntityIdentity>(
groupIds.add(group.id);

const groupRows: SoupRow<TEntity>[] = [createSoupGroupHeaderRow(group)];
const occurrences = new Map<string, number>();
const occurrences = options.keyByEntity
? listOccurrences
: new Map<string, number>();
group.entities.forEach((entity, index) => {
groupRows.push(
createSoupEntityRow(entity, {
groupId: group.id,
keyByEntity: options.keyByEntity,
occurrenceId:
options.getOccurrenceId?.(entity, index, group) ??
nextDuplicateOccurrence(entity, occurrences),
Expand Down
Loading