From 598fc8ee11d002889c3a47403402d498b66d3635 Mon Sep 17 00:00:00 2001 From: Tilman Vatteroth Date: Tue, 28 Mar 2023 15:32:15 +0200 Subject: [PATCH] feat(realtime): synchronize and show realtime activity state Signed-off-by: Tilman Vatteroth --- .../realtime-user-status-adapter.ts | 13 ++++++++ commons/src/message-transporters/message.ts | 6 ++++ frontend/locales/en.json | 2 +- .../editor-page/editor-pane/editor-pane.tsx | 2 ++ .../hooks/yjs/use-send-realtime-activity.ts | 33 +++++++++++++++++++ .../sidebar/user-line/user-line.module.scss | 8 ----- .../sidebar/user-line/user-line.tsx | 22 +++++++++---- .../active-indicator.module.scss | 8 +++++ .../active-indicator.tsx | 6 +++- .../own-user-line.tsx | 2 +- 10 files changed, 85 insertions(+), 17 deletions(-) create mode 100644 frontend/src/components/editor-page/editor-pane/hooks/yjs/use-send-realtime-activity.ts diff --git a/backend/src/realtime/realtime-note/realtime-user-status-adapter.ts b/backend/src/realtime/realtime-note/realtime-user-status-adapter.ts index 7ee3e2522..f83e0c760 100644 --- a/backend/src/realtime/realtime-note/realtime-user-status-adapter.ts +++ b/backend/src/realtime/realtime-note/realtime-user-status-adapter.ts @@ -78,10 +78,23 @@ export class RealtimeUserStatusAdapter { }, ) as Listener; + const realtimeUserSetActivityListener = connection.getTransporter().on( + MessageType.REALTIME_USER_SET_ACTIVITY, + (message) => { + if (this.realtimeUser.active === message.payload.active) { + return; + } + this.realtimeUser.active = message.payload.active; + this.sendRealtimeUserStatusUpdateEvent(connection); + }, + { objectify: true }, + ) as Listener; + connection.getTransporter().on('disconnected', () => { transporterMessagesListener.off(); transporterRequestMessageListener.off(); clientRemoveListener.off(); + realtimeUserSetActivityListener.off(); }); } diff --git a/commons/src/message-transporters/message.ts b/commons/src/message-transporters/message.ts index dfb2e410c..a93c394d4 100644 --- a/commons/src/message-transporters/message.ts +++ b/commons/src/message-transporters/message.ts @@ -16,6 +16,8 @@ export enum MessageType { REALTIME_USER_STATE_SET = 'REALTIME_USER_STATE_SET', REALTIME_USER_SINGLE_UPDATE = 'REALTIME_USER_SINGLE_UPDATE', REALTIME_USER_STATE_REQUEST = 'REALTIME_USER_STATE_REQUEST', + REALTIME_USER_SET_ACTIVITY = 'REALTIME_USER_SET_ACTIVITY', + READY = 'READY' } @@ -30,6 +32,10 @@ export interface MessagePayloads { } } [MessageType.REALTIME_USER_SINGLE_UPDATE]: RemoteCursor + + [MessageType.REALTIME_USER_SET_ACTIVITY]: { + active: boolean + } } export type Message = T extends keyof MessagePayloads diff --git a/frontend/locales/en.json b/frontend/locales/en.json index c6c216c0a..53aeda3fe 100644 --- a/frontend/locales/en.json +++ b/frontend/locales/en.json @@ -269,7 +269,7 @@ }, "onlineStatus": { "online": "Online", - "noUsers": "No users online" + "you": "(You)" }, "error": { "locked": { diff --git a/frontend/src/components/editor-page/editor-pane/editor-pane.tsx b/frontend/src/components/editor-page/editor-pane/editor-pane.tsx index faceaaddc..70ce7062f 100644 --- a/frontend/src/components/editor-page/editor-pane/editor-pane.tsx +++ b/frontend/src/components/editor-page/editor-pane/editor-pane.tsx @@ -28,6 +28,7 @@ import { useOnNoteDeleted } from './hooks/yjs/use-on-note-deleted' import { useRealtimeConnection } from './hooks/yjs/use-realtime-connection' import { useRealtimeDoc } from './hooks/yjs/use-realtime-doc' import { useReceiveRealtimeUsers } from './hooks/yjs/use-receive-realtime-users' +import { useSendRealtimeActivity } from './hooks/yjs/use-send-realtime-activity' import { useYDocSyncClientAdapter } from './hooks/yjs/use-y-doc-sync-client-adapter' import { useLinter } from './linter/linter' import { MaxLengthWarning } from './max-length-warning/max-length-warning' @@ -82,6 +83,7 @@ export const EditorPane: React.FC = ({ scrollState, onScroll, o useBindYTextToRedux(realtimeDoc) useReceiveRealtimeUsers(messageTransporter) + useSendRealtimeActivity(messageTransporter) const extensions = useMemo( () => [ diff --git a/frontend/src/components/editor-page/editor-pane/hooks/yjs/use-send-realtime-activity.ts b/frontend/src/components/editor-page/editor-pane/hooks/yjs/use-send-realtime-activity.ts new file mode 100644 index 000000000..a186dd02b --- /dev/null +++ b/frontend/src/components/editor-page/editor-pane/hooks/yjs/use-send-realtime-activity.ts @@ -0,0 +1,33 @@ +/* + * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import { useIsDocumentVisible } from '../../../../../hooks/common/use-is-document-visible' +import type { MessageTransporter } from '@hedgedoc/commons' +import { MessageType } from '@hedgedoc/commons' +import { useEffect } from 'react' +import { useIdle } from 'react-use' + +const INACTIVITY_TIMEOUT_SECONDS = 30 + +/** + * Sends the activity state (based on the fact if the tab is focused) to the backend. + * + * @param messageTransporter The message transporter that handles the connection to the backend + */ +export const useSendRealtimeActivity = (messageTransporter: MessageTransporter) => { + const active = useIsDocumentVisible() + const idling = useIdle(INACTIVITY_TIMEOUT_SECONDS * 1000) + + useEffect(() => { + messageTransporter.doAsSoonAsReady(() => { + messageTransporter.sendMessage({ + type: MessageType.REALTIME_USER_SET_ACTIVITY, + payload: { + active: active && !idling + } + }) + }) + }, [active, idling, messageTransporter]) +} diff --git a/frontend/src/components/editor-page/sidebar/user-line/user-line.module.scss b/frontend/src/components/editor-page/sidebar/user-line/user-line.module.scss index 31cdede99..bc789558c 100644 --- a/frontend/src/components/editor-page/sidebar/user-line/user-line.module.scss +++ b/frontend/src/components/editor-page/sidebar/user-line/user-line.module.scss @@ -20,11 +20,3 @@ flex: 1 1 0; overflow: hidden; } - -.active-indicator-container { - height: 100%; - display: flex; - flex: 0 0 20px; - align-items: center; - justify-content: center; -} diff --git a/frontend/src/components/editor-page/sidebar/user-line/user-line.tsx b/frontend/src/components/editor-page/sidebar/user-line/user-line.tsx index 77725a67a..53c04e94a 100644 --- a/frontend/src/components/editor-page/sidebar/user-line/user-line.tsx +++ b/frontend/src/components/editor-page/sidebar/user-line/user-line.tsx @@ -9,22 +9,28 @@ import { createCursorCssClass } from '../../editor-pane/codemirror-extensions/re import { ActiveIndicator } from '../users-online-sidebar-menu/active-indicator' import styles from './user-line.module.scss' import React, { useMemo } from 'react' +import { Trans, useTranslation } from 'react-i18next' export interface UserLineProps { username: string | null displayName: string active: boolean + own?: boolean color: number } /** * Represents a user in the realtime activity status. * - * @param username The name of the user to show. - * @param color The color of the user's edits. - * @param status The user's current online status. + * @param username The username of the user to show + * @param color The color of the user's edits + * @param status The user's current online status + * @param displayName The actual name that should be displayed + * @param own defines if this user line renders the own user or another one */ -export const UserLine: React.FC = ({ username, displayName, active, color }) => { +export const UserLine: React.FC = ({ username, displayName, active, own = false, color }) => { + useTranslation() + const avatar = useMemo(() => { if (username) { return ( @@ -48,9 +54,13 @@ export const UserLine: React.FC = ({ username, displayName, activ )}`} /> {avatar} -
+ {own ? ( + + + + ) : ( -
+ )} ) } diff --git a/frontend/src/components/editor-page/sidebar/users-online-sidebar-menu/active-indicator.module.scss b/frontend/src/components/editor-page/sidebar/users-online-sidebar-menu/active-indicator.module.scss index b7286d880..7fcea26ef 100644 --- a/frontend/src/components/editor-page/sidebar/users-online-sidebar-menu/active-indicator.module.scss +++ b/frontend/src/components/editor-page/sidebar/users-online-sidebar-menu/active-indicator.module.scss @@ -18,3 +18,11 @@ background-color: #d20000; } } + +.active-indicator-container { + height: 100%; + display: flex; + flex: 0 0 20px; + align-items: center; + justify-content: center; +} diff --git a/frontend/src/components/editor-page/sidebar/users-online-sidebar-menu/active-indicator.tsx b/frontend/src/components/editor-page/sidebar/users-online-sidebar-menu/active-indicator.tsx index 03e86a3b5..8616eae81 100644 --- a/frontend/src/components/editor-page/sidebar/users-online-sidebar-menu/active-indicator.tsx +++ b/frontend/src/components/editor-page/sidebar/users-online-sidebar-menu/active-indicator.tsx @@ -16,5 +16,9 @@ export interface ActiveIndicatorProps { * @param status The state of the indicator to render */ export const ActiveIndicator: React.FC = ({ active }) => { - return + return ( +
+ +
+ ) } diff --git a/frontend/src/components/editor-page/sidebar/users-online-sidebar-menu/own-user-line.tsx b/frontend/src/components/editor-page/sidebar/users-online-sidebar-menu/own-user-line.tsx index ccae46147..e90ab74ff 100644 --- a/frontend/src/components/editor-page/sidebar/users-online-sidebar-menu/own-user-line.tsx +++ b/frontend/src/components/editor-page/sidebar/users-online-sidebar-menu/own-user-line.tsx @@ -18,7 +18,7 @@ export const OwnUserLine: React.FC = () => { return ( - + ) }