diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js index 6665ff58eb..9d29b934c6 100644 --- a/app/javascript/flavours/glitch/actions/compose.js +++ b/app/javascript/flavours/glitch/actions/compose.js @@ -67,7 +67,7 @@ export const COMPOSE_UPLOAD_CHANGE_SUCCESS = 'COMPOSE_UPLOAD_UPDATE_SUCCESS' export const COMPOSE_UPLOAD_CHANGE_FAIL = 'COMPOSE_UPLOAD_UPDATE_FAIL'; export const COMPOSE_DOODLE_SET = 'COMPOSE_DOODLE_SET'; -export const COMPOSE_GIPHY_SET = 'COMPOSE_GIPHY_SET'; +export const COMPOSE_TENOR_SET = 'COMPOSE_TENOR_SET'; export const COMPOSE_POLL_ADD = 'COMPOSE_POLL_ADD'; export const COMPOSE_POLL_REMOVE = 'COMPOSE_POLL_REMOVE'; @@ -287,9 +287,9 @@ export function doodleSet(options) { }; } -export function giphySet(options) { +export function tenorSet(options) { return { - type: COMPOSE_GIPHY_SET, + type: COMPOSE_TENOR_SET, options: options, }; }; diff --git a/app/javascript/flavours/glitch/features/compose/components/options.js b/app/javascript/flavours/glitch/features/compose/components/options.js index e6ad2a3f59..e572ac5a52 100644 --- a/app/javascript/flavours/glitch/features/compose/components/options.js +++ b/app/javascript/flavours/glitch/features/compose/components/options.js @@ -133,7 +133,7 @@ class ComposerOptions extends ImmutablePureComponent { onChangeContentType: PropTypes.func, onTogglePoll: PropTypes.func, onDoodleOpen: PropTypes.func, - onEmbedGiphy: PropTypes.func, + onEmbedTenor: PropTypes.func, onModalClose: PropTypes.func, onModalOpen: PropTypes.func, onToggleSpoiler: PropTypes.func, @@ -156,7 +156,7 @@ class ComposerOptions extends ImmutablePureComponent { // Handles attachment clicks. handleClickAttach = (name) => { const { fileElement } = this; - const { onDoodleOpen, onEmbedGiphy } = this.props; + const { onDoodleOpen, onEmbedTenor } = this.props; // We switch over the name of the option. switch (name) { @@ -171,8 +171,8 @@ class ComposerOptions extends ImmutablePureComponent { } return; case 'gif': - if (onEmbedGiphy) { - onEmbedGiphy(); + if (onEmbedTenor) { + onEmbedTenor(); } return; } diff --git a/app/javascript/flavours/glitch/features/compose/containers/options_container.js b/app/javascript/flavours/glitch/features/compose/containers/options_container.js index 655780affd..cf1dac545b 100644 --- a/app/javascript/flavours/glitch/features/compose/containers/options_container.js +++ b/app/javascript/flavours/glitch/features/compose/containers/options_container.js @@ -49,8 +49,8 @@ const mapDispatchToProps = (dispatch) => ({ dispatch(openModal('DOODLE', { noEsc: true, noClose: true })); }, - onEmbedGiphy() { - dispatch(openModal('GIPHY', { noEsc: true })); + onEmbedTenor() { + dispatch(openModal('TENOR', { noEsc: true })); }, onModalClose() { diff --git a/app/javascript/flavours/glitch/features/ui/components/gif_modal.js b/app/javascript/flavours/glitch/features/ui/components/gif_modal.js index a13bfd60cb..d9e22a5a22 100644 --- a/app/javascript/flavours/glitch/features/ui/components/gif_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/gif_modal.js @@ -3,16 +3,16 @@ import PropTypes from 'prop-types'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { connect } from 'react-redux'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import { giphySet, uploadCompose } from 'flavours/glitch/actions/compose'; +import { tenorSet, uploadCompose } from 'flavours/glitch/actions/compose'; import IconButton from 'flavours/glitch/components/icon_button'; -import ReactGiphySearchbox from 'react-giphy-searchbox' +import Tenor from 'react-tenor'; import { defineMessages, injectIntl } from 'react-intl'; const messages = defineMessages({ - search: { id: 'giphy.search', defaultMessage: 'Search for GIFs' }, - error: { id: 'giphy.error', defaultMessage: 'Oops! Something went wrong. Please, try again.' }, - loading: { id: 'giphy.loading', defaultMessage: 'Loading...'}, - nomatches: { id: 'giphy.nomatches', defaultMessage: 'No matches found.' }, + search: { id: 'tenor.search', defaultMessage: 'Search for GIFs' }, + error: { id: 'tenor.error', defaultMessage: 'Oops! Something went wrong. Please, try again.' }, + loading: { id: 'tenor.loading', defaultMessage: 'Loading...' }, + nomatches: { id: 'tenor.nomatches', defaultMessage: 'No matches found.' }, close: { id: 'settings.close', defaultMessage: 'Close' }, }); @@ -28,12 +28,12 @@ function dataURLtoFile(dataurl, filename) { } const mapStateToProps = state => ({ - options: state.getIn(['compose', 'giphy']), + options: state.getIn(['compose', 'tenor']), }); const mapDispatchToProps = dispatch => ({ /** Set options in the redux store */ - setOpt: (opts) => dispatch(giphySet(opts)), + setOpt: (opts) => dispatch(tenorSet(opts)), /** Submit GIF for upload */ submit: (file) => dispatch(uploadCompose([file])), }); @@ -50,9 +50,10 @@ class GIFModal extends ImmutablePureComponent { submit: PropTypes.func.isRequired, }; - onDoneButton = (item) => { - const url = item["images"]["original"]["mp4"]; + onDoneButton = (result) => { + const url = result.media[0].mp4.url; var modal = this; + // eslint-disable-next-line promise/catch-or-return fetch(url).then(function(response) { return response.blob(); }).then(function(blob) { @@ -60,7 +61,7 @@ class GIFModal extends ImmutablePureComponent { reader.readAsDataURL(blob); reader.onloadend = function() { var dataUrl = reader.result; - const file = dataURLtoFile(dataUrl, 'giphy.mp4'); + const file = dataURLtoFile(dataUrl, 'tenor.mp4'); modal.props.submit(file); modal.props.onClose(); // close dialog }; @@ -72,23 +73,26 @@ class GIFModal extends ImmutablePureComponent { const { intl } = this.props; return ( -
-
-
- this.onDoneButton(item)} - masonryConfig={[ - { columns: 2, imageWidth: 190, gutter: 5 }, - { mq: "700px", columns: 2, imageWidth: 210, gutter: 5 } - ]} - autoFocus="true" - searchPlaceholder={intl.formatMessage(messages.search)} - messageError={intl.formatMessage(messages.error)} - messageLoading={intl.formatMessage(messages.loading)} - messageNoMatches={intl.formatMessage(messages.nomatches)} - wrapperClassName="giphy-modal__searchbox" - /> +
+
+
+
+ this.onDoneButton(result)} + masonryConfig={[ + { columns: 2, imageWidth: 190, gutter: 5 }, + { mq: '700px', columns: 2, imageWidth: 210, gutter: 5 } + ]} + autoFocus='true' + searchPlaceholder={intl.formatMessage(messages.search)} + messageError={intl.formatMessage(messages.error)} + messageLoading={intl.formatMessage(messages.loading)} + messageNoMatches={intl.formatMessage(messages.nomatches)} + /> +
+
Tenor logo
); diff --git a/app/javascript/flavours/glitch/features/ui/components/modal_root.js b/app/javascript/flavours/glitch/features/ui/components/modal_root.js index c98f6038b9..10fdbad33e 100644 --- a/app/javascript/flavours/glitch/features/ui/components/modal_root.js +++ b/app/javascript/flavours/glitch/features/ui/components/modal_root.js @@ -44,7 +44,7 @@ const MODAL_COMPONENTS = { 'BOOST': () => Promise.resolve({ default: BoostModal }), 'FAVOURITE': () => Promise.resolve({ default: FavouriteModal }), 'DOODLE': () => Promise.resolve({ default: DoodleModal }), - 'GIPHY': () => Promise.resolve({ default: GIFModal }), + 'TENOR': () => Promise.resolve({ default: GIFModal }), 'CONFIRM': () => Promise.resolve({ default: ConfirmationModal }), 'MUTE': MuteModal, 'BLOCK': BlockModal, @@ -92,7 +92,7 @@ export default class ModalRoot extends React.PureComponent { }; renderLoading = modalId => () => { - return ['MEDIA', 'VIDEO', 'BOOST', 'FAVOURITE', 'DOODLE', 'GIPHY', 'CONFIRM', 'ACTIONS'].indexOf(modalId) === -1 ? : null; + return ['MEDIA', 'VIDEO', 'BOOST', 'FAVOURITE', 'DOODLE', 'TENOR', 'CONFIRM', 'ACTIONS'].indexOf(modalId) === -1 ? : null; }; renderError = (props) => { diff --git a/app/javascript/flavours/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js index 13b47a08e5..cf746cdeac 100644 --- a/app/javascript/flavours/glitch/reducers/compose.js +++ b/app/javascript/flavours/glitch/reducers/compose.js @@ -38,7 +38,7 @@ import { COMPOSE_UPLOAD_CHANGE_SUCCESS, COMPOSE_UPLOAD_CHANGE_FAIL, COMPOSE_DOODLE_SET, - COMPOSE_GIPHY_SET, + COMPOSE_TENOR_SET, COMPOSE_RESET, COMPOSE_POLL_ADD, COMPOSE_POLL_REMOVE, @@ -107,6 +107,7 @@ const initialState = ImmutableMap({ resetFileKey: Math.floor((Math.random() * 0x10000)), idempotencyKey: null, tagHistory: ImmutableList(), + tenor: null, media_modal: ImmutableMap({ id: null, description: '', @@ -114,7 +115,6 @@ const initialState = ImmutableMap({ focusY: 0, dirty: false, }), - giphy: null, doodle: ImmutableMap({ fg: 'rgb( 0, 0, 0)', bg: 'rgb(255, 255, 255)', @@ -560,8 +560,8 @@ export default function compose(state = initialState, action) { })); case COMPOSE_DOODLE_SET: return state.mergeIn(['doodle'], action.options); - case COMPOSE_GIPHY_SET: - return state.mergeIn(['giphy'], action.options); + case COMPOSE_TENOR_SET: + return state.mergeIn(['tenor'], action.options); case REDRAFT: const do_not_federate = !!action.status.get('local_only'); let text = action.raw_text || unescapeHTML(expandMentions(action.status)); diff --git a/app/javascript/flavours/glitch/styles/components/giphy.scss b/app/javascript/flavours/glitch/styles/components/giphy.scss deleted file mode 100644 index 71c1de3204..0000000000 --- a/app/javascript/flavours/glitch/styles/components/giphy.scss +++ /dev/null @@ -1,13 +0,0 @@ -.giphy-modal { - @extend .boost-modal; - width: 500px; -} - -.giphy-modal__container { - text-align: center; - padding: 20px; -} - -.giphy-modal__searchbox { - width: 450px !important; -} \ No newline at end of file diff --git a/app/javascript/flavours/glitch/styles/components/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss index 79128d66f9..bc1c40fe97 100644 --- a/app/javascript/flavours/glitch/styles/components/index.scss +++ b/app/javascript/flavours/glitch/styles/components/index.scss @@ -1814,7 +1814,7 @@ noscript { @import 'search'; @import 'emoji'; @import 'doodle'; -@import 'giphy'; +@import 'tenor'; @import 'drawer'; @import 'media'; @import 'sensitive'; diff --git a/app/javascript/flavours/glitch/styles/components/tenor.scss b/app/javascript/flavours/glitch/styles/components/tenor.scss new file mode 100644 index 0000000000..69bd24945e --- /dev/null +++ b/app/javascript/flavours/glitch/styles/components/tenor.scss @@ -0,0 +1,217 @@ +.tenor-modal { + @extend .boost-modal; + width: 500px; +} + +.tenor-modal__container { + text-align: center; + padding: 20px; +} + +.tenor-modal__searchbox { + width: 450px !important; +} + +.react-tenor { + background-color: #f7f7f7; + border: 1px solid #ccc; + max-width: 480px; +} + +.react-tenor-active { + box-shadow: 0 0 5px 1px rgba(0, 0, 0, .2); +} + +.react-tenor--search-bar { + position: relative; +} + +.react-tenor--search { + background-color: white; + border: 1px solid #f7f7f7; + box-sizing: border-box; + color: #555; + font-family: Arial; + font-size: 1em; + line-height: 1.3; + overflow: visible; + padding: .25em .5em; + width: 100%; +} + +.react-tenor--search:focus { + box-shadow: 0 0 2px 2px #6a89af; + outline: none; +} + +.react-tenor--autocomplete { + box-sizing: border-box; + color: #aaa; + font-family: Arial; + font-size: 1em; + line-height: 1.3; + left: 0; + padding: .25em .5em; + pointer-events: none; + position: absolute; + top: 1px; +} + +.react-tenor--autocomplete span { + visibility: hidden; +} + +.react-tenor--spinner { + animation: react-tenor-spin 1s linear infinite; + height: 22px; + position: absolute; + right: 4px; + top: 3px; + width: 22px; +} + +.react-tenor--spinner path { + fill: #999; +} + +.react-tenor--suggestions { + overflow-x: auto; + padding: .5em .5em; + white-space: nowrap; +} + +.react-tenor--suggestions button { + background: #6a89af; + border: 1px solid #f7f7f7; + border-radius: 5px; + color: white; + cursor: pointer; + display: inline-block; + font-size: 1em; + padding: 3px 5px; +} + +.react-tenor--suggestions button:focus { + box-shadow: 0 0 2px 2px #6a89af; + outline: none; +} + +.react-tenor--suggestions button + button { + margin-left: .5em; +} + +.react-tenor--results { + display: flex; + flex-wrap: wrap; + position: relative; +} + +.react-tenor--result { + background: #6a89af; + background-image: repeating-linear-gradient( + 45deg, + rgba(255, 255, 255, .1), + rgba(255, 255, 255, .1) 15px, + transparent 0, + transparent 30px + ); + border: 0; + cursor: pointer; + display: inline-block; + flex-basis: 25%; + height: 120px; + opacity: 1; + padding: 0; + transition: opacity .3s; + width: 120px; +} + +.react-tenor--result span { + animation: react-tenor-fade-in .2s; + background-size: cover; + display: block; + height: 100%; + width: 100%; +} + +.react-tenor--result:focus { + box-shadow: 0 0 2px 2px #6a89af; + border: 1px solid #f7f7f7; + outline: none; + z-index: 1; +} + +.react-tenor--result:hover { + opacity: .5; +} + +@media screen and (max-width: 480px) { + .react-tenor--result { + flex-basis: 33%; + } +} + +.react-tenor--page-left, +.react-tenor--page-right { + background: #6a89af; + border: 0; + cursor: pointer; + height: 1.8em; + position: absolute; + top: calc(50% - .9em); + opacity: .5; + position: absolute; + transition: opacity .2s, left .2s, right .2s; + width: 1.8em; + z-index: -1; +} + +.react-tenor--results:hover .react-tenor--page-left, +.react-tenor--results:hover .react-tenor--page-right { + opacity: 1; + z-index: 1; +} + +.react-tenor--results:hover .react-tenor--page-left { + left: -1em; +} + +.react-tenor--results:hover .react-tenor--page-right { + right: -1em; +} + +.react-tenor--page-left div, +.react-tenor--page-right div { + background: inherit; + height: 1.6em; + transform: rotate(45deg); + top: .1em; + position: absolute; + width: 1.6em; +} + +.react-tenor--page-left { + left: -.3em; +} + +.react-tenor--page-left div { + left: -.7em; +} + +.react-tenor--page-right { + right: -.3em; +} + +.react-tenor--page-right div { + right: -.7em; +} + +@keyframes react-tenor-fade-in { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes react-tenor-spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} diff --git a/package.json b/package.json index a738f5806d..32fa9f7833 100644 --- a/package.json +++ b/package.json @@ -36,8 +36,8 @@ "@rails/ujs": "^6.1.7", "abortcontroller-polyfill": "^1.7.5", "array-includes": "^3.1.6", - "atrament": "0.2.4", "arrow-key-navigation": "^1.2.0", + "atrament": "0.2.4", "autoprefixer": "^9.8.8", "axios": "^1.3.2", "babel-loader": "^8.3.0", @@ -93,7 +93,6 @@ "react": "^16.14.0", "react-dom": "^16.14.0", "react-helmet": "^6.1.0", - "react-giphy-searchbox": "^1.5.4", "react-hotkeys": "^1.1.4", "react-immutable-proptypes": "^2.2.0", "react-immutable-pure-component": "^2.2.2", @@ -108,6 +107,7 @@ "react-select": "^5.7.0", "react-sparklines": "^1.7.0", "react-swipeable-views": "^0.14.0", + "react-tenor": "^2.2.0", "react-textarea-autosize": "^8.4.0", "react-toggle": "^4.1.3", "redis": "^4.0.6 <4.1.0", diff --git a/public/tenor.svg b/public/tenor.svg new file mode 100644 index 0000000000..bed2e8bc73 --- /dev/null +++ b/public/tenor.svg @@ -0,0 +1,22 @@ + + + + TENOR_VECTOR + Created with Sketch. + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 5cd6dd4e82..7e12ca474a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2865,7 +2865,7 @@ braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.0.1" -bricks.js@^1.7.0, bricks.js@^1.8.0: +bricks.js@^1.7.0: version "1.8.0" resolved "https://registry.yarnpkg.com/bricks.js/-/bricks.js-1.8.0.tgz#8fdeb3c0226af251f4d5727a7df7f9ac0092b4b2" integrity sha1-j96zwCJq8lH01XJ6fff5rACStLI= @@ -9058,7 +9058,7 @@ react-immutable-pure-component@^2.2.2: resolved "https://registry.yarnpkg.com/react-immutable-pure-component/-/react-immutable-pure-component-2.2.2.tgz#3014d3e20cd5a7a4db73b81f1f1464f4d351684b" integrity sha512-vkgoMJUDqHZfXXnjVlG3keCxSO/U6WeDQ5/Sl0GK2cH8TOxEzQ5jXqDXHEL/jqk6fsNxV05oH5kD7VNMUE2k+A== -react-infinite-scroller@^1.0.12, react-infinite-scroller@^1.2.4: +react-infinite-scroller@^1.0.12: version "1.2.4" resolved "https://registry.yarnpkg.com/react-infinite-scroller/-/react-infinite-scroller-1.2.4.tgz#f67eaec4940a4ce6417bebdd6e3433bfc38826e9" integrity sha512-/oOa0QhZjXPqaD6sictN2edFMsd3kkMiE19Vcz5JDgHpzEJVqYcmq+V3mkwO88087kvKGe1URNksHEOt839Ubw== @@ -9254,6 +9254,11 @@ react-swipeable-views@^0.14.0: react-swipeable-views-utils "^0.14.0" warning "^4.0.1" +react-tenor@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/react-tenor/-/react-tenor-2.2.0.tgz#98326868cc199165bacb911abdbbb48306232b6f" + integrity sha512-hs0KomduflTLU05fvKAtrf9f/aAj3wN2kPF2cl5cAOFPkhuaVJuWLNxRSAQN1m+aKHinujEQZ1fXp+gL1KgYhg== + react-test-renderer@^16.14.0: version "16.14.0" resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.14.0.tgz#e98360087348e260c56d4fe2315e970480c228ae"