From 4c966e5d6d1cbafe7a41d58268ffcb2cee31abe8 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Sat, 13 Apr 2024 05:13:53 +0100 Subject: [PATCH] [Embeds] "Embed post" post dropdown option (#3513) * add embed option to post dropdown menu * put embed post button behind a gate * increase line height in dialog * add gate to gate name union * hide embed button if PWI optout * Ungate embed button * Escape HTML, align implementations * Make dialog conditionally rendered * Memoize EmbedDialog * Render dialog lazily --------- Co-authored-by: Dan Abramov --- .../codeBrackets_stroke2_corner0_rounded.svg | 1 + bskyembed/src/screens/landing.tsx | 6 +- src/components/dialogs/Embed.tsx | 191 ++++++++++++++++++ src/components/icons/CodeBrackets.tsx | 5 + src/lib/constants.ts | 2 + src/view/com/util/forms/PostDropdownBtn.tsx | 31 ++- src/view/com/util/post-ctrls/PostCtrls.tsx | 1 + 7 files changed, 233 insertions(+), 4 deletions(-) create mode 100644 assets/icons/codeBrackets_stroke2_corner0_rounded.svg create mode 100644 src/components/dialogs/Embed.tsx create mode 100644 src/components/icons/CodeBrackets.tsx diff --git a/assets/icons/codeBrackets_stroke2_corner0_rounded.svg b/assets/icons/codeBrackets_stroke2_corner0_rounded.svg new file mode 100644 index 00000000..0cc23921 --- /dev/null +++ b/assets/icons/codeBrackets_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/bskyembed/src/screens/landing.tsx b/bskyembed/src/screens/landing.tsx index 88e84ffb..7c8ef281 100644 --- a/bskyembed/src/screens/landing.tsx +++ b/bskyembed/src/screens/landing.tsx @@ -159,6 +159,7 @@ function Snippet({thread}: {thread: AppBskyFeedDefs.ThreadViewPost}) { return '' } + const lang = record.langs && record.langs.length > 0 ? record.langs[0] : '' const profileHref = toShareUrl( ['/profile', thread.post.author.did].join('/'), ) @@ -167,10 +168,9 @@ function Snippet({thread}: {thread: AppBskyFeedDefs.ThreadViewPost}) { ['/profile', thread.post.author.did, 'post', urip.rkey].join('/'), ) - const lang = record.langs ? record.langs[0] : '' - // x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x - // DO NOT ADD ANY NEW INTERPOLATIOONS BELOW WITHOUT ESCAPING THEM! + // DO NOT ADD ANY NEW INTERPOLATIONS BELOW WITHOUT ESCAPING THEM! + // Also, keep this code synced with the app code in Embed.tsx. // x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x return `

${escapeHtml(record.text)}${ + record.embed + ? `

[image or embed]` + : '' + }

— ${escapeHtml( + postAuthor.displayName || postAuthor.handle, + )} (@${escapeHtml( + postAuthor.handle, + )}) ${escapeHtml( + niceDate(timestamp), + )}
` + }, [postUri, postCid, record, timestamp, postAuthor]) + + return ( + + + + Embed post + + + + Embed this post in your website. Simply copy the following snippet + and paste it into the HTML code of your website. + + + + + + + + + + + + + + ) +} + +/** + * Based on a snippet of code from React, which itself was based on the escape-html library. + * Copyright (c) Meta Platforms, Inc. and affiliates + * Copyright (c) 2012-2013 TJ Holowaychuk + * Copyright (c) 2015 Andreas Lubbe + * Copyright (c) 2015 Tiancheng "Timothy" Gu + * Licensed as MIT. + */ +const matchHtmlRegExp = /["'&<>]/ +function escapeHtml(string: string) { + const str = String(string) + const match = matchHtmlRegExp.exec(str) + if (!match) { + return str + } + let escape + let html = '' + let index + let lastIndex = 0 + for (index = match.index; index < str.length; index++) { + switch (str.charCodeAt(index)) { + case 34: // " + escape = '"' + break + case 38: // & + escape = '&' + break + case 39: // ' + escape = ''' + break + case 60: // < + escape = '<' + break + case 62: // > + escape = '>' + break + default: + continue + } + if (lastIndex !== index) { + html += str.slice(lastIndex, index) + } + lastIndex = index + 1 + html += escape + } + return lastIndex !== index ? html + str.slice(lastIndex, index) : html +} diff --git a/src/components/icons/CodeBrackets.tsx b/src/components/icons/CodeBrackets.tsx new file mode 100644 index 00000000..59d5fca9 --- /dev/null +++ b/src/components/icons/CodeBrackets.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const CodeBrackets_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M14.242 3.03a1 1 0 0 1 .728 1.213l-4 16a1 1 0 1 1-1.94-.485l4-16a1 1 0 0 1 1.213-.728ZM6.707 7.293a1 1 0 0 1 0 1.414L3.414 12l3.293 3.293a1 1 0 1 1-1.414 1.414l-4-4a1 1 0 0 1 0-1.414l4-4a1 1 0 0 1 1.414 0Zm10.586 0a1 1 0 0 1 1.414 0l4 4a1 1 0 0 1 0 1.414l-4 4a1 1 0 1 1-1.414-1.414L20.586 12l-3.293-3.293a1 1 0 0 1 0-1.414Z', +}) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 401c3936..bb49387c 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -7,6 +7,8 @@ export const BSKY_SERVICE = 'https://bsky.social' export const DEFAULT_SERVICE = BSKY_SERVICE const HELP_DESK_LANG = 'en-us' export const HELP_DESK_URL = `https://blueskyweb.zendesk.com/hc/${HELP_DESK_LANG}` +export const EMBED_SERVICE = 'https://embed.bsky.app' +export const EMBED_SCRIPT = `${EMBED_SERVICE}/static/embed.js` const BASE_FEEDBACK_FORM_URL = `${HELP_DESK_URL}/requests/new` export function FEEDBACK_FORM_URL({ diff --git a/src/view/com/util/forms/PostDropdownBtn.tsx b/src/view/com/util/forms/PostDropdownBtn.tsx index 04dfa203..31032396 100644 --- a/src/view/com/util/forms/PostDropdownBtn.tsx +++ b/src/view/com/util/forms/PostDropdownBtn.tsx @@ -28,12 +28,14 @@ import {getCurrentRoute} from 'lib/routes/helpers' import {shareUrl} from 'lib/sharing' import {toShareUrl} from 'lib/strings/url-helpers' import {useTheme} from 'lib/ThemeContext' -import {atoms as a, useTheme as useAlf} from '#/alf' +import {atoms as a, useBreakpoints, useTheme as useAlf} from '#/alf' import {useDialogControl} from '#/components/Dialog' import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' +import {EmbedDialog} from '#/components/dialogs/Embed' import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox' import {BubbleQuestion_Stroke2_Corner0_Rounded as Translate} from '#/components/icons/Bubble' import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/icons/Clipboard' +import {CodeBrackets_Stroke2_Corner0_Rounded as CodeBrackets} from '#/components/icons/CodeBrackets' import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash' import {Filter_Stroke2_Corner0_Rounded as Filter} from '#/components/icons/Filter' import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute' @@ -55,6 +57,7 @@ let PostDropdownBtn = ({ richText, style, hitSlop, + timestamp, }: { testID: string postAuthor: AppBskyActorDefs.ProfileViewBasic @@ -64,10 +67,12 @@ let PostDropdownBtn = ({ richText: RichTextAPI style?: StyleProp hitSlop?: PressableProps['hitSlop'] + timestamp: string }): React.ReactNode => { const {hasSession, currentAccount} = useSession() const theme = useTheme() const alf = useAlf() + const {gtMobile} = useBreakpoints() const {_} = useLingui() const defaultCtrlColor = theme.palette.default.postCtrl const langPrefs = useLanguagePrefs() @@ -83,6 +88,7 @@ let PostDropdownBtn = ({ const deletePromptControl = useDialogControl() const hidePromptControl = useDialogControl() const loggedOutWarningPromptControl = useDialogControl() + const embedPostControl = useDialogControl() const rootUri = record.reply?.root?.uri || postUri const isThreadMuted = mutedThreads.includes(rootUri) @@ -177,6 +183,8 @@ let PostDropdownBtn = ({ shareUrl(url) }, [href]) + const canEmbed = isWeb && gtMobile && !shouldShowLoggedOutWarning + return ( @@ -238,6 +246,16 @@ let PostDropdownBtn = ({ + + {canEmbed && ( + + {_(msg`Embed post`)} + + + )} {hasSession && ( @@ -350,6 +368,17 @@ let PostDropdownBtn = ({ onConfirm={onSharePost} confirmButtonCta={_(msg`Share anyway`)} /> + + {canEmbed && ( + + )} ) } diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx index cd4a3637..cb50ee6d 100644 --- a/src/view/com/util/post-ctrls/PostCtrls.tsx +++ b/src/view/com/util/post-ctrls/PostCtrls.tsx @@ -264,6 +264,7 @@ let PostCtrls = ({ richText={richText} style={styles.btnPad} hitSlop={big ? HITSLOP_20 : HITSLOP_10} + timestamp={post.indexedAt} />