about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/lib/api/index.ts2
-rw-r--r--src/state/models/media/image.ts13
-rw-r--r--src/view/com/composer/text-input/TextInput.web.tsx96
-rw-r--r--src/view/com/composer/text-input/web/LinkDecorator.ts13
-rw-r--r--src/view/com/posts/FeedItem.tsx3
-rw-r--r--src/view/com/util/images/AutoSizedImage.tsx4
-rw-r--r--src/view/com/util/post-ctrls/RepostButton.web.tsx91
-rw-r--r--src/view/com/util/post-embeds/index.tsx9
-rw-r--r--src/view/screens/Profile.tsx5
-rw-r--r--src/view/shell/desktop/LeftNav.tsx23
10 files changed, 128 insertions, 131 deletions
diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts
index 4ecd32046..8a9389a18 100644
--- a/src/lib/api/index.ts
+++ b/src/lib/api/index.ts
@@ -133,10 +133,12 @@ export async function post(store: RootStoreModel, opts: PostOpts) {
       opts.onStateChange?.(`Uploading image #${images.length + 1}...`)
       await image.compress()
       const path = image.compressed?.path ?? image.path
+      const {width, height} = image.compressed || image
       const res = await uploadBlob(store, path, 'image/jpeg')
       images.push({
         image: res.data.blob,
         alt: image.altText ?? '',
+        aspectRatio: {width, height},
       })
     }
 
diff --git a/src/state/models/media/image.ts b/src/state/models/media/image.ts
index 844ecb778..10aef0ff4 100644
--- a/src/state/models/media/image.ts
+++ b/src/state/models/media/image.ts
@@ -8,6 +8,7 @@ import {openCropper} from 'lib/media/picker'
 import {ActionCrop, FlipType, SaveFormat} from 'expo-image-manipulator'
 import {Position} from 'react-avatar-editor'
 import {Dimensions} from 'lib/media/types'
