feat(realtime): synchronize and show realtime activity state

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2023-03-28 15:32:15 +02:00 committed by Erik Michelson
parent 9497726a7c
commit 598fc8ee11
10 changed files with 85 additions and 17 deletions

View file

@ -78,10 +78,23 @@ export class RealtimeUserStatusAdapter {
}, },
) as Listener; ) 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', () => { connection.getTransporter().on('disconnected', () => {
transporterMessagesListener.off(); transporterMessagesListener.off();
transporterRequestMessageListener.off(); transporterRequestMessageListener.off();
clientRemoveListener.off(); clientRemoveListener.off();
realtimeUserSetActivityListener.off();
}); });
} }

View file

@ -16,6 +16,8 @@ export enum MessageType {
REALTIME_USER_STATE_SET = 'REALTIME_USER_STATE_SET', REALTIME_USER_STATE_SET = 'REALTIME_USER_STATE_SET',
REALTIME_USER_SINGLE_UPDATE = 'REALTIME_USER_SINGLE_UPDATE', REALTIME_USER_SINGLE_UPDATE = 'REALTIME_USER_SINGLE_UPDATE',
REALTIME_USER_STATE_REQUEST = 'REALTIME_USER_STATE_REQUEST', REALTIME_USER_STATE_REQUEST = 'REALTIME_USER_STATE_REQUEST',
REALTIME_USER_SET_ACTIVITY = 'REALTIME_USER_SET_ACTIVITY',
READY = 'READY' READY = 'READY'
} }
@ -30,6 +32,10 @@ export interface MessagePayloads {
} }
} }
[MessageType.REALTIME_USER_SINGLE_UPDATE]: RemoteCursor [MessageType.REALTIME_USER_SINGLE_UPDATE]: RemoteCursor
[MessageType.REALTIME_USER_SET_ACTIVITY]: {
active: boolean
}
} }
export type Message<T extends MessageType> = T extends keyof MessagePayloads export type Message<T extends MessageType> = T extends keyof MessagePayloads

View file

@ -269,7 +269,7 @@
}, },
"onlineStatus": { "onlineStatus": {
"online": "Online", "online": "Online",
"noUsers": "No users online" "you": "(You)"
}, },
"error": { "error": {
"locked": { "locked": {

View file

@ -28,6 +28,7 @@ import { useOnNoteDeleted } from './hooks/yjs/use-on-note-deleted'
import { useRealtimeConnection } from './hooks/yjs/use-realtime-connection' import { useRealtimeConnection } from './hooks/yjs/use-realtime-connection'
import { useRealtimeDoc } from './hooks/yjs/use-realtime-doc' import { useRealtimeDoc } from './hooks/yjs/use-realtime-doc'
import { useReceiveRealtimeUsers } from './hooks/yjs/use-receive-realtime-users' 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 { useYDocSyncClientAdapter } from './hooks/yjs/use-y-doc-sync-client-adapter'
import { useLinter } from './linter/linter' import { useLinter } from './linter/linter'
import { MaxLengthWarning } from './max-length-warning/max-length-warning' import { MaxLengthWarning } from './max-length-warning/max-length-warning'
@ -82,6 +83,7 @@ export const EditorPane: React.FC<EditorPaneProps> = ({ scrollState, onScroll, o
useBindYTextToRedux(realtimeDoc) useBindYTextToRedux(realtimeDoc)
useReceiveRealtimeUsers(messageTransporter) useReceiveRealtimeUsers(messageTransporter)
useSendRealtimeActivity(messageTransporter)
const extensions = useMemo( const extensions = useMemo(
() => [ () => [

View file

@ -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])
}

View file

@ -20,11 +20,3 @@
flex: 1 1 0; flex: 1 1 0;
overflow: hidden; overflow: hidden;
} }
.active-indicator-container {
height: 100%;
display: flex;
flex: 0 0 20px;
align-items: center;
justify-content: center;
}

View file

@ -9,22 +9,28 @@ import { createCursorCssClass } from '../../editor-pane/codemirror-extensions/re
import { ActiveIndicator } from '../users-online-sidebar-menu/active-indicator' import { ActiveIndicator } from '../users-online-sidebar-menu/active-indicator'
import styles from './user-line.module.scss' import styles from './user-line.module.scss'
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
import { Trans, useTranslation } from 'react-i18next'
export interface UserLineProps { export interface UserLineProps {
username: string | null username: string | null
displayName: string displayName: string
active: boolean active: boolean
own?: boolean
color: number color: number
} }
/** /**
* Represents a user in the realtime activity status. * Represents a user in the realtime activity status.
* *
* @param username The name of the user to show. * @param username The username of the user to show
* @param color The color of the user's edits. * @param color The color of the user's edits
* @param status The user's current online status. * @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<UserLineProps> = ({ username, displayName, active, color }) => { export const UserLine: React.FC<UserLineProps> = ({ username, displayName, active, own = false, color }) => {
useTranslation()
const avatar = useMemo(() => { const avatar = useMemo(() => {
if (username) { if (username) {
return ( return (
@ -48,9 +54,13 @@ export const UserLine: React.FC<UserLineProps> = ({ username, displayName, activ
)}`} )}`}
/> />
{avatar} {avatar}
<div className={styles['active-indicator-container']}> {own ? (
<span className={'px-1'}>
<Trans i18nKey={'editor.onlineStatus.you'}></Trans>
</span>
) : (
<ActiveIndicator active={active} /> <ActiveIndicator active={active} />
</div> )}
</div> </div>
) )
} }

View file

@ -18,3 +18,11 @@
background-color: #d20000; background-color: #d20000;
} }
} }
.active-indicator-container {
height: 100%;
display: flex;
flex: 0 0 20px;
align-items: center;
justify-content: center;
}

View file

@ -16,5 +16,9 @@ export interface ActiveIndicatorProps {
* @param status The state of the indicator to render * @param status The state of the indicator to render
*/ */
export const ActiveIndicator: React.FC<ActiveIndicatorProps> = ({ active }) => { export const ActiveIndicator: React.FC<ActiveIndicatorProps> = ({ active }) => {
return <span className={`${styles['activeIndicator']} ${active ? styles.active : styles.inactive}`} /> return (
<div className={styles['active-indicator-container']}>
<span className={`${styles['activeIndicator']} ${active ? styles.active : styles.inactive}`} />
</div>
)
} }

View file

@ -18,7 +18,7 @@ export const OwnUserLine: React.FC = () => {
return ( return (
<SidebarButton> <SidebarButton>
<UserLine displayName={ownDisplayname} username={ownUsername} color={ownStyleIndex} active={true} /> <UserLine displayName={ownDisplayname} username={ownUsername} color={ownStyleIndex} active={true} own={true} />
</SidebarButton> </SidebarButton>
) )
} }