This repository has been archived by the owner on Sep 11, 2024. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 829
Giphy integration #5814
Closed
panagiotis-c
wants to merge
8
commits into
matrix-org:develop
from
panagiotis-c:feat/giphy-integration
Closed
Giphy integration #5814
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
07f14f8
feat: create components for Giphy integration
panagiotis-c 467ed3b
add a simple svg icon for GIF
panagiotis-c 2d0d4a5
add Giphy integration to labs
panagiotis-c c92779f
make "confirm upload" dialog optional
panagiotis-c 2a1b503
feat: improve giphy integration:
panagiotis-c 9544d02
Merge branch 'develop' into feat/giphy-integration
panagiotis-c 19a5381
gif picker: fix types
panagiotis-c 72d0c94
i18n strings
panagiotis-c File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
.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; | ||
width: 240px; | ||
} | ||
|
||
.mx_GifPicker_initialNotice { | ||
text-align: center; | ||
margin: auto; | ||
} | ||
|
||
.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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
/* eslint-disable camelcase */ | ||
|
||
// 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; | ||
mp4: string; | ||
}; | ||
original: { | ||
height: string; | ||
width: string; | ||
mp4: string; | ||
webp: string; | ||
url: string; | ||
}; | ||
downsized: { | ||
url: string; | ||
}; | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
/* 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 { 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<Gif[]>> { | ||
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; | ||
} | ||
|
||
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<IProps, IState> { | ||
paginator: DebouncedFunc<() => Promise<Gif[]>>; | ||
canceled = false; | ||
|
||
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.paginator = gifPaginator(""); | ||
} | ||
|
||
onChangeFilter(filter: string) { | ||
this.paginator = gifPaginator(filter); | ||
this.setState({ filter, gifs: [] }); | ||
} | ||
|
||
async onEnterFilter() { | ||
this.setState({ loading: true }); | ||
|
||
const gifs = await this.paginator(); | ||
if (this.canceled) return; | ||
|
||
this.setState({ loading: false, gifs }); | ||
} | ||
|
||
async onFillRequest(backwards: boolean) { | ||
if (backwards || !this.state.filter || this.canceled) { | ||
return false; | ||
} | ||
|
||
this.setState({ loading: true }); | ||
const gifs = await this.paginator(); | ||
if (this.canceled) { | ||
return false; | ||
} | ||
this.setState({ | ||
loading: false, | ||
gifs, | ||
}); | ||
return false; | ||
} | ||
|
||
componentWillUnmount() { | ||
this.canceled = true; | ||
this.paginator.cancel(); | ||
} | ||
|
||
render() { | ||
const initialNotice = ( | ||
<div className="mx_GifPicker_initialNotice"> | ||
Type to search GIFs | ||
</div> | ||
); | ||
|
||
return ( | ||
<div className="mx_GifPicker"> | ||
<Search | ||
query={this.state.filter} | ||
onChange={this.onChangeFilter} | ||
onEnter={this.onEnterFilter} | ||
/> | ||
{ this.state.gifs.length === 0 && initialNotice } | ||
<ScrollPanel | ||
className="mx_GifPicker_scrollPanel" | ||
onFillRequest={this.onFillRequest} | ||
stickyBottom={false} | ||
startAtBottom={false} | ||
> | ||
{ this.state.gifs.map((gif) => ( | ||
<GifThumbnail | ||
key={gif.id} | ||
gif={gif} | ||
onClick={this.props.addGif} | ||
/> | ||
)) } | ||
{ this.state.loading && <Spinner /> } | ||
</ScrollPanel> | ||
</div> | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import React from "react"; | ||
import { replaceableComponent } from "../../../utils/replaceableComponent"; | ||
import { Gif } from './Gif'; | ||
|
||
interface IProps { | ||
gif: Gif; | ||
onClick(gif: Gif): void; | ||
} | ||
|
||
interface IState { | ||
} | ||
|
||
@replaceableComponent("views.gifpicker.GifThumbnail") | ||
export default class GifThumbnail extends React.Component<IProps, IState> { | ||
constructor(props) { | ||
super(props); | ||
} | ||
|
||
render() { | ||
const gif = this.props.gif; | ||
const rendition = gif.images.fixed_width; | ||
return ( | ||
<div onClick={() => this.props.onClick(gif)} className="mx_GifPicker_thumbnail"> | ||
<video autoPlay loop muted playsInline width={rendition.width} height={rendition.height}> | ||
<source src={rendition.mp4} type="video/mp4" /> | ||
Unsupported file type. | ||
</video> | ||
</div> | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
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 }) => { | ||
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); | ||
|
||
let contextMenu; | ||
if (menuDisplayed) { | ||
const buttonRect = button.current.getBoundingClientRect(); | ||
|
||
const onAddGif = (gif: Gif) => { | ||
closeMenu(); | ||
addGif(gif); | ||
}; | ||
|
||
contextMenu = <ContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu}> | ||
<GifPicker addGif={onAddGif} /> | ||
</ContextMenu>; | ||
} | ||
|
||
const className = classNames( | ||
"mx_MessageComposer_button", | ||
"mx_MessageComposer_gifButton", | ||
{ | ||
"mx_MessageComposer_button_highlight": menuDisplayed, | ||
}, | ||
); | ||
|
||
return <> | ||
<ContextMenuTooltipButton | ||
className={className} | ||
onClick={openMenu} | ||
isExpanded={menuDisplayed} | ||
title={_t('GIF picker')} | ||
inputRef={button} | ||
/> | ||
|
||
{ contextMenu } | ||
</>; | ||
}; | ||
|
||
export default GifButton; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This probably shouldn't be checked into git.
Unrelated though, this work is amazing :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks!
This is a public key so it is not a security issue, as the request will be done client-side and anyone can inspect it.
Giphy/giphy-js#120 (comment)
If this is approved by the product team a production key is needed in order to increase the limits by Giphy.