Bluesky app fork with some witchin' additions 💫 forked from jollywhoppers.com/witchsky.app
Something went wrong. Try again.
8.7 kB · 263 lines
TSX
at commit 5efae600
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264import {memo, useEffect, useMemo, useState} from 'react'import {View} from 'react-native'import {type AppBskyActorDefs, type 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 {Button, ButtonIcon, ButtonText} from '#/components/Button'import * as Dialog from '#/components/Dialog'import * as TextField from '#/components/forms/TextField'import * as ToggleButton from '#/components/forms/ToggleButton'import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'import { ChevronBottom_Stroke2_Corner0_Rounded as ChevronBottomIcon, ChevronRight_Stroke2_Corner0_Rounded as ChevronRightIcon,} from '#/components/icons/Chevron'import {CodeBrackets_Stroke2_Corner0_Rounded as CodeBracketsIcon} from '#/components/icons/CodeBrackets'import {Text} from '#/components/Typography'
export type ColorModeValues = 'system' | 'light' | 'dark'
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 {_, i18n} = useLingui() const [copied, setCopied] = useState(false) const [showCustomisation, setShowCustomisation] = useState(false) const [colorMode, setColorMode] = useState<ColorModeValues>('system')
// reset copied state after 2 seconds useEffect(() => { if (copied) { const timeout = setTimeout(() => { setCopied(false) }, 2000) return () => clearTimeout(timeout) } }, [copied])
const snippet = useMemo(() => { function toEmbedUrl(href: string) { return toShareUrl(href) + '?ref_src=embed' }
const lang = record.langs && record.langs.length > 0 ? record.langs[0] : '' const profileHref = toEmbedUrl(['/profile', postAuthor.did].join('/')) const urip = new AtUri(postUri) const href = toEmbedUrl( ['/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, )}" data-bluesky-embed-color-mode="${escapeHtml( colorMode, )}"><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(i18n, timestamp), )}</a></blockquote><script async src="${EMBED_SCRIPT}" charset="utf-8"></script>` }, [i18n, postUri, postCid, record, timestamp, postAuthor, colorMode])
return ( <Dialog.Inner label={_(msg`Embed post`)} style={[{maxWidth: 500}]}> <View style={[a.gap_lg]}> <View style={[a.gap_sm]}> <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.border, t.atoms.border_contrast_low, a.rounded_sm, a.overflow_hidden, ]}> <Button label={ showCustomisation ? _(msg`Hide customization options`) : _(msg`Show customization options`) } color="secondary" variant="ghost" size="small" shape="default" onPress={() => setShowCustomisation(c => !c)} style={[ a.justify_start, showCustomisation && t.atoms.bg_contrast_25, ]}> <ButtonIcon icon={showCustomisation ? ChevronBottomIcon : ChevronRightIcon} /> <ButtonText> <Trans>Customization options</Trans> </ButtonText> </Button>
{showCustomisation && ( <View style={[a.gap_sm, a.p_md]}> <Text style={[t.atoms.text_contrast_medium, a.font_semi_bold]}> <Trans>Color theme</Trans> </Text> <ToggleButton.Group label={_(msg`Color mode`)} values={[colorMode]} onChange={([value]) => setColorMode(value as ColorModeValues)}> <ToggleButton.Button name="system" label={_(msg`System`)}> <ToggleButton.ButtonText> <Trans>System</Trans> </ToggleButton.ButtonText> </ToggleButton.Button> <ToggleButton.Button name="light" label={_(msg`Light`)}> <ToggleButton.ButtonText> <Trans>Light</Trans> </ToggleButton.ButtonText> </ToggleButton.Button> <ToggleButton.Button name="dark" label={_(msg`Dark`)}> <ToggleButton.ButtonText> <Trans>Dark</Trans> </ToggleButton.ButtonText> </ToggleButton.Button> </ToggleButton.Group> </View> )} </View> <View style={[a.flex_row, a.gap_sm]}> <View style={[a.flex_1]}> <TextField.Root> <TextField.Icon icon={CodeBracketsIcon} /> <TextField.Input label={_(msg`Embed HTML code`)} editable={false} selection={{start: 0, end: snippet.length}} value={snippet} /> </TextField.Root> </View> <Button label={_(msg`Copy code`)} color="primary" variant="solid" size="large" onPress={() => { navigator.clipboard.writeText(snippet) setCopied(true) }}> {copied ? ( <> <ButtonIcon icon={CheckIcon} /> <ButtonText> <Trans>Copied!</Trans> </ButtonText> </> ) : ( <ButtonText> <Trans>Copy code</Trans> </ButtonText> )} </Button> </View> </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}