@xipkg/calls-risehand 0.0.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 ADDED
File without changes
@@ -0,0 +1,15 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+
3
+ declare const RaiseHandButton: ({ className }: {
4
+ className?: string;
5
+ }) => react_jsx_runtime.JSX.Element;
6
+
7
+ declare const useRaisedHands: () => {
8
+ toggleHand: () => Promise<void>;
9
+ isHandRaised: boolean;
10
+ isPending: boolean;
11
+ };
12
+
13
+ declare const useHandFocus: () => void;
14
+
15
+ export { RaiseHandButton, useHandFocus, useRaisedHands };
package/dist/index.mjs ADDED
@@ -0,0 +1,140 @@
1
+ import { Button } from '@xipkg/button';
2
+ import { Hand } from '@xipkg/icons';
3
+ import { cn } from '@xipkg/utils';
4
+ import { useRef, useMemo, useCallback, useEffect } from 'react';
5
+ import { RoomEvent, Track } from 'livekit-client';
6
+ import { useSoundEffectsStore, useCallStore } from '@xipkg/calls-store';
7
+ import { useCallsNavigation, useRoom, useCalls } from '@xipkg/calls-providers';
8
+ import { playSound } from '@xipkg/calls-utils';
9
+ import { useMaybeLayoutContext } from '@livekit/components-react';
10
+ import { jsx } from 'react/jsx-runtime';
11
+
12
+ // src/ui/RaiseHandButton.tsx
13
+ var useRaisedHands = () => {
14
+ const navigation = useCallsNavigation();
15
+ const prevHands = useRef({});
16
+ const handRaiseSoundVolume = useSoundEffectsStore((s) => s.handRaiseVolume);
17
+ const callId = navigation.getCallId();
18
+ const { room } = useRoom();
19
+ const { data: user } = useCalls().auth.useCurrentUser();
20
+ const { addRaisedHand, removeRaisedHand } = useCallStore();
21
+ const { updateParticipantMetadata, isPending } = useCalls().updateParticipantMetadata;
22
+ const isHandRaised = useMemo(() => {
23
+ try {
24
+ const parsed = JSON.parse(room?.localParticipant?.metadata ?? "{}");
25
+ return !!parsed.is_hand_raised;
26
+ } catch {
27
+ return false;
28
+ }
29
+ }, [room?.localParticipant?.metadata]);
30
+ const toggleHand = useCallback(async () => {
31
+ if (!room?.localParticipant || !callId) return;
32
+ try {
33
+ await updateParticipantMetadata({
34
+ classroom_id: callId,
35
+ is_hand_raised: !isHandRaised,
36
+ role: user?.defaultLayout
37
+ });
38
+ } catch (e) {
39
+ console.error("raise hand error", e);
40
+ }
41
+ }, [
42
+ room?.localParticipant,
43
+ callId,
44
+ updateParticipantMetadata,
45
+ isHandRaised,
46
+ user?.defaultLayout
47
+ ]);
48
+ useEffect(() => {
49
+ if (!room) return;
50
+ const syncParticipant = (participant) => {
51
+ try {
52
+ const parsed = JSON.parse(participant.metadata || "{}");
53
+ if (!("is_hand_raised" in parsed)) return;
54
+ const prev = prevHands.current[participant.identity];
55
+ if (parsed.is_hand_raised) {
56
+ addRaisedHand({
57
+ participantId: participant.identity,
58
+ participantName: participant.name ?? participant.identity,
59
+ timestamp: Date.now()
60
+ });
61
+ if (!prev) {
62
+ playSound("handRaise", handRaiseSoundVolume);
63
+ }
64
+ } else {
65
+ removeRaisedHand(participant.identity);
66
+ }
67
+ } catch (e) {
68
+ console.error("metadata parse error", e);
69
+ }
70
+ };
71
+ const handler = (_metadata, participant) => {
72
+ syncParticipant(participant);
73
+ };
74
+ room.on(RoomEvent.ParticipantMetadataChanged, handler);
75
+ syncParticipant(room.localParticipant);
76
+ room.remoteParticipants.forEach(syncParticipant);
77
+ return () => {
78
+ room.off(RoomEvent.ParticipantMetadataChanged, handler);
79
+ };
80
+ }, [room, addRaisedHand, removeRaisedHand, handRaiseSoundVolume]);
81
+ return {
82
+ toggleHand,
83
+ isHandRaised,
84
+ isPending
85
+ };
86
+ };
87
+ var useHandFocus = () => {
88
+ const { raisedHands } = useCallStore();
89
+ const { room } = useRoom();
90
+ const layoutContext = useMaybeLayoutContext();
91
+ useEffect(() => {
92
+ if (!room || raisedHands.length === 0 || !layoutContext?.pin.dispatch) return;
93
+ const earliestHand = raisedHands.reduce(
94
+ (earliest, current) => current.timestamp < earliest.timestamp ? current : earliest
95
+ );
96
+ const participant = room.getParticipantByIdentity(earliestHand.participantId);
97
+ if (participant) {
98
+ console.log(
99
+ "\u{1F3AF} Auto-focusing on participant with raised hand:",
100
+ earliestHand.participantName
101
+ );
102
+ const cameraTrack = Array.from(participant.videoTrackPublications.values()).find(
103
+ (track) => track.source === "camera"
104
+ );
105
+ if (cameraTrack) {
106
+ layoutContext.pin.dispatch({
107
+ msg: "set_pin",
108
+ trackReference: {
109
+ participant,
110
+ source: Track.Source.Camera,
111
+ publication: cameraTrack
112
+ }
113
+ });
114
+ }
115
+ } else {
116
+ console.log("\u26A0\uFE0F Participant not found:", earliestHand.participantId);
117
+ }
118
+ }, [raisedHands, room, layoutContext]);
119
+ };
120
+ var RaiseHandButton = ({ className }) => {
121
+ const { toggleHand } = useRaisedHands();
122
+ const { isHandRaised } = useCallStore();
123
+ return /* @__PURE__ */ jsx(
124
+ Button,
125
+ {
126
+ size: "icon",
127
+ variant: "ghost",
128
+ onClick: toggleHand,
129
+ className: cn(
130
+ `relative m-0 h-10 w-10 rounded-lg p-0 ${isHandRaised ? "bg-brand-20 text-brand-100" : "hover:bg-gray-5 text-gray-100"}`,
131
+ className
132
+ ),
133
+ children: /* @__PURE__ */ jsx(Hand, { className: cn("h-5 w-5", isHandRaised ? "fill-brand-100" : "fill-gray-100") })
134
+ }
135
+ );
136
+ };
137
+
138
+ export { RaiseHandButton, useHandFocus, useRaisedHands };
139
+ //# sourceMappingURL=index.mjs.map
140
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/hooks/useRaisedHands.ts","../src/hooks/useHandFocus.ts","../src/ui/RaiseHandButton.tsx"],"names":["useCallStore","useRoom","useEffect"],"mappings":"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","file":"index.mjs","sourcesContent":["import { useEffect, useCallback, useMemo, useRef } from 'react';\nimport { Participant, RoomEvent } from 'livekit-client';\nimport { useCallStore, useSoundEffectsStore } from '@xipkg/calls-store';\nimport { useCalls, useRoom, useCallsNavigation } from '@xipkg/calls-providers';\nimport { playSound } from '@xipkg/calls-utils';\n\nexport const useRaisedHands = () => {\n const navigation = useCallsNavigation();\n const prevHands = useRef<Record<string, boolean>>({});\n const handRaiseSoundVolume = useSoundEffectsStore((s) => s.handRaiseVolume);\n\n const callId = navigation.getCallId();\n const { room } = useRoom();\n const { data: user } = useCalls().auth.useCurrentUser();\n\n const { addRaisedHand, removeRaisedHand } = useCallStore();\n\n const { updateParticipantMetadata, isPending } = useCalls().updateParticipantMetadata;\n\n const isHandRaised = useMemo(() => {\n try {\n const parsed = JSON.parse(room?.localParticipant?.metadata ?? '{}');\n return !!parsed.is_hand_raised;\n } catch {\n return false;\n }\n }, [room?.localParticipant?.metadata]);\n\n const toggleHand = useCallback(async () => {\n if (!room?.localParticipant || !callId) return;\n\n try {\n await updateParticipantMetadata({\n classroom_id: callId,\n is_hand_raised: !isHandRaised,\n role: user?.defaultLayout,\n });\n } catch (e) {\n console.error('raise hand error', e);\n }\n }, [\n room?.localParticipant,\n callId,\n updateParticipantMetadata,\n isHandRaised,\n user?.defaultLayout,\n ]);\n\n useEffect(() => {\n if (!room) return;\n\n const syncParticipant = (participant: Participant) => {\n try {\n const parsed = JSON.parse(participant.metadata || '{}');\n\n if (!('is_hand_raised' in parsed)) return;\n\n const prev = prevHands.current[participant.identity];\n\n if (parsed.is_hand_raised) {\n addRaisedHand({\n participantId: participant.identity,\n participantName: participant.name ?? participant.identity,\n timestamp: Date.now(),\n });\n\n if (!prev) {\n playSound('handRaise', handRaiseSoundVolume);\n }\n } else {\n removeRaisedHand(participant.identity);\n }\n } catch (e) {\n console.error('metadata parse error', e);\n }\n };\n\n const handler = (_metadata: string | undefined, participant: Participant) => {\n syncParticipant(participant);\n };\n\n room.on(RoomEvent.ParticipantMetadataChanged, handler);\n\n syncParticipant(room.localParticipant);\n room.remoteParticipants.forEach(syncParticipant);\n\n return () => {\n room.off(RoomEvent.ParticipantMetadataChanged, handler);\n };\n }, [room, addRaisedHand, removeRaisedHand, handRaiseSoundVolume]);\n\n return {\n toggleHand,\n isHandRaised,\n isPending: isPending,\n };\n};\n","import { useEffect } from 'react';\nimport { Track } from 'livekit-client';\nimport { useCallStore } from '@xipkg/calls-store';\nimport { useRoom } from '@xipkg/calls-providers';\nimport { useMaybeLayoutContext } from '@livekit/components-react';\n\nexport const useHandFocus = () => {\n const { raisedHands } = useCallStore();\n const { room } = useRoom();\n const layoutContext = useMaybeLayoutContext();\n\n useEffect(() => {\n if (!room || raisedHands.length === 0 || !layoutContext?.pin.dispatch) return;\n\n // Находим участника с самой ранней поднятой рукой\n const earliestHand = raisedHands.reduce((earliest, current) =>\n current.timestamp < earliest.timestamp ? current : earliest,\n );\n\n // Ищем участника в комнате\n const participant = room.getParticipantByIdentity(earliestHand.participantId);\n\n if (participant) {\n console.log(\n '🎯 Auto-focusing on participant with raised hand:',\n earliestHand.participantName,\n );\n\n // Находим трек камеры участника для фокуса\n const cameraTrack = Array.from(participant.videoTrackPublications.values()).find(\n (track) => track.source === 'camera',\n );\n\n if (cameraTrack) {\n // Устанавливаем фокус на участника с поднятой рукой\n layoutContext.pin.dispatch({\n msg: 'set_pin',\n trackReference: {\n participant,\n source: Track.Source.Camera,\n publication: cameraTrack,\n },\n });\n }\n } else {\n console.log('⚠️ Participant not found:', earliestHand.participantId);\n }\n }, [raisedHands, room, layoutContext]);\n};\n","import { Button } from '@xipkg/button';\nimport { Hand } from '@xipkg/icons';\nimport { cn } from '@xipkg/utils';\nimport { useRaisedHands } from '../hooks';\nimport { useCallStore } from '@xipkg/calls-store';\n\nexport const RaiseHandButton = ({ className }: { className?: string }) => {\n const { toggleHand } = useRaisedHands();\n const { isHandRaised } = useCallStore();\n\n return (\n <Button\n size=\"icon\"\n variant=\"ghost\"\n onClick={toggleHand}\n className={cn(\n `relative m-0 h-10 w-10 rounded-lg p-0 ${\n isHandRaised ? 'bg-brand-20 text-brand-100' : 'hover:bg-gray-5 text-gray-100'\n }`,\n className,\n )}\n >\n <Hand className={cn('h-5 w-5', isHandRaised ? 'fill-brand-100' : 'fill-gray-100')} />\n </Button>\n );\n};\n"]}
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@xipkg/calls-risehand",
3
+ "version": "0.0.0",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "development": "./index.ts",
8
+ "types": "./dist/index.d.ts",
9
+ "import": "./dist/index.mjs",
10
+ "default": "./dist/index.mjs"
11
+ }
12
+ },
13
+ "license": "MIT",
14
+ "scripts": {
15
+ "lint": "eslint \"**/*.{ts,tsx}\"",
16
+ "dev": "tsup --watch",
17
+ "build": "tsup"
18
+ },
19
+ "dependencies": {
20
+ "@xipkg/icons": "^2.7.0",
21
+ "@xipkg/button": "3.2.0",
22
+ "@xipkg/calls-ui": "*",
23
+ "@xipkg/calls-utils": "*",
24
+ "@xipkg/calls-config": "*",
25
+ "@xipkg/calls-providers": "*",
26
+ "@xipkg/calls-store": "*"
27
+ },
28
+ "devDependencies": {
29
+ "@tanstack/react-query-devtools": "5.73.3",
30
+ "@eslint/js": "^9.19.0",
31
+ "common.typescript": "*",
32
+ "@types/node": "^20.3.1",
33
+ "@types/react": "^19.0.2",
34
+ "@types/react-dom": "^19.0.2",
35
+ "@xipkg/eslint": "3.2.0",
36
+ "@xipkg/typescript": "latest",
37
+ "common.eslint": "*",
38
+ "eslint": "^9.19.0",
39
+ "eslint-plugin-react-hooks": "^5.0.0",
40
+ "eslint-plugin-react-refresh": "^0.4.18",
41
+ "globals": "^15.14.0",
42
+ "typescript": "~5.7.2",
43
+ "typescript-eslint": "^8.22.0"
44
+ },
45
+ "peerDependencies": {
46
+ "react": "19"
47
+ },
48
+ "description": "calls package for rise hand feature",
49
+ "author": "xi.effect",
50
+ "main": "./dist/index.mjs",
51
+ "types": "./dist/index.d.ts",
52
+ "files": [
53
+ "dist"
54
+ ],
55
+ "publishConfig": {
56
+ "access": "public"
57
+ }
58
+ }