@microsoft/live-share-react 1.3.0-preview.0 → 1.3.1-preview.0

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.
Files changed (38) hide show
  1. package/.nycrc.json +9 -9
  2. package/LICENSE +86 -86
  3. package/README.md +608 -608
  4. package/package.json +50 -50
  5. package/src/index.ts +6 -6
  6. package/src/interfaces/LiveInterfaceExtensions.ts +26 -26
  7. package/src/interfaces/index.ts +1 -1
  8. package/src/internal/errors.ts +57 -57
  9. package/src/internal/index.ts +2 -2
  10. package/src/internal/type-guards.ts +9 -9
  11. package/src/live-hooks/index.ts +7 -7
  12. package/src/live-hooks/useLiveCanvas.ts +191 -191
  13. package/src/live-hooks/useLiveEvent.ts +140 -140
  14. package/src/live-hooks/useLiveFollowMode.ts +316 -316
  15. package/src/live-hooks/useLivePresence.ts +147 -147
  16. package/src/live-hooks/useLiveState.ts +110 -110
  17. package/src/live-hooks/useLiveTimer.ts +213 -213
  18. package/src/live-hooks/useMediaSynchronizer.ts +295 -295
  19. package/src/providers/AzureProvider.tsx +261 -261
  20. package/src/providers/LiveShareProvider.tsx +210 -210
  21. package/src/providers/index.ts +2 -2
  22. package/src/shared-hooks/index.ts +4 -4
  23. package/src/shared-hooks/useDynamicDDS.ts +82 -82
  24. package/src/shared-hooks/useSharedMap.ts +159 -159
  25. package/src/shared-hooks/useSharedState.ts +96 -96
  26. package/src/shared-hooks/useSharedStateRegistry.ts +170 -170
  27. package/src/types/ActionTypes.ts +161 -161
  28. package/src/types/PropTypes.ts +10 -10
  29. package/src/types/ResultTypes.ts +332 -332
  30. package/src/types/index.ts +3 -3
  31. package/src/utils/index.ts +1 -1
  32. package/src/utils/type-check.ts +69 -69
  33. package/tsconfig.build.json +5 -5
  34. package/tsconfig.json +72 -72
  35. package/bin/internal/useSharedStateRegistry.d.ts +0 -31
  36. package/bin/internal/useSharedStateRegistry.d.ts.map +0 -1
  37. package/bin/internal/useSharedStateRegistry.js +0 -102
  38. package/bin/internal/useSharedStateRegistry.js.map +0 -1
