diff --git a/client/package.json b/client/package.json index 9539c18bb3..978ba05042 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "@coralproject/talk", - "version": "9.0.7", + "version": "9.2.0", "author": "The Coral Project", "homepage": "https://coralproject.net/", "sideEffects": [ diff --git a/client/src/core/client/admin/components/MediaContainer/MediaContainer.tsx b/client/src/core/client/admin/components/MediaContainer/MediaContainer.tsx index 7a55511db7..d01ecdb607 100644 --- a/client/src/core/client/admin/components/MediaContainer/MediaContainer.tsx +++ b/client/src/core/client/admin/components/MediaContainer/MediaContainer.tsx @@ -7,6 +7,7 @@ import { MediaContainer_comment } from "coral-admin/__generated__/MediaContainer import ExternalMedia from "./ExternalMedia"; import GiphyMedia from "./GiphyMedia"; +import TenorMedia from "./TenorMedia"; import TwitterMedia from "./TwitterMedia"; import YouTubeMedia from "./YouTubeMedia"; @@ -57,6 +58,8 @@ const MediaContainer: FunctionComponent = ({ comment }) => { title={media.title} /> ); + case "TenorMedia": + return ; case "%other": return null; } @@ -80,6 +83,10 @@ const enhanced = withFragmentContainer({ still video } + ... on TenorMedia { + url + title + } ... on TwitterMedia { url } diff --git a/client/src/core/client/admin/components/MediaContainer/TenorMedia.tsx b/client/src/core/client/admin/components/MediaContainer/TenorMedia.tsx new file mode 100644 index 0000000000..506a432df7 --- /dev/null +++ b/client/src/core/client/admin/components/MediaContainer/TenorMedia.tsx @@ -0,0 +1,32 @@ +import React, { FunctionComponent, useCallback, useState } from "react"; + +import { BaseButton } from "coral-ui/components/v2"; + +import styles from "./Media.css"; + +interface Props { + url: string | null; + title: string | null; +} + +const TenorMedia: FunctionComponent = ({ url, title }) => { + const [showAnimated, setShowAnimated] = useState(false); + const toggleImage = useCallback(() => { + setShowAnimated(!showAnimated); + }, [showAnimated]); + return ( +
+ + {title + +
+ ); +}; + +export default TenorMedia; diff --git a/client/src/core/client/admin/components/ModerateCard/CommentRevisionContainer.tsx b/client/src/core/client/admin/components/ModerateCard/CommentRevisionContainer.tsx index e0ca1f4a5e..cc8dbc6a47 100644 --- a/client/src/core/client/admin/components/ModerateCard/CommentRevisionContainer.tsx +++ b/client/src/core/client/admin/components/ModerateCard/CommentRevisionContainer.tsx @@ -11,6 +11,7 @@ import YouTubeMedia from "../MediaContainer/YouTubeMedia"; import { CommentRevisionContainer_comment as CommentData } from "coral-admin/__generated__/CommentRevisionContainer_comment.graphql"; import { CommentContent } from "../Comment"; +import TenorMedia from "../MediaContainer/TenorMedia"; interface Props { comment: CommentData; @@ -63,6 +64,9 @@ const CommentRevisionContainer: FunctionComponent = ({ comment }) => { title={c.media.title} /> )} + {c.media && c.media.__typename === "TenorMedia" && ( + + )} ))} @@ -93,6 +97,10 @@ const enhanced = withFragmentContainer({ still video } + ... on TenorMedia { + url + title + } ... on TwitterMedia { url } diff --git a/client/src/core/client/admin/routes/Configure/sections/General/MediaLinksConfig.tsx b/client/src/core/client/admin/routes/Configure/sections/General/MediaLinksConfig.tsx index 28d9adcaf7..51b7738d1e 100644 --- a/client/src/core/client/admin/routes/Configure/sections/General/MediaLinksConfig.tsx +++ b/client/src/core/client/admin/routes/Configure/sections/General/MediaLinksConfig.tsx @@ -9,6 +9,7 @@ import { required, validateWhen, } from "coral-framework/lib/validation"; +import { GQLGIF_MEDIA_SOURCE } from "coral-framework/schema"; import { FieldSet, FormField, @@ -30,8 +31,8 @@ interface Props { disabled: boolean; } -const giphyIsEnabled: Condition = (value, values) => - Boolean(values.media && values.media.giphy.enabled); +const gifsAreEnabled: Condition = (value, values) => + Boolean(values.media && values.media.gifs.enabled); // eslint-disable-next-line no-unused-expressions graphql` @@ -43,10 +44,11 @@ graphql` youtube { enabled } - giphy { + gifs { enabled maxRating key + provider } } } @@ -62,10 +64,10 @@ const MediaLinksConfig: FunctionComponent = ({ disabled }) => { } container={
} > - + - Allow commenters to add a YouTube video, X post or GIF from GIPHY's - library to the end of their comment + Allow commenters to add a YouTube video, X post or GIF's to the end of + their comment @@ -108,11 +110,11 @@ const MediaLinksConfig: FunctionComponent = ({ disabled }) => { /> - - + + @@ -126,14 +128,57 @@ const MediaLinksConfig: FunctionComponent = ({ disabled }) => { } /> + {(props) => { - const giphyDisabled = + const gifsDisabled = !props.values.media || - !props.values.media.giphy || - !props.values.media.giphy.enabled; + !props.values.media.gifs || + !props.values.media.gifs.enabled; + const provider = props.values.media?.gifs?.provider; return ( <> + + + + + + + Determines which provider commenters will search for and + show GIFs from. + + + + {({ input }) => ( + <> + + + Giphy + + + + )} + + + {({ input }) => ( + <> + + + Tenor + + + + )} + + @@ -144,14 +189,14 @@ const MediaLinksConfig: FunctionComponent = ({ disabled }) => { appear in commenters’ search results - + {({ input }) => ( <> G @@ -165,14 +210,14 @@ const MediaLinksConfig: FunctionComponent = ({ disabled }) => { )} - + {({ input }) => ( <> PG @@ -186,14 +231,14 @@ const MediaLinksConfig: FunctionComponent = ({ disabled }) => { )} - + {({ input }) => ( <> PG-13 @@ -209,14 +254,14 @@ const MediaLinksConfig: FunctionComponent = ({ disabled }) => { )} - + {({ input }) => ( <> R @@ -235,31 +280,63 @@ const MediaLinksConfig: FunctionComponent = ({ disabled }) => { Configuration - - ), - }} - > - - For additional information on GIPHY’s API please visit: - https://developers.giphy.com/docs/api - - - - - + + {provider === GQLGIF_MEDIA_SOURCE.GIPHY && ( + + ), + }} + > + + For additional information on GIPHY’s API please visit: + https://developers.giphy.com/docs/api + + + )} + + {provider === GQLGIF_MEDIA_SOURCE.TENOR && ( + + ), + }} + > + + For additional information on TENOR’s API please visit: + https://developers.google.com/tenor/guides/endpoints + - + )} + + + {provider === GQLGIF_MEDIA_SOURCE.GIPHY && ( + + + + )} + {provider === GQLGIF_MEDIA_SOURCE.TENOR && ( + + + + )} + + {({ input, meta }) => ( )} diff --git a/client/src/core/client/admin/test/fixtures.ts b/client/src/core/client/admin/test/fixtures.ts index ee5c9afbe3..771fb3c95b 100644 --- a/client/src/core/client/admin/test/fixtures.ts +++ b/client/src/core/client/admin/test/fixtures.ts @@ -209,7 +209,7 @@ export const settings = createFixture({ premoderateSuspectWords: false, media: { twitter: { enabled: false }, - giphy: { enabled: false }, + gifs: { enabled: false }, youtube: { enabled: false }, external: { enabled: false }, }, diff --git a/client/src/core/client/count/index.ts b/client/src/core/client/count/index.ts index 33d05faa65..6226672fc6 100644 --- a/client/src/core/client/count/index.ts +++ b/client/src/core/client/count/index.ts @@ -1,6 +1,5 @@ import { COUNT_SELECTOR } from "coral-framework/constants"; import detectCountScript from "coral-framework/helpers/detectCountScript"; -import getCurrentScriptOrigin from "coral-framework/helpers/getCurrentScriptOrigin"; import resolveStoryURL from "coral-framework/helpers/resolveStoryURL"; import jsonp from "coral-framework/utils/jsonp"; @@ -23,7 +22,11 @@ interface DetectAndInjectArgs { /** Detects count elements and use jsonp to inject the counts. */ function detectAndInject(opts: DetectAndInjectArgs = {}) { - const ORIGIN = getCurrentScriptOrigin(); + // Get ORIGIN from the count.js script that we know will be on the page. + const ORIGIN = document + .querySelector(".coral-script") + ?.getAttribute("src") + ?.split("/assets")[0]; const STORY_URL = resolveStoryURL(window); /** A map of references pointing to the count query arguments */ diff --git a/client/src/core/client/framework/hooks/index.ts b/client/src/core/client/framework/hooks/index.ts index a9dc9452ae..14b059fea0 100644 --- a/client/src/core/client/framework/hooks/index.ts +++ b/client/src/core/client/framework/hooks/index.ts @@ -15,3 +15,4 @@ export { default as usePersistedSessionState } from "./usePersistedSessionState" export { default as useInMemoryState } from "./useInMemoryState"; export { default as useMemoizer } from "./useMemoizer"; export { default as useModerationLink } from "./useModerationLink"; +export { default as useDebounce } from "./useDebounce"; diff --git a/client/src/core/client/framework/hooks/useDebounce.ts b/client/src/core/client/framework/hooks/useDebounce.ts new file mode 100644 index 0000000000..1685fd5b57 --- /dev/null +++ b/client/src/core/client/framework/hooks/useDebounce.ts @@ -0,0 +1,41 @@ +import { useLayoutEffect, useMemo, useRef, useState } from "react"; + +const useDebounce = (callback: (...args: any[]) => void, delay: number) => { + const callbackRef = useRef(callback); + + useLayoutEffect(() => { + callbackRef.current = callback; + }); + + const [timer, setTimer] = useState(null); + + const naiveDebounce = useMemo(() => { + const deb = ( + func: (...args: any[]) => void, + delayMs: number, + ...args: any[] + ) => { + if (timer) { + clearTimeout(timer); + setTimer(null); + } + + const t = setTimeout(() => { + func(args); + }, delayMs); + + setTimer(t as unknown as number); + }; + + return deb; + }, [timer, setTimer]); + + return useMemo( + () => + (...args: any) => + naiveDebounce(callbackRef.current, delay, args), + [delay, naiveDebounce] + ); +}; + +export default useDebounce; diff --git a/client/src/core/client/stream/common/Media/TenorMedia.css b/client/src/core/client/stream/common/Media/TenorMedia.css new file mode 100644 index 0000000000..642bfb2c58 --- /dev/null +++ b/client/src/core/client/stream/common/Media/TenorMedia.css @@ -0,0 +1,7 @@ +.tenorMedia { + +} + +.tenorMediaImage { + max-width: 100%; +} diff --git a/client/src/core/client/stream/common/Media/TenorMedia.tsx b/client/src/core/client/stream/common/Media/TenorMedia.tsx new file mode 100644 index 0000000000..b47e482fdd --- /dev/null +++ b/client/src/core/client/stream/common/Media/TenorMedia.tsx @@ -0,0 +1,24 @@ +import React, { FunctionComponent } from "react"; + +import styles from "./TenorMedia.css"; + +interface Props { + url: string; + title?: string | null; +} + +const TenorMedia: FunctionComponent = ({ url, title }) => { + return ( +
+ {title +
+ ); +}; + +export default TenorMedia; diff --git a/client/src/core/client/stream/common/useFetchWithAuth.ts b/client/src/core/client/stream/common/useFetchWithAuth.ts new file mode 100644 index 0000000000..6aa0bc5c07 --- /dev/null +++ b/client/src/core/client/stream/common/useFetchWithAuth.ts @@ -0,0 +1,42 @@ +import { useCallback } from "react"; +import { graphql } from "react-relay"; + +import { buildURL, parseURL } from "coral-framework/utils"; + +import { useFetchWithAuth_local } from "coral-stream/__generated__/useFetchWithAuth_local.graphql"; + +import { useLocal } from "../../framework/lib/relay"; + +const processURL = (url: string) => { + const parsedURL = parseURL(url); + return buildURL(parsedURL); +}; + +const useFetchWithAuth = () => { + const [{ accessToken }] = useLocal(graphql` + fragment useFetchWithAuth_local on Local { + accessToken + } + `); + + const fetchWithAuth = useCallback( + async (url: string, init?: RequestInit) => { + const params = { + ...init, + headers: new Headers({ + Authorization: `Bearer ${accessToken}`, + }), + }; + + const processedURL = processURL(url); + const response = await fetch(processedURL, params); + + return response; + }, + [accessToken] + ); + + return fetchWithAuth; +}; + +export default useFetchWithAuth; diff --git a/client/src/core/client/stream/tabs/Comments/Comment/EditCommentForm/EditCommentFormContainer.tsx b/client/src/core/client/stream/tabs/Comments/Comment/EditCommentForm/EditCommentFormContainer.tsx index 264764ff78..fd46afe8c4 100644 --- a/client/src/core/client/stream/tabs/Comments/Comment/EditCommentForm/EditCommentFormContainer.tsx +++ b/client/src/core/client/stream/tabs/Comments/Comment/EditCommentForm/EditCommentFormContainer.tsx @@ -50,6 +50,11 @@ function getMediaFromComment(comment: CommentData) { type: "giphy", url: comment.revision.media.url, }; + case "TenorMedia": + return { + type: "tenor", + url: comment.revision.media.url, + }; case "TwitterMedia": return { type: "twitter", @@ -198,6 +203,10 @@ const enhanced = withEditCommentMutation( still video } + ... on TenorMedia { + url + title + } ... on TwitterMedia { url width @@ -242,10 +251,11 @@ const enhanced = withEditCommentMutation( youtube { enabled } - giphy { + gifs { enabled key maxRating + provider } external { enabled diff --git a/client/src/core/client/stream/tabs/Comments/Comment/MediaSection/MediaSectionContainer.tsx b/client/src/core/client/stream/tabs/Comments/Comment/MediaSection/MediaSectionContainer.tsx index 37ca91c655..3d82675c24 100644 --- a/client/src/core/client/stream/tabs/Comments/Comment/MediaSection/MediaSectionContainer.tsx +++ b/client/src/core/client/stream/tabs/Comments/Comment/MediaSection/MediaSectionContainer.tsx @@ -14,6 +14,7 @@ import { TwitterMedia, YouTubeMedia, } from "coral-stream/common/Media"; +import TenorMedia from "coral-stream/common/Media/TenorMedia"; import { AddIcon, ButtonSvgIcon, @@ -82,7 +83,7 @@ const MediaSectionContainer: FunctionComponent = ({ if ( (media.__typename === "TwitterMedia" && !settings.media.twitter.enabled) || (media.__typename === "YouTubeMedia" && !settings.media.youtube.enabled) || - (media.__typename === "GiphyMedia" && !settings.media.giphy.enabled) || + (media.__typename === "GiphyMedia" && !settings.media.gifs.enabled) || (media.__typename === "ExternalMedia" && !settings.media.external.enabled) ) { return null; @@ -113,7 +114,10 @@ const MediaSectionContainer: FunctionComponent = ({
)} {media.__typename === "GiphyMedia" && ( - Show GIF + Show GIF + )} + {media.__typename === "TenorMedia" && ( + Show GIF )} ); @@ -141,7 +145,10 @@ const MediaSectionContainer: FunctionComponent = ({
)} {media.__typename === "GiphyMedia" && ( - Hide GIF + Hide GIF + )} + {media.__typename === "TenorMedia" && ( + Hide GIF )} {media.__typename === "YouTubeMedia" && ( @@ -188,6 +195,7 @@ const MediaSectionContainer: FunctionComponent = ({ video={media.video} /> )} + {media.__typename === "TenorMedia" && } ); }; @@ -209,6 +217,9 @@ const enhanced = withFragmentContainer({ still video } + ... on TenorMedia { + url + } ... on TwitterMedia { url width @@ -234,8 +245,9 @@ const enhanced = withFragmentContainer({ youtube { enabled } - giphy { + gifs { enabled + provider } external { enabled diff --git a/client/src/core/client/stream/tabs/Comments/Comment/ReplyCommentForm/ReplyCommentFormContainer.tsx b/client/src/core/client/stream/tabs/Comments/Comment/ReplyCommentForm/ReplyCommentFormContainer.tsx index de1bb5700e..4d027c7e09 100644 --- a/client/src/core/client/stream/tabs/Comments/Comment/ReplyCommentForm/ReplyCommentFormContainer.tsx +++ b/client/src/core/client/stream/tabs/Comments/Comment/ReplyCommentForm/ReplyCommentFormContainer.tsx @@ -356,10 +356,11 @@ const enhanced = withCreateCommentReplyMutation( youtube { enabled } - giphy { + gifs { enabled key maxRating + provider } external { enabled diff --git a/client/src/core/client/stream/tabs/Comments/GifSearchInput/GifSearchInput.css b/client/src/core/client/stream/tabs/Comments/GifSearchInput/GifSearchInput.css new file mode 100644 index 0000000000..6fae8a3ab8 --- /dev/null +++ b/client/src/core/client/stream/tabs/Comments/GifSearchInput/GifSearchInput.css @@ -0,0 +1,9 @@ +.input { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.searchButton { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} diff --git a/client/src/core/client/stream/tabs/Comments/GifSearchInput/GifSearchInput.tsx b/client/src/core/client/stream/tabs/Comments/GifSearchInput/GifSearchInput.tsx new file mode 100644 index 0000000000..b9f12e4d59 --- /dev/null +++ b/client/src/core/client/stream/tabs/Comments/GifSearchInput/GifSearchInput.tsx @@ -0,0 +1,61 @@ +import { Localized } from "@fluent/react/compat"; +import React, { FunctionComponent, KeyboardEvent } from "react"; + +import { ButtonSvgIcon, SearchIcon } from "coral-ui/components/icons"; +import { + Button, + HorizontalGutter, + InputLabel, + TextField, +} from "coral-ui/components/v2"; + +import styles from "./GifSearchInput.css"; + +interface GifSearchInputProps { + debouncedQuery: string; + onChange: React.ChangeEventHandler; + onKeyPress: (e: KeyboardEvent) => void; + inputRef: React.RefObject; +} + +export const GifSearchInput: FunctionComponent = ({ + debouncedQuery, + onChange, + onKeyPress, + inputRef, +}) => { + return ( + + + + Search for a GIF + + + + + + } + ref={inputRef} + /> + + ); +}; diff --git a/client/src/core/client/stream/tabs/Comments/GiphyInput/GiphyInput.css b/client/src/core/client/stream/tabs/Comments/GiphyInput/GiphyInput.css index 419d3a03c4..43c0e00a8f 100644 --- a/client/src/core/client/stream/tabs/Comments/GiphyInput/GiphyInput.css +++ b/client/src/core/client/stream/tabs/Comments/GiphyInput/GiphyInput.css @@ -30,7 +30,6 @@ } .result { - /* flex: 1; */ min-height: 0; min-width: 0; } diff --git a/client/src/core/client/stream/tabs/Comments/GiphyInput/GiphyInput.tsx b/client/src/core/client/stream/tabs/Comments/GiphyInput/GiphyInput.tsx index 9179130cc6..7d9721a614 100644 --- a/client/src/core/client/stream/tabs/Comments/GiphyInput/GiphyInput.tsx +++ b/client/src/core/client/stream/tabs/Comments/GiphyInput/GiphyInput.tsx @@ -16,14 +16,9 @@ import React, { import useDebounce from "react-use/lib/useDebounce"; import useResizeObserver from "use-resize-observer"; -import { ButtonSvgIcon, SearchIcon } from "coral-ui/components/icons"; -import { - Button, - HorizontalGutter, - InputLabel, - TextField, -} from "coral-ui/components/v2"; +import { HorizontalGutter } from "coral-ui/components/v2"; +import { GifSearchInput } from "../GifSearchInput/GifSearchInput"; import GiphyAttribution from "./GiphyAttribution"; import styles from "./GiphyInput.css"; @@ -106,38 +101,12 @@ const GiphyInput: FunctionComponent = ({ return (
- - - - Search for a GIF - - - - - - } - ref={inputRef} - /> - +
{query && ( = ({ }, []); const toggleGIFSelector = useCallback(() => { - setMediaWidget(createWidgetToggle("giphy")); + setMediaWidget(createWidgetToggle("gifs")); }, []); - const showGifSelector = mediaWidget === "giphy"; + const showGifSelector = mediaWidget === "gifs"; const showExternalImageInput = mediaWidget === "external"; return ( @@ -277,6 +280,7 @@ const CommentForm: FunctionComponent = ({ onSubmit={handleSubmit} id="comments-postCommentForm-form" > +
{hasValidationErrors}
{mode === "rating" && ( )} @@ -366,7 +370,7 @@ const CommentForm: FunctionComponent = ({ ) : null} - {mediaConfig && mediaConfig.giphy.enabled ? ( + {mediaConfig && mediaConfig.gifs.enabled ? ( = ({ pastedMedia={pastedMedia} setPastedMedia={setPastedMedia} siteID={siteID} - giphyConfig={mediaConfig.giphy} + gifConfig={mediaConfig.gifs} />
diff --git a/client/src/core/client/stream/tabs/Comments/Stream/CommentForm/MediaField.tsx b/client/src/core/client/stream/tabs/Comments/Stream/CommentForm/MediaField.tsx index 1780150cf9..47ba774383 100644 --- a/client/src/core/client/stream/tabs/Comments/Stream/CommentForm/MediaField.tsx +++ b/client/src/core/client/stream/tabs/Comments/Stream/CommentForm/MediaField.tsx @@ -6,9 +6,12 @@ import { isMediaLink, MediaLink, } from "coral-common/common/lib/helpers/findMediaLinks"; +import { GQLGIF_MEDIA_SOURCE } from "coral-framework/schema"; import { AlertCircleIcon, SvgIcon } from "coral-ui/components/icons"; import { CallOut } from "coral-ui/components/v3"; +import { GIF_MEDIA_SOURCE } from "coral-stream/__generated__/MediaSettingsContainer_settings.graphql"; + import { MediaConfirmPrompt, MediaPreview, @@ -16,13 +19,15 @@ import { import ExternalImageInput from "../../ExternalImageInput"; import GiphyInput, { GifPreview } from "../../GiphyInput"; import { getMediaValidators } from "../../helpers"; +import TenorInput, { GifResult } from "../../TenorInput/TenorInput"; -export type Widget = "giphy" | "external" | null; +export type Widget = "gifs" | "external" | null; -interface GiphyConfig { +interface GifConfig { key: string | null; enabled: boolean; maxRating: string | null; + provider?: GIF_MEDIA_SOURCE | null; } interface Props { @@ -31,12 +36,12 @@ interface Props { siteID: string; pastedMedia: MediaLink | null; setPastedMedia: (media: MediaLink | null) => void; - giphyConfig: GiphyConfig; + gifConfig: GifConfig; } interface Media { id?: string; - type: "giphy" | "twitter" | "youtube" | "external"; + type: "giphy" | "tenor" | "twitter" | "youtube" | "external"; url: string; width?: string; height?: string; @@ -48,7 +53,7 @@ const MediaField: FunctionComponent = ({ siteID, pastedMedia, setPastedMedia, - giphyConfig, + gifConfig, }) => { const { input: { value, onChange }, @@ -58,13 +63,27 @@ const MediaField: FunctionComponent = ({ }); const onGiphySelect = useCallback( - (gif: IGif) => + (gif: IGif) => { onChange({ type: "giphy", id: gif.id, url: gif.images.original.url, - }), - [onChange] + }); + setWidget(null); + }, + [onChange, setWidget] + ); + + const onTenorSelect = useCallback( + (gif: GifResult) => { + onChange({ + type: "tenor", + id: gif.id, + url: gif.url, + }); + setWidget(null); + }, + [onChange, setWidget] ); const onExternalImageSelect = useCallback( @@ -148,14 +167,21 @@ const MediaField: FunctionComponent = ({ onConfirm={onConfirmPastedMedia} onRemove={onRemove} /> - ) : widget === "giphy" ? ( - giphyConfig.key && - giphyConfig.maxRating && ( - + ) : widget === "gifs" ? ( + gifConfig.key && + gifConfig.maxRating && ( + <> + {gifConfig.provider === GQLGIF_MEDIA_SOURCE.GIPHY && ( + + )} + {gifConfig.provider === GQLGIF_MEDIA_SOURCE.TENOR && ( + + )} + ) ) : widget === "external" ? ( diff --git a/client/src/core/client/stream/tabs/Comments/Stream/PostCommentForm/PostCommentForm.tsx b/client/src/core/client/stream/tabs/Comments/Stream/PostCommentForm/PostCommentForm.tsx index 17113ec47a..7a258c3059 100644 --- a/client/src/core/client/stream/tabs/Comments/Stream/PostCommentForm/PostCommentForm.tsx +++ b/client/src/core/client/stream/tabs/Comments/Stream/PostCommentForm/PostCommentForm.tsx @@ -18,7 +18,7 @@ import PostCommentSubmitStatusContainer from "./PostCommentSubmitStatusContainer import styles from "./PostCommentForm.css"; interface MediaProps { - type: "twitter" | "youtube" | "giphy" | "external"; + type: "twitter" | "youtube" | "giphy" | "tenor" | "external"; url: string; id: string | null; } diff --git a/client/src/core/client/stream/tabs/Comments/Stream/PostCommentForm/PostCommentFormContainer.tsx b/client/src/core/client/stream/tabs/Comments/Stream/PostCommentForm/PostCommentFormContainer.tsx index add2f3f7bf..2f9b126872 100644 --- a/client/src/core/client/stream/tabs/Comments/Stream/PostCommentForm/PostCommentFormContainer.tsx +++ b/client/src/core/client/stream/tabs/Comments/Stream/PostCommentForm/PostCommentFormContainer.tsx @@ -414,10 +414,11 @@ const enhanced = withFragmentContainer({ youtube { enabled } - giphy { + gifs { enabled key maxRating + provider } external { enabled diff --git a/client/src/core/client/stream/tabs/Comments/TenorInput/TenorAttribution.css b/client/src/core/client/stream/tabs/Comments/TenorInput/TenorAttribution.css new file mode 100644 index 0000000000..64087e51ef --- /dev/null +++ b/client/src/core/client/stream/tabs/Comments/TenorInput/TenorAttribution.css @@ -0,0 +1,4 @@ +.img { + align-self: flex-start; + width: 50px; +} diff --git a/client/src/core/client/stream/tabs/Comments/TenorInput/TenorAttribution.tsx b/client/src/core/client/stream/tabs/Comments/TenorInput/TenorAttribution.tsx new file mode 100644 index 0000000000..8b35734c27 --- /dev/null +++ b/client/src/core/client/stream/tabs/Comments/TenorInput/TenorAttribution.tsx @@ -0,0 +1,27 @@ +import { Localized } from "@fluent/react/compat"; +import React, { FunctionComponent } from "react"; + +import { Flex } from "coral-ui/components/v2"; + +import tenorAttributionImg from "./tenorAttribution.png"; + +import styles from "./TenorAttribution.css"; + +const TenorAttribution: FunctionComponent = () => { + return ( + + + powered by Tenor + + + ); +}; + +export default TenorAttribution; diff --git a/client/src/core/client/stream/tabs/Comments/TenorInput/TenorGrid.css b/client/src/core/client/stream/tabs/Comments/TenorInput/TenorGrid.css new file mode 100644 index 0000000000..458806cd34 --- /dev/null +++ b/client/src/core/client/stream/tabs/Comments/TenorInput/TenorGrid.css @@ -0,0 +1,37 @@ +.grid { + max-height: 300px; + overflow: auto; +} + +.gridColumns { + display: flex; + flex-direction: row; + justify-content: center; +} + +.gridColumn { + display: flex; + flex-direction: column; +} + +.gridControls { + width: 100%; + display: flex; + justify-content: center; + align-items: center; +} + +.gridItem { + width: 85px; + background: var(--palette-background-body); + border-style: none; + padding: 2px; + + display: flex; + justify-content: center; +} + +.gridImage { + width: 100%; + height: auto; +} diff --git a/client/src/core/client/stream/tabs/Comments/TenorInput/TenorGrid.tsx b/client/src/core/client/stream/tabs/Comments/TenorInput/TenorGrid.tsx new file mode 100644 index 0000000000..a12bc4642a --- /dev/null +++ b/client/src/core/client/stream/tabs/Comments/TenorInput/TenorGrid.tsx @@ -0,0 +1,158 @@ +import { Localized } from "@fluent/react/compat"; +import React, { + FunctionComponent, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; + +import { useCoralContext } from "coral-framework/lib/bootstrap"; +import { Button } from "coral-ui/components/v2"; + +import { GifResult } from "./TenorInput"; + +import styles from "./TenorGrid.css"; + +interface GridItemProps { + gif: GifResult; + onSelect: (gif: GifResult) => void; +} + +const TenorGridItem: FunctionComponent = ({ gif, onSelect }) => { + const onClick = useCallback(() => { + onSelect(gif); + }, [gif, onSelect]); + + return ( + + ); +}; + +interface GridColumnsProps { + gifs: GifResult[]; + onSelectGif: (gif: GifResult) => void; + numColumns: number; +} + +const TenorGridColumns: FunctionComponent = ({ + gifs, + onSelectGif, + numColumns, +}) => { + const columns = useMemo(() => { + const resultColumns: GifResult[][] = []; + for (let i = 0; i < numColumns; i++) { + resultColumns.push(new Array()); + } + + let columnIndex = 0; + // eslint-disable-next-line @typescript-eslint/prefer-for-of + for (let j = 0; j < gifs.length; j++) { + const column = resultColumns[columnIndex]; + const gif = gifs[j]; + + column.push(gif); + + columnIndex++; + if (columnIndex >= numColumns) { + columnIndex = 0; + } + } + + return resultColumns; + }, [gifs, numColumns]); + + return ( +
+ {columns.map((colGifs, colIndex) => { + return ( +
+ {colGifs && + colGifs.map((gif, index) => { + return ( + + ); + })} +
+ ); + })} +
+ ); +}; + +interface Props { + gifs: GifResult[]; + showLoadMore?: boolean; + + onSelectGif: (gif: GifResult) => void; + onLoadMore: () => void; +} + +const TenorGrid: FunctionComponent = ({ + gifs, + showLoadMore, + onSelectGif, + onLoadMore, +}) => { + const { window } = useCoralContext(); + + const gridRef = useRef(null); + const [cols, setCols] = useState(0); + + const resizeGrid = useCallback(() => { + if (!gridRef || !gridRef.current) { + setCols(0); + return; + } + + const rect = gridRef.current.getBoundingClientRect(); + const numCols = rect.width / 90; + + setCols(numCols); + }, [gridRef, setCols]); + + useEffect(() => { + window.requestAnimationFrame(resizeGrid); + window.addEventListener("resize", resizeGrid); + + return () => { + window.removeEventListener("resize", resizeGrid); + }; + // include gifs so we re-calc grid col's on gif change + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [gifs]); + + return ( +
+ {gifs && cols > 0 && ( + + )} + {showLoadMore && ( +
+ + + +
+ )} +
+ ); +}; + +export default TenorGrid; diff --git a/client/src/core/client/stream/tabs/Comments/TenorInput/TenorInput.css b/client/src/core/client/stream/tabs/Comments/TenorInput/TenorInput.css new file mode 100644 index 0000000000..1446ef3e8c --- /dev/null +++ b/client/src/core/client/stream/tabs/Comments/TenorInput/TenorInput.css @@ -0,0 +1,41 @@ +.root { + padding: var(--spacing-2); +} + +.noResults { + font-family: var(--font-family-primary); + font-size: var(--font-size-3); + color: var(--palette-text-100); +} + +.loading { + font-family: var(--font-family-primary); + font-size: var(--font-size-3); + color: var(--palette-text-100); + text-align: center; +} + +.results { + overflow: hidden; +} + +.result { + min-height: 0; + min-width: 0; +} + +.resultImg { + display: block; + max-width: 100%; +} + +.input { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.searchButton { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + diff --git a/client/src/core/client/stream/tabs/Comments/TenorInput/TenorInput.tsx b/client/src/core/client/stream/tabs/Comments/TenorInput/TenorInput.tsx new file mode 100644 index 0000000000..5d0f62e6fd --- /dev/null +++ b/client/src/core/client/stream/tabs/Comments/TenorInput/TenorInput.tsx @@ -0,0 +1,199 @@ +import { Localized } from "@fluent/react/compat"; +import React, { + ChangeEventHandler, + FunctionComponent, + KeyboardEvent, + Ref, + useCallback, + useEffect, + useRef, + useState, +} from "react"; +import useResizeObserver from "use-resize-observer"; + +import { useDebounce } from "coral-framework/hooks"; +import { useCoralContext } from "coral-framework/lib/bootstrap"; +import useFetchWithAuth from "coral-stream/common/useFetchWithAuth"; +import { ButtonSvgIcon, SearchIcon } from "coral-ui/components/icons"; +import { Button, HorizontalGutter, TextField } from "coral-ui/components/v2"; + +import TenorAttribution from "./TenorAttribution"; +import TenorGrid from "./TenorGrid"; + +import styles from "./TenorInput.css"; + +const DEBOUNCE_DELAY_MS = 1250; + +interface Props { + onSelect: (gif: GifResult) => void; + forwardRef?: Ref; +} + +export interface GifResult { + id: string; + url: string; + preview: string; + title?: string; +} + +export interface SearchPayload { + results: GifResult[]; + next?: string; +} + +const TenorInput: FunctionComponent = ({ onSelect }) => { + const { rootURL } = useCoralContext(); + const fetchWithAuth = useFetchWithAuth(); + + const [query, setQuery] = useState(""); + const [next, setNext] = useState(null); + const [gifs, setGifs] = useState([]); + + const inputRef = useRef(null); + + const { ref } = useResizeObserver(); + + const fetchGifs = useCallback( + async (q: string, n?: string | null) => { + if (!q || q.length === 0) { + return null; + } + + const url = new URL("/api/tenor/search", rootURL); + url.searchParams.set("query", q); + + if (n) { + url.searchParams.set("pos", n); + } + + const response = await fetchWithAuth(url.toString()); + + if (!response.ok) { + return null; + } + + const json = (await response.json()) as SearchPayload; + if (!json) { + return null; + } + + return json; + }, + [fetchWithAuth, rootURL] + ); + + const loadGifs = useCallback(async () => { + const response = await fetchGifs(query); + if (!response) { + return; + } + + setGifs(response.results); + setNext(response.next ?? null); + }, [query, fetchGifs]); + + const loadMoreGifs = useCallback(async () => { + const response = await fetchGifs(query, next); + if (!response) { + return; + } + + setGifs([...gifs, ...response.results]); + setNext(response.next ?? null); + }, [fetchGifs, gifs, query, next]); + + const debounceFetchGifs = useDebounce(loadGifs, DEBOUNCE_DELAY_MS); + + const onChange: ChangeEventHandler = useCallback( + async (e) => { + setQuery(e.target.value); + setTimeout(debounceFetchGifs, 300); + }, + [debounceFetchGifs, setQuery] + ); + + // Focus on the input as soon as the input is available. + useEffect(() => { + if (inputRef.current) { + inputRef.current.focus(); + } + }, []); + + const onKeyPress = useCallback( + async (e: KeyboardEvent) => { + if (e.key !== "Enter") { + return; + } + + debounceFetchGifs(); + + e.preventDefault(); + }, + [debounceFetchGifs] + ); + + const onClickSearch = useCallback(async () => { + setNext(null); + await loadGifs(); + }, [loadGifs]); + + const onLoadMore = useCallback(async () => { + if (!next) { + return; + } + + await loadMoreGifs(); + }, [loadMoreGifs, next]); + + const onGifClick = useCallback( + (gif: GifResult) => { + // Cancel any active timers that might cause the query to be changed. + setQuery(""); + onSelect(gif); + }, + [onSelect] + ); + + return ( +
+ + + + + } + ref={inputRef} + /> + 0 && query?.length > 0) + } + onSelectGif={onGifClick} + onLoadMore={onLoadMore} + /> + + +
+ ); +}; + +export default TenorInput; diff --git a/client/src/core/client/stream/tabs/Comments/TenorInput/tenorAttribution.png b/client/src/core/client/stream/tabs/Comments/TenorInput/tenorAttribution.png new file mode 100644 index 0000000000..5e39743ee3 Binary files /dev/null and b/client/src/core/client/stream/tabs/Comments/TenorInput/tenorAttribution.png differ diff --git a/client/src/core/client/stream/tabs/Comments/helpers/getCommentBodyValidators.ts b/client/src/core/client/stream/tabs/Comments/helpers/getCommentBodyValidators.ts index 99928bae08..838db1021a 100644 --- a/client/src/core/client/stream/tabs/Comments/helpers/getCommentBodyValidators.ts +++ b/client/src/core/client/stream/tabs/Comments/helpers/getCommentBodyValidators.ts @@ -13,7 +13,9 @@ import getHTMLCharacterLength from "./getHTMLCharacterLength"; const hasMediaAttached: Condition = (value, values) => !!values.media && - (values.media.type === "giphy" || values.media.type === "external") && + (values.media.type === "giphy" || + values.media.type === "tenor" || + values.media.type === "external") && !!values.media.url; const hasRatingAttached: Condition = (value, values) => !!values.rating; diff --git a/client/src/core/client/stream/tabs/Profile/Preferences/MediaSettingsContainer.tsx b/client/src/core/client/stream/tabs/Profile/Preferences/MediaSettingsContainer.tsx index 278f9aa2b6..2c46e9128f 100644 --- a/client/src/core/client/stream/tabs/Profile/Preferences/MediaSettingsContainer.tsx +++ b/client/src/core/client/stream/tabs/Profile/Preferences/MediaSettingsContainer.tsx @@ -74,7 +74,7 @@ const MediaSettingsContainer: FunctionComponent = ({ ); if ( - !settings.media.giphy.enabled && + !settings.media.gifs.enabled && !settings.media.twitter.enabled && !settings.media.youtube.enabled && !settings.media.external.enabled @@ -188,8 +188,9 @@ const enhanced = withFragmentContainer({ settings: graphql` fragment MediaSettingsContainer_settings on Settings { media { - giphy { + gifs { enabled + provider } twitter { enabled diff --git a/client/src/core/client/stream/test/fixtures.ts b/client/src/core/client/stream/test/fixtures.ts index 6c3f0adddc..c30cd4abb2 100644 --- a/client/src/core/client/stream/test/fixtures.ts +++ b/client/src/core/client/stream/test/fixtures.ts @@ -121,7 +121,7 @@ export const settings = createFixture({ media: { twitter: { enabled: false }, youtube: { enabled: false }, - giphy: { enabled: false }, + gifs: { enabled: false }, external: { enabled: false }, }, multisite: false, diff --git a/common/package.json b/common/package.json index 36a85ae064..5d590ced5b 100644 --- a/common/package.json +++ b/common/package.json @@ -1,6 +1,6 @@ { "name": "common", - "version": "9.0.7", + "version": "9.2.0", "description": "", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/config/package.json b/config/package.json index 93cddc8f28..2ffb9254b5 100644 --- a/config/package.json +++ b/config/package.json @@ -1,6 +1,6 @@ { "name": "common", - "version": "9.0.7", + "version": "9.2.0", "description": "", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/locales/ar-AE/stream.ftl b/locales/ar-AE/stream.ftl index 6322322e7e..1003d6db96 100644 --- a/locales/ar-AE/stream.ftl +++ b/locales/ar-AE/stream.ftl @@ -364,8 +364,8 @@ comments-permalink-linkCopied = تم نسخ الرابط comments-embedLinks-showEmbeds = إظهار التضمين comments-embedLinks-hideEmbeds = إخفاء التصمين -comments-embedLinks-show-giphy = إظهار الصورة المتحركة -comments-embedLinks-hide-giphy = إخفاء الصورة المتحركة +comments-embedLinks-show-gif = إظهار الصورة المتحركة +comments-embedLinks-hide-gif = إخفاء الصورة المتحركة comments-embedLinks-show-youtube = إظهار الفيديو comments-embedLinks-hide-youtube = إخفاء الفيديو diff --git a/locales/de-CH/stream.ftl b/locales/de-CH/stream.ftl index 2edf9fca03..f8fecce26b 100644 --- a/locales/de-CH/stream.ftl +++ b/locales/de-CH/stream.ftl @@ -323,8 +323,8 @@ comments-permalink-linkCopied = Link wurde kopiert comments-embedLinks-showEmbeds = Embeds anzeigen comments-embedLinks-hideEmbeds = Embeds verbergen -comments-embedLinks-show-giphy = GIF anzeigen -comments-embedLinks-hide-giphy = GIF verbergen +comments-embedLinks-show-gif = GIF anzeigen +comments-embedLinks-hide-gif = GIF verbergen comments-embedLinks-show-youtube = Video anzeigen comments-embedLinks-hide-youtube = Video verbergen diff --git a/locales/de/stream.ftl b/locales/de/stream.ftl index 3624fd6c3c..cd5a8164bf 100644 --- a/locales/de/stream.ftl +++ b/locales/de/stream.ftl @@ -374,8 +374,8 @@ comments-permalink-linkCopied = Link kopiert comments-embedLinks-showEmbeds = Eingebettete Links anzeigen comments-embedLinks-hideEmbeds = Eingebettete Links ausblenden -comments-embedLinks-show-giphy = GIF anzeigen -comments-embedLinks-hide-giphy = GIF ausblenden +comments-embedLinks-show-gif = GIF anzeigen +comments-embedLinks-hide-gif = GIF ausblenden comments-embedLinks-show-youtube = Video anzeigen comments-embedLinks-hide-youtube = Video ausblenden @@ -874,3 +874,82 @@ stream-footer-links-discussions = Weitere Diskussionen .title = Zu weiteren Diskussionen gehen. stream-footer-navigation = .aria-label = Kommentar-Fußzeile + +## Notifications + +notifications-title = Benachrichtigungen +notifications-loadMore = Mehr laden +notifications-loadNew = Neu laden + +notifications-adjustPreferences = Benachrichtigungseinstellungen anpassen unter Mein Profil > + +notification-comment-toggle-default-open = - Kommentar +notification-comment-toggle-default-closed = + Kommentar + +notifications-comment-showRemovedComment = + Entfernten Kommentar anzeigen +notifications-comment-hideRemovedComment = - Entfernten Kommentar ausblenden + +notification-comment-description-featured = Dein Kommentar zu "{ $title }" wurde von einem Mitglied unseres Teams hervorgehoben. +notification-comment-description-default = zu "{ $title }" +notification-comment-media-image = Bild +notification-comment-media-embed = Embed +notification-comment-media-gif = Gif + +notifications-yourIllegalContentReportHasBeenReviewed = + Dein Bericht über illegale Inhalte wurde überprüft +notifications-yourCommentHasBeenRejected = + Dein Kommentar wurde abgelehnt +notifications-yourCommentHasBeenApproved = + Dein Kommentar wurde genehmigt +notifications-yourCommentHasBeenFeatured = + Dein Kommentar wurde hervorgehoben +notifications-yourCommentHasReceivedAReply = + Neue Antwort von { $author } +notifications-defaultTitle = Benachrichtigung + +notifications-rejectedComment-body = + Der Inhalt deines Kommentars verstößt gegen unsere Gemeinschaftsrichtlinien. Der Kommentar wurde entfernt. +notifications-rejectedComment-wasPending-body = + Der Inhalt deines Kommentars verstößt gegen unsere Gemeinschaftsrichtlinien. +notifications-reasonForRemoval = Grund für die Entfernung +notifications-legalGrounds = Rechtliche Grundlage +notifications-additionalExplanation = Zusätzliche Erklärung + +notifications-repliedComment-hideReply = - Antwort ausblenden +notifications-repliedComment-showReply = + Antwort anzeigen +notifications-repliedComment-hideOriginalComment = - Mein Originalkommentar ausblenden +notifications-repliedComment-showOriginalComment = + Mein Originalkommentar anzeigen + +notifications-dsaReportLegality-legal = Legaler Inhalt +notifications-dsaReportLegality-illegal = Potenziell illegaler Inhalt +notifications-dsaReportLegality-unknown = Unbekannt + +notifications-rejectionReason-offensive = Dieser Kommentar enthält beleidigende Sprache +notifications-rejectionReason-abusive = Dieser Kommentar enthält missbräuchliche Sprache +notifications-rejectionReason-spam = Dieser Kommentar ist Spam +notifications-rejectionReason-bannedWord = Verbotenes Wort +notifications-rejectionReason-ad = Dieser Kommentar ist eine Werbung +notifications-rejectionReason-illegalContent = Dieser Kommentar enthält potenziell illegale Inhalte +notifications-rejectionReason-harassmentBullying = Dieser Kommentar enthält belästigende oder einschüchternde Sprache +notifications-rejectionReason-misinformation = Dieser Kommentar enthält Fehlinformationen +notifications-rejectionReason-hateSpeech = Dieser Kommentar enthält Hassrede +notifications-rejectionReason-irrelevant = Dieser Kommentar ist irrelevant für die Diskussion +notifications-rejectionReason-other = Andere Gründe +notifications-rejectionReason-other-customReason = Andere Gründe - { $customReason } +notifications-rejectionReason-unknown = Unbekannt + +notifications-reportDecisionMade-legal = + Am { $date } hast du einen Kommentar von { $author } gemeldet, weil er potenziell illegale Inhalte enthält. Nach Überprüfung deiner Meldung hat unser Moderationsteam entschieden, dass dieser Kommentar keine illegalen Inhalte zu enthalten scheint. Danke, dass du dazu beiträgst, unsere Gemeinschaft sicher zu halten. +notifications-reportDecisionMade-illegal = + Am { $date } hast du einen Kommentar von { $author } gemeldet, weil er potenziell illegale Inhalte enthält. Nach Überprüfung deiner Meldung hat unser Moderationsteam entschieden, dass dieser Kommentar illegale Inhalte enthält und wurde entfernt. Es können weitere Maßnahmen gegen den Kommentator ergriffen werden, jedoch wirst du nicht über weitere Schritte informiert. Danke, dass du dazu beiträgst, unsere Gemeinschaft sicher zu halten. + +notifications-methodOfRedress-none = + Alle Moderationsentscheidungen sind endgültig und können nicht angefochten werden +notifications-methodOfRedress-email = + Um eine Entscheidung, die hier erscheint, anzufechten, kontaktiere bitte { $email } +notifications-methodOfRedress-url = + Um eine Entscheidung, die hier erscheint, anzufechten, besuche bitte { $url } + +notifications-youDoNotCurrentlyHaveAny = Du hast derzeit keine Benachrichtigungen + +notifications-floatingIcon-close = Schließen \ No newline at end of file diff --git a/locales/en-US/admin.ftl b/locales/en-US/admin.ftl index 0a7427b042..b3e8d9f687 100644 --- a/locales/en-US/admin.ftl +++ b/locales/en-US/admin.ftl @@ -431,10 +431,12 @@ configure-general-sitewideCommenting-messageExplanation = #### Embed Links configure-general-embedLinks-title = Embedded media -configure-general-embedLinks-desc = Allow commenters to add a YouTube video, X post or GIF from GIPHY's library to the end of their comment +configure-general-embedLinks-desc = +configure-general-embedLinks-description = + Allow commenters to add a YouTube video, X post or GIF's to the end of their comment configure-general-embedLinks-enableTwitterEmbeds = Allow X post embeds configure-general-embedLinks-enableYouTubeEmbeds = Allow YouTube embeds -configure-general-embedLinks-enableGiphyEmbeds = Allow GIFs from GIPHY +configure-general-embedLinks-enableGifs = Allow GIFs configure-general-embedLinks-enableExternalEmbeds = Enable external media configure-general-embedLinks-On = Yes @@ -453,10 +455,23 @@ configure-general-embedLinks-giphyMaxRating-r = R configure-general-embedLinks-giphyMaxRating-r-desc = Strong language, strong sexual innuendo, violence, and illegal drug use; not suitable for teens or younger. No nudity. configure-general-embedLinks-configuration = Configuration + +configure-general-embedLinks-gifProvider = GIF provider +configure-general-embedLinks-gifProvider-desc = + Determines which provider commenters will search for and show GIFs from. + +configure-general-embedLinks-gifs-provider-Giphy = Giphy +configure-general-embedLinks-gifs-provider-Tenor = Tenor + configure-general-embedLinks-configuration-desc = +configure-general-embedLinks-configuration-giphy-desc = For additional information on GIPHY’s API please visit: https://developers.giphy.com/docs/api configure-general-embedLinks-giphyAPIKey = GIPHY API key +configure-general-embedLinks-configuration-tenor-desc = + For additional information on TENOR’s API please visit: https://developers.google.com/tenor/guides/endpoints +configure-general-embedLinks-tenorAPIKey = TENOR API key + #### Configure Announcements diff --git a/locales/en-US/stream.ftl b/locales/en-US/stream.ftl index bc7d022879..d12f893187 100644 --- a/locales/en-US/stream.ftl +++ b/locales/en-US/stream.ftl @@ -120,10 +120,13 @@ comments-replyList-showMoreReplies = Show More Replies comments-postComment-gifSearch = Search for a GIF comments-postComment-gifSearch-search = .aria-label = Search +comments-postComment-gifSearch-search-loadMore = Load more comments-postComment-gifSearch-loading = Loading... comments-postComment-gifSearch-no-results = No results found for {$query} comments-postComment-gifSearch-powered-by-giphy = .alt = Powered by giphy +comments-postComment-gifSearch-powered-by-tenor = + .alt = Powered by tenor comments-postComment-pasteImage = Paste image URL comments-postComment-insertImage = Insert @@ -464,8 +467,10 @@ comments-permalink-linkCopied = Link copied comments-embedLinks-showEmbeds = Show embeds comments-embedLinks-hideEmbeds = Hide embeds -comments-embedLinks-show-giphy = Show GIF -comments-embedLinks-hide-giphy = Hide GIF +comments-embedLinks-show-gif = Show GIF + +comments-embedLinks-hide-gif = Hide GIF +comments-embedLinks-hide-gif = Hide GIF comments-embedLinks-show-youtube = Show video comments-embedLinks-hide-youtube = Hide video diff --git a/locales/fi-FI/stream.ftl b/locales/fi-FI/stream.ftl index f1c51c11e1..9f8fdd9f24 100644 --- a/locales/fi-FI/stream.ftl +++ b/locales/fi-FI/stream.ftl @@ -325,8 +325,8 @@ comments-permalink-linkCopied = Linkki kopioitu comments-embedLinks-showEmbeds = Näytä upotukset comments-embedLinks-hideEmbeds = Piilota upotukset -comments-embedLinks-show-giphy = Näytä GIF -comments-embedLinks-hide-giphy = Piilota GIF +comments-embedLinks-show-gif = Näytä GIF +comments-embedLinks-hide-gif = Piilota GIF comments-embedLinks-show-youtube = Näytä video comments-embedLinks-hide-youtube = Piilota video diff --git a/locales/fr-FR/admin.ftl b/locales/fr-FR/admin.ftl index 60e581d45c..a83fdd7515 100755 --- a/locales/fr-FR/admin.ftl +++ b/locales/fr-FR/admin.ftl @@ -403,10 +403,12 @@ configure-general-sitewideCommenting-messageExplanation = #### Embed Links configure-general-embedLinks-title = Intégration de média -configure-general-embedLinks-desc = Permettre aux membres d'ajouter une vidéo Youtube, un Tweet ou un GIF de la bibliothèque GIPHY à la fin de leur commentaire +configure-general-embedLinks-desc = +configure-general-embedLinks-description = + Permettre aux membres d'ajouter une vidéo Youtube, un Tweet ou un GIF à la fin de leur commentaire configure-general-embedLinks-enableTwitterEmbeds = Permettre les intégration Twitter configure-general-embedLinks-enableYouTubeEmbeds = Permettre les intégration Youtube -configure-general-embedLinks-enableGiphyEmbeds = Permettre les GIF de GIPHY +configure-general-embedLinks-enableGifs = Permettre les GIF configure-general-embedLinks-enableExternalEmbeds = Permettre un média externe configure-general-embedLinks-On = Oui diff --git a/locales/fr-FR/stream.ftl b/locales/fr-FR/stream.ftl index e3ede453d0..9646211781 100755 --- a/locales/fr-FR/stream.ftl +++ b/locales/fr-FR/stream.ftl @@ -334,8 +334,8 @@ comments-stream-deleteAccount-callOut-cancel = comments-embedLinks-showEmbeds = Afficher les pièces jointes comments-embedLinks-hideEmbeds = Cacher les pièces jointes -comments-embedLinks-show-giphy = Afficher les GIF -comments-embedLinks-hide-giphy = Cacher les GIF +comments-embedLinks-show-gif = Afficher les GIF +comments-embedLinks-hide-gif = Cacher les GIF comments-embedLinks-show-youtube = Afficher les vidéos comments-embedLinks-hide-youtube = Cacher les vidéos diff --git a/locales/hu/stream.ftl b/locales/hu/stream.ftl index d031672c47..ae195340c3 100644 --- a/locales/hu/stream.ftl +++ b/locales/hu/stream.ftl @@ -365,8 +365,8 @@ comments-permalink-linkCopied = Link másolva comments-embedLinks-showEmbeds = Beágyazások megjelenítése comments-embedLinks-hideEmbeds = Beágyazások elrejtése -comments-embedLinks-show-giphy = GIF megjelenítése -comments-embedLinks-hide-giphy = GIF elrejtése +comments-embedLinks-show-gif = GIF megjelenítése +comments-embedLinks-hide-gif = GIF elrejtése comments-embedLinks-show-youtube = Videó megjelenítése comments-embedLinks-hide-youtube = Videó elrejtése diff --git a/locales/id-ID/admin.ftl b/locales/id-ID/admin.ftl index f161da80af..ba586d632a 100644 --- a/locales/id-ID/admin.ftl +++ b/locales/id-ID/admin.ftl @@ -604,13 +604,16 @@ configure-general-sitewideCommenting-messageExplanation = #### Embed Links configure-general-embedLinks-title = Media yang disematkan -configure-general-embedLinks-desc = Izinkan komentar untuk menambahkan video YouTube, Tweet, atau GIF dari perpustakaan GIPHY ke akhir komentar +configure-general-embedLinks-desc = +configure-general-embedLinks-description = + Izinkan komentar untuk menambahkan video YouTube, Tweet, atau GIF ke akhir komentar configure-general-embedLinks-enableTwitterEmbeds = Izinkan penyematan Twitter configure-general-embedLinks-enableYouTubeEmbeds = Izinkan penyematan YouTube -configure-general-embedLinks-enableGiphyEmbeds = Izinkan penyematan GIF dari GIPHY +configure-general-embedLinks-enableGifs = Izinkan penyematan GIF + configure-general-embedLinks-enableExternalEmbeds = Aktifkan media eksternal diff --git a/locales/id-ID/stream.ftl b/locales/id-ID/stream.ftl index 51fcdf0e55..65378eaa3a 100644 --- a/locales/id-ID/stream.ftl +++ b/locales/id-ID/stream.ftl @@ -378,8 +378,8 @@ comments-permalink-linkCopied = Tautan disalin comments-embedLinks-showEmbeds = Tampilkan sematan comments-embedLinks-hideEmbeds = Sembunyikan Sematan -comments-embedLinks-show-giphy = Tampilkan GIF -comments-embedLinks-hide-giphy = Sembunyikan GIF +comments-embedLinks-show-gif = Tampilkan GIF +comments-embedLinks-hide-gif = Sembunyikan GIF comments-embedLinks-show-youtube = Tampilkan video comments-embedLinks-hide-youtube = Sebunyikan video diff --git a/locales/it-IT/admin.ftl b/locales/it-IT/admin.ftl index 7a2aa6ccda..b1766ec7e5 100644 --- a/locales/it-IT/admin.ftl +++ b/locales/it-IT/admin.ftl @@ -386,10 +386,12 @@ configure-general-sitewideCommenting-messageExplanation = #### Embed Links configure-general-embedLinks-title = Embedded media -configure-general-embedLinks-desc = Consenti ai commentatori di aggiungere un video di YouTube, un Tweet o una GIF dalla libreria di GIPHY alla fine del loro commento +configure-general-embedLinks-desc = +configure-general-embedLinks-description = + Consenti ai commentatori di aggiungere un video di YouTube, un Tweet o una GIF alla fine del loro commento configure-general-embedLinks-enableTwitterEmbeds = Consenti Twitter embed configure-general-embedLinks-enableYouTubeEmbeds = Consenti YouTube embed -configure-general-embedLinks-enableGiphyEmbeds = Consenti GIFs da GIPHY +configure-general-embedLinks-enableGifs = Consenti GIFs configure-general-embedLinks-enableExternalEmbeds = Attiva external media configure-general-embedLinks-On = Sì diff --git a/locales/it-IT/stream.ftl b/locales/it-IT/stream.ftl index e8c8d05f02..11cec58241 100644 --- a/locales/it-IT/stream.ftl +++ b/locales/it-IT/stream.ftl @@ -378,8 +378,8 @@ comments-permalink-linkCopied = Link copiato comments-embedLinks-showEmbeds = Mostra gli embed comments-embedLinks-hideEmbeds = Nascondi gli embeds -comments-embedLinks-show-giphy = Mostra GIF -comments-embedLinks-hide-giphy = Nascondi GIF +comments-embedLinks-show-gif = Mostra GIF +comments-embedLinks-hide-gif = Nascondi GIF comments-embedLinks-show-youtube = Mostra video comments-embedLinks-hide-youtube = Nascondi video diff --git a/locales/ja-JP/stream.ftl b/locales/ja-JP/stream.ftl index ce1e9228ee..aa7c899ecf 100644 --- a/locales/ja-JP/stream.ftl +++ b/locales/ja-JP/stream.ftl @@ -368,8 +368,8 @@ comments-permalink-linkCopied = リンクをコピーしました comments-embedLinks-showEmbeds = 埋め込みを表示 comments-embedLinks-hideEmbeds = 埋め込みを非表示 -comments-embedLinks-show-giphy = GIFを表示 -comments-embedLinks-hide-giphy = GIFを非表示 +comments-embedLinks-show-gif = GIFを表示 +comments-embedLinks-hide-gif = GIFを非表示 comments-embedLinks-show-youtube = ビデオを表示 comments-embedLinks-hide-youtube = ビデオを非表示 diff --git a/locales/nb-NO/stream.ftl b/locales/nb-NO/stream.ftl index 0d6eb0d71d..a13f302bfa 100644 --- a/locales/nb-NO/stream.ftl +++ b/locales/nb-NO/stream.ftl @@ -399,8 +399,8 @@ comments-permalink-linkCopied = Lenke kopiert comments-embedLinks-showEmbeds = Vis eksternt innhold comments-embedLinks-hideEmbeds = Skjul eksternt innhold -comments-embedLinks-show-giphy = Vis GIF -comments-embedLinks-hide-giphy = Skjul GIF +comments-embedLinks-show-gif = Vis GIF +comments-embedLinks-hide-gif = Skjul GIF comments-embedLinks-show-youtube = Vis video comments-embedLinks-hide-youtube = Skjul video diff --git a/locales/nl-NL/admin.ftl b/locales/nl-NL/admin.ftl index dcc0cb2de6..ec99966320 100644 --- a/locales/nl-NL/admin.ftl +++ b/locales/nl-NL/admin.ftl @@ -420,10 +420,12 @@ configure-general-sitewideCommenting-messageExplanation = #### Embed Links configure-general-embedLinks-title = Embedded media -configure-general-embedLinks-desc = Sta reageerders toe om een YouTube-video, tweet of GIF uit GIPHYs bibliotheek aan het einde van hun reactie toe te voegen +configure-general-embedLinks-desc = +configure-general-embedLinks-description = + Sta reageerders toe om een YouTube-video, tweet of GIF aan het einde van hun reactie toe te voegen configure-general-embedLinks-enableTwitterEmbeds = Sta Twitter-embeds toe configure-general-embedLinks-enableYouTubeEmbeds = Sta YouTube-embeds toe -configure-general-embedLinks-enableGiphyEmbeds = Sta GIFs van GIPHY toe +configure-general-embedLinks-enableGifs = Sta GIFs toe configure-general-embedLinks-enableExternalEmbeds = Externe media toestaan configure-general-embedLinks-On = Ja diff --git a/locales/nl-NL/stream.ftl b/locales/nl-NL/stream.ftl index b058695246..aa45cd9cad 100644 --- a/locales/nl-NL/stream.ftl +++ b/locales/nl-NL/stream.ftl @@ -411,8 +411,8 @@ comments-permalink-linkCopied = Link kopiëren comments-embedLinks-showEmbeds = Laat ingesloten inhoud zien comments-embedLinks-hideEmbeds = Verberg ingesloten inhoud -comments-embedLinks-show-giphy = Toon GIF -comments-embedLinks-hide-giphy = Verberg GIF +comments-embedLinks-show-gif = Toon GIF +comments-embedLinks-hide-gif = Verberg GIF comments-embedLinks-show-youtube = Toon video comments-embedLinks-hide-youtube = Verberg video diff --git a/locales/pl/stream.ftl b/locales/pl/stream.ftl index 0aa312d1e7..fcd92bcae0 100644 --- a/locales/pl/stream.ftl +++ b/locales/pl/stream.ftl @@ -318,8 +318,8 @@ comments-stream-deleteAccount-callOut-cancelAccountDeletion = comments-embedLinks-showEmbeds = Pokaż embedy comments-embedLinks-hideEmbeds = Ukryj embedy -comments-embedLinks-show-giphy = Pokaż GIF -comments-embedLinks-hide-giphy = Ukryj GIF +comments-embedLinks-show-gif = Pokaż GIF +comments-embedLinks-hide-gif = Ukryj GIF comments-embedLinks-show-youtube = Pokaż film comments-embedLinks-hide-youtube = Ukryj film diff --git a/locales/pt-BR/admin.ftl b/locales/pt-BR/admin.ftl index ff5597878d..54ed14a275 100644 --- a/locales/pt-BR/admin.ftl +++ b/locales/pt-BR/admin.ftl @@ -431,10 +431,12 @@ configure-general-sitewideCommenting-messageExplanation = #### Embed Links configure-general-embedLinks-title = Mídia incorporada -configure-general-embedLinks-desc = Permitir que os comentaristas adicionem um vídeo do YouTube, tweet ou GIF da biblioteca do GIPHY ao final do comentário +configure-general-embedLinks-desc = +configure-general-embedLinks-description = + Permitir que os comentaristas adicionem um vídeo do YouTube, tweet ou GIF ao final do comentário configure-general-embedLinks-enableTwitterEmbeds = Permitir incorporações do Twitter configure-general-embedLinks-enableYouTubeEmbeds = Permitir incorporações do YouTube -configure-general-embedLinks-enableGiphyEmbeds = Permitir GIFs do GIPHY +configure-general-embedLinks-enableGifs = Permitir GIFs configure-general-embedLinks-enableExternalEmbeds = Habilitar mídia externa configure-general-embedLinks-On = Sim diff --git a/locales/pt-BR/stream.ftl b/locales/pt-BR/stream.ftl index f715996ebe..0bc6d85c94 100644 --- a/locales/pt-BR/stream.ftl +++ b/locales/pt-BR/stream.ftl @@ -460,8 +460,8 @@ comments-permalink-linkCopied = Link copiado comments-embedLinks-showEmbeds = Mostrar conteúdo embutido comments-embedLinks-hideEmbeds = Esconder conteúdo embutido -comments-embedLinks-show-giphy = Mostrar GIF -comments-embedLinks-hide-giphy = Esconder GIF +comments-embedLinks-show-gif = Mostrar GIF +comments-embedLinks-hide-gif = Esconder GIF comments-embedLinks-show-youtube = Mostrar vídeo comments-embedLinks-hide-youtube = ESconder vídeo diff --git a/locales/ru/stream.ftl b/locales/ru/stream.ftl index c461e82fa8..fbd1f0cfbb 100644 --- a/locales/ru/stream.ftl +++ b/locales/ru/stream.ftl @@ -324,8 +324,8 @@ comments-stream-deleteAccount-callOut-cancelAccountDeletion = comments-embedLinks-showEmbeds = Показать вставки comments-embedLinks-hideEmbeds = Скрыть вставки -comments-embedLinks-show-giphy = Показать GIF -comments-embedLinks-hide-giphy = Скрыть GIF +comments-embedLinks-show-gif = Показать GIF +comments-embedLinks-hide-gif = Скрыть GIF comments-embedLinks-show-youtube = Показать видел comments-embedLinks-hide-youtube = Скрыть видео diff --git a/locales/sk-SK/stream.ftl b/locales/sk-SK/stream.ftl index 5cad969e3f..473cf13bbc 100644 --- a/locales/sk-SK/stream.ftl +++ b/locales/sk-SK/stream.ftl @@ -408,8 +408,8 @@ comments-permalink-linkCopied = Odkaz skopírovaný comments-embedLinks-showEmbeds = Zobraziť vložené položky comments-embedLinks-hideEmbeds = Skryť vložené položky -comments-embedLinks-show-giphy = Zobraziť GIF -comments-embedLinks-hide-giphy = Skryť GIF +comments-embedLinks-show-gif = Zobraziť GIF +comments-embedLinks-hide-gif = Skryť GIF comments-embedLinks-show-youtube = Zobraziť video comments-embedLinks-hide-youtube = Skryť video diff --git a/locales/sv/admin.ftl b/locales/sv/admin.ftl index d34d45a74a..66b4dde1fc 100644 --- a/locales/sv/admin.ftl +++ b/locales/sv/admin.ftl @@ -429,10 +429,12 @@ configure-general-sitewideCommenting-messageExplanation = #### Bädda in länkar configure-general-embedLinks-title = Inbäddade medier -configure-general-embedLinks-desc = Tillåt kommentatorer att lägga till en YouTube-video, X inlägg eller GIF från GIPHYs bibliotek i slutet av deras kommentar +configure-general-embedLinks-desc = +configure-general-embedLinks-desc = + Tillåt kommentatorer att lägga till en YouTube-video, X inlägg eller GIF i slutet av deras kommentar configure-general-embedLinks-enableTwitterEmbeds = Tillåt inbäddning av X-inlägg configure-general-embedLinks-enableYouTubeEmbeds = Tillåt YouTube-inbäddningar -configure-general-embedLinks-enableGiphyEmbeds = Tillåt GIFs från GIPHY +configure-general-embedLinks-enableGifs = Tillåt GIFs configure-general-embedLinks-enableExternalEmbeds = Aktivera externa medier configure-general-embedLinks-On = Ja diff --git a/locales/sv/stream.ftl b/locales/sv/stream.ftl index e2f760d12d..27ead79752 100644 --- a/locales/sv/stream.ftl +++ b/locales/sv/stream.ftl @@ -463,8 +463,8 @@ comments-permalink-linkCopied = Länk kopierad comments-embedLinks-showEmbeds = Visa inbäddningar comments-embedLinks-hideEmbeds = Dölj inbäddningar -comments-embedLinks-show-giphy = Visa GIF -comments-embedLinks-hide-giphy = Dölj GIF +comments-embedLinks-show-gif = Visa GIF +comments-embedLinks-hide-gif = Dölj GIF comments-embedLinks-show-youtube = Visa video comments-embedLinks-hide-youtube = Dölj video diff --git a/locales/tr-TR/stream.ftl b/locales/tr-TR/stream.ftl index 4dfb8f8ad4..b766f7383d 100644 --- a/locales/tr-TR/stream.ftl +++ b/locales/tr-TR/stream.ftl @@ -348,8 +348,8 @@ comments-permalink-linkCopied = Link kopyalandı comments-embedLinks-showEmbeds = Embedleri göster comments-embedLinks-hideEmbeds = Embedleri gizle -comments-embedLinks-show-giphy = GIF’I göster -comments-embedLinks-hide-giphy = GIF’I gizle +comments-embedLinks-show-gif = GIF’I göster +comments-embedLinks-hide-gif = GIF’I gizle comments-embedLinks-show-youtube = Videoyu göster comments-embedLinks-hide-youtube = Videoyu gizle diff --git a/locales/zh-CN/admin.ftl b/locales/zh-CN/admin.ftl index 5af78edba3..fbb938a9d9 100644 --- a/locales/zh-CN/admin.ftl +++ b/locales/zh-CN/admin.ftl @@ -382,10 +382,11 @@ configure-general-sitewideCommenting-messageExplanation = ### Embed Links configure-general-embedLinks-title = 嵌入媒体 -configure-general-embedLinks-desc = 允许留言人在留言内容最后添加YouTube视频、推特或来自GIPHY相册的GIF。 +configure-general-embedLinks-desc = +configure-general-embedLinks-description = 允许留言人在留言内容最后添加YouTube视频、推特或来自GIF。 configure-general-embedLinks-enableTwitterEmbeds = 允许嵌入Twitter configure-general-embedLinks-enableYouTubeEmbeds = 允许嵌入YouTube -configure-general-embedLinks-enableGiphyEmbeds =允许嵌入来自 GIPHY的动图 +configure-general-embedLinks-enableGifs = 允许嵌入来的动图 configure-general-embedLinks-enableExternalEmbeds =启用外部媒体 configure-general-embedLinks-On = 开启 diff --git a/locales/zh-CN/stream.ftl b/locales/zh-CN/stream.ftl index 88a3392f7b..73a3773a12 100644 --- a/locales/zh-CN/stream.ftl +++ b/locales/zh-CN/stream.ftl @@ -370,8 +370,8 @@ comments-permalink-linkCopied = 链接已复制 comments-embedLinks-showEmbeds = 显示嵌入的内容 comments-embedLinks-hideEmbeds = 隐藏嵌入的内容 -comments-embedLinks-show-giphy = 显示动图 -comments-embedLinks-hide-giphy = 隐藏动图 +comments-embedLinks-show-gif = 显示动图 +comments-embedLinks-hide-gif = 隐藏动图 comments-embedLinks-show-youtube = 显示视频 comments-embedLinks-hide-youtube = 隐藏视频 diff --git a/server/package.json b/server/package.json index ce76c43679..46c902df9f 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "@coralproject/talk", - "version": "9.0.7", + "version": "9.2.0", "author": "The Coral Project", "homepage": "https://coralproject.net/", "sideEffects": [ diff --git a/server/src/core/server/app/handlers/api/comment/commentEmbed.ts b/server/src/core/server/app/handlers/api/comment/commentEmbed.ts index f2e87f2fce..2c6961405b 100644 --- a/server/src/core/server/app/handlers/api/comment/commentEmbed.ts +++ b/server/src/core/server/app/handlers/api/comment/commentEmbed.ts @@ -123,6 +123,7 @@ export const commentEmbedJSONPHandler = commentRevision, mediaUrl, giphyMedia, + tenorMedia, externalMediaUrl, } = await getCommentEmbedData(mongo, comment, tenant.id); @@ -157,6 +158,7 @@ export const commentEmbedJSONPHandler = customCSSURL: customCSSURLEmbed || customCSSURL, staticRoot: staticURI || tenantURL, giphyMedia, + tenorMedia, tenantURL, sanitized, replyMessage, diff --git a/server/src/core/server/app/handlers/api/oembedService.ts b/server/src/core/server/app/handlers/api/oembedService.ts index 6abf802747..41cc720e9f 100644 --- a/server/src/core/server/app/handlers/api/oembedService.ts +++ b/server/src/core/server/app/handlers/api/oembedService.ts @@ -107,6 +107,7 @@ export const oembedProviderHandler = ({ commentRevision, mediaUrl, giphyMedia, + tenorMedia, externalMediaUrl, simpleEmbedMediaUrl, } = await getCommentEmbedData(mongo, comment, tenant.id); @@ -158,6 +159,7 @@ export const oembedProviderHandler = ({ defaultFontsCSS, staticRoot: staticURI || tenantURL, giphyMedia, + tenorMedia, sanitized, replyMessage, goToConversationMessage, diff --git a/server/src/core/server/app/handlers/api/tenor/index.ts b/server/src/core/server/app/handlers/api/tenor/index.ts new file mode 100644 index 0000000000..8758377e0a --- /dev/null +++ b/server/src/core/server/app/handlers/api/tenor/index.ts @@ -0,0 +1,170 @@ +import Joi from "joi"; +import fetch from "node-fetch"; + +import { AppOptions } from "coral-server/app/"; +import { RequestHandler, TenantCoralRequest } from "coral-server/types/express"; + +const SEARCH_LIMIT = 32; +const TENOR_SEARCH_URL = "https://tenor.googleapis.com/v2/search"; + +const schema = Joi.object({ + query: Joi.string().required().not().empty(), + pos: Joi.string().optional(), +}); + +interface BodyPayload { + query: string; + pos?: string; +} + +interface MediaFormat { + url: string; + duration: number; + dims: number[]; + size: number; +} + +interface SearchResult { + id: string; + title: string; + created: number; + content_description: string; + itemurl: string; + url: string; + tags: string[]; + flags: []; + hasaudio: boolean; + media_formats: { + webm: MediaFormat; + mp4: MediaFormat; + nanowebm: MediaFormat; + loopedmp4: MediaFormat; + gifpreview: MediaFormat; + tinygifpreview: MediaFormat; + nanomp4: MediaFormat; + nanogifpreview: MediaFormat; + tinymp4: MediaFormat; + gif: MediaFormat; + webp: MediaFormat; + mediumgif: MediaFormat; + tinygif: MediaFormat; + nanogif: MediaFormat; + tinywebm: MediaFormat; + }; +} + +interface SearchPayload { + results: SearchResult[]; + next: string; +} + +export const convertGiphyContentRatingToTenorLevel = ( + rating: string | null | undefined +): string => { + if (!rating) { + return "high"; + } + + const lowerRating = rating.toLowerCase(); + if (lowerRating === "g") { + return "high"; + } + if (lowerRating === "pg") { + return "medium"; + } + if (lowerRating === "pg-13") { + return "low"; + } + if (lowerRating === "r") { + return "off"; + } + + return "high"; +}; + +export const tenorSearchHandler = + ({ mongo }: AppOptions): RequestHandler => + async (req, res, next) => { + const { tenant } = req.coral; + if (!tenant) { + res.status(404).send("tenant not found"); + return; + } + + if (!req.user) { + res.status(403).send("user is required"); + return; + } + + const result = schema.validate(req.query); + if (result.error || result.errors) { + res.status(400).send(result.errors); + return; + } + + const params = result.value as BodyPayload; + if (!params) { + res.sendStatus(400); + return; + } + + const gifsEnabled = tenant.media?.gifs.enabled ?? false; + if (!gifsEnabled) { + res.status(200).send({ + results: [], + }); + return; + } + + const apiKey = tenant.media?.gifs.key ?? null; + if (!apiKey || apiKey.length === 0) { + res.status(200).send({ + results: [], + }); + return; + } + + const contentFilter = convertGiphyContentRatingToTenorLevel( + tenant.media?.gifs.maxRating + ); + + const url = new URL(TENOR_SEARCH_URL); + url.searchParams.set("q", params.query); + url.searchParams.set("key", apiKey); + url.searchParams.set("limit", `${SEARCH_LIMIT}`); + url.searchParams.set("contentfilter", contentFilter); + + if (params.pos) { + url.searchParams.set("pos", params.pos); + } + + const response = await fetch(url.toString(), { + method: "GET", + }); + + if (!response.ok) { + res.status(500).send({ + results: [], + }); + return; + } + + const json = (await response.json()) as SearchPayload; + if (!json) { + res.status(500).send({ + results: [], + }); + } + + res.status(200).send({ + results: json.results.map((r) => { + return { + id: r.id, + title: r.title, + url: r.media_formats.gif.url, + preview: r.media_formats.nanogif.url, + }; + }), + next: json.next, + }); + }; diff --git a/server/src/core/server/app/helpers/commentEmbedHelpers.ts b/server/src/core/server/app/helpers/commentEmbedHelpers.ts index c46d1fef53..9f139da74f 100644 --- a/server/src/core/server/app/helpers/commentEmbedHelpers.ts +++ b/server/src/core/server/app/helpers/commentEmbedHelpers.ts @@ -62,6 +62,7 @@ export async function getCommentEmbedData( let mediaUrl = null; let giphyMedia = null; + let tenorMedia = null; let externalMediaUrl = null; let simpleEmbedMediaUrl = null; if ( @@ -81,6 +82,10 @@ export async function getCommentEmbedData( } simpleEmbedMediaUrl = giphyMedia.url; } + if (commentRevision.media?.type === "tenor") { + tenorMedia = commentRevision.media; + simpleEmbedMediaUrl = tenorMedia.url; + } return { comment, @@ -88,6 +93,7 @@ export async function getCommentEmbedData( commentRevision, mediaUrl, giphyMedia, + tenorMedia, externalMediaUrl, simpleEmbedMediaUrl, }; diff --git a/server/src/core/server/app/router/api/index.ts b/server/src/core/server/app/router/api/index.ts index ededd2e276..9211a4fde5 100644 --- a/server/src/core/server/app/router/api/index.ts +++ b/server/src/core/server/app/router/api/index.ts @@ -36,6 +36,7 @@ import { createDSAReportRouter } from "./dsaReport"; import { createNewInstallRouter } from "./install"; import { createRemoteMediaRouter } from "./remoteMedia"; import { createStoryRouter } from "./story"; +import { createTenorRouter } from "./tenor"; import { createNewUserRouter } from "./user"; export interface RouterOptions { @@ -119,6 +120,13 @@ export function createAPIRouter(app: AppOptions, options: RouterOptions) { externalMediaHandler(app) ); + router.use( + "/tenor", + corsWhitelisted(app.mongo), + authenticate(options.passport), + createTenorRouter(app) + ); + // General API error handler. router.use(notFoundMiddleware); router.use(JSONErrorHandler(app)); diff --git a/server/src/core/server/app/router/api/tenor.ts b/server/src/core/server/app/router/api/tenor.ts new file mode 100644 index 0000000000..451b5c2f53 --- /dev/null +++ b/server/src/core/server/app/router/api/tenor.ts @@ -0,0 +1,15 @@ +import { AppOptions } from "coral-server/app"; +import { tenorSearchHandler } from "coral-server/app/handlers/api/tenor"; +import { userLimiterMiddleware } from "coral-server/app/middleware"; + +import { createAPIRouter } from "./helpers"; + +export function createTenorRouter(app: AppOptions) { + const router = createAPIRouter({}); + + router.use(userLimiterMiddleware(app)); + + router.get("/search", tenorSearchHandler(app)); + + return router; +} diff --git a/server/src/core/server/app/views/commentEmbed/singleCommentEmbed.html b/server/src/core/server/app/views/commentEmbed/singleCommentEmbed.html index 04055012a7..74883096f8 100644 --- a/server/src/core/server/app/views/commentEmbed/singleCommentEmbed.html +++ b/server/src/core/server/app/views/commentEmbed/singleCommentEmbed.html @@ -222,6 +222,19 @@ /> {% endif %} {% endif %} + {% if tenorMedia %} + + + + {% endif %}
diff --git a/server/src/core/server/graph/resolvers/CommentMedia.ts b/server/src/core/server/graph/resolvers/CommentMedia.ts index 5a0d5f44e7..7ea2d346f5 100644 --- a/server/src/core/server/graph/resolvers/CommentMedia.ts +++ b/server/src/core/server/graph/resolvers/CommentMedia.ts @@ -8,6 +8,8 @@ const resolveType: GQLCommentMediaTypeResolver = ( switch (embed.type) { case "giphy": return "GiphyMedia"; + case "tenor": + return "TenorMedia"; case "youtube": return "YouTubeMedia"; case "twitter": diff --git a/server/src/core/server/graph/resolvers/GifMediaConfiguration.ts b/server/src/core/server/graph/resolvers/GifMediaConfiguration.ts new file mode 100644 index 0000000000..e96cb09ab5 --- /dev/null +++ b/server/src/core/server/graph/resolvers/GifMediaConfiguration.ts @@ -0,0 +1,13 @@ +import { + GQLGIF_MEDIA_SOURCE, + GQLGifMediaConfiguration, + GQLGifMediaConfigurationTypeResolver, +} from "coral-server/graph/schema/__generated__/types"; + +export const GifMediaConfiguration: GQLGifMediaConfigurationTypeResolver< + Partial +> = { + enabled: ({ enabled = false }) => enabled, + maxRating: ({ maxRating = "g" }) => maxRating, + provider: ({ provider = GQLGIF_MEDIA_SOURCE.GIPHY }) => provider, +}; diff --git a/server/src/core/server/graph/resolvers/GiphyMediaConfiguration.ts b/server/src/core/server/graph/resolvers/GiphyMediaConfiguration.ts deleted file mode 100644 index 7a2bc92176..0000000000 --- a/server/src/core/server/graph/resolvers/GiphyMediaConfiguration.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { - GQLGiphyMediaConfiguration, - GQLGiphyMediaConfigurationTypeResolver, -} from "coral-server/graph/schema/__generated__/types"; - -export const GiphyMediaConfiguration: GQLGiphyMediaConfigurationTypeResolver< - Partial -> = { - enabled: ({ enabled = false }) => enabled, - maxRating: ({ maxRating = "g" }) => maxRating, -}; diff --git a/server/src/core/server/graph/resolvers/MediaConfiguration.ts b/server/src/core/server/graph/resolvers/MediaConfiguration.ts index 678871b1bc..162fc66bfd 100644 --- a/server/src/core/server/graph/resolvers/MediaConfiguration.ts +++ b/server/src/core/server/graph/resolvers/MediaConfiguration.ts @@ -8,6 +8,6 @@ export const MediaConfiguration: GQLMediaConfigurationTypeResolver< > = { twitter: ({ twitter = {} }) => twitter, youtube: ({ youtube = {} }) => youtube, - giphy: ({ giphy = {} }) => giphy, + gifs: ({ gifs = {} }) => gifs, external: () => ({}), }; diff --git a/server/src/core/server/graph/resolvers/index.ts b/server/src/core/server/graph/resolvers/index.ts index 9e927b4f4a..2f409e177d 100644 --- a/server/src/core/server/graph/resolvers/index.ts +++ b/server/src/core/server/graph/resolvers/index.ts @@ -35,7 +35,7 @@ import { ExternalModerationPhase } from "./ExternalModerationPhase"; import { FacebookAuthIntegration } from "./FacebookAuthIntegration"; import { FeatureCommentPayload } from "./FeatureCommentPayload"; import { Flag } from "./Flag"; -import { GiphyMediaConfiguration } from "./GiphyMediaConfiguration"; +import { GifMediaConfiguration } from "./GifMediaConfiguration"; import { GoogleAuthIntegration } from "./GoogleAuthIntegration"; import { Invite } from "./Invite"; import { LiveConfiguration } from "./LiveConfiguration"; @@ -121,7 +121,7 @@ const Resolvers: GQLResolver = { FacebookAuthIntegration, FeatureCommentPayload, Flag, - GiphyMediaConfiguration, + GifMediaConfiguration, GoogleAuthIntegration, Invite, LiveConfiguration, diff --git a/server/src/core/server/graph/schema/schema.graphql b/server/src/core/server/graph/schema/schema.graphql index 388fedb03b..acb3431be2 100644 --- a/server/src/core/server/graph/schema/schema.graphql +++ b/server/src/core/server/graph/schema/schema.graphql @@ -1513,19 +1513,29 @@ type StoryMessageBox { ## Embed Links ################################################################################ -type GiphyMediaConfiguration { +enum GIF_MEDIA_SOURCE { + GIPHY + TENOR +} + +type GifMediaConfiguration { """ enabled is true when gif search via giphy and giphy media objects are enabled. """ enabled: Boolean! + """ + provider is the source that gif's are pulled from + """ + provider: GIF_MEDIA_SOURCE + """ maximum allowed rating for gifs, g, pg, pg-13, r. """ maxRating: String """ - key is the API key for Giphy. + key is the API key for the gif provider. """ key: String } @@ -1562,9 +1572,9 @@ type MediaConfiguration { youtube: YouTubeMediaConfiguration! """ - giphy is the configuration for Giphy support. + gifs is the configuration for GIF support. """ - giphy: GiphyMediaConfiguration! + gifs: GifMediaConfiguration! """ external is the configuration for external images. @@ -3990,6 +4000,21 @@ type CommentRevisionMetadata { externalModeration: [CommentRevisionExternalModerationPhaseMetadata!] } +""" +TenorMedia is a particular GIF that is provided by the Tenor platform. +""" +type TenorMedia { + """ + url is the URL to a image of the GIF. + """ + url: String! + + """ + title is the title of the GIF. + """ + title: String +} + """ GiphyMedia is a particular GIF that is provided by the Giphy platform. """ @@ -4100,7 +4125,7 @@ type ExternalMedia { """ CommentMedia is the various media types that can be attached to a Comment. """ -union CommentMedia = GiphyMedia | TwitterMedia | YouTubeMedia | ExternalMedia +union CommentMedia = GiphyMedia | TwitterMedia | YouTubeMedia | ExternalMedia | TenorMedia type CommentRevision { """ @@ -6383,12 +6408,17 @@ input YouTubeMediaConfigurationInput { enabled: Boolean } -input GiphyMediaConfigurationInput { +input GifMediaConfigurationInput { """ enabled is true when gif search via giphy and giphy media objects are enabled. """ enabled: Boolean + """ + provider is the source for which gifs are pulled from + """ + provider: GIF_MEDIA_SOURCE + """ maximum allowed rating for gifs, g, pg, pg-13, r """ @@ -6412,9 +6442,9 @@ input MediaConfigurationInput { youtube: YouTubeMediaConfigurationInput """ - giphy is the configuration for Giphy support. + gifs is the configuration for gif support. """ - giphy: GiphyMediaConfigurationInput + gifs: GifMediaConfigurationInput } """ diff --git a/server/src/core/server/locales/de-CH/errors.ftl b/server/src/core/server/locales/de-CH/errors.ftl index bcf7aee584..6723bc67bb 100644 --- a/server/src/core/server/locales/de-CH/errors.ftl +++ b/server/src/core/server/locales/de-CH/errors.ftl @@ -44,7 +44,7 @@ error-storyNotFound = Artikel ({$storyID}) nicht gefunden. error-commentNotFound = Kommentar ({$commentID}) nicht gefunden. error-commentRevisionNotFound = Kommentar ({ $commentID }) der Revision ({ $commentRevisionID }) nicht gefunden. error-invalidCredentials = eMail und/oder Passwort ist nicht korrekt. -error-toxicComment = Bis Du sicher? Die Ausdrucksweise in diesem Kommentar könnte unsere Community-Richtlinien verletzen. Du kannst den Kommentar editieren oder ihn einem Moderator zur Freigabe senden. +error-toxicComment = Bist Du sicher? Die Ausdrucksweise in diesem Kommentar könnte unsere Community-Richtlinien verletzen. Du kannst den Kommentar editieren oder ihn einem Moderator zur Freigabe senden. error-spamComment = Dieser Kommentar könnte Spam sein. Du kannst den Kommentar editieren oder ihn einem Moderator zur Freigabe senden. error-userAlreadySuspended = Der Benutzer ist aktuell bis zum {$until} suspendiert. error-userAlreadyBanned = Der Benutzer ist bereits verbannt. diff --git a/server/src/core/server/models/comment/revision.ts b/server/src/core/server/models/comment/revision.ts index d043077ce3..a8755a9616 100644 --- a/server/src/core/server/models/comment/revision.ts +++ b/server/src/core/server/models/comment/revision.ts @@ -95,6 +95,12 @@ export interface GiphyMedia { title?: string; } +export interface TenorMedia { + type: "tenor"; + id: string; + url: string; +} + export interface TwitterMedia { type: "twitter"; url: string; @@ -119,6 +125,7 @@ export interface ExternalMedia { export type CommentMedia = | GiphyMedia + | TenorMedia | TwitterMedia | YouTubeMedia | ExternalMedia; diff --git a/server/src/core/server/models/tenant/helpers.ts b/server/src/core/server/models/tenant/helpers.ts index 629491b83c..19d25e87d6 100644 --- a/server/src/core/server/models/tenant/helpers.ts +++ b/server/src/core/server/models/tenant/helpers.ts @@ -4,7 +4,10 @@ import { Config } from "coral-server/config"; import { InternalError } from "coral-server/errors"; import { translate } from "coral-server/services/i18n"; -import { GQLFEATURE_FLAG } from "coral-server/graph/schema/__generated__/types"; +import { + GQLFEATURE_FLAG, + GQLGIF_MEDIA_SOURCE, +} from "coral-server/graph/schema/__generated__/types"; import { AuthIntegrations } from "../settings"; import { LEGACY_FEATURE_FLAGS, Tenant } from "./tenant"; @@ -105,7 +108,7 @@ export function getWebhookEndpoint( export function supportsMediaType( tenant: Pick, - type: "twitter" | "youtube" | "giphy" | "external" + type: "twitter" | "youtube" | "giphy" | "tenor" | "external" ): tenant is Omit & Required> { switch (type) { case "external": @@ -115,7 +118,17 @@ export function supportsMediaType( case "youtube": return !!tenant.media?.youtube.enabled; case "giphy": - return !!tenant.media?.giphy.enabled && !!tenant.media.giphy.key; + return ( + !!tenant.media?.gifs.enabled && + !!tenant.media.gifs.key && + tenant.media.gifs.provider === GQLGIF_MEDIA_SOURCE.GIPHY + ); + case "tenor": + return ( + !!tenant.media?.gifs.enabled && + !!tenant.media.gifs.key && + tenant.media.gifs.provider === GQLGIF_MEDIA_SOURCE.TENOR + ); } } diff --git a/server/src/core/server/services/comments/media.ts b/server/src/core/server/services/comments/media.ts index 0f624b5981..de9e50fa44 100644 --- a/server/src/core/server/services/comments/media.ts +++ b/server/src/core/server/services/comments/media.ts @@ -4,6 +4,7 @@ import { WrappedInternalError } from "coral-server/errors"; import { ExternalMedia, GiphyMedia, + TenorMedia, TwitterMedia, YouTubeMedia, } from "coral-server/models/comment"; @@ -52,6 +53,23 @@ async function attachGiphyMedia( } } +async function attachTenorMedia( + tenant: Tenant, + id: string, + url: string +): Promise { + try { + // Return the formed Tenor Media. + return { + type: "tenor", + id, + url, + }; + } catch (err) { + throw new WrappedInternalError(err as Error, "cannot attach Tenor Media"); + } +} + async function attachExternalMedia( url: string, inputWidth?: string, @@ -137,7 +155,7 @@ async function attachOEmbedMedia( } export interface CreateCommentMediaInput { - type: "giphy" | "twitter" | "youtube" | "external"; + type: "giphy" | "tenor" | "twitter" | "youtube" | "external"; url: string; id?: string; width?: string; @@ -162,6 +180,14 @@ export async function attachMedia( } return attachGiphyMedia(tenant, input.id, input.url); + case "tenor": + if (!input.id) { + throw new Error( + "id is required when attaching a TenorMedia object to a comment" + ); + } + + return attachTenorMedia(tenant, input.id, input.url); case "external": return attachExternalMedia(input.url, input.width, input.height); case "twitter": diff --git a/server/src/core/server/services/comments/pipeline/phases/commentLength.ts b/server/src/core/server/services/comments/pipeline/phases/commentLength.ts index d252cc8052..ee21b1759d 100644 --- a/server/src/core/server/services/comments/pipeline/phases/commentLength.ts +++ b/server/src/core/server/services/comments/pipeline/phases/commentLength.ts @@ -38,6 +38,8 @@ export const commentLength: IntermediateModerationPhase = ({ if ( // Check if a Giphy attachment is attached... media?.type === "giphy" || + // Check if a Tenor attachment is attached... + media?.type === "tenor" || // Or a external image is attached... media?.type === "external" || // Or a rating is attached... diff --git a/server/src/core/server/services/comments/pipeline/phases/index.ts b/server/src/core/server/services/comments/pipeline/phases/index.ts index 8e8419114c..f1202f9ff0 100644 --- a/server/src/core/server/services/comments/pipeline/phases/index.ts +++ b/server/src/core/server/services/comments/pipeline/phases/index.ts @@ -62,11 +62,11 @@ export const moderationPhases: IntermediateModerationPhase[] = [ spam, detectLinks, - // Apply any pre-existing conditions to these comments. + // Run any external moderation phase that missed other filters. + external, + + // Apply any pre-moderation conditions to these comments. statusPreModerateNewCommenter, statusPreModerate, statusPreModerateUser, - - // Run any external moderation phase that missed other filters. - external, ]; diff --git a/server/src/core/server/services/comments/pipeline/phases/repeatPost.ts b/server/src/core/server/services/comments/pipeline/phases/repeatPost.ts index 3dfeb1ba87..3cb15e1a33 100644 --- a/server/src/core/server/services/comments/pipeline/phases/repeatPost.ts +++ b/server/src/core/server/services/comments/pipeline/phases/repeatPost.ts @@ -63,8 +63,11 @@ export const repeatPost: IntermediateModerationPhase = async ({ if (lastCommentBodyText !== bodyText) { // Body text is not the same, can't be a repeat post! similarity = false; - } else if (supportsMediaType(tenant, "giphy")) { - // Giphy is enabled. If the medias are the same, then this is a repeat + } else if ( + supportsMediaType(tenant, "giphy") || + supportsMediaType(tenant, "tenor") + ) { + // gifs are enabled. If the medias are the same, then this is a repeat // comment otherwise they are not. if ( (!lastCommentRevision.media && !media) || @@ -74,14 +77,15 @@ export const repeatPost: IntermediateModerationPhase = async ({ // comment had media but the current one does not similarity = true; } else if ( - // Check to see if the last comment revision has a Giphy Media + // Check to see if the last comment revision has a Giphy/Tenor Media // object. lastCommentRevision.media && - lastCommentRevision.media.type === "giphy" && - // Check to see if the current comment revision has a Giphy Media + (lastCommentRevision.media.type === "giphy" || + lastCommentRevision.media.type === "tenor") && + // Check to see if the current comment revision has a Giphy/Tenor Media // object. media && - media.type === "giphy" && + (media.type === "giphy" || media.type === "tenor") && // Check to see if the media id's are the same. lastCommentRevision.media.id === media.id ) { @@ -92,7 +96,7 @@ export const repeatPost: IntermediateModerationPhase = async ({ similarity = false; } } else { - // Body text was the same and Giphy support was not enabled. + // Body text was the same and Giphy/Tenor support was not enabled. similarity = true; } diff --git a/server/src/core/server/services/giphy/giphy.ts b/server/src/core/server/services/giphy/giphy.ts index daea517168..151da90ff2 100644 --- a/server/src/core/server/services/giphy/giphy.ts +++ b/server/src/core/server/services/giphy/giphy.ts @@ -59,7 +59,7 @@ const GiphyRetrieveResponseSchema = Joi.object().keys({ export function ratingIsAllowed(rating: string, tenant: Tenant) { const compareRating = rating.toLowerCase(); - const maxRating = tenant.media?.giphy.maxRating || "g"; + const maxRating = tenant.media?.gifs.maxRating || "g"; const compareIndex = RATINGS_ORDER.indexOf(compareRating); const maxIndex = RATINGS_ORDER.indexOf(maxRating); @@ -98,17 +98,17 @@ export async function searchGiphy( offset: string, tenant: Tenant ): Promise { - if (!supportsMediaType(tenant, "giphy") || !tenant.media.giphy.key) { + if (!supportsMediaType(tenant, "giphy") || !tenant.media.gifs.key) { throw new InternalError("Giphy was not enabled"); } const language = convertLanguage(tenant.locale); const url = new URL(GIPHY_SEARCH); - url.searchParams.set("api_key", tenant.media.giphy.key); + url.searchParams.set("api_key", tenant.media.gifs.key); url.searchParams.set("limit", "10"); url.searchParams.set("lang", language); url.searchParams.set("offset", offset); - url.searchParams.set("rating", tenant.media.giphy.maxRating!); + url.searchParams.set("rating", tenant.media.gifs.maxRating!); url.searchParams.set("q", query); try { @@ -125,7 +125,7 @@ export async function searchGiphy( } catch (err) { // Ensure that the API key doesn't get leaked to the logs by accident. if (err.message) { - err.message = err.message.replace(tenant.media.giphy.key, "[Sensitive]"); + err.message = err.message.replace(tenant.media.gifs.key, "[Sensitive]"); } // Rethrow the error. @@ -137,12 +137,12 @@ export async function retrieveFromGiphy( tenant: Tenant, id: string ): Promise { - if (!supportsMediaType(tenant, "giphy") || !tenant.media.giphy.key) { + if (!supportsMediaType(tenant, "giphy") || !tenant.media.gifs.key) { throw new InternalError("Giphy was not enabled"); } const url = new URL(`${GIPHY_FETCH}/${id}`); - url.searchParams.set("api_key", tenant.media.giphy.key); + url.searchParams.set("api_key", tenant.media.gifs.key); try { const res = await fetch(url.toString()); @@ -158,7 +158,7 @@ export async function retrieveFromGiphy( } catch (err) { // Ensure that the API key doesn't get leaked to the logs by accident. if (err.message) { - err.message = err.message.replace(tenant.media.giphy.key, "[Sensitive]"); + err.message = err.message.replace(tenant.media.gifs.key, "[Sensitive]"); } // Rethrow the error. diff --git a/server/src/core/server/stacks/editComment.ts b/server/src/core/server/stacks/editComment.ts index 5dcd350aa1..6063875375 100644 --- a/server/src/core/server/stacks/editComment.ts +++ b/server/src/core/server/stacks/editComment.ts @@ -25,6 +25,7 @@ import { getLatestRevision, GiphyMedia, retrieveComment, + TenorMedia, TwitterMedia, validateEditable, YouTubeMedia, @@ -161,6 +162,7 @@ export default async function edit( let media: | GiphyMedia + | TenorMedia | TwitterMedia | YouTubeMedia | ExternalMedia diff --git a/utilities/externalModPhase/src/main.ts b/utilities/externalModPhase/src/main.ts index 1fc2395b1d..66b3aab90f 100644 --- a/utilities/externalModPhase/src/main.ts +++ b/utilities/externalModPhase/src/main.ts @@ -52,6 +52,14 @@ const run = async () => { res.send(result); }); + app.post("/api/none", (req, res) => { + console.log(req.body); + + const result = {}; + + res.send(result); + }); + app.listen(PORT, HOST, () => { console.log(`external mod phase tester is listening on "${HOST}:${PORT}"...`); });