✨(frontend) add useBroadcastStore
Add the useBroadcastStore. It will give us the ability to easily broadcast actions to all connected clients. In this case, we requery the doc to everyone when a change relative to the doc rights is made.
This commit is contained in:
@@ -12,6 +12,7 @@ and this project adheres to
|
|||||||
## Added
|
## Added
|
||||||
|
|
||||||
- 🌐(frontend) Add German translation #255
|
- 🌐(frontend) Add German translation #255
|
||||||
|
- ✨(frontend) Add a broadcast store #387
|
||||||
|
|
||||||
## Changed
|
## Changed
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ and this project adheres to
|
|||||||
|
|
||||||
- 🦺(backend) add comma to sub regex #408
|
- 🦺(backend) add comma to sub regex #408
|
||||||
- 🐛(editor) collaborative user tag hidden when read only #385
|
- 🐛(editor) collaborative user tag hidden when read only #385
|
||||||
|
- 🐛(frontend) user have view access when revoked #387
|
||||||
|
|
||||||
|
|
||||||
## [1.7.0] - 2024-10-24
|
## [1.7.0] - 2024-10-24
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
useTrans,
|
useTrans,
|
||||||
useUpdateDoc,
|
useUpdateDoc,
|
||||||
} from '@/features/docs/doc-management';
|
} from '@/features/docs/doc-management';
|
||||||
import { useResponsiveStore } from '@/stores';
|
import { useBroadcastStore, useResponsiveStore } from '@/stores';
|
||||||
import { isFirefox } from '@/utils/userAgent';
|
import { isFirefox } from '@/utils/userAgent';
|
||||||
|
|
||||||
interface DocTitleProps {
|
interface DocTitleProps {
|
||||||
@@ -54,6 +54,7 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
|
|||||||
const headingText = headings?.[0]?.contentText;
|
const headingText = headings?.[0]?.contentText;
|
||||||
const debounceRef = useRef<NodeJS.Timeout>();
|
const debounceRef = useRef<NodeJS.Timeout>();
|
||||||
const { isMobile } = useResponsiveStore();
|
const { isMobile } = useResponsiveStore();
|
||||||
|
const { broadcast } = useBroadcastStore();
|
||||||
|
|
||||||
const { mutate: updateDoc } = useUpdateDoc({
|
const { mutate: updateDoc } = useUpdateDoc({
|
||||||
listInvalideQueries: [KEY_DOC, KEY_LIST_DOC],
|
listInvalideQueries: [KEY_DOC, KEY_LIST_DOC],
|
||||||
@@ -61,6 +62,9 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
|
|||||||
if (data.title !== untitledDocument) {
|
if (data.title !== untitledDocument) {
|
||||||
toast(t('Document title updated successfully'), VariantType.SUCCESS);
|
toast(t('Document title updated successfully'), VariantType.SUCCESS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Broadcast to every user connected to the document
|
||||||
|
broadcast(`${KEY_DOC}-${data.id}`);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { APIError, errorCauses, fetchAPI } from '@/api';
|
import { APIError, errorCauses, fetchAPI } from '@/api';
|
||||||
import { Doc } from '@/features/docs';
|
import { Doc, KEY_DOC } from '@/features/docs/doc-management';
|
||||||
|
import { useBroadcastStore } from '@/stores';
|
||||||
|
|
||||||
export type UpdateDocLinkParams = Pick<Doc, 'id'> &
|
export type UpdateDocLinkParams = Pick<Doc, 'id'> &
|
||||||
Partial<Pick<Doc, 'link_role' | 'link_reach'>>;
|
Partial<Pick<Doc, 'link_role' | 'link_reach'>>;
|
||||||
@@ -37,14 +38,20 @@ export function useUpdateDocLink({
|
|||||||
listInvalideQueries,
|
listInvalideQueries,
|
||||||
}: UpdateDocLinkProps = {}) {
|
}: UpdateDocLinkProps = {}) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const { broadcast } = useBroadcastStore();
|
||||||
|
|
||||||
return useMutation<Doc, APIError, UpdateDocLinkParams>({
|
return useMutation<Doc, APIError, UpdateDocLinkParams>({
|
||||||
mutationFn: updateDocLink,
|
mutationFn: updateDocLink,
|
||||||
onSuccess: (data) => {
|
onSuccess: (data, variable) => {
|
||||||
listInvalideQueries?.forEach((queryKey) => {
|
listInvalideQueries?.forEach((queryKey) => {
|
||||||
void queryClient.resetQueries({
|
void queryClient.resetQueries({
|
||||||
queryKey: [queryKey],
|
queryKey: [queryKey],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Broadcast to every user connected to the document
|
||||||
|
broadcast(`${KEY_DOC}-${variable.id}`);
|
||||||
|
|
||||||
onSuccess?.(data);
|
onSuccess?.(data);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ import { User } from '@/core/auth';
|
|||||||
import {
|
import {
|
||||||
Access,
|
Access,
|
||||||
Doc,
|
Doc,
|
||||||
|
KEY_DOC,
|
||||||
KEY_LIST_DOC,
|
KEY_LIST_DOC,
|
||||||
Role,
|
Role,
|
||||||
} from '@/features/docs/doc-management';
|
} from '@/features/docs/doc-management';
|
||||||
import { KEY_LIST_DOC_ACCESSES } from '@/features/docs/members/members-list';
|
import { KEY_LIST_DOC_ACCESSES } from '@/features/docs/members/members-list';
|
||||||
import { ContentLanguage } from '@/i18n/types';
|
import { ContentLanguage } from '@/i18n/types';
|
||||||
|
import { useBroadcastStore } from '@/stores';
|
||||||
|
|
||||||
import { OptionType } from '../types';
|
import { OptionType } from '../types';
|
||||||
|
|
||||||
@@ -53,9 +55,11 @@ export const createDocAccess = async ({
|
|||||||
|
|
||||||
export function useCreateDocAccess() {
|
export function useCreateDocAccess() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const { broadcast } = useBroadcastStore();
|
||||||
|
|
||||||
return useMutation<Access, APIError, CreateDocAccessParams>({
|
return useMutation<Access, APIError, CreateDocAccessParams>({
|
||||||
mutationFn: createDocAccess,
|
mutationFn: createDocAccess,
|
||||||
onSuccess: () => {
|
onSuccess: (_data, variable) => {
|
||||||
void queryClient.resetQueries({
|
void queryClient.resetQueries({
|
||||||
queryKey: [KEY_LIST_DOC],
|
queryKey: [KEY_LIST_DOC],
|
||||||
});
|
});
|
||||||
@@ -65,6 +69,9 @@ export function useCreateDocAccess() {
|
|||||||
void queryClient.resetQueries({
|
void queryClient.resetQueries({
|
||||||
queryKey: [KEY_LIST_DOC_ACCESSES],
|
queryKey: [KEY_LIST_DOC_ACCESSES],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Broadcast to every user connected to the document
|
||||||
|
broadcast(`${KEY_DOC}-${variable.docId}`);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
import { APIError, errorCauses, fetchAPI } from '@/api';
|
import { APIError, errorCauses, fetchAPI } from '@/api';
|
||||||
import { KEY_DOC, KEY_LIST_DOC } from '@/features/docs/doc-management';
|
import { KEY_DOC, KEY_LIST_DOC } from '@/features/docs/doc-management';
|
||||||
import { KEY_LIST_USER } from '@/features/docs/members/members-add';
|
import { KEY_LIST_USER } from '@/features/docs/members/members-add';
|
||||||
|
import { useBroadcastStore } from '@/stores';
|
||||||
|
|
||||||
import { KEY_LIST_DOC_ACCESSES } from './useDocAccesses';
|
import { KEY_LIST_DOC_ACCESSES } from './useDocAccesses';
|
||||||
|
|
||||||
@@ -39,6 +40,8 @@ type UseDeleteDocAccessOptions = UseMutationOptions<
|
|||||||
|
|
||||||
export const useDeleteDocAccess = (options?: UseDeleteDocAccessOptions) => {
|
export const useDeleteDocAccess = (options?: UseDeleteDocAccessOptions) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const { broadcast } = useBroadcastStore();
|
||||||
|
|
||||||
return useMutation<void, APIError, DeleteDocAccessProps>({
|
return useMutation<void, APIError, DeleteDocAccessProps>({
|
||||||
mutationFn: deleteDocAccess,
|
mutationFn: deleteDocAccess,
|
||||||
...options,
|
...options,
|
||||||
@@ -49,6 +52,10 @@ export const useDeleteDocAccess = (options?: UseDeleteDocAccessOptions) => {
|
|||||||
void queryClient.invalidateQueries({
|
void queryClient.invalidateQueries({
|
||||||
queryKey: [KEY_DOC],
|
queryKey: [KEY_DOC],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Broadcast to every user connected to the document
|
||||||
|
broadcast(`${KEY_DOC}-${variables.docId}`);
|
||||||
|
|
||||||
void queryClient.resetQueries({
|
void queryClient.resetQueries({
|
||||||
queryKey: [KEY_LIST_DOC],
|
queryKey: [KEY_LIST_DOC],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
KEY_LIST_DOC,
|
KEY_LIST_DOC,
|
||||||
Role,
|
Role,
|
||||||
} from '@/features/docs/doc-management';
|
} from '@/features/docs/doc-management';
|
||||||
|
import { useBroadcastStore } from '@/stores';
|
||||||
|
|
||||||
import { KEY_LIST_DOC_ACCESSES } from './useDocAccesses';
|
import { KEY_LIST_DOC_ACCESSES } from './useDocAccesses';
|
||||||
|
|
||||||
@@ -49,6 +50,8 @@ type UseUpdateDocAccessOptions = UseMutationOptions<
|
|||||||
|
|
||||||
export const useUpdateDocAccess = (options?: UseUpdateDocAccessOptions) => {
|
export const useUpdateDocAccess = (options?: UseUpdateDocAccessOptions) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const { broadcast } = useBroadcastStore();
|
||||||
|
|
||||||
return useMutation<Access, APIError, UpdateDocAccessProps>({
|
return useMutation<Access, APIError, UpdateDocAccessProps>({
|
||||||
mutationFn: updateDocAccess,
|
mutationFn: updateDocAccess,
|
||||||
...options,
|
...options,
|
||||||
@@ -59,6 +62,10 @@ export const useUpdateDocAccess = (options?: UseUpdateDocAccessOptions) => {
|
|||||||
void queryClient.invalidateQueries({
|
void queryClient.invalidateQueries({
|
||||||
queryKey: [KEY_DOC],
|
queryKey: [KEY_DOC],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Broadcast to every user connected to the document
|
||||||
|
broadcast(`${KEY_DOC}-${variables.docId}`);
|
||||||
|
|
||||||
void queryClient.invalidateQueries({
|
void queryClient.invalidateQueries({
|
||||||
queryKey: [KEY_LIST_DOC],
|
queryKey: [KEY_LIST_DOC],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Loader } from '@openfun/cunningham-react';
|
import { Loader } from '@openfun/cunningham-react';
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import { useRouter as useNavigate } from 'next/navigation';
|
import { useRouter as useNavigate } from 'next/navigation';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
@@ -8,8 +9,9 @@ import { Box, Text } from '@/components';
|
|||||||
import { TextErrors } from '@/components/TextErrors';
|
import { TextErrors } from '@/components/TextErrors';
|
||||||
import { useAuthStore } from '@/core/auth';
|
import { useAuthStore } from '@/core/auth';
|
||||||
import { DocEditor } from '@/features/docs/doc-editor';
|
import { DocEditor } from '@/features/docs/doc-editor';
|
||||||
import { useDoc, useDocStore } from '@/features/docs/doc-management';
|
import { KEY_DOC, useDoc, useDocStore } from '@/features/docs/doc-management';
|
||||||
import { MainLayout } from '@/layouts';
|
import { MainLayout } from '@/layouts';
|
||||||
|
import { useBroadcastStore } from '@/stores';
|
||||||
import { NextPageWithLayout } from '@/types/next';
|
import { NextPageWithLayout } from '@/types/next';
|
||||||
|
|
||||||
export function DocLayout() {
|
export function DocLayout() {
|
||||||
@@ -42,8 +44,10 @@ const DocPage = ({ id }: DocProps) => {
|
|||||||
const { data: docQuery, isError, error } = useDoc({ id });
|
const { data: docQuery, isError, error } = useDoc({ id });
|
||||||
const [doc, setDoc] = useState(docQuery);
|
const [doc, setDoc] = useState(docQuery);
|
||||||
const { setCurrentDoc, createProvider, docsStore } = useDocStore();
|
const { setCurrentDoc, createProvider, docsStore } = useDocStore();
|
||||||
|
const { setBroadcastProvider, addTask } = useBroadcastStore();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const provider = docsStore?.[id]?.provider;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (doc?.title) {
|
if (doc?.title) {
|
||||||
@@ -71,12 +75,29 @@ const DocPage = ({ id }: DocProps) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const provider = docsStore?.[doc.id]?.provider;
|
let newProvider = provider;
|
||||||
|
|
||||||
if (!provider || provider.document.guid !== doc.id) {
|
if (!provider || provider.document.guid !== doc.id) {
|
||||||
createProvider(doc.id, doc.content);
|
newProvider = createProvider(doc.id, doc.content);
|
||||||
}
|
}
|
||||||
}, [createProvider, doc, docsStore]);
|
|
||||||
|
setBroadcastProvider(newProvider);
|
||||||
|
}, [createProvider, doc, provider, setBroadcastProvider]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We add a broadcast task to reset the query cache
|
||||||
|
* when the document visibility changes.
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (!doc?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
addTask(`${KEY_DOC}-${doc.id}`, () => {
|
||||||
|
void queryClient.resetQueries({
|
||||||
|
queryKey: [KEY_DOC, { id: doc.id }],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [addTask, doc?.id, queryClient]);
|
||||||
|
|
||||||
if (isError && error) {
|
if (isError && error) {
|
||||||
if (error.status === 404) {
|
if (error.status === 404) {
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
|
export * from './useBroadcastStore';
|
||||||
export * from './useResponsiveStore';
|
export * from './useResponsiveStore';
|
||||||
|
|||||||
54
src/frontend/apps/impress/src/stores/useBroadcastStore.tsx
Normal file
54
src/frontend/apps/impress/src/stores/useBroadcastStore.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { HocuspocusProvider } from '@hocuspocus/provider';
|
||||||
|
import * as Y from 'yjs';
|
||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
interface BroadcastState {
|
||||||
|
addTask: (taskLabel: string, action: () => void) => void;
|
||||||
|
broadcast: (taskLabel: string) => void;
|
||||||
|
getBroadcastProvider: () => HocuspocusProvider | undefined;
|
||||||
|
provider?: HocuspocusProvider;
|
||||||
|
setBroadcastProvider: (provider: HocuspocusProvider) => void;
|
||||||
|
tasks: { [taskLabel: string]: Y.Array<string> };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useBroadcastStore = create<BroadcastState>((set, get) => ({
|
||||||
|
provider: undefined,
|
||||||
|
tasks: {},
|
||||||
|
setBroadcastProvider: (provider) => set({ provider }),
|
||||||
|
getBroadcastProvider: () => {
|
||||||
|
const provider = get().provider;
|
||||||
|
if (!provider) {
|
||||||
|
console.warn('Provider is not defined');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return provider;
|
||||||
|
},
|
||||||
|
addTask: (taskLabel, action) => {
|
||||||
|
const taskExistAlready = get().tasks[taskLabel];
|
||||||
|
const provider = get().getBroadcastProvider();
|
||||||
|
if (taskExistAlready || !provider) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const task = provider.document.getArray<string>(taskLabel);
|
||||||
|
task.observe(() => {
|
||||||
|
action();
|
||||||
|
});
|
||||||
|
|
||||||
|
set((state) => ({
|
||||||
|
tasks: {
|
||||||
|
...state.tasks,
|
||||||
|
[taskLabel]: task,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
broadcast: (taskLabel) => {
|
||||||
|
const task = get().tasks[taskLabel];
|
||||||
|
if (!task) {
|
||||||
|
console.warn(`Task ${taskLabel} is not defined`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
task.push([`broadcast: ${taskLabel}`]);
|
||||||
|
},
|
||||||
|
}));
|
||||||
Reference in New Issue
Block a user