From 07f14f80a4b02b8dd5bb213ee742422f0735f621 Mon Sep 17 00:00:00 2001 From: Panagiotis <27917356+panoschal@users.noreply.github.com> Date: Sat, 27 Mar 2021 21:14:23 +0200 Subject: [PATCH 1/7] feat: create components for Giphy integration create a new menu in MessageComposer, next to emoji picker and sticker picker consume the Giphy API display the GIF search results in an one-column list add option in settings for showing the GIF picker --- res/css/_components.scss | 1 + res/css/views/gifpicker/_GifPicker.scss | 31 +++++ src/components/views/gifpicker/Gif.ts | 25 ++++ src/components/views/gifpicker/GifPicker.tsx | 120 ++++++++++++++++++ .../views/gifpicker/GifThumbnail.tsx | 25 ++++ src/components/views/rooms/GifButton.tsx | 41 ++++++ src/components/views/rooms/MessageComposer.js | 22 ++++ .../tabs/user/PreferencesUserSettingsTab.js | 1 + src/settings/Settings.ts | 5 + 9 files changed, 271 insertions(+) create mode 100644 res/css/views/gifpicker/_GifPicker.scss create mode 100644 src/components/views/gifpicker/Gif.ts create mode 100644 src/components/views/gifpicker/GifPicker.tsx create mode 100644 src/components/views/gifpicker/GifThumbnail.tsx create mode 100644 src/components/views/rooms/GifButton.tsx diff --git a/res/css/_components.scss b/res/css/_components.scss index 9c895490b36..ce7c3ee52f2 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -145,6 +145,7 @@ @import "./views/elements/_TooltipButton.scss"; @import "./views/elements/_Validation.scss"; @import "./views/emojipicker/_EmojiPicker.scss"; +@import "./views/gifpicker/_GifPicker.scss"; @import "./views/groups/_GroupPublicityToggle.scss"; @import "./views/groups/_GroupRoomList.scss"; @import "./views/groups/_GroupUserSettings.scss"; diff --git a/res/css/views/gifpicker/_GifPicker.scss b/res/css/views/gifpicker/_GifPicker.scss new file mode 100644 index 00000000000..8fd90231ace --- /dev/null +++ b/res/css/views/gifpicker/_GifPicker.scss @@ -0,0 +1,31 @@ +.mx_GifPicker { + width: 240px; + height: 450px; + border-radius: 4px; + display: flex; + flex-direction: column; +} + +.mx_GifPicker_scrollPanel { + overflow-y: scroll; + flex: 1 1 0; + max-height: 450px; + // height: 450px; + width: 240px; +} + +.mx_GifPicker_initialNotice { + text-align: center; + margin: auto; +} + +.mx_GifPicker_thumbnail img { + border: 2px solid $input-darker-bg-color; + border-radius: 6px; + margin-bottom: 0.8em; + cursor: pointer; + + &:hover { + border-color: $strong-input-border-color; + } +} \ No newline at end of file diff --git a/src/components/views/gifpicker/Gif.ts b/src/components/views/gifpicker/Gif.ts new file mode 100644 index 00000000000..21edb77de53 --- /dev/null +++ b/src/components/views/gifpicker/Gif.ts @@ -0,0 +1,25 @@ +// a gif, as returned by the giphy endpoint +export interface Gif { + id: string; + title: string; + images: { + preview_gif: { + url: string; + width: string; + height: string; + }; + fixed_width: { + url: string; + width: string; + height: string; + webp: string; + }; + original: { + height: string; + width: string; + mp4: string; + webp: string; + url: string; + }; + }; +} \ No newline at end of file diff --git a/src/components/views/gifpicker/GifPicker.tsx b/src/components/views/gifpicker/GifPicker.tsx new file mode 100644 index 00000000000..ee4d8e232e6 --- /dev/null +++ b/src/components/views/gifpicker/GifPicker.tsx @@ -0,0 +1,120 @@ +import React from "react"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import Search from "../emojipicker/Search"; +import ScrollPanel from "../../structures/ScrollPanel"; +import Spinner from "../elements/Spinner"; +import GifThumbnail from "./GifThumbnail"; +import { throttle } from "lodash"; +import { Gif } from './Gif'; + +const API_KEY = "hhMBhTnD7k8BgyoZCM9UNM1vRsPcgzaC"; + +interface IProps { + addGif(gif: Gif): void; +} + +interface IState { + filter: string; + scrollTop: number; + // initial estimation of height, dialog is hardcoded to 450px height. + // should be enough to never have blank rows of emojis as + // 3 rows of overflow are also rendered. The actual value is updated on scroll. + viewportHeight: number; + loading: boolean; + gifs: Gif[]; +} + +@replaceableComponent("views.gifpicker.GifPicker") +export default class GifPicker extends React.Component { + constructor(props) { + super(props); + + this.state = { + filter: "", + scrollTop: 0, + viewportHeight: 280, + loading: false, + gifs: [], + }; + + this.onChangeFilter = this.onChangeFilter.bind(this); + this.onEnterFilter = this.onEnterFilter.bind(this); + this.onFillRequest = this.onFillRequest.bind(this); + this.searchGifs = this.searchGifs.bind(this); + this.searchGifs = throttle(this.searchGifs, 1000); + } + + async searchGifs(filter: string, offset: number) { + if (!filter) { + return [[], 0]; + } + const response = await fetch( + `https://api.giphy.com/v1/gifs/search?q=${encodeURIComponent( + filter + )}&api_key=${API_KEY}&limit=10&rating=g&offset=${offset}` + ); + const content = await response.json(); + return [content.data, content.pagination.offset]; + } + + onChangeFilter(filter) { + this.setState({ filter }); + } + + async onEnterFilter() { + this.setState({ loading: true }); + const [gifs, offset] = await this.searchGifs(this.state.filter, 0); + this.setState({ loading: false, gifs }); + } + + async onFillRequest(backwards) { + if (backwards) { + return false; + } + + this.setState({ loading: true }); + const [newGifs, offset] = await this.searchGifs( + this.state.filter, + this.state.gifs.length + ); + this.setState(({ gifs }) => ({ + loading: false, + gifs: offset === gifs.length ? gifs.concat(newGifs) : gifs, + })); + return false; + } + + render() { + const initialNotice = ( +
+ Type to search GIFs +
+ ); + + return ( +
+ + {this.state.gifs.length === 0 && initialNotice} + + {this.state.gifs.map((gif) => ( + this.props.addGif(gif)} + /> + ))} + {this.state.loading && } + +
+ ); + } +} diff --git a/src/components/views/gifpicker/GifThumbnail.tsx b/src/components/views/gifpicker/GifThumbnail.tsx new file mode 100644 index 00000000000..9b55ce0e049 --- /dev/null +++ b/src/components/views/gifpicker/GifThumbnail.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; + +interface IProps { + url: string; + onClick(): void; +} + +interface IState { +} + +@replaceableComponent("views.gifpicker.GifThumbnail") +export default class GifThumbnail extends React.Component { + constructor(props) { + super(props); + } + + render() { + return ( +
+ +
+ ); + } +} diff --git a/src/components/views/rooms/GifButton.tsx b/src/components/views/rooms/GifButton.tsx new file mode 100644 index 00000000000..f3e6750d23a --- /dev/null +++ b/src/components/views/rooms/GifButton.tsx @@ -0,0 +1,41 @@ +import classNames from 'classnames'; +import React from 'react'; +import { _t } from '../../../languageHandler'; +import { aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu } from '../../structures/ContextMenu'; +import GifPicker from '../gifpicker/GifPicker'; + +const GifButton = ({addGif}) => { + const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); + + let contextMenu; + if (menuDisplayed) { + const buttonRect = button.current.getBoundingClientRect(); + contextMenu = + + ; + } + + const className = classNames( + "mx_MessageComposer_button", + "mx_MessageComposer_gifButton", + { + "mx_MessageComposer_button_highlight": menuDisplayed, + }, + ); + + return <> + + + + + { contextMenu } + ; +}; + +export default GifButton; \ No newline at end of file diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index b7078766fb5..bdaf2a293dc 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -34,6 +34,7 @@ import {UPDATE_EVENT} from "../../../stores/AsyncStore"; import ActiveWidgetStore from "../../../stores/ActiveWidgetStore"; import {replaceableComponent} from "../../../utils/replaceableComponent"; import VoiceRecordComposerTile from "./VoiceRecordComposerTile"; +import GifButton from './GifButton'; function ComposerAvatar(props) { const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar'); @@ -178,6 +179,7 @@ export default class MessageComposer extends React.Component { this._onRoomStateEvents = this._onRoomStateEvents.bind(this); this._onTombstoneClick = this._onTombstoneClick.bind(this); this.renderPlaceholderText = this.renderPlaceholderText.bind(this); + this.addGif = this.addGif.bind(this); WidgetStore.instance.on(UPDATE_EVENT, this._onWidgetUpdate); ActiveWidgetStore.on('update', this._onActiveWidgetUpdate); this._dispatcherRef = null; @@ -317,6 +319,18 @@ export default class MessageComposer extends React.Component { }); } + async addGif(gif) { + const cli = MatrixClientPeg.get(); + const response = await fetch(gif.images.original.url) + const file = await response.blob(); + + ContentMessages.sharedInstance().sendContentListToRoom( + [file], + this.props.room.roomId, + cli + ); + } + sendMessage = () => { this.messageComposerInput._sendMessage(); } @@ -370,6 +384,14 @@ export default class MessageComposer extends React.Component { controls.push(); } + if ( + SettingsStore.getValue(UIFeature.Widgets) && + SettingsStore.getValue("MessageComposerInput.showGifButton") && + !this.state.haveRecording + ) { + controls.push(); + } + if (SettingsStore.getValue("feature_voice_messages")) { controls.push( Date: Sun, 28 Mar 2021 13:19:12 +0300 Subject: [PATCH 2/7] add a simple svg icon for GIF --- res/css/views/rooms/_MessageComposer.scss | 4 ++++ res/img/element-icons/room/composer/gif.svg | 3 +++ 2 files changed, 7 insertions(+) create mode 100644 res/img/element-icons/room/composer/gif.svg diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss index e6c0cc3f464..930ee552d5b 100644 --- a/res/css/views/rooms/_MessageComposer.scss +++ b/res/css/views/rooms/_MessageComposer.scss @@ -239,6 +239,10 @@ limitations under the License. mask-image: url('$(res)/img/element-icons/room/composer/sticker.svg'); } +.mx_MessageComposer_gifButton::before { + mask-image: url('$(res)/img/element-icons/room/composer/gif.svg'); +} + .mx_MessageComposer_sendMessage { cursor: pointer; position: relative; diff --git a/res/img/element-icons/room/composer/gif.svg b/res/img/element-icons/room/composer/gif.svg new file mode 100644 index 00000000000..4e639eea2af --- /dev/null +++ b/res/img/element-icons/room/composer/gif.svg @@ -0,0 +1,3 @@ + + GIF + \ No newline at end of file From 2d0d4a59323d86de8b044de6ab6b1f805b60cfc5 Mon Sep 17 00:00:00 2001 From: Panagiotis <27917356+panoschal@users.noreply.github.com> Date: Sun, 28 Mar 2021 13:21:49 +0300 Subject: [PATCH 3/7] add Giphy integration to labs --- src/components/views/rooms/MessageComposer.js | 2 +- .../views/settings/tabs/user/PreferencesUserSettingsTab.js | 1 - src/settings/Settings.ts | 7 ++++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index bdaf2a293dc..3ab878e6cf8 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -386,7 +386,7 @@ export default class MessageComposer extends React.Component { if ( SettingsStore.getValue(UIFeature.Widgets) && - SettingsStore.getValue("MessageComposerInput.showGifButton") && + SettingsStore.getValue("feature_giphy_integration") && !this.state.haveRecording ) { controls.push(); diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js index f62bb030961..238f875e22a 100644 --- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js @@ -37,7 +37,6 @@ export default class PreferencesUserSettingsTab extends React.Component { 'sendTypingNotifications', 'MessageComposerInput.ctrlEnterToSend', 'MessageComposerInput.showStickersButton', - 'MessageComposerInput.showGifButton', ]; static TIMELINE_SETTINGS = [ diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index 54f005a3d34..222c6051d19 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -266,10 +266,11 @@ export const SETTINGS: {[setting: string]: ISetting} = { displayName: _td('Show stickers button'), default: true, }, - "MessageComposerInput.showGifButton": { - supportedLevels: LEVELS_ACCOUNT_SETTINGS, + "feature_giphy_integration": { + isFeature: true, + supportedLevels: LEVELS_FEATURE, displayName: _td('Show GIF picker button'), - default: true, + default: false, }, // TODO: Wire up appropriately to UI (FTUE notifications) "Notifications.alwaysShowBadgeCounts": { From c92779f13d2e03f19ff4d6449a3500a09d2d4f46 Mon Sep 17 00:00:00 2001 From: Panagiotis <27917356+panoschal@users.noreply.github.com> Date: Sun, 28 Mar 2021 13:26:39 +0300 Subject: [PATCH 4/7] make "confirm upload" dialog optional add showConfirmationDialogs option to sendContentListToRoom so we can use the same function for directly uploading GIFs without displaying a confirmation dialog every time --- src/ContentMessages.tsx | 6 +++--- src/components/structures/RoomView.tsx | 4 ++-- src/components/views/rooms/MessageComposer.js | 9 +++++---- src/components/views/rooms/SendMessageComposer.js | 2 +- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx index 95b45cce4aa..c83771c6a09 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -27,6 +27,7 @@ import RoomViewStore from './stores/RoomViewStore'; import encrypt from "browser-encrypt-attachment"; import extractPngChunks from "png-chunks-extract"; import Spinner from "./components/views/elements/Spinner"; +import UploadConfirmDialog from "./components/views/dialogs/UploadConfirmDialog"; // Polyfill for Canvas.toBlob API using Canvas.toDataURL import "blueimp-canvas-to-blob"; @@ -385,7 +386,7 @@ export default class ContentMessages { } } - async sendContentListToRoom(files: File[], roomId: string, matrixClient: MatrixClient) { + async sendContentListToRoom(files: File[], roomId: string, matrixClient: MatrixClient, showConfirmationDialogs = true) { if (matrixClient.isGuest()) { dis.dispatch({action: 'require_registration'}); return; @@ -437,8 +438,7 @@ export default class ContentMessages { if (!shouldContinue) return; } - const UploadConfirmDialog = sdk.getComponent("dialogs.UploadConfirmDialog"); - let uploadAll = false; + let uploadAll = !showConfirmationDialogs; // Promise to complete before sending next file into room, used for synchronisation of file-sending // to match the order the files were specified in let promBefore = Promise.resolve(); diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 8a9c7cabd95..8269d90fb4c 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -704,7 +704,7 @@ export default class RoomView extends React.Component { break; case 'picture_snapshot': ContentMessages.sharedInstance().sendContentListToRoom( - [payload.file], this.state.room.roomId, this.context); + [payload.file], this.state.room.roomId, this.context, true); break; case 'notifier_enabled': case Action.UploadStarted: @@ -1179,7 +1179,7 @@ export default class RoomView extends React.Component { ev.stopPropagation(); ev.preventDefault(); ContentMessages.sharedInstance().sendContentListToRoom( - ev.dataTransfer.files, this.state.room.roomId, this.context, + Array.from(ev.dataTransfer.files), this.state.room.roomId, this.context, true ); dis.fire(Action.FocusComposer); diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 3ab878e6cf8..8b2db257b9f 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -141,7 +141,7 @@ class UploadButton extends React.Component { } ContentMessages.sharedInstance().sendContentListToRoom( - tfiles, this.props.roomId, MatrixClientPeg.get(), + tfiles, this.props.roomId, MatrixClientPeg.get(), true ); // This is the onChange handler for a file form control, but we're @@ -319,15 +319,16 @@ export default class MessageComposer extends React.Component { }); } - async addGif(gif) { + async addGif(gif: Gif) { const cli = MatrixClientPeg.get(); - const response = await fetch(gif.images.original.url) + const response = await fetch(gif.images.downsized.url) const file = await response.blob(); ContentMessages.sharedInstance().sendContentListToRoom( [file], this.props.room.roomId, - cli + cli, + false, ); } diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index aca2066d347..85bc6796b24 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -559,7 +559,7 @@ export default class SendMessageComposer extends React.Component { // from Finder) but more images copied from a different website // / word processor etc. ContentMessages.sharedInstance().sendContentListToRoom( - Array.from(clipboardData.files), this.props.room.roomId, this.context, + Array.from(clipboardData.files), this.props.room.roomId, this.context, true ); return true; // to skip internal onPaste handler } From 2a1b503ddff962d362f942e6800e80f0ae4ec6f1 Mon Sep 17 00:00:00 2001 From: Panagiotis <27917356+panoschal@users.noreply.github.com> Date: Sun, 28 Mar 2021 21:47:19 +0300 Subject: [PATCH 5/7] feat: improve giphy integration: add translations show mp4 version in preview, for smaller file sizes better handling for throttling requests and lazy loading list cancel promises on unmount --- res/css/views/gifpicker/_GifPicker.scss | 37 ++++--- src/ContentMessages.tsx | 7 +- src/components/structures/RoomView.tsx | 2 +- src/components/views/gifpicker/Gif.ts | 5 +- src/components/views/gifpicker/GifPicker.tsx | 97 +++++++++++++------ .../views/gifpicker/GifThumbnail.tsx | 14 ++- src/components/views/rooms/GifButton.tsx | 13 ++- src/components/views/rooms/MessageComposer.js | 8 +- .../views/rooms/SendMessageComposer.js | 2 +- src/i18n/strings/en_EN.json | 2 + 10 files changed, 123 insertions(+), 64 deletions(-) diff --git a/res/css/views/gifpicker/_GifPicker.scss b/res/css/views/gifpicker/_GifPicker.scss index 8fd90231ace..bfa9a778b82 100644 --- a/res/css/views/gifpicker/_GifPicker.scss +++ b/res/css/views/gifpicker/_GifPicker.scss @@ -1,31 +1,30 @@ .mx_GifPicker { - width: 240px; - height: 450px; - border-radius: 4px; - display: flex; - flex-direction: column; + width: 240px; + height: 450px; + border-radius: 4px; + display: flex; + flex-direction: column; } .mx_GifPicker_scrollPanel { - overflow-y: scroll; + overflow-y: scroll; flex: 1 1 0; - max-height: 450px; - // height: 450px; - width: 240px; + max-height: 450px; + width: 240px; } .mx_GifPicker_initialNotice { - text-align: center; - margin: auto; + text-align: center; + margin: auto; } -.mx_GifPicker_thumbnail img { - border: 2px solid $input-darker-bg-color; +.mx_GifPicker_thumbnail video { + border: 2px solid $input-darker-bg-color; border-radius: 6px; margin-bottom: 0.8em; - cursor: pointer; - - &:hover { - border-color: $strong-input-border-color; - } -} \ No newline at end of file + cursor: pointer; + + &:hover { + border-color: $strong-input-border-color; + } +} diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx index c83771c6a09..28b965d0cfe 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -386,7 +386,12 @@ export default class ContentMessages { } } - async sendContentListToRoom(files: File[], roomId: string, matrixClient: MatrixClient, showConfirmationDialogs = true) { + async sendContentListToRoom( + files: File[], + roomId: string, + matrixClient: MatrixClient, + showConfirmationDialogs = true, + ) { if (matrixClient.isGuest()) { dis.dispatch({action: 'require_registration'}); return; diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 8269d90fb4c..2f1025e5341 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -1179,7 +1179,7 @@ export default class RoomView extends React.Component { ev.stopPropagation(); ev.preventDefault(); ContentMessages.sharedInstance().sendContentListToRoom( - Array.from(ev.dataTransfer.files), this.state.room.roomId, this.context, true + Array.from(ev.dataTransfer.files), this.state.room.roomId, this.context, true, ); dis.fire(Action.FocusComposer); diff --git a/src/components/views/gifpicker/Gif.ts b/src/components/views/gifpicker/Gif.ts index 21edb77de53..b12044ef327 100644 --- a/src/components/views/gifpicker/Gif.ts +++ b/src/components/views/gifpicker/Gif.ts @@ -1,3 +1,5 @@ +/* eslint-disable camelcase */ + // a gif, as returned by the giphy endpoint export interface Gif { id: string; @@ -13,6 +15,7 @@ export interface Gif { width: string; height: string; webp: string; + mp4: string; }; original: { height: string; @@ -22,4 +25,4 @@ export interface Gif { url: string; }; }; -} \ No newline at end of file +} diff --git a/src/components/views/gifpicker/GifPicker.tsx b/src/components/views/gifpicker/GifPicker.tsx index ee4d8e232e6..2bdd71e6bb7 100644 --- a/src/components/views/gifpicker/GifPicker.tsx +++ b/src/components/views/gifpicker/GifPicker.tsx @@ -1,14 +1,51 @@ +/* eslint-disable camelcase */ import React from "react"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import Search from "../emojipicker/Search"; import ScrollPanel from "../../structures/ScrollPanel"; import Spinner from "../elements/Spinner"; import GifThumbnail from "./GifThumbnail"; -import { throttle } from "lodash"; -import { Gif } from './Gif'; +import { DebouncedFunc, throttle, uniqBy } from "lodash"; +import { Gif } from "./Gif"; const API_KEY = "hhMBhTnD7k8BgyoZCM9UNM1vRsPcgzaC"; +async function searchGifs( + filter: string, + offset: number, +): Promise<[Gif[], { offset: number; total_count: number }]> { + if (!filter) { + return [[], { offset: 0, total_count: 0 }]; + } + const response = await fetch( + `https://api.giphy.com/v1/gifs/search?q=${encodeURIComponent( + filter, + )}&api_key=${API_KEY}&limit=5&rating=g&offset=${offset}`, + ); + const content = await response.json(); + return [content.data, content.pagination]; +} + +function gifPaginator(filter): DebouncedFunc<() => Promise> { + let gifs = []; + let depleted = false; + return throttle(async () => { + if (depleted) { + return gifs; + } + const [newGifs, { total_count }] = await searchGifs( + filter, + gifs.length, + ); + gifs = gifs.concat(newGifs); + if (total_count === gifs.length) { + depleted = true; + } + const uniqueGifs: Gif[] = uniqBy(gifs, "id"); + return uniqueGifs; + }, 1000); +} + interface IProps { addGif(gif: Gif): void; } @@ -26,6 +63,9 @@ interface IState { @replaceableComponent("views.gifpicker.GifPicker") export default class GifPicker extends React.Component { + paginator: DebouncedFunc<() => Promise>; + canceled = false; + constructor(props) { super(props); @@ -40,50 +80,45 @@ export default class GifPicker extends React.Component { this.onChangeFilter = this.onChangeFilter.bind(this); this.onEnterFilter = this.onEnterFilter.bind(this); this.onFillRequest = this.onFillRequest.bind(this); - this.searchGifs = this.searchGifs.bind(this); - this.searchGifs = throttle(this.searchGifs, 1000); - } - - async searchGifs(filter: string, offset: number) { - if (!filter) { - return [[], 0]; - } - const response = await fetch( - `https://api.giphy.com/v1/gifs/search?q=${encodeURIComponent( - filter - )}&api_key=${API_KEY}&limit=10&rating=g&offset=${offset}` - ); - const content = await response.json(); - return [content.data, content.pagination.offset]; + this.paginator = gifPaginator(""); } - onChangeFilter(filter) { - this.setState({ filter }); + onChangeFilter(filter: string) { + this.paginator = gifPaginator(filter); + this.setState({ filter, gifs: [] }); } async onEnterFilter() { this.setState({ loading: true }); - const [gifs, offset] = await this.searchGifs(this.state.filter, 0); + + const gifs = await this.paginator(); + if (this.canceled) return; + this.setState({ loading: false, gifs }); } - async onFillRequest(backwards) { - if (backwards) { + async onFillRequest(backwards: boolean) { + if (backwards || !this.state.filter || this.canceled) { return false; } this.setState({ loading: true }); - const [newGifs, offset] = await this.searchGifs( - this.state.filter, - this.state.gifs.length - ); - this.setState(({ gifs }) => ({ + const gifs = await this.paginator(); + if (this.canceled) { + return false; + } + this.setState({ loading: false, - gifs: offset === gifs.length ? gifs.concat(newGifs) : gifs, - })); + gifs, + }); return false; } + componentWillUnmount() { + this.canceled = true; + this.paginator.cancel(); + } + render() { const initialNotice = (
@@ -108,8 +143,8 @@ export default class GifPicker extends React.Component { {this.state.gifs.map((gif) => ( this.props.addGif(gif)} + gif={gif} + onClick={this.props.addGif} /> ))} {this.state.loading && } diff --git a/src/components/views/gifpicker/GifThumbnail.tsx b/src/components/views/gifpicker/GifThumbnail.tsx index 9b55ce0e049..89d6149765a 100644 --- a/src/components/views/gifpicker/GifThumbnail.tsx +++ b/src/components/views/gifpicker/GifThumbnail.tsx @@ -1,9 +1,10 @@ import React from "react"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { Gif } from './Gif'; interface IProps { - url: string; - onClick(): void; + gif: Gif; + onClick(gif: Gif): void; } interface IState { @@ -16,9 +17,14 @@ export default class GifThumbnail extends React.Component { } render() { + const gif = this.props.gif; + const rendition = gif.images.fixed_width; return ( -
- +
this.props.onClick(gif)} className="mx_GifPicker_thumbnail"> +
); } diff --git a/src/components/views/rooms/GifButton.tsx b/src/components/views/rooms/GifButton.tsx index f3e6750d23a..ed665778593 100644 --- a/src/components/views/rooms/GifButton.tsx +++ b/src/components/views/rooms/GifButton.tsx @@ -2,6 +2,7 @@ import classNames from 'classnames'; import React from 'react'; import { _t } from '../../../languageHandler'; import { aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu } from '../../structures/ContextMenu'; +import { Gif } from '../gifpicker/Gif'; import GifPicker from '../gifpicker/GifPicker'; const GifButton = ({addGif}) => { @@ -10,8 +11,14 @@ const GifButton = ({addGif}) => { let contextMenu; if (menuDisplayed) { const buttonRect = button.current.getBoundingClientRect(); - contextMenu = - + + const onAddGif = (gif: Gif) => { + closeMenu(); + addGif(gif); + }; + + contextMenu = + ; } @@ -38,4 +45,4 @@ const GifButton = ({addGif}) => { ; }; -export default GifButton; \ No newline at end of file +export default GifButton; diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 8b2db257b9f..99caec92f13 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -35,6 +35,7 @@ import ActiveWidgetStore from "../../../stores/ActiveWidgetStore"; import {replaceableComponent} from "../../../utils/replaceableComponent"; import VoiceRecordComposerTile from "./VoiceRecordComposerTile"; import GifButton from './GifButton'; +import {Gif} from '../gifpicker/Gif'; function ComposerAvatar(props) { const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar'); @@ -68,7 +69,7 @@ const EmojiButton = ({addEmoji}) => { if (menuDisplayed) { const buttonRect = button.current.getBoundingClientRect(); const EmojiPicker = sdk.getComponent('emojipicker.EmojiPicker'); - contextMenu = + contextMenu = ; } @@ -141,7 +142,7 @@ class UploadButton extends React.Component { } ContentMessages.sharedInstance().sendContentListToRoom( - tfiles, this.props.roomId, MatrixClientPeg.get(), true + tfiles, this.props.roomId, MatrixClientPeg.get(), true, ); // This is the onChange handler for a file form control, but we're @@ -321,8 +322,9 @@ export default class MessageComposer extends React.Component { async addGif(gif: Gif) { const cli = MatrixClientPeg.get(); - const response = await fetch(gif.images.downsized.url) + const response = await fetch(gif.images.downsized.url); const file = await response.blob(); + file.name = gif.title; ContentMessages.sharedInstance().sendContentListToRoom( [file], diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 85bc6796b24..fdcc7963bb0 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -559,7 +559,7 @@ export default class SendMessageComposer extends React.Component { // from Finder) but more images copied from a different website // / word processor etc. ContentMessages.sharedInstance().sendContentListToRoom( - Array.from(clipboardData.files), this.props.room.roomId, this.context, true + Array.from(clipboardData.files), this.props.room.roomId, this.context, true, ); return true; // to skip internal onPaste handler } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index b5edf31d012..eda1ac39efc 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -803,6 +803,7 @@ "Use custom size": "Use custom size", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", "Show stickers button": "Show stickers button", + "Show GIF picker button": "Show GIF picker button", "Use a more compact ‘Modern’ layout": "Use a more compact ‘Modern’ layout", "Show a placeholder for removed messages": "Show a placeholder for removed messages", "Show join/leave messages (invites/kicks/bans unaffected)": "Show join/leave messages (invites/kicks/bans unaffected)", @@ -1449,6 +1450,7 @@ "Encrypting your message...": "Encrypting your message...", "Your message was sent": "Your message was sent", "Please select the destination room for this message": "Please select the destination room for this message", + "GIF picker": "GIF picker", "Scroll to most recent messages": "Scroll to most recent messages", "Close preview": "Close preview", "and %(count)s others...|other": "and %(count)s others...", From 19a538171bd40244a365989ff2ccb3ab56ad49e3 Mon Sep 17 00:00:00 2001 From: Panagiotis <27917356+panoschal@users.noreply.github.com> Date: Sun, 12 Sep 2021 13:10:32 +0300 Subject: [PATCH 6/7] gif picker: fix types --- src/ContentMessages.tsx | 1 - src/components/views/gifpicker/Gif.ts | 3 +++ src/components/views/gifpicker/GifPicker.tsx | 10 +++++----- src/components/views/rooms/GifButton.tsx | 6 ++---- src/components/views/rooms/MessageComposer.tsx | 10 +++++----- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx index 44f7dc50053..775bb3e2638 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -27,7 +27,6 @@ import RoomViewStore from './stores/RoomViewStore'; import encrypt from "browser-encrypt-attachment"; import extractPngChunks from "png-chunks-extract"; import Spinner from "./components/views/elements/Spinner"; -import UploadConfirmDialog from "./components/views/dialogs/UploadConfirmDialog"; import { Action } from "./dispatcher/actions"; import CountlyAnalytics from "./CountlyAnalytics"; import { diff --git a/src/components/views/gifpicker/Gif.ts b/src/components/views/gifpicker/Gif.ts index b12044ef327..816c7b24a76 100644 --- a/src/components/views/gifpicker/Gif.ts +++ b/src/components/views/gifpicker/Gif.ts @@ -24,5 +24,8 @@ export interface Gif { webp: string; url: string; }; + downsized: { + url: string; + }; }; } diff --git a/src/components/views/gifpicker/GifPicker.tsx b/src/components/views/gifpicker/GifPicker.tsx index 2bdd71e6bb7..1ae2fcab2eb 100644 --- a/src/components/views/gifpicker/GifPicker.tsx +++ b/src/components/views/gifpicker/GifPicker.tsx @@ -13,7 +13,7 @@ const API_KEY = "hhMBhTnD7k8BgyoZCM9UNM1vRsPcgzaC"; async function searchGifs( filter: string, offset: number, -): Promise<[Gif[], { offset: number; total_count: number }]> { +): Promise<[Gif[], { offset: number, total_count: number }]> { if (!filter) { return [[], { offset: 0, total_count: 0 }]; } @@ -133,21 +133,21 @@ export default class GifPicker extends React.Component { onChange={this.onChangeFilter} onEnter={this.onEnterFilter} /> - {this.state.gifs.length === 0 && initialNotice} + { this.state.gifs.length === 0 && initialNotice } - {this.state.gifs.map((gif) => ( + { this.state.gifs.map((gif) => ( - ))} - {this.state.loading && } + )) } + { this.state.loading && }
); diff --git a/src/components/views/rooms/GifButton.tsx b/src/components/views/rooms/GifButton.tsx index ed665778593..5af9bfeda1f 100644 --- a/src/components/views/rooms/GifButton.tsx +++ b/src/components/views/rooms/GifButton.tsx @@ -5,7 +5,7 @@ import { aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu } fr import { Gif } from '../gifpicker/Gif'; import GifPicker from '../gifpicker/GifPicker'; -const GifButton = ({addGif}) => { +const GifButton = ({ addGif }) => { const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); let contextMenu; @@ -37,9 +37,7 @@ const GifButton = ({addGif}) => { isExpanded={menuDisplayed} title={_t('GIF picker')} inputRef={button} - > - - + /> { contextMenu } ; diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index 8063a0810b8..38c2a6b8123 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -378,19 +378,19 @@ export default class MessageComposer extends React.Component { return true; } - async addGif(gif: Gif) { + private addGif = async (gif: Gif) => { const cli = MatrixClientPeg.get(); const response = await fetch(gif.images.downsized.url); - const file = await response.blob(); - file.name = gif.title; + const blob: any = await response.blob(); + blob.name = gif.title; ContentMessages.sharedInstance().sendContentListToRoom( - [file], + [blob as File], this.props.room.roomId, cli, false, ); - } + }; private sendMessage = async () => { if (this.state.haveRecording && this.voiceRecordingButton) { From 72d0c945b0633e789e3127c1af5e7325a60b18c7 Mon Sep 17 00:00:00 2001 From: Panagiotis <27917356+panoschal@users.noreply.github.com> Date: Sun, 12 Sep 2021 13:18:12 +0300 Subject: [PATCH 7/7] i18n strings --- src/i18n/strings/en_EN.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index b6110c07144..1616bd0466e 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1549,9 +1549,8 @@ "Sending your message...": "Sending your message...", "Encrypting your message...": "Encrypting your message...", "Your message was sent": "Your message was sent", - "Please select the destination room for this message": "Please select the destination room for this message", - "GIF picker": "GIF picker", "Failed to send": "Failed to send", + "GIF picker": "GIF picker", "Scroll to most recent messages": "Scroll to most recent messages", "Show %(count)s other previews|other": "Show %(count)s other previews", "Show %(count)s other previews|one": "Show %(count)s other preview",