1
- import { EuiPanel , EuiSpacer , EuiText } from '@elastic/eui' ;
2
- import { css } from '@emotion/react' ;
1
+ import { EuiPanel , EuiSpacer } from '@elastic/eui' ;
3
2
import { useObservable , useSubscription } from 'observable-hooks' ;
4
3
import type { ReactNode } from 'react' ;
5
4
import { useCallback , useEffect , useRef , useState } from 'react' ;
6
- import * as rxjs from 'rxjs' ;
5
+ import type * as rxjs from 'rxjs' ;
7
6
import type { GameLogLine } from '../../types/game.types' ;
7
+ import { GameStreamText } from './game-stream-text' ;
8
+ import {
9
+ excludeDuplicateEmptyLines ,
10
+ filterLinesForGameStreams ,
11
+ } from './game.utils' ;
8
12
9
13
export interface GameStreamProps {
10
14
/**
@@ -20,75 +24,15 @@ export interface GameStreamProps {
20
24
gameStreamIds : Array < string > ;
21
25
}
22
26
23
- /**
24
- * To help filter out duplicate empty log lines.
25
- */
26
- const emptyLogLine : GameLogLine = {
27
- eventId : '' ,
28
- streamId : '' ,
29
- text : '' ,
30
- styles : css ( ) ,
31
- } ;
32
-
33
- /**
34
- * Matches a log line that is either a newline or a prompt.
35
- * Effectively, an "empty" log line to the player.
36
- * https://regex101.com/r/TbkDIb/1
37
- */
38
- const emptyLogLineRegex = / ^ ( > ? ) ( \n + ) $ / ;
39
-
40
- /**
41
- * For the 'scroll' event to fire on the element, the overflow
42
- * property must be set. We rely on this to know if the user has
43
- * scrolled to the bottom (and we should engage in auto-scrolling)
44
- * or if they have scrolled away from the bottom (and we should
45
- * not auto-scroll).
46
- */
47
- const scrollablePanelStyles = css ( {
48
- overflowY : 'scroll' ,
49
- height : '100%' ,
50
- } ) ;
51
-
52
- const filterDuplicateEmptyLines : rxjs . MonoTypeOperatorFunction < GameLogLine > = (
53
- observable : rxjs . Observable < GameLogLine >
54
- ) => {
55
- return observable . pipe (
56
- // To do this, we need to compare the previous and current log lines.
57
- // We start with a blank log line so that the first real one is emitted.
58
- rxjs . startWith ( emptyLogLine ) ,
59
- rxjs . pairwise ( ) ,
60
- rxjs . filter ( ( [ prev , curr ] ) => {
61
- const previousText = prev . text ;
62
- const currentText = curr . text ;
63
-
64
- const previousWasNewline = emptyLogLineRegex . test ( previousText ) ;
65
- const currentIsNewline = emptyLogLineRegex . test ( currentText ) ;
66
-
67
- if ( ! currentIsNewline || ( currentIsNewline && ! previousWasNewline ) ) {
68
- return true ;
69
- }
70
- return false ;
71
- } ) ,
72
- // Unwind the pairwise to emit the current log line.
73
- rxjs . map ( ( [ _prev , curr ] ) => {
74
- return curr ;
75
- } )
76
- ) ;
77
- } ;
78
-
79
27
export const GameStream : React . FC < GameStreamProps > = (
80
28
props : GameStreamProps
81
29
) : ReactNode => {
82
30
const { stream$, gameStreamIds } = props ;
83
31
84
32
const filteredStream$ = useObservable ( ( ) => {
85
33
return stream$ . pipe (
86
- // Filter to only the game stream ids we care about.
87
- rxjs . filter ( ( logLine ) => {
88
- return gameStreamIds . includes ( logLine . streamId ) ;
89
- } ) ,
90
- // Avoid sending multiple blank newlines or prompts.
91
- filterDuplicateEmptyLines
34
+ filterLinesForGameStreams ( { gameStreamIds } ) ,
35
+ excludeDuplicateEmptyLines
92
36
) ;
93
37
} ) ;
94
38
@@ -168,23 +112,32 @@ export const GameStream: React.FC<GameStreamProps> = (
168
112
return (
169
113
< EuiPanel
170
114
panelRef = { scrollableRef }
171
- css = { scrollablePanelStyles }
115
+ // For the 'scroll' event to fire on the element, the overflow
116
+ // property must be set. We rely on this to know if the user has
117
+ // scrolled to the bottom and we should engage in auto-scrolling,
118
+ // or if they have scrolled away and we should not auto-scroll.
119
+ css = { {
120
+ overflowY : 'scroll' ,
121
+ height : '100%' ,
122
+ } }
172
123
className = "eui-scrollBar"
173
124
paddingSize = "none"
174
125
hasBorder = { false }
175
126
hasShadow = { false }
176
127
>
128
+ { /*
129
+ Disable scroll anchor so that when the user scrolls up in the stream
130
+ then as new content arrives it doesn't force the scroll position back.
131
+ Only when the user is scrolled to the bottom will the scroll position
132
+ be pinned to the bottom because that's the element with an anchor.
133
+ */ }
177
134
< div css = { { overflowAnchor : 'none' } } >
178
135
{ gameLogLines . map ( ( logLine ) => {
179
- return (
180
- < EuiText key = { logLine . eventId } css = { logLine . styles } >
181
- < span dangerouslySetInnerHTML = { { __html : logLine . text } } />
182
- </ EuiText >
183
- ) ;
136
+ return < GameStreamText key = { logLine . eventId } logLine = { logLine } /> ;
184
137
} ) }
185
138
</ div >
186
139
< EuiSpacer size = "s" />
187
- < div ref = { scrollTargetRef } />
140
+ < div ref = { scrollTargetRef } css = { { overflowAnchor : 'auto' } } />
188
141
</ EuiPanel >
189
142
) ;
190
143
} ;
0 commit comments