package/README.md CHANGED
@@ -1,608 +1,608 @@
1
- # Live Share React
2
-
3
- **DISCLAIMER:** This package is in preview and experimental. We are not yet committed to maintaining this package and may make breaking changes at any time. Read this package's [license](./LICENSE) for more information.
4
-
5
- Live Share React is designed to make building collaborative apps in React simple and intuitive, all using familiar patterns from functional React.
6
-
7
- Here is a simple example of how to get started:
8
-
9
- ```javascript
10
- // LiveShareApp.jsx
11
- import { LiveShareProvider } from "@microsoft/live-share-react";
12
- import { LiveShareHost } from "@microsoft/teams-js";
13
-
14
- const host = LiveShareHost.create();
15
-
16
- export function LiveShareApp() {
17
- // Call app.initialize() from teams-js before rendering LiveShareProvider
18
-
19
- return (
20
- <LiveShareProvider
21
- joinOnLoad={true}
22
- host={host}
23
- >
24
- <SharedCheckbox />
25
- </LiveShareProvider>
26
- );
27
- }
28
-
29
- // SharedCheckbox.jsx
30
- import { useSharedState } from "@microsoft/live-share-react";
31
-
32
- export function SharedCheckbox() {
33
- const [checked, setChecked] = useSharedState("MY-UNIQUE-ID", false);
34
- return (
35
- <input
36
- type="checkbox"
37
- checked={checked}
38
- onChange={() => {
39
- setChecked(!checked);
40
- }}
41
- />
42
- );
43
- }
44
- ```
45
-
46
- ## Getting started
47
-
48
- ## Installing
49
-
50
- To add the latest version of the SDK to your application using NPM:
51
-
52
- ```bash
53
- npm install fluid-framework @fluidframework/azure-client @microsoft/live-share @microsoft/live-share-media @microsoft/live-share-canvas @microsoft/live-share-react --save
54
- ```
55
-
56
- or using [Yarn](https://yarnpkg.com/):
57
-
58
- ```bash
59
- yarn add fluid-framework @fluidframework/azure-client @microsoft/live-share @microsoft/live-share-media @microsoft/live-share-canvas @microsoft/live-share-react
60
- ```
61
-
62
- ### Building package
63
-
64
- After cloning the [GitHub repository](https://www.github.com/microsoft/live-share-sdk), navigate to the root folder and perform:
65
-
66
- ```bash
67
- npm install
68
- npm run build
69
- ```
70
-
71
- This will use npm workspaces to hoist and build all dependencies.
72
-
73
- ### Sample app
74
-
75
- After installing/building the packages, you can also try out a [working sample here](../../samples/04.live-share-react/).
76
-
77
- ## Live Share React vs. Vanilla Fluid
78
-
79
- Fluid Framework and Live Share are powerful frameworks that can greatly simplify the effort of building collaborative applications, but for many React applications, it can take time to get used to. This experimental package aims to strip away as much of that learning curve as possible.
80
-
81
- Where traditional Fluid utilizes a developer-defined object schema with a hierarchal structure, this package abstracts that out on your behalf. While you can still tap into more complicated hierarchies with this library, it is designed to behave more like blob storage / NoSQL.
82
-
83
- To use Fluid's distributed-data structures (DDS) or Live Share's live objects, you can simply use the corresponding React hook, providing a unique identifier for that DDS. If no DDS exists when the component is first mounted, we automatically create one for you. Otherwise, we will connect to the existing one.
84
-
85
- Much like React itself, this package is opinionated, and it may not be for everyone. To learn more about using Live Share the traditional way, see our [Live Share README](../live-share/README.md).
86
-
87
- ## Types of hooks
88
-
89
- Live Share supports all of the live data structures provided through Live Share, and most of the officially supported DDS's available through Fluid Framework. If you have custom data objects, this package also exposes some underlying APIs for building your own custom React hooks.
90
-
91
- Here are the hooks provided by this library:
92
-
93
- ### useSharedState
94
-
95
- Inspired by React's own `useState` hook, `useSharedState` should feel familiar to React developers. Under the hood, this hook uses a Fluid `SharedMap` dedicated for `useSharedState`, listening for changes to the key provided and automatically updating the shared state with any changes. And yes, if two components in the same application use the same key, those components will be in sync with each other!
96
-
97
- Since these keys are a `string` you provide while calling `useSharedState`, you can dynamically load these into your app as needed. With this in mind, we've also provided an optional `disposeState` action if the state is no longer relevant to your app. If you don't dispose it, then the data will persist in the container should you access it later (up to the lifetime of the Fluid container).
98
-
99
- The following example shows how `useSharedState` can be used to dynamically create collaborative features in your app on the fly:
100
-
101
- ```javascript
102
- import { useSharedState } from "@microsoft/live-share-react";
103
-
104
- export function CounterCard({ card, onDelete }) {
105
- const [count, setCount, disposeCount] = useSharedState(
106
- `card-count:${card.id}`,
107
- 0
108
- );
109
- return (
110
- <div className="card">
111
- <h3>{card.title}</h3>
112
- <button
113
- onClick={() => {
114
- setCount(count + 1);
115
- }}
116
- >
117
- {"+1"}
118
- </button>
119
- <span>{`${count} `}</span>
120
- <button
121
- onClick={() => {
122
- onDelete(card.id);
123
- disposeCount();
124
- }}
125
- >
126
- {"Delete"}
127
- </button>
128
- </div>
129
- );
130
- }
131
- ```
132
-
133
- _NOTE:_ While this hook will get you pretty far on its own, carefully consider which of our React hook is best for your scenario.
134
-
135
- ### useSharedMap
136
-
137
- This hook loads a Fluid `SharedMap` corresponding with the key you provide. Compared to `useSharedState`, this hook allows you to render a collection of items with less risk of conflict when multiple users may be writing to the same object simultaneously. For example, rather than storing an entire list of objects in a single key of `useSharedState`, the map allows each item in the list to have its own unique key.
138
-
139
- While you write to keys individually, the `map` object exposed through the hook uses React state itself. Lets see an example in action:
140
-
141
- ```javascript
142
- import { useSharedMap } from "@microsoft/live-share-react";
143
- import { v4 as uuid } from "uuid";
144
-
145
- export function CardList() {
146
- const { map, setEntry, deleteEntry } = useSharedMap("CUSTOM-MAP-ID");
147
- return (
148
- <div>
149
- <h2>{"Cards"}</h2>
150
- <button
151
- onClick={() => {
152
- const id = uuid();
153
- setEntry(id, {
154
- id,
155
- title: "Custom Card",
156
- });
157
- }}
158
- >
159
- {"+ Add card"}
160
- </button>
161
- <div className="flex wrap row hAlign">
162
- {[...map.values()].map((cardValue) => (
163
- <CounterCard
164
- key={cardValue.id}
165
- card={cardValue}
166
- onDelete={deleteEntry}
167
- />
168
- ))}
169
- </div>
170
- </div>
171
- );
172
- }
173
- ```
174
-
175
- ### useLivePresence
176
-
177
- Presence makes it easy to track which users are currently in the session and assign custom data to them.
178
-
179
- ```javascript
180
- import { useLivePresence } from "@microsoft/live-share-react";
181
- import { PresenceState } from "@microsoft/live-share";
182
-
183
- export function OnlineUsers() {
184
- const { localUser, allUsers, updatePresence } = useLivePresence(
185
- "UNIQUE-PRESENCE-KEY", // required unique key for presence
186
- { favoriteColor: "red" } // optional custom data object
187
- );
188
- return (
189
- <div>
190
- <h1>{"Online Users:"}</h1>
191
- <div>
192
- {allUsers
193
- .filter((user) => user.state === PresenceState.online)
194
- .map((user) => (
195
- <div key={user.userId}>
196
- {user.displayName + " " + user.data!.favoriteColor}
197
- </div>
198
- ))}
199
- </div>
200
- <button
201
- onClick={() => {
202
- updatePresence(
203
- localUser.data,
204
- localUser?.state === PresenceState.offline
205
- ? PresenceState.online
206
- : PresenceState.offline,
207
- );
208
- }}
209
- >
210
- {`Toggle status`}
211
- </button>
212
- </div>
213
- );
214
- }
215
- ```
216
-
217
- ### useLiveState
218
-
219
- Unlike `useSharedState`, `useLiveState` is only stateful while one or more users are connected to it. This can make it easy to have state that behaves more closely to a regular React `useState`, when desireable.
220
-
221
- This component also features role verification, which allows you to choose the Teams meeting roles which are eligible to edit the state, if needed.
222
-
223
- ```javascript
224
- import { useLiveState } from "@microsoft/live-share-react";
225
- import { UserMeetingRole } from "@microsoft/live-share";
226
-
227
- const ALLOWED_ROLES = [UserMeetingRole.organizer, UserMeetingRole.presenter ];
228
-
229
- export function AppState() {
230
- const [state, setState] = useLiveState("CUSTOM-STATE-ID", ExampleAppState.WAITING, ALLOWED_ROLES);
231
-
232
- if (state === ExampleAppState.WAITING) {
233
- return (
234
- <div>
235
- <h2>{"Waiting"}</h2>
236
- <button
237
- onClick={() => {
238
- setState(ExampleAppState.START, {
239
- startedBy: "First Last",
240
- });
241
- }}
242
- >
243
- {"Start"}
244
- </button>
245
- </div>
246
- );
247
- }
248
- return (
249
- <div>
250
- <h2>{`Started by: ${data!.startedBy}`}</h2>
251
- <button
252
- onClick={() => {
253
- setState(ExampleAppState.WAITING, undefined);
254
- }}
255
- >
256
- {"End"}
257
- </button>
258
- </div>
259
- );
260
- };
261
- ```
262
-
263
- ### useLiveEvent
264
-
265
- If you want to generic JSON between clients that is completely transient, then `useLiveEvent` is a great choice. A good example of that is a reactions feature similar to that in Microsoft Teams meetings, since people joining a meeting late don't need to see reactions from earlier in the meeting.
266
-
267
- Here is a simple example:
268
-
269
- ```javascript
270
- import { useLiveEvent } from "@microsoft/live-share-react";
271
-
272
- export function Reactions() {
273
- const { latestEvent, sendEvent } = useLiveEvent("EVENT-ID");
274
-
275
- return (
276
- <div>
277
- {/* Buttons for sending reactions */}
278
- <button
279
- onClick={() => {
280
- sendEvent({ emoji: "❤️" });
281
- }}
282
- >
283
- {"❤️"}
284
- </button>
285
- <button
286
- onClick={() => {
287
- sendEvent({ emoji: "😂" });
288
- }}
289
- >
290
- {"😂"}
291
- </button>
292
- {/* Show latest reaction */}
293
- {latestEvent?.local === false && (
294
- <div>{`Received: ${latestEvent?.value.emoji}`}</div>
295
- )}
296
- {latestEvent?.local === true && (
297
- <div>{`Sent: ${latestEvent?.value.emoji}`}</div>
298
- )}
299
- </div>
300
- );
301
- }
302
- ```
303
-
304
- ### useLiveTimer
305
-
306
- You can use the `useLiveTimer` hook to build a synchronized countdown timer. A good example of that might be a meditation timer or a countdown for a round in a group activity.
307
-
308
- Here is a simple example:
309
-
310
- ```javascript
311
- import { useLiveTimer } from "@microsoft/live-share-react";
312
-
313
- export function CountdownTimer() {
314
- const { milliRemaining, timerConfig, start, pause, play } = useLiveTimer("TIMER-ID");
315
-
316
- return (
317
- <div>
318
- <button
319
- onClick={() => {
320
- start(60 * 1000);
321
- }}
322
- >
323
- { timerConfig === undefined ? "Start" : "Reset" }
324
- </button>
325
- { timerConfig !== undefined && (
326
- <button
327
- onClick={() => {
328
- if (timerConfig.running) {
329
- pause();
330
- } else {
331
- play();
332
- }
333
- }}
334
- >
335
- {timerConfig.running ? "Pause" : "Play" }
336
- </button>
337
- )}
338
- { milliRemaining !== undefined && (
339
- <p>
340
- { `${Math.round(milliRemaining / 1000)} / ${Math.round(timerConfig.duration) / 1000}` }
341
- </p>
342
- )}
343
- </div>
344
- );
345
- }
346
- ```
347
-
348
- ### useMediaSynchronizer
349
-
350
- If you want to synchronize video content, `@microsoft/live-share-media` is also supported by this package through the `useMediaSynchronizer` hook. Using any `HTMLMediaPlayer` element, or a delegate object matching our `IMediaPlayer` interface, you can easily build watch together capabilities into your app.
351
-
352
- Let's see this in action:
353
-
354
- ```javascript
355
- import { useMediaSynchronizer } from "@microsoft/live-share-react";
356
- import { UserMeetingRole } from "@microsoft/live-share";
357
- import { useRef } from "react";
358
-
359
- const ALLOWED_ROLES = [UserMeetingRole.organizer, UserMeetingRole.presenter];
360
-
361
- const INITIAL_TRACK =
362
- "https://storage.googleapis.com/media-session/big-buck-bunny/trailer.mov";
363
-
364
- export function VideoPlayer() {
365
- const videoRef = useRef(null);
366
- const { play, pause } = useMediaSynchronizer(
367
- "MEDIA-SESSION-ID",
368
- videoRef,
369
- INITIAL_TRACK,
370
- ALLOWED_ROLES
371
- );
372
-
373
- return (
374
- <div>
375
- <video ref={videoRef} />
376
- <button onClick={play}>{"Play"}</button>
377
- <button onClick={pause}>{"Pause"}</button>
378
- </div>
379
- );
380
- }
381
- ```
382
-
383
- ### useLiveCanvas
384
-
385
- If you want to add turn-key inking & cursors, use the `useLiveCanvas` hook, powered by `@microsoft/live-share-canvas`.
386
-
387
- Let's see this in action:
388
-
389
- ```javascript
390
- import { useLiveCanvas } from "@microsoft/live-share-react";
391
- import { InkingTool } from "@microsoft/live-share-canvas";
392
- import { useRef } from "react";
393
-
394
- export const ExampleLiveCanvas = () => {
395
- const liveCanvasRef = useRef(null);
396
- const { liveCanvas, inkingManager } = useLiveCanvas(
397
- "CUSTOM-LIVE-CANVAS",
398
- liveCanvasRef,
399
- );
400
-
401
- return (
402
- {/** Canvas currently needs to be a child of a parent with absolute styling */}
403
- <div style={{ position: "absolute"}}>
404
- <div
405
- ref={liveCanvasRef}
406
- // Best practice is to not define inline styles
407
- style={{ width: "556px", height: "224px" }}
408
- />
409
- {!!liveCanvas && (
410
- <div>
411
- <button
412
- onClick={() => {
413
- inkingManager.tool = InkingTool.pen;
414
- }}
415
- >
416
- {"Pen"}
417
- </button>
418
- <button
419
- onClick={() => {
420
- inkingManager.tool = InkingTool.laserPointer;
421
- }}
422
- >
423
- {"Laser pointer"}
424
- </button>
425
- </div>
426
- )}
427
- </div>
428
- );
429
- };
430
- ```
431
-
432
- ### useTaskManager
433
-
434
- If you want to ensure that only one user is responsible for a given task, you can use `useTaskManager`, which uses Fluid's `TaskManager` DDS.
435
-
436
- Let's see this in action:
437
-
438
- ```javascript
439
- import { useTaskManager } from "@microsoft/live-share-react";
440
-
441
- export const ExampleTaskManager = () => {
442
- const [taskId, setTaskId] = useState(undefined);
443
- const { lockedTask } = useTaskManager(
444
- "CUSTOM-TASK-MANAGER",
445
- taskId,
446
- );
447
-
448
- const displayText = lockedTask
449
- ? "You are assigned the task"
450
- : "Waiting for task assignment";
451
-
452
- return (
453
- <div>
454
- {!taskId && (
455
- <button
456
- onClick={() => {
457
- setTaskId("task-id")
458
- }}
459
- >
460
- {'Join task queue'}
461
- </button>
462
- )}
463
- {taskId && (
464
- <button
465
- onClick={() => {
466
- setTaskId(undefined)
467
- }}
468
- >
469
- {'Leave task queue'}
470
- </button>
471
- )}
472
- <p>{displayText}</p>
473
- </div>
474
- );
475
- };
476
- ```
477
-
478
- ### Custom Fluid object hooks
479
-
480
- If you want to dynamically load a custom Fluid object in your app, use the `useDynamicDDS` to create a custom hook. This is the same hook that Live Share React uses internally within our custom hooks, such as `useLiveEvent`. If you made a custom data object or are using one of Fluid's experimental data structures, you also must register your Fluid `LoadableObjectClass` with `DynamicObjectRegistry.registerObjectClass` to `@microsoft/live-share`, if it is not already.
481
-
482
- Implementations may vary for each dynamic object & hook. We will try and update this package periodically with new packages released by Fluid Framework and Live Share, as they are published.
483
-
484
- Example:
485
-
486
- ```typescript
487
- import React from "react";
488
- import { useDynamicDDS } from "@microsoft/live-share-react";
489
- import { DynamicObjectRegistry } from "@microsoft/live-share";
490
- import { TaskManager } from "@fluid-experimental/task-manager";
491
-
492
- // Register TaskManager as dynamic object
493
- DynamicObjectRegistry.registerObjectClass(TaskManager, TaskManager.getFactory().type);
494
-
495
- /**
496
- * A hook for joining a queue to lock tasks for a given id. Guaranteed to have only one user assigned to a task at a time.
497
- *
498
- * @param uniqueKey the unique key for the TaskManager DDS
499
- * @param taskId the task id to lock
500
- * @returns stateful data about the status of the task lock
501
- */
502
- export const useTaskManager = (uniqueKey: string, taskId?: string): {
503
- lockedTask: boolean;
504
- taskManager: TaskManager | undefined;
505
- } => {
506
- /**
507
- * TaskId currently in queue for
508
- */
509
- const currentTaskIdRef = React.useRef<string | undefined>(undefined);
510
- /**
511
- * Stateful boolean that is true when the user is currently assigned the task
512
- */
513
- const [lockedTask, setLockedTask] = React.useState<boolean>(false);
514
-
515
- /**
516
- * User facing: dynamically load the TaskManager DDS for the given unique key.
517
- */
518
- const { dds: taskManager } = useDynamicDDS<TaskManager>(uniqueKey, TaskManager);
519
-
520
- /**
521
- * When the task id changes, lock the task. When the task id is undefined, abandon the task.
522
- */
523
- React.useEffect(() => {
524
- let mounted = true;
525
- if (taskManager) {
526
- if (taskId && currentTaskIdRef.current !== taskId) {
527
- if (currentTaskIdRef.current) {
528
- taskManager.abandon(currentTaskIdRef.current);
529
- setLockedTask(false);
530
- }
531
- currentTaskIdRef.current = taskId;
532
- const onLockTask = async () => {
533
- try {
534
- await taskManager.lockTask(taskId);
535
- if (mounted) {
536
- setLockedTask(true);
537
- }
538
- } catch {
539
- if (mounted) {
540
- setLockedTask(false);
541
- currentTaskIdRef.current = undefined;
542
- }
543
- }
544
- }
545
- onLockTask();
546
- } else if (!taskId && currentTaskIdRef.current) {
547
- taskManager.abandon(currentTaskIdRef.current);
548
- setLockedTask(false);
549
- currentTaskIdRef.current = undefined;
550
- }
551
- }
552
- /**
553
- * When the component unmounts, abandon the task if it is still locked
554
- */
555
- return () => {
556
- mounted = false;
557
- if (currentTaskIdRef.current) {
558
- taskManager?.abandon(currentTaskIdRef.current);
559
- }
560
- currentTaskIdRef.current = undefined;
561
- }
562
- }, [taskManager, taskId]);
563
-
564
- return {
565
- lockedTask,
566
- taskManager,
567
- };
568
- };
569
- ```
570
-
571
- ## Code samples
572
-
573
- | Sample name | Description | Javascript |
574
- | ----------------- | ----------------------------------------------------------------------- | ----------------------------------------------------- |
575
- | Live Share React | Simple example with each of our custom Live Share React hooks. | [View](../../samples/javascript/04.live-share-react) |
576
-
577
- ## React version compatibility
578
-
579
- This package is compatible with React versions `^16.8.0` and greater, including React v18. In order to ensure compatibility with different versions React, this project does not currently use React Suspense for data fetching on load. We are closely monitoring React guidelines and may post updates as this evolves further. If you have feedback or thoughts on this topic, join the [discussion](https://github.com/microsoft/live-share-sdk/discussions/442).
580
-
581
- ## Package Compatibility
582
-
583
- The Live Share SDK contains dependencies for [@microsoft/teams-js](https://www.npmjs.com/package/@microsoft/teams-js) and [fluid-framework](https://www.npmjs.com/package/fluid-framework) packages among others. Both of these packages are sensitive to the package version your app any libraries use. You will likely run into issues if the package version your app uses doesn't match the version other libraries you depend on use.
584
-
585
- **It is critical that your app use the package dependencies listed in the table below.** Lookup the version of the `@microsoft/live-share` you're using and set any other dependencies in your package.json file to match:
586
-
587
- | @microsoft/live-share | @microsoft/teams-js | fluid-framework | @microsoft/live-share-\* | @fluidframework/azure-client | @microsoft/TeamsFx | @microsoft/TeamsFx-react |
588
- | --------------------- | -------------------- | --------------- | -------------------------- | ---------------------------- | ------------------------------- | ------------------------------- |
589
- | ^1.0.0 | ^2.11.0 | ^1.2.3 | ^1.0.0 | ^1.0.0 | ^2.5.0 | ^2.5.0 |
590
-
591
- ## Contributing
592
-
593
- There are several ways you can [contribute](../../CONTRIBUTING.md) to this project:
594
-
595
- - [Submit bugs](https://github.com/microsoft/live-share-sdk/issues) and help us verify fixes as they are checked in.
596
- - Review the source code changes.
597
- - Engage with other Live Share developers on [StackOverflow](https://stackoverflow.com/questions/tagged/live-share).
598
- - [Contribute bug fixes](../../CONTRIBUTING.md).
599
-
600
- This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact <opencode@microsoft.com> with any additional questions or comments.
601
-
602
- ## Reporting Security Issues
603
-
604
- Security issues and bugs should be reported privately, via email, to the Microsoft Security Response Center (MSRC) at <secure@microsoft.com>. You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Further information, including the [MSRC PGP](https://technet.microsoft.com/security/dn606155) key, can be found in the [Security TechCenter](https://technet.microsoft.com/security/default).
605
-
606
- Copyright (c) Microsoft Corporation. All rights reserved.
607
-
608
- Licensed under a special [Microsoft](./LICENSE) License.
1
+ # Live Share React
2
+
3
+ **DISCLAIMER:** This package is in preview and experimental. We are not yet committed to maintaining this package and may make breaking changes at any time. Read this package's [license](./LICENSE) for more information.
4
+
5
+ Live Share React is designed to make building collaborative apps in React simple and intuitive, all using familiar patterns from functional React.
6
+
7
+ Here is a simple example of how to get started:
8
+
9
+ ```javascript
10
+ // LiveShareApp.jsx
11
+ import { LiveShareProvider } from "@microsoft/live-share-react";
12
+ import { LiveShareHost } from "@microsoft/teams-js";
13
+
14
+ const host = LiveShareHost.create();
15
+
16
+ export function LiveShareApp() {
17
+ // Call app.initialize() from teams-js before rendering LiveShareProvider
18
+
19
+ return (
20
+ <LiveShareProvider
21
+ joinOnLoad={true}
22
+ host={host}
23
+ >
24
+ <SharedCheckbox />
25
+ </LiveShareProvider>
26
+ );
27
+ }
28
+
29
+ // SharedCheckbox.jsx
30
+ import { useSharedState } from "@microsoft/live-share-react";
31
+
32
+ export function SharedCheckbox() {
33
+ const [checked, setChecked] = useSharedState("MY-UNIQUE-ID", false);
34
+ return (
35
+ <input
36
+ type="checkbox"
37
+ checked={checked}
38
+ onChange={() => {
39
+ setChecked(!checked);
40
+ }}
41
+ />
42
+ );
43
+ }
44
+ ```
45
+
46
+ ## Getting started
47
+
48
+ ## Installing
49
+
50
+ To add the latest version of the SDK to your application using NPM:
51
+
52
+ ```bash
53
+ npm install fluid-framework @fluidframework/azure-client @microsoft/live-share @microsoft/live-share-media @microsoft/live-share-canvas @microsoft/live-share-react --save
54
+ ```
55
+
56
+ or using [Yarn](https://yarnpkg.com/):
57
+
58
+ ```bash
59
+ yarn add fluid-framework @fluidframework/azure-client @microsoft/live-share @microsoft/live-share-media @microsoft/live-share-canvas @microsoft/live-share-react
60
+ ```
61
+
62
+ ### Building package
63
+
64
+ After cloning the [GitHub repository](https://www.github.com/microsoft/live-share-sdk), navigate to the root folder and perform:
65
+
66
+ ```bash
67
+ npm install
68
+ npm run build
69
+ ```
70
+
71
+ This will use npm workspaces to hoist and build all dependencies.
72
+
73
+ ### Sample app
74
+
75
+ After installing/building the packages, you can also try out a [working sample here](../../samples/04.live-share-react/).
76
+
77
+ ## Live Share React vs. Vanilla Fluid
78
+
79
+ Fluid Framework and Live Share are powerful frameworks that can greatly simplify the effort of building collaborative applications, but for many React applications, it can take time to get used to. This experimental package aims to strip away as much of that learning curve as possible.
80
+
81
+ Where traditional Fluid utilizes a developer-defined object schema with a hierarchal structure, this package abstracts that out on your behalf. While you can still tap into more complicated hierarchies with this library, it is designed to behave more like blob storage / NoSQL.
82
+
83
+ To use Fluid's distributed-data structures (DDS) or Live Share's live objects, you can simply use the corresponding React hook, providing a unique identifier for that DDS. If no DDS exists when the component is first mounted, we automatically create one for you. Otherwise, we will connect to the existing one.
84
+
85
+ Much like React itself, this package is opinionated, and it may not be for everyone. To learn more about using Live Share the traditional way, see our [Live Share README](../live-share/README.md).
86
+
87
+ ## Types of hooks
88
+
89
+ Live Share supports all of the live data structures provided through Live Share, and most of the officially supported DDS's available through Fluid Framework. If you have custom data objects, this package also exposes some underlying APIs for building your own custom React hooks.
90
+
91
+ Here are the hooks provided by this library:
92
+
93
+ ### useSharedState
94
+
95
+ Inspired by React's own `useState` hook, `useSharedState` should feel familiar to React developers. Under the hood, this hook uses a Fluid `SharedMap` dedicated for `useSharedState`, listening for changes to the key provided and automatically updating the shared state with any changes. And yes, if two components in the same application use the same key, those components will be in sync with each other!
96
+
97
+ Since these keys are a `string` you provide while calling `useSharedState`, you can dynamically load these into your app as needed. With this in mind, we've also provided an optional `disposeState` action if the state is no longer relevant to your app. If you don't dispose it, then the data will persist in the container should you access it later (up to the lifetime of the Fluid container).
98
+
99
+ The following example shows how `useSharedState` can be used to dynamically create collaborative features in your app on the fly:
100
+
101
+ ```javascript
102
+ import { useSharedState } from "@microsoft/live-share-react";
103
+
104
+ export function CounterCard({ card, onDelete }) {
105
+ const [count, setCount, disposeCount] = useSharedState(
106
+ `card-count:${card.id}`,
107
+ 0
108
+ );
109
+ return (
110
+ <div className="card">
111
+ <h3>{card.title}</h3>
112
+ <button
113
+ onClick={() => {
114
+ setCount(count + 1);
115
+ }}
116
+ >
117
+ {"+1"}
118
+ </button>
119
+ <span>{`${count} `}</span>
120
+ <button
121
+ onClick={() => {
122
+ onDelete(card.id);
123
+ disposeCount();
124
+ }}
125
+ >
126
+ {"Delete"}
127
+ </button>
128
+ </div>
129
+ );
130
+ }
131
+ ```
132
+
133
+ _NOTE:_ While this hook will get you pretty far on its own, carefully consider which of our React hook is best for your scenario.
134
+
135
+ ### useSharedMap
136
+
137
+ This hook loads a Fluid `SharedMap` corresponding with the key you provide. Compared to `useSharedState`, this hook allows you to render a collection of items with less risk of conflict when multiple users may be writing to the same object simultaneously. For example, rather than storing an entire list of objects in a single key of `useSharedState`, the map allows each item in the list to have its own unique key.
138
+
139
+ While you write to keys individually, the `map` object exposed through the hook uses React state itself. Lets see an example in action:
140
+
141
+ ```javascript
142
+ import { useSharedMap } from "@microsoft/live-share-react";
143
+ import { v4 as uuid } from "uuid";
144
+
145
+ export function CardList() {
146
+ const { map, setEntry, deleteEntry } = useSharedMap("CUSTOM-MAP-ID");
147
+ return (
148
+ <div>
149
+ <h2>{"Cards"}</h2>
150
+ <button
151
+ onClick={() => {
152
+ const id = uuid();
153
+ setEntry(id, {
154
+ id,
155
+ title: "Custom Card",
156
+ });
157
+ }}
158
+ >
159
+ {"+ Add card"}
160
+ </button>
161
+ <div className="flex wrap row hAlign">
162
+ {[...map.values()].map((cardValue) => (
163
+ <CounterCard
164
+ key={cardValue.id}
165
+ card={cardValue}
166
+ onDelete={deleteEntry}
167
+ />
168
+ ))}
169
+ </div>
170
+ </div>
171
+ );
172
+ }
173
+ ```
174
+
175
+ ### useLivePresence
176
+
177
+ Presence makes it easy to track which users are currently in the session and assign custom data to them.
178
+
179
+ ```javascript
180
+ import { useLivePresence } from "@microsoft/live-share-react";
181
+ import { PresenceState } from "@microsoft/live-share";
182
+
183
+ export function OnlineUsers() {
184
+ const { localUser, allUsers, updatePresence } = useLivePresence(
185
+ "UNIQUE-PRESENCE-KEY", // required unique key for presence
186
+ { favoriteColor: "red" } // optional custom data object
187
+ );
188
+ return (
189
+ <div>
190
+ <h1>{"Online Users:"}</h1>
191
+ <div>
192
+ {allUsers
193
+ .filter((user) => user.state === PresenceState.online)
194
+ .map((user) => (
195
+ <div key={user.userId}>
196
+ {user.displayName + " " + user.data!.favoriteColor}
197
+ </div>
198
+ ))}
199
+ </div>
200
+ <button
201
+ onClick={() => {
202
+ updatePresence(
203
+ localUser.data,
204
+ localUser?.state === PresenceState.offline
205
+ ? PresenceState.online
206
+ : PresenceState.offline,
207
+ );
208
+ }}
209
+ >
210
+ {`Toggle status`}
211
+ </button>
212
+ </div>
213
+ );
214
+ }
215
+ ```
216
+
217
+ ### useLiveState
218
+
219
+ Unlike `useSharedState`, `useLiveState` is only stateful while one or more users are connected to it. This can make it easy to have state that behaves more closely to a regular React `useState`, when desireable.
220
+
221
+ This component also features role verification, which allows you to choose the Teams meeting roles which are eligible to edit the state, if needed.
222
+
223
+ ```javascript
224
+ import { useLiveState } from "@microsoft/live-share-react";
225
+ import { UserMeetingRole } from "@microsoft/live-share";
226
+
227
+ const ALLOWED_ROLES = [UserMeetingRole.organizer, UserMeetingRole.presenter ];
228
+
229
+ export function AppState() {
230
+ const [state, setState] = useLiveState("CUSTOM-STATE-ID", ExampleAppState.WAITING, ALLOWED_ROLES);
231
+
232
+ if (state === ExampleAppState.WAITING) {
233
+ return (
234
+ <div>
235
+ <h2>{"Waiting"}</h2>
236
+ <button
237
+ onClick={() => {
238
+ setState(ExampleAppState.START, {
239
+ startedBy: "First Last",
240
+ });
241
+ }}
242
+ >
243
+ {"Start"}
244
+ </button>
245
+ </div>
246
+ );
247
+ }
248
+ return (
249
+ <div>
250
+ <h2>{`Started by: ${data!.startedBy}`}</h2>
251
+ <button
252
+ onClick={() => {
253
+ setState(ExampleAppState.WAITING, undefined);
254
+ }}
255
+ >
256
+ {"End"}
257
+ </button>
258
+ </div>
259
+ );
260
+ };
261
+ ```
262
+
263
+ ### useLiveEvent
264
+
265
+ If you want to generic JSON between clients that is completely transient, then `useLiveEvent` is a great choice. A good example of that is a reactions feature similar to that in Microsoft Teams meetings, since people joining a meeting late don't need to see reactions from earlier in the meeting.
266
+
267
+ Here is a simple example:
268
+
269
+ ```javascript
270
+ import { useLiveEvent } from "@microsoft/live-share-react";
271
+
272
+ export function Reactions() {
273
+ const { latestEvent, sendEvent } = useLiveEvent("EVENT-ID");
274
+
275
+ return (
276
+ <div>
277
+ {/* Buttons for sending reactions */}
278
+ <button
279
+ onClick={() => {
280
+ sendEvent({ emoji: "❤️" });
281
+ }}
282
+ >
283
+ {"❤️"}
284
+ </button>
285
+ <button
286
+ onClick={() => {
287
+ sendEvent({ emoji: "😂" });
288
+ }}
289
+ >
290
+ {"😂"}
291
+ </button>
292
+ {/* Show latest reaction */}
293
+ {latestEvent?.local === false && (
294
+ <div>{`Received: ${latestEvent?.value.emoji}`}</div>
295
+ )}
296
+ {latestEvent?.local === true && (
297
+ <div>{`Sent: ${latestEvent?.value.emoji}`}</div>
298
+ )}
299
+ </div>
300
+ );
301
+ }
302
+ ```
303
+
304
+ ### useLiveTimer
305
+
306
+ You can use the `useLiveTimer` hook to build a synchronized countdown timer. A good example of that might be a meditation timer or a countdown for a round in a group activity.
307
+
308
+ Here is a simple example:
309
+
310
+ ```javascript
311
+ import { useLiveTimer } from "@microsoft/live-share-react";
312
+
313
+ export function CountdownTimer() {
314
+ const { milliRemaining, timerConfig, start, pause, play } = useLiveTimer("TIMER-ID");
315
+
316
+ return (
317
+ <div>
318
+ <button
319
+ onClick={() => {
320
+ start(60 * 1000);
321
+ }}
322
+ >
323
+ { timerConfig === undefined ? "Start" : "Reset" }
324
+ </button>
325
+ { timerConfig !== undefined && (
326
+ <button
327
+ onClick={() => {
328
+ if (timerConfig.running) {
329
+ pause();
330
+ } else {
331
+ play();
332
+ }
333
+ }}
334
+ >
335
+ {timerConfig.running ? "Pause" : "Play" }
336
+ </button>
337
+ )}
338
+ { milliRemaining !== undefined && (
339
+ <p>
340
+ { `${Math.round(milliRemaining / 1000)} / ${Math.round(timerConfig.duration) / 1000}` }
341
+ </p>
342
+ )}
343
+ </div>
344
+ );
345
+ }
346
+ ```
347
+
348
+ ### useMediaSynchronizer
349
+
350
+ If you want to synchronize video content, `@microsoft/live-share-media` is also supported by this package through the `useMediaSynchronizer` hook. Using any `HTMLMediaPlayer` element, or a delegate object matching our `IMediaPlayer` interface, you can easily build watch together capabilities into your app.
351
+
352
+ Let's see this in action:
353
+
354
+ ```javascript
355
+ import { useMediaSynchronizer } from "@microsoft/live-share-react";
356
+ import { UserMeetingRole } from "@microsoft/live-share";
357
+ import { useRef } from "react";
358
+
359
+ const ALLOWED_ROLES = [UserMeetingRole.organizer, UserMeetingRole.presenter];
360
+
361
+ const INITIAL_TRACK =
362
+ "https://storage.googleapis.com/media-session/big-buck-bunny/trailer.mov";
363
+
364
+ export function VideoPlayer() {
365
+ const videoRef = useRef(null);
366
+ const { play, pause } = useMediaSynchronizer(
367
+ "MEDIA-SESSION-ID",
368
+ videoRef,
369
+ INITIAL_TRACK,
370
+ ALLOWED_ROLES
371
+ );
372
+
373
+ return (
374
+ <div>
375
+ <video ref={videoRef} />
376
+ <button onClick={play}>{"Play"}</button>
377
+ <button onClick={pause}>{"Pause"}</button>
378
+ </div>
379
+ );
380
+ }
381
+ ```
382
+
383
+ ### useLiveCanvas
384
+
385
+ If you want to add turn-key inking & cursors, use the `useLiveCanvas` hook, powered by `@microsoft/live-share-canvas`.
386
+
387
+ Let's see this in action:
388
+
389
+ ```javascript
390
+ import { useLiveCanvas } from "@microsoft/live-share-react";
391
+ import { InkingTool } from "@microsoft/live-share-canvas";
392
+ import { useRef } from "react";
393
+
394
+ export const ExampleLiveCanvas = () => {
395
+ const liveCanvasRef = useRef(null);
396
+ const { liveCanvas, inkingManager } = useLiveCanvas(
397
+ "CUSTOM-LIVE-CANVAS",
398
+ liveCanvasRef,
399
+ );
400
+
401
+ return (
402
+ {/** Canvas currently needs to be a child of a parent with absolute styling */}
403
+ <div style={{ position: "absolute"}}>
404
+ <div
405
+ ref={liveCanvasRef}
406
+ // Best practice is to not define inline styles
407
+ style={{ width: "556px", height: "224px" }}
408
+ />
409
+ {!!liveCanvas && (
410
+ <div>
411
+ <button
412
+ onClick={() => {
413
+ inkingManager.tool = InkingTool.pen;
414
+ }}
415
+ >
416
+ {"Pen"}
417
+ </button>
418
+ <button
419
+ onClick={() => {
420
+ inkingManager.tool = InkingTool.laserPointer;
421
+ }}
422
+ >
423
+ {"Laser pointer"}
424
+ </button>
425
+ </div>
426
+ )}
427
+ </div>
428
+ );
429
+ };
430
+ ```
431
+
432
+ ### useTaskManager
433
+
434
+ If you want to ensure that only one user is responsible for a given task, you can use `useTaskManager`, which uses Fluid's `TaskManager` DDS.
435
+
436
+ Let's see this in action:
437
+
438
+ ```javascript
439
+ import { useTaskManager } from "@microsoft/live-share-react";
440
+
441
+ export const ExampleTaskManager = () => {
442
+ const [taskId, setTaskId] = useState(undefined);
443
+ const { lockedTask } = useTaskManager(
444
+ "CUSTOM-TASK-MANAGER",
445
+ taskId,
446
+ );
447
+
448
+ const displayText = lockedTask
449
+ ? "You are assigned the task"
450
+ : "Waiting for task assignment";
451
+
452
+ return (
453
+ <div>
454
+ {!taskId && (
455
+ <button
456
+ onClick={() => {
457
+ setTaskId("task-id")
458
+ }}
459
+ >
460
+ {'Join task queue'}
461
+ </button>
462
+ )}
463
+ {taskId && (
464
+ <button
465
+ onClick={() => {
466
+ setTaskId(undefined)
467
+ }}
468
+ >
469
+ {'Leave task queue'}
470
+ </button>
471
+ )}
472
+ <p>{displayText}</p>
473
+ </div>
474
+ );
475
+ };
476
+ ```
477
+
478
+ ### Custom Fluid object hooks
479
+
480
+ If you want to dynamically load a custom Fluid object in your app, use the `useDynamicDDS` to create a custom hook. This is the same hook that Live Share React uses internally within our custom hooks, such as `useLiveEvent`. If you made a custom data object or are using one of Fluid's experimental data structures, you also must register your Fluid `LoadableObjectClass` with `DynamicObjectRegistry.registerObjectClass` to `@microsoft/live-share`, if it is not already.
481
+
482
+ Implementations may vary for each dynamic object & hook. We will try and update this package periodically with new packages released by Fluid Framework and Live Share, as they are published.
483
+
484
+ Example:
485
+
486
+ ```typescript
487
+ import React from "react";
488
+ import { useDynamicDDS } from "@microsoft/live-share-react";
489
+ import { DynamicObjectRegistry } from "@microsoft/live-share";
490
+ import { TaskManager } from "@fluid-experimental/task-manager";
491
+
492
+ // Register TaskManager as dynamic object
493
+ DynamicObjectRegistry.registerObjectClass(TaskManager, TaskManager.getFactory().type);
494
+
495
+ /**
496
+ * A hook for joining a queue to lock tasks for a given id. Guaranteed to have only one user assigned to a task at a time.
497
+ *
498
+ * @param uniqueKey the unique key for the TaskManager DDS
499
+ * @param taskId the task id to lock
500
+ * @returns stateful data about the status of the task lock
501
+ */
502
+ export const useTaskManager = (uniqueKey: string, taskId?: string): {
503
+ lockedTask: boolean;
504
+ taskManager: TaskManager | undefined;
505
+ } => {
506
+ /**
507
+ * TaskId currently in queue for
508
+ */
509
+ const currentTaskIdRef = React.useRef<string | undefined>(undefined);
510
+ /**
511
+ * Stateful boolean that is true when the user is currently assigned the task
512
+ */
513
+ const [lockedTask, setLockedTask] = React.useState<boolean>(false);
514
+
515
+ /**
516
+ * User facing: dynamically load the TaskManager DDS for the given unique key.
517
+ */
518
+ const { dds: taskManager } = useDynamicDDS<TaskManager>(uniqueKey, TaskManager);
519
+
520
+ /**
521
+ * When the task id changes, lock the task. When the task id is undefined, abandon the task.
522
+ */
523
+ React.useEffect(() => {
524
+ let mounted = true;
525
+ if (taskManager) {
526
+ if (taskId && currentTaskIdRef.current !== taskId) {
527
+ if (currentTaskIdRef.current) {
528
+ taskManager.abandon(currentTaskIdRef.current);
529
+ setLockedTask(false);
530
+ }
531
+ currentTaskIdRef.current = taskId;
532
+ const onLockTask = async () => {
533
+ try {
534
+ await taskManager.lockTask(taskId);
535
+ if (mounted) {
536
+ setLockedTask(true);
537
+ }
538
+ } catch {
539
+ if (mounted) {
540
+ setLockedTask(false);
541
+ currentTaskIdRef.current = undefined;
542
+ }
543
+ }
544
+ }
545
+ onLockTask();
546
+ } else if (!taskId && currentTaskIdRef.current) {
547
+ taskManager.abandon(currentTaskIdRef.current);
548
+ setLockedTask(false);
549
+ currentTaskIdRef.current = undefined;
550
+ }
551
+ }
552
+ /**
553
+ * When the component unmounts, abandon the task if it is still locked
554
+ */
555
+ return () => {
556
+ mounted = false;
557
+ if (currentTaskIdRef.current) {
558
+ taskManager?.abandon(currentTaskIdRef.current);
559
+ }
560
+ currentTaskIdRef.current = undefined;
561
+ }
562
+ }, [taskManager, taskId]);
563
+
564
+ return {
565
+ lockedTask,
566
+ taskManager,
567
+ };
568
+ };
569
+ ```
570
+
571
+ ## Code samples
572
+
573
+ | Sample name | Description | Javascript |
574
+ | ----------------- | ----------------------------------------------------------------------- | ----------------------------------------------------- |
575
+ | Live Share React | Simple example with each of our custom Live Share React hooks. | [View](../../samples/javascript/04.live-share-react) |
576
+
577
+ ## React version compatibility
578
+
579
+ This package is compatible with React versions `^16.8.0` and greater, including React v18. In order to ensure compatibility with different versions React, this project does not currently use React Suspense for data fetching on load. We are closely monitoring React guidelines and may post updates as this evolves further. If you have feedback or thoughts on this topic, join the [discussion](https://github.com/microsoft/live-share-sdk/discussions/442).
580
+
581
+ ## Package Compatibility
582
+
583
+ The Live Share SDK contains dependencies for [@microsoft/teams-js](https://www.npmjs.com/package/@microsoft/teams-js) and [fluid-framework](https://www.npmjs.com/package/fluid-framework) packages among others. Both of these packages are sensitive to the package version your app any libraries use. You will likely run into issues if the package version your app uses doesn't match the version other libraries you depend on use.
584
+
585
+ **It is critical that your app use the package dependencies listed in the table below.** Lookup the version of the `@microsoft/live-share` you're using and set any other dependencies in your package.json file to match:
586
+
587
+ | @microsoft/live-share | @microsoft/teams-js | fluid-framework | @microsoft/live-share-\* | @fluidframework/azure-client | @microsoft/TeamsFx | @microsoft/TeamsFx-react |
588
+ | --------------------- | -------------------- | --------------- | -------------------------- | ---------------------------- | ------------------------------- | ------------------------------- |
589
+ | ^1.0.0 | ^2.11.0 | ^1.2.3 | ^1.0.0 | ^1.0.0 | ^2.5.0 | ^2.5.0 |
590
+
591
+ ## Contributing
592
+
593
+ There are several ways you can [contribute](../../CONTRIBUTING.md) to this project:
594
+
595
+ - [Submit bugs](https://github.com/microsoft/live-share-sdk/issues) and help us verify fixes as they are checked in.
596
+ - Review the source code changes.
597
+ - Engage with other Live Share developers on [StackOverflow](https://stackoverflow.com/questions/tagged/live-share).
598
+ - [Contribute bug fixes](../../CONTRIBUTING.md).
599
+
600
+ This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact <opencode@microsoft.com> with any additional questions or comments.
601
+
602
+ ## Reporting Security Issues
603
+
604
+ Security issues and bugs should be reported privately, via email, to the Microsoft Security Response Center (MSRC) at <secure@microsoft.com>. You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Further information, including the [MSRC PGP](https://technet.microsoft.com/security/dn606155) key, can be found in the [Security TechCenter](https://technet.microsoft.com/security/default).
605
+
606
+ Copyright (c) Microsoft Corporation. All rights reserved.
607
+
608
+ Licensed under a special [Microsoft](./LICENSE) License.