diff --git a/CHANGELOG.md b/CHANGELOG.md index 8eb967317..91d35a0a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,8 @@ and this project adheres to - ✨(collaboration) notify the backend when the worker persists new content - ✨(collaboration) let a user read the document's editing history - ✨(frontend) fall back to http polling when the websocket cannot be opened. +- 🔧(collaboration) make the version-history granularity configurable through + `COLLABORATION_VERSION_GRANULARITY_MS` ### Changed diff --git a/documentation/env.md b/documentation/env.md index 1b14d7a88..11454b34c 100644 --- a/documentation/env.md +++ b/documentation/env.md @@ -38,6 +38,7 @@ These are the environment variables you can set for the `impress-backend` contai | CACHES_SESSION_IGNORE_EXCEPTIONS | Ignoring exception, behave like a missed cache. Highly recommended to set it as False when used with redis (See https://github.com/jazzband/django-redis#memcached-exceptions-behavior) | False | | CACHES_SESSION_SOCKET_CONNECT_TIMEOUT | Timeout for the connection to be established for the session cache (In seconds) | 0.5 | | CACHES_SESSION_SOCKET_TIMEOUT | Timeout for read and write operations after the connection is established for the session cache (In seconds) | 1 | +| COLLABORATION_VERSION_GRANULARITY_MS | How coarse the document version history is, in milliseconds: edits closer together than this are shown as one version, and none spans more than this. Sent to the collaboration server as the grouping window and re-applied in the browser. | 60000 | | COLLABORATION_WS_INACTIVITY_TIMEOUT | Timeout (in seconds) after which the user is considered inactive when there is no activity. The WebSocket is closed after this inactivity period. `None` means disabled. | None | | COLLABORATION_WS_URL | Collaboration websocket url | | | CONVERSION_API_CONTENT_FIELD | Conversion api content field | content | diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index 4351614f2..4280e4660 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -2954,6 +2954,7 @@ class ConfigView(drf.views.APIView): "AI_FEATURE_BLOCKNOTE_ENABLED", "AI_FEATURE_LEGACY_ENABLED", "API_USERS_SEARCH_QUERY_MIN_LENGTH", + "COLLABORATION_VERSION_GRANULARITY_MS", "COLLABORATION_WS_URL", "COLLABORATION_WS_INACTIVITY_TIMEOUT", "CONVERSION_FILE_EXTENSIONS_ALLOWED", diff --git a/src/backend/core/tests/test_api_config.py b/src/backend/core/tests/test_api_config.py index 4eb879910..8bf4b5579 100644 --- a/src/backend/core/tests/test_api_config.py +++ b/src/backend/core/tests/test_api_config.py @@ -24,6 +24,7 @@ pytestmark = pytest.mark.django_db AI_FEATURE_BLOCKNOTE_ENABLED=False, AI_FEATURE_LEGACY_ENABLED=False, API_USERS_SEARCH_QUERY_MIN_LENGTH=6, + COLLABORATION_VERSION_GRANULARITY_MS=45000, COLLABORATION_WS_URL="http://testcollab/", COLLABORATION_WS_INACTIVITY_TIMEOUT=300, CONVERSION_UPLOAD_ENABLED=False, @@ -54,6 +55,7 @@ def test_api_config(is_authenticated): "AI_FEATURE_BLOCKNOTE_ENABLED": False, "AI_FEATURE_LEGACY_ENABLED": False, "API_USERS_SEARCH_QUERY_MIN_LENGTH": 6, + "COLLABORATION_VERSION_GRANULARITY_MS": 45000, "COLLABORATION_WS_URL": "http://testcollab/", "COLLABORATION_WS_INACTIVITY_TIMEOUT": 300, "CONVERSION_FILE_EXTENSIONS_ALLOWED": [".docx", ".md"], diff --git a/src/backend/impress/settings.py b/src/backend/impress/settings.py index 60136b03c..cf98a1f68 100755 --- a/src/backend/impress/settings.py +++ b/src/backend/impress/settings.py @@ -539,6 +539,13 @@ class Base(Configuration): environ_name="COLLABORATION_WS_INACTIVITY_TIMEOUT", environ_prefix=None, ) + # Granularity of the document version history, in milliseconds. + # Increase or decrease this value to adjust the granularity of version history. + COLLABORATION_VERSION_GRANULARITY_MS = values.IntegerValue( + 60000, + environ_name="COLLABORATION_VERSION_GRANULARITY_MS", + environ_prefix=None, + ) # Base url of the collaboration server's REST api, including its route # prefix (e.g. "http://yhub:3002/collaboration"). Server-to-server only: # used with an admin JWT to migrate legacy documents and, later, to kick diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-collaboration.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-collaboration.spec.ts index ab448455b..a5a05f2cd 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-collaboration.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-collaboration.spec.ts @@ -118,7 +118,7 @@ test.describe('Doc Collaboration', () => { await writeInEditor({ page, text: 'Hello after the reset' }); await expect(otherPage.getByText('Hello after the reset')).toBeVisible({ - timeout: 15000, + timeout: 10000, }); await cleanup(); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-version.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-version.spec.ts index db266cc17..ba8bf3e84 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-version.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-version.spec.ts @@ -4,50 +4,22 @@ import { createDoc, goToGridDoc, mockedDocument, - reopenDoc, + overrideConfig, verifyDocName, } from './utils-common'; import { openSuggestionMenu, writeInEditor } from './utils-editor'; -/** - * A version is at least a minute of editing: the collaboration server groups - * changes that are less than a minute apart, and the panel merges what is left - * across authors. That bound is the feature — a history of every few keystrokes - * is not a history — but it does mean a test cannot produce two versions - * without a minute of real time passing. The timestamps come from the server, - * so no clock can be faked to shorten it. - * - * The tests below therefore assert one version wherever one is enough, and only - * the restore test pays for a second one. - */ -const VERSION_GRANULARITY_MS = 60_000; - -test.beforeEach(async ({ page }) => { - await page.goto('/'); -}); +const COLLABORATION_VERSION_GRANULARITY_MS = 2000; test.describe('Doc Version', () => { - test('it displays an empty history', async ({ page, browserName }) => { - // Stubbed, because a document with no history at all cannot be reached - // through the interface: opening one writes to it, and that write is a - // change like any other. Only a document nobody has ever opened has an - // empty timeline. - await page.route('**/collaboration/activity/**', (route) => - route.fulfill({ json: { activity: [] } }), - ); - - await createDoc(page, 'doc-version-empty', browserName, 1); - - await page.getByLabel('Open the document options').click(); - await page.getByRole('menuitem', { name: 'Version history' }).click(); - - const modal = page.getByRole('dialog', { name: 'Version history' }); - await expect(modal.getByLabel('Version list')).toBeVisible(); - await expect(modal.getByText('No versions')).toBeVisible(); - }); - test('it displays the doc versions', async ({ page, browserName }) => { - const [randomDoc] = await createDoc(page, 'doc-version', browserName, 1); + await overrideConfig(page, { + COLLABORATION_VERSION_GRANULARITY_MS: `${COLLABORATION_VERSION_GRANULARITY_MS}`, + }); + + await page.goto('/'); + + await createDoc(page, 'doc-version', browserName, 1); // Initially, there is no version await page.getByLabel('Open the document options').click(); @@ -57,8 +29,15 @@ test.describe('Doc Version', () => { const modal = page.getByRole('dialog', { name: 'Version history' }); const panel = modal.getByLabel('Version list'); + await modal.getByRole('button', { name: 'close' }).click(); + await writeInEditor({ page, text: 'Hello World' }); + await page.waitForTimeout(COLLABORATION_VERSION_GRANULARITY_MS + 10); + + // Write more + await writeInEditor({ page, text: 'It will create a version' }); + const { suggestionMenu } = await openSuggestionMenu({ page }); await suggestionMenu.getByText('Add a callout block').click(); @@ -68,9 +47,12 @@ test.describe('Doc Version', () => { await expect(calloutBlock).toBeVisible(); - await reopenDoc(page, randomDoc); + await page.waitForTimeout(COLLABORATION_VERSION_GRANULARITY_MS + 10); - await expect(page.getByText('Hello World')).toBeVisible(); + // Write more + await writeInEditor({ page, text: 'It will create a second version' }); + + await page.waitForTimeout(COLLABORATION_VERSION_GRANULARITY_MS + 10); await page.getByLabel('Open the document options').click(); await page.getByRole('menuitem', { name: 'History' }).click(); @@ -78,24 +60,44 @@ test.describe('Doc Version', () => { await expect(panel).toBeVisible(); await expect(page.getByText('History', { exact: true })).toBeVisible(); await expect(page.getByRole('status')).toBeHidden(); - - // One entry: opening the document, naming it and typing into it all - // happened inside one minute, and a version is a minute of editing. const items = panel.locator('.version-item'); - await expect(items).toHaveCount(1); + await expect(items).toHaveCount(3); + await items.nth(2).click(); + + await expect(modal.getByText('Hello World')).toBeVisible(); + await expect(modal.getByText('It will create a version')).toBeHidden(); + await expect( + modal.locator('div[data-content-type="callout"]').first(), + ).toBeHidden(); + + await items.nth(1).click(); + + await expect(modal.getByText('Hello World')).toBeVisible(); + await expect(modal.getByText('It will create a version')).toBeVisible(); + await expect( + modal.locator('div[data-content-type="callout"]').first(), + ).toBeVisible(); + await expect( + modal.getByText('It will create a second version'), + ).toBeHidden(); await items.nth(0).click(); - // the preview renders the document as it stood at the end of that version await expect(modal.getByText('Hello World')).toBeVisible(); + await expect(modal.getByText('It will create a version')).toBeVisible(); await expect( modal.locator('div[data-content-type="callout"]').first(), ).toBeVisible(); + await expect( + modal.getByText('It will create a second version'), + ).toBeVisible(); }); test('it does not display the doc versions if not allowed', async ({ page, }) => { + await page.goto('/'); + await mockedDocument(page, { abilities: { versions_list: false, @@ -112,13 +114,13 @@ test.describe('Doc Version', () => { }); test('it restores the doc version', async ({ page, browserName }) => { - // The wait below is the whole reason for this budget, and it is not - // padding: two versions cannot exist any closer together. See the note at - // the top of the file before trying to make this faster. - test.setTimeout(VERSION_GRANULARITY_MS + 120_000); + await overrideConfig(page, { + COLLABORATION_VERSION_GRANULARITY_MS: `${COLLABORATION_VERSION_GRANULARITY_MS}`, + }); - const [randomDoc] = await createDoc(page, 'doc-version', browserName, 1); - await verifyDocName(page, randomDoc); + await page.goto('/'); + + await createDoc(page, 'doc-version', browserName, 1); const editor = await writeInEditor({ page, text: 'Hello' }); @@ -131,20 +133,16 @@ test.describe('Doc Version', () => { await thread.locator('[data-test="save"]').click(); await expect(thread).toBeHidden(); - await reopenDoc(page, randomDoc); - await expect(editor.getByText('Hello')).toBeVisible(); + await page.waitForTimeout(COLLABORATION_VERSION_GRANULARITY_MS + 10); - // Let the first version close before writing the text that has to end up - // in a second one — a minute of silence is what separates them. - await page.waitForTimeout(VERSION_GRANULARITY_MS + 1_000); + await writeInEditor({ page, text: 'World' }); - await page.locator('.bn-block-outer').last().click(); - await page.keyboard.press('Enter'); - await page.locator('.bn-block-outer').last().fill('World'); + await page.waitForTimeout(COLLABORATION_VERSION_GRANULARITY_MS + 10); - await reopenDoc(page, randomDoc); - - await expect(page.getByText('World')).toBeVisible(); + await editor.getByText('Hello').click(); + await thread.getByText('This is a comment').first().hover(); + await thread.locator('[data-test="resolve"]').click(); + await expect(thread).toBeHidden(); await page.getByLabel('Open the document options').click(); await page.getByRole('menuitem', { name: 'History' }).click(); @@ -152,14 +150,12 @@ test.describe('Doc Version', () => { const modal = page.getByRole('dialog', { name: 'Version history' }); const panel = modal.getByLabel('Version list'); await expect(panel).toBeVisible(); + await expect(page.getByText('History', { exact: true })).toBeVisible(); - - // newest first: the second item is the version that predates 'World' const items = panel.locator('.version-item'); - await expect(items).toHaveCount(2); - await items.nth(1).click(); + await expect(items).toHaveCount(3); + await items.nth(2).click(); - await expect(modal.getByText('Hello')).toBeVisible(); await expect(modal.getByText('World')).toBeHidden(); await page.getByRole('button', { name: 'Restore', exact: true }).click(); @@ -171,21 +167,23 @@ test.describe('Doc Version', () => { await page.getByLabel('Restore', { exact: true }).click(); - // The collaboration server applies the rollback and pushes it back over the - // connection this editor is already holding — nothing is reloaded here. const mainEditor = page.getByLabel('Document editor'); await expect(mainEditor.getByText('Hello')).toBeVisible(); await expect(mainEditor.getByText('World')).toBeHidden(); - // The comment survives, and that is the point: a restore undoes the changes - // made after the chosen version rather than replacing the document with an - // old copy of it. This comment belongs to the version being restored, so - // nothing about it was undone. - await expect(mainEditor.getByText('Hello')).toHaveClass('bn-thread-mark'); + // The old comment is not restored + await expect(mainEditor.getByText('Hello')).toHaveCSS( + 'background-color', + 'rgba(0, 0, 0, 0)', + ); - // and the document is still live afterwards - await mainEditor.getByText('Hello').click(); - await expect(thread.getByText('This is a comment').first()).toBeVisible(); + // We can add a new comment + await mainEditor.getByText('Hello').selectText(); + await page.getByRole('button', { name: 'Add comment' }).click(); + + await thread.getByRole('paragraph').first().fill('This is a comment'); + await thread.locator('[data-test="save"]').click(); + await expect(mainEditor.getByText('Hello')).toHaveClass('bn-thread-mark'); }); }); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/presenter-mode.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/presenter-mode.spec.ts index b9245c95e..92c252007 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/presenter-mode.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/presenter-mode.spec.ts @@ -3,7 +3,7 @@ import path from 'path'; import { Locator, Page, expect, test } from '@playwright/test'; import { PDFParse } from 'pdf-parse'; -import { createDoc, mockedDocument, reopenDoc } from './utils-common'; +import { createDoc, mockedDocument } from './utils-common'; import { openSuggestionMenu, tryFocusEditorContent, @@ -427,19 +427,10 @@ test.describe('Presenter Mode', () => { page, browserName, }) => { - const [docTitle] = await createDoc( - page, - 'presenter-deeplink', - browserName, - 1, - ); + await createDoc(page, 'presenter-deeplink', browserName, 1); await writeMultiSlideDoc(page); const docId = getDocIdFromUrl(page); - // Leave and come back before reloading through the deep-link, so what the - // deep-link opens is what the collaboration server kept, not what this tab - // still had in memory. - await reopenDoc(page, docTitle); await page.goto(`/docs/${docId}/?view=present&slide=3`); const overlay = page.getByRole('dialog', { name: 'Presenter mode' }); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts b/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts index af865f943..ea81b63bf 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts @@ -18,6 +18,7 @@ export const CONFIG = { AI_FEATURE_BLOCKNOTE_ENABLED: false, AI_FEATURE_LEGACY_ENABLED: true, API_USERS_SEARCH_QUERY_MIN_LENGTH: 3, + COLLABORATION_VERSION_GRANULARITY_MS: 60000, COLLABORATION_WS_INACTIVITY_TIMEOUT: 15, COLLABORATION_WS_URL: process.env.COLLABORATION_WS_URL, CONVERSION_UPLOAD_ENABLED: true, @@ -277,25 +278,6 @@ export const waitForResponseCreateDoc = (page: Page) => { ); }; -/** - * Leaves the doc and comes back to it, so that what follows reads the document - * from the collaboration server rather than from the editor that just wrote it. - * - * There is nothing to save: the collaboration server receives every change as - * it is typed, and the backend holds no copy of the content to be pushed to. - * (This used to wait for a `PATCH /content/`, which stopped existing with the - * migration — and so waited forever.) What makes the round trip meaningful is - * therefore the assertion that follows it: content still on the page after - * leaving and returning is content the server kept. - */ -export const reopenDoc = async (page: Page, title: string) => { - await page.getByRole('button', { name: 'Back to homepage' }).click(); - await expect(page.getByTestId('docs-grid')).toBeVisible(); - await expect(page.getByTestId('grid-loader')).toBeHidden(); - - await goToGridDoc(page, { title }); -}; - export const mockedDocument = async (page: Page, data: object) => { // document/[ID]/ or document/[ID]/tree/ routes let uuid: string | undefined; diff --git a/src/frontend/apps/e2e/__tests__/app-impress/utils-export.ts b/src/frontend/apps/e2e/__tests__/app-impress/utils-export.ts index ddd66983f..487a0246a 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/utils-export.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/utils-export.ts @@ -78,10 +78,6 @@ export const overrideDocContent = async ({ await page.goto(`/docs/${docId}/`); // the seed has to be on screen before anything is added after it - await expect(page.getByText('Hello Heading 1')).toBeVisible({ - timeout: 15000, - }); - await expect(page.getByText('copy/pasting out of doc')).toBeVisible(); // Add Image SVG diff --git a/src/frontend/apps/impress/src/core/config/api/useConfig.tsx b/src/frontend/apps/impress/src/core/config/api/useConfig.tsx index be5d56224..b309c0764 100644 --- a/src/frontend/apps/impress/src/core/config/api/useConfig.tsx +++ b/src/frontend/apps/impress/src/core/config/api/useConfig.tsx @@ -48,6 +48,7 @@ export interface ConfigResponse { AI_FEATURE_BLOCKNOTE_ENABLED?: boolean; AI_FEATURE_LEGACY_ENABLED?: boolean; API_USERS_SEARCH_QUERY_MIN_LENGTH?: number; + COLLABORATION_VERSION_GRANULARITY_MS?: number; COLLABORATION_WS_URL?: string; COLLABORATION_WS_INACTIVITY_TIMEOUT?: number | null; CONVERSION_FILE_EXTENSIONS_ALLOWED: string[]; diff --git a/src/frontend/apps/impress/src/features/docs/doc-versioning/__tests__/utils.test.ts b/src/frontend/apps/impress/src/features/docs/doc-versioning/__tests__/utils.test.ts index 350895db2..f22e9e55a 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-versioning/__tests__/utils.test.ts +++ b/src/frontend/apps/impress/src/features/docs/doc-versioning/__tests__/utils.test.ts @@ -1,7 +1,10 @@ import { describe, expect, it } from 'vitest'; import { ActivityEntry } from '../types'; -import { VERSION_GRANULARITY_MS, mergeActivityEntries } from '../utils'; +import { + VERSION_GRANULARITY_MS_FALLBACK as G, + mergeActivityEntries, +} from '../utils'; /** * `mergeActivityEntries` is the only part of the history policy that lives in @@ -15,8 +18,6 @@ const entry = (from: number, to: number, by: string | null): ActivityEntry => ({ by, }); -const G = VERSION_GRANULARITY_MS; - describe('mergeActivityEntries', () => { it('has nothing to say about an empty timeline', () => { expect(mergeActivityEntries([])).toEqual([]); diff --git a/src/frontend/apps/impress/src/features/docs/doc-versioning/api/useDocActivity.tsx b/src/frontend/apps/impress/src/features/docs/doc-versioning/api/useDocActivity.tsx index 294c2f254..309b63239 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-versioning/api/useDocActivity.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-versioning/api/useDocActivity.tsx @@ -6,12 +6,13 @@ import { UseQueryOptionsAPI, fetchCollaborationAPI, } from '@/api'; +import { useConfig } from '@/core/config/api'; import { useCollaborationTarget } from '@/core/config/hooks/useCollaborationUrl'; import { APIActivity, DocVersion } from '../types'; import { UNGROUPED_AUTHORS, - VERSION_GRANULARITY_MS, + VERSION_GRANULARITY_MS_FALLBACK, mergeActivityEntries, } from '../utils'; @@ -35,6 +36,7 @@ export type DocActivityParam = { const getDocActivity = async ( target: CollaborationTarget, { docId }: DocActivityParam, + granularityMs: number, ): Promise => { const { activity } = await fetchCollaborationAPI( target, @@ -43,8 +45,8 @@ const getDocActivity = async ( { query: { group: true, - groupMaxGap: VERSION_GRANULARITY_MS, - groupMaxDuration: VERSION_GRANULARITY_MS, + groupMaxGap: granularityMs, + groupMaxDuration: granularityMs, // the imported history is shown save by save — see UNGROUPED_AUTHORS. // The panel applies this again, on both sides of an entry. groupExclude: UNGROUPED_AUTHORS.join(','), @@ -53,7 +55,7 @@ const getDocActivity = async ( ); // ascending from the server, newest first for the panel - return mergeActivityEntries(activity).reverse(); + return mergeActivityEntries(activity, granularityMs).reverse(); }; export const KEY_DOC_ACTIVITY = 'doc-activity'; @@ -63,12 +65,17 @@ export function useDocActivity( queryConfig?: Omit, 'queryKey' | 'queryFn'>, ) { const target = useCollaborationTarget(); + const { data: conf } = useConfig(); + const granularityMs = + conf?.COLLABORATION_VERSION_GRANULARITY_MS ?? + VERSION_GRANULARITY_MS_FALLBACK; return useQuery({ - // `target` belongs in the key: it arrives with the configuration, so a - // query started before it resolved must not be reused after - queryKey: [KEY_DOC_ACTIVITY, params, target], - queryFn: () => getDocActivity(target as CollaborationTarget, params), + // `target` and `granularityMs` belong in the key: both arrive with the + // configuration, so a query started before they resolved must not be reused + queryKey: [KEY_DOC_ACTIVITY, params, target, granularityMs], + queryFn: () => + getDocActivity(target as CollaborationTarget, params, granularityMs), enabled: !!target, /** * Against the application's three-minute default, which is wrong for this diff --git a/src/frontend/apps/impress/src/features/docs/doc-versioning/components/ModalConfirmationVersion.tsx b/src/frontend/apps/impress/src/features/docs/doc-versioning/components/ModalConfirmationVersion.tsx index 5b5ac8497..c4d86c255 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-versioning/components/ModalConfirmationVersion.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-versioning/components/ModalConfirmationVersion.tsx @@ -9,10 +9,11 @@ import { useTranslation } from 'react-i18next'; import { createGlobalStyle } from 'styled-components'; import { Box, Text } from '@/components'; -import { Doc } from '@/docs/doc-management/'; +import { useThreadStore } from '@/docs/doc-comments/stores/useThreadStore'; +import { type Doc } from '@/docs/doc-management/'; import { useRestoreDocVersion } from '../api'; -import { DocVersion } from '../types'; +import { type DocVersion } from '../types'; const ModalStyle = createGlobalStyle` .c__modal__title { @@ -35,6 +36,7 @@ export const ModalConfirmationVersion = ({ }: ModalConfirmationVersionProps) => { const { t } = useTranslation(); const { toast } = useToastProvider(); + const { threadStore } = useThreadStore(); /** * The collaboration server undoes everything after this version and hands the @@ -45,6 +47,8 @@ export const ModalConfirmationVersion = ({ onSuccess: () => { toast(t('Version restored successfully'), VariantType.SUCCESS); onSuccess(); + + threadStore?.refreshThreads(); }, }); diff --git a/src/frontend/apps/impress/src/features/docs/doc-versioning/utils.ts b/src/frontend/apps/impress/src/features/docs/doc-versioning/utils.ts index 192c69e00..d8d6bc773 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-versioning/utils.ts +++ b/src/frontend/apps/impress/src/features/docs/doc-versioning/utils.ts @@ -9,7 +9,7 @@ import { ActivityEntry, DocVersion } from './types'; * at typing speed is far finer than anything worth listing — a history of every * few keystrokes is not a history. */ -export const VERSION_GRANULARITY_MS = 60_000; +export const VERSION_GRANULARITY_MS_FALLBACK = 60_000; /** * Authors whose changes are never merged into a version with anything else. @@ -56,7 +56,7 @@ export const UNGROUPED_AUTHORS = ['system']; */ export const mergeActivityEntries = ( activity: ActivityEntry[], - granularityMs: number = VERSION_GRANULARITY_MS, + granularityMs: number = VERSION_GRANULARITY_MS_FALLBACK, ): DocVersion[] => { const versions: DocVersion[] = []; const authors: Set[] = [];