From b0bbaf4c6ee0aca9e30bafbbb4e4dc06a698762c Mon Sep 17 00:00:00 2001 From: Carlos Date: Mon, 20 Jan 2025 12:06:29 -0300 Subject: [PATCH 1/4] feat(wip): superviz react package --- apps/playground/package.json | 1 + .../src/pages/superviz-react-room.tsx | 60 +++++++ apps/playground/src/router/router.tsx | 5 + package.json | 2 +- packages/react-room/.eslintrc.json | 43 +++++ packages/react-room/.gitignore | 26 +++ packages/react-room/.npmrc.ci | 2 + packages/react-room/.prettierrc | 8 + packages/react-room/.releaserc | 47 ++++++ packages/react-room/README.md | 93 +++++++++++ packages/react-room/eslint.config.mjs | 75 +++++++++ packages/react-room/package.json | 75 +++++++++ packages/react-room/src/contexts/room.tsx | 156 ++++++++++++++++++ packages/react-room/src/index.ts | 3 + packages/react-room/src/vite-env.d.ts | 1 + packages/react-room/tsconfig.json | 31 ++++ packages/react-room/tsconfig.node.json | 10 ++ packages/react-room/vite.config.ts | 38 +++++ pnpm-lock.yaml | 106 ++++++++++++ 19 files changed, 781 insertions(+), 1 deletion(-) create mode 100644 apps/playground/src/pages/superviz-react-room.tsx create mode 100644 packages/react-room/.eslintrc.json create mode 100644 packages/react-room/.gitignore create mode 100644 packages/react-room/.npmrc.ci create mode 100644 packages/react-room/.prettierrc create mode 100644 packages/react-room/.releaserc create mode 100644 packages/react-room/README.md create mode 100644 packages/react-room/eslint.config.mjs create mode 100644 packages/react-room/package.json create mode 100644 packages/react-room/src/contexts/room.tsx create mode 100644 packages/react-room/src/index.ts create mode 100644 packages/react-room/src/vite-env.d.ts create mode 100644 packages/react-room/tsconfig.json create mode 100644 packages/react-room/tsconfig.node.json create mode 100644 packages/react-room/vite.config.ts diff --git a/apps/playground/package.json b/apps/playground/package.json index 3bc1cecc..063d5121 100644 --- a/apps/playground/package.json +++ b/apps/playground/package.json @@ -19,6 +19,7 @@ "@superviz/sdk": "workspace:*", "@superviz/threejs-plugin": "workspace:*", "@superviz/yjs": "workspace:*", + "@superviz/react": "workspace:*", "@types/three": "^0.167.1", "lib0": "^0.2.98", "lodash": "^4.17.21", diff --git a/apps/playground/src/pages/superviz-react-room.tsx b/apps/playground/src/pages/superviz-react-room.tsx new file mode 100644 index 00000000..a1d52aca --- /dev/null +++ b/apps/playground/src/pages/superviz-react-room.tsx @@ -0,0 +1,60 @@ + +import { RoomProvider, useRoom } from '@superviz/react' +import { getConfig } from '../config'; + +import { v4 as generateId } from "uuid"; +const SUPERVIZ_KEY = getConfig("keys.superviz"); +const SUPERVIZ_ROOM_PREFIX = getConfig("roomPrefix"); +const componentName = "new-room-react"; +const uuid = generateId(); + +export const Children = () => { + const { joinRoom, leaveRoom } = useRoom({ + onMyParticipantJoined: (participant) => console.log('Component: My participant joined', participant), + onMyParticipantLeft: (participant) => console.log('Component: My participant left', participant), + onMyParticipantUpdated: (participant) => console.log('Component: My participant updated', participant), + onParticipantJoined: (participant) => console.log('Component: Participant joined', participant), + onParticipantLeft: (participant) => console.log('Component: Participant left', participant), + onParticipantUpdated: (participant) => console.log('Component: Participant updated', participant), + onRoomUpdated: (data) => console.log('Component: Room updated', data), + onError: (error) => console.error('Component: Room error', error), + }); + + const handleJoin = async () => { + await joinRoom({ + participant: { + id: uuid, + name: "Participant Name", + email: 'carlos@superviz.com', + avatar: { + model3DUrl: 'https://production.storage.superviz.com/readyplayerme/1.glb', + imageUrl: 'https://production.cdn.superviz.com/static/default-avatars/1.png', + } + }, + group: { + name: SUPERVIZ_ROOM_PREFIX, + id: SUPERVIZ_ROOM_PREFIX, + }, + roomId: `${SUPERVIZ_ROOM_PREFIX}-${componentName}`, + debug: true, + environment: 'dev', + }); + }; + + return ( +
+ + +
+ ); + + return <> +}; + +export function SupervizReactRoom() { + return ( + + + + ) +} \ No newline at end of file diff --git a/apps/playground/src/router/router.tsx b/apps/playground/src/router/router.tsx index 8d90d3ae..a2c70eb3 100644 --- a/apps/playground/src/router/router.tsx +++ b/apps/playground/src/router/router.tsx @@ -32,6 +32,7 @@ import { MousePointersWithNewRoom } from "../pages/mouse-pointers-with-new-room. import { MousePointersWithNewRoomHTML } from "../pages/mouse-pointers-with-new-room-html.tsx"; import { CommentsHtmlCasesWithNewRoom } from "../pages/comments-html-cases-with-new-room.tsx"; import { VideoWithNewRoom } from "../pages/video-with-new-room.tsx"; +import { SupervizReactRoom } from "../pages/superviz-react-room.tsx"; export const routeList: RouteObject[] = [ { @@ -164,6 +165,10 @@ export const routeList: RouteObject[] = [ { path: 'video-with-new-room', element: + }, + { + path: 'superviz-react-room', + element: } ], }, diff --git a/package.json b/package.json index 2ca50bca..c00699ea 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "build": "turbo build", "dev": "turbo dev", "lint": "turbo lint", - "watch": "turbo run watch --concurrency=11", + "watch": "turbo run watch --concurrency=12", "semantic-release": "turbo run semantic-release", "test:unit": "turbo run test:unit", "test:unit:watch": "turbo run test:unit:watch", diff --git a/packages/react-room/.eslintrc.json b/packages/react-room/.eslintrc.json new file mode 100644 index 00000000..fcd74b78 --- /dev/null +++ b/packages/react-room/.eslintrc.json @@ -0,0 +1,43 @@ +{ + "env": { + "browser": true, + "es2021": true + }, + "extends": [ + "eslint:recommended", + "plugin:react/recommended", + "plugin:@typescript-eslint/recommended", + "prettier" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaFeatures": { + "tsx": true + }, + "ecmaVersion": "latest", + "sourceType": "module" + }, + "plugins": ["react", "@typescript-eslint", "react-hooks", "prettier","simple-import-sort"], + "rules": { + "camelcase": "error", + "no-duplicate-imports": "error", + "@typescript-eslint/ban-ts-comment": "off", + "@typescript-eslint/no-explicit-any":"off", + "react/react-in-jsx-scope":"off", + "no-console": "off", + "no-alert": "error", + "react-hooks/exhaustive-deps": "off", + "react/prop-types": 0, + "react/display-name": 0, + "simple-import-sort/imports": "error", + "simple-import-sort/exports": "error", + "@typescript-eslint/no-empty-function":"off", + "react/no-unknown-property":"off", + "react/no-unescaped-entities ":"off" + }, + "settings": { + "import/resolver": { + "typescript": {} + } + } + } \ No newline at end of file diff --git a/packages/react-room/.gitignore b/packages/react-room/.gitignore new file mode 100644 index 00000000..1d229540 --- /dev/null +++ b/packages/react-room/.gitignore @@ -0,0 +1,26 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +public/vendor +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +.env \ No newline at end of file diff --git a/packages/react-room/.npmrc.ci b/packages/react-room/.npmrc.ci new file mode 100644 index 00000000..ec2ee41e --- /dev/null +++ b/packages/react-room/.npmrc.ci @@ -0,0 +1,2 @@ +@superviz:registry=https://registry.npmjs.org/ +//registry.npmjs.org/:_authToken=${NPM_TOKEN} \ No newline at end of file diff --git a/packages/react-room/.prettierrc b/packages/react-room/.prettierrc new file mode 100644 index 00000000..d723adfb --- /dev/null +++ b/packages/react-room/.prettierrc @@ -0,0 +1,8 @@ +{ + "semi": true, + "trailingComma": "all", + "arrowParens": "always", + "printWidth": 100, + "singleQuote": true, + "tabWidth": 2 +} diff --git a/packages/react-room/.releaserc b/packages/react-room/.releaserc new file mode 100644 index 00000000..23b0db6d --- /dev/null +++ b/packages/react-room/.releaserc @@ -0,0 +1,47 @@ +{ + "branches": [ + "main", + { + "name": "beta", + "channel": "beta", + "prerelease": true + }, + { + "name": "lab", + "channel": "lab", + "prerelease": true + } + ], + "tagFormat": "@superviz/react/${version}", + "plugins": [ + "@semantic-release/commit-analyzer", + [ + "@semantic-release/release-notes-generator", + { + "preset": "angular", + "parserOpts": { + "noteKeywords": [ + "BREAKING CHANGE", + "BREAKING CHANGES", + "BREAKING" + ] + }, + "writerOpts": { + "commitsSort": [ + "subject", + "scope" + ] + } + } + ], + "@anolilab/semantic-release-pnpm", + [ + "@semantic-release/github", + { + "successComment": ":tada: This issue has been resolved in version @superviz/react/${nextRelease.version} :tada:\n\nThe release is available on [GitHub release]()" + } + ] + + ], + "preset": "angular" +} diff --git a/packages/react-room/README.md b/packages/react-room/README.md new file mode 100644 index 00000000..6d81965a --- /dev/null +++ b/packages/react-room/README.md @@ -0,0 +1,93 @@ +

