Compare commits

...

6 Commits

Author SHA1 Message Date
Eric Bailey 6363d9da81 Adjust spacing on mobile 2024-10-31 14:09:13 -05:00
Eric Bailey 12a69ae2d1 Ok nevermind 2024-10-31 12:29:17 -05:00
Eric Bailey bf3e3eac7f Fix types, add default 2024-10-31 12:22:29 -05:00
Eric Bailey 644c747a95 Handle errors, short posts 2024-10-31 12:04:45 -05:00
Eric Bailey 265d0f2122 Clean up 2024-10-31 11:36:34 -05:00
Eric Bailey d9fcb4c217 Add GitHub Gist embed support 2024-10-31 10:52:24 -05:00
5 changed files with 341 additions and 14 deletions
+4
View File
@@ -38,6 +38,10 @@ export function setFontFamily(fontFamily: Device['fontFamily']) {
* Unused fonts are commented out, but the files are there if we need them.
*/
export function applyFonts(style: TextStyle, fontFamily: 'system' | 'theme') {
if (style.fontFamily === 'monospace') {
return
}
if (fontFamily === 'theme') {
if (isAndroid) {
style.fontFamily =
+12
View File
@@ -25,6 +25,7 @@ export const embedPlayerSources = [
'giphy',
'tenor',
'flickr',
'github',
] as const
export type EmbedPlayerSource = (typeof embedPlayerSources)[number]
@@ -45,6 +46,7 @@ export type EmbedPlayerType =
| 'giphy_gif'
| 'tenor_gif'
| 'flickr_album'
| 'github_gist'
export const externalEmbedLabels: Record<EmbedPlayerSource, string> = {
youtube: 'YouTube',
@@ -57,6 +59,7 @@ export const externalEmbedLabels: Record<EmbedPlayerSource, string> = {
appleMusic: 'Apple Music',
soundcloud: 'SoundCloud',
flickr: 'Flickr',
github: 'GitHub',
}
export interface EmbedPlayerParams {
@@ -448,6 +451,15 @@ export function parseEmbedPlayerFromUrl(
return undefined
}
}
if (urlp.hostname === 'gist.github.com') {
const id = urlp.pathname.replace(/^\//, '').split('/')[1]
return {
type: 'github_gist',
source: 'github',
playerUri: id,
}
}
}
export function getPlayerAspect({
+1
View File
@@ -105,6 +105,7 @@ const schema = z.object({
appleMusic: z.enum(externalEmbedOptions).optional(),
soundcloud: z.enum(externalEmbedOptions).optional(),
flickr: z.enum(externalEmbedOptions).optional(),
github: z.enum(externalEmbedOptions).optional(),
})
.optional(),
invites: z.object({
@@ -7,13 +7,17 @@ import {useLingui} from '@lingui/react'
import {parseAltFromGIFDescription} from '#/lib/gif-alt-text'
import {shareUrl} from '#/lib/sharing'
import {parseEmbedPlayerFromUrl} from '#/lib/strings/embed-player'
import {
EmbedPlayerParams,
parseEmbedPlayerFromUrl,
} from '#/lib/strings/embed-player'
import {toNiceDomain} from '#/lib/strings/url-helpers'
import {isNative} from '#/platform/detection'
import {useExternalEmbedsPrefs} from '#/state/preferences'
import {ExternalGifEmbed} from '#/view/com/util/post-embeds/ExternalGifEmbed'
import {ExternalPlayer} from '#/view/com/util/post-embeds/ExternalPlayerEmbed'
import {GifEmbed} from '#/view/com/util/post-embeds/GifEmbed'
import {GithubGist} from '#/view/com/util/post-embeds/GithubGist'
import {atoms as a, useTheme} from '#/alf'
import {Divider} from '#/components/Divider'
import {Earth_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
@@ -31,11 +35,7 @@ export const ExternalLinkEmbed = ({
style?: StyleProp<ViewStyle>
hideAlt?: boolean
}) => {
const {_} = useLingui()
const t = useTheme()
const externalEmbedPrefs = useExternalEmbedsPrefs()
const niceUrl = toNiceDomain(link.uri)
const imageUri = link.thumb
const embedPlayerParams = React.useMemo(() => {
const params = parseEmbedPlayerFromUrl(link.uri)
@@ -43,13 +43,6 @@ export const ExternalLinkEmbed = ({
return params
}
}, [link.uri, externalEmbedPrefs])
const hasMedia = Boolean(imageUri || embedPlayerParams)
const onShareExternal = useCallback(() => {
if (link.uri && isNative) {
shareUrl(link.uri)
}
}, [link.uri])
if (embedPlayerParams?.source === 'tenor') {
const parsedAlt = parseAltFromGIFDescription(link.description)
@@ -66,6 +59,49 @@ export const ExternalLinkEmbed = ({
)
}
if (
embedPlayerParams?.source === 'github' &&
embedPlayerParams?.type === 'github_gist'
) {
return (
<View style={style}>
<GithubGist info={link} id={embedPlayerParams.playerUri} />
</View>
)
}
return (
<ExternalLinkEmbedCard
link={link}
onOpen={onOpen}
embedPlayerParams={embedPlayerParams}
style={style}
/>
)
}
export function ExternalLinkEmbedCard({
link,
onOpen,
embedPlayerParams,
style,
}: {
link: AppBskyEmbedExternal.ViewExternal
onOpen?: () => void
embedPlayerParams?: EmbedPlayerParams
style?: StyleProp<ViewStyle>
}) {
const t = useTheme()
const {_} = useLingui()
const niceUrl = toNiceDomain(link.uri)
const hasMedia = Boolean(link.thumb || embedPlayerParams)
const onShareExternal = useCallback(() => {
if (link.uri && isNative) {
shareUrl(link.uri)
}
}, [link.uri])
return (
<Link
label={link.title || _(msg`Open link to ${niceUrl}`)}
@@ -86,12 +122,12 @@ export const ExternalLinkEmbed = ({
? t.atoms.border_contrast_high
: t.atoms.border_contrast_low,
]}>
{imageUri && !embedPlayerParams ? (
{link.thumb && !embedPlayerParams ? (
<Image
style={{
aspectRatio: 1.91,
}}
source={{uri: imageUri}}
source={{uri: link.thumb}}
accessibilityIgnoresInvertColors
/>
) : undefined}
@@ -0,0 +1,274 @@
import React from 'react'
import {LayoutChangeEvent, ScrollView, View} from 'react-native'
import {LinearGradient} from 'expo-linear-gradient'
import {AppBskyEmbedExternal} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useQuery} from '@tanstack/react-query'
import {shareUrl} from '#/lib/sharing'
import {toNiceDomain} from '#/lib/strings/url-helpers'
import {isNative} from '#/platform/detection'
import {ExternalLinkEmbedCard} from '#/view/com/util/post-embeds/ExternalLinkEmbed'
import {atoms as a, useBreakpoints,useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {Divider} from '#/components/Divider'
import {ChevronBottom_Stroke2_Corner0_Rounded as Chevron} from '#/components/icons/Chevron'
import {CodeBrackets_Stroke2_Corner0_Rounded as Code} from '#/components/icons/CodeBrackets'
import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
import {Link} from '#/components/Link'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
const MAX_HEIGHT = 400
export function GithubGist({
info,
id,
}: {
info: AppBskyEmbedExternal.ViewExternal
id: string
}) {
const t = useTheme()
const {_} = useLingui()
const {gtMobile} = useBreakpoints()
const [footerHeight, setFooterHeight] = React.useState(97) // magic #
const [showMore, setShowMore] = React.useState<boolean | undefined>()
const {data: files, error} = useQuery({
queryKey: ['gist', id],
async queryFn() {
const url = `https://api.github.com/gists/${id}`
const gist = await fetch(url).then(res => res.json())
const files = Object.values(gist.files) as {
filename: string
type: 'text/markdown' | string
language: 'Markdown' | string
raw_url: string
size: number
truncated: boolean
content: string
}[]
return files
},
})
const onShareExternal = React.useCallback(() => {
if (info.uri && isNative) {
shareUrl(info.uri)
}
}, [info.uri])
const onFooterLayout = React.useCallback(
(e: LayoutChangeEvent) => {
setFooterHeight(e.nativeEvent.layout.height)
},
[setFooterHeight],
)
const onCodeLayout = React.useCallback(
(e: LayoutChangeEvent) => {
if (showMore !== undefined) return
const height = e.nativeEvent.layout.height
if (height < 400) {
setShowMore(true)
} else {
setShowMore(false)
}
},
[showMore, setShowMore],
)
const onShowMore = React.useCallback(() => {
setShowMore(true)
}, [setShowMore])
if (error) {
return <ExternalLinkEmbedCard link={info} />
}
return (
<View
style={[
a.transition_color,
a.flex_col,
a.rounded_md,
a.overflow_hidden,
a.w_full,
a.border,
t.atoms.border_contrast_low,
]}>
<View style={[t.atoms.bg_contrast_25]}>
{error ? null : !files ? ( // handled above
<View style={[gtMobile ? a.p_lg : a.p_md]}>
<Loader />
</View>
) : (
<View style={[a.relative]}>
<View style={[gtMobile ? a.pt_lg : a.pt_md, a.relative]}>
<ScrollView
style={[
a.overflow_hidden,
showMore === undefined || showMore === false
? {
maxHeight: MAX_HEIGHT,
}
: {},
]}>
<ScrollView
horizontal
onLayout={onCodeLayout}
style={[{paddingBottom: footerHeight}]}>
<View>
<View
style={[
a.flex_row,
a.align_center,
a.gap_xs,
gtMobile ? a.px_lg : a.px_md,
a.pb_md,
]}>
<Code
size="xs"
style={[
a.transition_color,
{color: t.palette.primary_500},
]}
/>
<Text
style={[
a.text_sm,
a.italic,
t.atoms.text_contrast_low,
]}>
{files[0].filename}
</Text>
</View>
<Text
style={[
gtMobile ? a.px_lg : a.px_md,
{
fontFamily: 'monospace',
},
]}>
{files[0].content.trimRight()}
</Text>
</View>
</ScrollView>
</ScrollView>
{showMore === false && <View style={[a.absolute, a.inset_0]} />}
</View>
<View
style={[
a.absolute,
a.inset_0,
gtMobile ? a.p_lg : a.p_md,
{top: 'auto'},
]}
onLayout={onFooterLayout}>
<LinearGradient
colors={['rgba(0, 17, 36,0)', 'rgba(0, 17, 36,0.5)']}
locations={[0, 1]}
start={{x: 0, y: 0}}
end={{x: 0, y: 1}}
style={[a.absolute, a.inset_0]}
/>
{showMore === false && (
<View style={[a.flex_row, a.justify_center, a.pb_md]}>
<Button
label={_(msg`Show more`)}
size="small"
variant="solid"
color="primary"
style={[
a.rounded_full,
{
paddingVertical: 6,
},
]}
onPress={onShowMore}>
<ButtonText>Show more</ButtonText>
<ButtonIcon icon={Chevron} size="sm" position="right" />
</Button>
</View>
)}
<Link
label={info.title || _(msg`Open this Gist`)}
to={info.uri}
onLongPress={onShareExternal}>
{({hovered}) => (
<View
style={[
a.flex_1,
a.pt_sm,
a.rounded_sm,
a.border,
{gap: 3},
t.atoms.bg,
hovered
? t.atoms.border_contrast_high
: t.atoms.border_contrast_low,
]}>
<View style={[{gap: 3}, a.pb_xs, a.px_md]}>
<Text
emoji
numberOfLines={2}
style={[a.text_md, a.font_bold, a.leading_snug]}>
{info.title || info.uri}
</Text>
</View>
<View style={[a.px_md]}>
<Divider
style={[
hovered
? t.atoms.border_contrast_high
: t.atoms.border_contrast_low,
]}
/>
<View
style={[
a.flex_row,
a.align_center,
a.gap_xs,
a.pb_sm,
{
paddingTop: 6, // off menu
},
]}>
<Globe
size="xs"
style={[
a.transition_color,
hovered
? t.atoms.text_contrast_high
: t.atoms.text_contrast_low,
]}
/>
<Text
numberOfLines={1}
style={[
a.transition_color,
a.text_xs,
a.leading_tight,
hovered
? t.atoms.text_contrast_high
: t.atoms.text_contrast_medium,
]}>
{toNiceDomain(info.uri)}
</Text>
</View>
</View>
</View>
)}
</Link>
</View>
</View>
)}
</View>
</View>
)
}