diff options
-rw-r--r-- | package.json | 1 | ||||
-rw-r--r-- | src/lib/api/index.ts | 2 | ||||
-rw-r--r-- | src/state/models/media/image.ts | 13 | ||||
-rw-r--r-- | src/view/com/composer/text-input/TextInput.web.tsx | 96 | ||||
-rw-r--r-- | src/view/com/composer/text-input/web/LinkDecorator.ts | 13 | ||||
-rw-r--r-- | src/view/com/posts/FeedItem.tsx | 3 | ||||
-rw-r--r-- | src/view/com/util/images/AutoSizedImage.tsx | 4 | ||||
-rw-r--r-- | src/view/com/util/post-ctrls/RepostButton.web.tsx | 91 | ||||
-rw-r--r-- | src/view/com/util/post-embeds/index.tsx | 9 | ||||
-rw-r--r-- | src/view/screens/Profile.tsx | 5 | ||||
-rw-r--r-- | src/view/shell/desktop/LeftNav.tsx | 23 | ||||
-rw-r--r-- | yarn.lock | 14 |
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" |