Room list: scroll to newly creation section (#33210)
* feat(rls): emit tag when section is created * feat(vm): scroll to newly section tag * feat(view): scroll to new section
This commit is contained in:
@@ -485,13 +485,12 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient<EmptyObject> {
|
||||
|
||||
/**
|
||||
* Create a new section.
|
||||
* Emits {@link SECTION_CREATED_EVENT} and {@link LISTS_UPDATE_EVENT} if the section was successfully created.
|
||||
* Emits {@link SECTION_CREATED_EVENT} if the section was successfully created.
|
||||
*/
|
||||
public async createSection(): Promise<void> {
|
||||
const sectionIsCreated = await createSection();
|
||||
if (!sectionIsCreated) return;
|
||||
this.emit(SECTION_CREATED_EVENT);
|
||||
this.scheduleEmit();
|
||||
const tag = await createSection();
|
||||
if (!tag) return;
|
||||
this.emit(SECTION_CREATED_EVENT, tag);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -33,13 +33,13 @@ export type OrderedCustomSections = Tag[];
|
||||
* Creates a new custom section by showing a dialog to the user to enter the section name.
|
||||
* If the user confirms, it generates a unique tag for the section, saves the section data in the settings, and updates the ordered list of sections.
|
||||
*
|
||||
* @return A promise that resolves to true if the section was created, or false if the user cancelled the creation or if there was an error.
|
||||
* @return A promise that resolves to the new section tag if created, or undefined if cancelled.
|
||||
*/
|
||||
export async function createSection(): Promise<boolean> {
|
||||
export async function createSection(): Promise<string | undefined> {
|
||||
const modal = Modal.createDialog(CreateSectionDialog);
|
||||
|
||||
const [shouldCreateSection, sectionName] = await modal.finished;
|
||||
if (!shouldCreateSection || !sectionName) return false;
|
||||
if (!shouldCreateSection || !sectionName) return undefined;
|
||||
|
||||
const tag = `element.io.section.${window.crypto.randomUUID()}`;
|
||||
const newSection: CustomSection = { tag, name: sectionName };
|
||||
@@ -53,5 +53,5 @@ export async function createSection(): Promise<boolean> {
|
||||
const orderedSections = SettingsStore.getValue("RoomList.OrderedCustomSections") || [];
|
||||
orderedSections.push(tag);
|
||||
await SettingsStore.setValue("RoomList.OrderedCustomSections", null, SettingLevel.ACCOUNT, orderedSections);
|
||||
return true;
|
||||
return tag;
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ export class RoomListViewModel
|
||||
this.disposables.trackListener(
|
||||
RoomListStoreV3.instance,
|
||||
RoomListStoreV3Event.SectionCreated as any,
|
||||
this.onSectionCreated,
|
||||
this.onSectionCreated as (...args: unknown[]) => void,
|
||||
);
|
||||
|
||||
// Subscribe to active room changes to update selected room
|
||||
@@ -500,6 +500,7 @@ export class RoomListViewModel
|
||||
private async updateRoomListData(
|
||||
isRoomChange: boolean = false,
|
||||
roomIdOverride: string | null = null,
|
||||
scrollToSectionTag: string | undefined = undefined,
|
||||
): Promise<void> {
|
||||
// Determine the room ID to use for calculations
|
||||
// Use override if provided (e.g., during space changes), otherwise fall back to RoomViewStore
|
||||
@@ -544,17 +545,23 @@ export class RoomListViewModel
|
||||
// Update filter keys - only update if they have actually changed to prevent unnecessary re-renders of the room list
|
||||
const previousFilterKeys = this.snapshot.current.roomListState.filterKeys;
|
||||
const newFilterKeys = this.roomsResult.filterKeys?.map((k) => String(k));
|
||||
const viewSections = toRoomListSection(this.sections);
|
||||
|
||||
const resolvedScrollToSectionTag =
|
||||
scrollToSectionTag && viewSections.some((s) => s.id === scrollToSectionTag)
|
||||
? scrollToSectionTag
|
||||
: undefined;
|
||||
|
||||
const roomListState: RoomListViewState = {
|
||||
activeRoomIndex,
|
||||
spaceId: this.roomsResult.spaceId,
|
||||
filterKeys: keepIfSame(previousFilterKeys, newFilterKeys),
|
||||
scrollToSectionTag: resolvedScrollToSectionTag,
|
||||
};
|
||||
|
||||
const activeFilterId = this.activeFilter !== undefined ? filterKeyToIdMap.get(this.activeFilter) : undefined;
|
||||
const isRoomListEmpty = this.roomsResult.sections.every((section) => section.rooms.length === 0);
|
||||
const isLoadingRooms = RoomListStoreV3.instance.isLoadingRooms;
|
||||
|
||||
const viewSections = toRoomListSection(this.sections);
|
||||
const previousSections = this.snapshot.current.sections;
|
||||
|
||||
// Single atomic snapshot update
|
||||
@@ -586,7 +593,9 @@ export class RoomListViewModel
|
||||
}
|
||||
};
|
||||
|
||||
public onSectionCreated = (): void => {
|
||||
public onSectionCreated = (tag: string): void => {
|
||||
this.updateRoomListData(false, null, tag);
|
||||
|
||||
clearTimeout(this.toastRef);
|
||||
this.snapshot.merge({
|
||||
toast: "section_created",
|
||||
|
||||
@@ -1015,7 +1015,7 @@ describe("RoomListStoreV3", () => {
|
||||
it("emits SECTION_CREATED_EVENT and LISTS_UPDATE_EVENT when section is created", async () => {
|
||||
enableSections();
|
||||
getClientAndRooms();
|
||||
jest.spyOn(sectionModule, "createSection").mockResolvedValue(true);
|
||||
jest.spyOn(sectionModule, "createSection").mockResolvedValue("element.io.section.test-tag");
|
||||
|
||||
const store = new RoomListStoreV3Class(dispatcher);
|
||||
await store.start();
|
||||
@@ -1027,14 +1027,13 @@ describe("RoomListStoreV3", () => {
|
||||
|
||||
await store.createSection();
|
||||
|
||||
expect(sectionCreatedListener).toHaveBeenCalled();
|
||||
expect(listsUpdateListener).toHaveBeenCalled();
|
||||
expect(sectionCreatedListener).toHaveBeenCalledWith("element.io.section.test-tag");
|
||||
});
|
||||
|
||||
it("does not emit when section creation is cancelled", async () => {
|
||||
enableSections();
|
||||
getClientAndRooms();
|
||||
jest.spyOn(sectionModule, "createSection").mockResolvedValue(false);
|
||||
jest.spyOn(sectionModule, "createSection").mockResolvedValue(undefined);
|
||||
|
||||
const store = new RoomListStoreV3Class(dispatcher);
|
||||
await store.start();
|
||||
|
||||
@@ -21,10 +21,9 @@ describe("createSection", () => {
|
||||
});
|
||||
|
||||
it.each([
|
||||
[false, "", false],
|
||||
[true, "", false],
|
||||
[true, "My Section", true],
|
||||
])("returns %s when shouldCreate=%s and name='%s'", async (shouldCreate, name, expected) => {
|
||||
[false, "", undefined],
|
||||
[true, "", undefined],
|
||||
])("returns undefined when shouldCreate=%s and name='%s'", async (shouldCreate, name, expected) => {
|
||||
jest.spyOn(Modal, "createDialog").mockReturnValue({
|
||||
finished: Promise.resolve([shouldCreate, name]),
|
||||
close: jest.fn(),
|
||||
@@ -34,6 +33,16 @@ describe("createSection", () => {
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
it("returns the new tag when section is created", async () => {
|
||||
jest.spyOn(Modal, "createDialog").mockReturnValue({
|
||||
finished: Promise.resolve([true, "My Section"]),
|
||||
close: jest.fn(),
|
||||
} as any);
|
||||
|
||||
const result = await createSection();
|
||||
expect(result).toMatch(/^element\.io\.section\./);
|
||||
});
|
||||
|
||||
it("opens the CreateSectionDialog", async () => {
|
||||
const createDialogSpy = jest.spyOn(Modal, "createDialog").mockReturnValue({
|
||||
finished: Promise.resolve([false, ""]),
|
||||
|
||||
Reference in New Issue
Block a user