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

@ -1,4 +1,4 @@
import React, {useEffect, useRef, useState} from 'react'
import React from 'react'
import {observer} from 'mobx-react-lite'
import {
ActivityIndicator,
@ -13,6 +13,7 @@ import {
} from 'react-native'
import LinearGradient from 'react-native-linear-gradient'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {RichText} from '@atproto/api'
import {useAnalytics} from 'lib/analytics'
import {UserAutocompleteViewModel} from 'state/models/user-autocomplete-view'
import {ExternalEmbed} from './ExternalEmbed'
@ -30,11 +31,11 @@ import {SelectPhotoBtn} from './photos/SelectPhotoBtn'
import {OpenCameraBtn} from './photos/OpenCameraBtn'
import {SelectedPhotos} from './photos/SelectedPhotos'
import {usePalette} from 'lib/hooks/usePalette'
import QuoteEmbed from '../util/PostEmbeds/QuoteEmbed'
import QuoteEmbed from '../util/post-embeds/QuoteEmbed'
import {useExternalLinkFetch} from './useExternalLinkFetch'
import {isDesktopWeb} from 'platform/detection'
const MAX_TEXT_LENGTH = 256
const MAX_GRAPHEME_LENGTH = 300
export const ComposePost = observer(function ComposePost({
replyTo,
@ -50,17 +51,23 @@ export const ComposePost = observer(function ComposePost({
const {track} = useAnalytics()
const pal = usePalette('default')
const store = useStores()
const textInput = useRef<TextInputRef>(null)
const [isProcessing, setIsProcessing] = useState(false)
const [processingState, setProcessingState] = useState('')
const [error, setError] = useState('')
const [text, setText] = useState('')
const [quote, setQuote] = useState<ComposerOpts['quote'] | undefined>(
const textInput = React.useRef<TextInputRef>(null)
const [isProcessing, setIsProcessing] = React.useState(false)
const [processingState, setProcessingState] = React.useState('')
const [error, setError] = React.useState('')
const [richtext, setRichText] = React.useState(new RichText({text: ''}))
const graphemeLength = React.useMemo(
() => richtext.graphemeLength,
[richtext],
)
const [quote, setQuote] = React.useState<ComposerOpts['quote'] | undefined>(
initQuote,
)
const {extLink, setExtLink} = useExternalLinkFetch({setQuote})
const [suggestedLinks, setSuggestedLinks] = useState<Set<string>>(new Set())
const [selectedPhotos, setSelectedPhotos] = useState<string[]>([])
const [suggestedLinks, setSuggestedLinks] = React.useState<Set<string>>(
new Set(),
)
const [selectedPhotos, setSelectedPhotos] = React.useState<string[]>([])
const autocompleteView = React.useMemo<UserAutocompleteViewModel>(
() => new UserAutocompleteViewModel(store),
@ -78,11 +85,11 @@ export const ComposePost = observer(function ComposePost({
}, [textInput, onClose])
// initial setup
useEffect(() => {
React.useEffect(() => {
autocompleteView.setup()
}, [autocompleteView])
useEffect(() => {
React.useEffect(() => {
// HACK
// wait a moment before focusing the input to resolve some layout bugs with the keyboard-avoiding-view
// -prf
@ -132,18 +139,18 @@ export const ComposePost = observer(function ComposePost({
if (isProcessing) {
return
}
if (text.length > MAX_TEXT_LENGTH) {
if (richtext.graphemeLength > MAX_GRAPHEME_LENGTH) {
return
}
setError('')
if (text.trim().length === 0 && selectedPhotos.length === 0) {
if (richtext.text.trim().length === 0 && selectedPhotos.length === 0) {
setError('Did you want to say anything?')
return false
}
setIsProcessing(true)
try {
await apilib.post(store, {
rawText: text,
rawText: richtext.text,
replyTo: replyTo?.uri,
images: selectedPhotos,
quote: quote,
@ -172,7 +179,7 @@ export const ComposePost = observer(function ComposePost({
Toast.show(`Your ${replyTo ? 'reply' : 'post'} has been published`)
}, [
isProcessing,
text,
richtext,
setError,
setIsProcessing,
replyTo,
@ -187,7 +194,7 @@ export const ComposePost = observer(function ComposePost({
track,
])
const canPost = text.length <= MAX_TEXT_LENGTH
const canPost = graphemeLength <= MAX_GRAPHEME_LENGTH
const selectTextInputPlaceholder = replyTo
? 'Write your reply'
@ -215,7 +222,7 @@ export const ComposePost = observer(function ComposePost({
</View>
) : canPost ? (
<TouchableOpacity
testID="composerPublishButton"
testID="composerPublishBtn"
onPress={onPressPublish}>
<LinearGradient
colors={[gradients.blueLight.start, gradients.blueLight.end]}
@ -271,42 +278,41 @@ export const ComposePost = observer(function ComposePost({
<UserAvatar avatar={store.me.avatar} size={50} />
<TextInput
ref={textInput}
text={text}
richtext={richtext}
placeholder={selectTextInputPlaceholder}
suggestedLinks={suggestedLinks}
autocompleteView={autocompleteView}
onTextChanged={setText}
setRichText={setRichText}
onPhotoPasted={onPhotoPasted}
onSuggestedLinksChanged={setSuggestedLinks}
onError={setError}
/>
</View>
{quote ? (
<View style={s.mt5}>
<QuoteEmbed quote={quote} />
</View>
) : undefined}
<SelectedPhotos
selectedPhotos={selectedPhotos}
onSelectPhotos={onSelectPhotos}
/>
{!selectedPhotos.length && extLink && (
{selectedPhotos.length === 0 && extLink && (
<ExternalEmbed
link={extLink}
onRemove={() => setExtLink(undefined)}
/>
)}
{quote ? (
<View style={s.mt5}>
<QuoteEmbed quote={quote} />
</View>
) : undefined}
</ScrollView>
{!extLink &&
selectedPhotos.length === 0 &&
suggestedLinks.size > 0 &&
!quote ? (
suggestedLinks.size > 0 ? (
<View style={s.mb5}>
{Array.from(suggestedLinks).map(url => (
<TouchableOpacity
key={`suggested-${url}`}
testID="addLinkCardBtn"
style={[pal.borderDark, styles.addExtLinkBtn]}
onPress={() => onPressAddLinkCard(url)}>
<Text style={pal.text}>
@ -318,17 +324,17 @@ export const ComposePost = observer(function ComposePost({
) : null}
<View style={[pal.border, styles.bottomBar]}>
<SelectPhotoBtn
enabled={!quote && selectedPhotos.length < 4}
enabled={selectedPhotos.length < 4}
selectedPhotos={selectedPhotos}
onSelectPhotos={setSelectedPhotos}
/>
<OpenCameraBtn
enabled={!quote && selectedPhotos.length < 4}
enabled={selectedPhotos.length < 4}
selectedPhotos={selectedPhotos}
onSelectPhotos={setSelectedPhotos}
/>
<View style={s.flex1} />
<CharProgress count={text.length} />
<CharProgress count={graphemeLength} />
</View>
</SafeAreaView>
</TouchableWithoutFeedback>
@ -408,6 +414,7 @@ const styles = StyleSheet.create({
borderRadius: 24,
paddingHorizontal: 16,
paddingVertical: 12,
marginHorizontal: 10,
marginBottom: 4,
},
bottomBar: {

View file

@ -8,26 +8,24 @@ import ProgressPie from 'react-native-progress/Pie'
import {s} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
const MAX_TEXT_LENGTH = 256
const DANGER_TEXT_LENGTH = MAX_TEXT_LENGTH
const MAX_LENGTH = 300
const DANGER_LENGTH = MAX_LENGTH
export function CharProgress({count}: {count: number}) {
const pal = usePalette('default')
const textColor = count > DANGER_TEXT_LENGTH ? '#e60000' : pal.colors.text
const circleColor = count > DANGER_TEXT_LENGTH ? '#e60000' : pal.colors.link
const textColor = count > DANGER_LENGTH ? '#e60000' : pal.colors.text
const circleColor = count > DANGER_LENGTH ? '#e60000' : pal.colors.link
return (
<>
<Text style={[s.mr10, {color: textColor}]}>
{MAX_TEXT_LENGTH - count}
</Text>
<Text style={[s.mr10, {color: textColor}]}>{MAX_LENGTH - count}</Text>
<View>
{count > DANGER_TEXT_LENGTH ? (
{count > DANGER_LENGTH ? (
<ProgressPie
size={30}
borderWidth={4}
borderColor={circleColor}
color={circleColor}
progress={Math.min((count - MAX_TEXT_LENGTH) / MAX_TEXT_LENGTH, 1)}
progress={Math.min((count - MAX_LENGTH) / MAX_LENGTH, 1)}
/>
) : (
<ProgressCircle
@ -35,7 +33,7 @@ export function CharProgress({count}: {count: number}) {
borderWidth={1}
borderColor={pal.colors.border}
color={circleColor}
progress={count / MAX_TEXT_LENGTH}
progress={count / MAX_LENGTH}
/>
)}
</View>

View file

@ -76,7 +76,11 @@ export function OpenCameraBtn({
hitSlop={HITSLOP}>
<FontAwesomeIcon
icon="camera"
style={(enabled ? pal.link : pal.textLight) as FontAwesomeIconStyle}
style={
(enabled
? pal.link
: [pal.textLight, s.dimmed]) as FontAwesomeIconStyle
}
size={24}
/>
</TouchableOpacity>

View file

@ -86,7 +86,11 @@ export function SelectPhotoBtn({
hitSlop={HITSLOP}>
<FontAwesomeIcon
icon={['far', 'image']}
style={(enabled ? pal.link : pal.textLight) as FontAwesomeIconStyle}
style={
(enabled
? pal.link
: [pal.textLight, s.dimmed]) as FontAwesomeIconStyle
}
size={24}
/>
</TouchableOpacity>

View file

@ -9,13 +9,13 @@ import PasteInput, {
PastedFile,
PasteInputRef,
} from '@mattermost/react-native-paste-input'
import {AppBskyRichtextFacet, RichText} from '@atproto/api'
import isEqual from 'lodash.isequal'
import {UserAutocompleteViewModel} from 'state/models/user-autocomplete-view'
import {Autocomplete} from './mobile/Autocomplete'
import {Text} from 'view/com/util/text/Text'
import {useStores} from 'state/index'
import {cleanError} from 'lib/strings/errors'
import {detectLinkables, extractEntities} from 'lib/strings/rich-text-detection'
import {getImageDim} from 'lib/media/manip'
import {cropAndCompressFlow} from 'lib/media/picker'
import {getMentionAt, insertMentionAt} from 'lib/strings/mention-manip'
@ -33,11 +33,11 @@ export interface TextInputRef {
}
interface TextInputProps {
text: string
richtext: RichText
placeholder: string
suggestedLinks: Set<string>
autocompleteView: UserAutocompleteViewModel
onTextChanged: (v: string) => void
setRichText: (v: RichText) => void
onPhotoPasted: (uri: string) => void
onSuggestedLinksChanged: (uris: Set<string>) => void
onError: (err: string) => void
@ -51,11 +51,11 @@ interface Selection {
export const TextInput = React.forwardRef(
(
{
text,
richtext,
placeholder,
suggestedLinks,
autocompleteView,
onTextChanged,
setRichText,
onPhotoPasted,
onSuggestedLinksChanged,
onError,
@ -92,7 +92,9 @@ export const TextInput = React.forwardRef(
const onChangeText = React.useCallback(
(newText: string) => {
onTextChanged(newText)
const newRt = new RichText({text: newText})
newRt.detectFacetsWithoutResolution()
setRichText(newRt)
const prefix = getMentionAt(
newText,
@ -105,20 +107,21 @@ export const TextInput = React.forwardRef(
autocompleteView.setActive(false)
}
const ents = extractEntities(newText)?.filter(
ent => ent.type === 'link',
)
const set = new Set(ents ? ents.map(e => e.value) : [])
const set: Set<string> = new Set()
if (newRt.facets) {
for (const facet of newRt.facets) {
for (const feature of facet.features) {
if (AppBskyRichtextFacet.isLink(feature)) {
set.add(feature.uri)
}
}
}
}
if (!isEqual(set, suggestedLinks)) {
onSuggestedLinksChanged(set)
}
},
[
onTextChanged,
autocompleteView,
suggestedLinks,
onSuggestedLinksChanged,
],
[setRichText, autocompleteView, suggestedLinks, onSuggestedLinksChanged],
)
const onPaste = React.useCallback(
@ -159,31 +162,35 @@ export const TextInput = React.forwardRef(
const onSelectAutocompleteItem = React.useCallback(
(item: string) => {
onChangeText(
insertMentionAt(text, textInputSelection.current?.start || 0, item),
insertMentionAt(
richtext.text,
textInputSelection.current?.start || 0,
item,
),
)
autocompleteView.setActive(false)
},
[onChangeText, text, autocompleteView],
[onChangeText, richtext, autocompleteView],
)
const textDecorated = React.useMemo(() => {
let i = 0
return detectLinkables(text).map(v => {
if (typeof v === 'string') {
return Array.from(richtext.segments()).map(segment => {
if (!segment.facet) {
return (
<Text key={i++} style={[pal.text, styles.textInputFormatting]}>
{v}
{segment.text}
</Text>
)
} else {
return (
<Text key={i++} style={[pal.link, styles.textInputFormatting]}>
{v.link}
{segment.text}
</Text>
)
}
})
}, [text, pal.link, pal.text])
}, [richtext, pal.link, pal.text])
return (
<View style={styles.container}>

View file

@ -1,5 +1,6 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
import {RichText} from '@atproto/api'
import {useEditor, EditorContent, JSONContent} from '@tiptap/react'
import {Document} from '@tiptap/extension-document'
import {Link} from '@tiptap/extension-link'
@ -17,11 +18,11 @@ export interface TextInputRef {
}
interface TextInputProps {
text: string
richtext: RichText
placeholder: string
suggestedLinks: Set<string>
autocompleteView: UserAutocompleteViewModel
onTextChanged: (v: string) => void
setRichText: (v: RichText) => void
onPhotoPasted: (uri: string) => void
onSuggestedLinksChanged: (uris: Set<string>) => void
onError: (err: string) => void
@ -30,11 +31,11 @@ interface TextInputProps {
export const TextInput = React.forwardRef(
(
{
text,
richtext,
placeholder,
suggestedLinks,
autocompleteView,
onTextChanged,
setRichText,
// onPhotoPasted, TODO
onSuggestedLinksChanged,
}: // onError, TODO
@ -60,15 +61,15 @@ export const TextInput = React.forwardRef(
}),
Text,
],
content: text,
content: richtext.text.toString(),
autofocus: true,
editable: true,
injectCSS: true,
onUpdate({editor: editorProp}) {
const json = editorProp.getJSON()
const newText = editorJsonToText(json).trim()
onTextChanged(newText)
const newRt = new RichText({text: editorJsonToText(json).trim()})
setRichText(newRt)
const newSuggestedLinks = new Set(editorJsonToLinks(json))
if (!isEqual(newSuggestedLinks, suggestedLinks)) {