mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-24 18:56:32 -05:00
feat(realtime): synchronize and show realtime activity state
Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
9497726a7c
commit
598fc8ee11
10 changed files with 85 additions and 17 deletions
|
@ -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();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -269,7 +269,7 @@
|
||||||
},
|
},
|
||||||
"onlineStatus": {
|
"onlineStatus": {
|
||||||
"online": "Online",
|
"online": "Online",
|
||||||
"noUsers": "No users online"
|
"you": "(You)"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"locked": {
|
"locked": {
|
||||||
|
|
|
@ -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(
|
||||||
() => [
|
() => [
|
||||||
|
|
|
@ -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])
|
||||||
|
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue