vg-x07df 1.6.3 → 1.8.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.
- package/README.md +153 -0
- package/dist/channel/index.cjs +284 -15
- package/dist/channel/index.cjs.map +1 -1
- package/dist/channel/index.d.cts +75 -3
- package/dist/channel/index.d.ts +75 -3
- package/dist/channel/index.mjs +279 -16
- package/dist/channel/index.mjs.map +1 -1
- package/dist/duration-DvuGtU76.d.cts +39 -0
- package/dist/duration-DvuGtU76.d.ts +39 -0
- package/dist/index.cjs +126 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -3
- package/dist/index.d.ts +13 -3
- package/dist/index.mjs +128 -33
- package/dist/index.mjs.map +1 -1
- package/dist/livekit/index.cjs +4 -0
- package/dist/livekit/index.d.cts +1 -1
- package/dist/livekit/index.d.ts +1 -1
- package/dist/livekit/index.mjs +1 -1
- package/dist/utils/index.cjs +15 -0
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.d.cts +15 -36
- package/dist/utils/index.d.ts +15 -36
- package/dist/utils/index.mjs +13 -1
- package/dist/utils/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -81,6 +81,159 @@ import { LiveKitProvider, useTrack } from 'vg-x07df/livekit';
|
|
|
81
81
|
const track = useTrack();
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
+
## Video Call Features
|
|
85
|
+
|
|
86
|
+
### Video Initialization
|
|
87
|
+
|
|
88
|
+
The SDK automatically initializes camera capability but keeps it disabled by default for privacy:
|
|
89
|
+
|
|
90
|
+
- Camera permissions are requested when the room connects
|
|
91
|
+
- Video starts disabled (privacy-first approach)
|
|
92
|
+
- Users must explicitly enable video via UI controls
|
|
93
|
+
- Room is optimized with adaptive streaming and dynacast for performance
|
|
94
|
+
|
|
95
|
+
### Using Video Controls
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
import { useTrackToggle, Track } from "vg-x07df/livekit";
|
|
99
|
+
|
|
100
|
+
function VideoControls() {
|
|
101
|
+
// Toggle video on/off
|
|
102
|
+
const { toggle: toggleVideo, enabled: isVideoEnabled } = useTrackToggle({
|
|
103
|
+
source: Track.Source.Camera
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<button onClick={toggleVideo}>
|
|
108
|
+
{isVideoEnabled ? "Turn Off Video" : "Turn On Video"}
|
|
109
|
+
</button>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Displaying Video
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
import { VideoTrack, useParticipantTracks, Track } from "vg-x07df/livekit";
|
|
118
|
+
import { hasVideoTrack } from "vg-x07df/utils";
|
|
119
|
+
|
|
120
|
+
function ParticipantVideo({ participant }) {
|
|
121
|
+
const tracks = useParticipantTracks(participant, Track.Source.Camera);
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div className="participant-container">
|
|
125
|
+
{hasVideoTrack(participant) ? (
|
|
126
|
+
<VideoTrack
|
|
127
|
+
participant={participant}
|
|
128
|
+
source={Track.Source.Camera}
|
|
129
|
+
className="video-element"
|
|
130
|
+
/>
|
|
131
|
+
) : (
|
|
132
|
+
<div className="avatar-fallback">
|
|
133
|
+
{participant.identity}
|
|
134
|
+
</div>
|
|
135
|
+
)}
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Video Utilities
|
|
142
|
+
|
|
143
|
+
The SDK provides utility functions for common video operations:
|
|
144
|
+
|
|
145
|
+
```tsx
|
|
146
|
+
import { hasVideoTrack, getVideoTrack, hasVideoCapability } from "vg-x07df/utils";
|
|
147
|
+
|
|
148
|
+
// Check if participant has active video (enabled and published)
|
|
149
|
+
const hasActiveVideo = hasVideoTrack(participant);
|
|
150
|
+
|
|
151
|
+
// Get video track publication
|
|
152
|
+
const videoTrack = getVideoTrack(participant);
|
|
153
|
+
|
|
154
|
+
// Check if video capability exists (track exists, may be muted)
|
|
155
|
+
const canHaveVideo = hasVideoCapability(participant);
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Complete Video Implementation Example
|
|
159
|
+
|
|
160
|
+
```tsx
|
|
161
|
+
import {
|
|
162
|
+
VideoTrack,
|
|
163
|
+
useTrackToggle,
|
|
164
|
+
useParticipantTracks,
|
|
165
|
+
Track,
|
|
166
|
+
useParticipants
|
|
167
|
+
} from "vg-x07df/livekit";
|
|
168
|
+
import { hasVideoTrack } from "vg-x07df/utils";
|
|
169
|
+
|
|
170
|
+
function VideoCallInterface() {
|
|
171
|
+
const participants = useParticipants();
|
|
172
|
+
const { toggle: toggleVideo, enabled: isVideoEnabled } = useTrackToggle({
|
|
173
|
+
source: Track.Source.Camera
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<div className="video-call-container">
|
|
178
|
+
{/* Video Controls */}
|
|
179
|
+
<div className="controls">
|
|
180
|
+
<button onClick={toggleVideo}>
|
|
181
|
+
{isVideoEnabled ? "Turn Off Video" : "Turn On Video"}
|
|
182
|
+
</button>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
{/* Participant Videos */}
|
|
186
|
+
<div className="participants-grid">
|
|
187
|
+
{participants.map((participant) => (
|
|
188
|
+
<div key={participant.identity} className="participant-tile">
|
|
189
|
+
{hasVideoTrack(participant) ? (
|
|
190
|
+
<VideoTrack
|
|
191
|
+
participant={participant}
|
|
192
|
+
source={Track.Source.Camera}
|
|
193
|
+
className="video-element"
|
|
194
|
+
/>
|
|
195
|
+
) : (
|
|
196
|
+
<div className="avatar-placeholder">
|
|
197
|
+
{participant.identity.charAt(0).toUpperCase()}
|
|
198
|
+
</div>
|
|
199
|
+
)}
|
|
200
|
+
<span className="participant-name">
|
|
201
|
+
{participant.identity}
|
|
202
|
+
</span>
|
|
203
|
+
</div>
|
|
204
|
+
))}
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Video Troubleshooting
|
|
212
|
+
|
|
213
|
+
**Camera permissions denied:**
|
|
214
|
+
- The SDK handles permissions gracefully
|
|
215
|
+
- Users will see a browser permission prompt on first video toggle
|
|
216
|
+
- If permissions are denied, the call continues as audio-only
|
|
217
|
+
- Check browser settings if video toggle doesn't work
|
|
218
|
+
|
|
219
|
+
**Video not appearing:**
|
|
220
|
+
- Verify `hasVideoTrack(participant)` returns `true`
|
|
221
|
+
- Check that `useTrackToggle` shows `enabled: true`
|
|
222
|
+
- Ensure the `VideoTrack` component has the correct `participant` prop
|
|
223
|
+
- Confirm the participant has published their video track
|
|
224
|
+
|
|
225
|
+
**Performance issues with multiple videos:**
|
|
226
|
+
- The SDK uses optimized settings (720p, adaptive streaming, dynacast)
|
|
227
|
+
- Video quality automatically adjusts based on available bandwidth
|
|
228
|
+
- Multiple video streams are handled efficiently with simulcast
|
|
229
|
+
- Consider reducing video quality for low-bandwidth scenarios
|
|
230
|
+
|
|
231
|
+
**Audio works but video doesn't:**
|
|
232
|
+
- Check if camera is being used by another application
|
|
233
|
+
- Verify camera permissions in browser settings
|
|
234
|
+
- Try refreshing the page to reinitialize camera access
|
|
235
|
+
- Check browser console for camera-related errors
|
|
236
|
+
|
|
84
237
|
## Requirements
|
|
85
238
|
|
|
86
239
|
- React ≥18.0.0
|
package/dist/channel/index.cjs
CHANGED
|
@@ -460,8 +460,10 @@ var ChatService = class {
|
|
|
460
460
|
this.room.registerTextStreamHandler(
|
|
461
461
|
"chat:v1",
|
|
462
462
|
async (reader, participantInfo) => {
|
|
463
|
+
console.log("Got here: chat:v1", participantInfo.identity);
|
|
463
464
|
try {
|
|
464
465
|
const text = await reader.readAll();
|
|
466
|
+
console.log("Got here: chat:v1", text);
|
|
465
467
|
this.handleIncomingMessage(text);
|
|
466
468
|
} catch (error) {
|
|
467
469
|
logger.error("Error reading text stream", error);
|
|
@@ -828,6 +830,262 @@ function useChat() {
|
|
|
828
830
|
unreact
|
|
829
831
|
};
|
|
830
832
|
}
|
|
833
|
+
var defaultState2 = {
|
|
834
|
+
reactions: /* @__PURE__ */ new Map(),
|
|
835
|
+
participantCache: {},
|
|
836
|
+
ttlMs: 4e3
|
|
837
|
+
};
|
|
838
|
+
var useReactionsStore = zustand.create()(
|
|
839
|
+
immer.immer((set) => ({
|
|
840
|
+
...defaultState2,
|
|
841
|
+
setReaction: (participantId, reaction) => set((state) => {
|
|
842
|
+
state.reactions.set(participantId, reaction);
|
|
843
|
+
}),
|
|
844
|
+
clearReaction: (participantId) => set((state) => {
|
|
845
|
+
state.reactions.delete(participantId);
|
|
846
|
+
}),
|
|
847
|
+
upsertParticipantInfo: (id, info) => set((state) => {
|
|
848
|
+
state.participantCache[id] = info;
|
|
849
|
+
}),
|
|
850
|
+
pruneExpired: (now = Date.now()) => set((state) => {
|
|
851
|
+
const entries = Array.from(state.reactions.entries());
|
|
852
|
+
for (const [participantId, reaction] of entries) {
|
|
853
|
+
if (reaction.ts + state.ttlMs < now) {
|
|
854
|
+
state.reactions.delete(participantId);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}),
|
|
858
|
+
clear: () => set(() => ({
|
|
859
|
+
reactions: /* @__PURE__ */ new Map(),
|
|
860
|
+
participantCache: {},
|
|
861
|
+
ttlMs: defaultState2.ttlMs
|
|
862
|
+
}))
|
|
863
|
+
}))
|
|
864
|
+
);
|
|
865
|
+
function applyIncomingReaction(envelope) {
|
|
866
|
+
const { upsertParticipantInfo, setReaction } = useReactionsStore.getState();
|
|
867
|
+
if (envelope.sender.info) {
|
|
868
|
+
upsertParticipantInfo(envelope.sender.id, envelope.sender.info);
|
|
869
|
+
}
|
|
870
|
+
setReaction(envelope.sender.id, {
|
|
871
|
+
emoji: envelope.payload.emoji,
|
|
872
|
+
ts: Date.now(),
|
|
873
|
+
nonce: envelope.payload.nonce
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// src/channel/reactions/utils.ts
|
|
878
|
+
createLogger("reactions");
|
|
879
|
+
function validateEmoji(emoji) {
|
|
880
|
+
if (!emoji || typeof emoji !== "string") {
|
|
881
|
+
return { valid: false, error: "empty" };
|
|
882
|
+
}
|
|
883
|
+
if (emoji.length > 10) {
|
|
884
|
+
return { valid: false, error: "too long" };
|
|
885
|
+
}
|
|
886
|
+
return { valid: true };
|
|
887
|
+
}
|
|
888
|
+
function generateNonce() {
|
|
889
|
+
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// src/channel/reactions/service.ts
|
|
893
|
+
var logger3 = createLogger("reactions");
|
|
894
|
+
var ReactionsService = class {
|
|
895
|
+
constructor(room) {
|
|
896
|
+
this.isSubscribed = false;
|
|
897
|
+
this.lastSendAt = 0;
|
|
898
|
+
this.minIntervalMs = 200;
|
|
899
|
+
this.lastRemoteTs = /* @__PURE__ */ new Map();
|
|
900
|
+
this.room = room;
|
|
901
|
+
}
|
|
902
|
+
isRoomReady() {
|
|
903
|
+
if (!this.room) {
|
|
904
|
+
logger3.warn("Room not initialized");
|
|
905
|
+
return false;
|
|
906
|
+
}
|
|
907
|
+
if (this.room.state !== livekitClient.ConnectionState.Connected) {
|
|
908
|
+
logger3.warn("Room not connected", { state: this.room.state });
|
|
909
|
+
return false;
|
|
910
|
+
}
|
|
911
|
+
if (!this.room.localParticipant) {
|
|
912
|
+
logger3.warn("Local participant not available");
|
|
913
|
+
return false;
|
|
914
|
+
}
|
|
915
|
+
return true;
|
|
916
|
+
}
|
|
917
|
+
canSendNow() {
|
|
918
|
+
const now = Date.now();
|
|
919
|
+
if (now - this.lastSendAt < this.minIntervalMs) return false;
|
|
920
|
+
this.lastSendAt = now;
|
|
921
|
+
return true;
|
|
922
|
+
}
|
|
923
|
+
subscribe() {
|
|
924
|
+
if (this.isSubscribed) return;
|
|
925
|
+
this.room.registerTextStreamHandler("reactions:v1", async (reader) => {
|
|
926
|
+
try {
|
|
927
|
+
const text = await reader.readAll();
|
|
928
|
+
this.handleIncoming(text);
|
|
929
|
+
} catch (err) {
|
|
930
|
+
logger3.error("Error reading reactions stream", err);
|
|
931
|
+
}
|
|
932
|
+
});
|
|
933
|
+
this.pruneInterval = setInterval(() => {
|
|
934
|
+
useReactionsStore.getState().pruneExpired();
|
|
935
|
+
}, 1e3);
|
|
936
|
+
this.isSubscribed = true;
|
|
937
|
+
logger3.info("ReactionsService subscribed");
|
|
938
|
+
}
|
|
939
|
+
unsubscribe() {
|
|
940
|
+
this.isSubscribed = false;
|
|
941
|
+
if (this.pruneInterval) {
|
|
942
|
+
clearInterval(this.pruneInterval);
|
|
943
|
+
this.pruneInterval = void 0;
|
|
944
|
+
}
|
|
945
|
+
logger3.info("ReactionsService unsubscribed");
|
|
946
|
+
}
|
|
947
|
+
getLocalParticipantId() {
|
|
948
|
+
return this.room.localParticipant.identity;
|
|
949
|
+
}
|
|
950
|
+
async sendReaction(emoji) {
|
|
951
|
+
if (!this.isRoomReady()) {
|
|
952
|
+
useRtcStore.getState().addError({
|
|
953
|
+
code: "REACTIONS_ROOM_NOT_READY",
|
|
954
|
+
message: "Cannot send reaction: room not connected",
|
|
955
|
+
timestamp: Date.now()
|
|
956
|
+
});
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
const validation = validateEmoji(emoji);
|
|
960
|
+
if (!validation.valid) {
|
|
961
|
+
useRtcStore.getState().addError({
|
|
962
|
+
code: "REACTIONS_INVALID_EMOJI",
|
|
963
|
+
message: validation.error || "Invalid emoji",
|
|
964
|
+
timestamp: Date.now()
|
|
965
|
+
});
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
if (!this.canSendNow()) {
|
|
969
|
+
logger3.debug("Rate limited, skipping send");
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
const senderInfo = this.getSenderInfo();
|
|
973
|
+
const ts = Date.now();
|
|
974
|
+
const nonce = generateNonce();
|
|
975
|
+
useReactionsStore.getState().setReaction(senderInfo.id, {
|
|
976
|
+
emoji,
|
|
977
|
+
ts,
|
|
978
|
+
nonce
|
|
979
|
+
});
|
|
980
|
+
try {
|
|
981
|
+
const envelope = {
|
|
982
|
+
v: 1,
|
|
983
|
+
kind: "reaction",
|
|
984
|
+
roomId: this.room.name,
|
|
985
|
+
ts,
|
|
986
|
+
sender: senderInfo,
|
|
987
|
+
payload: {
|
|
988
|
+
emoji,
|
|
989
|
+
nonce
|
|
990
|
+
}
|
|
991
|
+
};
|
|
992
|
+
await this.room.localParticipant.sendText(JSON.stringify(envelope), {
|
|
993
|
+
topic: "reactions:v1"
|
|
994
|
+
});
|
|
995
|
+
logger3.debug("Reaction sent", { emoji });
|
|
996
|
+
} catch (error) {
|
|
997
|
+
logger3.error("Failed to send reaction", error);
|
|
998
|
+
useRtcStore.getState().addError({
|
|
999
|
+
code: "REACTIONS_SEND_FAILED",
|
|
1000
|
+
message: error instanceof Error ? error.message : "Failed to send reaction",
|
|
1001
|
+
timestamp: Date.now(),
|
|
1002
|
+
context: { emoji }
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
handleIncoming(text) {
|
|
1007
|
+
try {
|
|
1008
|
+
const parsed = JSON.parse(text);
|
|
1009
|
+
if (!this.isValidEnvelope(parsed)) {
|
|
1010
|
+
logger3.warn("Invalid reaction envelope received", parsed);
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
if (parsed.sender.id === this.getLocalParticipantId()) {
|
|
1014
|
+
logger3.debug("Ignoring self-echo reaction");
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
const lastTs = this.lastRemoteTs.get(parsed.sender.id) ?? Number.NEGATIVE_INFINITY;
|
|
1018
|
+
if (parsed.ts < lastTs) {
|
|
1019
|
+
logger3.debug("Ignoring out-of-order reaction", {
|
|
1020
|
+
sender: parsed.sender.id,
|
|
1021
|
+
ts: parsed.ts,
|
|
1022
|
+
lastTs
|
|
1023
|
+
});
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
this.lastRemoteTs.set(parsed.sender.id, parsed.ts);
|
|
1027
|
+
applyIncomingReaction(parsed);
|
|
1028
|
+
logger3.debug("Reaction received", { emoji: parsed.payload.emoji });
|
|
1029
|
+
} catch (error) {
|
|
1030
|
+
logger3.error("Error parsing incoming reaction", error);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
isValidEnvelope(e) {
|
|
1034
|
+
return e && e.v === 1 && e.kind === "reaction" && typeof e.roomId === "string" && e.roomId === this.room.name && typeof e.ts === "number" && e.ts > 0 && typeof e.sender?.id === "string" && typeof e.payload?.emoji === "string" && typeof e.payload?.nonce === "string";
|
|
1035
|
+
}
|
|
1036
|
+
getSenderInfo() {
|
|
1037
|
+
const localParticipant = this.room.localParticipant;
|
|
1038
|
+
const sender = {
|
|
1039
|
+
id: localParticipant.identity
|
|
1040
|
+
};
|
|
1041
|
+
if (localParticipant.metadata) {
|
|
1042
|
+
try {
|
|
1043
|
+
sender.info = JSON.parse(
|
|
1044
|
+
localParticipant.metadata
|
|
1045
|
+
);
|
|
1046
|
+
} catch {
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
return sender;
|
|
1050
|
+
}
|
|
1051
|
+
};
|
|
1052
|
+
var logger4 = createLogger("reactions:hook");
|
|
1053
|
+
function useReactions() {
|
|
1054
|
+
const service = useFeatureService("reactions");
|
|
1055
|
+
const reactions = useReactionsStore((state) => state.reactions);
|
|
1056
|
+
const participantCache = useReactionsStore((state) => state.participantCache);
|
|
1057
|
+
const getReactionFor = react.useCallback(
|
|
1058
|
+
(participantId) => {
|
|
1059
|
+
const reaction = reactions.get(participantId);
|
|
1060
|
+
return reaction?.emoji ?? null;
|
|
1061
|
+
},
|
|
1062
|
+
[reactions]
|
|
1063
|
+
);
|
|
1064
|
+
const getParticipantInfo = react.useCallback(
|
|
1065
|
+
(id) => participantCache[id] || null,
|
|
1066
|
+
[participantCache]
|
|
1067
|
+
);
|
|
1068
|
+
const sendReaction = react.useCallback(
|
|
1069
|
+
async (emoji) => {
|
|
1070
|
+
if (!service) {
|
|
1071
|
+
logger4.error("Cannot send reaction: service not ready");
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
return service.sendReaction(emoji);
|
|
1075
|
+
},
|
|
1076
|
+
[service]
|
|
1077
|
+
);
|
|
1078
|
+
const clearReaction = react.useCallback((participantId) => {
|
|
1079
|
+
useReactionsStore.getState().clearReaction(participantId);
|
|
1080
|
+
}, []);
|
|
1081
|
+
return {
|
|
1082
|
+
sendReaction,
|
|
1083
|
+
getReactionFor,
|
|
1084
|
+
clearReaction,
|
|
1085
|
+
getParticipantInfo,
|
|
1086
|
+
isReady: !!service
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
831
1089
|
|
|
832
1090
|
// src/channel/registry.ts
|
|
833
1091
|
var FEATURES = {
|
|
@@ -835,10 +1093,15 @@ var FEATURES = {
|
|
|
835
1093
|
name: "chat",
|
|
836
1094
|
createService: (room) => new ChatService(room),
|
|
837
1095
|
cleanupStore: () => useChatStore.getState().clearChat()
|
|
1096
|
+
},
|
|
1097
|
+
reactions: {
|
|
1098
|
+
name: "reactions",
|
|
1099
|
+
createService: (room) => new ReactionsService(room),
|
|
1100
|
+
cleanupStore: () => useReactionsStore.getState().clear()
|
|
838
1101
|
}
|
|
839
1102
|
};
|
|
840
|
-
var DEFAULT_FEATURES = ["chat"];
|
|
841
|
-
var
|
|
1103
|
+
var DEFAULT_FEATURES = ["chat", "reactions"];
|
|
1104
|
+
var logger5 = createLogger("channels:provider");
|
|
842
1105
|
function DataChannelProvider({
|
|
843
1106
|
room,
|
|
844
1107
|
features = DEFAULT_FEATURES,
|
|
@@ -848,52 +1111,52 @@ function DataChannelProvider({
|
|
|
848
1111
|
const [isReady, setIsReady] = react.useState(false);
|
|
849
1112
|
react.useEffect(() => {
|
|
850
1113
|
if (!room) {
|
|
851
|
-
|
|
1114
|
+
logger5.warn("DataChannelProvider mounted without room");
|
|
852
1115
|
return;
|
|
853
1116
|
}
|
|
854
|
-
|
|
1117
|
+
logger5.debug("Initializing features", { features });
|
|
855
1118
|
for (const featureName of features) {
|
|
856
1119
|
const feature = FEATURES[featureName];
|
|
857
1120
|
if (!feature) {
|
|
858
|
-
|
|
1121
|
+
logger5.warn(`Feature "${featureName}" not found in registry`);
|
|
859
1122
|
continue;
|
|
860
1123
|
}
|
|
861
1124
|
try {
|
|
862
|
-
|
|
1125
|
+
logger5.debug(`Initializing feature: ${featureName}`);
|
|
863
1126
|
const service = feature.createService(room);
|
|
864
1127
|
service.subscribe();
|
|
865
1128
|
services.current.set(featureName, service);
|
|
866
|
-
|
|
1129
|
+
logger5.info(`Feature "${featureName}" initialized`);
|
|
867
1130
|
} catch (error) {
|
|
868
|
-
|
|
1131
|
+
logger5.error(`Failed to initialize feature "${featureName}"`, error);
|
|
869
1132
|
}
|
|
870
1133
|
}
|
|
871
1134
|
setIsReady(true);
|
|
872
|
-
|
|
1135
|
+
logger5.info("All features initialized");
|
|
873
1136
|
return () => {
|
|
874
|
-
|
|
1137
|
+
logger5.debug("Cleaning up features");
|
|
875
1138
|
services.current.forEach((service, name) => {
|
|
876
1139
|
try {
|
|
877
|
-
|
|
1140
|
+
logger5.debug(`Unsubscribing feature: ${name}`);
|
|
878
1141
|
service.unsubscribe();
|
|
879
1142
|
} catch (error) {
|
|
880
|
-
|
|
1143
|
+
logger5.error(`Failed to unsubscribe feature "${name}"`, error);
|
|
881
1144
|
}
|
|
882
1145
|
});
|
|
883
1146
|
for (const featureName of features) {
|
|
884
1147
|
const feature = FEATURES[featureName];
|
|
885
1148
|
if (feature?.cleanupStore) {
|
|
886
1149
|
try {
|
|
887
|
-
|
|
1150
|
+
logger5.debug(`Cleaning store for feature: ${featureName}`);
|
|
888
1151
|
feature.cleanupStore();
|
|
889
1152
|
} catch (error) {
|
|
890
|
-
|
|
1153
|
+
logger5.error(`Failed to cleanup store for "${featureName}"`, error);
|
|
891
1154
|
}
|
|
892
1155
|
}
|
|
893
1156
|
}
|
|
894
1157
|
services.current.clear();
|
|
895
1158
|
setIsReady(false);
|
|
896
|
-
|
|
1159
|
+
logger5.info("Features cleanup complete");
|
|
897
1160
|
};
|
|
898
1161
|
}, [room, features]);
|
|
899
1162
|
const contextValue = react.useMemo(
|
|
@@ -908,14 +1171,20 @@ function DataChannelProvider({
|
|
|
908
1171
|
|
|
909
1172
|
exports.ChatService = ChatService;
|
|
910
1173
|
exports.DataChannelProvider = DataChannelProvider;
|
|
1174
|
+
exports.ReactionsService = ReactionsService;
|
|
1175
|
+
exports.applyIncomingReaction = applyIncomingReaction;
|
|
911
1176
|
exports.compareEntries = compareEntries;
|
|
912
1177
|
exports.generateEntryId = generateEntryId;
|
|
1178
|
+
exports.generateNonce = generateNonce;
|
|
913
1179
|
exports.getCurrentTimestamp = getCurrentTimestamp;
|
|
914
1180
|
exports.isValidEnvelope = isValidEnvelope;
|
|
915
1181
|
exports.useChat = useChat;
|
|
916
1182
|
exports.useChatStore = useChatStore;
|
|
917
1183
|
exports.useDataChannelContext = useDataChannelContext;
|
|
918
1184
|
exports.useFeatureService = useFeatureService;
|
|
1185
|
+
exports.useReactions = useReactions;
|
|
1186
|
+
exports.useReactionsStore = useReactionsStore;
|
|
919
1187
|
exports.validateContent = validateContent;
|
|
1188
|
+
exports.validateEmoji = validateEmoji;
|
|
920
1189
|
//# sourceMappingURL=index.cjs.map
|
|
921
1190
|
//# sourceMappingURL=index.cjs.map
|