@100mslive/hms-whiteboard 0.0.6-alpha.0 → 0.0.6-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,169 @@
1
+ import React, { ComponentType, useEffect, useLayoutEffect, useRef, useState } from 'react';
2
+ import { useValue } from '@tldraw/state';
3
+ import { Editor, hardResetEditor } from '@tldraw/tldraw';
4
+ import classNames from 'classnames';
5
+
6
+ const DISCORD_URL = 'https://discord.gg/pTge2BwDBq';
7
+
8
+ export type TLErrorFallbackComponent = ComponentType<{
9
+ error: unknown;
10
+ refresh: () => void;
11
+ editor?: Editor;
12
+ }>;
13
+
14
+ export const ErrorFallback: TLErrorFallbackComponent = ({ error, editor, refresh }) => {
15
+ const containerRef = useRef<HTMLDivElement>(null);
16
+ const [shouldShowError, setShouldShowError] = useState(process.env.NODE_ENV !== 'production');
17
+ const [didCopy, setDidCopy] = useState(false);
18
+ const [shouldShowResetConfirmation, setShouldShowResetConfirmation] = useState(false);
19
+
20
+ const errorMessage = error instanceof Error ? error.message : String(error);
21
+ const errorStack = error instanceof Error ? error.stack : null;
22
+
23
+ const isDarkModeFromApp = useValue(
24
+ 'isDarkMode',
25
+ () => {
26
+ try {
27
+ if (editor) {
28
+ return editor.user.getIsDarkMode();
29
+ }
30
+ } catch {
31
+ // we're in a funky error state so this might not work for spooky
32
+ // reasons. if not, we'll have another attempt later:
33
+ }
34
+ return null;
35
+ },
36
+ [editor],
37
+ );
38
+ const [
39
+ ,
40
+ // isDarkMode
41
+ setIsDarkMode,
42
+ ] = useState<null | boolean>(null);
43
+ useLayoutEffect(() => {
44
+ // if we found a theme class from the app, we can just use that
45
+ if (isDarkModeFromApp !== null) {
46
+ setIsDarkMode(isDarkModeFromApp);
47
+ }
48
+
49
+ // do any of our parents have a theme class? if yes then we can just
50
+ // rely on that and don't need to set our own class
51
+ let parent = containerRef.current?.parentElement;
52
+ let foundParentThemeClass = false;
53
+ while (parent) {
54
+ if (parent.classList.contains('tl-theme__dark') || parent.classList.contains('tl-theme__light')) {
55
+ foundParentThemeClass = true;
56
+ break;
57
+ }
58
+ parent = parent.parentElement;
59
+ }
60
+ if (foundParentThemeClass) {
61
+ setIsDarkMode(null);
62
+ return;
63
+ }
64
+
65
+ // if we can't find a theme class from the app or from a parent, we have
66
+ // to fall back on using a media query:
67
+ setIsDarkMode(window.matchMedia('(prefers-color-scheme: dark)').matches);
68
+ }, [isDarkModeFromApp]);
69
+
70
+ useEffect(() => {
71
+ if (didCopy) {
72
+ const timeout = setTimeout(() => {
73
+ setDidCopy(false);
74
+ }, 2000);
75
+ return () => clearTimeout(timeout);
76
+ }
77
+ }, [didCopy]);
78
+
79
+ const copyError = () => {
80
+ const textarea = document.createElement('textarea');
81
+ textarea.value = errorStack ?? errorMessage;
82
+ document.body.appendChild(textarea);
83
+ textarea.select();
84
+ document.execCommand('copy');
85
+ textarea.remove();
86
+ setDidCopy(true);
87
+ };
88
+
89
+ const resetLocalState = async () => {
90
+ hardResetEditor();
91
+ };
92
+
93
+ return (
94
+ <div
95
+ ref={containerRef}
96
+ className={classNames(
97
+ 'tl-container tl-error-boundary',
98
+ // error-boundary is sometimes used outside of the theme
99
+ // container, so we need to provide it with a theme for our
100
+ // styles to work correctly
101
+ // 100ms: default light theme
102
+ // isDarkMode === null ? '' : isDarkMode ? 'tl-theme__dark' : 'tl-theme__light',
103
+ 'tl-theme__light',
104
+ )}
105
+ style={{ position: 'static' }}
106
+ >
107
+ <div className="tl-error-boundary__overlay" />
108
+ {/* {editor && (
109
+ // opportunistically attempt to render the canvas to reassure
110
+ // the user that their document is still there. there's a good
111
+ // chance this won't work (ie the error that we're currently
112
+ // notifying the user about originates in the canvas) so it's
113
+ // not a big deal if it doesn't work - in that case we just have
114
+ // a plain grey background.
115
+ <ErrorBoundary onError={noop} fallback={() => null}>
116
+ <EditorContext.Provider value={editor}>
117
+ <div className="tl-overlay tl-error-boundary__canvas">
118
+ <Canvas />
119
+ </div>
120
+ </EditorContext.Provider>
121
+ </ErrorBoundary>
122
+ )} */}
123
+ <div
124
+ className={classNames('tl-modal', 'tl-error-boundary__content', {
125
+ 'tl-error-boundary__content__expanded': shouldShowError && !shouldShowResetConfirmation,
126
+ })}
127
+ >
128
+ {shouldShowResetConfirmation ? (
129
+ <>
130
+ <h2>Are you sure?</h2>
131
+ <p>Resetting your data will delete your drawing and cannot be undone.</p>
132
+ <div className="tl-error-boundary__content__actions">
133
+ <button onClick={() => setShouldShowResetConfirmation(false)}>Cancel</button>
134
+ <button className="tl-error-boundary__reset" onClick={resetLocalState}>
135
+ Reset data
136
+ </button>
137
+ </div>
138
+ </>
139
+ ) : (
140
+ <>
141
+ <h2>Something&apos;s gone wrong.</h2>
142
+ <p>
143
+ Sorry, we encountered an error. Please refresh the page to continue. If you keep seeing this error, you
144
+ can <a href={DISCORD_URL}>ask for help on Discord</a>.
145
+ </p>
146
+ {shouldShowError && (
147
+ <div className="tl-error-boundary__content__error">
148
+ <pre>
149
+ <code>{errorStack ?? errorMessage}</code>
150
+ </pre>
151
+ <button onClick={copyError}>{didCopy ? 'Copied!' : 'Copy'}</button>
152
+ </div>
153
+ )}
154
+ <div className="tl-error-boundary__content__actions">
155
+ <button onClick={() => setShouldShowError(!shouldShowError)}>
156
+ {shouldShowError ? 'Hide details' : 'Show details'}
157
+ </button>
158
+ <div className="tl-error-boundary__content__actions__group">
159
+ <button className="tl-error-boundary__refresh" onClick={refresh}>
160
+ Refresh
161
+ </button>
162
+ </div>
163
+ </div>
164
+ </>
165
+ )}
166
+ </div>
167
+ </div>
168
+ );
169
+ };
@@ -0,0 +1,60 @@
1
+ import React, { useState } from 'react';
2
+ import { Editor, Tldraw } from '@tldraw/tldraw';
3
+ import { ErrorFallback } from './ErrorFallback';
4
+ import { useCollaboration } from './hooks/useCollaboration';
5
+ import './index.css';
6
+
7
+ export interface WhiteboardProps {
8
+ endpoint?: string;
9
+ token: string;
10
+ zoomToContent?: boolean;
11
+ transparentCanvas?: boolean;
12
+ onMount?: (args: { store?: unknown; editor?: unknown }) => void;
13
+ }
14
+ export function Whiteboard(props: WhiteboardProps) {
15
+ const [key, setKey] = useState(Date.now() + props.token);
16
+
17
+ return <CollaborativeEditor key={key} refresh={() => setKey(Date.now() + props.token)} {...props} />;
18
+ }
19
+
20
+ function CollaborativeEditor({
21
+ endpoint,
22
+ token,
23
+ zoomToContent,
24
+ transparentCanvas,
25
+ onMount,
26
+ refresh,
27
+ }: WhiteboardProps & { refresh: () => void }) {
28
+ const [editor, setEditor] = useState<Editor>();
29
+ const store = useCollaboration({
30
+ endpoint,
31
+ token,
32
+ editor,
33
+ zoomToContent,
34
+ });
35
+
36
+ const handleMount = (editor: Editor) => {
37
+ setEditor(editor);
38
+ // @ts-expect-error - for debugging
39
+ window.editor = editor;
40
+ onMount?.({ store: store.store, editor });
41
+ };
42
+
43
+ if (store.status === 'synced-remote' && store.connectionStatus === 'offline') {
44
+ return <ErrorFallback error={Error('Network connection lost')} editor={editor} refresh={refresh} />;
45
+ }
46
+
47
+ return (
48
+ <Tldraw
49
+ className={transparentCanvas ? 'transparent-canvas' : ''}
50
+ autoFocus
51
+ store={store}
52
+ onMount={handleMount}
53
+ components={{
54
+ ErrorFallback: ({ error, editor }) => <ErrorFallback editor={editor} error={error} refresh={refresh} />,
55
+ }}
56
+ hideUi={editor?.getInstanceState()?.isReadonly}
57
+ initialState={editor?.getInstanceState()?.isReadonly ? 'hand' : 'select'}
58
+ />
59
+ );
60
+ }
@@ -0,0 +1,174 @@
1
+ // @generated by protobuf-ts 2.9.4 with parameter long_type_string
2
+ // @generated from protobuf file "sessionstore.proto" (package "sessionstorepb", syntax proto3)
3
+ // tslint:disable
4
+ import type { RpcOptions, RpcTransport, ServerStreamingCall, ServiceInfo, UnaryCall } from '@protobuf-ts/runtime-rpc';
5
+ import { stackIntercept } from '@protobuf-ts/runtime-rpc';
6
+ import type {
7
+ ChangeStream,
8
+ CountRequest,
9
+ CountResponse,
10
+ DeleteRequest,
11
+ DeleteResponse,
12
+ Event,
13
+ GetRequest,
14
+ GetResponse,
15
+ HelloRequest,
16
+ HelloResponse,
17
+ OpenRequest,
18
+ SetRequest,
19
+ SetResponse,
20
+ SubscribeRequest,
21
+ } from './sessionstore';
22
+ import { Api, Store } from './sessionstore';
23
+ /**
24
+ * @generated from protobuf service sessionstorepb.Api
25
+ */
26
+ export interface IApiClient {
27
+ /**
28
+ * @generated from protobuf rpc: Hello(sessionstorepb.HelloRequest) returns (sessionstorepb.HelloResponse);
29
+ */
30
+ hello(input: HelloRequest, options?: RpcOptions): UnaryCall<HelloRequest, HelloResponse>;
31
+ /**
32
+ * @generated from protobuf rpc: Subscribe(sessionstorepb.SubscribeRequest) returns (stream sessionstorepb.Event);
33
+ */
34
+ subscribe(input: SubscribeRequest, options?: RpcOptions): ServerStreamingCall<SubscribeRequest, Event>;
35
+ }
36
+ /**
37
+ * @generated from protobuf service sessionstorepb.Api
38
+ */
39
+ export class ApiClient implements IApiClient, ServiceInfo {
40
+ typeName = Api.typeName;
41
+ methods = Api.methods;
42
+ options = Api.options;
43
+ constructor(private readonly _transport: RpcTransport) {}
44
+ /**
45
+ * @generated from protobuf rpc: Hello(sessionstorepb.HelloRequest) returns (sessionstorepb.HelloResponse);
46
+ */
47
+ hello(input: HelloRequest, options?: RpcOptions): UnaryCall<HelloRequest, HelloResponse> {
48
+ const method = this.methods[0],
49
+ opt = this._transport.mergeOptions(options);
50
+ return stackIntercept<HelloRequest, HelloResponse>('unary', this._transport, method, opt, input);
51
+ }
52
+ /**
53
+ * @generated from protobuf rpc: Subscribe(sessionstorepb.SubscribeRequest) returns (stream sessionstorepb.Event);
54
+ */
55
+ subscribe(input: SubscribeRequest, options?: RpcOptions): ServerStreamingCall<SubscribeRequest, Event> {
56
+ const method = this.methods[1],
57
+ opt = this._transport.mergeOptions(options);
58
+ return stackIntercept<SubscribeRequest, Event>('serverStreaming', this._transport, method, opt, input);
59
+ }
60
+ }
61
+ // metadata token -> session id, room id, user id, username
62
+
63
+ // open is used for presence
64
+
65
+ // change stream will return all keys in order of oldest to newsest.
66
+
67
+ // max number of keys -> 5000
68
+ // max size per key -> 10240 Bytes
69
+
70
+ /**
71
+ * @generated from protobuf service sessionstorepb.Store
72
+ */
73
+ export interface IStoreClient {
74
+ /**
75
+ * open - start listening to updates in keys with provided match patterns
76
+ * provide change_id as last received ID to resume updates
77
+ *
78
+ * @generated from protobuf rpc: open(sessionstorepb.OpenRequest) returns (stream sessionstorepb.ChangeStream);
79
+ */
80
+ open(input: OpenRequest, options?: RpcOptions): ServerStreamingCall<OpenRequest, ChangeStream>;
81
+ /**
82
+ * get last stored value in given key
83
+ *
84
+ * @generated from protobuf rpc: get(sessionstorepb.GetRequest) returns (sessionstorepb.GetResponse);
85
+ */
86
+ get(input: GetRequest, options?: RpcOptions): UnaryCall<GetRequest, GetResponse>;
87
+ /**
88
+ * set key value
89
+ *
90
+ * @generated from protobuf rpc: set(sessionstorepb.SetRequest) returns (sessionstorepb.SetResponse);
91
+ */
92
+ set(input: SetRequest, options?: RpcOptions): UnaryCall<SetRequest, SetResponse>;
93
+ /**
94
+ * delete key from store
95
+ *
96
+ * @generated from protobuf rpc: delete(sessionstorepb.DeleteRequest) returns (sessionstorepb.DeleteResponse);
97
+ */
98
+ delete(input: DeleteRequest, options?: RpcOptions): UnaryCall<DeleteRequest, DeleteResponse>;
99
+ /**
100
+ * count get count of keys
101
+ *
102
+ * @generated from protobuf rpc: count(sessionstorepb.CountRequest) returns (sessionstorepb.CountResponse);
103
+ */
104
+ count(input: CountRequest, options?: RpcOptions): UnaryCall<CountRequest, CountResponse>;
105
+ }
106
+ // metadata token -> session id, room id, user id, username
107
+
108
+ // open is used for presence
109
+
110
+ // change stream will return all keys in order of oldest to newsest.
111
+
112
+ // max number of keys -> 5000
113
+ // max size per key -> 10240 Bytes
114
+
115
+ /**
116
+ * @generated from protobuf service sessionstorepb.Store
117
+ */
118
+ export class StoreClient implements IStoreClient, ServiceInfo {
119
+ typeName = Store.typeName;
120
+ methods = Store.methods;
121
+ options = Store.options;
122
+ constructor(private readonly _transport: RpcTransport) {}
123
+ /**
124
+ * open - start listening to updates in keys with provided match patterns
125
+ * provide change_id as last received ID to resume updates
126
+ *
127
+ * @generated from protobuf rpc: open(sessionstorepb.OpenRequest) returns (stream sessionstorepb.ChangeStream);
128
+ */
129
+ open(input: OpenRequest, options?: RpcOptions): ServerStreamingCall<OpenRequest, ChangeStream> {
130
+ const method = this.methods[0],
131
+ opt = this._transport.mergeOptions(options);
132
+ return stackIntercept<OpenRequest, ChangeStream>('serverStreaming', this._transport, method, opt, input);
133
+ }
134
+ /**
135
+ * get last stored value in given key
136
+ *
137
+ * @generated from protobuf rpc: get(sessionstorepb.GetRequest) returns (sessionstorepb.GetResponse);
138
+ */
139
+ get(input: GetRequest, options?: RpcOptions): UnaryCall<GetRequest, GetResponse> {
140
+ const method = this.methods[1],
141
+ opt = this._transport.mergeOptions(options);
142
+ return stackIntercept<GetRequest, GetResponse>('unary', this._transport, method, opt, input);
143
+ }
144
+ /**
145
+ * set key value
146
+ *
147
+ * @generated from protobuf rpc: set(sessionstorepb.SetRequest) returns (sessionstorepb.SetResponse);
148
+ */
149
+ set(input: SetRequest, options?: RpcOptions): UnaryCall<SetRequest, SetResponse> {
150
+ const method = this.methods[2],
151
+ opt = this._transport.mergeOptions(options);
152
+ return stackIntercept<SetRequest, SetResponse>('unary', this._transport, method, opt, input);
153
+ }
154
+ /**
155
+ * delete key from store
156
+ *
157
+ * @generated from protobuf rpc: delete(sessionstorepb.DeleteRequest) returns (sessionstorepb.DeleteResponse);
158
+ */
159
+ delete(input: DeleteRequest, options?: RpcOptions): UnaryCall<DeleteRequest, DeleteResponse> {
160
+ const method = this.methods[3],
161
+ opt = this._transport.mergeOptions(options);
162
+ return stackIntercept<DeleteRequest, DeleteResponse>('unary', this._transport, method, opt, input);
163
+ }
164
+ /**
165
+ * count get count of keys
166
+ *
167
+ * @generated from protobuf rpc: count(sessionstorepb.CountRequest) returns (sessionstorepb.CountResponse);
168
+ */
169
+ count(input: CountRequest, options?: RpcOptions): UnaryCall<CountRequest, CountResponse> {
170
+ const method = this.methods[4],
171
+ opt = this._transport.mergeOptions(options);
172
+ return stackIntercept<CountRequest, CountResponse>('unary', this._transport, method, opt, input);
173
+ }
174
+ }