Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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