+import {isIOS} from 'platform/detection'
 
 export interface ImageManipulationAttributes {
   aspectRatio?: '4:3' | '1:1' | '3:4' | 'None'
@@ -164,8 +165,13 @@ export class ImageModel implements Omit<RNImage, 'size'> {
   // Mobile
   async crop() {
     try {
-      // openCropper requires an output width and height hence
-      // getting upload dimensions before cropping is necessary.
+      // NOTE
+      // on ios, react-native-image-cropper gives really bad quality
+      // without specifying width and height. on android, however, the
+      // crop stretches incorrectly if you do specify it. these are
+      // both separate bugs in the library. we deal with that by
+      // providing width & height for ios only
+      // -prf
       const {width, height} = this.getUploadDimensions({
         width: this.width,
         height: this.height,
@@ -175,8 +181,7 @@ export class ImageModel implements Omit<RNImage, 'size'> {
         mediaType: 'photo',
         path: this.path,
         freeStyleCropEnabled: true,
-        width,
-        height,
+        ...(isIOS ? {width, height} : {}),
       })
 
       runInAction(() => {
diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx
index 7eea904ab..31e372567 100644
--- a/src/view/com/composer/text-input/TextInput.web.tsx
+++ b/src/view/com/composer/text-input/TextInput.web.tsx
@@ -17,6 +17,7 @@ import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
 import {isUriImage, blobToDataUri} from 'lib/media/util'
 import {Emoji} from './web/EmojiPicker.web'
 import {LinkDecorator} from './web/LinkDecorator'
+import {generateJSON} from '@tiptap/html'
 
 export interface TextInputRef {
   focus: () => void
@@ -52,6 +53,26 @@ export const TextInput = React.forwardRef(function TextInputImpl(
   ref,
 ) {
   const modeClass = useColorSchemeStyle('ProseMirror-light', 'ProseMirror-dark')
+  const extensions = React.useMemo(
+    () => [
+      Document,
+      LinkDecorator,
+      Mention.configure({
+        HTMLAttributes: {
+          class: 'mention',
+        },
+        suggestion: createSuggestion({autocompleteView}),
+      }),
+      Paragraph,
+      Placeholder.configure({
+        placeholder,
+      }),
+      Text,
+      History,
+      Hardbreak,
+    ],
+    [autocompleteView, placeholder],
+  )
 
   React.useEffect(() => {
     textInputWebEmitter.addListener('publish', onPressPublish)
@@ -68,23 +89,7 @@ export const TextInput = React.forwardRef(function TextInputImpl(
 
   const editor = useEditor(
     {
-      extensions: [
-        Document,
-        LinkDecorator,
-        Mention.configure({
-          HTMLAttributes: {
-            class: 'mention',
-          },
-          suggestion: createSuggestion({autocompleteView}),
-        }),
-        Paragraph,
-        Placeholder.configure({
-          placeholder,
-        }),
-        Text,
-        History,
-        Hardbreak,
-      ],
+      extensions,
       editorProps: {
         attributes: {
           class: modeClass,
@@ -107,7 +112,7 @@ export const TextInput = React.forwardRef(function TextInputImpl(
           }
         },
       },
-      content: textToEditorJson(richtext.text.toString()),
+      content: generateJSON(richtext.text.toString(), extensions),
       autofocus: 'end',
       editable: true,
       injectCSS: true,
@@ -182,61 +187,6 @@ function editorJsonToText(json: JSONContent): string {
   return text
 }
 
-function textToEditorJson(text: string): JSONContent {
-  if (text === '' || text.length === 0) {
-    return {
-      text: '',
-    }
-  }
-
-  const lines = text.split('\n')
-  const docContent: JSONContent[] = []
-
-  for (const line of lines) {
-    if (line.trim() === '') {
-      continue // skip empty lines
-    }
-
-    const paragraphContent: JSONContent[] = []
-    let position = 0
-
-    while (position < line.length) {
-      if (line[position] === '@') {
-        // Handle mentions
-        let endPosition = position + 1
-        while (endPosition < line.length && /\S/.test(line[endPosition])) {
-          endPosition++
-        }
-        const mentionId = line.substring(position + 1, endPosition)
-        paragraphContent.push({
-          type: 'mention',
-          attrs: {id: mentionId},
-        })
-        position = endPosition
-      } else {
-        // Handle regular text
-        let endPosition = line.indexOf('@', position)
-        if (endPosition === -1) endPosition = line.length
-        paragraphContent.push({
-          type: 'text',
-          text: line.substring(position, endPosition),
-        })
-        position = endPosition
-      }
-    }
-
-    docContent.push({
-      type: 'paragraph',
-      content: paragraphContent,
-    })
-  }
-
-  return {
-    type: 'doc',
-    content: docContent,
-  }
-}
-
 const styles = StyleSheet.create({
   container: {
     flex: 1,
diff --git a/src/view/com/composer/text-input/web/LinkDecorator.ts b/src/view/com/composer/text-input/web/LinkDecorator.ts
index 531e8d5a0..19945de08 100644
--- a/src/view/com/composer/text-input/web/LinkDecorator.ts
+++ b/src/view/com/composer/text-input/web/LinkDecorator.ts
@@ -16,7 +16,6 @@
 
 import {Mark} from '@tiptap/core'
 import {Plugin, PluginKey} from '@tiptap/pm/state'
-import {findChildren} from '@tiptap/core'
 import {Node as ProsemirrorNode} from '@tiptap/pm/model'
 import {Decoration, DecorationSet} from '@tiptap/pm/view'
 import {isValidDomain} from 'lib/strings/url-helpers'
@@ -36,20 +35,20 @@ export const LinkDecorator = Mark.create({
 function getDecorations(doc: ProsemirrorNode) {
   const decorations: Decoration[] = []
 
-  findChildren(doc, node => node.type.name === 'paragraph').forEach(
-    paragraphNode => {
-      const textContent = paragraphNode.node.textContent
+  doc.descendants((node, pos) => {
+    if (node.isText && node.text) {
+      const textContent = node.textContent
 
       // links
       iterateUris(textContent, (from, to) => {
         decorations.push(
-          Decoration.inline(paragraphNode.pos + from, paragraphNode.pos + to, {
+          Decoration.inline(pos + from, pos + to, {
             class: 'autolink',
           }),
         )
       })
-    },
-  )
+    }
+  })
 
   return DecorationSet.create(doc, decorations)
 }
diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx
index f6b6e5339..1ceae80ae 100644
--- a/src/view/com/posts/FeedItem.tsx
+++ b/src/view/com/posts/FeedItem.tsx
@@ -178,7 +178,7 @@ export const FeedItem = observer(function FeedItemImpl({
           )}
         </View>
 
-        <View style={{paddingTop: 12}}>
+        <View style={{paddingTop: 12, flexShrink: 1}}>
           {source ? (
             <Link
               title={sanitizeDisplayName(source.displayName)}
@@ -211,6 +211,7 @@ export const FeedItem = observer(function FeedItemImpl({
                 style={{
                   marginRight: 4,
                   color: pal.colors.textLight,
+                  minWidth: 16,
                 }}
               />
               <Text
diff --git a/src/view/com/util/images/AutoSizedImage.tsx b/src/view/com/util/images/AutoSizedImage.tsx
index da2f7ab45..035e29c25 100644
--- a/src/view/com/util/images/AutoSizedImage.tsx
+++ b/src/view/com/util/images/AutoSizedImage.tsx
@@ -11,6 +11,7 @@ const MAX_ASPECT_RATIO = 5 // 5/1
 interface Props {
   alt?: string
   uri: string
+  dimensionsHint?: Dimensions
   onPress?: () => void
   onLongPress?: () => void
   onPressIn?: () => void
@@ -21,6 +22,7 @@ interface Props {
 export function AutoSizedImage({
   alt,
   uri,
+  dimensionsHint,
   onPress,
   onLongPress,
   onPressIn,
@@ -29,7 +31,7 @@ export function AutoSizedImage({
 }: Props) {
   const store = useStores()
   const [dim, setDim] = React.useState<Dimensions | undefined>(
-    store.imageSizes.get(uri),
+    dimensionsHint || store.imageSizes.get(uri),
   )
   const [aspectRatio, setAspectRatio] = React.useState<number>(
     dim ? calc(dim) : 1,
diff --git a/src/view/com/util/post-ctrls/RepostButton.web.tsx b/src/view/com/util/post-ctrls/RepostButton.web.tsx
index eab6e2fef..57f544d41 100644
--- a/src/view/com/util/post-ctrls/RepostButton.web.tsx
+++ b/src/view/com/util/post-ctrls/RepostButton.web.tsx
@@ -1,17 +1,23 @@
-import React, {useMemo} from 'react'
+import React from 'react'
 import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
 import {RepostIcon} from 'lib/icons'
-import {DropdownButton} from '../forms/DropdownButton'
 import {colors} from 'lib/styles'
 import {useTheme} from 'lib/ThemeContext'
 import {Text} from '../text/Text'
 
+import {
+  NativeDropdown,
+  DropdownItem as NativeDropdownItem,
+} from '../forms/NativeDropdown'
+import {EventStopper} from '../EventStopper'
+
 interface Props {
   isReposted: boolean
   repostCount?: number
   big?: boolean
   onRepost: () => void
   onQuote: () => void
+  style?: StyleProp<ViewStyle>
 }
 
 export const RepostButton = ({
@@ -30,44 +36,55 @@ export const RepostButton = ({
     [theme],
   )
 
-  const items = useMemo(
-    () => [
-      {
-        label: isReposted ? 'Undo repost' : 'Repost',
-        icon: 'retweet' as const,
-        onPress: onRepost,
+  const dropdownItems: NativeDropdownItem[] = [
+    {
+      label: isReposted ? 'Undo repost' : 'Repost',
+      testID: 'repostDropdownRepostBtn',
+      icon: {
+        ios: {name: 'repeat'},
+        android: '',
+        web: 'retweet',
       },
-      {label: 'Quote post', icon: 'quote-left' as const, onPress: onQuote},
-    ],
-    [isReposted, onRepost, onQuote],
-  )
+      onPress: onRepost,
+    },
+    {
+      label: 'Quote post',
+      testID: 'repostDropdownQuoteBtn',
+      icon: {
+        ios: {name: 'quote.bubble'},
+        android: '',
+        web: 'quote-left',
+      },
+      onPress: onQuote,
+    },
+  ]
 
   return (
-    <DropdownButton
-      type="bare"
-      items={items}
-      bottomOffset={4}
-      openToRight
-      rightOffset={-40}>
-      <View
-        style={[
-          styles.control,
-          !big && styles.controlPad,
-          (isReposted
-            ? styles.reposted
-            : defaultControlColor) as StyleProp<ViewStyle>,
-        ]}>
-        <RepostIcon strokeWidth={2.4} size={big ? 24 : 20} />
-        {typeof repostCount !== 'undefined' ? (
-          <Text
-            testID="repostCount"
-            type={isReposted ? 'md-bold' : 'md'}
-            style={styles.repostCount}>
-            {repostCount ?? 0}
-          </Text>
-        ) : undefined}
-      </View>
-    </DropdownButton>
+    <EventStopper>
+      <NativeDropdown
+        items={dropdownItems}
+        accessibilityLabel="Repost or quote post"
+        accessibilityHint="">
+        <View
+          style={[
+            styles.control,
+            !big && styles.controlPad,
+            (isReposted
+              ? styles.reposted
+              : defaultControlColor) as StyleProp<ViewStyle>,
+          ]}>
+          <RepostIcon strokeWidth={2.2} size={big ? 24 : 20} />
+          {typeof repostCount !== 'undefined' ? (
+            <Text
+              testID="repostCount"
+              type={isReposted ? 'md-bold' : 'md'}
+              style={styles.repostCount}>
+              {repostCount ?? 0}
+            </Text>
+          ) : undefined}
+        </View>
+      </NativeDropdown>
+    </EventStopper>
   )
 }
 
diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx
index ce6da4a1b..2d79eed8f 100644
--- a/src/view/com/util/post-embeds/index.tsx
+++ b/src/view/com/util/post-embeds/index.tsx
@@ -93,7 +93,11 @@ export function PostEmbeds({
     const {images} = embed
 
     if (images.length > 0) {
-      const items = embed.images.map(img => ({uri: img.fullsize, alt: img.alt}))
+      const items = embed.images.map(img => ({
+        uri: img.fullsize,
+        alt: img.alt,
+        aspectRatio: img.aspectRatio,
+      }))
       const openLightbox = (index: number) => {
         store.shell.openLightbox(new ImagesLightbox(items, index))
       }
@@ -104,12 +108,13 @@ export function PostEmbeds({
       }
 
       if (images.length === 1) {
-        const {alt, thumb} = images[0]
+        const {alt, thumb, aspectRatio} = images[0]
         return (
           <View style={[styles.imagesContainer, style]}>
             <AutoSizedImage
               alt={alt}
               uri={thumb}
+              dimensionsHint={aspectRatio}
               onPress={() => openLightbox(0)}
               onPressIn={() => onPressIn(0)}
               style={[
diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx
index efcb588f6..596bda57e 100644
--- a/src/view/screens/Profile.tsx
+++ b/src/view/screens/Profile.tsx
@@ -91,7 +91,10 @@ export const ProfileScreen = withAuthRequired(
     const onPressCompose = React.useCallback(() => {
       track('ProfileScreen:PressCompose')
       const mention =
-        uiState.profile.handle === store.me.handle ? '' : uiState.profile.handle
+        uiState.profile.handle === store.me.handle ||
+        uiState.profile.handle === 'handle.invalid'
+          ? undefined
+          : uiState.profile.handle
       store.shell.openComposer({mention})
     }, [store, track, uiState])
     const onSelectView = React.useCallback(
diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx
index b19d5e8ab..fb3d66462 100644
--- a/src/view/shell/desktop/LeftNav.tsx
+++ b/src/view/shell/desktop/LeftNav.tsx
@@ -185,20 +185,33 @@ function ComposeBtn() {
   const {getState} = useNavigation()
   const {isTablet} = useWebMediaQueries()
 
-  const getProfileHandle = () => {
+  const getProfileHandle = async () => {
     const {routes} = getState()
     const currentRoute = routes[routes.length - 1]
+
     if (currentRoute.name === 'Profile') {
-      const {name: handle} =
+      let handle: string | undefined = (
         currentRoute.params as CommonNavigatorParams['Profile']
-      if (handle === store.me.handle) return undefined
+      ).name
+
+      if (handle.startsWith('did:')) {
+        const cached = await store.profiles.cache.get(handle)
+        const profile = cached ? cached.data : undefined
+        // if we can't resolve handle, set to undefined
+        handle = profile?.handle || undefined
+      }
+
+      if (!handle || handle === store.me.handle || handle === 'handle.invalid')
+        return undefined
+
       return handle
     }
+
     return undefined
   }
 
-  const onPressCompose = () =>
-    store.shell.openComposer({mention: getProfileHandle()})
+  const onPressCompose = async () =>
+    store.shell.openComposer({mention: await getProfileHandle()})
 
   if (isTablet) {
     return null