From 65e9696c6af6c64aebb303f2e96f751dbb1d7821 Mon Sep 17 00:00:00 2001 From: Kathryn Beaty Date: Mon, 30 Sep 2024 09:56:50 -0400 Subject: [PATCH 1/4] initial update to pause/play gifs in mod queue --- .../ConversationModalCommentContainer.tsx | 1 + .../components/MediaContainer/GiphyMedia.tsx | 39 ++++++++++++- .../admin/components/MediaContainer/Media.css | 5 ++ .../MediaContainer/MediaContainer.tsx | 17 +++++- .../server/app/handlers/api/tenor/index.ts | 1 + .../core/server/graph/schema/schema.graphql | 27 ++++++++- .../core/server/models/comment/revision.ts | 5 ++ .../core/server/services/comments/media.ts | 11 ++++ .../src/core/server/services/tenor/index.ts | 1 + .../src/core/server/services/tenor/tenor.ts | 57 +++++++++++++++++++ 10 files changed, 160 insertions(+), 4 deletions(-) create mode 100644 server/src/core/server/services/tenor/index.ts create mode 100644 server/src/core/server/services/tenor/tenor.ts diff --git a/client/src/core/client/admin/components/ConversationModal/ConversationModalCommentContainer.tsx b/client/src/core/client/admin/components/ConversationModal/ConversationModalCommentContainer.tsx index 2fd4919620..74d8a294a7 100644 --- a/client/src/core/client/admin/components/ConversationModal/ConversationModalCommentContainer.tsx +++ b/client/src/core/client/admin/components/ConversationModal/ConversationModalCommentContainer.tsx @@ -128,6 +128,7 @@ const ConversationModalCommentContainer: FunctionComponent = ({ disabled: false, }; }, [comment.status]); + return ( diff --git a/client/src/core/client/admin/components/MediaContainer/GiphyMedia.tsx b/client/src/core/client/admin/components/MediaContainer/GiphyMedia.tsx index 33bfe4e006..83373ddfcb 100644 --- a/client/src/core/client/admin/components/MediaContainer/GiphyMedia.tsx +++ b/client/src/core/client/admin/components/MediaContainer/GiphyMedia.tsx @@ -2,7 +2,7 @@ import { Localized } from "@fluent/react/compat"; import React, { FunctionComponent, useCallback, useState } from "react"; import { ButtonPlayIcon, SvgIcon } from "coral-ui/components/icons"; -import { BaseButton, Flex } from "coral-ui/components/v2"; +import { BaseButton, Button, Flex } from "coral-ui/components/v2"; import styles from "./Media.css"; @@ -12,6 +12,7 @@ interface Props { width: number | null; height: number | null; video: string | null; + url: string | null; } const GiphyMedia: FunctionComponent = ({ @@ -20,11 +21,47 @@ const GiphyMedia: FunctionComponent = ({ width, height, video, + url, }) => { const [showAnimated, setShowAnimated] = useState(false); const toggleImage = useCallback(() => { setShowAnimated(!showAnimated); }, [showAnimated]); + + if (!still && !video) { + // Fallback to show/hide gif if there is no still and no video + return ( + <> + + {showAnimated && ( +
+ {title +
+ )} + + ); + } + return (
{!showAnimated && still && ( diff --git a/client/src/core/client/admin/components/MediaContainer/Media.css b/client/src/core/client/admin/components/MediaContainer/Media.css index 2bb032ddc3..3c509abc55 100644 --- a/client/src/core/client/admin/components/MediaContainer/Media.css +++ b/client/src/core/client/admin/components/MediaContainer/Media.css @@ -32,3 +32,8 @@ display: block; max-width: 100%; } + +.showHideButton { + margin-top: var(--spacing-2); + margin-bottom: var(--spacing-2); +} diff --git a/client/src/core/client/admin/components/MediaContainer/MediaContainer.tsx b/client/src/core/client/admin/components/MediaContainer/MediaContainer.tsx index d01ecdb607..7bc1172822 100644 --- a/client/src/core/client/admin/components/MediaContainer/MediaContainer.tsx +++ b/client/src/core/client/admin/components/MediaContainer/MediaContainer.tsx @@ -7,7 +7,6 @@ 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"; @@ -30,6 +29,7 @@ const MediaContainer: FunctionComponent = ({ comment }) => { title={media.title} width={media.width} height={media.height} + url={media.url} /> ); case "ExternalMedia": @@ -59,7 +59,16 @@ const MediaContainer: FunctionComponent = ({ comment }) => { /> ); case "TenorMedia": - return ; + return ( + + ); case "%other": return null; } @@ -86,6 +95,10 @@ const enhanced = withFragmentContainer({ ... on TenorMedia { url title + width + height + tenorStill: still + tenorVideo: video } ... on TwitterMedia { url diff --git a/server/src/core/server/app/handlers/api/tenor/index.ts b/server/src/core/server/app/handlers/api/tenor/index.ts index 8758377e0a..9b2ecdb7cd 100644 --- a/server/src/core/server/app/handlers/api/tenor/index.ts +++ b/server/src/core/server/app/handlers/api/tenor/index.ts @@ -133,6 +133,7 @@ export const tenorSearchHandler = url.searchParams.set("key", apiKey); url.searchParams.set("limit", `${SEARCH_LIMIT}`); url.searchParams.set("contentfilter", contentFilter); + url.searchParams.set("media_filter", "gif,nanogif"); if (params.pos) { url.searchParams.set("pos", params.pos); diff --git a/server/src/core/server/graph/schema/schema.graphql b/server/src/core/server/graph/schema/schema.graphql index acb3431be2..0a22e21e36 100644 --- a/server/src/core/server/graph/schema/schema.graphql +++ b/server/src/core/server/graph/schema/schema.graphql @@ -4013,6 +4013,26 @@ type TenorMedia { title is the title of the GIF. """ title: String + + """ + still is a thumbnail preview of the GIF. + """ + still: String + + """ + video is a URL to the mp4 video of the GIF. + """ + video: String + + """ + width is the width of the GIF in pixels. + """ + width: Int + + """ + height is the height of the GIF in pixels. + """ + height: Int } """ @@ -4125,7 +4145,12 @@ type ExternalMedia { """ CommentMedia is the various media types that can be attached to a Comment. """ -union CommentMedia = GiphyMedia | TwitterMedia | YouTubeMedia | ExternalMedia | TenorMedia +union CommentMedia = + GiphyMedia + | TwitterMedia + | YouTubeMedia + | ExternalMedia + | TenorMedia type CommentRevision { """ diff --git a/server/src/core/server/models/comment/revision.ts b/server/src/core/server/models/comment/revision.ts index a8755a9616..d6151cc920 100644 --- a/server/src/core/server/models/comment/revision.ts +++ b/server/src/core/server/models/comment/revision.ts @@ -99,6 +99,11 @@ export interface TenorMedia { type: "tenor"; id: string; url: string; + title?: string; + still?: string; + width?: number; + height?: number; + video?: string; } export interface TwitterMedia { diff --git a/server/src/core/server/services/comments/media.ts b/server/src/core/server/services/comments/media.ts index de9e50fa44..1d7983517d 100644 --- a/server/src/core/server/services/comments/media.ts +++ b/server/src/core/server/services/comments/media.ts @@ -14,6 +14,7 @@ import { retrieveFromGiphy, } from "coral-server/services/giphy"; import { fetchOEmbedResponse } from "coral-server/services/oembed"; +import { retrieveFromTenor } from "coral-server/services/tenor"; async function attachGiphyMedia( tenant: Tenant, @@ -58,12 +59,22 @@ async function attachTenorMedia( id: string, url: string ): Promise { + const { results } = await retrieveFromTenor(tenant, id); + if (!results || !(results.length === 1)) { + return; + } + const data = results[0]; try { // Return the formed Tenor Media. return { type: "tenor", id, url, + title: data.title, + still: data.media_formats.gifpreview.url, + width: data.media_formats.gifpreview.dims[0], + height: data.media_formats.gifpreview.dims[1], + video: data.media_formats.mp4.url, }; } catch (err) { throw new WrappedInternalError(err as Error, "cannot attach Tenor Media"); diff --git a/server/src/core/server/services/tenor/index.ts b/server/src/core/server/services/tenor/index.ts new file mode 100644 index 0000000000..60c6d7c0d4 --- /dev/null +++ b/server/src/core/server/services/tenor/index.ts @@ -0,0 +1 @@ +export * from "./tenor"; diff --git a/server/src/core/server/services/tenor/tenor.ts b/server/src/core/server/services/tenor/tenor.ts new file mode 100644 index 0000000000..76632a658a --- /dev/null +++ b/server/src/core/server/services/tenor/tenor.ts @@ -0,0 +1,57 @@ +import { InternalError } from "coral-server/errors"; +import { validateSchema } from "coral-server/helpers"; +import { Tenant, supportsMediaType } from "coral-server/models/tenant"; +import { createFetch } from "coral-server/services/fetch"; +import Joi from "joi"; + +const TENOR_FETCH_URL = "https://tenor.googleapis.com/v2/posts"; +const fetch = createFetch({ name: "tenor" }); + +const TenorResponseMediaObjectSchema = Joi.object().keys({ + gifpreview: { url: Joi.string().required(), dims: Joi.array().required() }, + mp4: { url: Joi.string().required() }, +}); + +const TenorResponseObjectsSchema = Joi.array().items({ + id: Joi.string().required(), + title: Joi.string().allow(""), + media_formats: TenorResponseMediaObjectSchema, +}); + +const TenorRetrieveResponseSchema = Joi.object().keys({ + results: TenorResponseObjectsSchema, +}); + +export async function retrieveFromTenor( + tenant: Tenant, + id: string +): Promise { + if (!supportsMediaType(tenant, "tenor") || !tenant.media.gifs.key) { + throw new InternalError("Tenor was not enabled"); + } + + const url = new URL(`${TENOR_FETCH_URL}`); + url.searchParams.set("key", tenant.media.gifs.key); + url.searchParams.set("ids", id); + url.searchParams.set("media_filter", "gifpreview,mp4"); + + try { + const res = await fetch(url.toString()); + if (!res.ok) { + throw new InternalError("response from Tenor was not ok", { + status: res.status, + }); + } + + const data = await res.json(); + return validateSchema(TenorRetrieveResponseSchema, data); + } 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.gifs.key, "[Sensitive]"); + } + + // Rethrow the error. + throw err; + } +} From 400c4c0c690370c720a0d3076e4d23e4128e069b Mon Sep 17 00:00:00 2001 From: Kathryn Beaty Date: Mon, 30 Sep 2024 10:09:21 -0400 Subject: [PATCH 2/4] rename gifmedia comp; remove tenormedia comp; update commentrevisioncontainer --- .../{GiphyMedia.tsx => GifMedia.tsx} | 4 +-- .../MediaContainer/MediaContainer.tsx | 6 ++-- .../components/MediaContainer/TenorMedia.tsx | 32 ------------------- .../admin/components/MediaContainer/index.ts | 2 +- .../ModerateCard/CommentRevisionContainer.tsx | 19 ++++++++--- 5 files changed, 21 insertions(+), 42 deletions(-) rename client/src/core/client/admin/components/MediaContainer/{GiphyMedia.tsx => GifMedia.tsx} (97%) delete mode 100644 client/src/core/client/admin/components/MediaContainer/TenorMedia.tsx diff --git a/client/src/core/client/admin/components/MediaContainer/GiphyMedia.tsx b/client/src/core/client/admin/components/MediaContainer/GifMedia.tsx similarity index 97% rename from client/src/core/client/admin/components/MediaContainer/GiphyMedia.tsx rename to client/src/core/client/admin/components/MediaContainer/GifMedia.tsx index 83373ddfcb..164f3c9f10 100644 --- a/client/src/core/client/admin/components/MediaContainer/GiphyMedia.tsx +++ b/client/src/core/client/admin/components/MediaContainer/GifMedia.tsx @@ -15,7 +15,7 @@ interface Props { url: string | null; } -const GiphyMedia: FunctionComponent = ({ +const GifMedia: FunctionComponent = ({ still, title, width, @@ -111,4 +111,4 @@ const GiphyMedia: FunctionComponent = ({ ); }; -export default GiphyMedia; +export default GifMedia; diff --git a/client/src/core/client/admin/components/MediaContainer/MediaContainer.tsx b/client/src/core/client/admin/components/MediaContainer/MediaContainer.tsx index 7bc1172822..24a8c10bf0 100644 --- a/client/src/core/client/admin/components/MediaContainer/MediaContainer.tsx +++ b/client/src/core/client/admin/components/MediaContainer/MediaContainer.tsx @@ -6,7 +6,7 @@ import { withFragmentContainer } from "coral-framework/lib/relay"; import { MediaContainer_comment } from "coral-admin/__generated__/MediaContainer_comment.graphql"; import ExternalMedia from "./ExternalMedia"; -import GiphyMedia from "./GiphyMedia"; +import GifMedia from "./GifMedia"; import TwitterMedia from "./TwitterMedia"; import YouTubeMedia from "./YouTubeMedia"; @@ -23,7 +23,7 @@ const MediaContainer: FunctionComponent = ({ comment }) => { switch (media.__typename) { case "GiphyMedia": return ( - = ({ comment }) => { ); case "TenorMedia": return ( - = ({ 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/MediaContainer/index.ts b/client/src/core/client/admin/components/MediaContainer/index.ts index ac8cb33873..8ff24d19c8 100644 --- a/client/src/core/client/admin/components/MediaContainer/index.ts +++ b/client/src/core/client/admin/components/MediaContainer/index.ts @@ -1,4 +1,4 @@ export { default as MediaContainer } from "./MediaContainer"; -export { default as GiphyMedia } from "./GiphyMedia"; +export { default as GifMedia } from "./GifMedia"; export { default as TwitterMedia } from "./TwitterMedia"; export { default as YouTubeMedia } from "./YouTubeMedia"; diff --git a/client/src/core/client/admin/components/ModerateCard/CommentRevisionContainer.tsx b/client/src/core/client/admin/components/ModerateCard/CommentRevisionContainer.tsx index cc8dbc6a47..6646fc058c 100644 --- a/client/src/core/client/admin/components/ModerateCard/CommentRevisionContainer.tsx +++ b/client/src/core/client/admin/components/ModerateCard/CommentRevisionContainer.tsx @@ -4,14 +4,13 @@ import { graphql } from "react-relay"; import { withFragmentContainer } from "coral-framework/lib/relay"; import { HorizontalGutter, Timestamp } from "coral-ui/components/v2"; import ExternalMedia from "../MediaContainer/ExternalMedia"; -import GiphyMedia from "../MediaContainer/GiphyMedia"; +import GifMedia from "../MediaContainer/GifMedia"; import TwitterMedia from "../MediaContainer/TwitterMedia"; 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; @@ -33,12 +32,13 @@ const CommentRevisionContainer: FunctionComponent = ({ comment }) => { {c.createdAt} {c.body ? c.body : ""} {c.media && c.media.__typename === "GiphyMedia" && ( - )} {c.media && c.media.__typename === "ExternalMedia" && ( @@ -65,7 +65,14 @@ const CommentRevisionContainer: FunctionComponent = ({ comment }) => { /> )} {c.media && c.media.__typename === "TenorMedia" && ( - + )}
))} @@ -100,6 +107,10 @@ const enhanced = withFragmentContainer({ ... on TenorMedia { url title + width + height + tenorStill: still + tenorVideo: video } ... on TwitterMedia { url From 325fa3bff490c892d5c3c935582924b68232e542 Mon Sep 17 00:00:00 2001 From: Kathryn Beaty Date: Mon, 30 Sep 2024 10:23:29 -0400 Subject: [PATCH 3/4] fix linting --- .../ConversationModal/ConversationModalCommentContainer.tsx | 1 - server/src/core/server/services/tenor/tenor.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/client/src/core/client/admin/components/ConversationModal/ConversationModalCommentContainer.tsx b/client/src/core/client/admin/components/ConversationModal/ConversationModalCommentContainer.tsx index 74d8a294a7..2fd4919620 100644 --- a/client/src/core/client/admin/components/ConversationModal/ConversationModalCommentContainer.tsx +++ b/client/src/core/client/admin/components/ConversationModal/ConversationModalCommentContainer.tsx @@ -128,7 +128,6 @@ const ConversationModalCommentContainer: FunctionComponent = ({ disabled: false, }; }, [comment.status]); - return ( diff --git a/server/src/core/server/services/tenor/tenor.ts b/server/src/core/server/services/tenor/tenor.ts index 76632a658a..e5e17527e4 100644 --- a/server/src/core/server/services/tenor/tenor.ts +++ b/server/src/core/server/services/tenor/tenor.ts @@ -1,6 +1,6 @@ import { InternalError } from "coral-server/errors"; import { validateSchema } from "coral-server/helpers"; -import { Tenant, supportsMediaType } from "coral-server/models/tenant"; +import { supportsMediaType, Tenant } from "coral-server/models/tenant"; import { createFetch } from "coral-server/services/fetch"; import Joi from "joi"; From a648c023324f3d7590f43caf6639581d9b27538f Mon Sep 17 00:00:00 2001 From: Kathryn Beaty Date: Mon, 30 Sep 2024 11:41:01 -0400 Subject: [PATCH 4/4] move tenor api types into common; use for tenor fetch payload --- common/lib/types/tenor.ts | 44 +++++++++++++++++++ .../server/app/handlers/api/tenor/index.ts | 42 +----------------- .../src/core/server/services/tenor/tenor.ts | 6 ++- 3 files changed, 49 insertions(+), 43 deletions(-) create mode 100644 common/lib/types/tenor.ts diff --git a/common/lib/types/tenor.ts b/common/lib/types/tenor.ts new file mode 100644 index 0000000000..d36e73465d --- /dev/null +++ b/common/lib/types/tenor.ts @@ -0,0 +1,44 @@ +export interface MediaFormat { + url: string; + duration: number; + dims: number[]; + size: number; +} + +export 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; + }; +} + +export interface SearchPayload { + results: SearchResult[]; + next: string; +} + +export interface FetchPayload { + results: SearchResult[]; +} diff --git a/server/src/core/server/app/handlers/api/tenor/index.ts b/server/src/core/server/app/handlers/api/tenor/index.ts index 9b2ecdb7cd..6f63a7a755 100644 --- a/server/src/core/server/app/handlers/api/tenor/index.ts +++ b/server/src/core/server/app/handlers/api/tenor/index.ts @@ -1,6 +1,7 @@ import Joi from "joi"; import fetch from "node-fetch"; +import { SearchPayload } from "coral-common/common/lib/types/tenor"; import { AppOptions } from "coral-server/app/"; import { RequestHandler, TenantCoralRequest } from "coral-server/types/express"; @@ -17,47 +18,6 @@ interface BodyPayload { 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 => { diff --git a/server/src/core/server/services/tenor/tenor.ts b/server/src/core/server/services/tenor/tenor.ts index e5e17527e4..f7e4f3cea1 100644 --- a/server/src/core/server/services/tenor/tenor.ts +++ b/server/src/core/server/services/tenor/tenor.ts @@ -1,8 +1,10 @@ +import Joi from "joi"; + +import { FetchPayload } from "coral-common/common/lib/types/tenor"; import { InternalError } from "coral-server/errors"; import { validateSchema } from "coral-server/helpers"; import { supportsMediaType, Tenant } from "coral-server/models/tenant"; import { createFetch } from "coral-server/services/fetch"; -import Joi from "joi"; const TENOR_FETCH_URL = "https://tenor.googleapis.com/v2/posts"; const fetch = createFetch({ name: "tenor" }); @@ -25,7 +27,7 @@ const TenorRetrieveResponseSchema = Joi.object().keys({ export async function retrieveFromTenor( tenant: Tenant, id: string -): Promise { +): Promise { if (!supportsMediaType(tenant, "tenor") || !tenant.media.gifs.key) { throw new InternalError("Tenor was not enabled"); }