From 3370394510f1d1e286bab0800ecc2fe4d85649e5 Mon Sep 17 00:00:00 2001 From: Essem <smswessem@gmail.com> Date: Sun, 5 Nov 2023 20:56:45 -0600 Subject: [PATCH] Add Tenor GIF picker Co-authored-by: koyu <me@koyu.space> --- .../flavours/glitch/actions/compose.js | 11 +- .../features/compose/components/options.jsx | 15 ++- .../compose/containers/options_container.js | 20 ++- .../features/ui/components/gif_modal.jsx | 108 ++++++++++++++++ .../features/ui/components/modal_root.jsx | 4 +- .../flavours/glitch/reducers/compose.js | 4 + .../glitch/styles/components/index.scss | 1 + .../glitch/styles/components/modal.scss | 1 + .../glitch/styles/components/tenor.scss | 120 ++++++++++++++++++ package.json | 1 + public/tenor.svg | 22 ++++ yarn.lock | 5 + 12 files changed, 308 insertions(+), 4 deletions(-) create mode 100644 app/javascript/flavours/glitch/features/ui/components/gif_modal.jsx create mode 100644 app/javascript/flavours/glitch/styles/components/tenor.scss create mode 100644 public/tenor.svg diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js index 9e0b123704..283928a0c0 100644 --- a/app/javascript/flavours/glitch/actions/compose.js +++ b/app/javascript/flavours/glitch/actions/compose.js @@ -70,6 +70,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_TENOR_SET = 'COMPOSE_TENOR_SET'; export const COMPOSE_POLL_ADD = 'COMPOSE_POLL_ADD'; export const COMPOSE_POLL_REMOVE = 'COMPOSE_POLL_REMOVE'; @@ -297,7 +298,14 @@ export function doodleSet(options) { }; } -export function uploadCompose(files) { +export function tenorSet(options) { + return { + type: COMPOSE_TENOR_SET, + options: options, + }; +} + +export function uploadCompose(files, alt = '') { return function (dispatch, getState) { const uploadLimit = 4; const media = getState().getIn(['compose', 'media_attachments']); @@ -323,6 +331,7 @@ export function uploadCompose(files) { resizeImage(f).then(file => { const data = new FormData(); data.append('file', file); + data.append('description', alt); // Account for disparity in size of original image and resized data total += file.size - f.size; diff --git a/app/javascript/flavours/glitch/features/compose/components/options.jsx b/app/javascript/flavours/glitch/features/compose/components/options.jsx index 98bba41198..f03da41bed 100644 --- a/app/javascript/flavours/glitch/features/compose/components/options.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/options.jsx @@ -137,6 +137,9 @@ class ComposerOptions extends ImmutablePureComponent { onChangeContentType: PropTypes.func, onTogglePoll: PropTypes.func, onDoodleOpen: PropTypes.func, + onEmbedTenor: PropTypes.func, + onModalClose: PropTypes.func, + onModalOpen: PropTypes.func, onToggleSpoiler: PropTypes.func, onUpload: PropTypes.func, contentType: PropTypes.string, @@ -157,7 +160,7 @@ class ComposerOptions extends ImmutablePureComponent { // Handles attachment clicks. handleClickAttach = (name) => { const { fileElement } = this; - const { onDoodleOpen } = this.props; + const { onDoodleOpen, onEmbedTenor } = this.props; // We switch over the name of the option. switch (name) { @@ -171,6 +174,11 @@ class ComposerOptions extends ImmutablePureComponent { onDoodleOpen(); } return; + case 'gif': + if (onEmbedTenor) { + onEmbedTenor(); + } + return; } }; @@ -252,6 +260,11 @@ class ComposerOptions extends ImmutablePureComponent { name: 'doodle', text: formatMessage(messages.doodle), }, + { + icon: 'file-image-o', + name: 'gif', + text: formatMessage(messages.gif), + } ]} onChange={this.handleClickAttach} title={formatMessage(messages.attach)} 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 c44d3ce305..94e5fe20e2 100644 --- a/app/javascript/flavours/glitch/features/compose/containers/options_container.js +++ b/app/javascript/flavours/glitch/features/compose/containers/options_container.js @@ -6,7 +6,7 @@ import { addPoll, removePoll, } from 'flavours/glitch/actions/compose'; -import { openModal } from 'flavours/glitch/actions/modal'; +import { openModal, closeModal } from 'flavours/glitch/actions/modal'; import Options from '../components/options'; @@ -51,6 +51,24 @@ const mapDispatchToProps = (dispatch) => ({ modalProps: { noEsc: true, noClose: true }, })); }, + + onEmbedTenor() { + dispatch(openModal({ + modalType: 'TENOR', + modalProps: { noEsc: true }, + })); + }, + + onModalClose() { + dispatch(closeModal({ + modalType: undefined, + ignoreFocus: false, + })); + }, + + onModalOpen(props) { + dispatch(openModal({ modalType: 'ACTIONS', modalProps: props })); + }, }); export default connect(mapStateToProps, mapDispatchToProps)(Options); diff --git a/app/javascript/flavours/glitch/features/ui/components/gif_modal.jsx b/app/javascript/flavours/glitch/features/ui/components/gif_modal.jsx new file mode 100644 index 0000000000..0ab2800d90 --- /dev/null +++ b/app/javascript/flavours/glitch/features/ui/components/gif_modal.jsx @@ -0,0 +1,108 @@ +import PropTypes from 'prop-types'; + +import { defineMessages, injectIntl } from 'react-intl'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { connect } from 'react-redux'; + +import Tenor from 'react-tenor'; + +import { tenorSet, uploadCompose } from 'flavours/glitch/actions/compose'; +import { IconButton } from 'flavours/glitch/components/icon_button'; + +const messages = defineMessages({ + 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' }, +}); + +// Utility for converting base64 image to binary for upload +// https://stackoverflow.com/questions/35940290/how-to-convert-base64-string-to-javascript-file-object-like-as-from-file-input-f +function dataURLtoFile(dataurl, filename) { + let arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1], + bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n); + while(n--){ + u8arr[n] = bstr.charCodeAt(n); + } + return new File([u8arr], filename, { type: mime }); +} + +const mapStateToProps = state => ({ + options: state.getIn(['compose', 'tenor']), +}); + +const mapDispatchToProps = dispatch => ({ + /** + * Set options in the redux store + * @param opts + */ + setOpt: (opts) => dispatch(tenorSet(opts)), + /** + * Submit GIF for upload + * @param file + * @param alt + */ + submit: (file, alt) => dispatch(uploadCompose([file], alt)), +}); + +class GIFModal extends ImmutablePureComponent { + + static propTypes = { + intl: PropTypes.object.isRequired, + options: ImmutablePropTypes.map, + onClose: PropTypes.func.isRequired, + setOpt: PropTypes.func.isRequired, + submit: PropTypes.func.isRequired, + }; + + onDoneButton = (result) => { + const url = result.media[0].mp4.url; + const alt = result.content_description; + var modal = this; + // eslint-disable-next-line promise/catch-or-return + fetch(url).then(function(response) { + return response.blob(); + }).then(function(blob) { + const reader = new FileReader(); + reader.readAsDataURL(blob); + reader.onloadend = function() { + var dataUrl = reader.result; + const file = dataURLtoFile(dataUrl, 'tenor.mp4'); + modal.props.submit(file, alt); + modal.props.onClose(); // close dialog + }; + }); + }; + + render () { + + const { intl } = this.props; + + return ( + <div className='modal-root__modal tenor-modal'> + <div className='tenor-modal__container'> + <IconButton title={intl.formatMessage(messages.close)} icon='close' size='16' onClick={this.props.onClose} style={{ float: 'right' }} /> + <Tenor + token='FJBKNQSVF2DD' + // eslint-disable-next-line react/jsx-no-bind + onSelect={result => this.onDoneButton(result)} + autoFocus='true' + searchPlaceholder={intl.formatMessage(messages.search)} + messageError={intl.formatMessage(messages.error)} + messageLoading={intl.formatMessage(messages.loading)} + messageNoMatches={intl.formatMessage(messages.nomatches)} + contentFilter='off' + /> + <br /><img src='/tenor.svg' alt='Tenor logo' /> + </div> + </div> + ); + } + +} + + +export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(GIFModal)); diff --git a/app/javascript/flavours/glitch/features/ui/components/modal_root.jsx b/app/javascript/flavours/glitch/features/ui/components/modal_root.jsx index 78a7ac2297..1226f54de3 100644 --- a/app/javascript/flavours/glitch/features/ui/components/modal_root.jsx +++ b/app/javascript/flavours/glitch/features/ui/components/modal_root.jsx @@ -33,6 +33,7 @@ import DeprecatedSettingsModal from './deprecated_settings_modal'; import DoodleModal from './doodle_modal'; import FavouriteModal from './favourite_modal'; import FocalPointModal from './focal_point_modal'; +import GIFModal from './gif_modal'; import ImageModal from './image_modal'; import MediaModal from './media_modal'; import ModalLoading from './modal_loading'; @@ -47,6 +48,7 @@ export const MODAL_COMPONENTS = { 'BOOST': () => Promise.resolve({ default: BoostModal }), 'FAVOURITE': () => Promise.resolve({ default: FavouriteModal }), 'DOODLE': () => Promise.resolve({ default: DoodleModal }), + 'TENOR': () => Promise.resolve({ default: GIFModal }), 'CONFIRM': () => Promise.resolve({ default: ConfirmationModal }), 'MUTE': MuteModal, 'BLOCK': BlockModal, @@ -94,7 +96,7 @@ export default class ModalRoot extends PureComponent { }; renderLoading = modalId => () => { - return ['MEDIA', 'VIDEO', 'BOOST', 'FAVOURITE', 'DOODLE', 'CONFIRM', 'ACTIONS'].indexOf(modalId) === -1 ? <ModalLoading /> : null; + return ['MEDIA', 'VIDEO', 'BOOST', 'FAVOURITE', 'DOODLE', 'TENOR', 'CONFIRM', 'ACTIONS'].indexOf(modalId) === -1 ? <ModalLoading /> : null; }; renderError = (props) => { diff --git a/app/javascript/flavours/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js index 0915ecba0f..66aad890fe 100644 --- a/app/javascript/flavours/glitch/reducers/compose.js +++ b/app/javascript/flavours/glitch/reducers/compose.js @@ -40,6 +40,7 @@ import { COMPOSE_UPLOAD_CHANGE_SUCCESS, COMPOSE_UPLOAD_CHANGE_FAIL, COMPOSE_DOODLE_SET, + COMPOSE_TENOR_SET, COMPOSE_RESET, COMPOSE_POLL_ADD, COMPOSE_POLL_REMOVE, @@ -108,6 +109,7 @@ const initialState = ImmutableMap({ resetFileKey: Math.floor((Math.random() * 0x10000)), idempotencyKey: null, tagHistory: ImmutableList(), + tenor: null, media_modal: ImmutableMap({ id: null, description: '', @@ -567,6 +569,8 @@ export default function compose(state = initialState, action) { })); case COMPOSE_DOODLE_SET: return state.mergeIn(['doodle'], 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/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss index d94f123648..eb9c6c4278 100644 --- a/app/javascript/flavours/glitch/styles/components/index.scss +++ b/app/javascript/flavours/glitch/styles/components/index.scss @@ -11,6 +11,7 @@ @import 'search'; @import 'emoji'; @import 'doodle'; +@import 'tenor'; @import 'drawer'; @import 'media'; @import 'sensitive'; diff --git a/app/javascript/flavours/glitch/styles/components/modal.scss b/app/javascript/flavours/glitch/styles/components/modal.scss index d0db308a15..e7ce1da576 100644 --- a/app/javascript/flavours/glitch/styles/components/modal.scss +++ b/app/javascript/flavours/glitch/styles/components/modal.scss @@ -414,6 +414,7 @@ } .doodle-modal, +.tenor-modal, .boost-modal, .confirmation-modal, .report-modal, 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..d9e1292757 --- /dev/null +++ b/app/javascript/flavours/glitch/styles/components/tenor.scss @@ -0,0 +1,120 @@ +.tenor-modal__container { + text-align: center; + padding: 20px; +} + +.tenor-modal__container .icon-button { + margin: 10px; +} + +.react-tenor-active { + box-shadow: 0 0 5px 1px rgba(0, 0, 0, 20%); +} + +.react-tenor--search { + box-sizing: border-box; + color: #555; + font-size: 1.5em; + margin-bottom: 10px; + line-height: 1.3; + overflow: visible; + padding: 10px; + width: 100%; + -webkit-appearance: none; +} + +.react-tenor--search[type='search']::-webkit-search-cancel-button, +.react-tenor--search[type='search']::-webkit-search-decoration { + -webkit-appearance: none; + appearance: none; +} + +.react-tenor--search:focus { + box-shadow: 0 0 2px 2px #6a89af; + outline: none; +} + +.react-tenor--autocomplete { + display: none; +} + +.react-tenor--autocomplete span { + visibility: hidden; +} + +.react-tenor--spinner { + display: none; +} + +.react-tenor--suggestions { + display: none; +} + +.react-tenor--results { + display: flex; + flex-wrap: wrap; + position: relative; +} + +.react-tenor--result { + background: rgba(87, 87, 131, 15%); + border: 0; + cursor: pointer; + display: inline-block; + flex-basis: 25%; + height: 120px; + opacity: 1; + padding: 0; + transition: opacity 0.3s; + width: 120px; +} + +.react-tenor--result span { + animation: react-tenor-fade-in 0.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: 0.5; +} + +@media screen and (width <= 480px) { + .react-tenor--result { + flex-basis: 33%; + } +} + +.react-tenor--page-left, +.react-tenor--page-right { + display: none; +} + +@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 40578504e8..fbd87fd2f6 100644 --- a/package.json +++ b/package.json @@ -121,6 +121,7 @@ "react-select": "^5.7.3", "react-sparklines": "^1.7.0", "react-swipeable-views": "^0.14.0", + "react-tenor": "^2.2.0", "react-textarea-autosize": "^8.4.1", "react-toggle": "^4.1.3", "redux": "^4.2.1", 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 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg width="50px" height="14px" viewBox="0 0 1584 447" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch --> + <title>TENOR_VECTOR</title> + <desc>Created with Sketch.</desc> + <defs></defs> + <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> + <g id="TENOR_VECTOR" fill="#007add"> + <g id="TENOR_GREY"> + <g id="Layer_1"> + <g id="Group"> + <path d="M314.8,295.7 C317.2,313.7 322.4,329.8 331.6,344.3 C353.2,378.4 384.6,394.6 424.8,394.3 C457.8,394.1 486.1,382.1 510.5,360.1 C523.3,348.5 541.3,350.7 550.3,364.6 C556.8,374.6 555.5,386.7 546.5,395.7 C519.2,423 486.7,440 448.2,444.7 C423,447.8 397.9,447.2 373.4,439.6 C317.6,422.3 280.5,385.3 264,329.7 C244.7,264.4 255,203.7 300.5,151.5 C331.4,116.2 371.4,99.4 418.5,100.2 C459.1,100.9 494.5,114.7 523,144.2 C546.7,168.7 559.8,198.6 565.8,231.8 C568.1,244.4 569.1,257.2 569.7,270 C570.4,284.1 559.4,295.1 545.3,295.8 C543.3,295.9 541.3,295.8 539.3,295.8 L321.9,295.8 C319.9,295.7 317.8,295.7 314.8,295.7 L314.8,295.7 Z M510,245.4 C508.5,232.4 504.8,220.4 499.9,208.7 C470.7,140 394.8,138.2 355.8,171.7 C335.3,189.3 323.2,211.8 317.2,237.8 C316.6,240.2 316.4,242.6 315.9,245.4 L510,245.4 L510,245.4 Z" id="Shape"></path> + <path d="M970.4,271.3 C970.5,182.9 1039,99.8 1143.8,100 C1241.5,100.1 1316.4,175.8 1316.2,274.2 C1316,370.9 1239.6,446.3 1141.7,446.4 C1047.4,446.4 970.2,367.6 970.4,271.3 L970.4,271.3 Z M1257,275.9 C1256.5,241.6 1246.6,213 1224.9,188.8 C1180.6,139.3 1099.7,141.2 1057.2,192.2 C1021.8,234.7 1019.3,309.7 1060.4,355.9 C1097,397.1 1162.8,405.6 1207.6,373.6 C1241.7,349.2 1256,314.7 1257,275.9 L1257,275.9 Z" id="Shape"></path> + <path d="M683.5,148.6 C689.8,142.6 694.9,137.5 700.2,132.7 C725.8,109.3 756.4,99.7 790.5,100 C817.4,100.3 842.7,106.3 865.1,121.9 C892.9,141.3 908,168.8 913.9,201.5 C915.8,212.1 916.8,223 916.9,233.8 C917.2,295 917,356.1 917,417.3 C917,429 909.6,440.1 898.7,444.1 C887.3,448.3 876.6,446.6 867.8,437.8 C861.6,431.7 858.9,424 858.9,415.2 C859,358.5 859.2,301.9 858.7,245.2 C858.6,234 857,222.5 854.3,211.7 C845.8,178.6 822.1,158.5 788.2,154.6 C764.1,151.8 741.2,155.6 721.1,170.2 C694.4,189.6 683.7,217.2 683.6,249.2 C683.4,305 683.6,360.9 683.5,416.7 C683.5,438.1 662.7,452 642.9,443.9 C632.4,439.6 625.3,428.2 625.3,415.4 L625.3,326.9 L625.3,129.4 C625.3,118.1 630.5,109.5 640.1,103.9 C650,98.1 660.3,98.4 670,104.5 C678.3,109.8 683.3,117.5 683.5,127.7 C683.6,134.3 683.5,140.7 683.5,148.6 L683.5,148.6 Z" id="Shape"></path> + <path d="M124.9,106.8 L131.4,106.8 L203.4,106.8 C214.2,106.8 223,110.3 228.6,120 C237.9,136.3 228.4,156 209.8,158.8 C206.9,159.2 203.8,159.3 200.8,159.3 L131.3,159.3 L124.9,159.3 L124.9,164.9 L124.9,343.4 C124.9,355.4 126.8,366.9 134.9,376.7 C141.5,384.7 150.4,388.1 160.1,389.6 C174.6,391.9 189,390.7 203.1,386.3 C216.5,382 229.7,388 234.2,400.5 C238.8,413.2 232.3,425.7 219.9,431.7 C203.1,439.9 185.5,443.5 166.9,444.5 C118.7,446.9 75.4,411.9 68.1,364.1 C67.1,357.9 66.8,351.5 66.8,345.2 C66.7,285.7 66.7,226.2 66.7,166.7 L66.7,159.8 C64.5,159.7 62.7,159.5 61,159.5 C50,159.5 39,159.7 28,159.4 C10,158.9 -1.6,145.4 1.1,128.4 C3,116.4 13.9,107.4 27.6,107 C38.4,106.7 49.3,106.9 60.1,106.9 C66.7,106.9 66.7,106.9 66.7,100 L66.7,30.5 C66.7,13.5 79,0.5 95.4,0.3 C111.8,0.1 124.7,13.2 124.8,30.4 C124.9,53.7 124.8,77.1 124.8,100.4 C124.9,102.3 124.9,104.3 124.9,106.8 L124.9,106.8 Z" id="Shape"></path> + <path d="M1432.4,170.1 C1441,160.3 1448.6,150.6 1457.4,142 C1477.5,122.4 1501.1,108.7 1528.9,103.2 C1537.3,101.5 1546.1,101.1 1554.7,101.5 C1571,102.4 1583.2,116 1583.2,132 C1583.2,147.8 1570.8,161.2 1554.7,162.7 C1542.8,163.8 1530.7,164 1519,166.5 C1486.9,173.4 1464.2,192.8 1449.6,221.9 C1436.8,247.3 1432.5,274.7 1432.4,302.8 C1432.2,340.3 1432.4,377.8 1432.4,415.3 C1432.4,432.7 1419.6,446.2 1403.1,446.1 C1386.5,446 1374,433.1 1374,415.7 L1374,128.8 C1374,115.8 1383.2,104 1395.6,100.9 C1409.2,97.5 1422.4,103.1 1428.3,115.6 C1430.6,120.4 1431.9,126 1432.1,131.4 C1432.7,142.5 1432.3,153.7 1432.3,164.9 C1432.4,166.6 1432.4,168.2 1432.4,170.1 L1432.4,170.1 Z" id="Shape"></path> + </g> + </g> + </g> + </g> + </g> +</svg> \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 1f23f26dde..3d70b770cd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10418,6 +10418,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@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-18.2.0.tgz#1dd912bd908ff26da5b9fca4fd1c489b9523d37e"