mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
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>— ${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 = '"'
172 break
173 case 38: // &
174 escape = '&'
175 break
176 case 39: // '
177 escape = '''
178 break
179 case 60: // <
180 escape = '<'
181 break
182 case 62: // >
183 escape = '>'
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}