Lex refactor (#362)

* Remove the hackcheck for upgrades

* Rename the PostEmbeds folder to match the codebase style

* Updates to latest lex refactor

* Update to use new bsky agent

* Update to use api package's richtext library

* Switch to upsertProfile

* Add TextEncoder/TextDecoder polyfill

* Add Intl.Segmenter polyfill

* Update composer to calculate lengths by grapheme

* Fix detox

* Fix login in e2e

* Create account e2e passing

* Implement an e2e mocking framework

* Don't use private methods on mobx models as mobx can't track them

* Add tooling for e2e-specific builds and add e2e media-picker mock

* Add some tests and fix some bugs around profile editing

* Add shell tests

* Add home screen tests

* Add thread screen tests

* Add tests for other user profile screens

* Add search screen tests

* Implement profile imagery change tools and tests

* Update to new embed behaviors

* Add post tests

* Fix to profile-screen test

* Fix session resumption

* Update web composer to new api

* 1.11.0

* Fix pagination cursor parameters

* Add quote posts to notifications

* Fix embed layouts

* Remove youtube inline player and improve tap handling on link cards

* Reset minimal shell mode on all screen loads and feed swipes (close #299)

* Update podfile.lock

* Improve post notfound UI (close #366)

* Bump atproto packages
This commit is contained in:
Paul Frazee 2023-03-31 13:17:26 -05:00 committed by GitHub
parent 19f3a2fa92
commit a3334a01a2
133 changed files with 3103 additions and 2839 deletions

View file

@ -0,0 +1,62 @@
import React from 'react'
import {Text} from '../text/Text'
import {AutoSizedImage} from '../images/AutoSizedImage'
import {StyleSheet, View} from 'react-native'
import {usePalette} from 'lib/hooks/usePalette'
import {AppBskyEmbedExternal} from '@atproto/api'
export const ExternalLinkEmbed = ({
link,
imageChild,
}: {
link: AppBskyEmbedExternal.ViewExternal
imageChild?: React.ReactNode
}) => {
const pal = usePalette('default')
return (
<>
{link.thumb ? (
<AutoSizedImage uri={link.thumb} style={styles.extImage}>
{imageChild}
</AutoSizedImage>
) : undefined}
<View style={styles.extInner}>
<Text type="md-bold" numberOfLines={2} style={[pal.text]}>
{link.title || link.uri}
</Text>
<Text
type="sm"
numberOfLines={1}
style={[pal.textLight, styles.extUri]}>
{link.uri}
</Text>
{link.description ? (
<Text
type="sm"
numberOfLines={2}
style={[pal.text, styles.extDescription]}>
{link.description}
</Text>
) : undefined}
</View>
</>
)
}
const styles = StyleSheet.create({
extInner: {
padding: 10,
},
extImage: {
borderTopLeftRadius: 6,
borderTopRightRadius: 6,
width: '100%',
maxHeight: 200,
},
extUri: {
marginTop: 2,
},
extDescription: {
marginTop: 4,
},
})

View file

@ -0,0 +1,81 @@
import React from 'react'
import {StyleProp, StyleSheet, ViewStyle} from 'react-native'
import {AppBskyEmbedImages, AppBskyEmbedRecordWithMedia} from '@atproto/api'
import {AtUri} from '../../../../third-party/uri'
import {PostMeta} from '../PostMeta'
import {Link} from '../Link'
import {Text} from '../text/Text'
import {usePalette} from 'lib/hooks/usePalette'
import {ComposerOptsQuote} from 'state/models/ui/shell'
import {PostEmbeds} from '.'
export function QuoteEmbed({
quote,
style,
}: {
quote: ComposerOptsQuote
style?: StyleProp<ViewStyle>
}) {
const pal = usePalette('default')
const itemUrip = new AtUri(quote.uri)
const itemHref = `/profile/${quote.author.handle}/post/${itemUrip.rkey}`
const itemTitle = `Post by ${quote.author.handle}`
const isEmpty = React.useMemo(
() => quote.text.trim().length === 0,
[quote.text],
)
const imagesEmbed = React.useMemo(
() =>
quote.embeds?.find(
embed =>
AppBskyEmbedImages.isView(embed) ||
AppBskyEmbedRecordWithMedia.isView(embed),
),
[quote.embeds],
)
return (
<Link
style={[styles.container, pal.border, style]}
href={itemHref}
title={itemTitle}>
<PostMeta
authorAvatar={quote.author.avatar}
authorHandle={quote.author.handle}
authorDisplayName={quote.author.displayName}
postHref={itemHref}
timestamp={quote.indexedAt}
/>
<Text type="post-text" style={pal.text} numberOfLines={6}>
{isEmpty ? (
<Text style={pal.link} lineHeight={1.5}>
View post
</Text>
) : (
quote.text
)}
</Text>
{AppBskyEmbedImages.isView(imagesEmbed) && (
<PostEmbeds embed={imagesEmbed} />
)}
{AppBskyEmbedRecordWithMedia.isView(imagesEmbed) && (
<PostEmbeds embed={imagesEmbed.media} />
)}
</Link>
)
}
export default QuoteEmbed
const styles = StyleSheet.create({
container: {
borderRadius: 8,
paddingVertical: 8,
paddingHorizontal: 12,
borderWidth: 1,
},
quotePost: {
flex: 1,
paddingLeft: 13,
paddingRight: 8,
},
})

View file

@ -0,0 +1,55 @@
import React from 'react'
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import {usePalette} from 'lib/hooks/usePalette'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {ExternalLinkEmbed} from './ExternalLinkEmbed'
import {AppBskyEmbedExternal} from '@atproto/api'
import {Link} from '../Link'
export const YoutubeEmbed = ({
link,
style,
}: {
link: AppBskyEmbedExternal.ViewExternal
style?: StyleProp<ViewStyle>
}) => {
const pal = usePalette('default')
const imageChild = (
<View style={styles.playButton}>
<FontAwesomeIcon icon="play" size={24} color="white" />
</View>
)
return (
<Link
style={[styles.extOuter, pal.view, pal.border, style]}
href={link.uri}
noFeedback>
<ExternalLinkEmbed link={link} imageChild={imageChild} />
</Link>
)
}
const styles = StyleSheet.create({
extOuter: {
borderWidth: 1,
borderRadius: 8,
},
playButton: {
position: 'absolute',
alignSelf: 'center',
alignItems: 'center',
top: '44%',
justifyContent: 'center',
backgroundColor: 'black',
padding: 10,
borderRadius: 50,
opacity: 0.8,
},
webView: {
alignItems: 'center',
alignContent: 'center',
justifyContent: 'center',
},
})

View file

@ -0,0 +1,198 @@
import React from 'react'
import {
StyleSheet,
StyleProp,
View,
ViewStyle,
Image as RNImage,
} from 'react-native'
import {
AppBskyEmbedImages,
AppBskyEmbedExternal,
AppBskyEmbedRecord,
AppBskyEmbedRecordWithMedia,
AppBskyFeedPost,
} from '@atproto/api'
import {Link} from '../Link'
import {AutoSizedImage} from '../images/AutoSizedImage'
import {ImageLayoutGrid} from '../images/ImageLayoutGrid'
import {ImagesLightbox} from 'state/models/ui/shell'
import {useStores} from 'state/index'
import {usePalette} from 'lib/hooks/usePalette'
import {saveImageModal} from 'lib/media/manip'
import {YoutubeEmbed} from './YoutubeEmbed'
import {ExternalLinkEmbed} from './ExternalLinkEmbed'
import {getYoutubeVideoId} from 'lib/strings/url-helpers'
import QuoteEmbed from './QuoteEmbed'
type Embed =
| AppBskyEmbedRecord.View
| AppBskyEmbedImages.View
| AppBskyEmbedExternal.View
| AppBskyEmbedRecordWithMedia.View
| {$type: string; [k: string]: unknown}
export function PostEmbeds({
embed,
style,
}: {
embed?: Embed
style?: StyleProp<ViewStyle>
}) {
const pal = usePalette('default')
const store = useStores()
if (
AppBskyEmbedRecordWithMedia.isView(embed) &&
AppBskyEmbedRecord.isViewRecord(embed.record.record) &&
AppBskyFeedPost.isRecord(embed.record.record.value) &&
AppBskyFeedPost.validateRecord(embed.record.record.value).success
) {
return (
<View style={[styles.stackContainer, style]}>
<PostEmbeds embed={embed.media} />
<QuoteEmbed
quote={{
author: embed.record.record.author,
cid: embed.record.record.cid,
uri: embed.record.record.uri,
indexedAt: embed.record.record.indexedAt,
text: embed.record.record.value.text,
embeds: embed.record.record.embeds,
}}
/>
</View>
)
}
if (AppBskyEmbedRecord.isView(embed)) {
if (
AppBskyEmbedRecord.isViewRecord(embed.record) &&
AppBskyFeedPost.isRecord(embed.record.value) &&
AppBskyFeedPost.validateRecord(embed.record.value).success
) {
return (
<QuoteEmbed
quote={{
author: embed.record.author,
cid: embed.record.cid,
uri: embed.record.uri,
indexedAt: embed.record.indexedAt,
text: embed.record.value.text,
embeds: embed.record.embeds,
}}
style={style}
/>
)
}
}
if (AppBskyEmbedImages.isView(embed)) {
if (embed.images.length > 0) {
const uris = embed.images.map(img => img.fullsize)
const openLightbox = (index: number) => {
store.shell.openLightbox(new ImagesLightbox(uris, index))
}
const onLongPress = (index: number) => {
saveImageModal({uri: uris[index]})
}
const onPressIn = (index: number) => {
const firstImageToShow = uris[index]
RNImage.prefetch(firstImageToShow)
uris.forEach(uri => {
if (firstImageToShow !== uri) {
// First image already prefeched above
RNImage.prefetch(uri)
}
})
}
if (embed.images.length === 4) {
return (
<View style={[styles.imagesContainer, style]}>
<ImageLayoutGrid
type="four"
uris={embed.images.map(img => img.thumb)}
onPress={openLightbox}
onLongPress={onLongPress}
onPressIn={onPressIn}
/>
</View>
)
} else if (embed.images.length === 3) {
return (
<View style={[styles.imagesContainer, style]}>
<ImageLayoutGrid
type="three"
uris={embed.images.map(img => img.thumb)}
onPress={openLightbox}
onLongPress={onLongPress}
onPressIn={onPressIn}
/>
</View>
)
} else if (embed.images.length === 2) {
return (
<View style={[styles.imagesContainer, style]}>
<ImageLayoutGrid
type="two"
uris={embed.images.map(img => img.thumb)}
onPress={openLightbox}
onLongPress={onLongPress}
onPressIn={onPressIn}
/>
</View>
)
} else {
return (
<View style={[styles.imagesContainer, style]}>
<AutoSizedImage
uri={embed.images[0].thumb}
onPress={() => openLightbox(0)}
onLongPress={() => onLongPress(0)}
onPressIn={() => onPressIn(0)}
style={styles.singleImage}
/>
</View>
)
}
}
}
if (AppBskyEmbedExternal.isView(embed)) {
const link = embed.external
const youtubeVideoId = getYoutubeVideoId(link.uri)
if (youtubeVideoId) {
return <YoutubeEmbed link={link} style={style} />
}
return (
<Link
style={[styles.extOuter, pal.view, pal.border, style]}
href={link.uri}
noFeedback>
<ExternalLinkEmbed link={link} />
</Link>
)
}
return <View />
}
const styles = StyleSheet.create({
stackContainer: {
gap: 6,
},
imagesContainer: {
marginTop: 4,
},
singleImage: {
borderRadius: 8,
maxHeight: 500,
},
extOuter: {
borderWidth: 1,
borderRadius: 8,
marginTop: 4,
},
})