feat: Added ability to click another user to observe them (sync scroll position) (#2858)
* feat: Added ability to click another user to observe them, mainly for fun * language, lower debounce, prevent tooltip from hiding when toggling observation * fix: Don't allow observing self, added banner at top of screen * Dont edit tooltip as it's confusing between our actions and theirs * snapshots
This commit is contained in:
@@ -16,6 +16,7 @@ type Props = EditorProps &
|
||||
WithTranslation & {
|
||||
onChangeTitle: (text: string) => void;
|
||||
title: string;
|
||||
id: string;
|
||||
document: Document;
|
||||
isDraft: boolean;
|
||||
multiplayer?: boolean;
|
||||
|
||||
@@ -29,6 +29,7 @@ import TemplatesMenu from "~/menus/TemplatesMenu";
|
||||
import { NavigationNode } from "~/types";
|
||||
import { metaDisplay } from "~/utils/keyboard";
|
||||
import { newDocumentPath, editDocumentUrl } from "~/utils/routeHelpers";
|
||||
import ObservingBanner from "./ObservingBanner";
|
||||
import PublicBreadcrumb from "./PublicBreadcrumb";
|
||||
import ShareButton from "./ShareButton";
|
||||
|
||||
@@ -194,6 +195,7 @@ function DocumentHeader({
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
<ObservingBanner />
|
||||
{isMobile && (
|
||||
<TocWrapper>
|
||||
<TableOfContentsMenu headings={headings} />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { HocuspocusProvider, WebSocketStatus } from "@hocuspocus/provider";
|
||||
import { throttle } from "lodash";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useHistory } from "react-router-dom";
|
||||
@@ -27,7 +28,9 @@ export type ConnectionStatus =
|
||||
| "disconnected"
|
||||
| void;
|
||||
|
||||
type AwarenessChangeEvent = { states: { user: { id: string }; cursor: any }[] };
|
||||
type AwarenessChangeEvent = {
|
||||
states: { user: { id: string }; cursor: any; scrollY: number | undefined }[];
|
||||
};
|
||||
|
||||
type ConnectionStatusEvent = { status: ConnectionStatus };
|
||||
|
||||
@@ -68,6 +71,23 @@ function MultiplayerEditor({ onSynced, ...props }: Props, ref: any) {
|
||||
token,
|
||||
});
|
||||
|
||||
const syncScrollPosition = throttle(() => {
|
||||
provider.setAwarenessField(
|
||||
"scrollY",
|
||||
window.scrollY / window.innerHeight
|
||||
);
|
||||
}, 200);
|
||||
|
||||
const finishObserving = () => {
|
||||
if (ui.observingUserId) {
|
||||
ui.setObservingUser(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("click", finishObserving);
|
||||
window.addEventListener("wheel", finishObserving);
|
||||
window.addEventListener("scroll", syncScrollPosition);
|
||||
|
||||
provider.on("authenticationFailed", () => {
|
||||
showToast(
|
||||
t(
|
||||
@@ -78,12 +98,16 @@ function MultiplayerEditor({ onSynced, ...props }: Props, ref: any) {
|
||||
});
|
||||
|
||||
provider.on("awarenessChange", ({ states }: AwarenessChangeEvent) => {
|
||||
states.forEach(({ user, cursor }) => {
|
||||
states.forEach(({ user, cursor, scrollY }) => {
|
||||
if (user) {
|
||||
// could know if the user is editing here using `state.cursor` but it
|
||||
// feels distracting in the UI, once multiplayer is on for everyone we
|
||||
// can stop diffentiating
|
||||
presence.touch(documentId, user.id, !!cursor);
|
||||
|
||||
if (scrollY !== undefined && user.id === ui.observingUserId) {
|
||||
window.scrollTo({
|
||||
top: scrollY * window.innerHeight,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -128,6 +152,9 @@ function MultiplayerEditor({ onSynced, ...props }: Props, ref: any) {
|
||||
setRemoteProvider(provider);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("click", finishObserving);
|
||||
window.removeEventListener("wheel", finishObserving);
|
||||
window.removeEventListener("scroll", syncScrollPosition);
|
||||
provider?.destroy();
|
||||
localProvider?.destroy();
|
||||
setRemoteProvider(null);
|
||||
@@ -228,6 +255,6 @@ function MultiplayerEditor({ onSynced, ...props }: Props, ref: any) {
|
||||
);
|
||||
}
|
||||
|
||||
export default React.forwardRef<typeof MultiplayerEditor, any>(
|
||||
export default React.forwardRef<typeof MultiplayerEditor, Props>(
|
||||
MultiplayerEditor
|
||||
);
|
||||
|
||||
60
app/scenes/Document/components/ObservingBanner.tsx
Normal file
60
app/scenes/Document/components/ObservingBanner.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { m, AnimatePresence } from "framer-motion";
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import useStores from "~/hooks/useStores";
|
||||
|
||||
const transition = {
|
||||
type: "spring",
|
||||
stiffness: 500,
|
||||
damping: 30,
|
||||
};
|
||||
|
||||
/**
|
||||
* A small banner that is displayed at the top of the screen when the user is
|
||||
* observing another user while editing a document
|
||||
*/
|
||||
function ObservingBanner() {
|
||||
const { ui, users } = useStores();
|
||||
const { t } = useTranslation();
|
||||
const user = ui.observingUserId ? users.get(ui.observingUserId) : undefined;
|
||||
|
||||
return (
|
||||
<Positioner>
|
||||
<AnimatePresence>
|
||||
{user && (
|
||||
<Banner
|
||||
$color={user.color}
|
||||
transition={transition}
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: -5 }}
|
||||
exit={{ opacity: 0, y: -30 }}
|
||||
>
|
||||
{t("Observing {{ userName }}", { userName: user.name })}
|
||||
</Banner>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</Positioner>
|
||||
);
|
||||
}
|
||||
|
||||
const Positioner = styled.div`
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
`;
|
||||
|
||||
const Banner = styled(m.div)<{ $color: string }>`
|
||||
padding: 6px 6px 1px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
z-index: ${(props) => props.theme.depths.header + 1};
|
||||
color: ${(props) => props.theme.white};
|
||||
background: ${(props) => props.$color};
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
`;
|
||||
|
||||
export default observer(ObservingBanner);
|
||||
Reference in New Issue
Block a user