mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
at samuel/exp-cli 263 lines 8.7 kB view raw
1import {memo, useEffect, useMemo, useState} from 'react' 2import {View} from 'react-native' 3import {AppBskyActorDefs, AppBskyFeedPost, AtUri} from '@atproto/api' 4import {msg, Trans} from '@lingui/macro' 5import {useLingui} from '@lingui/react' 6 7import {EMBED_SCRIPT} from '#/lib/constants' 8import {niceDate} from '#/lib/strings/time' 9import {toShareUrl} from '#/lib/strings/url-helpers' 10import {atoms as a, useTheme} from '#/alf' 11import {Button, ButtonIcon, ButtonText} from '#/components/Button' 12import * as Dialog from '#/components/Dialog' 13import * as TextField from '#/components/forms/TextField' 14import * as ToggleButton from '#/components/forms/ToggleButton' 15import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check' 16import { 17 ChevronBottom_Stroke2_Corner0_Rounded as ChevronBottomIcon, 18 ChevronRight_Stroke2_Corner0_Rounded as ChevronRightIcon, 19} from '#/components/icons/Chevron' 20import {CodeBrackets_Stroke2_Corner0_Rounded as CodeBracketsIcon} from '#/components/icons/CodeBrackets' 21import {Text} from '#/components/Typography' 22 23export type ColorModeValues = 'system' | 'light' | 'dark' 24 25type EmbedDialogProps = { 26 control: Dialog.DialogControlProps 27 postAuthor: AppBskyActorDefs.ProfileViewBasic 28 postCid: string 29 postUri: string 30 record: AppBskyFeedPost.Record 31 timestamp: string 32} 33 34let EmbedDialog = ({control, ...rest}: EmbedDialogProps): React.ReactNode => { 35 return ( 36 <Dialog.Outer control={control}> 37 <Dialog.Handle /> 38 <EmbedDialogInner {...rest} /> 39 </Dialog.Outer> 40 ) 41} 42EmbedDialog = memo(EmbedDialog) 43export {EmbedDialog} 44 45function EmbedDialogInner({ 46 postAuthor, 47 postCid, 48 postUri, 49 record, 50 timestamp, 51}: Omit<EmbedDialogProps, 'control'>) { 52 const t = useTheme() 53 const {_, i18n} = useLingui() 54 const [copied, setCopied] = useState(false) 55 const [showCustomisation, setShowCustomisation] = useState(false) 56 const [colorMode, setColorMode] = useState<ColorModeValues>('system') 57 58 // reset copied state after 2 seconds 59 useEffect(() => { 60 if (copied) { 61 const timeout = setTimeout(() => { 62 setCopied(false) 63 }, 2000) 64 return () => clearTimeout(timeout) 65 } 66 }, [copied]) 67 68 const snippet = useMemo(() => { 69 function toEmbedUrl(href: string) { 70 return toShareUrl(href) + '?ref_src=embed' 71 } 72 73 const lang = record.langs && record.langs.length > 0 ? record.langs[0] : '' 74 const profileHref = toEmbedUrl(['/profile', postAuthor.did].join('/')) 75 const urip = new AtUri(postUri) 76 const href = toEmbedUrl( 77 ['/profile', postAuthor.did, 'post', urip.rkey].join('/'), 78 ) 79 80 // 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 81 // DO NOT ADD ANY NEW INTERPOLATIONS BELOW WITHOUT ESCAPING THEM! 82 // Also, keep this code synced with the bskyembed code in landing.tsx. 83 // 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 84 return `<blockquote class="bluesky-embed" data-bluesky-uri="${escapeHtml( 85 postUri, 86 )}" data-bluesky-cid="${escapeHtml( 87 postCid, 88 )}" data-bluesky-embed-color-mode="${escapeHtml( 89 colorMode, 90 )}"><p lang="${escapeHtml(lang)}">${escapeHtml(record.text)}${ 91 record.embed 92 ? `<br><br><a href="${escapeHtml(href)}">[image or embed]</a>` 93 : '' 94 }</p>&mdash; ${escapeHtml( 95 postAuthor.displayName || postAuthor.handle, 96 )} (<a href="${escapeHtml(profileHref)}">@${escapeHtml( 97 postAuthor.handle, 98 )}</a>) <a href="${escapeHtml(href)}">${escapeHtml( 99 niceDate(i18n, timestamp), 100 )}</a></blockquote><script async src="${EMBED_SCRIPT}" charset="utf-8"></script>` 101 }, [i18n, postUri, postCid, record, timestamp, postAuthor, colorMode]) 102 103 return ( 104 <Dialog.Inner label={_(msg`Embed post`)} style={[{maxWidth: 500}]}> 105 <View style={[a.gap_lg]}> 106 <View style={[a.gap_sm]}> 107 <Text style={[a.text_2xl, a.font_heavy]}> 108 <Trans>Embed post</Trans> 109 </Text> 110 <Text 111 style={[a.text_md, t.atoms.text_contrast_medium, a.leading_normal]}> 112 <Trans> 113 Embed this post in your website. Simply copy the following snippet 114 and paste it into the HTML code of your website. 115 </Trans> 116 </Text> 117 </View> 118 <View 119 style={[ 120 a.border, 121 t.atoms.border_contrast_low, 122 a.rounded_sm, 123 a.overflow_hidden, 124 ]}> 125 <Button 126 label={ 127 showCustomisation 128 ? _(msg`Hide customization options`) 129 : _(msg`Show customization options`) 130 } 131 color="secondary" 132 variant="ghost" 133 size="small" 134 shape="default" 135 onPress={() => setShowCustomisation(c => !c)} 136 style={[ 137 a.justify_start, 138 showCustomisation && t.atoms.bg_contrast_25, 139 ]}> 140 <ButtonIcon 141 icon={showCustomisation ? ChevronBottomIcon : ChevronRightIcon} 142 /> 143 <ButtonText> 144 <Trans>Customization options</Trans> 145 </ButtonText> 146 </Button> 147 148 {showCustomisation && ( 149 <View style={[a.gap_sm, a.p_md]}> 150 <Text style={[t.atoms.text_contrast_medium, a.font_bold]}> 151 <Trans>Color theme</Trans> 152 </Text> 153 <ToggleButton.Group 154 label={_(msg`Color mode`)} 155 values={[colorMode]} 156 onChange={([value]) => setColorMode(value as ColorModeValues)}> 157 <ToggleButton.Button name="system" label={_(msg`System`)}> 158 <ToggleButton.ButtonText> 159 <Trans>System</Trans> 160 </ToggleButton.ButtonText> 161 </ToggleButton.Button> 162 <ToggleButton.Button name="light" label={_(msg`Light`)}> 163 <ToggleButton.ButtonText> 164 <Trans>Light</Trans> 165 </ToggleButton.ButtonText> 166 </ToggleButton.Button> 167 <ToggleButton.Button name="dark" label={_(msg`Dark`)}> 168 <ToggleButton.ButtonText> 169 <Trans>Dark</Trans> 170 </ToggleButton.ButtonText> 171 </ToggleButton.Button> 172 </ToggleButton.Group> 173 </View> 174 )} 175 </View> 176 <View style={[a.flex_row, a.gap_sm]}> 177 <View style={[a.flex_1]}> 178 <TextField.Root> 179 <TextField.Icon icon={CodeBracketsIcon} /> 180 <TextField.Input 181 label={_(msg`Embed HTML code`)} 182 editable={false} 183 selection={{start: 0, end: snippet.length}} 184 value={snippet} 185 /> 186 </TextField.Root> 187 </View> 188 <Button 189 label={_(msg`Copy code`)} 190 color="primary" 191 variant="solid" 192 size="large" 193 onPress={() => { 194 navigator.clipboard.writeText(snippet) 195 setCopied(true) 196 }}> 197 {copied ? ( 198 <> 199 <ButtonIcon icon={CheckIcon} /> 200 <ButtonText> 201 <Trans>Copied!</Trans> 202 </ButtonText> 203 </> 204 ) : ( 205 <ButtonText> 206 <Trans>Copy code</Trans> 207 </ButtonText> 208 )} 209 </Button> 210 </View> 211 </View> 212 <Dialog.Close /> 213 </Dialog.Inner> 214 ) 215} 216 217/** 218 * Based on a snippet of code from React, which itself was based on the escape-html library. 219 * Copyright (c) Meta Platforms, Inc. and affiliates 220 * Copyright (c) 2012-2013 TJ Holowaychuk 221 * Copyright (c) 2015 Andreas Lubbe 222 * Copyright (c) 2015 Tiancheng "Timothy" Gu 223 * Licensed as MIT. 224 */ 225const matchHtmlRegExp = /["'&<>]/ 226function escapeHtml(string: string) { 227 const str = String(string) 228 const match = matchHtmlRegExp.exec(str) 229 if (!match) { 230 return str 231 } 232 let escape 233 let html = '' 234 let index 235 let lastIndex = 0 236 for (index = match.index; index < str.length; index++) { 237 switch (str.charCodeAt(index)) { 238 case 34: // " 239 escape = '&quot;' 240 break 241 case 38: // & 242 escape = '&amp;' 243 break 244 case 39: // ' 245 escape = '&#x27;' 246 break 247 case 60: // < 248 escape = '&lt;' 249 break 250 case 62: // > 251 escape = '&gt;' 252 break 253 default: 254 continue 255 } 256 if (lastIndex !== index) { 257 html += str.slice(lastIndex, index) 258 } 259 lastIndex = index + 1 260 html += escape 261 } 262 return lastIndex !== index ? html + str.slice(lastIndex, index) : html 263}