Skip to content

Commit

Permalink
add embed preview in composer
Browse files Browse the repository at this point in the history
  • Loading branch information
mozzius authored and estrattonbailey committed May 30, 2024
1 parent 16b577c commit 795ceb9
Show file tree
Hide file tree
Showing 6 changed files with 302 additions and 50 deletions.
2 changes: 1 addition & 1 deletion src/components/dms/MessageItemEmbed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ let MessageItemEmbed = ({
const itemHref = makeProfileLink(post.author, 'post', itemUrip.rkey)

return (
<Link to={itemHref}>
<Link to={itemHref} style={a.mt_2xs}>
<View
style={[
a.w_full,
Expand Down
23 changes: 21 additions & 2 deletions src/screens/Messages/Conversation/MessageInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,20 @@ import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useTheme} from '#/alf'
import {useSharedInputStyles} from '#/components/forms/TextField'
import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlane} from '#/components/icons/PaperPlane'
import {useExtractEmbedFromFacets} from './MessageInputEmbed'

const AnimatedTextInput = Animated.createAnimatedComponent(TextInput)

export function MessageInput({
onSendMessage,
hasEmbed,
setEmbed,
children,
}: {
onSendMessage: (message: string) => void
hasEmbed: boolean
setEmbed: (embedUrl: string | undefined) => void
children?: React.ReactNode
}) {
const {_} = useLingui()
const t = useTheme()
Expand All @@ -53,9 +60,10 @@ export function MessageInput({
const inputRef = useAnimatedRef<TextInput>()

useSaveMessageDraft(message)
useExtractEmbedFromFacets(message, setEmbed)

const onSubmit = React.useCallback(() => {
if (message.trim() === '') {
if (!hasEmbed && message.trim() === '') {
return
}
if (new Graphemer().countGraphemes(message) > MAX_DM_GRAPHEME_LENGTH) {
Expand All @@ -66,13 +74,23 @@ export function MessageInput({
onSendMessage(message)
playHaptic()
setMessage('')
setEmbed(undefined)

// Pressing the send button causes the text input to lose focus, so we need to
// re-focus it after sending
setTimeout(() => {
inputRef.current?.focus()
}, 100)
}, [message, clearDraft, onSendMessage, playHaptic, _, inputRef])
}, [
hasEmbed,
message,
clearDraft,
onSendMessage,
playHaptic,
setEmbed,
_,
inputRef,
])

useFocusedInputHandler(
{
Expand Down Expand Up @@ -101,6 +119,7 @@ export function MessageInput({

return (
<View style={[a.px_md, a.pb_sm, a.pt_xs]}>
{children}
<View
style={[
a.w_full,
Expand Down
14 changes: 12 additions & 2 deletions src/screens/Messages/Conversation/MessageInput.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,18 @@ import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useTheme} from '#/alf'
import {useSharedInputStyles} from '#/components/forms/TextField'
import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlane} from '#/components/icons/PaperPlane'
import {useExtractEmbedFromFacets} from './MessageInputEmbed'

export function MessageInput({
onSendMessage,
hasEmbed,
setEmbed,
children,
}: {
onSendMessage: (message: string) => void
hasEmbed: boolean
setEmbed: (embedUrl: string | undefined) => void
children?: React.ReactNode
}) {
const {isTabletOrDesktop} = useWebMediaQueries()
const {_} = useLingui()
Expand All @@ -35,7 +42,7 @@ export function MessageInput({
const [textAreaHeight, setTextAreaHeight] = React.useState(38)

const onSubmit = React.useCallback(() => {
if (message.trim() === '') {
if (!hasEmbed && message.trim() === '') {
return
}
if (new Graphemer().countGraphemes(message) > MAX_DM_GRAPHEME_LENGTH) {
Expand All @@ -45,7 +52,8 @@ export function MessageInput({
clearDraft()
onSendMessage(message)
setMessage('')
}, [message, onSendMessage, _, clearDraft])
setEmbed(undefined)
}, [message, onSendMessage, _, clearDraft, hasEmbed, setEmbed])

const onKeyDown = React.useCallback(
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
Expand Down Expand Up @@ -87,9 +95,11 @@ export function MessageInput({
)

useSaveMessageDraft(message)
useExtractEmbedFromFacets(message, setEmbed)

return (
<View style={a.p_sm}>
{children}
<View
style={[
a.flex_row,
Expand Down
216 changes: 216 additions & 0 deletions src/screens/Messages/Conversation/MessageInputEmbed.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import React, {useCallback, useEffect, useMemo, useState} from 'react'
import {LayoutAnimation, View} from 'react-native'
import {
AppBskyFeedPost,
AppBskyRichtextFacet,
AtUri,
RichText as RichTextAPI,
} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {RouteProp, useNavigation, useRoute} from '@react-navigation/native'

import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
import {makeProfileLink} from '#/lib/routes/links'
import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types'
import {
convertBskyAppUrlIfNeeded,
isBskyPostUrl,
makeRecordUri,
} from '#/lib/strings/url-helpers'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {usePostQuery} from '#/state/queries/post'
import {PostMeta} from '#/view/com/util/PostMeta'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import {Loader} from '#/components/Loader'
import {ContentHider} from '#/components/moderation/ContentHider'
import {PostAlerts} from '#/components/moderation/PostAlerts'
import {RichText} from '#/components/RichText'
import {Text} from '#/components/Typography'

export function useMessageEmbed() {
const route =
useRoute<RouteProp<CommonNavigatorParams, 'MessagesConversation'>>()
const navigation = useNavigation<NavigationProp>()
const embedFromParams = route.params.embed

const [embedUri, setEmbed] = useState(embedFromParams)

return {
embedUri,
setEmbed: useCallback(
(embedUrl: string | undefined) => {
if (!embedUrl) {
navigation.setParams({embed: undefined})
setEmbed(undefined)
return
}

if (embedFromParams) return

const url = convertBskyAppUrlIfNeeded(embedUrl)
const [_0, user, _1, rkey] = url.split('/').filter(Boolean)
const uri = makeRecordUri(user, 'app.bsky.feed.post', rkey)

console.log('setEmbed', uri)

setEmbed(uri)
},
[embedFromParams, navigation],
),
}
}

export function useExtractEmbedFromFacets(
message: string,
setEmbed: (embedUrl: string | undefined) => void,
) {
const rt = new RichTextAPI({text: message})
rt.detectFacetsWithoutResolution()

let uriFromFacet: string | undefined

for (const facet of rt.facets ?? []) {
for (const feature of facet.features) {
if (AppBskyRichtextFacet.isLink(feature) && isBskyPostUrl(feature.uri)) {
uriFromFacet = feature.uri
break
}
}
}

useEffect(() => {
if (uriFromFacet) {
setEmbed(uriFromFacet)
}
}, [uriFromFacet, setEmbed])
}

export function MessageInputEmbed({
embedUri,
setEmbed,
}: {
embedUri: string | undefined
setEmbed: (embedUrl: string | undefined) => void
}) {
const t = useTheme()
console.log('embedUri', embedUri)
const {data: post, status} = usePostQuery(embedUri)
const {_} = useLingui()

const moderationOpts = useModerationOpts()
const moderation = useMemo(
() =>
moderationOpts && post ? moderatePost(post, moderationOpts) : undefined,
[moderationOpts, post],
)

const {rt, record} = useMemo(() => {
if (
post &&
AppBskyFeedPost.isRecord(post.record) &&
AppBskyFeedPost.validateRecord(post.record).success
) {
return {
rt: new RichTextAPI({
text: post.record.text,
facets: post.record.facets,
}),
record: post.record,
}
}

return {rt: undefined, record: undefined}
}, [post])

if (!embedUri) {
return null
}

let content = null
switch (status) {
case 'pending':
content = (
<View
style={[a.flex_1, {minHeight: 64}, a.justify_center, a.align_center]}>
<Loader />
</View>
)
break
case 'error':
content = (
<View
style={[a.flex_1, {minHeight: 64}, a.justify_center, a.align_center]}>
<Text style={a.text_center}>Could not fetch post</Text>
</View>
)
break
case 'success':
const itemUrip = new AtUri(post.uri)
const itemHref = makeProfileLink(post.author, 'post', itemUrip.rkey)

if (!post || !moderation || !rt || !record) {
return null
}

content = (
<View
style={[
a.flex_1,
t.atoms.bg_contrast_25,
t.atoms.border_contrast_low,
a.rounded_md,
a.border,
a.p_sm,
a.mb_sm,
]}
pointerEvents="none">
<PostMeta
showAvatar
author={post.author}
moderation={moderation}
authorHasWarning={!!post.author.labels?.length}
timestamp={post.indexedAt}
postHref={itemHref}
style={a.flex_0}
/>
<ContentHider modui={moderation.ui('contentView')}>
<PostAlerts modui={moderation.ui('contentView')} style={a.py_xs} />
{rt.text && (
<View style={a.mt_xs}>
<RichText
enableTags
testID="postText"
value={rt}
style={[a.text_sm, t.atoms.text_contrast_high]}
authorHandle={post.author.handle}
numberOfLines={3}
/>
</View>
)}
</ContentHider>
</View>
)
break
}

return (
<View style={[a.flex_row, a.gap_2xs]}>
{content}
<Button
label={_(msg`Remove embed`)}
onPress={() => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
setEmbed(undefined)
}}
size="small"
variant="ghost"
color="secondary"
shape="round">
<ButtonIcon icon={X} />
</Button>
</View>
)
}
Loading

0 comments on commit 795ceb9

Please sign in to comment.