From 4c966e5d6d1cbafe7a41d58268ffcb2cee31abe8 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Sat, 13 Apr 2024 05:13:53 +0100 Subject: [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 --- src/components/dialogs/Embed.tsx | 191 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 src/components/dialogs/Embed.tsx (limited to 'src/components/dialogs/Embed.tsx') diff --git a/src/components/dialogs/Embed.tsx b/src/components/dialogs/Embed.tsx new file mode 100644 index 000000000..0478dbae8 --- /dev/null +++ b/src/components/dialogs/Embed.tsx @@ -0,0 +1,191 @@ +import React, {memo, useRef, useState} from 'react' +import {TextInput, View} from 'react-native' +import {AppBskyActorDefs, AppBskyFeedPost, AtUri} from '@atproto/api' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {EMBED_SCRIPT} from '#/lib/constants' +import {niceDate} from '#/lib/strings/time' +import {toShareUrl} from '#/lib/strings/url-helpers' +import {atoms as a, useTheme} from '#/alf' +import * as Dialog from '#/components/Dialog' +import * as TextField from '#/components/forms/TextField' +import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' +import {CodeBrackets_Stroke2_Corner0_Rounded as CodeBrackets} from '#/components/icons/CodeBrackets' +import {Text} from '#/components/Typography' +import {Button, ButtonIcon, ButtonText} from '../Button' + +type EmbedDialogProps = { + control: Dialog.DialogControlProps + postAuthor: AppBskyActorDefs.ProfileViewBasic + postCid: string + postUri: string + record: AppBskyFeedPost.Record + timestamp: string +} + +let EmbedDialog = ({control, ...rest}: EmbedDialogProps): React.ReactNode => { + return ( + + + + + ) +} +EmbedDialog = memo(EmbedDialog) +export {EmbedDialog} + +function EmbedDialogInner({ + postAuthor, + postCid, + postUri, + record, + timestamp, +}: Omit) { + const t = useTheme() + const {_} = useLingui() + const ref = useRef(null) + const [copied, setCopied] = useState(false) + + // reset copied state after 2 seconds + React.useEffect(() => { + if (copied) { + const timeout = setTimeout(() => { + setCopied(false) + }, 2000) + return () => clearTimeout(timeout) + } + }, [copied]) + + const snippet = React.useMemo(() => { + const lang = record.langs && record.langs.length > 0 ? record.langs[0] : '' + const profileHref = toShareUrl(['/profile', postAuthor.did].join('/')) + const urip = new AtUri(postUri) + const href = toShareUrl( + ['/profile', postAuthor.did, 'post', urip.rkey].join('/'), + ) + + // 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 INTERPOLATIONS BELOW WITHOUT ESCAPING THEM! + // Also, keep this code synced with the bskyembed code in landing.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 +} -- cgit 1.4.1