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:
parent
19f3a2fa92
commit
a3334a01a2
133 changed files with 3103 additions and 2839 deletions
|
@ -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: {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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)) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue