mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
at verify-code 195 lines 6.0 kB view raw
1import React, {memo, useRef, useState} from 'react' 2import {TextInput, 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 * as Dialog from '#/components/Dialog' 12import * as TextField from '#/components/forms/TextField' 13import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' 14import {CodeBrackets_Stroke2_Corner0_Rounded as CodeBrackets} from '#/components/icons/CodeBrackets' 15import {Text} from '#/components/Typography' 16import {Button, ButtonIcon, ButtonText} from '../Button' 17 18type EmbedDialogProps = { 19 control: Dialog.DialogControlProps 20 postAuthor: AppBskyActorDefs.ProfileViewBasic 21 postCid: string 22 postUri: string 23 record: AppBskyFeedPost.Record 24 timestamp: string 25} 26 27let EmbedDialog = ({control, ...rest}: EmbedDialogProps): React.ReactNode => { 28 return ( 29 <Dialog.Outer control={control}> 30 <Dialog.Handle /> 31 <EmbedDialogInner {...rest} /> 32 </Dialog.Outer> 33 ) 34} 35EmbedDialog = memo(EmbedDialog) 36export {EmbedDialog} 37 38function EmbedDialogInner({ 39 postAuthor, 40 postCid, 41 postUri, 42 record, 43 timestamp, 44}: Omit<EmbedDialogProps, 'control'>) { 45 const t = useTheme() 46 const {_, i18n} = useLingui() 47 const ref = useRef<TextInput>(null) 48 const [copied, setCopied] = useState(false) 49 50 // reset copied state after 2 seconds 51 React.useEffect(() => { 52 if (copied) { 53 const timeout = setTimeout(() => { 54 setCopied(false) 55 }, 2000) 56 return () => clearTimeout(timeout) 57 } 58 }, [copied]) 59 60 const snippet = React.useMemo(() => { 61 function toEmbedUrl(href: string) { 62 return toShareUrl(href) + '?ref_src=embed' 63 } 64 65 const lang = record.langs && record.langs.length > 0 ? record.langs[0] : '' 66 const profileHref = toEmbedUrl(['/profile', postAuthor.did].join('/')) 67 const urip = new AtUri(postUri) 68 const href = toEmbedUrl( 69 ['/profile', postAuthor.did, 'post', urip.rkey].join('/'), 70 ) 71 72 // 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 73 // DO NOT ADD ANY NEW INTERPOLATIONS BELOW WITHOUT ESCAPING THEM! 74 // Also, keep this code synced with the bskyembed code in landing.tsx. 75 // 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 76 return `<blockquote class="bluesky-embed" data-bluesky-uri="${escapeHtml( 77 postUri, 78 )}" data-bluesky-cid="${escapeHtml(postCid)}"><p lang="${escapeHtml( 79 lang, 80 )}">${escapeHtml(record.text)}${ 81 record.embed 82 ? `<br><br><a href="${escapeHtml(href)}">[image or embed]</a>` 83 : '' 84 }</p>&mdash; ${escapeHtml( 85 postAuthor.displayName || postAuthor.handle, 86 )} (<a href="${escapeHtml(profileHref)}">@${escapeHtml( 87 postAuthor.handle, 88 )}</a>) <a href="${escapeHtml(href)}">${escapeHtml( 89 niceDate(i18n, timestamp), 90 )}</a></blockquote><script async src="${EMBED_SCRIPT}" charset="utf-8"></script>` 91 }, [i18n, postUri, postCid, record, timestamp, postAuthor]) 92 93 return ( 94 <Dialog.Inner label="Embed post" style={[a.gap_md, {maxWidth: 500}]}> 95 <View style={[a.gap_sm, a.pb_lg]}> 96 <Text style={[a.text_2xl, a.font_bold]}> 97 <Trans>Embed post</Trans> 98 </Text> 99 <Text 100 style={[a.text_md, t.atoms.text_contrast_medium, a.leading_normal]}> 101 <Trans> 102 Embed this post in your website. Simply copy the following snippet 103 and paste it into the HTML code of your website. 104 </Trans> 105 </Text> 106 </View> 107 108 <View style={[a.flex_row, a.gap_sm]}> 109 <TextField.Root> 110 <TextField.Icon icon={CodeBrackets} /> 111 <TextField.Input 112 label={_(msg`Embed HTML code`)} 113 editable={false} 114 selection={{start: 0, end: snippet.length}} 115 value={snippet} 116 style={{}} 117 /> 118 </TextField.Root> 119 <Button 120 label={_(msg`Copy code`)} 121 color="primary" 122 variant="solid" 123 size="medium" 124 onPress={() => { 125 ref.current?.focus() 126 ref.current?.setSelection(0, snippet.length) 127 navigator.clipboard.writeText(snippet) 128 setCopied(true) 129 }}> 130 {copied ? ( 131 <> 132 <ButtonIcon icon={Check} /> 133 <ButtonText> 134 <Trans>Copied!</Trans> 135 </ButtonText> 136 </> 137 ) : ( 138 <ButtonText> 139 <Trans>Copy code</Trans> 140 </ButtonText> 141 )} 142 </Button> 143 </View> 144 <Dialog.Close /> 145 </Dialog.Inner> 146 ) 147} 148 149/** 150 * Based on a snippet of code from React, which itself was based on the escape-html library. 151 * Copyright (c) Meta Platforms, Inc. and affiliates 152 * Copyright (c) 2012-2013 TJ Holowaychuk 153 * Copyright (c) 2015 Andreas Lubbe 154 * Copyright (c) 2015 Tiancheng "Timothy" Gu 155 * Licensed as MIT. 156 */ 157const matchHtmlRegExp = /["'&<>]/ 158function escapeHtml(string: string) { 159 const str = String(string) 160 const match = matchHtmlRegExp.exec(str) 161 if (!match) { 162 return str 163 } 164 let escape 165 let html = '' 166 let index 167 let lastIndex = 0 168 for (index = match.index; index < str.length; index++) { 169 switch (str.charCodeAt(index)) { 170 case 34: // " 171 escape = '&quot;' 172 break 173 case 38: // & 174 escape = '&amp;' 175 break 176 case 39: // ' 177 escape = '&#x27;' 178 break 179 case 60: // < 180 escape = '&lt;' 181 break 182 case 62: // > 183 escape = '&gt;' 184 break 185 default: 186 continue 187 } 188 if (lastIndex !== index) { 189 html += str.slice(lastIndex, index) 190 } 191 lastIndex = index + 1 192 html += escape 193 } 194 return lastIndex !== index ? html + str.slice(lastIndex, index) : html 195}