diff --git a/config.js b/config.js index 0f9747d7b..87805d42a 100644 --- a/config.js +++ b/config.js @@ -1300,6 +1300,21 @@ var config = { // Specifies whether the chat emoticons are disabled or not // disableChatSmileys: false, + // Settings for the GIPHY integration. + // giphy: { + // // Whether the feature is enabled or not. + // enabled: false, + // // SDK API Key from Giphy. + // sdkKey: '', + // // Display mode can be one of: + // // - tile: show the GIF on the tile of the participant that sent it. + // // - chat: show the GIF as a message in chat + // // - all: all of the above. This is the default option + // displayMode: 'all', + // // How long the GIF should be displayed on the tile (in miliseconds). + // tileTime: 5000 + // }, + // Allow all above example options to include a trailing comma and // prevent fear when commenting out the last value. makeJsonParserHappy: 'even if last key had a trailing comma' diff --git a/css/_reactions-menu.scss b/css/_reactions-menu.scss index 94f23e3a8..2534ebff6 100644 --- a/css/_reactions-menu.scss +++ b/css/_reactions-menu.scss @@ -7,7 +7,20 @@ border-radius: 3px; padding: 16px; + &.with-gif { + width: 328px; + + .reactions-row .toolbox-button:last-of-type { + top: 3px; + + & .toolbox-icon.toggled { + background-color: #000000; + } + } + } + &.overflow { + width: 100%; .toolbox-icon { width: 48px; @@ -27,6 +40,10 @@ .toolbox-button { margin-right: 0; } + + .toolbox-button:last-of-type { + top: 0; + } } } @@ -56,6 +73,7 @@ .toolbox-button { margin-right: 8px; touch-action: manipulation; + position: relative; } .toolbox-button:last-of-type { diff --git a/images/GIPHY_icon.png b/images/GIPHY_icon.png new file mode 100644 index 000000000..575628c23 Binary files /dev/null and b/images/GIPHY_icon.png differ diff --git a/images/GIPHY_logo.png b/images/GIPHY_logo.png new file mode 100644 index 000000000..044113471 Binary files /dev/null and b/images/GIPHY_logo.png differ diff --git a/lang/main.json b/lang/main.json index b27bb86de..3ba773ba4 100644 --- a/lang/main.json +++ b/lang/main.json @@ -421,6 +421,10 @@ "veryBad": "Very Bad", "veryGood": "Very Good" }, + "giphy": { + "noResults": "No results found :(", + "search": "Search GIPHY" + }, "helpView": { "header": "Help center" }, @@ -487,6 +491,7 @@ "focusLocal": "Focus on your video", "focusRemote": "Focus on another person's video", "fullScreen": "View or exit full screen", + "giphyMenu": "Toggle GIPHY menu", "keyboardShortcuts": "Keyboard shortcuts", "localRecording": "Show or hide local recording controls", "mute": "Mute or unmute your microphone", @@ -1007,6 +1012,7 @@ "expand": "Expand", "feedback": "Leave feedback", "fullScreen": "Toggle full screen", + "giphy": "Toggle GIPHY menu", "grantModerator": "Grant Moderator Rights", "hangup": "Leave the meeting", "help": "Help", @@ -1076,6 +1082,7 @@ "exitFullScreen": "Exit full screen", "exitTileView": "Exit tile view", "feedback": "Leave feedback", + "giphy": "Toggle GIPHY menu", "hangup": "Leave the meeting", "help": "Help", "invite": "Invite people", diff --git a/package-lock.json b/package-lock.json index aef28648e..fda3bb5eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,8 @@ "@atlaskit/theme": "11.0.2", "@atlaskit/toggle": "12.0.3", "@atlaskit/tooltip": "17.1.2", + "@giphy/js-fetch-api": "4.1.2", + "@giphy/react-components": "5.6.0", "@hapi/bourne": "2.0.0", "@jitsi/js-utils": "2.0.0", "@jitsi/logger": "2.0.0", @@ -3282,6 +3284,100 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@giphy/js-analytics": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@giphy/js-analytics/-/js-analytics-4.0.7.tgz", + "integrity": "sha512-s4+GUXWwyxJVm6i7GHiQvQlMaXkHGCkh4uqjpisX5IiHxTNheSDMHHX0SyRLpTL5rdnvBkiBxlH8iOv9w3pNwg==", + "dependencies": { + "@giphy/js-types": "^4.1.0", + "@giphy/js-util": "^4.0.1", + "append-query": "^2.1.0", + "throttle-debounce": "^3.0.1" + } + }, + "node_modules/@giphy/js-brand": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@giphy/js-brand/-/js-brand-2.0.4.tgz", + "integrity": "sha512-q2iRyRWmKpCLAt1G7LzcHjw8s/cvSSoA1SfoQL47Tx0/yGwo8xCiFcFPFZJZsCcVMLCwA7/UmxTkdRydQVhCNw==", + "dependencies": { + "emotion": "10.0.27" + } + }, + "node_modules/@giphy/js-fetch-api": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@giphy/js-fetch-api/-/js-fetch-api-4.1.2.tgz", + "integrity": "sha512-wDfDQu8HiVkLb+YXcZf8QFbznmMHWbg86ZBydYmnp2mfuHyaHKsz9n9PnxdH3RorMS9YM/Ca/zqAM5y89Qj+Hw==", + "dependencies": { + "@giphy/js-types": "^4.1.0", + "@giphy/js-util": "^4.0.1", + "qs": "^6.9.4" + } + }, + "node_modules/@giphy/js-types": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@giphy/js-types/-/js-types-4.1.0.tgz", + "integrity": "sha512-+qSN4Mx4TmrjLQm4SC0I/ZBkb5eWM94sljXwfjIlqn0GMSR3geqEqwmE9Uf/ldgzFh+XMMCasQjIdUl2nWc++Q==" + }, + "node_modules/@giphy/js-util": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@giphy/js-util/-/js-util-4.0.1.tgz", + "integrity": "sha512-46wXgt5Y+MxZjuzjE6JlvMLE+6Vlag+PYxbyTxpsunhmOKNoYK99d51E09iynmXTFuZWYgWJR9LcfnzqsWHy+Q==", + "dependencies": { + "@giphy/js-types": "^4.1.0", + "dompurify": "^2.2.2", + "uuid": "^8.3.0" + } + }, + "node_modules/@giphy/react-components": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@giphy/react-components/-/react-components-5.6.0.tgz", + "integrity": "sha512-RxDGNB+MoR2f+OjMSFK+Cu294xe6Vr9jM6Y8xj5Y9yyAbrNqIsEV0imrAqOuWDJeA3FC23a6RVPBghwG56jebw==", + "dependencies": { + "@emotion/core": "10.1.1", + "@emotion/styled": "10.0.27", + "@giphy/js-analytics": "^4.0.7", + "@giphy/js-brand": "^2.0.4", + "@giphy/js-fetch-api": "^4.1.2", + "@giphy/js-types": "^4.1.0", + "@giphy/js-util": "^4.0.1", + "emotion-theming": "10.0.27", + "intersection-observer": "^0.11.0", + "react-use": "17.2.4", + "throttle-debounce": "^3.0.1" + }, + "peerDependencies": { + "react": "16.10.2 - 17" + } + }, + "node_modules/@giphy/react-components/node_modules/@emotion/core": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@emotion/core/-/core-10.1.1.tgz", + "integrity": "sha512-ZMLG6qpXR8x031NXD8HJqugy/AZSkAuMxxqB46pmAR7ze47MhNJ56cdoX243QPZdGctrdfo+s08yZTiwaUcRKA==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "@emotion/cache": "^10.0.27", + "@emotion/css": "^10.0.27", + "@emotion/serialize": "^0.11.15", + "@emotion/sheet": "0.9.4", + "@emotion/utils": "0.11.3" + }, + "peerDependencies": { + "react": ">=16.3.0" + } + }, + "node_modules/@giphy/react-components/node_modules/@emotion/styled": { + "version": "10.0.27", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-10.0.27.tgz", + "integrity": "sha512-iK/8Sh7+NLJzyp9a5+vIQIXTYxfT4yB/OJbjzQanB2RZpvmzBQOHZWhpAMZWYEKRNNbsD6WfBw5sVWkb6WzS/Q==", + "dependencies": { + "@emotion/styled-base": "^10.0.27", + "babel-plugin-emotion": "^10.0.27" + }, + "peerDependencies": { + "@emotion/core": "^10.0.27", + "react": ">=16.3.0" + } + }, "node_modules/@hapi/bourne": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-2.0.0.tgz", @@ -5042,6 +5138,11 @@ "@types/istanbul-lib-report": "*" } }, + "node_modules/@types/js-cookie": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.7.tgz", + "integrity": "sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==" + }, "node_modules/@types/json-schema": { "version": "7.0.9", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", @@ -5389,6 +5490,11 @@ "node": ">=10.0.0" } }, + "node_modules/@xobotyi/scrollbar-width": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz", + "integrity": "sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==" + }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -5698,6 +5804,14 @@ "resolved": "https://registry.npmjs.org/appdirsjs/-/appdirsjs-1.2.6.tgz", "integrity": "sha512-D8wJNkqMCeQs3kLasatELsddox/Xqkhp+J07iXGyL54fVN7oc+nmNfYzGuCs1IEP6uBw+TfpuO3JKwc+lECy4w==" }, + "node_modules/append-query": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/append-query/-/append-query-2.1.1.tgz", + "integrity": "sha512-adm0E8o1o7ay+HbkWvGIpNNeciLB/rxJ0heThHuzSSVq5zcdQ5/ZubFnUoY0imFmk6gZVghSpwoubLVtwi9EHQ==", + "dependencies": { + "extend": "^3.0.2" + } + }, "node_modules/aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", @@ -7128,6 +7242,14 @@ "node": ">=0.10.0" } }, + "node_modules/copy-to-clipboard": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz", + "integrity": "sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw==", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, "node_modules/core-js": { "version": "2.6.12", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", @@ -7175,6 +7297,17 @@ "node": ">=4" } }, + "node_modules/create-emotion": { + "version": "10.0.27", + "resolved": "https://registry.npmjs.org/create-emotion/-/create-emotion-10.0.27.tgz", + "integrity": "sha512-fIK73w82HPPn/RsAij7+Zt8eCE8SptcJ3WoRMfxMtjteYxud8GDTKKld7MYwAX2TVhrw29uR1N/bVGxeStHILg==", + "dependencies": { + "@emotion/cache": "^10.0.27", + "@emotion/serialize": "^0.11.15", + "@emotion/sheet": "0.9.4", + "@emotion/utils": "0.11.3" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -7217,6 +7350,15 @@ "node": ">4" } }, + "node_modules/css-in-js-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-2.0.1.tgz", + "integrity": "sha512-PJF0SpJT+WdbVVt0AOYp9C8GnuruRlL/UFW7932nLWmFLQTaWEzTBQEx7/hn4BuV+WON75iAViSUJLiU3PKbpA==", + "dependencies": { + "hyphenate-style-name": "^1.0.2", + "isobject": "^3.0.1" + } + }, "node_modules/css-loader": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-3.6.0.tgz", @@ -7831,6 +7973,11 @@ } ] }, + "node_modules/dompurify": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.3.6.tgz", + "integrity": "sha512-OFP2u/3T1R5CEgWCEONuJ1a5+MFKnOYpkywpUSxv/dj1LeBT1erK+JwM7zK0ROy2BRhqVCf0LRw/kHqKuMkVGg==" + }, "node_modules/domutils": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", @@ -7899,6 +8046,29 @@ "node": ">= 4" } }, + "node_modules/emotion": { + "version": "10.0.27", + "resolved": "https://registry.npmjs.org/emotion/-/emotion-10.0.27.tgz", + "integrity": "sha512-2xdDzdWWzue8R8lu4G76uWX5WhyQuzATon9LmNeCy/2BHVC6dsEpfhN1a0qhELgtDVdjyEA6J8Y/VlI5ZnaH0g==", + "dependencies": { + "babel-plugin-emotion": "^10.0.27", + "create-emotion": "^10.0.27" + } + }, + "node_modules/emotion-theming": { + "version": "10.0.27", + "resolved": "https://registry.npmjs.org/emotion-theming/-/emotion-theming-10.0.27.tgz", + "integrity": "sha512-MlF1yu/gYh8u+sLUqA0YuA9JX0P4Hb69WlKc/9OLo+WCXuX6sy/KoIa+qJimgmr2dWqnypYKYPX37esjDBbhdw==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "@emotion/weak-memoize": "0.2.5", + "hoist-non-react-statics": "^3.3.0" + }, + "peerDependencies": { + "@emotion/core": "^10.0.27", + "react": ">=16.3.0" + } + }, "node_modules/encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -8990,6 +9160,11 @@ } ] }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, "node_modules/extend-shallow": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", @@ -9103,12 +9278,22 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, + "node_modules/fast-shallow-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz", + "integrity": "sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==" + }, "node_modules/fastest-levenshtein": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz", "integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==", "dev": true }, + "node_modules/fastest-stable-stringify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fastest-stable-stringify/-/fastest-stable-stringify-2.0.2.tgz", + "integrity": "sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==" + }, "node_modules/fastq": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", @@ -10156,6 +10341,14 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/inline-style-prefixer": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-6.0.1.tgz", + "integrity": "sha512-AsqazZ8KcRzJ9YPN1wMH2aNM7lkWQ8tSPrW5uDk1ziYwiAPWSZnUsC7lfZq+BDqLqz0B4Pho5wscWcJzVvRzDQ==", + "dependencies": { + "css-in-js-utils": "^2.0.0" + } + }, "node_modules/internal-slot": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", @@ -10178,6 +10371,11 @@ "node": ">= 0.10" } }, + "node_modules/intersection-observer": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/intersection-observer/-/intersection-observer-0.11.0.tgz", + "integrity": "sha512-KZArj2QVnmdud9zTpKf279m2bbGfG+4/kn16UU0NL3pTVl52ZHiJ9IRNSsnn6jaHrL9EGLFM5eWjTx2fz/+zoQ==" + }, "node_modules/invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -11154,6 +11352,11 @@ "resolved": "https://registry.npmjs.org/jquery-i18next/-/jquery-i18next-1.2.1.tgz", "integrity": "sha512-UNcw3rgxoKjGEg4w23FEn2h3OlPJU7rPzsgDuXDBZktIzeiVbJohs9Cv9hj8oP8KNfBRKOoErL/OVxg2FaAR4g==" }, + "node_modules/js-cookie": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", + "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==" + }, "node_modules/js-md5": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.6.1.tgz", @@ -12622,6 +12825,30 @@ "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==", "optional": true }, + "node_modules/nano-css": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/nano-css/-/nano-css-5.3.4.tgz", + "integrity": "sha512-wfcviJB6NOxDIDfr7RFn/GlaN7I/Bhe4d39ZRCJ3xvZX60LVe2qZ+rDqM49nm4YT81gAjzS+ZklhKP/Gnfnubg==", + "dependencies": { + "css-tree": "^1.1.2", + "csstype": "^3.0.6", + "fastest-stable-stringify": "^2.0.2", + "inline-style-prefixer": "^6.0.0", + "rtl-css-js": "^1.14.0", + "sourcemap-codec": "^1.4.8", + "stacktrace-js": "^2.0.2", + "stylis": "^4.0.6" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/nano-css/node_modules/stylis": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.0.13.tgz", + "integrity": "sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag==" + }, "node_modules/nanoid": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz", @@ -14504,7 +14731,6 @@ "version": "6.9.7", "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz", "integrity": "sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==", - "dev": true, "engines": { "node": ">=0.6" }, @@ -15671,6 +15897,40 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, + "node_modules/react-universal-interface": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz", + "integrity": "sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==", + "peerDependencies": { + "react": "*", + "tslib": "*" + } + }, + "node_modules/react-use": { + "version": "17.2.4", + "resolved": "https://registry.npmjs.org/react-use/-/react-use-17.2.4.tgz", + "integrity": "sha512-vQGpsAM0F5UIlshw5UI8ULGPS4yn5rm7/qvn3T1Gnkrz7YRMEEMh+ynKcmRloOyiIeLvKWiQjMiwRGtdbgs5qQ==", + "dependencies": { + "@types/js-cookie": "^2.2.6", + "@xobotyi/scrollbar-width": "^1.9.5", + "copy-to-clipboard": "^3.3.1", + "fast-deep-equal": "^3.1.3", + "fast-shallow-equal": "^1.0.0", + "js-cookie": "^2.2.1", + "nano-css": "^5.3.1", + "react-universal-interface": "^0.6.2", + "resize-observer-polyfill": "^1.5.1", + "screenfull": "^5.1.0", + "set-harmonic-interval": "^1.0.1", + "throttle-debounce": "^3.0.1", + "ts-easing": "^0.2.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0", + "react-dom": "^16.8.0 || ^17.0.0" + } + }, "node_modules/react-window": { "version": "1.8.6", "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.6.tgz", @@ -15972,6 +16232,11 @@ "canvas": "2.8.0" } }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" + }, "node_modules/resolve": { "version": "1.22.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", @@ -16100,6 +16365,14 @@ "node": "6.* || >= 7.*" } }, + "node_modules/rtl-css-js": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.15.0.tgz", + "integrity": "sha512-99Cu4wNNIhrI10xxUaABHsdDqzalrSRTie4GeCmbGVuehm4oj+fIy8fTzB+16pmKe8Bv9rl+hxIBez6KxExTew==", + "dependencies": { + "@babel/runtime": "^7.1.2" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -16352,6 +16625,17 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/screenfull": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/screenfull/-/screenfull-5.2.0.tgz", + "integrity": "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==", + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/sdp": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/sdp/-/sdp-3.0.3.tgz", @@ -16544,6 +16828,14 @@ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, + "node_modules/set-harmonic-interval": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-harmonic-interval/-/set-harmonic-interval-1.0.1.tgz", + "integrity": "sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==", + "engines": { + "node": ">=6.9" + } + }, "node_modules/set-value": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", @@ -16956,6 +17248,11 @@ "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", "deprecated": "See https://github.com/lydell/source-map-url#deprecated" }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==" + }, "node_modules/spdx-exceptions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", @@ -17037,11 +17334,46 @@ "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==" }, + "node_modules/stack-generator": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.5.tgz", + "integrity": "sha512-/t1ebrbHkrLrDuNMdeAcsvynWgoH/i4o8EGGfX7dEYDoTXOYVAkEpFdtshlvabzc6JlJ8Kf9YdFEoz7JkzGN9Q==", + "dependencies": { + "stackframe": "^1.1.1" + } + }, "node_modules/stackframe": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.2.1.tgz", "integrity": "sha512-h88QkzREN/hy8eRdyNhhsO7RSJ5oyTqxxmmn0dzBIMUclZsjpfmrsg81vp8mjjAs2vAZ72nyWxRUwSwmh0e4xg==" }, + "node_modules/stacktrace-gps": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/stacktrace-gps/-/stacktrace-gps-3.0.4.tgz", + "integrity": "sha512-qIr8x41yZVSldqdqe6jciXEaSCKw1U8XTXpjDuy0ki/apyTn/r3w9hDAAQOhZdxvsC93H+WwwEu5cq5VemzYeg==", + "dependencies": { + "source-map": "0.5.6", + "stackframe": "^1.1.1" + } + }, + "node_modules/stacktrace-gps/node_modules/source-map": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "integrity": "sha1-dc449SvwczxafwwRjYEzSiu19BI=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stacktrace-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stacktrace-js/-/stacktrace-js-2.0.2.tgz", + "integrity": "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==", + "dependencies": { + "error-stack-parser": "^2.0.6", + "stack-generator": "^2.0.5", + "stacktrace-gps": "^3.0.4" + } + }, "node_modules/stacktrace-parser": { "version": "0.1.10", "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz", @@ -17823,6 +18155,14 @@ "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==" }, + "node_modules/throttle-debounce": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-3.0.1.tgz", + "integrity": "sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==", + "engines": { + "node": ">=10" + } + }, "node_modules/through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -17949,6 +18289,11 @@ "node": ">=8.0" } }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI=" + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -17977,6 +18322,11 @@ "integrity": "sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=", "dev": true }, + "node_modules/ts-easing": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/ts-easing/-/ts-easing-0.2.0.tgz", + "integrity": "sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==" + }, "node_modules/tsconfig-paths": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.12.0.tgz", @@ -21810,6 +22160,92 @@ } } }, + "@giphy/js-analytics": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@giphy/js-analytics/-/js-analytics-4.0.7.tgz", + "integrity": "sha512-s4+GUXWwyxJVm6i7GHiQvQlMaXkHGCkh4uqjpisX5IiHxTNheSDMHHX0SyRLpTL5rdnvBkiBxlH8iOv9w3pNwg==", + "requires": { + "@giphy/js-types": "^4.1.0", + "@giphy/js-util": "^4.0.1", + "append-query": "^2.1.0", + "throttle-debounce": "^3.0.1" + } + }, + "@giphy/js-brand": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@giphy/js-brand/-/js-brand-2.0.4.tgz", + "integrity": "sha512-q2iRyRWmKpCLAt1G7LzcHjw8s/cvSSoA1SfoQL47Tx0/yGwo8xCiFcFPFZJZsCcVMLCwA7/UmxTkdRydQVhCNw==", + "requires": { + "emotion": "10.0.27" + } + }, + "@giphy/js-fetch-api": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@giphy/js-fetch-api/-/js-fetch-api-4.1.2.tgz", + "integrity": "sha512-wDfDQu8HiVkLb+YXcZf8QFbznmMHWbg86ZBydYmnp2mfuHyaHKsz9n9PnxdH3RorMS9YM/Ca/zqAM5y89Qj+Hw==", + "requires": { + "@giphy/js-types": "^4.1.0", + "@giphy/js-util": "^4.0.1", + "qs": "^6.9.4" + } + }, + "@giphy/js-types": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@giphy/js-types/-/js-types-4.1.0.tgz", + "integrity": "sha512-+qSN4Mx4TmrjLQm4SC0I/ZBkb5eWM94sljXwfjIlqn0GMSR3geqEqwmE9Uf/ldgzFh+XMMCasQjIdUl2nWc++Q==" + }, + "@giphy/js-util": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@giphy/js-util/-/js-util-4.0.1.tgz", + "integrity": "sha512-46wXgt5Y+MxZjuzjE6JlvMLE+6Vlag+PYxbyTxpsunhmOKNoYK99d51E09iynmXTFuZWYgWJR9LcfnzqsWHy+Q==", + "requires": { + "@giphy/js-types": "^4.1.0", + "dompurify": "^2.2.2", + "uuid": "^8.3.0" + } + }, + "@giphy/react-components": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@giphy/react-components/-/react-components-5.6.0.tgz", + "integrity": "sha512-RxDGNB+MoR2f+OjMSFK+Cu294xe6Vr9jM6Y8xj5Y9yyAbrNqIsEV0imrAqOuWDJeA3FC23a6RVPBghwG56jebw==", + "requires": { + "@emotion/core": "10.1.1", + "@emotion/styled": "10.0.27", + "@giphy/js-analytics": "^4.0.7", + "@giphy/js-brand": "^2.0.4", + "@giphy/js-fetch-api": "^4.1.2", + "@giphy/js-types": "^4.1.0", + "@giphy/js-util": "^4.0.1", + "emotion-theming": "10.0.27", + "intersection-observer": "^0.11.0", + "react-use": "17.2.4", + "throttle-debounce": "^3.0.1" + }, + "dependencies": { + "@emotion/core": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@emotion/core/-/core-10.1.1.tgz", + "integrity": "sha512-ZMLG6qpXR8x031NXD8HJqugy/AZSkAuMxxqB46pmAR7ze47MhNJ56cdoX243QPZdGctrdfo+s08yZTiwaUcRKA==", + "requires": { + "@babel/runtime": "^7.5.5", + "@emotion/cache": "^10.0.27", + "@emotion/css": "^10.0.27", + "@emotion/serialize": "^0.11.15", + "@emotion/sheet": "0.9.4", + "@emotion/utils": "0.11.3" + } + }, + "@emotion/styled": { + "version": "10.0.27", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-10.0.27.tgz", + "integrity": "sha512-iK/8Sh7+NLJzyp9a5+vIQIXTYxfT4yB/OJbjzQanB2RZpvmzBQOHZWhpAMZWYEKRNNbsD6WfBw5sVWkb6WzS/Q==", + "requires": { + "@emotion/styled-base": "^10.0.27", + "babel-plugin-emotion": "^10.0.27" + } + } + } + }, "@hapi/bourne": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-2.0.0.tgz", @@ -23139,6 +23575,11 @@ "@types/istanbul-lib-report": "*" } }, + "@types/js-cookie": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.7.tgz", + "integrity": "sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==" + }, "@types/json-schema": { "version": "7.0.9", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", @@ -23465,6 +23906,11 @@ "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.5.tgz", "integrity": "sha512-V3BIhmY36fXZ1OtVcI9W+FxQqxVLsPKcNjWigIaa81dLC9IolJl5Mt4Cvhmr0flUnjSpTdrbMTSbXqYqV5dT6A==" }, + "@xobotyi/scrollbar-width": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz", + "integrity": "sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==" + }, "@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -23703,6 +24149,14 @@ "resolved": "https://registry.npmjs.org/appdirsjs/-/appdirsjs-1.2.6.tgz", "integrity": "sha512-D8wJNkqMCeQs3kLasatELsddox/Xqkhp+J07iXGyL54fVN7oc+nmNfYzGuCs1IEP6uBw+TfpuO3JKwc+lECy4w==" }, + "append-query": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/append-query/-/append-query-2.1.1.tgz", + "integrity": "sha512-adm0E8o1o7ay+HbkWvGIpNNeciLB/rxJ0heThHuzSSVq5zcdQ5/ZubFnUoY0imFmk6gZVghSpwoubLVtwi9EHQ==", + "requires": { + "extend": "^3.0.2" + } + }, "aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", @@ -24815,6 +25269,14 @@ "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" }, + "copy-to-clipboard": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz", + "integrity": "sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw==", + "requires": { + "toggle-selection": "^1.0.6" + } + }, "core-js": { "version": "2.6.12", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", @@ -24852,6 +25314,17 @@ "parse-json": "^4.0.0" } }, + "create-emotion": { + "version": "10.0.27", + "resolved": "https://registry.npmjs.org/create-emotion/-/create-emotion-10.0.27.tgz", + "integrity": "sha512-fIK73w82HPPn/RsAij7+Zt8eCE8SptcJ3WoRMfxMtjteYxud8GDTKKld7MYwAX2TVhrw29uR1N/bVGxeStHILg==", + "requires": { + "@emotion/cache": "^10.0.27", + "@emotion/serialize": "^0.11.15", + "@emotion/sheet": "0.9.4", + "@emotion/utils": "0.11.3" + } + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -24882,6 +25355,15 @@ "timsort": "^0.3.0" } }, + "css-in-js-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-2.0.1.tgz", + "integrity": "sha512-PJF0SpJT+WdbVVt0AOYp9C8GnuruRlL/UFW7932nLWmFLQTaWEzTBQEx7/hn4BuV+WON75iAViSUJLiU3PKbpA==", + "requires": { + "hyphenate-style-name": "^1.0.2", + "isobject": "^3.0.1" + } + }, "css-loader": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-3.6.0.tgz", @@ -25355,6 +25837,11 @@ } } }, + "dompurify": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.3.6.tgz", + "integrity": "sha512-OFP2u/3T1R5CEgWCEONuJ1a5+MFKnOYpkywpUSxv/dj1LeBT1erK+JwM7zK0ROy2BRhqVCf0LRw/kHqKuMkVGg==" + }, "domutils": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", @@ -25411,6 +25898,25 @@ "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==" }, + "emotion": { + "version": "10.0.27", + "resolved": "https://registry.npmjs.org/emotion/-/emotion-10.0.27.tgz", + "integrity": "sha512-2xdDzdWWzue8R8lu4G76uWX5WhyQuzATon9LmNeCy/2BHVC6dsEpfhN1a0qhELgtDVdjyEA6J8Y/VlI5ZnaH0g==", + "requires": { + "babel-plugin-emotion": "^10.0.27", + "create-emotion": "^10.0.27" + } + }, + "emotion-theming": { + "version": "10.0.27", + "resolved": "https://registry.npmjs.org/emotion-theming/-/emotion-theming-10.0.27.tgz", + "integrity": "sha512-MlF1yu/gYh8u+sLUqA0YuA9JX0P4Hb69WlKc/9OLo+WCXuX6sy/KoIa+qJimgmr2dWqnypYKYPX37esjDBbhdw==", + "requires": { + "@babel/runtime": "^7.5.5", + "@emotion/weak-memoize": "0.2.5", + "hoist-non-react-statics": "^3.3.0" + } + }, "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -26249,6 +26755,11 @@ } } }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, "extend-shallow": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", @@ -26347,12 +26858,22 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, + "fast-shallow-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz", + "integrity": "sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==" + }, "fastest-levenshtein": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz", "integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==", "dev": true }, + "fastest-stable-stringify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fastest-stable-stringify/-/fastest-stable-stringify-2.0.2.tgz", + "integrity": "sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==" + }, "fastq": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", @@ -27166,6 +27687,14 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "inline-style-prefixer": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-6.0.1.tgz", + "integrity": "sha512-AsqazZ8KcRzJ9YPN1wMH2aNM7lkWQ8tSPrW5uDk1ziYwiAPWSZnUsC7lfZq+BDqLqz0B4Pho5wscWcJzVvRzDQ==", + "requires": { + "css-in-js-utils": "^2.0.0" + } + }, "internal-slot": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", @@ -27182,6 +27711,11 @@ "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", "dev": true }, + "intersection-observer": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/intersection-observer/-/intersection-observer-0.11.0.tgz", + "integrity": "sha512-KZArj2QVnmdud9zTpKf279m2bbGfG+4/kn16UU0NL3pTVl52ZHiJ9IRNSsnn6jaHrL9EGLFM5eWjTx2fz/+zoQ==" + }, "invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -27885,6 +28419,11 @@ "resolved": "https://registry.npmjs.org/jquery-i18next/-/jquery-i18next-1.2.1.tgz", "integrity": "sha512-UNcw3rgxoKjGEg4w23FEn2h3OlPJU7rPzsgDuXDBZktIzeiVbJohs9Cv9hj8oP8KNfBRKOoErL/OVxg2FaAR4g==" }, + "js-cookie": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", + "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==" + }, "js-md5": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.6.1.tgz", @@ -29115,6 +29654,28 @@ "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==", "optional": true }, + "nano-css": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/nano-css/-/nano-css-5.3.4.tgz", + "integrity": "sha512-wfcviJB6NOxDIDfr7RFn/GlaN7I/Bhe4d39ZRCJ3xvZX60LVe2qZ+rDqM49nm4YT81gAjzS+ZklhKP/Gnfnubg==", + "requires": { + "css-tree": "^1.1.2", + "csstype": "^3.0.6", + "fastest-stable-stringify": "^2.0.2", + "inline-style-prefixer": "^6.0.0", + "rtl-css-js": "^1.14.0", + "sourcemap-codec": "^1.4.8", + "stacktrace-js": "^2.0.2", + "stylis": "^4.0.6" + }, + "dependencies": { + "stylis": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.0.13.tgz", + "integrity": "sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag==" + } + } + }, "nanoid": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz", @@ -30587,8 +31148,7 @@ "qs": { "version": "6.9.7", "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==", - "dev": true + "integrity": "sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==" }, "query-string": { "version": "7.1.1", @@ -31335,6 +31895,32 @@ } } }, + "react-universal-interface": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz", + "integrity": "sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==" + }, + "react-use": { + "version": "17.2.4", + "resolved": "https://registry.npmjs.org/react-use/-/react-use-17.2.4.tgz", + "integrity": "sha512-vQGpsAM0F5UIlshw5UI8ULGPS4yn5rm7/qvn3T1Gnkrz7YRMEEMh+ynKcmRloOyiIeLvKWiQjMiwRGtdbgs5qQ==", + "requires": { + "@types/js-cookie": "^2.2.6", + "@xobotyi/scrollbar-width": "^1.9.5", + "copy-to-clipboard": "^3.3.1", + "fast-deep-equal": "^3.1.3", + "fast-shallow-equal": "^1.0.0", + "js-cookie": "^2.2.1", + "nano-css": "^5.3.1", + "react-universal-interface": "^0.6.2", + "resize-observer-polyfill": "^1.5.1", + "screenfull": "^5.1.0", + "set-harmonic-interval": "^1.0.1", + "throttle-debounce": "^3.0.1", + "ts-easing": "^0.2.0", + "tslib": "^2.1.0" + } + }, "react-window": { "version": "1.8.6", "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.6.tgz", @@ -31572,6 +32158,11 @@ "canvas": "2.8.0" } }, + "resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" + }, "resolve": { "version": "1.22.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", @@ -31664,6 +32255,14 @@ "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==" }, + "rtl-css-js": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.15.0.tgz", + "integrity": "sha512-99Cu4wNNIhrI10xxUaABHsdDqzalrSRTie4GeCmbGVuehm4oj+fIy8fTzB+16pmKe8Bv9rl+hxIBez6KxExTew==", + "requires": { + "@babel/runtime": "^7.1.2" + } + }, "run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -31860,6 +32459,11 @@ "ajv-keywords": "^3.5.2" } }, + "screenfull": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/screenfull/-/screenfull-5.2.0.tgz", + "integrity": "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==" + }, "sdp": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/sdp/-/sdp-3.0.3.tgz", @@ -32028,6 +32632,11 @@ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, + "set-harmonic-interval": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-harmonic-interval/-/set-harmonic-interval-1.0.1.tgz", + "integrity": "sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==" + }, "set-value": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", @@ -32369,6 +32978,11 @@ "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==" }, + "sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==" + }, "spdx-exceptions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", @@ -32441,11 +33055,45 @@ "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==" }, + "stack-generator": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.5.tgz", + "integrity": "sha512-/t1ebrbHkrLrDuNMdeAcsvynWgoH/i4o8EGGfX7dEYDoTXOYVAkEpFdtshlvabzc6JlJ8Kf9YdFEoz7JkzGN9Q==", + "requires": { + "stackframe": "^1.1.1" + } + }, "stackframe": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.2.1.tgz", "integrity": "sha512-h88QkzREN/hy8eRdyNhhsO7RSJ5oyTqxxmmn0dzBIMUclZsjpfmrsg81vp8mjjAs2vAZ72nyWxRUwSwmh0e4xg==" }, + "stacktrace-gps": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/stacktrace-gps/-/stacktrace-gps-3.0.4.tgz", + "integrity": "sha512-qIr8x41yZVSldqdqe6jciXEaSCKw1U8XTXpjDuy0ki/apyTn/r3w9hDAAQOhZdxvsC93H+WwwEu5cq5VemzYeg==", + "requires": { + "source-map": "0.5.6", + "stackframe": "^1.1.1" + }, + "dependencies": { + "source-map": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "integrity": "sha1-dc449SvwczxafwwRjYEzSiu19BI=" + } + } + }, + "stacktrace-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stacktrace-js/-/stacktrace-js-2.0.2.tgz", + "integrity": "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==", + "requires": { + "error-stack-parser": "^2.0.6", + "stack-generator": "^2.0.5", + "stacktrace-gps": "^3.0.4" + } + }, "stacktrace-parser": { "version": "0.1.10", "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz", @@ -33035,6 +33683,11 @@ "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==" }, + "throttle-debounce": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-3.0.1.tgz", + "integrity": "sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==" + }, "through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -33145,6 +33798,11 @@ "is-number": "^7.0.0" } }, + "toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI=" + }, "toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -33167,6 +33825,11 @@ "integrity": "sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=", "dev": true }, + "ts-easing": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/ts-easing/-/ts-easing-0.2.0.tgz", + "integrity": "sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==" + }, "tsconfig-paths": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.12.0.tgz", diff --git a/package.json b/package.json index b53f963c6..82ff2b999 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,8 @@ "@atlaskit/theme": "11.0.2", "@atlaskit/toggle": "12.0.3", "@atlaskit/tooltip": "17.1.2", + "@giphy/js-fetch-api": "4.1.2", + "@giphy/react-components": "5.6.0", "@hapi/bourne": "2.0.0", "@jitsi/js-utils": "2.0.0", "@jitsi/logger": "2.0.0", diff --git a/react/features/analytics/AnalyticsEvents.js b/react/features/analytics/AnalyticsEvents.js index 26e861151..99ab51275 100644 --- a/react/features/analytics/AnalyticsEvents.js +++ b/react/features/analytics/AnalyticsEvents.js @@ -899,3 +899,15 @@ export function createBreakoutRoomsEvent(actionSubject) { source: 'breakout.rooms' }; } + +/** + * Creates and event which indicates a GIF was sent. + * + * @returns {Object} The event in a format suitable for sending via + * sendAnalytics. + */ +export function createGifSentEvent() { + return { + action: 'gif.sent' + }; +} diff --git a/react/features/app/middlewares.any.js b/react/features/app/middlewares.any.js index 5cc31fc88..b38cfd9b8 100644 --- a/react/features/app/middlewares.any.js +++ b/react/features/app/middlewares.any.js @@ -30,6 +30,7 @@ import '../display-name/middleware'; import '../etherpad/middleware'; import '../filmstrip/middleware'; import '../follow-me/middleware'; +import '../gifs/middleware'; import '../invite/middleware'; import '../jaas/middleware'; import '../large-video/middleware'; diff --git a/react/features/app/reducers.any.js b/react/features/app/reducers.any.js index 1a8416259..30098e022 100644 --- a/react/features/app/reducers.any.js +++ b/react/features/app/reducers.any.js @@ -33,6 +33,7 @@ import '../dynamic-branding/reducer'; import '../etherpad/reducer'; import '../filmstrip/reducer'; import '../follow-me/reducer'; +import '../gifs/reducer'; import '../google-api/reducer'; import '../invite/reducer'; import '../jaas/reducer'; diff --git a/react/features/base/config/configWhitelist.js b/react/features/base/config/configWhitelist.js index deebb0824..7bb49d9ab 100644 --- a/react/features/base/config/configWhitelist.js +++ b/react/features/base/config/configWhitelist.js @@ -161,6 +161,7 @@ export default [ 'forceJVB121Ratio', 'forceTurnRelay', 'gatherStats', + 'giphy', 'googleApiApplicationClientID', 'hiddenPremeetingButtons', 'hideConferenceSubject', diff --git a/react/features/base/premeeting/components/web/InputField.js b/react/features/base/premeeting/components/web/InputField.js index 2d034e222..4d6945969 100644 --- a/react/features/base/premeeting/components/web/InputField.js +++ b/react/features/base/premeeting/components/web/InputField.js @@ -2,6 +2,7 @@ import React, { PureComponent } from 'react'; +import { isMobileBrowser } from '../../../environment/utils'; import { getFieldValue } from '../../../react'; type Props = { @@ -132,6 +133,9 @@ export default class InputField extends PureComponent { onKeyDown = { this._onKeyDown } placeholder = { this.props.placeHolder } readOnly = { this.props.readOnly } + // eslint-disable-next-line react/jsx-no-bind + ref = { inputElement => this.props.autoFocus && isMobileBrowser() + && inputElement && inputElement.focus() } type = { this.props.type } value = { this.state.value } /> ); diff --git a/react/features/base/react/components/web/Message.js b/react/features/base/react/components/web/Message.js index 77cec123d..df0a2dac9 100644 --- a/react/features/base/react/components/web/Message.js +++ b/react/features/base/react/components/web/Message.js @@ -3,6 +3,9 @@ import React, { Component } from 'react'; import { toArray } from 'react-emoji-render'; +import GifMessage from '../../../../chat/components/web/GifMessage'; +import { isGifMessage } from '../../../../gifs/functions'; + import Linkify from './Linkify'; type Props = { @@ -44,16 +47,26 @@ class Message extends Component { const content = []; - for (const token of tokens) { - if (token.includes('://')) { + // check if the message is a GIF + if (isGifMessage(text)) { + const url = text.substring(4, text.length - 1); - // Bypass the emojification when urls are involved - content.push(token); - } else { - content.push(...toArray(token, { className: 'smiley' })); + content.push(); + } else { + for (const token of tokens) { + + if (token.includes('://')) { + + // Bypass the emojification when urls are involved + content.push(token); + } else { + content.push(...toArray(token, { className: 'smiley' })); + } + + content.push(' '); } - - content.push(' '); } content.forEach(token => { diff --git a/react/features/chat/components/web/GifMessage.js b/react/features/chat/components/web/GifMessage.js new file mode 100644 index 000000000..c001b5d17 --- /dev/null +++ b/react/features/chat/components/web/GifMessage.js @@ -0,0 +1,42 @@ +// @flow + +import { makeStyles } from '@material-ui/styles'; +import React from 'react'; + +type Props = { + + /** + * URL of the GIF. + */ + url: string +} + +const useStyles = makeStyles(() => { + return { + container: { + display: 'flex', + justifyContent: 'center', + overflow: 'hidden', + maxHeight: '150px', + + '& img': { + maxWidth: '100%', + maxHeight: '100%', + objectFit: 'contain', + flexGrow: '1' + } + } + }; +}); + +const GifMessage = ({ url }: Props) => { + const styles = useStyles(); + + return (
+ { +
); +}; + +export default GifMessage; diff --git a/react/features/chat/middleware.js b/react/features/chat/middleware.js index 862eaef1e..ddf81b30e 100644 --- a/react/features/chat/middleware.js +++ b/react/features/chat/middleware.js @@ -18,6 +18,9 @@ import { } from '../base/participants'; import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux'; import { playSound, registerSound, unregisterSound } from '../base/sounds'; +import { addGif } from '../gifs/actions'; +import { GIF_PREFIX } from '../gifs/constants'; +import { getGifDisplayMode, isGifMessage } from '../gifs/functions'; import { NOTIFICATION_TIMEOUT_TYPE, showMessageNotification } from '../notifications'; import { resetNbUnreadPollsMessages } from '../polls/actions'; import { ADD_REACTION_MESSAGE } from '../reactions/actionTypes'; @@ -226,25 +229,21 @@ function _addChatMsgListener(conference, store) { conference.on( JitsiConferenceEvents.MESSAGE_RECEIVED, (id, message, timestamp) => { - _handleReceivedMessage(store, { - id, + _onConferenceMessageReceived(store, { id, message, - privateMessage: false, - lobbyChat: false, - timestamp - }); + timestamp, + privateMessage: false }); } ); conference.on( JitsiConferenceEvents.PRIVATE_MESSAGE_RECEIVED, (id, message, timestamp) => { - _handleReceivedMessage(store, { + _onConferenceMessageReceived(store, { id, message, - privateMessage: true, - lobbyChat: false, - timestamp + timestamp, + privateMessage: true }); } ); @@ -283,6 +282,45 @@ function _addChatMsgListener(conference, store) { }); } +/** + * Handles a received message. + * + * @param {Object} store - Redux store. + * @param {Object} message - The message object. + * @returns {void} + */ +function _onConferenceMessageReceived(store, { id, message, timestamp, privateMessage }) { + const isGif = isGifMessage(message); + + if (isGif) { + _handleGifMessageReceived(store, id, message); + if (getGifDisplayMode(store.getState()) === 'tile') { + return; + } + } + _handleReceivedMessage(store, { + id, + message, + privateMessage, + lobbyChat: false, + timestamp + }, true, isGif); +} + +/** + * Handles a received gif message. + * + * @param {Object} store - Redux store. + * @param {string} id - Id of the participant that sent the message. + * @param {string} message - The message sent. + * @returns {void} + */ +function _handleGifMessageReceived(store, id, message) { + const url = message.substring(GIF_PREFIX.length, message.length - 1); + + store.dispatch(addGif(id, url)); +} + /** * Handles a chat error received from the xmpp server. * diff --git a/react/features/filmstrip/components/web/Thumbnail.js b/react/features/filmstrip/components/web/Thumbnail.js index 3b106a353..de78ac040 100644 --- a/react/features/filmstrip/components/web/Thumbnail.js +++ b/react/features/filmstrip/components/web/Thumbnail.js @@ -24,6 +24,8 @@ import { updateLastTrackVideoMediaEvent } from '../../../base/tracks'; import { getVideoObjectPosition } from '../../../face-centering/functions'; +import { hideGif, showGif } from '../../../gifs/actions'; +import { getGifDisplayMode, getGifForParticipant } from '../../../gifs/functions'; import { PresenceLabel } from '../../../presence-status'; import { getCurrentLayout, LAYOUTS } from '../../../video-layout'; import { @@ -96,6 +98,11 @@ export type Props = {| */ _disableTileEnlargement: boolean, + /** + * URL of GIF sent by this participant, null if there's none. + */ + _gifSrc ?: string, + /** * The height of the Thumbnail. */ @@ -181,16 +188,16 @@ export type Props = {| */ _width: number, + /** + * An object containing CSS classes. + */ + classes: Object, + /** * The redux dispatch function. */ dispatch: Function, - /** - * An object containing the CSS classes. - */ - classes: Object, - /** * The horizontal offset in px for the thumbnail. Used to center the thumbnails from the last row in tile view. */ @@ -267,10 +274,14 @@ const defaultStyles = theme => { position: 'absolute', width: '100%', height: '100%', - zIndex: '9', + zIndex: 9, borderRadius: '4px' }, + borderIndicatorOnTop: { + zIndex: 11 + }, + activeSpeaker: { '& .active-speaker-indicator': { boxShadow: `inset 0px 0px 0px 4px ${theme.palette.link01Active} !important` @@ -281,6 +292,25 @@ const defaultStyles = theme => { '& .raised-hand-border': { boxShadow: `inset 0px 0px 0px 2px ${theme.palette.warning02} !important` } + }, + + gif: { + position: 'absolute', + width: '100%', + height: '100%', + zIndex: 11, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + overflow: 'hidden', + backgroundColor: theme.palette.ui02, + + '& img': { + maxWidth: '100%', + maxHeight: '100%', + objectFit: 'contain', + flexGrow: '1' + } } }; }; @@ -339,6 +369,8 @@ class Thumbnail extends Component { this._onTouchMove = this._onTouchMove.bind(this); this._showPopover = this._showPopover.bind(this); this._hidePopover = this._hidePopover.bind(this); + this._onGifMouseEnter = this._onGifMouseEnter.bind(this); + this._onGifMouseLeave = this._onGifMouseLeave.bind(this); } /** @@ -741,6 +773,52 @@ class Thumbnail extends Component { return className; } + _onGifMouseEnter: () => void; + + /** + * Keep showing the GIF for the current participant. + * + * @returns {void} + */ + _onGifMouseEnter() { + const { dispatch, _participant: { id } } = this.props; + + dispatch(showGif(id)); + } + + _onGifMouseLeave: () => void; + + /** + * Keep showing the GIF for the current participant. + * + * @returns {void} + */ + _onGifMouseLeave() { + const { dispatch, _participant: { id } } = this.props; + + dispatch(hideGif(id)); + } + + /** + * Renders GIF. + * + * @returns {Component} + */ + _renderGif() { + const { _gifSrc, classes } = this.props; + + return _gifSrc && ( +
+ GIF +
+ ); + } + _onCanPlay: Object => void; /** @@ -798,7 +876,8 @@ class Thumbnail extends Component { _localFlipX, _participant, _videoTrack, - classes + classes, + _gifSrc } = this.props; const { id } = _participant || {}; const { isHovered, popoverVisible } = this.state; @@ -850,9 +929,9 @@ class Thumbnail extends Component { } ) } style = { styles.thumbnail }> - {local + {!_gifSrc && (local ? {video} - : video} + : video)}
{ local = { local } participantId = { id } />
- { this._renderAvatar(styles.avatar) } + {!_gifSrc && this._renderAvatar(styles.avatar) } { !local && (
{
)} -
-
+ {this._renderGif()} +
+
); } @@ -1003,6 +1089,9 @@ function _mapStateToProps(state, ownProps): Object { } } + const { gifUrl: gifSrc } = getGifForParticipant(state, id); + const mode = getGifDisplayMode(state); + return { _audioTrack, _currentLayout, @@ -1023,7 +1112,8 @@ function _mapStateToProps(state, ownProps): Object { _raisedHand: hasRaisedHand(participant), _videoObjectPosition: getVideoObjectPosition(state, participant?.id), _videoTrack, - ...size + ...size, + _gifSrc: mode === 'chat' ? null : gifSrc }; } diff --git a/react/features/gifs/actionTypes.js b/react/features/gifs/actionTypes.js new file mode 100644 index 000000000..894075863 --- /dev/null +++ b/react/features/gifs/actionTypes.js @@ -0,0 +1,55 @@ +/** + * Adds a gif for a given participant. + * {{ + * type: ADD_GIF_FOR_PARTICIPANT, + * participantId: string, + * gifUrl: string, + * timeoutID: number + * }} + */ +export const ADD_GIF_FOR_PARTICIPANT = 'ADD_GIF_FOR_PARTICIPANT'; + +/** + * Set timeout to hide a gif for a given participant. + * {{ + * type: HIDE_GIF_FOR_PARTICIPANT, + * participantId: string + * }} + */ +export const HIDE_GIF_FOR_PARTICIPANT = 'HIDE_GIF_FOR_PARTICIPANT'; + +/** + * Removes a gif for a given participant. + * {{ + * type: REMOVE_GIF_FOR_PARTICIPANT, + * participantId: string + * }} + */ +export const REMOVE_GIF_FOR_PARTICIPANT = 'REMOVE_GIF_FOR_PARTICIPANT'; + +/** + * Set gif menu drawer visibility. + * {{ + * type: SET_GIF_DRAWER_VISIBILITY, + * visible: boolean + * }} + */ +export const SET_GIF_DRAWER_VISIBILITY = 'SET_GIF_DRAWER_VISIBILITY'; + +/** + * Set gif menu visibility. + * {{ + * type: SET_GIF_MENU_VISIBILITY, + * visible: boolean + * }} + */ +export const SET_GIF_MENU_VISIBILITY = 'SET_GIF_MENU_VISIBILITY'; + +/** + * Keep showing a gif for a given participant. + * {{ + * type: SHOW_GIF_FOR_PARTICIPANT, + * participantId: string + * }} + */ +export const SHOW_GIF_FOR_PARTICIPANT = 'SHOW_GIF_FOR_PARTICIPANT'; diff --git a/react/features/gifs/actions.js b/react/features/gifs/actions.js new file mode 100644 index 000000000..7a33ce5d6 --- /dev/null +++ b/react/features/gifs/actions.js @@ -0,0 +1,88 @@ +import { + ADD_GIF_FOR_PARTICIPANT, + HIDE_GIF_FOR_PARTICIPANT, + REMOVE_GIF_FOR_PARTICIPANT, + SET_GIF_DRAWER_VISIBILITY, + SET_GIF_MENU_VISIBILITY, + SHOW_GIF_FOR_PARTICIPANT +} from './actionTypes'; + +/** + * Adds a GIF for a given participant. + * + * @param {string} participantId - The id of the participant that sent the GIF. + * @param {string} gifUrl - The URL of the GIF. + * @returns {Object} + */ +export function addGif(participantId, gifUrl) { + return { + type: ADD_GIF_FOR_PARTICIPANT, + participantId, + gifUrl + }; +} + +/** + * Removes the GIF of the given participant. + * + * @param {string} participantId - The Id of the participant for whom to remove the GIF. + * @returns {Object} + */ +export function removeGif(participantId) { + return { + type: REMOVE_GIF_FOR_PARTICIPANT, + participantId + }; +} + +/** + * Keep showing the GIF of the given participant. + * + * @param {string} participantId - The Id of the participant for whom to show the GIF. + * @returns {Object} + */ +export function showGif(participantId) { + return { + type: SHOW_GIF_FOR_PARTICIPANT, + participantId + }; +} + +/** + * Set timeout to hide the GIF of the given participant. + * + * @param {string} participantId - The Id of the participant for whom to show the GIF. + * @returns {Object} + */ +export function hideGif(participantId) { + return { + type: HIDE_GIF_FOR_PARTICIPANT, + participantId + }; +} + +/** + * Set visibility of the GIF drawer. + * + * @param {boolean} visible - Whether or not it should be visible. + * @returns {Object} + */ +export function setGifDrawerVisibility(visible) { + return { + type: SET_GIF_DRAWER_VISIBILITY, + visible + }; +} + +/** + * Set visibility of the GIF menu. + * + * @param {boolean} visible - Whether or not it should be visible. + * @returns {Object} + */ +export function setGifMenuVisibility(visible) { + return { + type: SET_GIF_MENU_VISIBILITY, + visible + }; +} diff --git a/react/features/gifs/components/_.web.js b/react/features/gifs/components/_.web.js new file mode 100644 index 000000000..b80c83af3 --- /dev/null +++ b/react/features/gifs/components/_.web.js @@ -0,0 +1 @@ +export * from './web'; diff --git a/react/features/gifs/components/index.js b/react/features/gifs/components/index.js new file mode 100644 index 000000000..cda61441e --- /dev/null +++ b/react/features/gifs/components/index.js @@ -0,0 +1 @@ +export * from './_'; diff --git a/react/features/gifs/components/web/GifsMenu.js b/react/features/gifs/components/web/GifsMenu.js new file mode 100644 index 000000000..d2f8da7bd --- /dev/null +++ b/react/features/gifs/components/web/GifsMenu.js @@ -0,0 +1,223 @@ +// @flow + +import { GiphyFetch } from '@giphy/js-fetch-api'; +import { Grid } from '@giphy/react-components'; +import { makeStyles } from '@material-ui/core'; +import clsx from 'clsx'; +import React, { useCallback, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { batch, useDispatch, useSelector } from 'react-redux'; + +import { createGifSentEvent, sendAnalytics } from '../../../analytics'; +import InputField from '../../../base/premeeting/components/web/InputField'; +import BaseTheme from '../../../base/ui/components/BaseTheme'; +import { sendMessage } from '../../../chat/actions.any'; +import { SCROLL_SIZE } from '../../../filmstrip'; +import { toggleReactionsMenuVisibility } from '../../../reactions/actions.web'; +import { setOverflowMenuVisible } from '../../../toolbox/actions.web'; +import { Drawer, JitsiPortal } from '../../../toolbox/components/web'; +import { showOverflowDrawer } from '../../../toolbox/functions.web'; +import { setGifDrawerVisibility } from '../../actions'; +import { formatGifUrlMessage, getGifAPIKey, getGifUrl } from '../../functions'; + +const OVERFLOW_DRAWER_PADDING = BaseTheme.spacing(3); + +const useStyles = makeStyles(theme => { + return { + gifsMenu: { + width: '100%', + marginBottom: `${theme.spacing(2)}px`, + display: 'flex', + flexDirection: 'column', + + '& div:focus': { + border: '1px solid red !important', + boxSizing: 'border-box' + } + }, + + searchField: { + backgroundColor: theme.palette.field01, + borderRadius: `${theme.shape.borderRadius}px`, + border: 'none', + outline: 0, + ...theme.typography.bodyShortRegular, + lineHeight: `${theme.typography.bodyShortRegular.lineHeight}px`, + color: theme.palette.text01, + padding: `${theme.spacing(2)}px ${theme.spacing(3)}px`, + width: '100%', + marginBottom: `${theme.spacing(3)}px` + }, + + gifContainer: { + height: '245px', + overflowY: 'auto' + }, + + logoContainer: { + width: `calc(100% - ${SCROLL_SIZE}px)`, + backgroundColor: '#121119', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + color: '#fff', + marginTop: `${theme.spacing(1)}px` + }, + + overflowMenu: { + padding: `${theme.spacing(3)}px`, + width: '100%', + boxSizing: 'border-box' + }, + + gifContainerOverflow: { + flexGrow: 1 + }, + + drawer: { + display: 'flex', + height: '100%' + } + }; +}); + +/** + * Gifs menu. + * + * @returns {ReactElement} + */ +function GifsMenu() { + const API_KEY = useSelector(getGifAPIKey); + const giphyFetch = new GiphyFetch(API_KEY); + const [ searchKey, setSearchKey ] = useState(); + const styles = useStyles(); + const dispatch = useDispatch(); + const { t } = useTranslation(); + const overflowDrawer = useSelector(showOverflowDrawer); + const { clientWidth } = useSelector(state => state['features/base/responsive-ui']); + + const fetchGifs = useCallback(async (offset = 0) => { + const options = { + rating: 'pg-13', + limit: 20, + offset + }; + + if (!searchKey) { + return await giphyFetch.trending(options); + } + + return await giphyFetch.search(searchKey, options); + }, [ searchKey ]); + + const onDrawerClose = useCallback(() => { + dispatch(setGifDrawerVisibility(false)); + dispatch(setOverflowMenuVisible(false)); + }); + + const handleGifClick = useCallback((gif, e) => { + e?.stopPropagation(); + const url = getGifUrl(gif); + + sendAnalytics(createGifSentEvent()); + batch(() => { + dispatch(sendMessage(formatGifUrlMessage(url), true)); + dispatch(toggleReactionsMenuVisibility()); + overflowDrawer && onDrawerClose(); + }); + }, [ dispatch, overflowDrawer ]); + + const handleGifKeyPress = useCallback((gif, e) => { + if (e.nativeEvent.keyCode === 13) { + handleGifClick(gif, null); + } + }, [ handleGifClick ]); + + const handleSearchKeyChange = useCallback(value => { + setSearchKey(value); + }); + + const handleKeyDown = useCallback(e => { + if (e.keyCode === 38) { // up arrow + e.preventDefault(); + + // if the first gif is focused move focus to the input + if (document.activeElement.previousElementSibling === null) { + document.querySelector('.gif-input').focus(); + } else { + document.activeElement.previousElementSibling.focus(); + } + } else if (e.keyCode === 40) { // down arrow + e.preventDefault(); + + // if the input is focused move focus to the first gif + if (document.activeElement.classList.contains('gif-input')) { + document.querySelector('.giphy-gif').focus(); + } else { + document.activeElement.nextElementSibling.focus(); + } + } + }, []); + + useEffect(() => { + document.addEventListener('keydown', handleKeyDown); + + return () => document.removeEventListener('keydown', handleKeyDown); + }, []); + + // For some reason, the Grid component does not do an initial call on mobile. + // This fixes that. + useEffect(() => setSearchKey(''), []); + + const gifMenu = ( +
+ +
+ +
+
+ Powered by + GIPHY Logo +
+
+ ); + + return overflowDrawer ? ( + + + {gifMenu} + + + ) : gifMenu; +} + +export default GifsMenu; diff --git a/react/features/gifs/components/web/GifsMenuButton.js b/react/features/gifs/components/web/GifsMenuButton.js new file mode 100644 index 000000000..a9cca3075 --- /dev/null +++ b/react/features/gifs/components/web/GifsMenuButton.js @@ -0,0 +1,42 @@ +import React, { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useDispatch, useSelector } from 'react-redux'; + +import ReactionButton from '../../../reactions/components/web/ReactionButton'; +import { showOverflowDrawer } from '../../../toolbox/functions.web'; +import { setGifDrawerVisibility, setGifMenuVisibility } from '../../actions'; +import { isGifsMenuOpen } from '../../functions'; + +const GifsMenuButton = () => { + const menuOpen = useSelector(isGifsMenuOpen); + const overflowDrawer = useSelector(showOverflowDrawer); + const { t } = useTranslation(); + const dispatch = useDispatch(); + + const icon = ( + GIPHY Logo + ); + + const handleClick = useCallback(() => + dispatch( + overflowDrawer + ? setGifDrawerVisibility(!menuOpen) + : setGifMenuVisibility(!menuOpen) + ) + , [ menuOpen, overflowDrawer ]); + + return ( + + ); +}; + +export default GifsMenuButton; diff --git a/react/features/gifs/components/web/index.js b/react/features/gifs/components/web/index.js new file mode 100644 index 000000000..18b114f87 --- /dev/null +++ b/react/features/gifs/components/web/index.js @@ -0,0 +1,4 @@ +// @flow + +export { default as GifsMenuButton } from './GifsMenuButton'; +export { default as GifsMenu } from './GifsMenu'; diff --git a/react/features/gifs/constants.js b/react/features/gifs/constants.js new file mode 100644 index 000000000..03d848091 --- /dev/null +++ b/react/features/gifs/constants.js @@ -0,0 +1,9 @@ +/** + * The default time that GIFs will be displayed on the tile. + */ +export const GIF_DEFAULT_TIMEOUT = 5000; + +/** + * The prefix for formatted GIF messages. + */ +export const GIF_PREFIX = 'gif['; diff --git a/react/features/gifs/functions.js b/react/features/gifs/functions.js new file mode 100644 index 000000000..a355ffece --- /dev/null +++ b/react/features/gifs/functions.js @@ -0,0 +1,96 @@ +import { showOverflowDrawer } from '../toolbox/functions.web'; + +import { GIF_PREFIX } from './constants'; + +/** + * Gets the URL of the GIF for the given participant or null if there's none. + * + * @param {Object} state - Redux state. + * @param {string} participantId - Id of the participant for which to remove the GIF. + * @returns {Object} + */ +export function getGifForParticipant(state, participantId) { + return state['features/gifs'].gifList.get(participantId) || {}; +} + +/** + * Whether or not the message is a GIF message. + * + * @param {string} message - Message to check. + * @returns {boolean} + */ +export function isGifMessage(message) { + return message.trim().startsWith(GIF_PREFIX); +} + +/** + * Returns the visibility state of the gifs menu. + * + * @param {Object} state - The state of the application. + * @returns {boolean} + */ +export function isGifsMenuOpen(state) { + const overflowDrawer = showOverflowDrawer(state); + const { drawerVisible, menuOpen } = state['features/gifs']; + + return overflowDrawer ? drawerVisible : menuOpen; +} + +/** + * Returns the url of the gif selected in the gifs menu. + * + * @param {Object} gif - The gif data. + * @returns {boolean} + */ +export function getGifUrl(gif) { + const embedUrl = gif?.embed_url || ''; + const idx = embedUrl.lastIndexOf('/'); + const id = embedUrl.substr(idx + 1); + + return `https://i.giphy.com/media/${id}/giphy.webp`; +} + +/** + * Formats the gif message. + * + * @param {string} url - GIF url. + * @returns {string} + */ +export function formatGifUrlMessage(url) { + return `${GIF_PREFIX}${url}]`; +} + +/** + * Get the Giphy API Key from config. + * + * @param {Object} state - Redux state. + * @returns {string} + */ +export function getGifAPIKey(state) { + return state['features/base/config']?.giphy?.sdkKey; +} + +/** + * Returns whether or not the feature is enabled. + * + * @param {Object} state - Redux state. + * @returns {boolean} + */ +export function isGifEnabled(state) { + const { disableThirdPartyRequests } = state['features/base/config']; + const { giphy } = state['features/base/config']; + + return !disableThirdPartyRequests && giphy?.enabled && Boolean(giphy?.sdkKey); +} + +/** + * Get the GIF display mode. + * + * @param {Object} state - Redux state. + * @returns {string} + */ +export function getGifDisplayMode(state) { + const { giphy } = state['features/base/config']; + + return giphy?.displayMode || 'all'; +} diff --git a/react/features/gifs/middleware.js b/react/features/gifs/middleware.js new file mode 100644 index 000000000..731b8e2d9 --- /dev/null +++ b/react/features/gifs/middleware.js @@ -0,0 +1,60 @@ +import { MiddlewareRegistry } from '../base/redux'; + +import { ADD_GIF_FOR_PARTICIPANT, HIDE_GIF_FOR_PARTICIPANT, SHOW_GIF_FOR_PARTICIPANT } from './actionTypes'; +import { removeGif } from './actions'; +import { GIF_DEFAULT_TIMEOUT } from './constants'; +import { getGifForParticipant } from './functions'; + +/** + * Middleware which intercepts Gifs actions to handle changes to the + * visibility timeout of the Gifs. + * + * @param {Store} store - The redux store. + * @returns {Function} + */ +MiddlewareRegistry.register(store => next => action => { + const { dispatch, getState } = store; + const state = getState(); + + switch (action.type) { + case ADD_GIF_FOR_PARTICIPANT: { + const id = action.participantId; + const { giphy } = state['features/base/config']; + + _clearGifTimeout(state, id); + const timeoutID = setTimeout(() => dispatch(removeGif(id)), giphy?.tileTime || GIF_DEFAULT_TIMEOUT); + + action.timeoutID = timeoutID; + break; + } + case SHOW_GIF_FOR_PARTICIPANT: { + const id = action.participantId; + + _clearGifTimeout(state, id); + break; + } + case HIDE_GIF_FOR_PARTICIPANT: { + const { giphy } = state['features/base/config']; + const id = action.participantId; + const timeoutID = setTimeout(() => dispatch(removeGif(id)), giphy?.tileTime || GIF_DEFAULT_TIMEOUT); + + action.timeoutID = timeoutID; + break; + } + } + + return next(action); +}); + +/** + * Clears GIF timeout. + * + * @param {Object} state - Redux state. + * @param {string} id - Id of the participant for whom to clear the timeout. + * @returns {void} + */ +function _clearGifTimeout(state, id) { + const gif = getGifForParticipant(state, id); + + clearTimeout(gif?.timeoutID); +} diff --git a/react/features/gifs/reducer.js b/react/features/gifs/reducer.js new file mode 100644 index 000000000..c9e171b68 --- /dev/null +++ b/react/features/gifs/reducer.js @@ -0,0 +1,73 @@ + +import { ReducerRegistry } from '../base/redux'; + +import { + ADD_GIF_FOR_PARTICIPANT, + HIDE_GIF_FOR_PARTICIPANT, + REMOVE_GIF_FOR_PARTICIPANT, + SET_GIF_DRAWER_VISIBILITY, + SET_GIF_MENU_VISIBILITY +} from './actionTypes'; + +const initialState = { + drawerVisible: false, + gifList: new Map(), + menuOpen: false +}; + +ReducerRegistry.register( + 'features/gifs', + (state = initialState, action) => { + switch (action.type) { + case ADD_GIF_FOR_PARTICIPANT: { + const newList = state.gifList; + + newList.set(action.participantId, { + gifUrl: action.gifUrl, + timeoutID: action.timeoutID + }); + + return { + ...state, + gifList: newList + }; + } + case REMOVE_GIF_FOR_PARTICIPANT: { + const newList = state.gifList; + + newList.delete(action.participantId); + + return { + ...state, + gifList: newList + }; + } + case HIDE_GIF_FOR_PARTICIPANT: { + const newList = state.gifList; + const gif = state.gifList.get(action.participantId); + + newList.set(action.participantId, { + gifUrl: gif.gifUrl, + timeoutID: action.timeoutID + }); + + return { + ...state, + gifList: newList + }; + } + case SET_GIF_DRAWER_VISIBILITY: + return { + ...state, + drawerVisible: action.visible + }; + case SET_GIF_MENU_VISIBILITY: + return { + ...state, + menuOpen: action.visible + }; + } + + return state; + }); + diff --git a/react/features/reactions/components/web/ReactionsMenu.js b/react/features/reactions/components/web/ReactionsMenu.js index 16ccb55eb..1fbc82ed1 100644 --- a/react/features/reactions/components/web/ReactionsMenu.js +++ b/react/features/reactions/components/web/ReactionsMenu.js @@ -3,6 +3,7 @@ /* eslint-disable react/jsx-no-bind */ import { withStyles } from '@material-ui/styles'; +import clsx from 'clsx'; import React, { Component } from 'react'; import { bindActionCreators } from 'redux'; @@ -15,6 +16,8 @@ import { isMobileBrowser } from '../../../base/environment/utils'; import { translate } from '../../../base/i18n'; import { getLocalParticipant, hasRaisedHand, raiseHand } from '../../../base/participants'; import { connect } from '../../../base/redux'; +import { GifsMenu, GifsMenuButton } from '../../../gifs/components'; +import { isGifEnabled, isGifsMenuOpen } from '../../../gifs/functions'; import { dockToolbox } from '../../../toolbox/actions.web'; import { addReactionToBuffer } from '../../actions.any'; import { toggleReactionsMenuVisibility } from '../../actions.web'; @@ -29,6 +32,16 @@ type Props = { */ _dockToolbox: Function, + /** + * Whether or not the GIF feature is enabled. + */ + _isGifEnabled: boolean, + + /** + * Whether or not the GIF menu is visible. + */ + _isGifMenuVisible: boolean, + /** * Whether or not it's a mobile browser. */ @@ -193,12 +206,16 @@ class ReactionsMenu extends Component { * @inheritdoc */ render() { - const { _raisedHand, t, overflowMenu, _isMobile, classes } = this.props; + const { _raisedHand, t, overflowMenu, _isMobile, classes, _isGifMenuVisible, _isGifEnabled } = this.props; return ( -
+
+ {_isGifEnabled && _isGifMenuVisible && }
{ this._getReactionButtons() } + {_isGifEnabled && }
{_isMobile && (
@@ -231,6 +248,8 @@ function mapStateToProps(state) { return { _localParticipantID: localParticipant.id, _isMobile: isMobileBrowser(), + _isGifEnabled: isGifEnabled(state), + _isGifMenuVisible: isGifsMenuOpen(state), _raisedHand: hasRaisedHand(localParticipant) }; } diff --git a/react/features/toolbox/components/web/Drawer.js b/react/features/toolbox/components/web/Drawer.js index 40f160ed2..6e1809b94 100644 --- a/react/features/toolbox/components/web/Drawer.js +++ b/react/features/toolbox/components/web/Drawer.js @@ -8,6 +8,11 @@ import { DRAWER_MAX_HEIGHT } from '../../constants'; type Props = { + /** + * Class name for custom styles. + */ + className: string, + /** * The component(s) to be displayed within the drawer menu. */ @@ -40,6 +45,7 @@ const useStyles = makeStyles(theme => { */ function Drawer({ children, + className = '', isOpen, onClose }: Props) { @@ -72,7 +78,7 @@ function Drawer({ className = 'drawer-menu-container' onClick = { handleOutsideClick }>
{children}
diff --git a/react/features/toolbox/components/web/Toolbox.js b/react/features/toolbox/components/web/Toolbox.js index 433b95b13..e4e94b071 100644 --- a/react/features/toolbox/components/web/Toolbox.js +++ b/react/features/toolbox/components/web/Toolbox.js @@ -3,6 +3,7 @@ import { withStyles } from '@material-ui/core/styles'; import clsx from 'clsx'; import React, { Component, Fragment } from 'react'; +import { batch } from 'react-redux'; import keyboardShortcut from '../../../../../modules/keyboardshortcut/keyboardshortcut'; import { @@ -30,6 +31,8 @@ import { ChatButton } from '../../../chat/components'; import { EmbedMeetingButton } from '../../../embed-meeting'; import { SharedDocumentButton } from '../../../etherpad'; import { FeedbackButton } from '../../../feedback'; +import { setGifMenuVisibility } from '../../../gifs/actions'; +import { isGifEnabled } from '../../../gifs/functions'; import { InviteButton } from '../../../invite/components/add-people-dialog'; import { isVpaasMeeting } from '../../../jaas/functions'; import { KeyboardShortcutsButton } from '../../../keyboard-shortcuts'; @@ -41,6 +44,7 @@ import { import { ParticipantsPaneButton } from '../../../participants-pane/components/web'; import { getParticipantsPaneOpen } from '../../../participants-pane/functions'; import { addReactionToBuffer } from '../../../reactions/actions.any'; +import { toggleReactionsMenuVisibility } from '../../../reactions/actions.web'; import { ReactionsMenuButton } from '../../../reactions/components'; import { REACTIONS, REACTIONS_MENU_HEIGHT } from '../../../reactions/constants'; import { isReactionsEnabled } from '../../../reactions/functions.any'; @@ -159,6 +163,11 @@ type Props = { */ _fullScreen: boolean, + /** + * Whether or not the GIFs feature is enabled. + */ + _gifsEnabled: boolean, + /** * Whether the app has Salesforce integration. */ @@ -334,7 +343,7 @@ class Toolbox extends Component { * @returns {void} */ componentDidMount() { - const { _toolbarButtons, t, dispatch, _reactionsEnabled } = this.props; + const { _toolbarButtons, t, dispatch, _reactionsEnabled, _gifsEnabled } = this.props; const KEYBOARD_SHORTCUTS = [ isToolbarButtonEnabled('videoquality', _toolbarButtons) && { character: 'A', @@ -408,6 +417,22 @@ class Toolbox extends Component { shortcut.helpDescription, shortcut.altKey); }); + + if (_gifsEnabled) { + const onGifShortcut = () => { + batch(() => { + dispatch(toggleReactionsMenuVisibility()); + dispatch(setGifMenuVisibility(true)); + }); + }; + + APP.keyboardshortcut.registerShortcut( + 'G', + null, + onGifShortcut, + t('keyboardShortcuts.giphyMenu') + ); + } } } @@ -1410,6 +1435,7 @@ function _mapStateToProps(state, ownProps) { _disabled: Boolean(iAmRecorder || iAmSipGateway), _feedbackConfigured: Boolean(callStatsID), _fullScreen: fullScreen, + _gifsEnabled: isGifEnabled(state), _isProfileDisabled: Boolean(disableProfile), _isIosMobile: isIosMobileBrowser(), _isMobile: isMobileBrowser(),