From 190041fc5a1c016564f34a1fd56d2f6ae8099a2e Mon Sep 17 00:00:00 2001 From: Robert Pintilii Date: Fri, 11 Mar 2022 15:00:49 +0200 Subject: [PATCH] feat(gif) Added GIF support (GIPHY integration) (#11021) Show GIF menu in reactions menu Search GIFs using the GIPHY API Show GIFs as images in chat Show GIFs on the thumbnail of the participant that sent it Move GIF focus using up/ down arrows and send with Enter Added analytics --- config.js | 15 + css/_reactions-menu.scss | 18 + images/GIPHY_icon.png | Bin 0 -> 284 bytes images/GIPHY_logo.png | Bin 0 -> 1066 bytes lang/main.json | 7 + package-lock.json | 669 +++++++++++++++++- package.json | 2 + react/features/analytics/AnalyticsEvents.js | 12 + react/features/app/middlewares.any.js | 1 + react/features/app/reducers.any.js | 1 + react/features/base/config/configWhitelist.js | 1 + .../premeeting/components/web/InputField.js | 4 + .../base/react/components/web/Message.js | 29 +- .../chat/components/web/GifMessage.js | 42 ++ react/features/chat/middleware.js | 58 +- .../filmstrip/components/web/Thumbnail.js | 116 ++- react/features/gifs/actionTypes.js | 55 ++ react/features/gifs/actions.js | 88 +++ react/features/gifs/components/_.web.js | 1 + react/features/gifs/components/index.js | 1 + .../features/gifs/components/web/GifsMenu.js | 223 ++++++ .../gifs/components/web/GifsMenuButton.js | 42 ++ react/features/gifs/components/web/index.js | 4 + react/features/gifs/constants.js | 9 + react/features/gifs/functions.js | 96 +++ react/features/gifs/middleware.js | 60 ++ react/features/gifs/reducer.js | 73 ++ .../reactions/components/web/ReactionsMenu.js | 23 +- .../features/toolbox/components/web/Drawer.js | 8 +- .../toolbox/components/web/Toolbox.js | 28 +- 30 files changed, 1648 insertions(+), 38 deletions(-) create mode 100644 images/GIPHY_icon.png create mode 100644 images/GIPHY_logo.png create mode 100644 react/features/chat/components/web/GifMessage.js create mode 100644 react/features/gifs/actionTypes.js create mode 100644 react/features/gifs/actions.js create mode 100644 react/features/gifs/components/_.web.js create mode 100644 react/features/gifs/components/index.js create mode 100644 react/features/gifs/components/web/GifsMenu.js create mode 100644 react/features/gifs/components/web/GifsMenuButton.js create mode 100644 react/features/gifs/components/web/index.js create mode 100644 react/features/gifs/constants.js create mode 100644 react/features/gifs/functions.js create mode 100644 react/features/gifs/middleware.js create mode 100644 react/features/gifs/reducer.js 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 0000000000000000000000000000000000000000..575628c23821329eec2095297f28e22c35740c21 GIT binary patch literal 284 zcmeAS@N?(olHy`uVBq!ia0vp^qChOo!VDx6jc;BBQY`6?zK#qG8~eHcB(ehe3dtTp zz6=aiY77hwEes65fI;OkH}&M25un`X1sK_?hjDV&eO#)MB;LC025=XxI3Ey!{);R zIaiH1b}ue|c4p@0n3$N3#N8GT(wh&wW9IlakDKFLq{_Fu2M^Y{#tfQ&Ai~X89%D%3dI82Gy5z>w8X;s6Yz$q3p%b4@J>pLP?P@s31sK81+HH zC@L_t_YafI7=ebcbn;X;IezRvvcqHJ-^@c zoO23F)^AB5iM(Q!41pv;LXsd!kdP!S9jq4pI2GHU^84Ajzo6uy64|lmLkkjQvWsa?+9f^*iXk-k>66<N&r?VV+FPziN zkxvA_|9WnWdFog#x%AN5dXoesy$S!aRF;5@uBVSpj{_A2*-ry1)o!<`_k4mu1;pcu zb)H}pWn~U@-g{P4%7H)|@d20wlrc;K2=LS%5x%${=uCz2-0nSU8-jD_aE+mHX*BsR zqJhB>Log&55Jbx+;9cK)B8Qe5GY!wcFw@>y)PUBl*@*A7-r)7JB|AQ@4Ri#P(bN5$ z+#3y4fR+nOW@X)YEmNJo{oEtNkFf8ZjEQLuZ@iAs+rsjEhNvOU=QMqQ$UaaXx3iP@Q2v3crPX~311^q*yOc4S6Bq2y@;gv#jZdL%q@ zF!G%L4LLo^ZfrQM*=>pd+PV^uQxZTwVS@&{T2YpRhBr2zQ8#YO9l}%8>Cl9sK}!yn zroE=GqU0%tgAWFtZ4HMb5;#H^$r#@!o8FRF5+u?v=He@LGD7*5BZ*~b%o36WNrHqV kVaczK_Q^2h7ykqp0B={K))BsD1poj507*qoM6N<$f-!yU(EtDd literal 0 HcmV?d00001 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(),