mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
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>— ${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 = '"'
240 break
241 case 38: // &
242 escape = '&'
243 break
244 case 39: // '
245 escape = '''
246 break
247 case 60: // <
248 escape = '<'
249 break
250 case 62: // >
251 escape = '>'
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}