1
1
import dynamic from 'next/dynamic' ;
2
+ import { useObservable , useSubscription } from 'observable-hooks' ;
2
3
import type { ReactNode } from 'react' ;
3
- import { createElement , isValidElement , useEffect , useState } from 'react' ;
4
+ import { useCallback , useEffect , useState } from 'react' ;
4
5
import * as rxjs from 'rxjs' ;
5
6
import { Grid } from '../components/grid' ;
6
7
import { useLogger } from '../components/logger' ;
@@ -10,110 +11,226 @@ import { useLogger } from '../components/logger';
10
11
// https://nextjs.org/docs/messages/react-hydration-error
11
12
const GridNoSSR = dynamic ( async ( ) => Grid , { ssr : false } ) ;
12
13
13
- interface DougProps {
14
- stream$ : rxjs . Observable < ReactNode > ;
14
+ interface DougCmpProps {
15
+ stream$ : rxjs . Observable < { streamId : string ; text : string } > ;
15
16
}
16
17
17
- const DougCmp : React . FC < DougProps > = ( props : DougProps ) : ReactNode => {
18
+ const DougCmp : React . FC < DougCmpProps > = ( props : DougCmpProps ) : ReactNode => {
18
19
const { stream$ } = props ;
19
20
20
- const [ lines , setLines ] = useState < Array < string > > ( [ ] ) ;
21
+ useSubscription ( stream$ , ( stream ) => {
22
+ if ( stream . text === '__CLEAR_STREAM__' ) {
23
+ setGameText ( [ ] ) ;
24
+ } else {
25
+ appendGameText ( stream . text ) ;
26
+ }
27
+ } ) ;
28
+
29
+ const [ gameText , setGameText ] = useState < Array < string > > ( [ ] ) ;
30
+
31
+ const appendGameText = useCallback ( ( newText : string ) => {
32
+ const scrollbackBuffer = 500 ; // max number of most recent lines to keep
33
+ newText = newText . replace ( / \n / g, '<br/>' ) ;
34
+ setGameText ( ( oldTexts ) => {
35
+ return oldTexts . concat ( newText ) . slice ( scrollbackBuffer * - 1 ) ;
36
+ } ) ;
37
+ } , [ ] ) ;
38
+
39
+ return (
40
+ < div >
41
+ { gameText . map ( ( text , index ) => {
42
+ return (
43
+ < span key = { index } style = { { fontFamily : 'Verdana' } } >
44
+ < span dangerouslySetInnerHTML = { { __html : text } } />
45
+ </ span >
46
+ ) ;
47
+ } ) }
48
+ </ div >
49
+ ) ;
50
+ } ;
51
+
52
+ // I started tracking this via `useState` but when calling it's setter
53
+ // the value did not update fast enough before a text game event
54
+ // was received, resulting in text routing to the wrong stream window.
55
+ let gameStreamId = '' ;
56
+
57
+ const GridPage : React . FC = ( ) : ReactNode => {
58
+ const { logger } = useLogger ( 'page:grid' ) ;
59
+
60
+ // Game events by subscribing to the game event IPC channel.
61
+ // Are routed to the correct game stream window via `gameStreamSubject$`.
62
+ const gameEventsSubject$ = useObservable ( ( ) => {
63
+ return new rxjs . Subject < { type : string } & Record < string , any > > ( ) ;
64
+ } ) ;
65
+
66
+ // Content destined for a specific game stream window.
67
+ // For example, 'room' or 'combat'.
68
+ const gameStreamSubject$ = useObservable ( ( ) => {
69
+ return new rxjs . Subject < { streamId : string ; text : string } > ( ) ;
70
+ } ) ;
71
+
72
+ // Track high level game events such as stream ids and formatting.
73
+ // Re-emit text events to the game stream subject to get to grid items.
74
+ useSubscription ( gameEventsSubject$ , ( gameEvent ) => {
75
+ switch ( gameEvent . type ) {
76
+ case 'TEXT' :
77
+ gameStreamSubject$ . next ( {
78
+ streamId : gameStreamId ,
79
+ text : gameEvent . text ,
80
+ } ) ;
81
+ break ;
82
+ case 'CLEAR_STREAM' :
83
+ gameStreamSubject$ . next ( {
84
+ streamId : gameEvent . streamId ,
85
+ text : '__CLEAR_STREAM__' ,
86
+ } ) ;
87
+ break ;
88
+ case 'PUSH_STREAM' :
89
+ gameStreamId = gameEvent . streamId ;
90
+ break ;
91
+ case 'POP_STREAM' :
92
+ gameStreamId = '' ;
93
+ break ;
94
+ }
95
+ } ) ;
21
96
22
97
useEffect ( ( ) => {
23
- console . log ( 'subscribing to stream' ) ;
24
- const subscription = stream$ . subscribe ( ( element ) => {
25
- if ( element ) {
26
- if ( isValidElement ( element ) ) {
27
- setLines ( ( lines ) => [ ...lines , element . props . children ] ) ;
28
- }
98
+ window . api . onMessage (
99
+ 'game:connect' ,
100
+ ( _event , { accountName, characterName, gameCode } ) => {
101
+ logger . info ( 'game:connect' , { accountName, characterName, gameCode } ) ;
29
102
}
103
+ ) ;
104
+
105
+ return ( ) => {
106
+ window . api . removeAllListeners ( 'game:connect' ) ;
107
+ } ;
108
+ } , [ logger ] ) ;
109
+
110
+ useEffect ( ( ) => {
111
+ window . api . onMessage (
112
+ 'game:disconnect' ,
113
+ ( _event , { accountName, characterName, gameCode } ) => {
114
+ logger . info ( 'game:disconnect' , {
115
+ accountName,
116
+ characterName,
117
+ gameCode,
118
+ } ) ;
119
+ }
120
+ ) ;
121
+
122
+ return ( ) => {
123
+ window . api . removeAllListeners ( 'game:disconnect' ) ;
124
+ } ;
125
+ } , [ logger ] ) ;
126
+
127
+ useEffect ( ( ) => {
128
+ window . api . onMessage ( 'game:error' , ( _event , error : Error ) => {
129
+ logger . error ( 'game:error' , { error } ) ;
30
130
} ) ;
31
131
32
132
return ( ) => {
33
- console . log ( 'unmounting' ) ;
34
- subscription . unsubscribe ( ) ;
133
+ window . api . removeAllListeners ( 'game:error' ) ;
35
134
} ;
36
- } , [ stream$ ] ) ;
135
+ } , [ logger ] ) ;
37
136
38
- const output = lines . map ( ( line , index ) => {
39
- return < p key = { index } > { line } </ p > ;
40
- } ) ;
41
- console . log ( 'rendering lines' , { output } ) ;
137
+ useEffect ( ( ) => {
138
+ window . api . onMessage ( 'game:event' , ( _event , gameEvent ) => {
139
+ logger . debug ( 'game:event' , { gameEvent } ) ;
140
+ gameEventsSubject$ . next ( gameEvent ) ;
141
+ } ) ;
42
142
43
- return < div > { output } </ div > ;
44
- } ;
143
+ return ( ) => {
144
+ window . api . removeAllListeners ( 'game:event' ) ;
145
+ } ;
146
+ } , [ logger , gameEventsSubject$ ] ) ;
45
147
46
- const GridPage : React . FC = ( ) : ReactNode => {
47
- const { logger } = useLogger ( 'page:grid' ) ;
148
+ // TODO the list of items we inject should come from user preferences
149
+ // if none then provide our own default list
150
+ // TODO users should be able to add/remove items from the grid
151
+ // we already support closing grid items, but not synced to prefs yet
48
152
49
- // TODO get a filtered game-stream of only <pushStream> tags
50
- // TODO load grid layout from storage (the 'i' property is the key)
51
- // TODO load game-window key values from storage (e.g. { id: 'percWindow', label: 'Spells' })
52
- // a lot of these we know, but DR may introduce others in the future
53
- // so there will be our default data plus user-defined data
54
-
55
- const gamePushStream$ = rxjs . interval ( 1000 ) . pipe (
56
- rxjs . take ( 10 ) ,
57
- rxjs . map ( ( i ) => {
58
- if ( i % 2 === 0 ) {
59
- return createElement (
60
- 'pushStream' ,
61
- { id : 'room' } ,
62
- 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla vitae elit libero, a pharetra augue.'
63
- ) ;
64
- }
65
- return createElement (
66
- 'pushStream' ,
67
- { id : 'percWindow' } ,
68
- 'Ethereal Shield (5 roisean)'
69
- ) ;
70
- } )
71
- ) ;
153
+ // TODO subscribe to game events and route them to the correct grid item
72
154
73
155
return (
74
156
< GridNoSSR
75
157
items = { [
76
158
{
77
159
itemId : 'room' ,
78
160
title : 'Room' ,
79
- // content: <DougCmp stream$={gamePushStream$} />,
80
- content : < div > empty</ div > ,
161
+ content : (
162
+ < DougCmp
163
+ stream$ = { gameStreamSubject$ . pipe (
164
+ rxjs . filter ( ( m ) => m . streamId === 'room' )
165
+ ) }
166
+ />
167
+ ) ,
81
168
} ,
82
169
{
83
170
itemId : 'percWindow' ,
84
171
title : 'Spells' ,
85
- // content: <DougCmp stream$={gamePushStream$} />,
86
- content : < div > empty</ div > ,
172
+ content : (
173
+ < DougCmp
174
+ stream$ = { gameStreamSubject$ . pipe (
175
+ rxjs . filter ( ( m ) => m . streamId === 'percWindow' )
176
+ ) }
177
+ />
178
+ ) ,
87
179
} ,
88
180
{
89
181
itemId : 'inv' ,
90
182
title : 'Inventory' ,
91
- // content: <DougCmp stream$={gamePushStream$} />,
92
- content : < div > empty</ div > ,
183
+ content : (
184
+ < DougCmp
185
+ stream$ = { gameStreamSubject$ . pipe (
186
+ rxjs . filter ( ( m ) => m . streamId === 'inv' )
187
+ ) }
188
+ />
189
+ ) ,
93
190
} ,
94
191
{
95
192
itemId : 'familiar' ,
96
193
title : 'Familiar' ,
97
- // content: <DougCmp stream$={gamePushStream$} />,
98
- content : < div > empty</ div > ,
194
+ content : (
195
+ < DougCmp
196
+ stream$ = { gameStreamSubject$ . pipe (
197
+ rxjs . filter ( ( m ) => m . streamId === 'familiar' )
198
+ ) }
199
+ />
200
+ ) ,
99
201
} ,
100
202
{
101
203
itemId : 'thoughts' ,
102
204
title : 'Thoughts' ,
103
- // content: <DougCmp stream$={gamePushStream$} />,
104
- content : < div > empty</ div > ,
205
+ content : (
206
+ < DougCmp
207
+ stream$ = { gameStreamSubject$ . pipe (
208
+ rxjs . filter ( ( m ) => m . streamId === 'thoughts' )
209
+ ) }
210
+ />
211
+ ) ,
105
212
} ,
106
213
{
107
214
itemId : 'combat' ,
108
215
title : 'Combat' ,
109
- // content: <DougCmp stream$={gamePushStream$} />,
110
- content : < div > empty</ div > ,
216
+ content : (
217
+ < DougCmp
218
+ stream$ = { gameStreamSubject$ . pipe (
219
+ rxjs . filter ( ( m ) => m . streamId === 'combat' )
220
+ ) }
221
+ />
222
+ ) ,
111
223
} ,
112
224
{
113
225
itemId : 'main' ,
114
226
title : 'Main' ,
115
- // content: <DougCmp stream$={gamePushStream$} />,
116
- content : < div > empty</ div > ,
227
+ content : (
228
+ < DougCmp
229
+ stream$ = { gameStreamSubject$ . pipe (
230
+ rxjs . filter ( ( m ) => m . streamId === '' )
231
+ ) }
232
+ />
233
+ ) ,
117
234
} ,
118
235
] }
119
236
/>
0 commit comments