add composer nudge

This commit is contained in:
Samuel Newman
2025-04-21 14:43:54 +03:00
parent 35daa5a343
commit d9786aeebe
4 changed files with 403 additions and 122 deletions
+4
View File
@@ -195,6 +195,10 @@ export const EMOJI_REACTION_LIMIT = 5
export const urls = { export const urls = {
website: { website: {
support: {
communityGuidelines:
'https://bsky.social/about/support/community-guidelines',
},
blog: { blog: {
initialVerificationAnnouncement: `https://bsky.social/about/blog/04-21-2025-verification`, initialVerificationAnnouncement: `https://bsky.social/about/blog/04-21-2025-verification`,
}, },
+5
View File
@@ -146,6 +146,11 @@ export type MetricEvents = {
'composer:gif:open': {} 'composer:gif:open': {}
'composer:gif:select': {} 'composer:gif:select': {}
'composer:nudge:feedback': {
replyTo?: string
text: string
}
// Data events // Data events
'account:create:begin': {} 'account:create:begin': {}
'account:create:success': { 'account:create:success': {
+34 -4
View File
@@ -128,6 +128,7 @@ import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import * as Prompt from '#/components/Prompt' import * as Prompt from '#/components/Prompt'
import {Text as NewText} from '#/components/Typography' import {Text as NewText} from '#/components/Typography'
import {BottomSheetPortalProvider} from '../../../../modules/bottom-sheet' import {BottomSheetPortalProvider} from '../../../../modules/bottom-sheet'
import LanguageNudgeDialog from './LanguageNudge'
import { import {
type ComposerAction, type ComposerAction,
composerReducer, composerReducer,
@@ -376,7 +377,10 @@ export const ComposePost = ({
), ),
) )
const onPressPublish = React.useCallback(async () => { const languageNudgeControl = useDialogControl()
const onPressPublish = useCallback(
async (bypassNudge = false) => {
if (isPublishing) { if (isPublishing) {
return return
} }
@@ -400,6 +404,16 @@ export const ComposePost = ({
setError('') setError('')
setIsPublishing(true) setIsPublishing(true)
if (!bypassNudge) {
// api call goes here
await new Promise(res => setTimeout(res, 500))
if (true) {
setIsPublishing(false)
languageNudgeControl.open()
return
}
}
let postUri let postUri
try { try {
postUri = ( postUri = (
@@ -492,7 +506,8 @@ export const ComposePost = ({
? _(msg`Your reply has been published`) ? _(msg`Your reply has been published`)
: _(msg`Your post has been published`), : _(msg`Your post has been published`),
) )
}, [ },
[
_, _,
agent, agent,
thread, thread,
@@ -505,7 +520,13 @@ export const ComposePost = ({
replyTo, replyTo,
setLangPrefs, setLangPrefs,
queryClient, queryClient,
]) languageNudgeControl,
],
)
const postAnyway = useCallback(() => {
return onPressPublish(true)
}, [onPressPublish])
// Preserves the referential identity passed to each post item. // Preserves the referential identity passed to each post item.
// Avoids re-rendering all posts on each keystroke. // Avoids re-rendering all posts on each keystroke.
@@ -697,6 +718,15 @@ export const ComposePost = ({
{!isWebFooterSticky && footer} {!isWebFooterSticky && footer}
</View> </View>
<LanguageNudgeDialog
// TODO: account for threads - will need to thread through API response
postText={thread.posts[0].richtext}
replyToUri={replyTo?.uri}
control={languageNudgeControl}
postAnyway={postAnyway}
discardReply={onClose}
/>
<Prompt.Basic <Prompt.Basic
control={discardPromptControl} control={discardPromptControl}
title={_(msg`Discard draft?`)} title={_(msg`Discard draft?`)}
@@ -997,7 +1027,7 @@ function ComposerTopBar({
shape="default" shape="default"
size="small" size="small"
style={[a.rounded_full, a.py_sm]} style={[a.rounded_full, a.py_sm]}
onPress={onPublish} onPress={() => onPublish()}
disabled={!canPost || isPublishQueued}> disabled={!canPost || isPublishQueued}>
<ButtonText style={[a.text_md]}> <ButtonText style={[a.text_md]}>
{isReply ? ( {isReply ? (
+242
View File
@@ -0,0 +1,242 @@
import {useState} from 'react'
import {LayoutAnimation, View} from 'react-native'
import {type AppBskyActorDefs, type RichText} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {urls} from '#/lib/constants'
import {logger} from '#/logger'
import {useProfileQuery} from '#/state/queries/profile'
import {useSession} from '#/state/session'
import {atoms as a, useTheme, web} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {InlineLinkText} from '#/components/Link'
import {Text} from '#/components/Typography'
import {QuoteEmbed} from '../util/post-embeds/QuoteEmbed'
export default function LanguageNudgeDialog({
control,
postText,
replyToUri,
postAnyway,
discardReply,
}: {
control: Dialog.DialogControlProps
postText: RichText
replyToUri?: string
postAnyway: () => void
discardReply: () => void
}) {
return (
<Dialog.Outer
control={control}
nativeOptions={{preventExpansion: true}}
webOptions={{alignCenter: true}}>
<Dialog.Handle />
<DialogInner
postText={postText}
replyToUri={replyToUri}
postAnyway={postAnyway}
discardReply={discardReply}
/>
</Dialog.Outer>
)
}
function DialogInner({
postText,
replyToUri,
postAnyway,
discardReply,
}: {
postText: RichText
replyToUri?: string
postAnyway: () => void
discardReply: () => void
}) {
const control = Dialog.useDialogContext()
const {_} = useLingui()
const t = useTheme()
const {currentAccount} = useSession()
const profile = useProfileQuery({did: currentAccount?.did})
const [feedbackSubmitted, setFeedbackSubmitted] = useState(false)
return (
<Dialog.ScrollableInner
label={_(msg`Are you sure you want to post this?`)}
style={web({maxWidth: 500})}
contentContainerStyle={[a.px_0]}>
<View style={[a.flex_1, a.gap_md, a.align_center, a.py_lg, a.px_xl]}>
<View
style={[
{
width: 64,
height: 64,
backgroundColor: t.palette.primary_50,
},
a.rounded_full,
a.justify_center,
a.align_center,
]}
aria-hidden={true}
accessibilityElementsHidden
importantForAccessibility="no-hide-descendants">
{/* No need to translate, it's decorative */}
<Text
style={[
a.leading_tight,
a.text_xl,
a.font_bold,
a.text_center,
{color: t.palette.primary_500},
]}
maxFontSizeMultiplier={1}
minimumFontScale={1}>
@#!
</Text>
</View>
<Text style={[a.text_2xl, a.leading_snug, a.font_bold, a.text_center]}>
<Trans>Are you sure you want to post this?</Trans>
</Text>
<Text
style={[
a.text_md,
a.leading_snug,
a.text_center,
t.atoms.text_contrast_high,
]}>
{replyToUri ? (
<Trans>
Your reply includes language that might go against our{' '}
<InlineLinkText
label={_(msg`Community guidelines`)}
to={urls.website.support.communityGuidelines}
style={[a.text_md, a.leading_snug]}>
community guidelines
</InlineLinkText>
.
</Trans>
) : (
<Trans>
Your post includes language that might go against our{' '}
<InlineLinkText
label={_(msg`Community guidelines`)}
to={urls.website.support.communityGuidelines}
style={[a.text_md, a.leading_snug]}>
community guidelines
</InlineLinkText>
.
</Trans>
)}
</Text>
{profile.data && (
<View style={[a.w_full, a.pointer_events_none]}>
<QuoteEmbed
quote={{
$type: 'app.bsky.feed.defs#postView',
uri: `at://${profile.data.did}/app.bsky.feed.post/draft`,
cid: '',
author: profile.data as AppBskyActorDefs.ProfileViewBasic,
record: {
$type: 'app.bsky.feed.post',
createdAt: new Date().toISOString(),
text: postText.text,
facets: postText.facets,
},
indexedAt: new Date().toISOString(),
}}
/>
</View>
)}
{feedbackSubmitted ? (
<Text
style={[
a.py_sm,
a.text_sm,
a.leading_snug,
a.text_center,
t.atoms.text_contrast_medium,
a.italic,
]}>
Feedback submitted
</Text>
) : (
<Button
label={_(msg`Did we get this wrong?`)}
onPress={() => {
logger.metric('composer:nudge:feedback', {
replyTo: replyToUri,
text: postText.text,
})
LayoutAnimation.configureNext(
LayoutAnimation.Presets.easeInEaseOut,
)
setFeedbackSubmitted(true)
}}
size="small"
color="secondary"
variant="ghost">
<ButtonText>
<Trans>Did we get this wrong?</Trans>
</ButtonText>
</Button>
)}
</View>
<View
style={[
a.flex_1,
a.pt_xl,
a.gap_md,
a.align_stretch,
a.border_t,
t.atoms.border_contrast_low,
a.px_xl,
]}>
<Button
label={_(msg`Keep editing`)}
onPress={() => control.close()}
size="large"
color="primary"
variant="solid">
<ButtonText>
<Trans>Keep editing</Trans>
</ButtonText>
</Button>
<Button
label={replyToUri ? _(msg`Discard reply`) : _(msg`Discard post`)}
onPress={() => control.close(discardReply)}
size="large"
color="negative"
variant="outline">
<ButtonText>
{replyToUri ? (
<Trans>Discard reply</Trans>
) : (
<Trans>Discard post</Trans>
)}
</ButtonText>
</Button>
<Button
label={_(msg`Reply anyway`)}
onPress={() => control.close(postAnyway)}
size="large"
color="secondary"
variant="solid">
<ButtonText>
{replyToUri ? (
<Trans>Reply anyway</Trans>
) : (
<Trans>Post anyway</Trans>
)}
</ButtonText>
</Button>
</View>
<Dialog.Close />
</Dialog.ScrollableInner>
)
}