-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[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 <[email protected]>
- Loading branch information
Showing
7 changed files
with
233 additions
and
4 deletions.
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<Dialog.Outer control={control}> | ||
<Dialog.Handle /> | ||
<EmbedDialogInner {...rest} /> | ||
</Dialog.Outer> | ||
) | ||
} | ||
EmbedDialog = memo(EmbedDialog) | ||
export {EmbedDialog} | ||
|
||
function EmbedDialogInner({ | ||
postAuthor, | ||
postCid, | ||
postUri, | ||
record, | ||
timestamp, | ||
}: Omit<EmbedDialogProps, 'control'>) { | ||
const t = useTheme() | ||
const {_} = useLingui() | ||
const ref = useRef<TextInput>(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 `<blockquote class="bluesky-embed" data-bluesky-uri="${escapeHtml( | ||
postUri, | ||
)}" data-bluesky-cid="${escapeHtml(postCid)}"><p lang="${escapeHtml( | ||
lang, | ||
)}">${escapeHtml(record.text)}${ | ||
record.embed | ||
? `<br><br><a href="${escapeHtml(href)}">[image or embed]</a>` | ||
: '' | ||
}</p>— ${escapeHtml( | ||
postAuthor.displayName || postAuthor.handle, | ||
)} (<a href="${escapeHtml(profileHref)}">@${escapeHtml( | ||
postAuthor.handle, | ||
)}</a>) <a href="${escapeHtml(href)}">${escapeHtml( | ||
niceDate(timestamp), | ||
)}</a></blockquote><script async src="${EMBED_SCRIPT}" charset="utf-8"></script>` | ||
}, [postUri, postCid, record, timestamp, postAuthor]) | ||
|
||
return ( | ||
<Dialog.Inner label="Embed post" style={[a.gap_md, {maxWidth: 500}]}> | ||
<View style={[a.gap_sm, a.pb_lg]}> | ||
<Text style={[a.text_2xl, a.font_bold]}> | ||
<Trans>Embed post</Trans> | ||
</Text> | ||
<Text | ||
style={[a.text_md, t.atoms.text_contrast_medium, a.leading_normal]}> | ||
<Trans> | ||
Embed this post in your website. Simply copy the following snippet | ||
and paste it into the HTML code of your website. | ||
</Trans> | ||
</Text> | ||
</View> | ||
|
||
<View style={[a.flex_row, a.gap_sm]}> | ||
<TextField.Root> | ||
<TextField.Icon icon={CodeBrackets} /> | ||
<TextField.Input | ||
label={_(msg`Embed HTML code`)} | ||
editable={false} | ||
selection={{start: 0, end: snippet.length}} | ||
value={snippet} | ||
style={{}} | ||
/> | ||
</TextField.Root> | ||
<Button | ||
label={_(msg`Copy code`)} | ||
color="primary" | ||
variant="solid" | ||
size="medium" | ||
onPress={() => { | ||
ref.current?.focus() | ||
ref.current?.setSelection(0, snippet.length) | ||
navigator.clipboard.writeText(snippet) | ||
setCopied(true) | ||
}}> | ||
{copied ? ( | ||
<> | ||
<ButtonIcon icon={Check} /> | ||
<ButtonText> | ||
<Trans>Copied!</Trans> | ||
</ButtonText> | ||
</> | ||
) : ( | ||
<ButtonText> | ||
<Trans>Copy code</Trans> | ||
</ButtonText> | ||
)} | ||
</Button> | ||
</View> | ||
<Dialog.Close /> | ||
</Dialog.Inner> | ||
) | ||
} | ||
|
||
/** | ||
* 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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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', | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters