diff --git a/packages/react-sdk/src/App.tsx b/packages/react-sdk/src/App.tsx
index 0da61f2b..3993528e 100644
--- a/packages/react-sdk/src/App.tsx
+++ b/packages/react-sdk/src/App.tsx
@@ -15,20 +15,105 @@
*/
import { useWidgetApi } from '@matrix-widget-toolkit/react';
+import { getLogger } from 'loglevel';
+import { useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Layout, LayoutProps } from './components/Layout';
import { PageLoader } from './components/common/PageLoader';
-import { useOwnedWhiteboard, useWhiteboardManager } from './state';
+import {
+ isValidWhiteboardDocumentSnapshot,
+ useActiveWhiteboardInstance,
+ useOwnedWhiteboard,
+ useWhiteboardManager,
+} from './state';
+import {
+ cancelableSnapshotTimer,
+ usePersistOnJoinOrInvite,
+} from './state/usePersistOnJoinOrInvite';
+import { useGetDocumentSnapshotQuery } from './store/api';
export type AppProps = {
layoutProps?: LayoutProps;
};
+const logger = getLogger('App');
+
export const App = ({ layoutProps }: AppProps) => {
const { t } = useTranslation('neoboard');
const { value, loading } = useOwnedWhiteboard();
const whiteboardManager = useWhiteboardManager();
- const ownUserId = useWidgetApi().widgetParameters.userId;
+ const whiteboardInstance = useActiveWhiteboardInstance();
+ const widgetApi = useWidgetApi();
+ const ownUserId = widgetApi.widgetParameters.userId;
+ const { limitedHistoryVisibility, delayedPersist, lastMembershipEventTs } =
+ usePersistOnJoinOrInvite();
+ const { data: latestSnapshot } = useGetDocumentSnapshotQuery({
+ documentId: whiteboardInstance.getDocumentId(),
+ validator: isValidWhiteboardDocumentSnapshot,
+ });
+
+ const handlePersist = useCallback(() => {
+ if (latestSnapshot !== undefined && lastMembershipEventTs !== undefined) {
+ if (latestSnapshot.event.origin_server_ts < lastMembershipEventTs) {
+ logger.debug('Saving snapshot due to membership updates');
+ whiteboardInstance.persist(true);
+ }
+ }
+ }, [latestSnapshot, lastMembershipEventTs, whiteboardInstance]);
+
+ useEffect(() => {
+ // We don't need to do anything if we're not in a room with limited history visibility
+ // or the whiteboard is still loading
+ if (!limitedHistoryVisibility || whiteboardInstance.isLoading()) {
+ return;
+ }
+
+ // We're in a room with limited history visibility, so we check
+ // if a snapshot is required after recent membership changes (invites+joins)
+ if (latestSnapshot !== undefined && lastMembershipEventTs !== undefined) {
+ // Is the snapshot outdated when compared to the last membership event?
+ if (latestSnapshot.event.origin_server_ts < lastMembershipEventTs) {
+ // We don't delay persisting if the membership event was sent by the current user
+ if (!delayedPersist) {
+ logger.debug(
+ 'Saving snapshot immediately due to current user sending the membership event',
+ );
+
+ whiteboardInstance.persist(true);
+ } else {
+ // Start a cancelable timer to persist the snapshot after a random delay
+ // If other clients run this earlier and send a snapshot, we don't need to persist
+ const delay = Math.floor(Math.random() * 20) + 10;
+ const timer = cancelableSnapshotTimer(handlePersist, delay * 1000);
+ logger.debug(
+ 'Will try to save a snapshot in ',
+ delay,
+ 's due to membership updates',
+ );
+
+ // cancel the timer if the component unmounts or the dependencies change
+ return () => {
+ logger.debug('Canceled delayed snapshot persistence timer');
+ timer.cancel();
+ };
+ }
+ } else {
+ logger.debug(
+ 'We have a fresh snapshot after membership updates, no need to persist',
+ );
+ }
+ }
+ return;
+ }, [
+ delayedPersist,
+ handlePersist,
+ lastMembershipEventTs,
+ latestSnapshot,
+ limitedHistoryVisibility,
+ loading,
+ whiteboardInstance,
+ widgetApi,
+ ]);
if (!ownUserId) {
throw new Error('Unknown user id');
diff --git a/packages/react-sdk/src/components/FallbackSnapshotProvider/index.ts b/packages/react-sdk/src/components/FallbackSnapshotProvider/index.ts
deleted file mode 100644
index d592ea74..00000000
--- a/packages/react-sdk/src/components/FallbackSnapshotProvider/index.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-/*
- * Copyright 2024 Nordeck IT + Consulting GmbH
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-export { FallbackSnapshotProvider } from './FallbackSnapshotProvider';
diff --git a/packages/react-sdk/src/components/Layout/Layout.tsx b/packages/react-sdk/src/components/Layout/Layout.tsx
index 66babeec..81c4315c 100644
--- a/packages/react-sdk/src/components/Layout/Layout.tsx
+++ b/packages/react-sdk/src/components/Layout/Layout.tsx
@@ -29,7 +29,6 @@ import { BoardBar } from '../BoardBar';
import { CollaborationBar } from '../CollaborationBar';
import { DeveloperTools } from '../DeveloperTools/DeveloperTools';
import { ElementOverridesProvider } from '../ElementOverridesProvider';
-import { FallbackSnapshotProvider } from '../FallbackSnapshotProvider';
import { FullscreenModeBar } from '../FullscreenModeBar';
import { GuidedTour } from '../GuidedTour';
import { HelpCenterBar } from '../HelpCenterBar';
@@ -78,54 +77,49 @@ export function Layout({ height = '100vh' }: LayoutProps) {
}
return (
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
- {slideIds.map((slideId) => (
-
-
-
-
- {uploadDragOverlay}
-
-
-
- ))}
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+ {slideIds.map((slideId) => (
+
+
+
+
+ {uploadDragOverlay}
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+
);
}
diff --git a/packages/react-sdk/src/lib/testUtils/documentTestUtils.tsx b/packages/react-sdk/src/lib/testUtils/documentTestUtils.tsx
index 767ac111..e8b20974 100644
--- a/packages/react-sdk/src/lib/testUtils/documentTestUtils.tsx
+++ b/packages/react-sdk/src/lib/testUtils/documentTestUtils.tsx
@@ -130,7 +130,6 @@ export function mockWhiteboardManager(
observeIsLoading: () => of(false),
destroy: () => {},
persist: vi.fn().mockResolvedValue(undefined),
- getLatestDocumentSnapshot: () => undefined,
};
const whiteboardInstance = new WhiteboardInstanceImpl(
diff --git a/packages/react-sdk/src/model/roomHistoryVisibilityEvent.ts b/packages/react-sdk/src/model/roomHistoryVisibilityEvent.ts
index 95d2b9f5..23bc9dfa 100644
--- a/packages/react-sdk/src/model/roomHistoryVisibilityEvent.ts
+++ b/packages/react-sdk/src/model/roomHistoryVisibilityEvent.ts
@@ -29,7 +29,9 @@ const roomHistoryVisibilityEventSchema = Joi.object<
RoomHistoryVisibilityEvent,
true
>({
- history_visibility: Joi.string().required(),
+ history_visibility: Joi.string()
+ .required()
+ .valid('invited', 'joined', 'shared', 'world_readable'),
}).unknown();
export function isValidRoomHistoryVisibilityEvent(
diff --git a/packages/react-sdk/src/state/presentationManagerImpl.test.ts b/packages/react-sdk/src/state/presentationManagerImpl.test.ts
index 82b6792c..fe2ea543 100644
--- a/packages/react-sdk/src/state/presentationManagerImpl.test.ts
+++ b/packages/react-sdk/src/state/presentationManagerImpl.test.ts
@@ -80,6 +80,7 @@ describe('presentationManager', () => {
getPresentationManager: vi.fn(),
getSlide: vi.fn(),
getSlideIds: vi.fn(),
+ getDocumentId: vi.fn(),
getWhiteboardId: vi.fn(),
getWhiteboardStatistics: vi.fn(),
import: vi.fn(),
diff --git a/packages/react-sdk/src/state/synchronizedDocumentImpl.ts b/packages/react-sdk/src/state/synchronizedDocumentImpl.ts
index 03f79d08..09512ee0 100644
--- a/packages/react-sdk/src/state/synchronizedDocumentImpl.ts
+++ b/packages/react-sdk/src/state/synchronizedDocumentImpl.ts
@@ -14,7 +14,6 @@
* limitations under the License.
*/
-import { RoomEvent } from '@matrix-widget-toolkit/api';
import { Base64 } from 'js-base64';
import { clone } from 'lodash';
import { getLogger } from 'loglevel';
@@ -35,7 +34,6 @@ import {
throttleTime,
} from 'rxjs';
import { isDefined } from '../lib';
-import { DocumentSnapshot } from '../model';
import {
documentSnapshotApi,
setSnapshotFailed,
@@ -200,8 +198,8 @@ export class SynchronizedDocumentImpl>
}
private async persistDocument(doc: Document, force = false) {
+ // If there is no outstanding snapshot and we are not forcing to persist, we can skip this
if (!this.statistics.snapshotOutstanding && !force) {
- // No outstanding snapshot, do nothing
return;
}
@@ -223,24 +221,6 @@ export class SynchronizedDocumentImpl>
await this.persistDocument(this.document, force);
}
- getLatestDocumentSnapshot(): RoomEvent | undefined {
- const state = this.store.getState();
- const documentId = this.documentId;
-
- const result = documentSnapshotApi.endpoints.getDocumentSnapshot.select({
- documentId,
- validator: () => true,
- })(state);
-
- if (!result.isLoading) {
- const snapshotData = result.data;
-
- if (snapshotData) {
- return snapshotData.event;
- }
- }
- }
-
private async createDocumentSnapshot(
documentId: string,
data: Uint8Array,
diff --git a/packages/react-sdk/src/state/types.ts b/packages/react-sdk/src/state/types.ts
index 8668d0b5..15781a28 100644
--- a/packages/react-sdk/src/state/types.ts
+++ b/packages/react-sdk/src/state/types.ts
@@ -14,9 +14,9 @@
* limitations under the License.
*/
-import { RoomEvent, StateEvent, WidgetApi } from '@matrix-widget-toolkit/api';
+import { StateEvent, WidgetApi } from '@matrix-widget-toolkit/api';
import { BehaviorSubject, Observable } from 'rxjs';
-import { DocumentSnapshot, Whiteboard } from '../model';
+import { Whiteboard } from '../model';
import { CommunicationChannelStatistics } from './communication';
import {
Document,
@@ -55,13 +55,10 @@ export type WhiteboardStatistics = {
communicationChannel: CommunicationChannelStatistics;
};
-export type PersistOptions = {
- timestamp: number;
- immediate: boolean;
-};
-
/** An instance of a whiteboard that can be used to read and manipulate it. */
export type WhiteboardInstance = {
+ /** Returns the id of the document */
+ getDocumentId(): string;
/** Returns the id of the whiteboard. */
getWhiteboardId(): string;
/**
@@ -153,7 +150,7 @@ export type WhiteboardInstance = {
destroy(): void;
/** Persist the whiteboard state. */
- persist(options?: PersistOptions): Promise;
+ persist(force?: boolean): Promise;
};
export type ElementUpdate = {
@@ -267,8 +264,6 @@ export type SynchronizedDocument> = {
destroy(): void;
/** Persist the document immediately. */
persist(force: boolean): Promise;
- /** Get the latest document snapshot, if available */
- getLatestDocumentSnapshot(): RoomEvent | undefined;
};
export type PresentationState =
diff --git a/packages/react-sdk/src/components/FallbackSnapshotProvider/FallbackSnapshotProvider.tsx b/packages/react-sdk/src/state/usePersistOnJoinOrInvite.ts
similarity index 54%
rename from packages/react-sdk/src/components/FallbackSnapshotProvider/FallbackSnapshotProvider.tsx
rename to packages/react-sdk/src/state/usePersistOnJoinOrInvite.ts
index 0eb929c6..d5e61ff2 100644
--- a/packages/react-sdk/src/components/FallbackSnapshotProvider/FallbackSnapshotProvider.tsx
+++ b/packages/react-sdk/src/state/usePersistOnJoinOrInvite.ts
@@ -19,72 +19,91 @@ import {
StateEvent,
} from '@matrix-widget-toolkit/api';
import { useWidgetApi } from '@matrix-widget-toolkit/react';
-import { PropsWithChildren, useEffect } from 'react';
-import { RoomEncryptionEvent, RoomHistoryVisibilityEvent } from '../../model';
-import { useActiveWhiteboardInstance } from '../../state';
+import { RoomEncryptionEvent, RoomHistoryVisibilityEvent } from '../model';
+import { useActiveWhiteboardInstance } from '../state';
import {
useGetRoomEncryptionQuery,
useGetRoomHistoryVisibilityQuery,
useGetRoomMembersQuery,
-} from '../../store/api';
+} from '../store/api';
-export function FallbackSnapshotProvider({ children }: PropsWithChildren<{}>) {
+type PersistOnJoinOrInviteResult = {
+ limitedHistoryVisibility: boolean;
+ delayedPersist: boolean;
+ lastMembershipEventTs?: number;
+};
+
+export function usePersistOnJoinOrInvite(): PersistOnJoinOrInviteResult {
const whiteboardInstance = useActiveWhiteboardInstance();
const { data: roomMembers } = useGetRoomMembersQuery();
const { data: roomEncryption } = useGetRoomEncryptionQuery();
const { data: roomHistoryVisibility } = useGetRoomHistoryVisibilityQuery();
- const ownUserId = useWidgetApi().widgetParameters.userId;
-
- useEffect(() => {
- const isRoomEncrypted = checkIfRoomEncrypted(roomEncryption);
- const roomHistoryVisibilityValue = getRoomHistoryVisibilityValue(
- roomHistoryVisibility,
- );
- const isRoomHistoryVisibilityShared = checkIfRoomHistoryVisibilityShared(
- roomHistoryVisibilityValue,
- );
- const sendFallbackSnapshot = shouldSendFallbackSnapshot(
- isRoomEncrypted,
- isRoomHistoryVisibilityShared,
- roomHistoryVisibilityValue,
- );
-
- if (sendFallbackSnapshot) {
- const membershipFilter = getMembershipFilter(roomHistoryVisibilityValue);
- const members = roomMembers?.entities;
-
- if (members) {
- const invitesOrJoins = Object.values(members).filter(membershipFilter);
- const sortedInvitesOrJoins = invitesOrJoins.sort((a, b) => {
- return b.origin_server_ts - a.origin_server_ts;
- });
- const lastInviteOrJoin = sortedInvitesOrJoins[0];
-
- if (
- shouldUseInviteTimestamp(lastInviteOrJoin, roomHistoryVisibilityValue)
- ) {
- // TODO: Implement logic to get the timestamp of the invite event.
- // There is currently no way to get an event by id or
- // fetch an older state event from the room timeline using the Widget API
- }
-
- const immediate = shouldPersistImmediately(lastInviteOrJoin, ownUserId);
-
- whiteboardInstance.persist({
- timestamp: lastInviteOrJoin.origin_server_ts,
- immediate,
- });
+ const widgetApi = useWidgetApi();
+ const ownUserId = widgetApi.widgetParameters.userId;
+
+ if (whiteboardInstance.isLoading()) {
+ return {
+ limitedHistoryVisibility: false,
+ delayedPersist: false,
+ };
+ }
+
+ const isRoomEncrypted = checkIfRoomEncrypted(roomEncryption);
+ const roomHistoryVisibilityValue = getRoomHistoryVisibilityValue(
+ roomHistoryVisibility,
+ );
+ const limitedHistoryVisibility = roomHasLimitedHistoryVisibility(
+ isRoomEncrypted,
+ roomHistoryVisibilityValue,
+ );
+
+ let delayedPersist = true;
+ let lastMembershipEventTs: number | undefined = undefined;
+
+ if (limitedHistoryVisibility) {
+ const membershipFilter = getMembershipFilter(roomHistoryVisibilityValue);
+ const members = roomMembers?.entities;
+
+ if (members) {
+ const invitesOrJoins = Object.values(members).filter(membershipFilter);
+ const sortedInvitesOrJoins = invitesOrJoins.sort((a, b) => {
+ return b.origin_server_ts - a.origin_server_ts;
+ });
+ const lastInviteOrJoin = sortedInvitesOrJoins[0];
+
+ if (shouldUseInvite(lastInviteOrJoin, roomHistoryVisibilityValue)) {
+ // TODO: Implement logic to get the timestamp of the invite event.
+ // There is currently no way to get an event by id or
+ // fetch an older state event from the room timeline using the Widget API
+ // lastInviteOrJoin = getInviteEvent(lastInviteOrJoin);
}
+
+ delayedPersist = !shouldPersistImmediately(lastInviteOrJoin, ownUserId);
+ lastMembershipEventTs = lastInviteOrJoin.origin_server_ts;
}
- }, [
- roomMembers,
- whiteboardInstance,
- roomEncryption,
- roomHistoryVisibility,
- ownUserId,
- ]);
+ }
+
+ return {
+ limitedHistoryVisibility,
+ delayedPersist,
+ lastMembershipEventTs,
+ };
+}
- return <>{children}>;
+export function cancelableSnapshotTimer(
+ callback: () => void,
+ delay: number,
+): { cancel: () => void } {
+ let timerId: NodeJS.Timeout | null = setTimeout(callback, delay);
+
+ return {
+ cancel: () => {
+ if (timerId) {
+ clearTimeout(timerId);
+ timerId = null;
+ }
+ },
+ };
}
function checkIfRoomEncrypted(roomEncryption?: {
@@ -101,20 +120,14 @@ function getRoomHistoryVisibilityValue(roomHistoryVisibility?: {
return roomHistoryVisibility?.event?.content?.history_visibility;
}
-function checkIfRoomHistoryVisibilityShared(
- roomHistoryVisibilityValue: string | undefined,
-): boolean {
- return roomHistoryVisibilityValue === 'shared';
-}
-
-function shouldSendFallbackSnapshot(
+function roomHasLimitedHistoryVisibility(
isRoomEncrypted: boolean,
- isRoomHistoryVisibilityShared: boolean,
roomHistoryVisibilityValue: string | undefined,
): boolean {
return (
isRoomEncrypted ||
- (!isRoomHistoryVisibilityShared && roomHistoryVisibilityValue !== undefined)
+ (roomHistoryVisibilityValue !== undefined &&
+ roomHistoryVisibilityValue !== 'shared')
);
}
@@ -129,7 +142,7 @@ function getMembershipFilter(
member.content.membership === 'join';
}
-function shouldUseInviteTimestamp(
+function shouldUseInvite(
lastInviteOrJoin: StateEvent,
roomHistoryVisibilityValue: string | undefined,
) {
diff --git a/packages/react-sdk/src/components/FallbackSnapshotProvider/FallbackSnapshotProvider.test.tsx b/packages/react-sdk/src/state/usePersistOnJoinorInvite.test.tsx
similarity index 52%
rename from packages/react-sdk/src/components/FallbackSnapshotProvider/FallbackSnapshotProvider.test.tsx
rename to packages/react-sdk/src/state/usePersistOnJoinorInvite.test.tsx
index 8ed1ad3f..92b88126 100644
--- a/packages/react-sdk/src/components/FallbackSnapshotProvider/FallbackSnapshotProvider.test.tsx
+++ b/packages/react-sdk/src/state/usePersistOnJoinorInvite.test.tsx
@@ -15,7 +15,7 @@
*/
import { MockedWidgetApi, mockWidgetApi } from '@matrix-widget-toolkit/testing';
-import { render } from '@testing-library/react';
+import { renderHook } from '@testing-library/react';
import { ComponentType, PropsWithChildren } from 'react';
import {
afterEach,
@@ -28,21 +28,21 @@ import {
Mocked,
vi,
} from 'vitest';
+import { App } from '../App';
import {
mockWhiteboardManager,
WhiteboardTestingContextProvider,
-} from '../../lib/testUtils';
+} from '../lib/testUtils';
import {
mockRoomEncryption,
mockRoomHistoryVisibility,
-} from '../../lib/testUtils/matrixTestUtils';
-import * as state from '../../state';
+} from '../lib/testUtils/matrixTestUtils';
+import * as state from '../state';
+import * as storeApi from '../store/api';
import {
- useGetRoomEncryptionQuery,
- useGetRoomHistoryVisibilityQuery,
- useGetRoomMembersQuery,
-} from '../../store';
-import { FallbackSnapshotProvider } from './FallbackSnapshotProvider';
+ cancelableSnapshotTimer,
+ usePersistOnJoinOrInvite,
+} from './usePersistOnJoinOrInvite';
const mockRoomMembersOtherUser = {
entities: {
@@ -74,6 +74,14 @@ const mockRoomMembersOwnUser = {
},
};
+const mockLatestDocumentSnapshot = {
+ event: { origin_server_ts: 100 },
+};
+
+const mockRecentDocumentSnapshot = {
+ event: { origin_server_ts: 3000 },
+};
+
const mockRoomHistoryVisibilityInvited = mockRoomHistoryVisibility({
content: { history_visibility: 'invited' },
});
@@ -84,27 +92,42 @@ const mockRoomHistoryVisibilityShared = mockRoomHistoryVisibility();
const mockWhiteboardInstance = {
persist: vi.fn(),
+ isLoading: () => false,
+ getDocumentId: () => '$document-0',
};
let widgetApi: MockedWidgetApi;
afterEach(() => widgetApi.stop());
beforeEach(() => (widgetApi = mockWidgetApi()));
-describe('', () => {
+describe('usePersistOnJoinOrInvite', () => {
let Wrapper: ComponentType>;
let whiteboardManager: Mocked;
beforeAll(() => {
- vi.mock('../../store/api', () => {
+ vi.mock('./usePersistOnJoinOrInvite', async () => {
+ const usePersistOnJoinOrInvite = await vi.importActual<
+ typeof import('./usePersistOnJoinOrInvite')
+ >('./usePersistOnJoinOrInvite');
return {
+ ...usePersistOnJoinOrInvite,
+ cancelableSnapshotTimer: vi.fn(() => ({ cancel: vi.fn() })),
+ };
+ });
+ vi.mock('../store/api', async () => {
+ const storeApi =
+ await vi.importActual('../store/api');
+ return {
+ ...storeApi,
+ useGetDocumentSnapshotQuery: vi.fn(),
useGetRoomMembersQuery: vi.fn(),
useGetRoomEncryptionQuery: vi.fn(),
useGetRoomHistoryVisibilityQuery: vi.fn(),
};
});
- vi.mock('../../state', async () => {
+ vi.mock('../state', async () => {
const state =
- await vi.importActual('../../state');
+ await vi.importActual('../state');
return {
...state,
useActiveWhiteboardInstance: vi.fn(),
@@ -120,44 +143,71 @@ describe('', () => {
(state.useActiveWhiteboardInstance as Mock).mockReturnValue(
mockWhiteboardInstance,
);
- (useGetRoomMembersQuery as Mock).mockReturnValue({
+ (storeApi.useGetRoomMembersQuery as Mock).mockReturnValue({
data: mockRoomMembersOtherUser,
});
+ (storeApi.useGetDocumentSnapshotQuery as Mock).mockReturnValue({
+ data: mockLatestDocumentSnapshot,
+ });
- Wrapper = ({ children }: PropsWithChildren<{}>) => {
- return (
-
- {children}
-
- );
- };
+ ({ whiteboardManager } = mockWhiteboardManager());
+
+ Wrapper = () => (
+
+
+
+ );
});
it('should not persist snapshot if room is unencrypted and history is shared', () => {
- (useGetRoomEncryptionQuery as Mock).mockReturnValue({ data: undefined });
- (useGetRoomHistoryVisibilityQuery as Mock).mockReturnValue({
+ (storeApi.useGetRoomEncryptionQuery as Mock).mockReturnValue({
+ data: undefined,
+ });
+ (storeApi.useGetRoomHistoryVisibilityQuery as Mock).mockReturnValue({
data: { event: mockRoomHistoryVisibilityShared },
});
- render();
+ const { result: _result } = renderHook(usePersistOnJoinOrInvite, {
+ wrapper: Wrapper,
+ });
+
+ expect(mockWhiteboardInstance.persist).not.toHaveBeenCalled();
+ });
+
+ it('should not persist snapshot if the snapshot is more recent than the last membership event', () => {
+ (storeApi.useGetRoomEncryptionQuery as Mock).mockReturnValue({
+ data: { event: mockRoomEncryption() },
+ });
+ (storeApi.useGetRoomHistoryVisibilityQuery as Mock).mockReturnValue({
+ data: { event: mockRoomHistoryVisibilityInvited },
+ });
+ (storeApi.useGetDocumentSnapshotQuery as Mock).mockReturnValue({
+ data: mockRecentDocumentSnapshot,
+ });
+
+ const { result: _result } = renderHook(usePersistOnJoinOrInvite, {
+ wrapper: Wrapper,
+ });
expect(mockWhiteboardInstance.persist).not.toHaveBeenCalled();
});
it('should persist snapshot if the room is encrypted', () => {
- (useGetRoomEncryptionQuery as Mock).mockReturnValue({
+ (storeApi.useGetRoomEncryptionQuery as Mock).mockReturnValue({
data: { event: mockRoomEncryption() },
});
- (useGetRoomHistoryVisibilityQuery as Mock).mockReturnValue({
+ (storeApi.useGetRoomHistoryVisibilityQuery as Mock).mockReturnValue({
data: { event: mockRoomHistoryVisibilityShared },
});
- render();
+ const { result: _result } = renderHook(usePersistOnJoinOrInvite, {
+ wrapper: Wrapper,
+ });
- expect(mockWhiteboardInstance.persist).toHaveBeenCalled();
+ expect(cancelableSnapshotTimer).toHaveBeenCalled();
});
it.each([
@@ -166,19 +216,18 @@ describe('', () => {
])(
'should persist snapshot if the last event is a join and history visibility is invited (%s room)',
(_, encryptionState) => {
- (useGetRoomEncryptionQuery as Mock).mockReturnValue({
+ (storeApi.useGetRoomEncryptionQuery as Mock).mockReturnValue({
data: encryptionState,
});
- (useGetRoomHistoryVisibilityQuery as Mock).mockReturnValue({
+ (storeApi.useGetRoomHistoryVisibilityQuery as Mock).mockReturnValue({
data: { event: mockRoomHistoryVisibilityInvited },
});
- render();
-
- expect(mockWhiteboardInstance.persist).toHaveBeenCalledWith({
- timestamp: 2000,
- immediate: false,
+ const { result: _result } = renderHook(usePersistOnJoinOrInvite, {
+ wrapper: Wrapper,
});
+
+ expect(cancelableSnapshotTimer).toHaveBeenCalled();
},
);
@@ -188,19 +237,18 @@ describe('', () => {
])(
'should persist snapshot if the last event is a join and history visibility is joined (%s room)',
(_, encryptionState) => {
- (useGetRoomEncryptionQuery as Mock).mockReturnValue({
+ (storeApi.useGetRoomEncryptionQuery as Mock).mockReturnValue({
data: encryptionState,
});
- (useGetRoomHistoryVisibilityQuery as Mock).mockReturnValue({
+ (storeApi.useGetRoomHistoryVisibilityQuery as Mock).mockReturnValue({
data: { event: mockRoomHistoryVisibilityJoined },
});
- render();
-
- expect(mockWhiteboardInstance.persist).toHaveBeenCalledWith({
- timestamp: 1000,
- immediate: false,
+ const { result: _result } = renderHook(usePersistOnJoinOrInvite, {
+ wrapper: Wrapper,
});
+
+ expect(cancelableSnapshotTimer).toHaveBeenCalled();
},
);
@@ -210,22 +258,21 @@ describe('', () => {
])(
'should persist snapshot immediatly if the last event is an invite and inviter is the current user (%s room)',
(_, encryptionState) => {
- (useGetRoomMembersQuery as Mock).mockReturnValue({
+ (storeApi.useGetRoomMembersQuery as Mock).mockReturnValue({
data: mockRoomMembersOwnUser,
});
- (useGetRoomEncryptionQuery as Mock).mockReturnValue({
+ (storeApi.useGetRoomEncryptionQuery as Mock).mockReturnValue({
data: encryptionState,
});
- (useGetRoomHistoryVisibilityQuery as Mock).mockReturnValue({
+ (storeApi.useGetRoomHistoryVisibilityQuery as Mock).mockReturnValue({
data: { event: mockRoomHistoryVisibilityInvited },
});
- render();
-
- expect(mockWhiteboardInstance.persist).toHaveBeenCalledWith({
- timestamp: 2000,
- immediate: true,
+ const { result: _result } = renderHook(usePersistOnJoinOrInvite, {
+ wrapper: Wrapper,
});
+
+ expect(mockWhiteboardInstance.persist).toHaveBeenCalled();
},
);
});
diff --git a/packages/react-sdk/src/state/whiteboardInstanceImpl.test.ts b/packages/react-sdk/src/state/whiteboardInstanceImpl.test.ts
index de6897e4..2eb0a869 100644
--- a/packages/react-sdk/src/state/whiteboardInstanceImpl.test.ts
+++ b/packages/react-sdk/src/state/whiteboardInstanceImpl.test.ts
@@ -81,7 +81,6 @@ describe('WhiteboardInstanceImpl', () => {
.mockReturnValue(observeDocumentStatisticsSubject),
observeIsLoading: vi.fn().mockReturnValue(observeIsLoadingSubject),
persist: vi.fn(),
- getLatestDocumentSnapshot: vi.fn(),
};
});
diff --git a/packages/react-sdk/src/state/whiteboardInstanceImpl.ts b/packages/react-sdk/src/state/whiteboardInstanceImpl.ts
index 34019cc6..f622adcc 100644
--- a/packages/react-sdk/src/state/whiteboardInstanceImpl.ts
+++ b/packages/react-sdk/src/state/whiteboardInstanceImpl.ts
@@ -15,7 +15,7 @@
*/
import { StateEvent, WidgetApi } from '@matrix-widget-toolkit/api';
-import { cloneDeep, debounce, isEqual } from 'lodash';
+import { cloneDeep, isEqual } from 'lodash';
import {
BehaviorSubject,
Observable,
@@ -60,7 +60,6 @@ import { PresentationManagerImpl } from './presentationManagerImpl';
import { LocalForageDocumentStorage } from './storage';
import { SynchronizedDocumentImpl } from './synchronizedDocumentImpl';
import {
- PersistOptions,
PresentationManager,
SynchronizedDocument,
WhiteboardInstance,
@@ -388,6 +387,10 @@ export class WhiteboardInstanceImpl implements WhiteboardInstance {
}
}
+ getDocumentId(): string {
+ return this.whiteboardEvent.content.documentId;
+ }
+
getWhiteboardId(): string {
return this.whiteboardEvent.event_id;
}
@@ -453,23 +456,8 @@ export class WhiteboardInstanceImpl implements WhiteboardInstance {
this.communicationChannel.destroy();
}
- async persist(options?: PersistOptions): Promise {
- if (options !== undefined) {
- const snapshot = this.synchronizedDocument.getLatestDocumentSnapshot();
- if (snapshot && snapshot.origin_server_ts < options.timestamp) {
- if (options.immediate) {
- await this.synchronizedDocument.persist(true);
- } else {
- const delay = Math.floor(Math.random() * 20) + 10;
- debounce(
- () => this.synchronizedDocument.persist(true),
- delay * 1000,
- )();
- }
- }
- } else {
- await this.synchronizedDocument.persist(false);
- }
+ async persist(force: boolean = false): Promise {
+ await this.synchronizedDocument.persist(force);
}
clearUndoManager(): void {
diff --git a/packages/react-sdk/src/store/api/documentSnapshotApi.ts b/packages/react-sdk/src/store/api/documentSnapshotApi.ts
index 92e8c68e..1179ade3 100644
--- a/packages/react-sdk/src/store/api/documentSnapshotApi.ts
+++ b/packages/react-sdk/src/store/api/documentSnapshotApi.ts
@@ -358,4 +358,5 @@ export function createChunks(
}
// consume the store using the hooks generated by RTK Query
-export const { useCreateDocumentMutation } = documentSnapshotApi;
+export const { useCreateDocumentMutation, useGetDocumentSnapshotQuery } =
+ documentSnapshotApi;
diff --git a/packages/react-sdk/src/store/api/index.ts b/packages/react-sdk/src/store/api/index.ts
index 2e163928..1602404a 100644
--- a/packages/react-sdk/src/store/api/index.ts
+++ b/packages/react-sdk/src/store/api/index.ts
@@ -18,6 +18,7 @@ export { baseApi } from './baseApi';
export {
documentSnapshotApi,
useCreateDocumentMutation,
+ useGetDocumentSnapshotQuery,
} from './documentSnapshotApi';
export {
powerLevelsApi,