UI work
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M16.86 3.907c.25.027.47.18.582.405.764 1.527 1.559 3.26 1.559 6.251 0 4.493-3.882 8.14-8.14 8.14a5.92 5.92 0 0 1-4.604-2.178c-.741.682-1.133 1.635-1.133 2.735a.74.74 0 0 1-1.48 0c0-1.632.65-3.05 1.82-4.009a5.923 5.923 0 0 1 4.367-8.3c2.124-.41 3.375-.697 4.278-1.087.852-.367 1.409-.835 2.095-1.685a.74.74 0 0 1 .656-.272m-.273 2.053a6 6 0 0 1-1.89 1.263c-1.077.464-2.491.777-4.586 1.181l-.014.003A4.442 4.442 0 0 0 6.74 14.48 6.3 6.3 0 0 1 8 14.095c.805-.16 1.646-.498 2.38-.892.74-.397 1.32-.823 1.623-1.126a.74.74 0 0 1 1.047 1.046c-.438.438-1.153.945-1.97 1.384-.825.442-1.805.843-2.79 1.039h-.002a5 5 0 0 0-.744.207 4.43 4.43 0 0 0 3.314 1.473h.003l.324-.01c3.34-.18 6.336-3.117 6.336-6.651 0-2.067-.413-3.428-.933-4.604"/></svg>
|
||||||
|
After Width: | Height: | Size: 828 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M7.638 11.873c.096.796.45 2.419 1.255 3.787.097.191.354.16.419 0l4.636-8.401a.394.394 0 0 0-.097-.51 3.73 3.73 0 0 0-2.575-.89c-3.703.095-3.735 4.71-3.638 6.014m5.086 6.237c3.606-.095 3.735-4.677 3.606-5.95-.064-.828-.418-2.483-1.255-3.915-.065-.127-.29-.127-.355 0l-4.668 8.465c-.064.191-.032.382.097.51a3.73 3.73 0 0 0 2.575.89m3.928-13.365-.193.35c-.129.223-.065.477.129.573C19.163 7.29 20 10.09 20 11.937c0 2.291-1.931 7.637-7.823 7.637-1.159 0-2.157-.159-3.058-.445a.415.415 0 0 0-.483.159l-.258.509c-.096.19-.322.255-.515.16l-.354-.192a.37.37 0 0 1-.16-.509l.224-.414c.097-.19.033-.413-.128-.509C4.869 16.678 4 13.846 4 12.064c0-2.291 1.77-7.637 7.694-7.637 1.224 0 2.254.159 3.155.445a.46.46 0 0 0 .515-.19l.29-.51a.366.366 0 0 1 .483-.127l.386.19c.161.096.226.319.129.51"/></svg>
|
||||||
|
After Width: | Height: | Size: 881 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M4.686 8.752c1.117 0 1.88.517 2.246 1.52q.247.678.249 1.643l-.001.097q-.014.94-.267 1.6l-.074.176c-.394.858-1.123 1.303-2.153 1.303a2.32 2.32 0 0 1-1.408-.455l-.035-.028v1.966a1 1 0 0 1-.008.074c-.029.178-.146.267-.359.267H.854q-.273 0-.335-.216l-.01-.035a1 1 0 0 1-.009-.09v-7.34a.7.7 0 0 1 .02-.149.27.27 0 0 1 .155-.18l.027-.01.005-.002a.5.5 0 0 1 .126-.02h2.066q.04 0 .076.007h.007l.048.013a.27.27 0 0 1 .204.231l.005.028a1 1 0 0 1 .004.07 2 2 0 0 1 .185-.131l.039-.024a2 2 0 0 1 .289-.144q.413-.17.93-.171m5.367.041c.46 0 .958.048 1.655.289l.015.005.021.008.06.021.02.007.018.008.017.007.033.014.024.012.015.008.02.01.003.004.01.005.006.004.01.007.003.003.01.008.005.003.009.008.003.003.009.007.011.012.008.009.004.004.006.008.005.005.005.007.013.023.005.009.004.007.002.003a.4.4 0 0 1 .036.134l.004.05.083 1.389a1 1 0 0 1 0 .083v.01l-.004.03-.007.043-.002.008q0 .006-.004.012v.005l-.005.014-.005.014-.002.003-.005.011-.002.004-.006.01-.002.004-.005.008q0 .003-.003.005l-.003.003-.006.009-.003.004-.018.016-.023.018-.005.003-.01.005h-.002l-.01.006q-.004 0-.008.003l-.007.002-.007.002-.009.003-.007.001-.01.003-.005.001-.013.002H12l-.012.002h-.007l-.013.001h-.048l-.013-.003-.057-.008-.124-.02a9 9 0 0 0-1.272-.112c-.804 0-1.146.265-1.146.857 0 .495.212.857 1.134.857a10 10 0 0 0 1.396-.12c.271-.049.366.06.354.326l-.083 1.388a1 1 0 0 1-.01.093l-.001.008-.003.013-.002.006-.004.012-.018.047-.007.016-.002.002-.006.011-.003.006c-.068.107-.192.153-.358.21a4.5 4.5 0 0 1-1.655.3c-1.24 0-2.135-.377-2.706-.996.306-.61.445-1.365.445-2.219 0-.828-.13-1.562-.418-2.16.574-.6 1.464-.963 2.69-.963m11.419-1.227q.354 0 .354.363v1.073c.386.048.796.116 1.265.21.2.041.327.242.285.446l-.307 1.477a.374.374 0 0 1-.438.293 17 17 0 0 0-.805-.142v.603c0 .724.425.82.71.821.117 0 .33-.024.52-.048.271-.036.39.072.378.338l-.082 1.46c-.012.29-.166.46-.415.52a5.5 5.5 0 0 1-1.24.145c-1.869 0-2.614-.978-2.614-2.982v-.985l-.165.008a.4.4 0 0 1-.262-.085l-.002-.002a.4.4 0 0 1-.04-.039l-.004-.003a.4.4 0 0 1-.05-.071l-.003-.005a.4.4 0 0 1-.023-.052l-.002-.005-.008-.025v-.006a.4.4 0 0 1-.01-.057l-.001-.011v-.006l-.001-.019v-.784c0-.165.07-.322.193-.43.191-.168.493-.435.759-.673.154-.139.296-.268.397-.363l.003-.003.744-.683c.201-.181.438-.278.71-.278zM14.052 7c.249 0 .368.12.368.362v2.52c.375-.199.75-.37 1.143-.512.91-.328 1.852-.47 3.017-.482l-.283.25a1.14 1.14 0 0 0-.385.858v.784q.002.214.083.394c-.294.02-.56.052-.806.096l.01.029c.436 1.35 1.099 3.357 1.099 3.357.082.254-.013.386-.273.386h-2.01a.524.524 0 0 1-.532-.386l-.77-2.338q-.142.09-.293.188v2.174q.002.362-.367.362h-1.665a.95.95 0 0 0 .23-.6l.083-1.39v-.007c.008-.165-.001-.484-.243-.718-.248-.24-.564-.225-.748-.194l-.033.005v-.355l.033.006c.197.035.521.046.77-.208.23-.234.24-.544.233-.713v-.009l-.083-1.388a.93.93 0 0 0-.317-.685 1.3 1.3 0 0 0-.382-.209l-.093-.032-.161-.054V7.362q0-.362.354-.362zM3.919 10.868c-.556 0-.74.423-.74 1.132s.184 1.132.74 1.132.738-.423.738-1.132-.182-1.132-.738-1.132"/></svg>
|
||||||
|
After Width: | Height: | Size: 3.0 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M12.781 4c1.543 0 2.853.528 3.752 1.661q.132.167.25.347.537.358.963.891C18.62 8 19 9.54 19 11.355c0 1.444-.241 2.715-.777 3.73q.081.057.156.128c.238.224.376.504.463.752l.006.014.004.015c.112.359.188.88-.106 1.362q-.091.15-.205.26.024.154.013.322c-.042.569-.407.95-.721 1.18-.234.169-.568.355-.986.355a1.3 1.3 0 0 1-.627-.157 1.3 1.3 0 0 1-.63.157c-.4 0-.744-.186-.978-.356l-.01-.008c-.258-.193-.571-.503-.68-.952a2 2 0 0 1-.523-.278l-.01-.008c-.16-.12-.34-.284-.48-.497a5 5 0 0 1-.373-.1v1.687c0 .341-.085.773-.423 1.11-.333.331-.756.412-1.089.412H7.705c-.334 0-.752-.086-1.08-.42a1.46 1.46 0 0 1-.394-.836 1.4 1.4 0 0 1-.819-.402C5.084 18.491 5 18.064 5 17.723V5.72c0-.341.084-.767.412-1.102.328-.334.746-.42 1.08-.42h3.32c.27 0 .596.053.886.245A4.85 4.85 0 0 1 12.781 4m0 .929c-.95 0-1.747.277-2.368.772 0-.376-.194-.574-.601-.574h-3.32q-.582 0-.583.594v12.002q0 .594.583.594h3.32c.407 0 .601-.198.601-.594v-3.189c.63.487 1.41.782 2.321.792.005-.147.037-.294.083-.43.068-.215.17-.425.336-.585a.91.91 0 0 1 .742-.248 1 1 0 0 1 .209-.425c.236-.277.582-.354.906-.354.32 0 .667.078.902.356q.049.059.086.122c.579-.868.879-2.089.879-3.644 0-3.406-1.437-5.19-4.096-5.19m2.229 8.819c-.532 0-.744.259-.68.784l.037.194-.175-.092a.9.9 0 0 0-.394-.111c-.266 0-.44.167-.55.526-.166.488.018.774.541.866l.192.027-.137.158c-.376.368-.348.7.083 1.023.165.12.32.183.449.184.211 0 .395-.147.542-.433l.092-.176.092.176c.146.286.32.433.532.433.146 0 .294-.064.459-.184.431-.313.449-.655.082-1.033l-.128-.148.183-.027c.533-.092.697-.378.541-.876-.119-.34-.293-.507-.54-.507a.96.96 0 0 0-.405.11l-.183.084.027-.194q.124-.786-.66-.784m-3.49-5.349c.913 0 1.213.696 1.213 1.858s-.3 1.856-1.213 1.856c-.912 0-1.212-.694-1.212-1.856s.3-1.857 1.213-1.858"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M11.862 3.042a.25.25 0 0 1 .276 0c.098.066.15.712.255 2.004l.036.45h2.484a3.56 3.56 0 0 1 3.559 3.559v.613h-1.228v-.613a2.33 2.33 0 0 0-2.331-2.332H12.53c.11 1.243.22 1.965.545 2.557a3.93 3.93 0 0 0 1.519 1.537c.727.41 1.66.5 3.526.682l1.025.1c1.165.114 1.748.17 1.813.268a.24.24 0 0 1-.002.273c-.066.097-.65.144-1.817.24l-.668.054v2.512a3.56 3.56 0 0 1-3.56 3.559h-2.486l-.033.42c-.104 1.31-.157 1.966-.255 2.033a.25.25 0 0 1-.276 0c-.098-.066-.15-.722-.255-2.033l-.033-.42H9.022a3.56 3.56 0 0 1-3.56-3.56v-.613H6.69v.613a2.33 2.33 0 0 0 2.332 2.332h2.452c-.112-1.29-.223-2.034-.557-2.639a3.93 3.93 0 0 0-1.548-1.544c-.74-.406-1.69-.484-3.59-.64l-.92-.074c-1.166-.096-1.75-.143-1.816-.24a.24.24 0 0 1-.002-.273c.065-.097.647-.154 1.813-.268l.609-.06V9.056a3.56 3.56 0 0 1 3.559-3.56h2.549l.036-.449c.105-1.292.157-1.938.255-2.004m5.382 9.495c-1.272.114-2.011.227-2.613.557a3.93 3.93 0 0 0-1.548 1.544c-.334.605-.445 1.349-.557 2.64h2.387a2.33 2.33 0 0 0 2.331-2.332zM9.022 6.723A2.33 2.33 0 0 0 6.69 9.055v2.364c1.342-.138 2.102-.257 2.715-.602a3.93 3.93 0 0 0 1.52-1.537c.323-.592.434-1.314.544-2.557z" clip-rule="evenodd"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -1,108 +0,0 @@
|
|||||||
import {View} from 'react-native'
|
|
||||||
import {plural} from '@lingui/core/macro'
|
|
||||||
import {Trans, useLingui} from '@lingui/react/macro'
|
|
||||||
|
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
|
||||||
import {Clock_Stroke2_Corner0_Rounded as Clock} from '#/components/icons/Clock'
|
|
||||||
import {Leaf_Stroke2_Corner0_Rounded as Leaf} from '#/components/icons/Leaf'
|
|
||||||
import {Text} from '#/components/Typography'
|
|
||||||
import {type PublicationViewExternal} from './types'
|
|
||||||
|
|
||||||
export function MetaRow({link}: {link: PublicationViewExternal}) {
|
|
||||||
const t = useTheme()
|
|
||||||
const {i18n} = useLingui()
|
|
||||||
|
|
||||||
// Guard against malformed dates (REG/EDGE from sweep): only render when
|
|
||||||
// `new Date(...)` produces a finite timestamp.
|
|
||||||
let formattedDate: string | undefined
|
|
||||||
if (link.createdAt) {
|
|
||||||
const parsed = new Date(link.createdAt)
|
|
||||||
if (!Number.isNaN(parsed.getTime())) {
|
|
||||||
formattedDate = i18n.date(parsed, {dateStyle: 'medium'})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.flex_row,
|
|
||||||
a.flex_wrap,
|
|
||||||
a.align_center,
|
|
||||||
a.justify_between,
|
|
||||||
a.gap_xs,
|
|
||||||
]}>
|
|
||||||
<View style={[a.flex_row, a.flex_wrap, a.align_center, a.gap_md]}>
|
|
||||||
{formattedDate && (
|
|
||||||
<Text style={[a.text_xs, a.leading_snug, t.atoms.text_contrast_high]}>
|
|
||||||
{formattedDate}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
{/*
|
|
||||||
TODO(APP-2160): Enable once the lexicon exposes an aggregate share
|
|
||||||
count for the external URL. `associatedBskyPost.repostCount` is the
|
|
||||||
wrong semantics (reshares of a single canonical post, not shares of
|
|
||||||
the URL). Component exists below for the moment that field lands.
|
|
||||||
*/}
|
|
||||||
{/* <SharesChip count={???} /> */}
|
|
||||||
{typeof link.readingTime === 'number' && link.readingTime > 0 && (
|
|
||||||
<ReadingTimeChip minutes={link.readingTime} />
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
{/*
|
|
||||||
TODO(APP-2160): Enable once the lexicon exposes a host/platform name
|
|
||||||
on `viewExternalSource` (e.g. "Leaflet"). Component exists below for
|
|
||||||
the moment that field lands.
|
|
||||||
*/}
|
|
||||||
{/* <HostedByChip name={???} /> */}
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ReadingTimeChip({minutes}: {minutes: number}) {
|
|
||||||
const t = useTheme()
|
|
||||||
return (
|
|
||||||
<View style={[a.flex_row, a.align_center, a.gap_2xs]}>
|
|
||||||
<Clock size="xs" style={[t.atoms.text_contrast_high]} />
|
|
||||||
<Text style={[a.text_xs, a.leading_snug, t.atoms.text_contrast_high]}>
|
|
||||||
<Trans comment="Reading time in minutes for an external article. # is the number.">
|
|
||||||
{plural(minutes, {one: '# min', other: '# min'})}
|
|
||||||
</Trans>
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
function SharesChip({count}: {count: number}) {
|
|
||||||
const t = useTheme()
|
|
||||||
return (
|
|
||||||
<View style={[a.flex_row, a.align_center, a.gap_2xs]}>
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
a.text_xs,
|
|
||||||
a.leading_snug,
|
|
||||||
a.underline,
|
|
||||||
t.atoms.text_contrast_high,
|
|
||||||
]}>
|
|
||||||
<Trans comment="Number of times an external URL has been shared on Bluesky. # is the count.">
|
|
||||||
{plural(count, {one: '# share', other: '# shares'})}
|
|
||||||
</Trans>
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
function HostedByChip({name}: {name: string}) {
|
|
||||||
const t = useTheme()
|
|
||||||
return (
|
|
||||||
<View style={[a.flex_row, a.align_center, a.gap_2xs]}>
|
|
||||||
<Leaf size="xs" style={[t.atoms.text_contrast_high]} />
|
|
||||||
<Text style={[a.text_xs, a.leading_snug, t.atoms.text_contrast_high]}>
|
|
||||||
<Trans comment="Host platform label on an external article card. e.g. 'Hosted by Leaflet'.">
|
|
||||||
Hosted by {name}
|
|
||||||
</Trans>
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
import {useEffect} from 'react'
|
|
||||||
import {View} from 'react-native'
|
|
||||||
import {Trans, useLingui} from '@lingui/react/macro'
|
|
||||||
|
|
||||||
import {toNiceDomain} from '#/lib/strings/url-helpers'
|
|
||||||
import {logger} from '#/logger'
|
|
||||||
import {useProfileQuery} from '#/state/queries/profile'
|
|
||||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
|
||||||
import {Link} from '#/components/Link'
|
|
||||||
import {Text} from '#/components/Typography'
|
|
||||||
import {type PublicationViewExternalSource} from './types'
|
|
||||||
import {parseDidFromAtUri} from './util'
|
|
||||||
|
|
||||||
export function PublicationFooter({
|
|
||||||
source,
|
|
||||||
}: {
|
|
||||||
source: PublicationViewExternalSource
|
|
||||||
}) {
|
|
||||||
const t = useTheme()
|
|
||||||
const {t: l} = useLingui()
|
|
||||||
|
|
||||||
const did = parseDidFromAtUri(source.associatedRecord?.uri)
|
|
||||||
const profileQuery = useProfileQuery({did: did ?? undefined})
|
|
||||||
const profileError = profileQuery.error
|
|
||||||
useEffect(() => {
|
|
||||||
if (profileError && did) {
|
|
||||||
// Log once per error transition. React Query handles transient network
|
|
||||||
// failures via retry; once the error is set we don't want to re-log on
|
|
||||||
// every render (could burn Sentry quota with a feed of broken DIDs).
|
|
||||||
logger.error('PublicationEmbed handle resolve failed', {
|
|
||||||
safeMessage: profileError,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [profileError, did])
|
|
||||||
const handle = did ? profileQuery.data?.handle : undefined
|
|
||||||
|
|
||||||
const name = source.name || (source.uri ? toNiceDomain(source.uri) : '')
|
|
||||||
|
|
||||||
const content = (hovered: boolean) => (
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.flex_row,
|
|
||||||
a.align_center,
|
|
||||||
a.px_md,
|
|
||||||
a.py_md,
|
|
||||||
a.transition_color,
|
|
||||||
{gap: 10},
|
|
||||||
hovered ? t.atoms.bg_contrast_25 : null,
|
|
||||||
]}
|
|
||||||
testID="publication-embed-footer">
|
|
||||||
<UserAvatar type="user" size={40} avatar={source.icon} />
|
|
||||||
<View style={[a.flex_1, {minWidth: 0, gap: 2}]}>
|
|
||||||
<Text
|
|
||||||
numberOfLines={1}
|
|
||||||
style={[a.text_sm, a.font_medium, t.atoms.text]}>
|
|
||||||
{name}
|
|
||||||
</Text>
|
|
||||||
{handle && (
|
|
||||||
<Text
|
|
||||||
numberOfLines={1}
|
|
||||||
testID="publication-embed-handle"
|
|
||||||
style={[a.text_xs, t.atoms.text_contrast_medium]}>
|
|
||||||
<Trans comment="Authorship line on the publication card. {handle} is a bsky handle.">
|
|
||||||
by @{handle}
|
|
||||||
</Trans>
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!source.uri) {
|
|
||||||
// No publication URL to link to; render the row as static content.
|
|
||||||
return content(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
to={source.uri}
|
|
||||||
label={
|
|
||||||
source.name ? l`View publication: ${source.name}` : l`View publication`
|
|
||||||
}
|
|
||||||
shouldProxy>
|
|
||||||
{({hovered}) => content(hovered)}
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import {parseDidFromAtUri} from '#/components/Post/Embed/ExternalEmbed/PublicationEmbed/util'
|
|
||||||
|
|
||||||
describe('parseDidFromAtUri', () => {
|
|
||||||
it('extracts a plc DID from a publication at-uri', () => {
|
|
||||||
expect(
|
|
||||||
parseDidFromAtUri('at://did:plc:abc123/site.standard.publication/3jx'),
|
|
||||||
).toBe('did:plc:abc123')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('extracts a web DID', () => {
|
|
||||||
expect(
|
|
||||||
parseDidFromAtUri(
|
|
||||||
'at://did:web:example.com/site.standard.publication/3jx',
|
|
||||||
),
|
|
||||||
).toBe('did:web:example.com')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns undefined for non-at-uri strings', () => {
|
|
||||||
expect(parseDidFromAtUri('https://example.com')).toBeUndefined()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns undefined for empty / nullish input', () => {
|
|
||||||
expect(parseDidFromAtUri('')).toBeUndefined()
|
|
||||||
expect(parseDidFromAtUri(undefined)).toBeUndefined()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns undefined when the authority is not a DID', () => {
|
|
||||||
expect(
|
|
||||||
parseDidFromAtUri('at://alice.bsky.social/site.standard.publication/3jx'),
|
|
||||||
).toBeUndefined()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
import {type StyleProp, View, type ViewStyle} from 'react-native'
|
|
||||||
import {Image} from 'expo-image'
|
|
||||||
import {useLingui} from '@lingui/react/macro'
|
|
||||||
|
|
||||||
import {useHaptics} from '#/lib/haptics'
|
|
||||||
import {shareUrl} from '#/lib/sharing'
|
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
|
||||||
import {Divider} from '#/components/Divider'
|
|
||||||
import {Link} from '#/components/Link'
|
|
||||||
import {Text} from '#/components/Typography'
|
|
||||||
import {IS_NATIVE} from '#/env'
|
|
||||||
import {MetaRow} from './MetaRow'
|
|
||||||
import {PublicationFooter} from './PublicationFooter'
|
|
||||||
import {
|
|
||||||
type PublicationViewExternal,
|
|
||||||
type PublicationViewExternalSource,
|
|
||||||
} from './types'
|
|
||||||
|
|
||||||
export function PublicationEmbed({
|
|
||||||
link,
|
|
||||||
source,
|
|
||||||
onOpen,
|
|
||||||
style,
|
|
||||||
}: {
|
|
||||||
link: PublicationViewExternal
|
|
||||||
source: PublicationViewExternalSource
|
|
||||||
onOpen?: () => void
|
|
||||||
style?: StyleProp<ViewStyle>
|
|
||||||
}) {
|
|
||||||
const t = useTheme()
|
|
||||||
const {t: l} = useLingui()
|
|
||||||
const playHaptic = useHaptics()
|
|
||||||
|
|
||||||
const onPress = () => {
|
|
||||||
playHaptic('Light')
|
|
||||||
onOpen?.()
|
|
||||||
}
|
|
||||||
|
|
||||||
const onShareExternal = () => {
|
|
||||||
if (link.uri && IS_NATIVE) {
|
|
||||||
playHaptic('Heavy')
|
|
||||||
shareUrl(link.uri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
testID="publication-embed"
|
|
||||||
style={[
|
|
||||||
a.w_full,
|
|
||||||
a.rounded_md,
|
|
||||||
a.overflow_hidden,
|
|
||||||
a.border,
|
|
||||||
t.atoms.border_contrast_low,
|
|
||||||
style,
|
|
||||||
]}>
|
|
||||||
<Link
|
|
||||||
to={link.uri}
|
|
||||||
label={link.title || l`Open link to ${link.uri}`}
|
|
||||||
shouldProxy
|
|
||||||
onPress={onPress}
|
|
||||||
onLongPress={onShareExternal}>
|
|
||||||
{({hovered}) => (
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.flex_col,
|
|
||||||
a.w_full,
|
|
||||||
a.transition_color,
|
|
||||||
hovered ? t.atoms.bg_contrast_25 : null,
|
|
||||||
]}>
|
|
||||||
{link.thumb ? (
|
|
||||||
<Image
|
|
||||||
style={[a.aspect_card]}
|
|
||||||
source={{uri: link.thumb}}
|
|
||||||
accessibilityIgnoresInvertColors
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
<View style={[a.p_md, {gap: 6}]}>
|
|
||||||
<View style={[{gap: 4}]}>
|
|
||||||
<Text
|
|
||||||
emoji
|
|
||||||
numberOfLines={3}
|
|
||||||
style={[
|
|
||||||
a.text_md,
|
|
||||||
a.font_semi_bold,
|
|
||||||
a.leading_snug,
|
|
||||||
t.atoms.text,
|
|
||||||
]}>
|
|
||||||
{link.title || link.uri}
|
|
||||||
</Text>
|
|
||||||
{link.description ? (
|
|
||||||
<Text
|
|
||||||
emoji
|
|
||||||
numberOfLines={2}
|
|
||||||
style={[a.text_xs, a.leading_snug, t.atoms.text]}>
|
|
||||||
{link.description}
|
|
||||||
</Text>
|
|
||||||
) : null}
|
|
||||||
</View>
|
|
||||||
<MetaRow link={link} />
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</Link>
|
|
||||||
<Divider />
|
|
||||||
<PublicationFooter source={source} />
|
|
||||||
{/*
|
|
||||||
Note: `source.theme` (background/foreground/accent/accentForeground
|
|
||||||
RGB triplets) is exposed by the lexicon but not consumed here. The
|
|
||||||
current design does not apply per-publication accent colors. Pick up
|
|
||||||
when product asks for theme accenting.
|
|
||||||
*/}
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import {
|
|
||||||
type AppBskyEmbedExternal,
|
|
||||||
type ComAtprotoRepoStrongRef,
|
|
||||||
} from '@atproto/api'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Local extension of `app.bsky.embed.external#viewExternal` that mirrors
|
|
||||||
* atproto PR #4915. Once @atproto/api ships those fields, delete this file
|
|
||||||
* and replace usages with `AppBskyEmbedExternal.ViewExternal` directly.
|
|
||||||
*/
|
|
||||||
export type PublicationViewExternal = AppBskyEmbedExternal.ViewExternal & {
|
|
||||||
createdAt?: string
|
|
||||||
updatedAt?: string
|
|
||||||
readingTime?: number
|
|
||||||
source?: PublicationViewExternalSource
|
|
||||||
associatedRecord?: ComAtprotoRepoStrongRef.Main
|
|
||||||
associatedBskyPost?: ComAtprotoRepoStrongRef.Main
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PublicationViewExternalSource {
|
|
||||||
$type?: 'app.bsky.embed.external#viewExternalSource'
|
|
||||||
uri?: string
|
|
||||||
icon?: string
|
|
||||||
name?: string
|
|
||||||
description?: string
|
|
||||||
theme?: PublicationViewExternalSourceTheme
|
|
||||||
associatedRecord?: ComAtprotoRepoStrongRef.Main
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PublicationViewExternalSourceTheme {
|
|
||||||
$type?: 'app.bsky.embed.external#viewExternalSourceTheme'
|
|
||||||
background?: PublicationColorRGB
|
|
||||||
foreground?: PublicationColorRGB
|
|
||||||
accent?: PublicationColorRGB
|
|
||||||
accentForeground?: PublicationColorRGB
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PublicationColorRGB {
|
|
||||||
r: number
|
|
||||||
g: number
|
|
||||||
b: number
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
/**
|
|
||||||
* Extract the DID from an at-uri of the form `at://did:<method>:<id>/<nsid>/<rkey>`.
|
|
||||||
* Returns undefined if the input is falsy, not an at-uri, or the authority is not a DID.
|
|
||||||
*/
|
|
||||||
export function parseDidFromAtUri(uri: string | undefined): string | undefined {
|
|
||||||
if (!uri || !uri.startsWith('at://')) return undefined
|
|
||||||
const authority = uri.slice('at://'.length).split('/')[0]
|
|
||||||
if (!authority || !authority.startsWith('did:')) return undefined
|
|
||||||
return authority
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,717 @@
|
|||||||
|
import {useCallback, useMemo} from 'react'
|
||||||
|
import {type StyleProp, View, type ViewStyle} from 'react-native'
|
||||||
|
import {Image} from 'expo-image'
|
||||||
|
import {type AppBskyEmbedExternal, AtUri} from '@atproto/api'
|
||||||
|
import {plural} from '@lingui/core/macro'
|
||||||
|
import {useLingui} from '@lingui/react/macro'
|
||||||
|
|
||||||
|
import {useHaptics} from '#/lib/haptics'
|
||||||
|
import {shareUrl} from '#/lib/sharing'
|
||||||
|
import {
|
||||||
|
exemptExternalEmbedSources,
|
||||||
|
parseEmbedPlayerFromUrl,
|
||||||
|
} from '#/lib/strings/embed-player'
|
||||||
|
import {niceDate} from '#/lib/strings/time'
|
||||||
|
import {toNiceDomain} from '#/lib/strings/url-helpers'
|
||||||
|
import {useExternalEmbedsPrefs} from '#/state/preferences'
|
||||||
|
import {useProfileQuery} from '#/state/queries/profile'
|
||||||
|
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
|
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||||
|
import {ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
|
import {Divider} from '#/components/Divider'
|
||||||
|
import {Clock_Stroke2_Corner0_Rounded as Clock} from '#/components/icons/Clock'
|
||||||
|
import {Leaflet} from '#/components/icons/community/Leaflet'
|
||||||
|
import {Offprint} from '#/components/icons/community/Offprint'
|
||||||
|
import {Pckt} from '#/components/icons/community/Pckt'
|
||||||
|
import {StandardSite} from '#/components/icons/community/StandardSite'
|
||||||
|
import {Earth_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
|
||||||
|
import {Link} from '#/components/Link'
|
||||||
|
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
import {IS_NATIVE} from '#/env'
|
||||||
|
|
||||||
|
export type ThemeColors = {
|
||||||
|
accent: string
|
||||||
|
accentForeground: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isStandardSiteEmbed(view: AppBskyEmbedExternal.ViewExternal) {
|
||||||
|
return view.associatedRefs?.some(ref =>
|
||||||
|
new AtUri(ref.uri).collection.startsWith('site.standard.'),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useStandardSitePublisherConfig(
|
||||||
|
view: AppBskyEmbedExternal.ViewExternal,
|
||||||
|
) {
|
||||||
|
return useMemo(() => {
|
||||||
|
try {
|
||||||
|
const u = new URL(view.source?.uri || '')
|
||||||
|
if (u.host.endsWith('leaflet.pub')) {
|
||||||
|
return {
|
||||||
|
name: 'Leaflet',
|
||||||
|
Icon: Leaflet,
|
||||||
|
}
|
||||||
|
} else if (u.host.endsWith('pckt.blog')) {
|
||||||
|
return {
|
||||||
|
name: 'pckt',
|
||||||
|
Icon: Pckt,
|
||||||
|
}
|
||||||
|
} else if (u.host.endsWith('offprint.app')) {
|
||||||
|
return {
|
||||||
|
name: 'Offprint',
|
||||||
|
Icon: Offprint,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
} catch (e) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}, [view])
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StandardSiteEmbed = ({
|
||||||
|
view,
|
||||||
|
onOpen,
|
||||||
|
style,
|
||||||
|
hideSubscribe,
|
||||||
|
}: {
|
||||||
|
view: AppBskyEmbedExternal.ViewExternal
|
||||||
|
onOpen?: () => void
|
||||||
|
style?: StyleProp<ViewStyle>
|
||||||
|
hideSubscribe?: boolean
|
||||||
|
}) => {
|
||||||
|
const {t: l, i18n} = useLingui()
|
||||||
|
const t = useTheme()
|
||||||
|
const playHaptic = useHaptics()
|
||||||
|
const externalEmbedPrefs = useExternalEmbedsPrefs()
|
||||||
|
const niceUrl = toNiceDomain(view.uri)
|
||||||
|
const imageUri = view.thumb
|
||||||
|
const embedPlayerParams = useMemo(() => {
|
||||||
|
const params = parseEmbedPlayerFromUrl(view.uri)
|
||||||
|
if (!params) return
|
||||||
|
const canShow = externalEmbedPrefs?.[params.source] !== 'hide'
|
||||||
|
if (canShow || exemptExternalEmbedSources.has(params.source)) {
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
}, [view.uri, externalEmbedPrefs])
|
||||||
|
const hasMedia = Boolean(imageUri || embedPlayerParams)
|
||||||
|
const isStandard = view.associatedRefs?.some(ref =>
|
||||||
|
new AtUri(ref.uri).collection.startsWith('site.standard.'),
|
||||||
|
)
|
||||||
|
const isStandardPublication = view.associatedRefs?.every(
|
||||||
|
ref =>
|
||||||
|
new AtUri(ref.uri).collection === 'site.standard.publication' &&
|
||||||
|
new AtUri(ref.uri).collection !== 'site.standard.document',
|
||||||
|
)
|
||||||
|
const themeColors = useMemo(() => {
|
||||||
|
let accent = t.atoms.text.color
|
||||||
|
let accentForeground = t.atoms.text_inverted.color
|
||||||
|
const {accentRGB, accentForegroundRGB} = view.source?.theme || {}
|
||||||
|
if (accent && accentForeground) {
|
||||||
|
accent = colorRGBToHex(accentRGB)
|
||||||
|
accentForeground = colorRGBToHex(accentForegroundRGB)
|
||||||
|
}
|
||||||
|
return {accent, accentForeground}
|
||||||
|
}, [view])
|
||||||
|
|
||||||
|
const maybeAuthorDid = useMemo(() => {
|
||||||
|
const publicationUri = view.associatedRefs?.find(
|
||||||
|
ref => new AtUri(ref.uri).collection === 'site.standard.publication',
|
||||||
|
)?.uri
|
||||||
|
if (!publicationUri) return null
|
||||||
|
return new AtUri(publicationUri)?.did
|
||||||
|
}, [view])
|
||||||
|
|
||||||
|
const onPress = useCallback(() => {
|
||||||
|
playHaptic('Light')
|
||||||
|
onOpen?.()
|
||||||
|
}, [playHaptic, onOpen])
|
||||||
|
|
||||||
|
const onLongPress = useCallback(() => {
|
||||||
|
if (view.uri && IS_NATIVE) {
|
||||||
|
playHaptic('Heavy')
|
||||||
|
shareUrl(view.uri)
|
||||||
|
}
|
||||||
|
}, [view.uri, playHaptic])
|
||||||
|
|
||||||
|
if (isStandardPublication) {
|
||||||
|
return (
|
||||||
|
<PublicationCard
|
||||||
|
author={{did: maybeAuthorDid}}
|
||||||
|
hideSubscribe={hideSubscribe}
|
||||||
|
view={view}
|
||||||
|
onPress={onPress}
|
||||||
|
onLongPress={onLongPress}
|
||||||
|
style={style}
|
||||||
|
themeColors={themeColors}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_col,
|
||||||
|
a.rounded_md,
|
||||||
|
a.overflow_hidden,
|
||||||
|
a.w_full,
|
||||||
|
a.border,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
style,
|
||||||
|
]}>
|
||||||
|
<Link
|
||||||
|
shouldProxy
|
||||||
|
to={view.uri}
|
||||||
|
label={view.title || l`Open link to ${niceUrl}`}
|
||||||
|
onPress={onPress}
|
||||||
|
onLongPress={onLongPress}>
|
||||||
|
{({hovered}) => (
|
||||||
|
<View style={[a.w_full]}>
|
||||||
|
{imageUri ? (
|
||||||
|
<Image
|
||||||
|
style={[a.aspect_card]}
|
||||||
|
source={{uri: imageUri}}
|
||||||
|
accessibilityIgnoresInvertColors
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
) : undefined}
|
||||||
|
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_1,
|
||||||
|
a.pt_sm,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
hasMedia && a.border_t,
|
||||||
|
{gap: 3},
|
||||||
|
isStandard && a.pt_md,
|
||||||
|
]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.pb_xs,
|
||||||
|
a.px_md,
|
||||||
|
{gap: 3},
|
||||||
|
isStandard && [{gap: 5}, a.pb_sm],
|
||||||
|
]}>
|
||||||
|
<Text
|
||||||
|
emoji
|
||||||
|
numberOfLines={3}
|
||||||
|
style={[
|
||||||
|
a.text_md,
|
||||||
|
a.font_semi_bold,
|
||||||
|
a.leading_snug,
|
||||||
|
isStandard && [
|
||||||
|
a.text_lg,
|
||||||
|
a.font_bold,
|
||||||
|
hovered && a.underline,
|
||||||
|
],
|
||||||
|
]}>
|
||||||
|
{view.title}
|
||||||
|
</Text>
|
||||||
|
{view.description ? (
|
||||||
|
<Text
|
||||||
|
emoji
|
||||||
|
numberOfLines={view.thumb ? 2 : 4}
|
||||||
|
style={[a.text_sm, a.leading_snug]}>
|
||||||
|
{view.description}
|
||||||
|
</Text>
|
||||||
|
) : undefined}
|
||||||
|
|
||||||
|
{isStandard && (view.createdAt || view.readingTime) && (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_row,
|
||||||
|
a.align_center,
|
||||||
|
a.gap_md,
|
||||||
|
{paddingTop: 2},
|
||||||
|
]}>
|
||||||
|
{view.createdAt && (
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
a.text_xs,
|
||||||
|
a.leading_snug,
|
||||||
|
t.atoms.text_contrast_high,
|
||||||
|
]}>
|
||||||
|
{niceDate(i18n, view.createdAt, 'medium', 'none')}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{view.readingTime && (
|
||||||
|
<View style={[a.flex_row, a.align_center, a.gap_2xs]}>
|
||||||
|
<Clock size="xs" style={t.atoms.text_contrast_high} />
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
a.text_xs,
|
||||||
|
a.leading_snug,
|
||||||
|
t.atoms.text_contrast_high,
|
||||||
|
]}>
|
||||||
|
{l({
|
||||||
|
message: plural(view.readingTime, {
|
||||||
|
one: '#m',
|
||||||
|
other: '#m',
|
||||||
|
}),
|
||||||
|
comment: `How long it takes to read an article, in minutes. Displayed in a short form, e.g. "5m" for 5 minutes.`,
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{!view.source && (
|
||||||
|
<View style={[a.px_md]}>
|
||||||
|
<Divider />
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_row,
|
||||||
|
a.align_center,
|
||||||
|
a.gap_2xs,
|
||||||
|
a.pb_sm,
|
||||||
|
{
|
||||||
|
paddingTop: 6, // off menu
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<Globe
|
||||||
|
size="xs"
|
||||||
|
style={[
|
||||||
|
a.transition_color,
|
||||||
|
hovered
|
||||||
|
? t.atoms.text_contrast_medium
|
||||||
|
: t.atoms.text_contrast_low,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
numberOfLines={1}
|
||||||
|
style={[
|
||||||
|
a.transition_color,
|
||||||
|
a.text_xs,
|
||||||
|
a.leading_snug,
|
||||||
|
hovered
|
||||||
|
? t.atoms.text_contrast_high
|
||||||
|
: t.atoms.text_contrast_medium,
|
||||||
|
]}>
|
||||||
|
{toNiceDomain(view.uri)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{view.source && (
|
||||||
|
<View style={[a.px_md]}>
|
||||||
|
<Divider />
|
||||||
|
<PublicationFooter
|
||||||
|
view={view}
|
||||||
|
onPress={onPress}
|
||||||
|
onLongPress={onLongPress}
|
||||||
|
themeColors={themeColors}
|
||||||
|
author={{did: maybeAuthorDid}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PublicationCard({
|
||||||
|
view,
|
||||||
|
hideSubscribe,
|
||||||
|
onPress,
|
||||||
|
onLongPress,
|
||||||
|
themeColors,
|
||||||
|
style,
|
||||||
|
author,
|
||||||
|
}: {
|
||||||
|
view: AppBskyEmbedExternal.ViewExternal
|
||||||
|
hideSubscribe?: boolean
|
||||||
|
onPress?: () => void
|
||||||
|
onLongPress?: () => void
|
||||||
|
themeColors: ThemeColors
|
||||||
|
style?: StyleProp<ViewStyle>
|
||||||
|
author: {did: string | null | undefined}
|
||||||
|
}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const {t: l} = useLingui()
|
||||||
|
const {gtPhone} = useBreakpoints()
|
||||||
|
const profileQuery = useProfileQuery({did: author.did ?? undefined})
|
||||||
|
const handle = author.did ? profileQuery.data?.handle : undefined
|
||||||
|
const highlightedPublisher = useStandardSitePublisherConfig(view)
|
||||||
|
|
||||||
|
if (!view.source) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
shouldProxy
|
||||||
|
to={view.source.uri}
|
||||||
|
label={l`Subscribe`}
|
||||||
|
onPress={onPress}
|
||||||
|
onLongPress={onLongPress}>
|
||||||
|
{({hovered}) => (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_col,
|
||||||
|
a.rounded_md,
|
||||||
|
a.overflow_hidden,
|
||||||
|
a.w_full,
|
||||||
|
a.border,
|
||||||
|
a.p_md,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
style,
|
||||||
|
]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_1,
|
||||||
|
a.align_center,
|
||||||
|
a.justify_between,
|
||||||
|
a.gap_md,
|
||||||
|
gtPhone && [a.flex_row, a.gap_sm],
|
||||||
|
]}
|
||||||
|
testID="publication-embed-footer">
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.w_full,
|
||||||
|
a.flex_row,
|
||||||
|
a.align_center,
|
||||||
|
a.gap_sm,
|
||||||
|
gtPhone && a.flex_1,
|
||||||
|
]}>
|
||||||
|
<>
|
||||||
|
<PublicationIcon
|
||||||
|
view={view}
|
||||||
|
size={40}
|
||||||
|
hovered={hovered}
|
||||||
|
themeColors={themeColors}
|
||||||
|
/>
|
||||||
|
<View style={[a.flex_1, a.gap_2xs]}>
|
||||||
|
<Text
|
||||||
|
numberOfLines={1}
|
||||||
|
style={[
|
||||||
|
a.text_md,
|
||||||
|
a.font_semi_bold,
|
||||||
|
t.atoms.text,
|
||||||
|
hovered && a.underline,
|
||||||
|
]}>
|
||||||
|
{view.source?.title}
|
||||||
|
</Text>
|
||||||
|
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
type: 'domain',
|
||||||
|
enabled: !highlightedPublisher,
|
||||||
|
value: toNiceDomain(view.source?.uri || ''),
|
||||||
|
hasPrev: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'author',
|
||||||
|
enabled: true,
|
||||||
|
value: handle ? l`by @${handle}` : undefined,
|
||||||
|
hasPrev:
|
||||||
|
!highlightedPublisher && Boolean(view.source?.uri),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
.filter(item => item.enabled && item.value)
|
||||||
|
.map(item => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{item.hasPrev && (
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
a.text_xs,
|
||||||
|
a.leading_snug,
|
||||||
|
t.atoms.text_contrast_medium,
|
||||||
|
]}>
|
||||||
|
•
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
<Text
|
||||||
|
numberOfLines={1}
|
||||||
|
style={[
|
||||||
|
a.text_xs,
|
||||||
|
a.leading_snug,
|
||||||
|
t.atoms.text_contrast_medium,
|
||||||
|
]}>
|
||||||
|
{item.value}
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{!hideSubscribe && (
|
||||||
|
<SubscribeButton
|
||||||
|
view={view}
|
||||||
|
style={[!gtPhone && [a.w_full, a.justify_center]]}
|
||||||
|
onPress={onPress}
|
||||||
|
onLongPress={onLongPress}
|
||||||
|
themeColors={themeColors}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{view.description && (
|
||||||
|
<View style={[a.pt_sm]}>
|
||||||
|
<Text style={[a.text_sm, a.leading_snug]} numberOfLines={3}>
|
||||||
|
{view.description}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SubscribeButton({
|
||||||
|
view,
|
||||||
|
onPress,
|
||||||
|
onLongPress,
|
||||||
|
style,
|
||||||
|
}: {
|
||||||
|
view: AppBskyEmbedExternal.ViewExternal
|
||||||
|
onPress?: () => void
|
||||||
|
onLongPress?: () => void
|
||||||
|
themeColors: ThemeColors
|
||||||
|
style?: StyleProp<ViewStyle>
|
||||||
|
}) {
|
||||||
|
const {t: l} = useLingui()
|
||||||
|
const highlightedPublisher = useStandardSitePublisherConfig(view)
|
||||||
|
const cta = highlightedPublisher
|
||||||
|
? l`Subscribe on ${highlightedPublisher.name}`
|
||||||
|
: l`View publication`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
shouldProxy
|
||||||
|
to={view.source!.uri}
|
||||||
|
label={cta}
|
||||||
|
size="small"
|
||||||
|
color="secondary_inverted"
|
||||||
|
style={[style, {gap: 5}]}
|
||||||
|
onPress={onPress}
|
||||||
|
onLongPress={onLongPress}>
|
||||||
|
{highlightedPublisher ? (
|
||||||
|
<>
|
||||||
|
<View style={[a.flex_row, a.align_center, {gap: 7}]}>
|
||||||
|
<ButtonIcon icon={highlightedPublisher.Icon} size="lg" />
|
||||||
|
<ButtonText>|</ButtonText>
|
||||||
|
</View>
|
||||||
|
<ButtonText>{cta}</ButtonText>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<ButtonText>{cta}</ButtonText>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PublicationIcon({
|
||||||
|
view,
|
||||||
|
size,
|
||||||
|
hovered,
|
||||||
|
themeColors,
|
||||||
|
}: {
|
||||||
|
view: AppBskyEmbedExternal.ViewExternal
|
||||||
|
size: number
|
||||||
|
hovered?: boolean
|
||||||
|
themeColors: ThemeColors
|
||||||
|
}) {
|
||||||
|
const opacity = hovered ? 0.6 : 0.2
|
||||||
|
return view.source?.icon ? (
|
||||||
|
<View>
|
||||||
|
<UserAvatar
|
||||||
|
noBorder
|
||||||
|
type="labeler"
|
||||||
|
size={size}
|
||||||
|
avatar={view.source.icon}
|
||||||
|
extraAviStyle={{borderRadius: a.rounded_sm.borderRadius}}
|
||||||
|
/>
|
||||||
|
<MediaInsetBorder
|
||||||
|
style={[
|
||||||
|
a.rounded_sm,
|
||||||
|
{
|
||||||
|
borderColor: themeColors.accentForeground,
|
||||||
|
opacity,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.align_center,
|
||||||
|
a.justify_center,
|
||||||
|
a.rounded_sm,
|
||||||
|
{
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
backgroundColor: themeColors.accent,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<StandardSite width={size * 0.8} fill={themeColors.accentForeground} />
|
||||||
|
<MediaInsetBorder
|
||||||
|
style={[
|
||||||
|
a.rounded_sm,
|
||||||
|
{
|
||||||
|
borderColor: themeColors.accentForeground,
|
||||||
|
opacity,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PublicationFooter({
|
||||||
|
view,
|
||||||
|
hideSubscribe,
|
||||||
|
onPress,
|
||||||
|
onLongPress,
|
||||||
|
themeColors,
|
||||||
|
author,
|
||||||
|
}: {
|
||||||
|
view: AppBskyEmbedExternal.ViewExternal
|
||||||
|
hideSubscribe?: boolean
|
||||||
|
themeColors: ThemeColors
|
||||||
|
onPress?: () => void
|
||||||
|
onLongPress?: () => void
|
||||||
|
author: {did: string | null | undefined}
|
||||||
|
}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const {t: l} = useLingui()
|
||||||
|
const {gtPhone} = useBreakpoints()
|
||||||
|
const profileQuery = useProfileQuery({did: author.did ?? undefined})
|
||||||
|
const handle = author.did ? profileQuery.data?.handle : undefined
|
||||||
|
const highlightedPublisher = useMemo(() => {
|
||||||
|
try {
|
||||||
|
const u = new URL(view.source?.uri || '')
|
||||||
|
if (u.host.endsWith('leaflet.pub')) {
|
||||||
|
return 'Leaflet'
|
||||||
|
} else if (u.host.endsWith('pckt.blog')) {
|
||||||
|
return 'pckt'
|
||||||
|
} else if (u.host.endsWith('offprint.app')) {
|
||||||
|
return 'Offprint'
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
} catch (e) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}, [view])
|
||||||
|
|
||||||
|
if (!view.source) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_1,
|
||||||
|
a.align_center,
|
||||||
|
a.justify_between,
|
||||||
|
a.py_md,
|
||||||
|
a.gap_md,
|
||||||
|
gtPhone && [a.flex_row, a.gap_sm],
|
||||||
|
]}
|
||||||
|
testID="publication-embed-footer">
|
||||||
|
<Link
|
||||||
|
shouldProxy
|
||||||
|
to={view.source.uri}
|
||||||
|
label={l`Subscribe`}
|
||||||
|
onPress={onPress}
|
||||||
|
onLongPress={onLongPress}
|
||||||
|
style={[
|
||||||
|
a.w_full,
|
||||||
|
a.flex_row,
|
||||||
|
a.align_center,
|
||||||
|
a.gap_sm,
|
||||||
|
gtPhone && a.flex_1,
|
||||||
|
]}>
|
||||||
|
{({hovered}) => (
|
||||||
|
<>
|
||||||
|
<PublicationIcon
|
||||||
|
view={view}
|
||||||
|
size={32}
|
||||||
|
hovered={hovered}
|
||||||
|
themeColors={themeColors}
|
||||||
|
/>
|
||||||
|
<View style={[a.flex_1, a.gap_2xs]}>
|
||||||
|
<Text
|
||||||
|
numberOfLines={1}
|
||||||
|
style={[
|
||||||
|
a.text_sm,
|
||||||
|
a.font_medium,
|
||||||
|
t.atoms.text,
|
||||||
|
hovered && a.underline,
|
||||||
|
]}>
|
||||||
|
{view.source?.title}
|
||||||
|
</Text>
|
||||||
|
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
type: 'domain',
|
||||||
|
enabled: !highlightedPublisher,
|
||||||
|
value: toNiceDomain(view.source?.uri || ''),
|
||||||
|
hasPrev: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'author',
|
||||||
|
enabled: true,
|
||||||
|
value: handle ? l`by @${handle}` : undefined,
|
||||||
|
hasPrev: !highlightedPublisher && Boolean(view.source?.uri),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
.filter(item => item.enabled && item.value)
|
||||||
|
.map(item => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{item.hasPrev && (
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
a.text_xs,
|
||||||
|
a.leading_snug,
|
||||||
|
t.atoms.text_contrast_medium,
|
||||||
|
]}>
|
||||||
|
•
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
<Text
|
||||||
|
numberOfLines={1}
|
||||||
|
style={[
|
||||||
|
a.text_xs,
|
||||||
|
a.leading_snug,
|
||||||
|
t.atoms.text_contrast_medium,
|
||||||
|
]}>
|
||||||
|
{item.value}
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{!hideSubscribe && (
|
||||||
|
<SubscribeButton
|
||||||
|
view={view}
|
||||||
|
style={[!gtPhone && [a.w_full, a.justify_center]]}
|
||||||
|
onPress={onPress}
|
||||||
|
onLongPress={onLongPress}
|
||||||
|
themeColors={themeColors}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function colorRGBToHex(
|
||||||
|
rgb: AppBskyEmbedExternal.ViewExternalSourceTheme['accentRGB'],
|
||||||
|
): string {
|
||||||
|
if (!rgb) return '#000000'
|
||||||
|
const {r, g, b} = rgb
|
||||||
|
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`
|
||||||
|
}
|
||||||
@@ -22,6 +22,10 @@ import {useInteractionState} from '#/components/hooks/useInteractionState'
|
|||||||
import {GalleryBleed} from '#/components/images/Gallery'
|
import {GalleryBleed} from '#/components/images/Gallery'
|
||||||
import {ContentHider} from '#/components/moderation/ContentHider'
|
import {ContentHider} from '#/components/moderation/ContentHider'
|
||||||
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
||||||
|
import {
|
||||||
|
isStandardSiteEmbed,
|
||||||
|
StandardSiteEmbed,
|
||||||
|
} from '#/components/Post/Embed/StandardSiteEmbed'
|
||||||
import {RichText} from '#/components/RichText'
|
import {RichText} from '#/components/RichText'
|
||||||
import {Embed as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
|
import {Embed as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
|
||||||
import {SubtleHover} from '#/components/SubtleHover'
|
import {SubtleHover} from '#/components/SubtleHover'
|
||||||
@@ -32,8 +36,6 @@ import {
|
|||||||
parseEmbed,
|
parseEmbed,
|
||||||
} from '#/types/bsky/post'
|
} from '#/types/bsky/post'
|
||||||
import {ExternalEmbed} from './ExternalEmbed'
|
import {ExternalEmbed} from './ExternalEmbed'
|
||||||
import {PublicationEmbed} from './ExternalEmbed/PublicationEmbed'
|
|
||||||
import {type PublicationViewExternal} from './ExternalEmbed/PublicationEmbed/types'
|
|
||||||
import {ModeratedFeedEmbed} from './FeedEmbed'
|
import {ModeratedFeedEmbed} from './FeedEmbed'
|
||||||
import {ImageEmbed} from './ImageEmbed'
|
import {ImageEmbed} from './ImageEmbed'
|
||||||
import {ModeratedListEmbed} from './ListEmbed'
|
import {ModeratedListEmbed} from './ListEmbed'
|
||||||
@@ -97,16 +99,13 @@ function MediaEmbed({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
case 'link': {
|
case 'link': {
|
||||||
const external = embed.view.external as PublicationViewExternal
|
if (isStandardSiteEmbed(embed.view.external)) {
|
||||||
if (external.source) {
|
|
||||||
return (
|
return (
|
||||||
<ContentHider
|
<ContentHider
|
||||||
modui={rest.moderation?.ui('contentMedia')}
|
modui={rest.moderation?.ui('contentMedia')}
|
||||||
activeStyle={[a.mt_sm]}>
|
activeStyle={[a.mt_sm]}>
|
||||||
<PublicationEmbed
|
<StandardSiteEmbed
|
||||||
link={external}
|
view={embed.view.external}
|
||||||
source={external.source}
|
|
||||||
onOpen={rest.onOpen}
|
|
||||||
style={[a.mt_sm, rest.style]}
|
style={[a.mt_sm, rest.style]}
|
||||||
/>
|
/>
|
||||||
</ContentHider>
|
</ContentHider>
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import {createSinglePathSVG} from '../TEMPLATE'
|
||||||
|
|
||||||
|
export const Leaflet = createSinglePathSVG({
|
||||||
|
path: 'M16.86 3.907c.25.027.47.18.582.405.764 1.527 1.559 3.26 1.559 6.251 0 4.493-3.882 8.14-8.14 8.14a5.92 5.92 0 0 1-4.604-2.178c-.741.682-1.133 1.635-1.133 2.735a.74.74 0 0 1-1.48 0c0-1.632.65-3.05 1.82-4.009a5.923 5.923 0 0 1 4.367-8.3c2.124-.41 3.375-.697 4.278-1.087.852-.367 1.409-.835 2.095-1.685a.74.74 0 0 1 .656-.272m-.273 2.053a6 6 0 0 1-1.89 1.263c-1.077.464-2.491.777-4.586 1.181l-.014.003A4.442 4.442 0 0 0 6.74 14.48 6.3 6.3 0 0 1 8 14.095c.805-.16 1.646-.498 2.38-.892.74-.397 1.32-.823 1.623-1.126a.74.74 0 0 1 1.047 1.046c-.438.438-1.153.945-1.97 1.384-.825.442-1.805.843-2.79 1.039h-.002a5 5 0 0 0-.744.207 4.43 4.43 0 0 0 3.314 1.473h.003l.324-.01c3.34-.18 6.336-3.117 6.336-6.651 0-2.067-.413-3.428-.933-4.604',
|
||||||
|
})
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import {createSinglePathSVG} from '../TEMPLATE'
|
||||||
|
|
||||||
|
export const Offprint = createSinglePathSVG({
|
||||||
|
path: 'M7.638 11.873c.096.796.45 2.419 1.255 3.787.097.191.354.16.419 0l4.636-8.401a.394.394 0 0 0-.097-.51 3.73 3.73 0 0 0-2.575-.89c-3.703.095-3.735 4.71-3.638 6.014m5.086 6.237c3.606-.095 3.735-4.677 3.606-5.95-.064-.828-.418-2.483-1.255-3.915-.065-.127-.29-.127-.355 0l-4.668 8.465c-.064.191-.032.382.097.51a3.73 3.73 0 0 0 2.575.89m3.928-13.365-.193.35c-.129.223-.065.477.129.573C19.163 7.29 20 10.09 20 11.937c0 2.291-1.931 7.637-7.823 7.637-1.159 0-2.157-.159-3.058-.445a.415.415 0 0 0-.483.159l-.258.509c-.096.19-.322.255-.515.16l-.354-.192a.37.37 0 0 1-.16-.509l.224-.414c.097-.19.033-.413-.128-.509C4.869 16.678 4 13.846 4 12.064c0-2.291 1.77-7.637 7.694-7.637 1.224 0 2.254.159 3.155.445a.46.46 0 0 0 .515-.19l.29-.51a.366.366 0 0 1 .483-.127l.386.19c.161.096.226.319.129.51',
|
||||||
|
})
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import {createSinglePathSVG} from '../TEMPLATE'
|
||||||
|
|
||||||
|
export const PcktFull = createSinglePathSVG({
|
||||||
|
path: 'M4.686 8.752c1.117 0 1.88.517 2.246 1.52q.247.678.249 1.643l-.001.097q-.014.94-.267 1.6l-.074.176c-.394.858-1.123 1.303-2.153 1.303a2.32 2.32 0 0 1-1.408-.455l-.035-.028v1.966a1 1 0 0 1-.008.074c-.029.178-.146.267-.359.267H.854q-.273 0-.335-.216l-.01-.035a1 1 0 0 1-.009-.09v-7.34a.7.7 0 0 1 .02-.149.27.27 0 0 1 .155-.18l.027-.01.005-.002a.5.5 0 0 1 .126-.02h2.066q.04 0 .076.007h.007l.048.013a.27.27 0 0 1 .204.231l.005.028a1 1 0 0 1 .004.07 2 2 0 0 1 .185-.131l.039-.024a2 2 0 0 1 .289-.144q.413-.17.93-.171m5.367.041c.46 0 .958.048 1.655.289l.015.005.021.008.06.021.02.007.018.008.017.007.033.014.024.012.015.008.02.01.003.004.01.005.006.004.01.007.003.003.01.008.005.003.009.008.003.003.009.007.011.012.008.009.004.004.006.008.005.005.005.007.013.023.005.009.004.007.002.003a.4.4 0 0 1 .036.134l.004.05.083 1.389a1 1 0 0 1 0 .083v.01l-.004.03-.007.043-.002.008q0 .006-.004.012v.005l-.005.014-.005.014-.002.003-.005.011-.002.004-.006.01-.002.004-.005.008q0 .003-.003.005l-.003.003-.006.009-.003.004-.018.016-.023.018-.005.003-.01.005h-.002l-.01.006q-.004 0-.008.003l-.007.002-.007.002-.009.003-.007.001-.01.003-.005.001-.013.002H12l-.012.002h-.007l-.013.001h-.048l-.013-.003-.057-.008-.124-.02a9 9 0 0 0-1.272-.112c-.804 0-1.146.265-1.146.857 0 .495.212.857 1.134.857a10 10 0 0 0 1.396-.12c.271-.049.366.06.354.326l-.083 1.388a1 1 0 0 1-.01.093l-.001.008-.003.013-.002.006-.004.012-.018.047-.007.016-.002.002-.006.011-.003.006c-.068.107-.192.153-.358.21a4.5 4.5 0 0 1-1.655.3c-1.24 0-2.135-.377-2.706-.996.306-.61.445-1.365.445-2.219 0-.828-.13-1.562-.418-2.16.574-.6 1.464-.963 2.69-.963m11.419-1.227q.354 0 .354.363v1.073c.386.048.796.116 1.265.21.2.041.327.242.285.446l-.307 1.477a.374.374 0 0 1-.438.293 17 17 0 0 0-.805-.142v.603c0 .724.425.82.71.821.117 0 .33-.024.52-.048.271-.036.39.072.378.338l-.082 1.46c-.012.29-.166.46-.415.52a5.5 5.5 0 0 1-1.24.145c-1.869 0-2.614-.978-2.614-2.982v-.985l-.165.008a.4.4 0 0 1-.262-.085l-.002-.002a.4.4 0 0 1-.04-.039l-.004-.003a.4.4 0 0 1-.05-.071l-.003-.005a.4.4 0 0 1-.023-.052l-.002-.005-.008-.025v-.006a.4.4 0 0 1-.01-.057l-.001-.011v-.006l-.001-.019v-.784c0-.165.07-.322.193-.43.191-.168.493-.435.759-.673.154-.139.296-.268.397-.363l.003-.003.744-.683c.201-.181.438-.278.71-.278zM14.052 7c.249 0 .368.12.368.362v2.52c.375-.199.75-.37 1.143-.512.91-.328 1.852-.47 3.017-.482l-.283.25a1.14 1.14 0 0 0-.385.858v.784q.002.214.083.394c-.294.02-.56.052-.806.096l.01.029c.436 1.35 1.099 3.357 1.099 3.357.082.254-.013.386-.273.386h-2.01a.524.524 0 0 1-.532-.386l-.77-2.338q-.142.09-.293.188v2.174q.002.362-.367.362h-1.665a.95.95 0 0 0 .23-.6l.083-1.39v-.007c.008-.165-.001-.484-.243-.718-.248-.24-.564-.225-.748-.194l-.033.005v-.355l.033.006c.197.035.521.046.77-.208.23-.234.24-.544.233-.713v-.009l-.083-1.388a.93.93 0 0 0-.317-.685 1.3 1.3 0 0 0-.382-.209l-.093-.032-.161-.054V7.362q0-.362.354-.362zM3.919 10.868c-.556 0-.74.423-.74 1.132s.184 1.132.74 1.132.738-.423.738-1.132-.182-1.132-.738-1.132',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const Pckt = createSinglePathSVG({
|
||||||
|
path: 'M12.781 4c1.543 0 2.853.528 3.752 1.661q.132.167.25.347.537.358.963.891C18.62 8 19 9.54 19 11.355c0 1.444-.241 2.715-.777 3.73q.081.057.156.128c.238.224.376.504.463.752l.006.014.004.015c.112.359.188.88-.106 1.362q-.091.15-.205.26.024.154.013.322c-.042.569-.407.95-.721 1.18-.234.169-.568.355-.986.355a1.3 1.3 0 0 1-.627-.157 1.3 1.3 0 0 1-.63.157c-.4 0-.744-.186-.978-.356l-.01-.008c-.258-.193-.571-.503-.68-.952a2 2 0 0 1-.523-.278l-.01-.008c-.16-.12-.34-.284-.48-.497a5 5 0 0 1-.373-.1v1.687c0 .341-.085.773-.423 1.11-.333.331-.756.412-1.089.412H7.705c-.334 0-.752-.086-1.08-.42a1.46 1.46 0 0 1-.394-.836 1.4 1.4 0 0 1-.819-.402C5.084 18.491 5 18.064 5 17.723V5.72c0-.341.084-.767.412-1.102.328-.334.746-.42 1.08-.42h3.32c.27 0 .596.053.886.245A4.85 4.85 0 0 1 12.781 4m0 .929c-.95 0-1.747.277-2.368.772 0-.376-.194-.574-.601-.574h-3.32q-.582 0-.583.594v12.002q0 .594.583.594h3.32c.407 0 .601-.198.601-.594v-3.189c.63.487 1.41.782 2.321.792.005-.147.037-.294.083-.43.068-.215.17-.425.336-.585a.91.91 0 0 1 .742-.248 1 1 0 0 1 .209-.425c.236-.277.582-.354.906-.354.32 0 .667.078.902.356q.049.059.086.122c.579-.868.879-2.089.879-3.644 0-3.406-1.437-5.19-4.096-5.19m2.229 8.819c-.532 0-.744.259-.68.784l.037.194-.175-.092a.9.9 0 0 0-.394-.111c-.266 0-.44.167-.55.526-.166.488.018.774.541.866l.192.027-.137.158c-.376.368-.348.7.083 1.023.165.12.32.183.449.184.211 0 .395-.147.542-.433l.092-.176.092.176c.146.286.32.433.532.433.146 0 .294-.064.459-.184.431-.313.449-.655.082-1.033l-.128-.148.183-.027c.533-.092.697-.378.541-.876-.119-.34-.293-.507-.54-.507a.96.96 0 0 0-.405.11l-.183.084.027-.194q.124-.786-.66-.784m-3.49-5.349c.913 0 1.213.696 1.213 1.858s-.3 1.856-1.213 1.856c-.912 0-1.212-.694-1.212-1.856s.3-1.857 1.213-1.858',
|
||||||
|
})
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import {createSinglePathSVG} from '../TEMPLATE'
|
||||||
|
|
||||||
|
export const StandardSite = createSinglePathSVG({
|
||||||
|
path: 'M11.862 3.042a.25.25 0 0 1 .276 0c.098.066.15.712.255 2.004l.036.45h2.484a3.56 3.56 0 0 1 3.559 3.559v.613h-1.228v-.613a2.33 2.33 0 0 0-2.331-2.332H12.53c.11 1.243.22 1.965.545 2.557a3.93 3.93 0 0 0 1.519 1.537c.727.41 1.66.5 3.526.682l1.025.1c1.165.114 1.748.17 1.813.268a.24.24 0 0 1-.002.273c-.066.097-.65.144-1.817.24l-.668.054v2.512a3.56 3.56 0 0 1-3.56 3.559h-2.486l-.033.42c-.104 1.31-.157 1.966-.255 2.033a.25.25 0 0 1-.276 0c-.098-.066-.15-.722-.255-2.033l-.033-.42H9.022a3.56 3.56 0 0 1-3.56-3.56v-.613H6.69v.613a2.33 2.33 0 0 0 2.332 2.332h2.452c-.112-1.29-.223-2.034-.557-2.639a3.93 3.93 0 0 0-1.548-1.544c-.74-.406-1.69-.484-3.59-.64l-.92-.074c-1.166-.096-1.75-.143-1.816-.24a.24.24 0 0 1-.002-.273c.065-.097.647-.154 1.813-.268l.609-.06V9.056a3.56 3.56 0 0 1 3.559-3.56h2.549l.036-.449c.105-1.292.157-1.938.255-2.004m5.382 9.495c-1.272.114-2.011.227-2.613.557a3.93 3.93 0 0 0-1.548 1.544c-.334.605-.445 1.349-.557 2.64h2.387a2.33 2.33 0 0 0 2.331-2.332zM9.022 6.723A2.33 2.33 0 0 0 6.69 9.055v2.364c1.342-.138 2.102-.257 2.715-.602a3.93 3.93 0 0 0 1.52-1.537c.323-.592.434-1.314.544-2.557z',
|
||||||
|
})
|
||||||
@@ -442,6 +442,7 @@ async function resolveMedia(
|
|||||||
title: resolvedLink.title,
|
title: resolvedLink.title,
|
||||||
description: resolvedLink.description,
|
description: resolvedLink.description,
|
||||||
thumb: blob,
|
thumb: blob,
|
||||||
|
associatedRefs: resolvedLink.associatedRefs,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import {
|
|||||||
} from '#/lib/strings/url-helpers'
|
} from '#/lib/strings/url-helpers'
|
||||||
import {type ComposerImage} from '#/state/gallery'
|
import {type ComposerImage} from '#/state/gallery'
|
||||||
import {createComposerImage} from '#/state/gallery'
|
import {createComposerImage} from '#/state/gallery'
|
||||||
import {type PublicationViewExternalSource} from '#/components/Post/Embed/ExternalEmbed/PublicationEmbed/types'
|
|
||||||
import {type Gif} from '#/features/gifPicker/types'
|
import {type Gif} from '#/features/gifPicker/types'
|
||||||
import {createGIFDescription} from '../gif-alt-text'
|
import {createGIFDescription} from '../gif-alt-text'
|
||||||
|
|
||||||
@@ -40,12 +39,8 @@ type ResolvedExternalLink = {
|
|||||||
* The AT-URI of the Atmosphere record representing this external content, if
|
* The AT-URI of the Atmosphere record representing this external content, if
|
||||||
* it exists. Example: a site.standard.document record.
|
* it exists. Example: a site.standard.document record.
|
||||||
*/
|
*/
|
||||||
associatedRecord?: LinkMeta['associatedRecord']
|
associatedRefs?: LinkMeta['associatedRefs']
|
||||||
// APP-2160 / APP-2155: populated by Cardyb when the URL resolves to a
|
view?: LinkMeta['view']
|
||||||
// standard.site publication. Until APP-2155 ships these stay undefined.
|
|
||||||
source?: PublicationViewExternalSource
|
|
||||||
createdAt?: string
|
|
||||||
readingTime?: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ResolvedPostRecord = {
|
type ResolvedPostRecord = {
|
||||||
@@ -251,7 +246,8 @@ async function resolveExternal(
|
|||||||
title: result.title ?? '',
|
title: result.title ?? '',
|
||||||
description: result.description ?? '',
|
description: result.description ?? '',
|
||||||
thumb: result.image ? await imageToThumb(result.image) : undefined,
|
thumb: result.image ? await imageToThumb(result.image) : undefined,
|
||||||
associatedRecord: result.associatedRecord,
|
associatedRefs: result.associatedRefs,
|
||||||
|
view: result.view,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {type BskyAgent} from '@atproto/api'
|
import {type AppBskyEmbedExternal, type BskyAgent} from '@atproto/api'
|
||||||
|
|
||||||
import {LINK_META_PROXY} from '#/lib/constants'
|
import {LINK_META_PROXY} from '#/lib/constants'
|
||||||
import {getGiphyMetaUri} from '#/lib/strings/embed-player'
|
import {getGiphyMetaUri} from '#/lib/strings/embed-player'
|
||||||
@@ -26,7 +26,8 @@ export interface LinkMeta {
|
|||||||
* The AT-URI of the Atmosphere record representing this external content, if
|
* The AT-URI of the Atmosphere record representing this external content, if
|
||||||
* it exists. Example: a site.standard.document record.
|
* it exists. Example: a site.standard.document record.
|
||||||
*/
|
*/
|
||||||
associatedRecord?: string
|
associatedRefs?: AppBskyEmbedExternal.External['associatedRefs']
|
||||||
|
view?: AppBskyEmbedExternal.View
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getLinkMeta(
|
export async function getLinkMeta(
|
||||||
@@ -94,7 +95,8 @@ export async function getLinkMeta(
|
|||||||
meta.description = body.description
|
meta.description = body.description
|
||||||
meta.image = body.image
|
meta.image = body.image
|
||||||
meta.title = body.title
|
meta.title = body.title
|
||||||
meta.associatedRecord = body.associated_record
|
meta.associatedRefs = body.associated_refs
|
||||||
|
meta.view = body.view || body.external_view
|
||||||
if (shouldFollowRedirect) {
|
if (shouldFollowRedirect) {
|
||||||
meta.url = body.url
|
meta.url = body.url
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,21 +5,23 @@ export function niceDate(
|
|||||||
i18n: I18n,
|
i18n: I18n,
|
||||||
date: number | string | Date,
|
date: number | string | Date,
|
||||||
dateStyle: 'short' | 'medium' | 'long' | 'full' | 'dot separated' = 'long',
|
dateStyle: 'short' | 'medium' | 'long' | 'full' | 'dot separated' = 'long',
|
||||||
|
timeStyle: 'short' | 'medium' | 'long' | 'full' | 'none' = 'short',
|
||||||
) {
|
) {
|
||||||
|
const ts = timeStyle === 'none' ? undefined : timeStyle
|
||||||
const d = new Date(date)
|
const d = new Date(date)
|
||||||
|
|
||||||
if (dateStyle === 'dot separated') {
|
if (dateStyle === 'dot separated') {
|
||||||
return i18n._(
|
return i18n._(
|
||||||
msg({
|
msg({
|
||||||
context: 'date and time formatted like this: [time] · [date]',
|
context: 'date and time formatted like this: [time] · [date]',
|
||||||
message: `${i18n.date(d, {timeStyle: 'short'})} · ${i18n.date(d, {dateStyle: 'medium'})}`,
|
message: `${i18n.date(d, {timeStyle: ts})} · ${i18n.date(d, {dateStyle: 'medium'})}`,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return i18n.date(d, {
|
return i18n.date(d, {
|
||||||
dateStyle,
|
dateStyle,
|
||||||
timeStyle: 'short',
|
timeStyle: ts,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,12 @@ import {ExternalEmbedRemoveBtn} from '#/view/com/composer/ExternalEmbedRemoveBtn
|
|||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
import {ExternalEmbed} from '#/components/Post/Embed/ExternalEmbed'
|
import {ExternalEmbed} from '#/components/Post/Embed/ExternalEmbed'
|
||||||
import {PublicationEmbed} from '#/components/Post/Embed/ExternalEmbed/PublicationEmbed'
|
|
||||||
import {type PublicationViewExternal} from '#/components/Post/Embed/ExternalEmbed/PublicationEmbed/types'
|
|
||||||
import {ModeratedFeedEmbed} from '#/components/Post/Embed/FeedEmbed'
|
import {ModeratedFeedEmbed} from '#/components/Post/Embed/FeedEmbed'
|
||||||
import {ModeratedListEmbed} from '#/components/Post/Embed/ListEmbed'
|
import {ModeratedListEmbed} from '#/components/Post/Embed/ListEmbed'
|
||||||
|
import {
|
||||||
|
isStandardSiteEmbed,
|
||||||
|
StandardSiteEmbed,
|
||||||
|
} from '#/components/Post/Embed/StandardSiteEmbed'
|
||||||
import {Embed as StarterPackEmbed} from '#/components/StarterPack/StarterPackCard'
|
import {Embed as StarterPackEmbed} from '#/components/StarterPack/StarterPackCard'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {type Gif} from '#/features/gifPicker/types'
|
import {type Gif} from '#/features/gifPicker/types'
|
||||||
@@ -88,21 +90,19 @@ export const ExternalEmbedLink = ({
|
|||||||
const linkComponent = useMemo(() => {
|
const linkComponent = useMemo(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
if (data.type === 'external') {
|
if (data.type === 'external') {
|
||||||
if (data.source) {
|
if (data.view && isStandardSiteEmbed(data.view.external)) {
|
||||||
const publicationLink: PublicationViewExternal = {
|
|
||||||
uri,
|
|
||||||
title: data.title || uri,
|
|
||||||
description: data.description,
|
|
||||||
thumb: data.thumb?.source.path,
|
|
||||||
createdAt: data.createdAt,
|
|
||||||
readingTime: data.readingTime,
|
|
||||||
source: data.source,
|
|
||||||
associatedRecord: data.associatedRecord
|
|
||||||
? {uri: data.associatedRecord, cid: ''}
|
|
||||||
: undefined,
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<PublicationEmbed link={publicationLink} source={data.source} />
|
<StandardSiteEmbed
|
||||||
|
hideSubscribe
|
||||||
|
view={{
|
||||||
|
...data.view?.external,
|
||||||
|
title: data.view?.external?.title || data.title || uri,
|
||||||
|
uri,
|
||||||
|
description:
|
||||||
|
data.view?.external?.description || data.description,
|
||||||
|
thumb: data.view?.external?.thumb || data.thumb?.source.path,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ interface UserAvatarProps extends BaseUserAvatarProps {
|
|||||||
noBorder?: boolean
|
noBorder?: boolean
|
||||||
onLoad?: () => void
|
onLoad?: () => void
|
||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
|
extraAviStyle?: ViewStyle
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EditableUserAvatarProps extends BaseUserAvatarProps {
|
interface EditableUserAvatarProps extends BaseUserAvatarProps {
|
||||||
@@ -224,6 +225,7 @@ let UserAvatar = ({
|
|||||||
live,
|
live,
|
||||||
hideLiveBadge,
|
hideLiveBadge,
|
||||||
noBorder,
|
noBorder,
|
||||||
|
extraAviStyle,
|
||||||
}: UserAvatarProps): React.ReactNode => {
|
}: UserAvatarProps): React.ReactNode => {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const finalShape = overrideShape ?? (type === 'user' ? 'circle' : 'square')
|
const finalShape = overrideShape ?? (type === 'user' ? 'circle' : 'square')
|
||||||
@@ -241,8 +243,9 @@ let UserAvatar = ({
|
|||||||
height: size,
|
height: size,
|
||||||
borderRadius,
|
borderRadius,
|
||||||
backgroundColor: t.palette.contrast_25,
|
backgroundColor: t.palette.contrast_25,
|
||||||
|
...extraAviStyle,
|
||||||
}
|
}
|
||||||
}, [finalShape, size, t])
|
}, [finalShape, size, t, extraAviStyle])
|
||||||
|
|
||||||
const borderStyle = useMemo(() => {
|
const borderStyle = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
|
|||||||
Reference in New Issue
Block a user