From a3334a01a221877d3e06e02f960fda441f3460bd Mon Sep 17 00:00:00 2001
From: Paul Frazee <pfrazee@gmail.com>
Date: Fri, 31 Mar 2023 13:17:26 -0500
Subject: 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
---
 src/view/com/composer/Composer.tsx | 73 +++++++++++++++++++++-----------------
 1 file changed, 40 insertions(+), 33 deletions(-)

(limited to 'src/view/com/composer/Composer.tsx')

diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx
index 572eea927..6009debdd 100644
--- a/src/view/com/composer/Composer.tsx
+++ b/src/view/com/composer/Composer.tsx
@@ -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: {
-- 
cgit 1.4.1