+ SuperViz Logo +

+ +

+ Discord + GitHub issues + GitHub pull requests + npm type definitions + Downloads +

+ +SuperViz provides powerful SDKs and APIs that enable developers to easily integrate real-time features into web applications. Our platform accelerates development across various industries with robust, scalable infrastructure and a low-code approach. SuperViz SDK enables you to use one of our components: + +- Contextual Comments + - [Contextual Comments for HTML](https://docs.superviz.com/react-sdk/contextual-comments/HTML) + - [Contextual Comments for Canvas element](https://docs.superviz.com/react-sdk/contextual-comments/canvas) + - [Contextual Comments for Autodesk](https://docs.superviz.com/react-sdk/contextual-comments/autodesk) + - [Contextual Comments for Matterport](https://docs.superviz.com/react-sdk/contextual-comments/matterport) +- Presence + - [Real-time Mouse Pointers](https://docs.superviz.com/react-sdk/presence/mouse-pointers) + - [Real-time Data Engine](https://docs.superviz.com/react-sdk/presence/real-time-data-engine) + - [Who-is-Online](https://docs.superviz.com/react-sdk/presence/who-is-online) + - [Presence in Autodesk](https://docs.superviz.com/react-sdk/presence/AutodeskPresence) + - [Presence in Matterport](https://docs.superviz.com/react-sdk/presence/MatterportPresence) + - [Presence in ThreeJS](https://docs.superviz.com/react-sdk/presence/ThreeJsPresence) +- [Video Conference](https://docs.superviz.com/react-sdk/video/video-conference) +- [YJS Provider](https://docs.superviz.com/collaboration/api-reference/superviz-sdk-react/yjs) + +You can also combine components to create a custom solution for your application. + +How to start coding with SuperViz? After installing this package, you’ll need to [create an account](https://dashboard.superviz.com/) to retrieve a SuperViz Token and start coding. + +## Quickstart + +### 1. Installation + +Install SuperViz SDK in your React app with the npm package: + +```bash +npm install --save @superviz/react-sdk +``` + +Or, with yarn: + +```bash +yarn add @superviz/react-sdk +``` + +### 2. Import the SDK + +Once installed, import the SDK to your code: + +```jsx +import { SuperVizRoomProvider } from "@superviz/react-sdk"; +``` + +### 3. Initialize the SDK + +After importing the SDK, you can initialize our provider by passing your `DEVELOPER_KEY` and important information about the participant. You can see details for the options object on the [React Initialization page](https://docs.superviz.com/react-sdk/initialization). + +The SuperVizRoomProvider is your primary gateway to access all SDK features, offering the essential methods to add its components. + +```jsx +", + name: "", + }} + participant={{ + id: "", + name: "", + }} + roomId="" +> +

This is a room

+
+``` + +## Documentation + +You can find the complete documentation for every component and how to initialize them on the [SuperViz SDK Documentation page](https://docs.superviz.com/react-sdk/initialization). + +You can also find the complete changelog on the [Release Notes page](https://docs.superviz.com/releases). + +## Contributing + +If you are interested in contributing to SuperViz SDK, the best place to get involved with the community is through the [Discord server](https://discord.gg/weZ3Bfv6WZ), there you can find the latest news, ask questions, and share your experiences with SuperViz SDK. + +## License + +SuperViz SDK is licensed under the [BSD 2-Clause License](LICENSE). \ No newline at end of file diff --git a/packages/react-room/eslint.config.mjs b/packages/react-room/eslint.config.mjs new file mode 100644 index 00000000..ba968e3c --- /dev/null +++ b/packages/react-room/eslint.config.mjs @@ -0,0 +1,75 @@ +import react from "eslint-plugin-react"; +import typescriptEslint from "@typescript-eslint/eslint-plugin"; +import reactHooks from "eslint-plugin-react-hooks"; +import prettier from "eslint-plugin-prettier"; +import simpleImportSort from "eslint-plugin-simple-import-sort"; +import { fixupPluginRules } from "@eslint/compat"; +import globals from "globals"; +import tsParser from "@typescript-eslint/parser"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import js from "@eslint/js"; +import { FlatCompat } from "@eslint/eslintrc"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all +}); + +export default [...compat.extends( + "eslint:recommended", + "plugin:react/recommended", + "plugin:@typescript-eslint/recommended", + "prettier", +), { + plugins: { + react, + "@typescript-eslint": typescriptEslint, + "react-hooks": fixupPluginRules(reactHooks), + prettier, + "simple-import-sort": simpleImportSort, + }, + + languageOptions: { + globals: { + ...globals.browser, + }, + + parser: tsParser, + ecmaVersion: "latest", + sourceType: "module", + + parserOptions: { + ecmaFeatures: { + tsx: true, + }, + }, + }, + + settings: { + "import/resolver": { + typescript: {}, + }, + }, + + rules: { + camelcase: "error", + "no-duplicate-imports": "error", + "@typescript-eslint/ban-ts-comment": "off", + "@typescript-eslint/no-explicit-any": "off", + "react/react-in-jsx-scope": "off", + "no-console": "off", + "no-alert": "error", + "react-hooks/exhaustive-deps": "off", + "react/prop-types": 0, + "react/display-name": 0, + "simple-import-sort/imports": "error", + "simple-import-sort/exports": "error", + "@typescript-eslint/no-empty-function": "off", + "react/no-unknown-property": "off", + "react/no-unescaped-entities ": "off", + }, +}]; \ No newline at end of file diff --git a/packages/react-room/package.json b/packages/react-room/package.json new file mode 100644 index 00000000..df150306 --- /dev/null +++ b/packages/react-room/package.json @@ -0,0 +1,75 @@ +{ + "name": "@superviz/react", + "private": false, + "version": "0.0.1", + "type": "module", + "scripts": { + "watch": "./node_modules/typescript/bin/tsc && vite build --watch", + "build": "./node_modules/typescript/bin/tsc && vite build && cp package.json dist/package.json && cp README.md dist/README.md", + "prepack": "pnpm build", + "lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'", + "lint:fix": "eslint --fix 'src/**/*.{jsx,ts,tsx}'", + "format": "prettier --write src//**/*.{ts,tsx,css} --config ./.prettierrc", + "semantic-release": "semantic-release", + "commit": "git-cz" + }, + "files": [ + "dist" + ], + "exports": { + ".": { + "import": "./dist/superviz-sdk-react.es.js", + "require": "./dist/superviz-sdk-react.cjs.js", + "types": "./dist/index.d.ts" + }, + "./dist/style.css": "./dist/style.css" + }, + "main": "./dist/superviz-sdk-react.cjs.js", + "module": "./dist/superviz-sdk-react.es.js", + "types": "./dist/index.d.ts", + "publishConfig": { + "access": "public", + "scope": "@superviz" + }, + "dependencies": { + "@superviz/room": "workspace:*", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "peerDependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "yjs": ">=13.6.20" + }, + "devDependencies": { + "@anolilab/semantic-release-pnpm": "^1.1.3", + "@eslint/compat": "^1.1.1", + "@rollup/plugin-replace": "^6.0.1", + "@semantic-release/release-notes-generator": "^14.0.1", + "@types/node": "^20.14.9", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@typescript-eslint/eslint-plugin": "^7.14.1", + "@typescript-eslint/parser": "^8.17.0", + "@vitejs/plugin-react": "^4.3.1", + "@vitejs/plugin-react-swc": "^3.7.0", + "eslint": "^9.6.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", + "eslint-plugin-react": "^7.34.3", + "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-react-refresh": "^0.4.7", + "eslint-plugin-simple-import-sort": "^12.1.0", + "glob": "^10.4.2", + "husky": "^9.0.11", + "lint-staged": "^15.2.7", + "prettier": "^3.3.2", + "react-hooks": "^1.0.1", + "semantic-release": "^24.0.0", + "typescript": "^5.6.2", + "vite": "^5.4.6", + "vite-plugin-dts": "^3.9.1", + "vite-plugin-linter": "^2.1.1", + "vite-tsconfig-paths": "^4.3.2" + } +} diff --git a/packages/react-room/src/contexts/room.tsx b/packages/react-room/src/contexts/room.tsx new file mode 100644 index 00000000..2574c50c --- /dev/null +++ b/packages/react-room/src/contexts/room.tsx @@ -0,0 +1,156 @@ +import React, { + createContext, + useContext, + ReactNode, + useCallback, + useMemo, + useRef +} from 'react'; + +interface RoomCallbacks { + onMyParticipantJoined?: (participant: any) => void; + onMyParticipantLeft?: (participant: any) => void; + onMyParticipantUpdated?: (participant: any) => void; + onParticipantJoined?: (participant: any) => void; + onParticipantLeft?: (participant: any) => void; + onParticipantUpdated?: (participant: any) => void; + onError?: (error: any) => void; + onRoomUpdated?: (data: any) => void; +} + +interface RoomContextInternalProps { + joinRoom: (options: any) => Promise; + leaveRoom: () => void; + addComponent: (component: any) => void; + removeComponent: (component: any) => void; + setCallbacks: (callbacks: RoomCallbacks) => void; +} + +const RoomContext = createContext(undefined); + +const RoomProvider: React.FC<{ + children: ReactNode + developerToken: string +}> = ({ children }) => { + const callbacks = useRef({}); + + const joinRoom = useCallback(async (options: any) => { + console.log('Joining room with options', options, callbacks); + }, [callbacks]); + + const leaveRoom = useCallback(() => { + console.log('Leaving room'); + }, []); + + const addComponent = useCallback(() => { + console.log('Adding component'); + }, []); + + const removeComponent = useCallback(() => { + console.log('Removing component'); + }, []); + + const updateCallbacks = useCallback((newCallbacks: RoomCallbacks) => { + callbacks.current = newCallbacks; + }, []); + + const onMyParticipantJoined = useCallback((participant: any) => { + console.log('My participant joined'); + + if(callbacks.current.onMyParticipantJoined) { + callbacks.current.onMyParticipantJoined(participant); + } + }, [callbacks.current]); + + const onMyParticipantLeft = useCallback((participant: any) => { + console.log('My participant left'); + + if(callbacks.current.onMyParticipantLeft) { + callbacks.current.onMyParticipantLeft(participant); + } + }, [callbacks.current]); + + const onMyParticipantUpdated = useCallback((participant: any) => { + console.log('My participant updated'); + + if(callbacks.current.onMyParticipantUpdated) { + callbacks.current.onMyParticipantUpdated(participant); + } + }, [callbacks.current]); + const onParticipantJoined = useCallback((participant: any) => { + console.log('Participant joined'); + + if(callbacks.current.onParticipantJoined) { + callbacks.current.onParticipantJoined(participant); + } + }, [callbacks.current]); + + const onParticipantLeft = useCallback((participant: any) => { + console.log('Participant left'); + + if(callbacks.current.onParticipantLeft) { + callbacks.current.onParticipantLeft(participant); + } + }, [callbacks.current]); + + const onParticipantUpdated = useCallback((participant: any) => { + console.log('Participant updated'); + + if(callbacks.current.onParticipantUpdated) { + callbacks.current.onParticipantUpdated(participant); + } + }, [callbacks.current]); + + const onError = useCallback((error: any) => { + console.log('Error'); + + if(callbacks.current.onError) { + callbacks.current.onError(error); + } + }, [callbacks.current]); + + const onRoomUpdated = useCallback((data: any) => { + console.log('Room updated'); + + if(callbacks.current.onRoomUpdated) { + callbacks.current.onRoomUpdated(data); + } + }, [callbacks.current]); + + return ( + + {children} + + ); +}; + +const useRoom = (callbacks: RoomCallbacks) => { + const context = useContext(RoomContext) + + if (context === undefined) { + throw new Error('useRoom must be used within a RoomProvider'); + } + + if(Object.keys(callbacks).length) { + context.setCallbacks(callbacks); + } + + return useMemo(() => { + return { + joinRoom: context.joinRoom, + leaveRoom: context.leaveRoom, + addComponent: context.addComponent, + removeComponent: context.removeComponent + } + }, [context]); +}; + +export { RoomProvider, useRoom }; \ No newline at end of file diff --git a/packages/react-room/src/index.ts b/packages/react-room/src/index.ts new file mode 100644 index 00000000..48b2fcde --- /dev/null +++ b/packages/react-room/src/index.ts @@ -0,0 +1,3 @@ + + +export { RoomProvider, useRoom} from './contexts/room'; \ No newline at end of file diff --git a/packages/react-room/src/vite-env.d.ts b/packages/react-room/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/packages/react-room/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/react-room/tsconfig.json b/packages/react-room/tsconfig.json new file mode 100644 index 00000000..4a0d9637 --- /dev/null +++ b/packages/react-room/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "allowJs": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "declaration": true, + "skipLibCheck": true, + "esModuleInterop": true , + "declarationMap": true, + "baseUrl": ".", + "paths": { + "react-vite-library": [ "src/component/index.ts" ] + }, + "typeRoots": [ + "node_modules/@types", + "src/component/index.d.ts" + ] + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/packages/react-room/tsconfig.node.json b/packages/react-room/tsconfig.node.json new file mode 100644 index 00000000..bdf5d669 --- /dev/null +++ b/packages/react-room/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "Node", + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + }, + "include": ["vite.config.ts","package.json"], +} \ No newline at end of file diff --git a/packages/react-room/vite.config.ts b/packages/react-room/vite.config.ts new file mode 100644 index 00000000..deee6f5a --- /dev/null +++ b/packages/react-room/vite.config.ts @@ -0,0 +1,38 @@ +import path from 'node:path'; + +import react from '@vitejs/plugin-react-swc'; +import { defineConfig } from 'vite'; +import dts from 'vite-plugin-dts'; +import tsConfigPaths from 'vite-tsconfig-paths'; + +import { peerDependencies } from './package.json'; +import replace from '@rollup/plugin-replace'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + react(), + tsConfigPaths(), + dts({ + include: ['src/**/*.{ts,tsx}'], + exclude: ['**/*.spec.ts', '**/*.spec.tsx', 'src/main.tsx', 'src/demo.tsx'], + }), + replace({ + 'Object.defineProperty(exports, "__esModule", { value: true });': + 'Object.defineProperty(exports || {}, "__esModule", { value: true });', + delimiters: ['\n', '\n'], + preventAssignment: true, + }), + ], + build: { + lib: { + entry: path.resolve('src', 'index.ts'), + name: 'superviz-sdk-react', + formats: ['es', 'cjs'], + fileName: (format) => `superviz-sdk-react.${format}.js`, + }, + rollupOptions: { + external: Object.keys(peerDependencies), + }, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c5e067e3..cbb188a0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: '@superviz/matterport-plugin': specifier: workspace:* version: link:../../packages/matterport + '@superviz/react': + specifier: workspace:* + version: link:../../packages/react-room '@superviz/react-sdk': specifier: workspace:* version: link:../../packages/react-sdk @@ -312,6 +315,109 @@ importers: specifier: ^5.6.2 version: 5.6.2 + packages/react-room: + dependencies: + '@superviz/room': + specifier: workspace:* + version: link:../room + react: + specifier: ^18.2.0 + version: 18.3.1 + react-dom: + specifier: ^18.2.0 + version: 18.3.1(react@18.3.1) + yjs: + specifier: '>=13.6.20' + version: 13.6.20 + devDependencies: + '@anolilab/semantic-release-pnpm': + specifier: ^1.1.3 + version: 1.1.3(yaml@2.5.1) + '@eslint/compat': + specifier: ^1.1.1 + version: 1.1.1 + '@rollup/plugin-replace': + specifier: ^6.0.1 + version: 6.0.1(rollup@4.21.3) + '@semantic-release/release-notes-generator': + specifier: ^14.0.1 + version: 14.0.1(semantic-release@24.0.0(typescript@5.6.2)) + '@types/node': + specifier: ^20.14.9 + version: 20.16.5 + '@types/react': + specifier: ^18.3.3 + version: 18.3.5 + '@types/react-dom': + specifier: ^18.3.0 + version: 18.3.0 + '@typescript-eslint/eslint-plugin': + specifier: ^7.14.1 + version: 7.18.0(@typescript-eslint/parser@8.17.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2))(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2) + '@typescript-eslint/parser': + specifier: ^8.17.0 + version: 8.17.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2) + '@vitejs/plugin-react': + specifier: ^4.3.1 + version: 4.3.1(vite@5.4.6(@types/node@20.16.5)(terser@5.32.0)) + '@vitejs/plugin-react-swc': + specifier: ^3.7.0 + version: 3.7.0(vite@5.4.6(@types/node@20.16.5)(terser@5.32.0)) + eslint: + specifier: ^9.6.0 + version: 9.10.0(jiti@1.21.6) + eslint-config-prettier: + specifier: ^9.1.0 + version: 9.1.0(eslint@9.10.0(jiti@1.21.6)) + eslint-plugin-prettier: + specifier: ^5.1.3 + version: 5.2.1(@types/eslint@8.56.2)(eslint-config-prettier@9.1.0(eslint@9.10.0(jiti@1.21.6)))(eslint@9.10.0(jiti@1.21.6))(prettier@3.3.3) + eslint-plugin-react: + specifier: ^7.34.3 + version: 7.36.1(eslint@9.10.0(jiti@1.21.6)) + eslint-plugin-react-hooks: + specifier: ^4.6.2 + version: 4.6.2(eslint@9.10.0(jiti@1.21.6)) + eslint-plugin-react-refresh: + specifier: ^0.4.7 + version: 0.4.11(eslint@9.10.0(jiti@1.21.6)) + eslint-plugin-simple-import-sort: + specifier: ^12.1.0 + version: 12.1.1(eslint@9.10.0(jiti@1.21.6)) + glob: + specifier: ^10.4.2 + version: 10.4.5 + husky: + specifier: ^9.0.11 + version: 9.1.6 + lint-staged: + specifier: ^15.2.7 + version: 15.2.10 + prettier: + specifier: ^3.3.2 + version: 3.3.3 + react-hooks: + specifier: ^1.0.1 + version: 1.0.1 + semantic-release: + specifier: ^24.0.0 + version: 24.0.0(typescript@5.6.2) + typescript: + specifier: ^5.6.2 + version: 5.6.2 + vite: + specifier: ^5.4.6 + version: 5.4.6(@types/node@20.16.5)(terser@5.32.0) + vite-plugin-dts: + specifier: ^3.9.1 + version: 3.9.1(@types/node@20.16.5)(rollup@4.21.3)(typescript@5.6.2)(vite@5.4.6(@types/node@20.16.5)(terser@5.32.0)) + vite-plugin-linter: + specifier: ^2.1.1 + version: 2.1.1(rollup@4.21.3) + vite-tsconfig-paths: + specifier: ^4.3.2 + version: 4.3.2(typescript@5.6.2)(vite@5.4.6(@types/node@20.16.5)(terser@5.32.0)) + packages/react-sdk: dependencies: '@superviz/autodesk-viewer-plugin': From 101486039726d9884f2f33bfc6712b039f7d5dd1 Mon Sep 17 00:00:00 2001 From: Carlos Date: Mon, 20 Jan 2025 15:02:53 -0300 Subject: [PATCH 2/4] feat: initialize superviz/room with superviz/react --- packages/react-room/src/contexts/room.tsx | 126 ++++++++++++++++------ 1 file changed, 96 insertions(+), 30 deletions(-) diff --git a/packages/react-room/src/contexts/room.tsx b/packages/react-room/src/contexts/room.tsx index 2574c50c..7367be7f 100644 --- a/packages/react-room/src/contexts/room.tsx +++ b/packages/react-room/src/contexts/room.tsx @@ -1,21 +1,52 @@ +import { Participant, Room, RoomState, createRoom } from '@superviz/room' + import React, { createContext, useContext, ReactNode, useCallback, useMemo, - useRef + useRef, + useState } from 'react'; +type InitializeRoomParams = { + roomId: string; + participant: { + id: string; + name?: string; + email?: string; + avatar?: { + model3DUrl?: string; + imageUrl?: string; + }; + }; + group: { + id: string; + name: string; + }; + debug?: boolean; + environment?: 'dev' | 'prod'; +} + +type RoomError = { + code: string, + message: string +} + +type RoomUpdate = { + status: RoomState | `${RoomState}` +} + interface RoomCallbacks { - onMyParticipantJoined?: (participant: any) => void; - onMyParticipantLeft?: (participant: any) => void; - onMyParticipantUpdated?: (participant: any) => void; - onParticipantJoined?: (participant: any) => void; - onParticipantLeft?: (participant: any) => void; - onParticipantUpdated?: (participant: any) => void; - onError?: (error: any) => void; - onRoomUpdated?: (data: any) => void; + onMyParticipantJoined?: (participant: Participant) => void; + onMyParticipantLeft?: (participant: Participant) => void; + onMyParticipantUpdated?: (participant: Participant) => void; + onParticipantJoined?: (participant: Participant) => void; + onParticipantLeft?: (participant: Participant) => void; + onParticipantUpdated?: (participant: Participant) => void; + onError?: (error: RoomError) => void; + onRoomUpdated?: (data: RoomUpdate) => void; } interface RoomContextInternalProps { @@ -31,15 +62,50 @@ const RoomContext = createContext(undefine const RoomProvider: React.FC<{ children: ReactNode developerToken: string -}> = ({ children }) => { +}> = ({ children, developerToken }) => { const callbacks = useRef({}); + const room = useRef(null); + const initialized = useRef(false); - const joinRoom = useCallback(async (options: any) => { - console.log('Joining room with options', options, callbacks); - }, [callbacks]); + const joinRoom = useCallback(async (options: InitializeRoomParams) => { + if(initialized.current) { + console.warn('[SuperViz] Room already initialized, leaving room before joining again'); + return; + } + + initialized.current = true; + + room.current = await createRoom( + Object.assign({}, options, { developerToken }) + ); + + room.current.subscribe('my-participant.joined', onMyParticipantJoined); + room.current.subscribe('my-participant.left', onMyParticipantLeft); + room.current.subscribe('my-participant.updated', onMyParticipantUpdated); + room.current.subscribe('participant.joined', onParticipantJoined); + room.current.subscribe('participant.left', onParticipantLeft); + room.current.subscribe('participant.updated', onParticipantUpdated); + room.current.subscribe('room.error', onError); + room.current.subscribe('room.update', onRoomUpdated); + }, [callbacks, initialized]); const leaveRoom = useCallback(() => { - console.log('Leaving room'); + if(!initialized.current) { + console.warn('[SuperViz] Room not initialized, nothing to leave'); + return; + } + + room.current?.leave(); + room.current?.unsubscribe('my-participant.joined', onMyParticipantJoined); + room.current?.unsubscribe('my-participant.left', onMyParticipantLeft); + room.current?.unsubscribe('my-participant.updated', onMyParticipantUpdated); + room.current?.unsubscribe('participant.joined', onParticipantJoined); + room.current?.unsubscribe('participant.left', onParticipantLeft); + room.current?.unsubscribe('participant.updated', onParticipantUpdated); + room.current?.unsubscribe('room.error', onError); + room.current?.unsubscribe('room.update', onRoomUpdated); + + initialized.current = false; }, []); const addComponent = useCallback(() => { @@ -54,63 +120,63 @@ const RoomProvider: React.FC<{ callbacks.current = newCallbacks; }, []); - const onMyParticipantJoined = useCallback((participant: any) => { - console.log('My participant joined'); + const onMyParticipantJoined = useCallback((participant: Participant) => { + console.log('My participant joined', participant); if(callbacks.current.onMyParticipantJoined) { callbacks.current.onMyParticipantJoined(participant); } }, [callbacks.current]); - const onMyParticipantLeft = useCallback((participant: any) => { - console.log('My participant left'); + const onMyParticipantLeft = useCallback((participant: Participant) => { + console.log('My participant left', participant); if(callbacks.current.onMyParticipantLeft) { callbacks.current.onMyParticipantLeft(participant); } }, [callbacks.current]); - const onMyParticipantUpdated = useCallback((participant: any) => { - console.log('My participant updated'); + const onMyParticipantUpdated = useCallback((participant: Participant) => { + console.log('My participant updated', participant); if(callbacks.current.onMyParticipantUpdated) { callbacks.current.onMyParticipantUpdated(participant); } }, [callbacks.current]); - const onParticipantJoined = useCallback((participant: any) => { - console.log('Participant joined'); + const onParticipantJoined = useCallback((participant: Participant) => { + console.log('Participant joined', participant); if(callbacks.current.onParticipantJoined) { callbacks.current.onParticipantJoined(participant); } }, [callbacks.current]); - const onParticipantLeft = useCallback((participant: any) => { - console.log('Participant left'); + const onParticipantLeft = useCallback((participant: Participant) => { + console.log('Participant left', participant); if(callbacks.current.onParticipantLeft) { callbacks.current.onParticipantLeft(participant); } }, [callbacks.current]); - const onParticipantUpdated = useCallback((participant: any) => { - console.log('Participant updated'); + const onParticipantUpdated = useCallback((participant: Participant) => { + console.log('Participant updated', participant); if(callbacks.current.onParticipantUpdated) { callbacks.current.onParticipantUpdated(participant); } }, [callbacks.current]); - const onError = useCallback((error: any) => { - console.log('Error'); + const onError = useCallback((error: RoomError) => { + console.log('Error', error); if(callbacks.current.onError) { callbacks.current.onError(error); } }, [callbacks.current]); - const onRoomUpdated = useCallback((data: any) => { - console.log('Room updated'); + const onRoomUpdated = useCallback((data: RoomUpdate) => { + console.log('Room updated', data); if(callbacks.current.onRoomUpdated) { callbacks.current.onRoomUpdated(data); From 1e26f9932df6be11df289a049270c629ebe4e0fb Mon Sep 17 00:00:00 2001 From: Carlos Date: Tue, 21 Jan 2025 08:59:41 -0300 Subject: [PATCH 3/4] feat: handle room callbacks --- packages/react-room/src/contexts/room.tsx | 105 +++++++++++++--------- 1 file changed, 65 insertions(+), 40 deletions(-) diff --git a/packages/react-room/src/contexts/room.tsx b/packages/react-room/src/contexts/room.tsx index 7367be7f..c91cd287 100644 --- a/packages/react-room/src/contexts/room.tsx +++ b/packages/react-room/src/contexts/room.tsx @@ -7,7 +7,8 @@ import React, { useCallback, useMemo, useRef, - useState + useState, + useEffect } from 'react'; type InitializeRoomParams = { @@ -49,6 +50,8 @@ interface RoomCallbacks { onRoomUpdated?: (data: RoomUpdate) => void; } +type Callback = (participant: Participant | RoomError | RoomUpdate) => void; + interface RoomContextInternalProps { joinRoom: (options: any) => Promise; leaveRoom: () => void; @@ -63,7 +66,16 @@ const RoomProvider: React.FC<{ children: ReactNode developerToken: string }> = ({ children, developerToken }) => { - const callbacks = useRef({}); + const callbacks = useRef>({ + onMyParticipantJoined: [], + onMyParticipantLeft: [], + onMyParticipantUpdated: [], + onParticipantJoined: [], + onParticipantLeft: [], + onParticipantUpdated: [], + onError: [], + onRoomUpdated: [], + }); const room = useRef(null); const initialized = useRef(false); @@ -104,6 +116,16 @@ const RoomProvider: React.FC<{ room.current?.unsubscribe('participant.updated', onParticipantUpdated); room.current?.unsubscribe('room.error', onError); room.current?.unsubscribe('room.update', onRoomUpdated); + callbacks.current = { + onMyParticipantJoined: [], + onMyParticipantLeft: [], + onMyParticipantUpdated: [], + onParticipantJoined: [], + onParticipantLeft: [], + onParticipantUpdated: [], + onError: [], + onRoomUpdated: [], + } initialized.current = false; }, []); @@ -116,70 +138,73 @@ const RoomProvider: React.FC<{ console.log('Removing component'); }, []); - const updateCallbacks = useCallback((newCallbacks: RoomCallbacks) => { - callbacks.current = newCallbacks; - }, []); + const updateCallbacks = (newCallbacks: RoomCallbacks) => { + Object.keys(newCallbacks).forEach((key) => { + const callbackKey = key as keyof RoomCallbacks; + + if (callbacks.current[callbackKey]) { + const existingCallbacks = callbacks.current[callbackKey]; + + const callbackMap: Map = new Map( + existingCallbacks.map((cb) => [cb.toString(), cb]) + ); + + const newCallback = newCallbacks[callbackKey] as Callback; + + if (newCallback && !callbackMap.has(newCallback.toString())) { + callbackMap.set(newCallback.toString(), newCallback); + callbacks.current[callbackKey] = Array.from(callbackMap.values()); + } + } + }); + + }; const onMyParticipantJoined = useCallback((participant: Participant) => { - console.log('My participant joined', participant); - if(callbacks.current.onMyParticipantJoined) { - callbacks.current.onMyParticipantJoined(participant); + callbacks.current.onMyParticipantJoined.forEach(cb => cb(participant)); } }, [callbacks.current]); const onMyParticipantLeft = useCallback((participant: Participant) => { - console.log('My participant left', participant); - if(callbacks.current.onMyParticipantLeft) { - callbacks.current.onMyParticipantLeft(participant); + callbacks.current.onMyParticipantLeft.forEach(cb => cb(participant)); } }, [callbacks.current]); const onMyParticipantUpdated = useCallback((participant: Participant) => { - console.log('My participant updated', participant); - if(callbacks.current.onMyParticipantUpdated) { - callbacks.current.onMyParticipantUpdated(participant); + callbacks.current.onMyParticipantUpdated.forEach(cb => cb(participant)); } }, [callbacks.current]); - const onParticipantJoined = useCallback((participant: Participant) => { - console.log('Participant joined', participant); + const onParticipantJoined = useCallback((participant: Participant) => { if(callbacks.current.onParticipantJoined) { - callbacks.current.onParticipantJoined(participant); + callbacks.current.onParticipantJoined.forEach(cb => cb(participant)); } }, [callbacks.current]); const onParticipantLeft = useCallback((participant: Participant) => { - console.log('Participant left', participant); - if(callbacks.current.onParticipantLeft) { - callbacks.current.onParticipantLeft(participant); + callbacks.current.onParticipantLeft.forEach(cb => cb(participant)); } }, [callbacks.current]); const onParticipantUpdated = useCallback((participant: Participant) => { - console.log('Participant updated', participant); - if(callbacks.current.onParticipantUpdated) { - callbacks.current.onParticipantUpdated(participant); + callbacks.current.onParticipantUpdated.forEach(cb => cb(participant)); } }, [callbacks.current]); const onError = useCallback((error: RoomError) => { - console.log('Error', error); - if(callbacks.current.onError) { - callbacks.current.onError(error); + callbacks.current.onError.forEach(cb => cb(error)); } }, [callbacks.current]); const onRoomUpdated = useCallback((data: RoomUpdate) => { - console.log('Room updated', data); - if(callbacks.current.onRoomUpdated) { - callbacks.current.onRoomUpdated(data); + callbacks.current.onRoomUpdated.forEach(cb => cb(data)); } }, [callbacks.current]); @@ -205,18 +230,18 @@ const useRoom = (callbacks: RoomCallbacks) => { throw new Error('useRoom must be used within a RoomProvider'); } - if(Object.keys(callbacks).length) { - context.setCallbacks(callbacks); - } - - return useMemo(() => { - return { - joinRoom: context.joinRoom, - leaveRoom: context.leaveRoom, - addComponent: context.addComponent, - removeComponent: context.removeComponent + useMemo(() => { + if(Object.keys(callbacks).length) { + context.setCallbacks(callbacks); } - }, [context]); + }, [callbacks]) + + return { + joinRoom: context.joinRoom, + leaveRoom: context.leaveRoom, + addComponent: context.addComponent, + removeComponent: context.removeComponent + } }; export { RoomProvider, useRoom }; \ No newline at end of file From 03ec599fcd4c1bcf12a3683a09e31b6239bec57a Mon Sep 17 00:00:00 2001 From: Carlos Date: Tue, 21 Jan 2025 11:01:27 -0300 Subject: [PATCH 4/4] feat: add and remove components from room --- .../src/pages/superviz-react-room.tsx | 24 +++++++-- packages/react-room/src/contexts/room.tsx | 51 ++++++++++++++----- 2 files changed, 58 insertions(+), 17 deletions(-) diff --git a/apps/playground/src/pages/superviz-react-room.tsx b/apps/playground/src/pages/superviz-react-room.tsx index a1d52aca..99563a4d 100644 --- a/apps/playground/src/pages/superviz-react-room.tsx +++ b/apps/playground/src/pages/superviz-react-room.tsx @@ -1,15 +1,17 @@ import { RoomProvider, useRoom } from '@superviz/react' +import { VideoConference } from '@superviz/sdk'; import { getConfig } from '../config'; import { v4 as generateId } from "uuid"; +import { useEffect } from 'react'; const SUPERVIZ_KEY = getConfig("keys.superviz"); const SUPERVIZ_ROOM_PREFIX = getConfig("roomPrefix"); const componentName = "new-room-react"; const uuid = generateId(); export const Children = () => { - const { joinRoom, leaveRoom } = useRoom({ + const { room, joinRoom, leaveRoom, addComponent } = useRoom({ onMyParticipantJoined: (participant) => console.log('Component: My participant joined', participant), onMyParticipantLeft: (participant) => console.log('Component: My participant left', participant), onMyParticipantUpdated: (participant) => console.log('Component: My participant updated', participant), @@ -39,16 +41,28 @@ export const Children = () => { debug: true, environment: 'dev', }); + + const video = new VideoConference({ + collaborationMode: { enabled: false }, + participantType: 'host' + }); + + addComponent(video); }; + useEffect(() => { + handleJoin(); + + return () => leaveRoom(); + }, []) + return (
- - + +
+
); - - return <> }; export function SupervizReactRoom() { diff --git a/packages/react-room/src/contexts/room.tsx b/packages/react-room/src/contexts/room.tsx index c91cd287..f32e05dc 100644 --- a/packages/react-room/src/contexts/room.tsx +++ b/packages/react-room/src/contexts/room.tsx @@ -1,4 +1,5 @@ import { Participant, Room, RoomState, createRoom } from '@superviz/room' +import type { Component } from '@superviz/room/dist/common/types/component.types'; import React, { createContext, @@ -6,9 +7,7 @@ import React, { ReactNode, useCallback, useMemo, - useRef, - useState, - useEffect + useRef, } from 'react'; type InitializeRoomParams = { @@ -53,11 +52,12 @@ interface RoomCallbacks { type Callback = (participant: Participant | RoomError | RoomUpdate) => void; interface RoomContextInternalProps { - joinRoom: (options: any) => Promise; + joinRoom: (options: InitializeRoomParams) => Promise; leaveRoom: () => void; - addComponent: (component: any) => void; - removeComponent: (component: any) => void; + addComponent: (component: unknown) => void; + removeComponent: (component: unknown) => void; setCallbacks: (callbacks: RoomCallbacks) => void; + room: Room | null; } const RoomContext = createContext(undefined); @@ -66,6 +66,7 @@ const RoomProvider: React.FC<{ children: ReactNode developerToken: string }> = ({ children, developerToken }) => { + const components = useRef>(new Map()); const callbacks = useRef>({ onMyParticipantJoined: [], onMyParticipantLeft: [], @@ -130,12 +131,36 @@ const RoomProvider: React.FC<{ initialized.current = false; }, []); - const addComponent = useCallback(() => { - console.log('Adding component'); + const addComponent = useCallback((component: unknown) => { + if(!initialized.current) { + console.warn('[SuperViz] Room not initialized, cannot add component'); + return; + } + + if(components.current.has((component as Component).name)) { + console.warn('[SuperViz] Component already initialized, cannot add again'); + return; + } + + components.current.set((component as Component).name, component); + + room.current?.addComponent(component); }, []); - const removeComponent = useCallback(() => { - console.log('Removing component'); + const removeComponent = useCallback((component: unknown) => { + if(!initialized.current) { + console.warn('[SuperViz] Room not initialized, cannot remove component'); + return; + } + + if(!components.current.has((component as Component).name)) { + console.warn('[SuperViz] Component not initialized yet, cannot remove'); + return; + } + + components.current.delete((component as Component).name); + + room.current?.removeComponent(component); }, []); const updateCallbacks = (newCallbacks: RoomCallbacks) => { @@ -215,7 +240,8 @@ const RoomProvider: React.FC<{ leaveRoom, setCallbacks: updateCallbacks, addComponent, - removeComponent + removeComponent, + room: room.current }} > {children} @@ -240,7 +266,8 @@ const useRoom = (callbacks: RoomCallbacks) => { joinRoom: context.joinRoom, leaveRoom: context.leaveRoom, addComponent: context.addComponent, - removeComponent: context.removeComponent + removeComponent: context.removeComponent, + room: context.room } };