GIF picker
This commit is contained in:
parent
ff336628f8
commit
c18efef7b6
|
@ -179,6 +179,10 @@ Available variables:
|
||||||
- May specify a self-hosted Lingva instance, powered by either [lingva-translate](https://github.com/thedaviddelta/lingva-translate) or [lingva-api](https://github.com/cheeaun/lingva-api)
|
- May specify a self-hosted Lingva instance, powered by either [lingva-translate](https://github.com/thedaviddelta/lingva-translate) or [lingva-api](https://github.com/cheeaun/lingva-api)
|
||||||
- List of fallback instances hard-coded in `/.env`
|
- List of fallback instances hard-coded in `/.env`
|
||||||
- [↗️ List of lingva-translate instances](https://github.com/thedaviddelta/lingva-translate?tab=readme-ov-file#instances)
|
- [↗️ List of lingva-translate instances](https://github.com/thedaviddelta/lingva-translate?tab=readme-ov-file#instances)
|
||||||
|
- `PHANPY_GIPHY_API_KEY` (optional, no defaults):
|
||||||
|
- API key for [GIPHY](https://developers.giphy.com/). See [API docs](https://developers.giphy.com/docs/api/).
|
||||||
|
- If provided, a setting will appear for users to enable the GIF picker in the composer. Disabled by default.
|
||||||
|
- This is not self-hosted.
|
||||||
|
|
||||||
### Static site hosting
|
### Static site hosting
|
||||||
|
|
||||||
|
|
3
src/assets/powered-by-giphy.svg
Normal file
3
src/assets/powered-by-giphy.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" viewBox="0 0 641 223">
|
||||||
|
<path fill="#aaa" d="M86 214c-9-1-17-4-24-8l-6-3-5-5-5-4-4-6-4-6-3-8-2-8v-27l2-9 3-9 4-6 4-6 5-5 5-5 7-3 6-4 7-2 7-2 12-1h12l7 1 8 2 7 4 7 3 5 5 5 4-10 10-10 9-4-3-10-5-5-1H88l-5 2-6 3-3 4-4 4-2 5-2 6v6l-1 7 1 7 2 7 3 5 2 4 4 3 4 3 5 2 6 2h9l10-1 5-2 6-3v-16H91v-27h59v54l-1 3-2 3-5 4-4 4-5 3-5 2-8 2-8 2-10 1H92l-6-1zm266-62V91h34v46h44V91h34v121h-34v-46h-44v46h-34v-61zm-182-1V90h34v121h-34v-60zm59-1V90h35l36 1 5 2c3 0 8 2 10 4l5 2 4 5 5 4 3 7 3 7 1 13v13l-4 6-3 7-4 4-5 5-5 2-5 3-6 2-5 1-18 1h-18v32h-34v-61zm67-2 3-2 2-4 2-5v-5l-2-4-2-4-3-2-3-3h-30v31h30l3-2zm226 39v-24l-8-12-18-28a1751 1751 0 0 0-20-31v-2h39l7 12 12 21 6 9 13-21 13-21h38v2l-41 61-7 10v48h-34v-24zM109 66l-4-1-5-5-5-4-1-5-3-9v-5l1-5c2-7 3-10 8-15l4-4 7-2 7-2h7l6 1 5 2 5 2 3 4 4 3 2 6 2 5v13l-2 5-2 6-4 4-3 3-5 2-4 2-9 1h-9l-5-2zm22-11 4-2 3-4 2-5V34l-2-4-2-4-3-2-4-3-5-1h-6l-4 2-5 2-2 4-3 5-1 3v4l1 5 2 5 2 2 5 3 4 2h10l4-2zM37 39V11h33l3 1 3 2 4 3 3 3 1 5 1 4v5l-1 4-3 4-3 5-4 1-3 2-11 1H49v16H37V39zm31 0 3-2 1-2 1-2v-4l-1-3-3-2-2-2H49v18h15l4-1zm107 25a512 512 0 0 0-19-53h14l4 14 6 19 1 4 1-1 7-19 5-17h9l6 19 7 18v-1l2-6 5-17 4-13h14v1l-4 12-16 41v2h-5l-5-1-6-15-6-15-1 1-3 7-6 15-2 8h-11l-1-3zm74-25V11h42v11h-29v2l-1 5v4h29v11h-28v11h2l15 1h13v11h-43V39zm55 0V11h33l5 3 5 2 2 4 2 5v10l-2 3-1 4-5 3-5 3 5 5 8 10 3 4h-14l-7-9-8-10h-9v19h-12V39zm33-3 2-3v-6l-3-3-2-3h-18v16h1v1h17l2-2zm26 3V11h42v11h-29l-1 6v5h29v11h-28v5l-1 5 1 1v1h30v11h-43V39zm54 0V11h17l18 1 4 2 5 3 2 4 3 4 2 6 1 6v5c-1 6-3 12-6 15l-3 4-5 3-5 2-17 1h-16V39zm33 14 5-5 2-3v-6l-1-6-1-3-1-3-4-3-3-2h-5l-6-1-3 1h-3v34h9l8-1 3-2zm50-14V11h34l5 2 4 2 2 3 2 3v9l-2 2-3 4-1 1 3 3 3 4 1 3 1 4-1 4-1 4-3 3-3 3-5 1-5 1h-31V39zm34 15 2-1v-6l-2-2-2-2h-20v13h20l2-2zm-3-22 4-2v-6l-2-1-2-2h-19v12h16l4-1zm42 24V45l-6-9-11-17-5-8h15l4 8 7 11 2 3 7-11 7-11h14l-11 16-11 17v23h-12V56z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
|
@ -727,3 +727,163 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes gif-shake {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
transform: rotate(5deg);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
transform: rotate(-5deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.gif-picker-button {
|
||||||
|
span {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 11.5px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:is(:hover, :focus) {
|
||||||
|
span {
|
||||||
|
animation: gif-shake 0.3s 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#gif-picker-sheet {
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
input[type='search'] {
|
||||||
|
flex-grow: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
mask-image: linear-gradient(
|
||||||
|
to right,
|
||||||
|
transparent 2px,
|
||||||
|
black 16px,
|
||||||
|
black calc(100% - 16px),
|
||||||
|
transparent calc(100% - 2px)
|
||||||
|
);
|
||||||
|
|
||||||
|
@media (min-height: 480px) {
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 50vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.loading {
|
||||||
|
opacity: 0.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-state {
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
min-height: 100px;
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
list-style: none;
|
||||||
|
padding: 8px 2px;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
@media (min-height: 480px) {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||||
|
grid-auto-rows: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 4px;
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
background-color: transparent;
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: var(--bg-faded-color);
|
||||||
|
|
||||||
|
@media (min-height: 480px) {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:is(:hover, :focus) {
|
||||||
|
background-color: var(--link-bg-color);
|
||||||
|
box-shadow: 0 0 0 2px var(--link-light-color);
|
||||||
|
filter: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
figure {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: var(--figure-width);
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
@media (min-height: 480px) {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
figcaption {
|
||||||
|
font-size: 0.8em;
|
||||||
|
padding: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
color: var(--text-insignificant-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
background-color: var(--img-bg-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
vertical-align: top;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
|
||||||
|
@media (min-height: 480px) {
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -11,6 +11,8 @@ import { uid } from 'uid/single';
|
||||||
import { useDebouncedCallback, useThrottledCallback } from 'use-debounce';
|
import { useDebouncedCallback, useThrottledCallback } from 'use-debounce';
|
||||||
import { useSnapshot } from 'valtio';
|
import { useSnapshot } from 'valtio';
|
||||||
|
|
||||||
|
import poweredByGiphyURL from '../assets/powered-by-giphy.svg';
|
||||||
|
|
||||||
import Menu2 from '../components/menu2';
|
import Menu2 from '../components/menu2';
|
||||||
import supportedLanguages from '../data/status-supported-languages';
|
import supportedLanguages from '../data/status-supported-languages';
|
||||||
import urlRegex from '../data/url-regex';
|
import urlRegex from '../data/url-regex';
|
||||||
|
@ -41,7 +43,10 @@ import Loader from './loader';
|
||||||
import Modal from './modal';
|
import Modal from './modal';
|
||||||
import Status from './status';
|
import Status from './status';
|
||||||
|
|
||||||
const { PHANPY_IMG_ALT_API_URL: IMG_ALT_API_URL } = import.meta.env;
|
const {
|
||||||
|
PHANPY_IMG_ALT_API_URL: IMG_ALT_API_URL,
|
||||||
|
PHANPY_GIPHY_API_KEY: GIPHY_API_KEY,
|
||||||
|
} = import.meta.env;
|
||||||
|
|
||||||
const supportedLanguagesMap = supportedLanguages.reduce((acc, l) => {
|
const supportedLanguagesMap = supportedLanguages.reduce((acc, l) => {
|
||||||
const [code, common, native] = l;
|
const [code, common, native] = l;
|
||||||
|
@ -610,6 +615,7 @@ function Compose({
|
||||||
}, [mediaAttachments]);
|
}, [mediaAttachments]);
|
||||||
|
|
||||||
const [showEmoji2Picker, setShowEmoji2Picker] = useState(false);
|
const [showEmoji2Picker, setShowEmoji2Picker] = useState(false);
|
||||||
|
const [showGIFPicker, setShowGIFPicker] = useState(false);
|
||||||
|
|
||||||
const [topSupportedLanguages, restSupportedLanguages] = useMemo(() => {
|
const [topSupportedLanguages, restSupportedLanguages] = useMemo(() => {
|
||||||
const topLanguages = [];
|
const topLanguages = [];
|
||||||
|
@ -1235,6 +1241,18 @@ function Compose({
|
||||||
>
|
>
|
||||||
<Icon icon="emoji2" />
|
<Icon icon="emoji2" />
|
||||||
</button>
|
</button>
|
||||||
|
{!!states.settings.composerGIFPicker && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="toolbar-button gif-picker-button"
|
||||||
|
disabled={uiState === 'loading'}
|
||||||
|
onClick={() => {
|
||||||
|
setShowGIFPicker(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>GIF</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
<div class="spacer" />
|
<div class="spacer" />
|
||||||
{uiState === 'loading' ? (
|
{uiState === 'loading' ? (
|
||||||
|
@ -1319,6 +1337,64 @@ function Compose({
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
{showGIFPicker && (
|
||||||
|
<Modal
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
setShowGIFPicker(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<GIFPickerModal
|
||||||
|
onClose={() => setShowGIFPicker(false)}
|
||||||
|
onSelect={({ url, type, alt_text }) => {
|
||||||
|
console.log('GIF URL', url);
|
||||||
|
if (mediaAttachments.length >= maxMediaAttachments) {
|
||||||
|
alert(
|
||||||
|
`You can only attach up to ${maxMediaAttachments} files.`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Download the GIF and insert it as media attachment
|
||||||
|
(async () => {
|
||||||
|
let theToast;
|
||||||
|
try {
|
||||||
|
theToast = showToast({
|
||||||
|
text: 'Downloading GIF…',
|
||||||
|
duration: -1,
|
||||||
|
});
|
||||||
|
const blob = await fetch(url, {
|
||||||
|
referrerPolicy: 'no-referrer',
|
||||||
|
}).then((res) => res.blob());
|
||||||
|
const file = new File(
|
||||||
|
[blob],
|
||||||
|
type === 'video/mp4' ? 'video.mp4' : 'image.gif',
|
||||||
|
{
|
||||||
|
type,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const newMediaAttachments = [
|
||||||
|
...mediaAttachments,
|
||||||
|
{
|
||||||
|
file,
|
||||||
|
type,
|
||||||
|
size: file.size,
|
||||||
|
id: null,
|
||||||
|
description: alt_text || '',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
setMediaAttachments(newMediaAttachments);
|
||||||
|
theToast?.hideToast?.();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
theToast?.hideToast?.();
|
||||||
|
showToast('Failed to download GIF');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2246,4 +2322,225 @@ function CustomEmojisModal({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const GIFS_PER_PAGE = 20;
|
||||||
|
function GIFPickerModal({ onClose = () => {}, onSelect = () => {} }) {
|
||||||
|
const [uiState, setUIState] = useState('default');
|
||||||
|
const [results, setResults] = useState([]);
|
||||||
|
const formRef = useRef(null);
|
||||||
|
const qRef = useRef(null);
|
||||||
|
const currentOffset = useRef(0);
|
||||||
|
const scrollableRef = useRef(null);
|
||||||
|
|
||||||
|
function fetchGIFs({ offset }) {
|
||||||
|
console.log('fetchGIFs', { offset });
|
||||||
|
if (!qRef.current?.value) return;
|
||||||
|
setUIState('loading');
|
||||||
|
scrollableRef.current?.scrollTo?.({
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
behavior: 'smooth',
|
||||||
|
});
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const query = {
|
||||||
|
api_key: GIPHY_API_KEY,
|
||||||
|
q: qRef.current.value,
|
||||||
|
rating: 'g',
|
||||||
|
limit: GIFS_PER_PAGE,
|
||||||
|
bundle: 'messaging_non_clips',
|
||||||
|
offset,
|
||||||
|
};
|
||||||
|
const response = await fetch(
|
||||||
|
'https://api.giphy.com/v1/gifs/search?' + new URLSearchParams(query),
|
||||||
|
{
|
||||||
|
referrerPolicy: 'no-referrer',
|
||||||
|
},
|
||||||
|
).then((r) => r.json());
|
||||||
|
currentOffset.current = response.pagination?.offset || 0;
|
||||||
|
setResults(response);
|
||||||
|
setUIState('results');
|
||||||
|
} catch (e) {
|
||||||
|
setUIState('error');
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
qRef.current?.focus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="gif-picker-sheet" class="sheet">
|
||||||
|
{!!onClose && (
|
||||||
|
<button type="button" class="sheet-close" onClick={onClose}>
|
||||||
|
<Icon icon="x" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<header>
|
||||||
|
<form
|
||||||
|
ref={formRef}
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
fetchGIFs({ offset: 0 });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
ref={qRef}
|
||||||
|
type="search"
|
||||||
|
name="q"
|
||||||
|
placeholder="Search GIFs"
|
||||||
|
required
|
||||||
|
autocomplete="off"
|
||||||
|
autocorrect="off"
|
||||||
|
autocapitalize="off"
|
||||||
|
spellCheck="false"
|
||||||
|
dir="auto"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="image"
|
||||||
|
class="powered-button"
|
||||||
|
src={poweredByGiphyURL}
|
||||||
|
width="86"
|
||||||
|
height="30"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</header>
|
||||||
|
<main ref={scrollableRef} class={uiState === 'loading' ? 'loading' : ''}>
|
||||||
|
{uiState === 'default' && (
|
||||||
|
<div class="ui-state">
|
||||||
|
<p class="insignificant">Type to search GIFs</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{uiState === 'loading' && !results?.data?.length && (
|
||||||
|
<div class="ui-state">
|
||||||
|
<Loader abrupt />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{results?.data?.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<ul>
|
||||||
|
{results.data.map((gif) => {
|
||||||
|
const { id, images, title, alt_text } = gif;
|
||||||
|
const {
|
||||||
|
fixed_height_small,
|
||||||
|
fixed_height_downsampled,
|
||||||
|
fixed_height,
|
||||||
|
original,
|
||||||
|
} = images;
|
||||||
|
const theImage = fixed_height_small?.url
|
||||||
|
? fixed_height_small
|
||||||
|
: fixed_height_downsampled?.url
|
||||||
|
? fixed_height_downsampled
|
||||||
|
: fixed_height;
|
||||||
|
let { url, webp, width, height } = theImage;
|
||||||
|
if (+height > 100) {
|
||||||
|
width = (width / height) * 100;
|
||||||
|
height = 100;
|
||||||
|
}
|
||||||
|
const urlObj = new URL(url);
|
||||||
|
const strippedURL = urlObj.origin + urlObj.pathname;
|
||||||
|
let strippedWebP;
|
||||||
|
if (webp) {
|
||||||
|
const webpObj = new URL(webp);
|
||||||
|
strippedWebP = webpObj.origin + webpObj.pathname;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<li key={id}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
const { mp4, url } = original;
|
||||||
|
const theURL = mp4 || url;
|
||||||
|
const urlObj = new URL(theURL);
|
||||||
|
const strippedURL = urlObj.origin + urlObj.pathname;
|
||||||
|
onClose();
|
||||||
|
onSelect({
|
||||||
|
url: strippedURL,
|
||||||
|
type: mp4 ? 'video/mp4' : 'image/gif',
|
||||||
|
alt_text: alt_text || title,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<figure
|
||||||
|
style={{
|
||||||
|
'--figure-width': width + 'px',
|
||||||
|
// width: width + 'px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<picture>
|
||||||
|
{strippedWebP && (
|
||||||
|
<source srcset={strippedWebP} type="image/webp" />
|
||||||
|
)}
|
||||||
|
<img
|
||||||
|
src={strippedURL}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
alt={alt_text}
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
onLoad={(e) => {
|
||||||
|
e.target.style.backgroundColor = 'transparent';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</picture>
|
||||||
|
<figcaption>{alt_text || title}</figcaption>
|
||||||
|
</figure>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
<p class="pagination">
|
||||||
|
{results.pagination?.offset > 0 && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="light small"
|
||||||
|
disabled={uiState === 'loading'}
|
||||||
|
onClick={() => {
|
||||||
|
fetchGIFs({
|
||||||
|
offset: results.pagination?.offset - GIFS_PER_PAGE,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="chevron-left" />
|
||||||
|
<span>Previous</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<span />
|
||||||
|
{results.pagination?.offset + results.pagination?.count <
|
||||||
|
results.pagination?.total_count && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="light small"
|
||||||
|
disabled={uiState === 'loading'}
|
||||||
|
onClick={() => {
|
||||||
|
fetchGIFs({
|
||||||
|
offset: results.pagination?.offset + GIFS_PER_PAGE,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>Next</span> <Icon icon="chevron-right" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
uiState === 'results' && (
|
||||||
|
<div class="ui-state">
|
||||||
|
<p>No results</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
{uiState === 'error' && (
|
||||||
|
<div class="ui-state">
|
||||||
|
<p>Error loading GIFs</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default Compose;
|
export default Compose;
|
||||||
|
|
|
@ -347,6 +347,7 @@ button[hidden] {
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type='text'],
|
input[type='text'],
|
||||||
|
input[type='search'],
|
||||||
textarea,
|
textarea,
|
||||||
select {
|
select {
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
|
@ -356,6 +357,7 @@ select {
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
input[type='text']:focus,
|
input[type='text']:focus,
|
||||||
|
input[type='search']:focus,
|
||||||
textarea:focus,
|
textarea:focus,
|
||||||
select:focus {
|
select:focus {
|
||||||
border-color: var(--outline-color);
|
border-color: var(--outline-color);
|
||||||
|
@ -371,7 +373,7 @@ textarea:disabled {
|
||||||
background-color: var(--bg-faded-color);
|
background-color: var(--bg-faded-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
:is(input[type='text'], textarea, select).block {
|
:is(input[type='text'], input[type='search'], textarea, select).block {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,7 @@ const {
|
||||||
PHANPY_WEBSITE: WEBSITE,
|
PHANPY_WEBSITE: WEBSITE,
|
||||||
PHANPY_PRIVACY_POLICY_URL: PRIVACY_POLICY_URL,
|
PHANPY_PRIVACY_POLICY_URL: PRIVACY_POLICY_URL,
|
||||||
PHANPY_IMG_ALT_API_URL: IMG_ALT_API_URL,
|
PHANPY_IMG_ALT_API_URL: IMG_ALT_API_URL,
|
||||||
|
PHANPY_GIPHY_API_KEY: GIPHY_API_KEY,
|
||||||
} = import.meta.env;
|
} = import.meta.env;
|
||||||
|
|
||||||
function Settings({ onClose }) {
|
function Settings({ onClose }) {
|
||||||
|
@ -433,6 +434,37 @@ function Settings({ onClose }) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
{!!GIPHY_API_KEY && authenticated && (
|
||||||
|
<li>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={snapStates.settings.composerGIFPicker}
|
||||||
|
onChange={(e) => {
|
||||||
|
states.settings.composerGIFPicker = e.target.checked;
|
||||||
|
}}
|
||||||
|
/>{' '}
|
||||||
|
GIF Picker for composer
|
||||||
|
</label>
|
||||||
|
<div class="sub-section insignificant">
|
||||||
|
<small>
|
||||||
|
Note: This feature uses external GIF search service, powered
|
||||||
|
by{' '}
|
||||||
|
<a
|
||||||
|
href="https://developers.giphy.com/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
GIPHY
|
||||||
|
</a>
|
||||||
|
. G-rated (suitable for viewing by all ages), tracking
|
||||||
|
parameters are stripped, referrer information is omitted
|
||||||
|
from requests, but search queries and IP address information
|
||||||
|
will still reach their servers.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
{!!IMG_ALT_API_URL && authenticated && (
|
{!!IMG_ALT_API_URL && authenticated && (
|
||||||
<li>
|
<li>
|
||||||
<label>
|
<label>
|
||||||
|
|
|
@ -67,6 +67,7 @@ const states = proxy({
|
||||||
contentTranslationAutoInline: false,
|
contentTranslationAutoInline: false,
|
||||||
shortcutSettingsCloudImportExport: false,
|
shortcutSettingsCloudImportExport: false,
|
||||||
mediaAltGenerator: false,
|
mediaAltGenerator: false,
|
||||||
|
composerGIFPicker: false,
|
||||||
cloakMode: false,
|
cloakMode: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -99,6 +100,8 @@ export function initStates() {
|
||||||
store.account.get('settings-shortcutSettingsCloudImportExport') ?? false;
|
store.account.get('settings-shortcutSettingsCloudImportExport') ?? false;
|
||||||
states.settings.mediaAltGenerator =
|
states.settings.mediaAltGenerator =
|
||||||
store.account.get('settings-mediaAltGenerator') ?? false;
|
store.account.get('settings-mediaAltGenerator') ?? false;
|
||||||
|
states.settings.composerGIFPicker =
|
||||||
|
store.account.get('settings-composerGIFPicker') ?? false;
|
||||||
states.settings.cloakMode = store.account.get('settings-cloakMode') ?? false;
|
states.settings.cloakMode = store.account.get('settings-cloakMode') ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,6 +143,9 @@ subscribe(states, (changes) => {
|
||||||
if (path.join('.') === 'settings.mediaAltGenerator') {
|
if (path.join('.') === 'settings.mediaAltGenerator') {
|
||||||
store.account.set('settings-mediaAltGenerator', !!value);
|
store.account.set('settings-mediaAltGenerator', !!value);
|
||||||
}
|
}
|
||||||
|
if (path.join('.') === 'settings.composerGIFPicker') {
|
||||||
|
store.account.set('settings-composerGIFPicker', !!value);
|
||||||
|
}
|
||||||
if (path?.[0] === 'shortcuts') {
|
if (path?.[0] === 'shortcuts') {
|
||||||
store.account.set('shortcuts', states.shortcuts);
|
store.account.set('shortcuts', states.shortcuts);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue