about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--package.json1
-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
-rw-r--r--yarn.lock14
12 files changed, 143 insertions, 131 deletions
diff --git a/package.json b/package.json
index 61c20d2f5..32ca3de7a 100644
--- a/package.json
+++ b/package.json
@@ -63,6 +63,7 @@
     "@tiptap/extension-paragraph": "^2.0.0-beta.220",
     "@tiptap/extension-placeholder": "^2.0.0-beta.220",
     "@tiptap/extension-text": "^2.0.0-beta.220",
+    "@tiptap/html": "^2.1.11",
     "@tiptap/pm": "^2.0.0-beta.220",
     "@tiptap/react": "^2.0.0-beta.220",
     "@tiptap/suggestion": "^2.0.0-beta.220",
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
diff --git a/yarn.lock b/yarn.lock
index 2bd08edb9..7533bb439 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4972,6 +4972,13 @@
   resolved "https://registry.yarnpkg.com/@tiptap/extension-text/-/extension-text-2.1.6.tgz#23f36114ee164e3da2fd326145ac7b7f8bd34c56"
   integrity sha512-CqV0N6ngoXZFeJGlQ86FSZJ/0k7+BN3S6aSUcb5DRAKsSEv/Ga1LvSG24sHy+dwjTuj3EtRPJSVZTFcSB17ZSA==
 
+"@tiptap/html@^2.1.11":
+  version "2.1.11"
+  resolved "https://registry.yarnpkg.com/@tiptap/html/-/html-2.1.11.tgz#998421b526f200d01c549f37eb8fae2a0d1f0ed6"
+  integrity sha512-VKmBb1c3YN9hZfBzkV+QERf3ZWBUHHxjv2/BOr/Dw6mbb6+0iA1nxO9vQYPUb+xAmlm0n8vWwc7YQ8rxBwTKWQ==
+  dependencies:
+    zeed-dom "^0.9.19"
+
 "@tiptap/pm@^2.0.0-beta.220":
   version "2.1.6"
   resolved "https://registry.yarnpkg.com/@tiptap/pm/-/pm-2.1.6.tgz#4c196a7147fedd71316ef3413bb0e98d5c97726d"
@@ -19227,6 +19234,13 @@ yocto-queue@^1.0.0:
   resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251"
   integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==
 
+zeed-dom@^0.9.19:
+  version "0.9.26"
+  resolved "https://registry.yarnpkg.com/zeed-dom/-/zeed-dom-0.9.26.tgz#f0127d1024b34a1233a321bd6d0275b3ba998b30"
+  integrity sha512-HWjX8rA3Y/RI32zby3KIN1D+mgskce+She4K7kRyyx62OiVxJ5FnYm8vWq0YVAja3Tf2S1M0XAc6O2lRFcMgcQ==
+  dependencies:
+    css-what "^6.1.0"
+
 zeego@^1.6.2:
   version "1.7.0"
   resolved "https://registry.yarnpkg.com/zeego/-/zeego-1.7.0.tgz#8034adb842199c4ccf21bcb19877800bff18606b"