diff --git a/app/components/Editor.js b/app/components/Editor.js index b349938a..8a7ede7c 100644 --- a/app/components/Editor.js +++ b/app/components/Editor.js @@ -40,6 +40,7 @@ export type Props = {| maxLength?: number, scrollTo?: string, theme?: Theme, + className?: string, handleDOMEvents?: Object, readOnlyWriteCheckboxes?: boolean, onBlur?: (event: SyntheticEvent<>) => any, @@ -276,6 +277,7 @@ const StyledEditor = styled(RichMarkdownEditor)` } > div { opacity: 0; + transition: opacity 100ms ease-in-out; position: absolute; top: -1.8em; font-size: 13px; @@ -295,11 +297,14 @@ const StyledEditor = styled(RichMarkdownEditor)` &:hover { > div { opacity: 1; - transition: opacity 100ms ease-in-out; } } } } + + &.show-cursor-names .ProseMirror-yjs-cursor > div { + opacity: 1; + } `; const EditorTooltip = ({ children, ...props }) => ( diff --git a/app/hooks/useIsMounted.js b/app/hooks/useIsMounted.js new file mode 100644 index 00000000..c2b70402 --- /dev/null +++ b/app/hooks/useIsMounted.js @@ -0,0 +1,21 @@ +// @flow +import * as React from "react"; + +/** + * Hook to check if component is still mounted + * + * @returns {boolean} true if the component is mounted, false otherwise + */ +export default function useIsMounted() { + const isMounted = React.useRef(false); + + React.useEffect(() => { + isMounted.current = true; + + return () => { + isMounted.current = false; + }; + }, []); + + return React.useCallback(() => isMounted.current, []); +} diff --git a/app/scenes/Document/components/MultiplayerEditor.js b/app/scenes/Document/components/MultiplayerEditor.js index f1e2a688..1581264c 100644 --- a/app/scenes/Document/components/MultiplayerEditor.js +++ b/app/scenes/Document/components/MultiplayerEditor.js @@ -10,6 +10,7 @@ import env from "env"; import useCurrentToken from "hooks/useCurrentToken"; import useCurrentUser from "hooks/useCurrentUser"; import useIdle from "hooks/useIdle"; +import useIsMounted from "hooks/useIsMounted"; import usePageVisibility from "hooks/usePageVisibility"; import useStores from "hooks/useStores"; import useToasts from "hooks/useToasts"; @@ -28,6 +29,7 @@ function MultiplayerEditor({ ...props }: Props, ref: any) { const currentUser = useCurrentUser(); const { presence, ui } = useStores(); const token = useCurrentToken(); + const [showCursorNames, setShowCursorNames] = React.useState(false); const [remoteProvider, setRemoteProvider] = React.useState(); const [isLocalSynced, setLocalSynced] = React.useState(false); const [isRemoteSynced, setRemoteSynced] = React.useState(false); @@ -35,6 +37,7 @@ function MultiplayerEditor({ ...props }: Props, ref: any) { const { showToast } = useToasts(); const isIdle = useIdle(); const isVisible = usePageVisibility(); + const isMounted = useIsMounted(); // Provider initialization must be within useLayoutEffect rather than useState // or useMemo as both of these are ran twice in React StrictMode resulting in @@ -75,6 +78,18 @@ function MultiplayerEditor({ ...props }: Props, ref: any) { }); }); + const showCursorNames = () => { + setShowCursorNames(true); + setTimeout(() => { + if (isMounted()) { + setShowCursorNames(false); + } + }, 2000); + provider.off("awarenessChange", showCursorNames); + }; + + provider.on("awarenessChange", showCursorNames); + localProvider.on("synced", () => // only set local storage to "synced" if it's loaded a non-empty doc setLocalSynced(!!ydoc.get("default")._start) @@ -180,6 +195,7 @@ function MultiplayerEditor({ ...props }: Props, ref: any) { extensions={extensions} ref={showCache ? undefined : ref} style={showCache ? { display: "none" } : undefined} + className={showCursorNames ? "show-cursor-names" : undefined} /> );