Skip to content

Commit a53b7e7

Browse files
committed
feat: game content component
1 parent 941c237 commit a53b7e7

File tree

3 files changed

+144
-0
lines changed

3 files changed

+144
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { EuiText } from '@elastic/eui';
2+
import { useObservable, useSubscription } from 'observable-hooks';
3+
import type { ReactNode } from 'react';
4+
import { useCallback, useEffect, useRef, useState } from 'react';
5+
import * as rxjs from 'rxjs';
6+
import type { GameLogLine } from './game.types';
7+
8+
export interface GameContentProps {
9+
/**
10+
* The stream of game text to display.
11+
* The stream is additive, so each new line will be appended to the end.
12+
* The special log line text '__CLEAR_STREAM__' will clear all prior lines.
13+
*/
14+
stream$: rxjs.Observable<GameLogLine>;
15+
/**
16+
* The list of game stream ids that this component should display.
17+
* Most components will only display a single stream id.
18+
*/
19+
gameStreamIds: Array<string>;
20+
/**
21+
* Enable to automatically scroll to the bottom of the game stream window
22+
* as new log lines are added. This effect only occurs if the user
23+
* is already scrolled to the bottom to ensure they see latest content.
24+
*/
25+
enableScrollToNewLogLines: boolean;
26+
}
27+
28+
export const GameContent: React.FC<GameContentProps> = (
29+
props: GameContentProps
30+
): ReactNode => {
31+
const { stream$, gameStreamIds, enableScrollToNewLogLines } = props;
32+
33+
const filteredStream$ = useObservable(() => {
34+
return stream$.pipe(rxjs.filter((m) => gameStreamIds.includes(m.streamId)));
35+
});
36+
37+
useSubscription(filteredStream$, (logLine) => {
38+
if (logLine.text === '__CLEAR_STREAM__') {
39+
setGameLogLines([]);
40+
} else {
41+
appendGameLogLine(logLine);
42+
}
43+
});
44+
45+
const scrollableRef = useRef<HTMLDivElement>(null);
46+
const scrollBottomRef = useRef<HTMLSpanElement>(null);
47+
48+
const [autoScrollEnabled, setAutoScrollEnabled] = useState<boolean>(
49+
enableScrollToNewLogLines
50+
);
51+
52+
const [gameLogLines, setGameLogLines] = useState<Array<GameLogLine>>([]);
53+
54+
const appendGameLogLine = useCallback((newLogLine: GameLogLine) => {
55+
// Max number of most recent lines to keep.
56+
const scrollbackBuffer = 500;
57+
setGameLogLines((oldLogLines) => {
58+
// Append new log line to the list.
59+
let newLogLines = oldLogLines.concat(newLogLine);
60+
// Trim the back of the list to keep it within the scrollback buffer.
61+
newLogLines = newLogLines.slice(scrollbackBuffer * -1);
62+
return newLogLines;
63+
});
64+
}, []);
65+
66+
useEffect(() => {
67+
if (!enableScrollToNewLogLines) {
68+
return;
69+
}
70+
71+
let scrollableElmt = scrollableRef.current;
72+
73+
const onScroll = () => {
74+
scrollableElmt = scrollableRef.current;
75+
76+
if (!scrollableElmt) {
77+
return;
78+
}
79+
80+
const { scrollTop, scrollHeight, clientHeight } = scrollableElmt;
81+
const difference = scrollHeight - clientHeight - scrollTop;
82+
const enableAutoScroll = difference <= clientHeight;
83+
84+
setAutoScrollEnabled(enableAutoScroll);
85+
};
86+
87+
scrollableElmt?.addEventListener('scroll', onScroll);
88+
89+
return () => {
90+
scrollableElmt?.removeEventListener('scroll', onScroll);
91+
};
92+
}, [enableScrollToNewLogLines]);
93+
94+
if (autoScrollEnabled) {
95+
scrollBottomRef.current?.scrollIntoView({
96+
behavior: 'instant',
97+
block: 'end',
98+
inline: 'nearest',
99+
});
100+
}
101+
102+
return (
103+
<div
104+
ref={scrollableRef}
105+
className={'eui-yScroll'}
106+
style={{ height: '100%', overflowY: 'scroll' }}
107+
>
108+
{gameLogLines.map((logLine) => {
109+
return (
110+
<EuiText key={logLine.eventId} css={logLine.styles}>
111+
{logLine.text}
112+
</EuiText>
113+
);
114+
})}
115+
<span ref={scrollBottomRef} />
116+
</div>
117+
);
118+
};
119+
120+
GameContent.displayName = 'GameContent';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { SerializedStyles } from '@emotion/react';
2+
3+
export interface GameLogLine {
4+
/**
5+
* A unique id for this log line.
6+
* Primarily used for React keys.
7+
* https://react.dev/learn/rendering-lists
8+
*/
9+
eventId: string;
10+
/**
11+
* The game stream id that this line is destined for.
12+
*/
13+
streamId: string;
14+
/**
15+
* The text formatting to apply to this line.
16+
*/
17+
styles: SerializedStyles;
18+
/**
19+
* The text to display.
20+
*/
21+
text: string;
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './game-content';
2+
export * from './game.types';

0 commit comments

Comments
 (0)