diff --git a/debian/changelog b/debian/changelog index 58d477a..d966b39 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,5 +1,5 @@ -bbb-plugin-template (0.1.0) jammy; urgency=medium +bbb-plugin-reaction-stack (0.0.1) jammy; urgency=medium * initial build - -- Firstname Lastname Thu, 04 Jul 2024 14:56:18 -0400 + -- João Victor Thu, 04 Jul 2024 14:56:18 -0400 diff --git a/debian/control b/debian/control index ed35082..5ecac7b 100644 --- a/debian/control +++ b/debian/control @@ -1,15 +1,12 @@ -Source: bbb-plugin-template +Source: bbb-plugin-reaction-stack Section: web Priority: extra -Maintainer: Firstname Lastname +Maintainer: João Victor Build-Depends: debhelper (>= 13), nodejs (>= 18) Standards-Version: 4.1.4 -Homepage: https://github.com/bigbluebutton/plugin-template +Homepage: https://github.com/bigbluebutton/bbb-plugin-reaction-stack -Package: bbb-plugin-template +Package: bbb-plugin-reaction-stack Architecture: all Depends: ${misc:Depends}, nodejs -Description: Share a webpage with all session participants - An official BigBlueButton plugin which allows - the presenter to display a web page to - all viewers inside of a session. +Description: Renders users reactions in stack-like list below the navbar. diff --git a/debian/copyright b/debian/copyright index a6d5b06..b953be0 100644 --- a/debian/copyright +++ b/debian/copyright @@ -1,5 +1,5 @@ Format: http://dep.debian.net/deps/dep5 -Upstream-Name: bbb-plugin-template +Upstream-Name: bbb-plugin-reaction-stack Files: * Copyright: 2024 BigBlueButton Inc. and by respective authors diff --git a/debian/rules b/debian/rules index 513df94..c904203 100755 --- a/debian/rules +++ b/debian/rules @@ -8,5 +8,5 @@ override_dh_auto_build: npm run build-bundle override_dh_auto_install: - install -d debian/bbb-plugin-template/var/www/bigbluebutton-default/assets/plugins/bbb-plugin-template - cp -r dist/* debian/bbb-plugin-template/var/www/bigbluebutton-default/assets/plugins/bbb-plugin-template + install -d debian/bbb-plugin-reaction-stack/var/www/bigbluebutton-default/assets/plugins/bbb-plugin-reaction-stack + cp -r dist/* debian/bbb-plugin-reaction-stack/var/www/bigbluebutton-default/assets/plugins/bbb-plugin-reaction-stack diff --git a/manifest.json b/manifest.json index 3a8e704..0f51f63 100644 --- a/manifest.json +++ b/manifest.json @@ -1,6 +1,6 @@ { - "requiredSdkVersion": "~0.0.73", - "name": "", - "javascriptEntrypointUrl": ".js", - "localesBaseUrl": "locales" + "requiredSdkVersion": "~0.0.91", + "version": "0.0.1", + "name": "BbbPluginReactionStack", + "javascriptEntrypointUrl": "BbbPluginReactionStack.js" } diff --git a/package-lock.json b/package-lock.json index 7cbee4f..e47045f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,19 @@ { - "name": "", + "name": "bbb-plugin-reaction-stack", "version": "0.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "", + "name": "bbb-plugin-reaction-stack", "version": "0.0.1", "dependencies": { + "@lottiefiles/dotlottie-react": "^0.17.7", "babel-plugin-syntax-dynamic-import": "^6.18.0", - "bigbluebutton-html-plugin-sdk": "0.0.73", + "bigbluebutton-html-plugin-sdk": "^0.0.91", + "motion": "^12.23.24", "path": "^0.12.7", + "radash": "^12.1.1", "react": "^18.2.0", "react-dom": "^18.2.0", "styled-components": "^5.3.3" @@ -127,6 +130,7 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.0", @@ -1663,6 +1667,7 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz", "integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==", + "peer": true, "dependencies": { "@emotion/memoize": "^0.9.0" } @@ -1914,6 +1919,24 @@ "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", "dev": true }, + "node_modules/@lottiefiles/dotlottie-react": { + "version": "0.17.7", + "resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-react/-/dotlottie-react-0.17.7.tgz", + "integrity": "sha512-A6wO3zqkDx/t0ULfctcr1Bmb1f1hc4zUV3NcbKQOsBGAOIx1vABV/fRabFYElvbJl9lmOR24yMh//Z0fvvJV+Q==", + "license": "MIT", + "dependencies": { + "@lottiefiles/dotlottie-web": "0.56.0" + }, + "peerDependencies": { + "react": "^17 || ^18 || ^19" + } + }, + "node_modules/@lottiefiles/dotlottie-web": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-web/-/dotlottie-web-0.56.0.tgz", + "integrity": "sha512-bWHRIGzjZs3Hjkz0JRsCMX2ya9a1tGU4atdrlfM3UoN0iamsDE64kSCMfGuchCwGAxg0xEh84CkF+SVV1NU9ow==", + "license": "MIT" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2137,6 +2160,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz", "integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==", "devOptional": true, + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -2283,6 +2307,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/types": "6.21.0", @@ -2727,6 +2752,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2770,6 +2796,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -3210,9 +3237,10 @@ "dev": true }, "node_modules/bigbluebutton-html-plugin-sdk": { - "version": "0.0.73", - "resolved": "https://registry.npmjs.org/bigbluebutton-html-plugin-sdk/-/bigbluebutton-html-plugin-sdk-0.0.73.tgz", - "integrity": "sha512-upf5Np45+F26qlad1ZUDJFFyAFyPHF3cU81jskEUq2VoEzUCX8zjBVvIsksut79Slz/B622UbX8blGTm+s7/lw==", + "version": "0.0.91", + "resolved": "https://registry.npmjs.org/bigbluebutton-html-plugin-sdk/-/bigbluebutton-html-plugin-sdk-0.0.91.tgz", + "integrity": "sha512-kB4iky35Fb3HZL2dk7FYPz39W2b+AVObNgTCmde1rSRVzI8ki1k87uff+bWey8jYOzD7+jbUTx9u9QExoIvWRA==", + "license": "LGPL-3.0", "dependencies": { "@apollo/client": "^3.8.7", "@browser-bunyan/console-formatted-stream": "^1.8.0", @@ -3334,6 +3362,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001669", "electron-to-chromium": "^1.5.41", @@ -4123,6 +4152,7 @@ "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", "dev": true, + "peer": true, "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" @@ -4348,6 +4378,7 @@ "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -4489,6 +4520,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", "dev": true, + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.8", @@ -4565,6 +4597,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", "dev": true, + "peer": true, "dependencies": { "aria-query": "^5.3.2", "array-includes": "^3.1.8", @@ -4625,6 +4658,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.2.tgz", "integrity": "sha512-EsTAnj9fLVr/GZleBLFbj/sSuXeWmp1eXIN60ceYnZveqEaUCyW4X+Vh4WTdUhCkW4xutXYqTXCUSyqD4rB75w==", "dev": true, + "peer": true, "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", @@ -4657,6 +4691,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", "dev": true, + "peer": true, "engines": { "node": ">=10" }, @@ -5224,6 +5259,33 @@ "node": ">= 0.6" } }, + "node_modules/framer-motion": { + "version": "12.23.24", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.24.tgz", + "integrity": "sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.23.23", + "motion-utils": "^12.23.6", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -6968,6 +7030,47 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/motion": { + "version": "12.23.24", + "resolved": "https://registry.npmjs.org/motion/-/motion-12.23.24.tgz", + "integrity": "sha512-Rc5E7oe2YZ72N//S3QXGzbnXgqNrTESv8KKxABR20q2FLch9gHLo0JLyYo2hZ238bZ9Gx6cWhj9VO0IgwbMjCw==", + "license": "MIT", + "dependencies": { + "framer-motion": "^12.23.24", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/motion-dom": { + "version": "12.23.23", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz", + "integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.23.6" + } + }, + "node_modules/motion-utils": { + "version": "12.23.6", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", + "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -7590,6 +7693,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.0", @@ -7780,6 +7884,15 @@ } ] }, + "node_modules/radash": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/radash/-/radash-12.1.1.tgz", + "integrity": "sha512-h36JMxKRqrAxVD8201FrCpyeNuUY9Y5zZwujr20fFO77tpUtGa6EZzfKw/3WaiBX95fq7+MpsuMLNdSnORAwSA==", + "license": "MIT", + "engines": { + "node": ">=14.18.0" + } + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -7817,6 +7930,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -7828,6 +7942,7 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" @@ -7839,8 +7954,7 @@ "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "peer": true + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" }, "node_modules/readable-stream": { "version": "3.6.2", @@ -8253,6 +8367,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -8897,6 +9012,7 @@ "version": "5.3.11", "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.11.tgz", "integrity": "sha512-uuzIIfnVkagcVHv9nE0VPlHPSCmXIUGKfJ42LNjxCCTDTL5sgnJ8Z7GZBq0EnLYGln77tPpEpExt2+qa+cZqSw==", + "peer": true, "dependencies": { "@babel/helper-module-imports": "^7.0.0", "@babel/traverse": "^7.4.5", @@ -9296,6 +9412,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -9512,6 +9629,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.95.0.tgz", "integrity": "sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q==", "dev": true, + "peer": true, "dependencies": { "@types/estree": "^1.0.5", "@webassemblyjs/ast": "^1.12.1", @@ -9558,6 +9676,7 @@ "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "dev": true, + "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1", diff --git a/package.json b/package.json index d4ff611..85efcf5 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,15 @@ { - "name": "", + "name": "bbb-plugin-reaction-stack", "version": "0.0.1", "private": true, "main": "./src/index.tsx", "dependencies": { + "@lottiefiles/dotlottie-react": "^0.17.7", "babel-plugin-syntax-dynamic-import": "^6.18.0", - "bigbluebutton-html-plugin-sdk": "0.0.73", + "bigbluebutton-html-plugin-sdk": "^0.0.91", + "motion": "^12.23.24", "path": "^0.12.7", + "radash": "^12.1.1", "react": "^18.2.0", "react-dom": "^18.2.0", "styled-components": "^5.3.3" diff --git a/src/components/stack/component.tsx b/src/components/stack/component.tsx new file mode 100644 index 0000000..5549db3 --- /dev/null +++ b/src/components/stack/component.tsx @@ -0,0 +1,55 @@ +import * as React from 'react'; +import * as ReactDOM from 'react-dom/client'; +import { debounce } from 'radash'; +import { FloatingWindow, PluginApi } from 'bigbluebutton-html-plugin-sdk'; +import ReactionWindow from '../window/component'; + +interface ReactionStackProps { + pluginApi: PluginApi; +} + +function ReactionStack({ + pluginApi, +}: ReactionStackProps): React.ReactNode { + React.useEffect(() => { + const navbar = document.getElementById('Navbar'); + + const createReactionWindow = (top: number, left: number) => new FloatingWindow({ + id: `bbb-plugin-reaction-stack-${top}-${left}`, + top, + left, + movable: true, + backgroundColor: 'transparent', + boxShadow: 'none', + contentFunction(element) { + const root = ReactDOM.createRoot(element); + root.render( + , + ); + return root; + }, + }); + + if (navbar) { + const updateWindow = debounce({ delay: 500 }, () => { + const navbarRect = navbar.getBoundingClientRect(); + pluginApi.setFloatingWindows([createReactionWindow(navbarRect.bottom, navbarRect.left)]); + }); + const observer = new ResizeObserver(updateWindow); + observer.observe(navbar); + return () => { + observer.disconnect(); + pluginApi.setFloatingWindows([]); + }; + } + + pluginApi.setFloatingWindows([createReactionWindow(0, 0)]); + return () => { + pluginApi.setFloatingWindows([]); + }; + }, [pluginApi]); + + return null; +} + +export default ReactionStack; diff --git a/src/components/window/component.tsx b/src/components/window/component.tsx new file mode 100644 index 0000000..9822883 --- /dev/null +++ b/src/components/window/component.tsx @@ -0,0 +1,85 @@ +import * as React from 'react'; +import { PluginApi } from 'bigbluebutton-html-plugin-sdk'; +import { USER_REACTIONS } from '../../graphql/queries'; +import { type UserReactionResponse, type UserReaction } from '../../graphql/types'; +import { + ReactionList, ReactionItem, UserName, Emoji, +} from './styles'; +import { LottieEmoji } from '../../emojis'; + +const initialCursor = new Date().toUTCString(); +const timeoutIds = new Map>(); + +interface ReactionWindowProps { + pluginApi: PluginApi; +} + +function ReactionWindow({ + pluginApi, +}: ReactionWindowProps) { + const { + data: userReactions, + } = pluginApi.useCustomSubscription( + USER_REACTIONS, + { variables: { initialCursor } }, + ); + const [reactions, setReactions] = React.useState>({}); + + React.useEffect(() => { + if (!userReactions) { + return; + } + const newReactions = { ...reactions }; + userReactions.user_reaction_stream.forEach((reaction) => { + const userTimeout = timeoutIds.get(reaction.userId); + if (userTimeout) clearTimeout(userTimeout); + if (reaction.reactionEmoji === 'none') { + delete newReactions[reaction.userId]; + } else { + newReactions[reaction.userId] = reaction; + timeoutIds.set( + reaction.userId, + setTimeout( + () => { + timeoutIds.delete(reaction.userId); + setReactions((prevReactions) => { + const newState = { ...prevReactions }; + delete newState[reaction.userId]; + return newState; + }); + }, + Math.max(new Date(reaction.expiresAt).getTime() - Date.now(), 0), + ), + ); + } + }); + setReactions(newReactions); + }, [userReactions]); + + if (!Object.values(reactions).length) return null; + + const reactionsItems = Object.values(reactions) + .sort((reaction1, reaction2) => ( + new Date(reaction2.createdAt).getTime() - new Date(reaction1.createdAt).getTime())); + + return ( + + {reactionsItems.map((reaction) => ( + + + + + {reaction.user.name} + + ))} + + ); +} + +export default ReactionWindow; diff --git a/src/components/window/styles.ts b/src/components/window/styles.ts new file mode 100644 index 0000000..b074056 --- /dev/null +++ b/src/components/window/styles.ts @@ -0,0 +1,94 @@ +import styled, { keyframes } from 'styled-components'; + +const slideInFade = keyframes` + from { + opacity: 0; + transform: translateX(-20px); + } + to { + opacity: 1; + transform: translateX(0); + } +`; + +const fadeOut = keyframes` + from { + opacity: 1; + } + to { + opacity: 0; + } +`; + +const fancyEmojiEntry = keyframes` + 0% { + transform: scale(2); + opacity: 0; + } + 50% { + transform: scale(1.3); + } + 100% { + transform: scale(1); + opacity: 1; + } +`; + +export const ReactionList = styled.ul` + list-style: none; + padding: 2rem; + margin: 0; + display: flex; + flex-direction: column; + gap: 0.5rem; + overflow: hidden; +`; + +export const ReactionItem = styled.li<{ backgroundColor: string }>` + padding-inline-start: 2rem; + padding-inline-end: 1rem; + background-color: ${(props) => props.backgroundColor}; + border-radius: 0.5rem; + display: flex; + align-items: center; + gap: 0.5rem; + position: relative; + height: calc(2rem - 2px); + width: 8rem; + + @media (max-width: 768px) { + height: calc(1.5rem - 2px); + } + + @media (prefers-reduced-motion: no-preference) { + animation: ${slideInFade} 0.4s ease-out forwards, + ${fadeOut} 30s linear forwards; + } +`; + +export const UserName = styled.span` + color: #eee; + max-width: 100%; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +`; + +export const Emoji = styled.span` + height: 2.5rem; + line-height: 2.5rem; + font-size: 2rem; + aspect-ratio: 1; + position: absolute; + left: -1rem; + + @media (max-width: 768px) { + height: 2rem; + line-height: 2rem; + font-size: 1.5rem; + } + + @media (prefers-reduced-motion: no-preference) { + animation: ${fancyEmojiEntry} 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55) forwards; + } +`; diff --git a/src/emojis.tsx b/src/emojis.tsx new file mode 100644 index 0000000..e32e4b8 --- /dev/null +++ b/src/emojis.tsx @@ -0,0 +1,55 @@ +import * as React from 'react'; +import { DotLottie, DotLottieReact } from '@lottiefiles/dotlottie-react'; + +export function LottieEmoji({ + codePoint, + fallbackEmoji, +}: { + codePoint: string; + fallbackEmoji: string; +}) { + const [instance, setInstance] = React.useState(null); + const [fallback, setFallback] = React.useState(null); + const [loading, setLoading] = React.useState(true); + React.useEffect(() => { + const controller = new AbortController(); + fetch( + `https://fonts.gstatic.com/s/e/notoemoji/latest/${codePoint}/lottie.json`, + { cache: 'force-cache', signal: controller.signal }, + ).then((response) => { + if (!response.ok) { + setFallback(fallbackEmoji); + } + }).finally(() => { + setLoading(false); + }); + return () => { + controller.abort(); + }; + }, [fallbackEmoji, codePoint]); + React.useEffect(() => { + if (instance) { + const id = setTimeout(() => { + instance.stop(); + }, 3000); + return () => { + clearTimeout(id); + }; + } + return undefined; + }, [instance]); + if (loading) return null; + if (fallback) return fallback; + return ( + instance.play()} + onMouseLeave={() => instance.stop()} + /> + ); +} diff --git a/src/graphql/queries.ts b/src/graphql/queries.ts new file mode 100644 index 0000000..64358d6 --- /dev/null +++ b/src/graphql/queries.ts @@ -0,0 +1,17 @@ +export const USER_REACTIONS = ` + subscription UserReactions($initialCursor: timestamptz) { + user_reaction_stream( + batch_size: 10, + cursor: { initial_value: { createdAt: $initialCursor } }, + ) { + createdAt + expiresAt + reactionEmoji + userId + user { + name + color + } + } + } +`; diff --git a/src/graphql/types.ts b/src/graphql/types.ts new file mode 100644 index 0000000..f92e283 --- /dev/null +++ b/src/graphql/types.ts @@ -0,0 +1,14 @@ +export interface UserReaction { + createdAt: string; // ISO string + expiresAt: string; // ISO string + reactionEmoji: string; + userId: string; + user: { + name: string; + color: string; + }; +} + +export interface UserReactionResponse { + user_reaction_stream: UserReaction[]; +} diff --git a/src/main/component.tsx b/src/main/component.tsx index 772c276..7cc8b26 100644 --- a/src/main/component.tsx +++ b/src/main/component.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { BbbPluginSdk, PluginApi } from 'bigbluebutton-html-plugin-sdk'; +import ReactionStack from '../components/stack/component'; interface MainComponentProps { pluginUuid: string; @@ -12,9 +13,9 @@ function MainComponent( BbbPluginSdk.initialize(uuid); const pluginApi: PluginApi = BbbPluginSdk.getPluginApi(uuid); - console.log('Hello world', pluginApi); - - return null; + return ( + + ); } export default MainComponent; diff --git a/webpack.config.js b/webpack.config.js index 5f01c5e..a8b635d 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -5,8 +5,8 @@ const path = require('path'); module.exports = { entry: './src/index.tsx', output: { - filename: '.js', - library: '', + filename: 'BbbPluginReactionStack.js', + library: 'BbbPluginReactionStack', libraryTarget: 'umd', publicPath: '/', globalObject: 'this',