* Add modservice screen and profile-header-card * Drop the guidelines for now * Remove ununsed constants * Add label & label group descriptions * Not found state * Reorg, add icon * Subheader * Header * Complete header * Clean up * Add all groups * Fix scroll view * Dialogs side quest * Remove log * Add (WIP) debug mod page * Dialog solution * Add note * Clean up and reorganize localized moderation strings * Memoize * Add example * Add first ReportDialog screen * Report dialog step 2 * Submit * Integrate updates * Move moderation screen * Migrate buttons * Migrate everything * Rough sketch * Fix types * Update atoms values * Abstract ModerationServiceCard * Hook up data to settings page * Handle subscription * Rough enablement * Rough enablement * Some validation, fixes * More work on the mod debug screen * Hook up data * Update invalidation * Hook up data to ReportDialog * Fix native error * Refactor/rewrite the entire moderation-application system * Fix toggles * Add copyright and other option to report * Handle reports on profile vs content * Little cleanup * Get post hiding back in gear * Better loading flow on Mod screen * Clean up Mod screen * Clean up ProfileMod screen * Handle muting correctly * Update enablement on ProfileMod screen * Improve Moderation screen and dialog * Styling, handle disabled labelers * Rework list of labels on own content * Use moderateNotification() * ReportDialog updates * Fix button overflow * Simplify the ProfileModerationService ui * Mod screen design * Move moderation card from the profile header to a tab * Small tweaks to the moderation screen * Enable toggle on mod page * Add notifs to debugmod and dont filter notifs from followed users * Add moderator-service profile view * Wire up more of the modservice data to profiles * A bunch of speculative non-working UI * Cleanup: delete old code * Update ModerationDetailsDialog * Update ReportDialog * Update LabelsOnMe dialog * Handle ReportDialog load better * Rename LabelsOnMeDialog, fix close * Experiment to put labeling under a tab of a normal profile * Moderator variation of profile * Remove dead code and start moving toward latest modsdk * Remove a bunch of now-dead label strings * Update ModDebug to be a bit more intuitive and support custom labels * Minor ui tweaks * Improve consistency of display name blurring * Fix profile-card warning rendering * More debugmod UI tuning * Update to use new labeler semantics * Delete some dead code and do some refactoring * Update profile to pull from labeler definition * Implement new label config controls (wip) * Tweak ui * Implement preference controls on labelers * Rework label pref ui * Get moderation screen working * Add asyncstorage query persistence * Implement label handling * Small cleanup * Implement Likes dialog * Fix: remove text outside of text element * Cleanup * Fix likes dialog on mobile * Implement the label appeal flow * Get report flow working again with temporarily fixed report options * Update onboarding * Enforce limit of ten labeler subscriptions * Fix type errors * Fix lint errors * Improve types of RQ * Some work on Likes dialog, needs discussion * Bit of ReportDialog cleanup * Replace non-single-path SVG * Update nudity descriptions * Update to use new sdk updates * Add adult-content-enabled behavior to label config * Use the default setting of custom labels * Handle global moderation label prefs with the global settings * Fix missing postAuthor * Fix empty moderation page * Add mutewords control back to Mod screen * Tweak adult setting styles * Remove deprecated global labels * Handle underage users on mod screen * Adjust font sizes * Swap in RichText * Like button improvements * Tweaks to Labeler profile * Design tweaks for mod pref dialog * Add tertiary button color * Switch moderation UIs to tertiary color * Update mutewords and hiddenposts to use the new sdk * Add test-environment mod authority * Switch 'gore' to 'graphic-media' * Move nudity out of the adult content control * Remove focus styles from buttons - let the browser behavior handle it * Fixes to the adult content age-gating in moderaiton * Ditch tertiary button color, lighten secondary button * Fix some colors * Remove focused overrides from toggles * Liked by screen * Rework the moderationlabelpref * Fix optimistic like * Cleanup * Change how onboarding handles adult content enabled/disabled * Add special handling of the mod authorities * Tweaks * Update the default labeler avatar to a shield * Add route to go server * Avoid dups due to bad config * Fix attrs * Fix: dont try to detect link/label mismatches on post meta * Correctly show the label behavior when adult content is disabled * Readd the local hiddenPosts handling * WIP * Fix bad merge * Conten hider design tweaks * Fix text string breakage * Adjust source text in ContentHider * Fix link bug * Design tweaks to ContentHider and ModDetailsDialog * Adjust spacing of inform badges * Adjust spacing of embeds in posts * Style tweaks to post/profile alerts * Labels on me and dialog * Remove bad focus styles from post dropdown * Better spacing solution * Tune moderation UIs * Moderation UI tweaks for mobile * Move labelers query on Mod screen * Update to use new SDK appLabelers semantics * Implement report submission * Replace the report modal entirely with the report dialog * Add @ to mod details dialog handle * Bump SDK package * Remove silly type * Add to AWS build CI * Fix ToggleButton overflow * Clean up ModServiceCard, rename to LabelingServiceCard * Hackfix to translate gore labels to graphic-media * Tune content hider sizing on web desktop * Handle self labels * Fix spacing below text-only posts * Fix: send appeals to the right labeler * Give mod page links interactive states * Fix references * Remove focus handling * Remove remnant * Remove the like count from the subscribed labeler listing * Bump @atproto/api@0.11.1 * Remove extra @ * Fix: persist labels to local storage to reduce coverage gaps * update dipendencies * revert dipendencies * Add some explainers on how blocking affects labelers * Tweak copy * Fix underline color in header * Fix profile menu * Handle card overflow * Remove metrics from header * Mute 'account' not 'user' * Show metrics if self * Show the labels tab on logged out view * Fix bad merge * Use purple theming on labelers * Tighten space on LabelerCard * Set staleTime to 6hrs for labeler details * Memoize the memoizers * Drop staleTime to 60s * Move label defs into a context to reduce recomputes * Submit view tweaks * Move labeler fetch below auth * Mitigation: hardcode the bluesky moderation labeler name * Bump sdk * Add missing translated string Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Add missing translated string Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Hailey's fix for incorrect profile tabs Co-authored-by: Hailey <me@haileyok.com> * Feedback * Fix borders, add bottom space * Hailey's fix pt 2 Co-authored-by: Hailey <me@haileyok.com> * Fix post tabs * Integrate feedback pt 1 Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Integrate feedback pt 2 Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Integrate feedback pt 3 Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Integrate feedback pt 4 Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Integrate feedback pt 5 Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Integrate feedback pt 6 Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Integrate feedback pt 7 Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Integrate feedback pt 8 Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> * Format * Integrate new bday modal * Use public agent for getServices * Update casing --------- Co-authored-by: Eric Bailey <git@esb.lol> Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com> Co-authored-by: Hailey <me@haileyok.com>
595 lines
18 KiB
TypeScript
595 lines
18 KiB
TypeScript
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'
|
|
import {observer} from 'mobx-react-lite'
|
|
import {
|
|
ActivityIndicator,
|
|
BackHandler,
|
|
Keyboard,
|
|
KeyboardAvoidingView,
|
|
Platform,
|
|
Pressable,
|
|
ScrollView,
|
|
StyleSheet,
|
|
TouchableOpacity,
|
|
View,
|
|
} from 'react-native'
|
|
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
|
import LinearGradient from 'react-native-linear-gradient'
|
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
|
import {RichText} from '@atproto/api'
|
|
import {useAnalytics} from 'lib/analytics/analytics'
|
|
import {useIsKeyboardVisible} from 'lib/hooks/useIsKeyboardVisible'
|
|
import {ExternalEmbed} from './ExternalEmbed'
|
|
import {Text} from '../util/text/Text'
|
|
import * as Toast from '../util/Toast'
|
|
// TODO: Prevent naming components that coincide with RN primitives
|
|
// due to linting false positives
|
|
import {TextInput, TextInputRef} from './text-input/TextInput'
|
|
import {CharProgress} from './char-progress/CharProgress'
|
|
import {UserAvatar} from '../util/UserAvatar'
|
|
import * as apilib from 'lib/api/index'
|
|
import {ComposerOpts} from 'state/shell/composer'
|
|
import {s, colors, gradients} from 'lib/styles'
|
|
import {cleanError} from 'lib/strings/errors'
|
|
import {shortenLinks} from 'lib/strings/rich-text-manip'
|
|
import {toShortUrl} from 'lib/strings/url-helpers'
|
|
import {SelectPhotoBtn} from './photos/SelectPhotoBtn'
|
|
import {OpenCameraBtn} from './photos/OpenCameraBtn'
|
|
import {ThreadgateBtn} from './threadgate/ThreadgateBtn'
|
|
import {usePalette} from 'lib/hooks/usePalette'
|
|
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
|
import {useExternalLinkFetch} from './useExternalLinkFetch'
|
|
import {isWeb, isNative, isAndroid, isIOS} from 'platform/detection'
|
|
import {QuoteEmbed} from '../util/post-embeds/QuoteEmbed'
|
|
import {GalleryModel} from 'state/models/media/gallery'
|
|
import {Gallery} from './photos/Gallery'
|
|
import {MAX_GRAPHEME_LENGTH} from 'lib/constants'
|
|
import {LabelsBtn} from './labels/LabelsBtn'
|
|
import {SelectLangBtn} from './select-language/SelectLangBtn'
|
|
import {SuggestedLanguage} from './select-language/SuggestedLanguage'
|
|
import {insertMentionAt} from 'lib/strings/mention-manip'
|
|
import {Trans, msg} from '@lingui/macro'
|
|
import {useLingui} from '@lingui/react'
|
|
import {useModals} from '#/state/modals'
|
|
import {useRequireAltTextEnabled} from '#/state/preferences'
|
|
import {
|
|
useLanguagePrefs,
|
|
useLanguagePrefsApi,
|
|
toPostLanguages,
|
|
} from '#/state/preferences/languages'
|
|
import {useSession, getAgent} from '#/state/session'
|
|
import {useProfileQuery} from '#/state/queries/profile'
|
|
import {useComposerControls} from '#/state/shell/composer'
|
|
import {emitPostCreated} from '#/state/events'
|
|
import {ThreadgateSetting} from '#/state/queries/threadgate'
|
|
import {logger} from '#/logger'
|
|
import {ComposerReplyTo} from 'view/com/composer/ComposerReplyTo'
|
|
import * as Prompt from '#/components/Prompt'
|
|
import {useDialogStateControlContext} from 'state/dialogs'
|
|
import {logEvent} from '#/lib/statsig/statsig'
|
|
|
|
type Props = ComposerOpts
|
|
export const ComposePost = observer(function ComposePost({
|
|
replyTo,
|
|
onPost,
|
|
quote: initQuote,
|
|
mention: initMention,
|
|
openPicker,
|
|
text: initText,
|
|
imageUris: initImageUris,
|
|
}: Props) {
|
|
const {currentAccount} = useSession()
|
|
const {data: currentProfile} = useProfileQuery({did: currentAccount!.did})
|
|
const {isModalActive} = useModals()
|
|
const {closeComposer} = useComposerControls()
|
|
const {track} = useAnalytics()
|
|
const pal = usePalette('default')
|
|
const {isDesktop, isMobile} = useWebMediaQueries()
|
|
const {_} = useLingui()
|
|
const requireAltTextEnabled = useRequireAltTextEnabled()
|
|
const langPrefs = useLanguagePrefs()
|
|
const setLangPrefs = useLanguagePrefsApi()
|
|
const textInput = useRef<TextInputRef>(null)
|
|
const discardPromptControl = Prompt.usePromptControl()
|
|
const {closeAllDialogs} = useDialogStateControlContext()
|
|
|
|
const [isKeyboardVisible] = useIsKeyboardVisible({iosUseWillEvents: true})
|
|
const [isProcessing, setIsProcessing] = useState(false)
|
|
const [processingState, setProcessingState] = useState('')
|
|
const [error, setError] = useState('')
|
|
const [richtext, setRichText] = useState(
|
|
new RichText({
|
|
text: initText
|
|
? initText
|
|
: initMention
|
|
? insertMentionAt(
|
|
`@${initMention}`,
|
|
initMention.length + 1,
|
|
`${initMention}`,
|
|
) // insert mention if passed in
|
|
: '',
|
|
}),
|
|
)
|
|
const graphemeLength = useMemo(() => {
|
|
return shortenLinks(richtext).graphemeLength
|
|
}, [richtext])
|
|
const [quote, setQuote] = useState<ComposerOpts['quote'] | undefined>(
|
|
initQuote,
|
|
)
|
|
const {extLink, setExtLink} = useExternalLinkFetch({setQuote})
|
|
const [labels, setLabels] = useState<string[]>([])
|
|
const [threadgate, setThreadgate] = useState<ThreadgateSetting[]>([])
|
|
const [suggestedLinks, setSuggestedLinks] = useState<Set<string>>(new Set())
|
|
const gallery = useMemo(
|
|
() => new GalleryModel(initImageUris),
|
|
[initImageUris],
|
|
)
|
|
const onClose = useCallback(() => {
|
|
closeComposer()
|
|
}, [closeComposer])
|
|
|
|
const insets = useSafeAreaInsets()
|
|
const viewStyles = useMemo(
|
|
() => ({
|
|
paddingBottom:
|
|
isAndroid || (isIOS && !isKeyboardVisible) ? insets.bottom : 0,
|
|
paddingTop: isAndroid ? insets.top : isMobile ? 15 : 0,
|
|
}),
|
|
[insets, isKeyboardVisible, isMobile],
|
|
)
|
|
|
|
const onPressCancel = useCallback(() => {
|
|
if (graphemeLength > 0 || !gallery.isEmpty) {
|
|
closeAllDialogs()
|
|
if (Keyboard) {
|
|
Keyboard.dismiss()
|
|
}
|
|
discardPromptControl.open()
|
|
} else {
|
|
onClose()
|
|
}
|
|
}, [
|
|
graphemeLength,
|
|
gallery.isEmpty,
|
|
closeAllDialogs,
|
|
discardPromptControl,
|
|
onClose,
|
|
])
|
|
// android back button
|
|
useEffect(() => {
|
|
if (!isAndroid) {
|
|
return
|
|
}
|
|
const backHandler = BackHandler.addEventListener(
|
|
'hardwareBackPress',
|
|
() => {
|
|
onPressCancel()
|
|
return true
|
|
},
|
|
)
|
|
|
|
return () => {
|
|
backHandler.remove()
|
|
}
|
|
}, [onPressCancel])
|
|
|
|
// listen to escape key on desktop web
|
|
const onEscape = useCallback(
|
|
(e: KeyboardEvent) => {
|
|
if (e.key === 'Escape') {
|
|
onPressCancel()
|
|
}
|
|
},
|
|
[onPressCancel],
|
|
)
|
|
useEffect(() => {
|
|
if (isWeb && !isModalActive) {
|
|
window.addEventListener('keydown', onEscape)
|
|
return () => window.removeEventListener('keydown', onEscape)
|
|
}
|
|
}, [onEscape, isModalActive])
|
|
|
|
const onPressAddLinkCard = useCallback(
|
|
(uri: string) => {
|
|
setExtLink({uri, isLoading: true})
|
|
},
|
|
[setExtLink],
|
|
)
|
|
|
|
const onPhotoPasted = useCallback(
|
|
async (uri: string) => {
|
|
track('Composer:PastedPhotos')
|
|
await gallery.paste(uri)
|
|
},
|
|
[gallery, track],
|
|
)
|
|
|
|
const onPressPublish = async () => {
|
|
if (isProcessing || graphemeLength > MAX_GRAPHEME_LENGTH) {
|
|
return
|
|
}
|
|
if (requireAltTextEnabled && gallery.needsAltText) {
|
|
return
|
|
}
|
|
|
|
setError('')
|
|
|
|
if (richtext.text.trim().length === 0 && gallery.isEmpty && !extLink) {
|
|
setError(_(msg`Did you want to say anything?`))
|
|
return
|
|
}
|
|
if (extLink?.isLoading) {
|
|
setError(_(msg`Please wait for your link card to finish loading`))
|
|
return
|
|
}
|
|
|
|
setIsProcessing(true)
|
|
|
|
let postUri
|
|
try {
|
|
postUri = (
|
|
await apilib.post(getAgent(), {
|
|
rawText: richtext.text,
|
|
replyTo: replyTo?.uri,
|
|
images: gallery.images,
|
|
quote,
|
|
extLink,
|
|
labels,
|
|
threadgate,
|
|
onStateChange: setProcessingState,
|
|
langs: toPostLanguages(langPrefs.postLanguage),
|
|
})
|
|
).uri
|
|
} catch (e: any) {
|
|
logger.error(e, {
|
|
message: `Composer: create post failed`,
|
|
hasImages: gallery.size > 0,
|
|
})
|
|
|
|
if (extLink) {
|
|
setExtLink({
|
|
...extLink,
|
|
isLoading: true,
|
|
localThumb: undefined,
|
|
} as apilib.ExternalEmbedDraft)
|
|
}
|
|
setError(cleanError(e.message))
|
|
setIsProcessing(false)
|
|
return
|
|
} finally {
|
|
if (postUri) {
|
|
logEvent('post:create', {
|
|
imageCount: gallery.size,
|
|
isReply: replyTo != null,
|
|
hasLink: extLink != null,
|
|
hasQuote: quote != null,
|
|
langs: langPrefs.postLanguage,
|
|
logContext: 'Composer',
|
|
})
|
|
}
|
|
track('Create Post', {
|
|
imageCount: gallery.size,
|
|
})
|
|
if (replyTo && replyTo.uri) track('Post:Reply')
|
|
}
|
|
if (postUri && !replyTo) {
|
|
emitPostCreated()
|
|
}
|
|
setLangPrefs.savePostLanguageToHistory()
|
|
onPost?.()
|
|
onClose()
|
|
Toast.show(
|
|
replyTo
|
|
? _(msg`Your reply has been published`)
|
|
: _(msg`Your post has been published`),
|
|
)
|
|
}
|
|
|
|
const canPost = useMemo(
|
|
() =>
|
|
graphemeLength <= MAX_GRAPHEME_LENGTH &&
|
|
(!requireAltTextEnabled || !gallery.needsAltText),
|
|
[graphemeLength, requireAltTextEnabled, gallery.needsAltText],
|
|
)
|
|
const selectTextInputPlaceholder = replyTo
|
|
? _(msg`Write your reply`)
|
|
: _(msg`What's up?`)
|
|
|
|
const canSelectImages = useMemo(() => gallery.size < 4, [gallery.size])
|
|
const hasMedia = gallery.size > 0 || Boolean(extLink)
|
|
|
|
const onEmojiButtonPress = useCallback(() => {
|
|
openPicker?.(textInput.current?.getCursorPosition())
|
|
}, [openPicker])
|
|
|
|
return (
|
|
<KeyboardAvoidingView
|
|
testID="composePostView"
|
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
|
style={styles.outer}>
|
|
<View style={[s.flex1, viewStyles]} aria-modal accessibilityViewIsModal>
|
|
<View style={[styles.topbar, isDesktop && styles.topbarDesktop]}>
|
|
<TouchableOpacity
|
|
testID="composerDiscardButton"
|
|
onPress={onPressCancel}
|
|
onAccessibilityEscape={onPressCancel}
|
|
accessibilityRole="button"
|
|
accessibilityLabel={_(msg`Cancel`)}
|
|
accessibilityHint={_(
|
|
msg`Closes post composer and discards post draft`,
|
|
)}>
|
|
<Text style={[pal.link, s.f18]}>
|
|
<Trans>Cancel</Trans>
|
|
</Text>
|
|
</TouchableOpacity>
|
|
<View style={s.flex1} />
|
|
{isProcessing ? (
|
|
<>
|
|
<Text style={pal.textLight}>{processingState}</Text>
|
|
<View style={styles.postBtn}>
|
|
<ActivityIndicator />
|
|
</View>
|
|
</>
|
|
) : (
|
|
<>
|
|
<LabelsBtn
|
|
labels={labels}
|
|
onChange={setLabels}
|
|
hasMedia={hasMedia}
|
|
/>
|
|
{replyTo ? null : (
|
|
<ThreadgateBtn
|
|
threadgate={threadgate}
|
|
onChange={setThreadgate}
|
|
/>
|
|
)}
|
|
{canPost ? (
|
|
<TouchableOpacity
|
|
testID="composerPublishBtn"
|
|
onPress={onPressPublish}
|
|
accessibilityRole="button"
|
|
accessibilityLabel={
|
|
replyTo ? _(msg`Publish reply`) : _(msg`Publish post`)
|
|
}
|
|
accessibilityHint="">
|
|
<LinearGradient
|
|
colors={[
|
|
gradients.blueLight.start,
|
|
gradients.blueLight.end,
|
|
]}
|
|
start={{x: 0, y: 0}}
|
|
end={{x: 1, y: 1}}
|
|
style={styles.postBtn}>
|
|
<Text style={[s.white, s.f16, s.bold]}>
|
|
{replyTo ? (
|
|
<Trans context="action">Reply</Trans>
|
|
) : (
|
|
<Trans context="action">Post</Trans>
|
|
)}
|
|
</Text>
|
|
</LinearGradient>
|
|
</TouchableOpacity>
|
|
) : (
|
|
<View style={[styles.postBtn, pal.btn]}>
|
|
<Text style={[pal.textLight, s.f16, s.bold]}>
|
|
<Trans context="action">Post</Trans>
|
|
</Text>
|
|
</View>
|
|
)}
|
|
</>
|
|
)}
|
|
</View>
|
|
{requireAltTextEnabled && gallery.needsAltText && (
|
|
<View style={[styles.reminderLine, pal.viewLight]}>
|
|
<View style={styles.errorIcon}>
|
|
<FontAwesomeIcon
|
|
icon="exclamation"
|
|
style={{color: colors.red4}}
|
|
size={10}
|
|
/>
|
|
</View>
|
|
<Text style={[pal.text, s.flex1]}>
|
|
<Trans>One or more images is missing alt text.</Trans>
|
|
</Text>
|
|
</View>
|
|
)}
|
|
{error !== '' && (
|
|
<View style={styles.errorLine}>
|
|
<View style={styles.errorIcon}>
|
|
<FontAwesomeIcon
|
|
icon="exclamation"
|
|
style={{color: colors.red4}}
|
|
size={10}
|
|
/>
|
|
</View>
|
|
<Text style={[s.red4, s.flex1]}>{error}</Text>
|
|
</View>
|
|
)}
|
|
<ScrollView
|
|
style={styles.scrollView}
|
|
keyboardShouldPersistTaps="always">
|
|
{replyTo ? <ComposerReplyTo replyTo={replyTo} /> : undefined}
|
|
|
|
<View
|
|
style={[
|
|
pal.border,
|
|
styles.textInputLayout,
|
|
isNative && styles.textInputLayoutMobile,
|
|
]}>
|
|
<UserAvatar avatar={currentProfile?.avatar} size={50} />
|
|
<TextInput
|
|
ref={textInput}
|
|
richtext={richtext}
|
|
placeholder={selectTextInputPlaceholder}
|
|
suggestedLinks={suggestedLinks}
|
|
autoFocus={true}
|
|
setRichText={setRichText}
|
|
onPhotoPasted={onPhotoPasted}
|
|
onPressPublish={onPressPublish}
|
|
onSuggestedLinksChanged={setSuggestedLinks}
|
|
onError={setError}
|
|
accessible={true}
|
|
accessibilityLabel={_(msg`Write post`)}
|
|
accessibilityHint={_(
|
|
msg`Compose posts up to ${MAX_GRAPHEME_LENGTH} characters in length`,
|
|
)}
|
|
/>
|
|
</View>
|
|
|
|
<Gallery gallery={gallery} />
|
|
{gallery.isEmpty && extLink && (
|
|
<ExternalEmbed
|
|
link={extLink}
|
|
onRemove={() => setExtLink(undefined)}
|
|
/>
|
|
)}
|
|
{quote ? (
|
|
<View style={[s.mt5, isWeb && s.mb10]}>
|
|
<QuoteEmbed quote={quote} />
|
|
</View>
|
|
) : undefined}
|
|
</ScrollView>
|
|
{!extLink && suggestedLinks.size > 0 ? (
|
|
<View style={s.mb5}>
|
|
{Array.from(suggestedLinks)
|
|
.slice(0, 3)
|
|
.map(url => (
|
|
<TouchableOpacity
|
|
key={`suggested-${url}`}
|
|
testID="addLinkCardBtn"
|
|
style={[pal.borderDark, styles.addExtLinkBtn]}
|
|
onPress={() => onPressAddLinkCard(url)}
|
|
accessibilityRole="button"
|
|
accessibilityLabel={_(msg`Add link card`)}
|
|
accessibilityHint={_(
|
|
msg`Creates a card with a thumbnail. The card links to ${url}`,
|
|
)}>
|
|
<Text style={pal.text}>
|
|
<Trans>Add link card:</Trans>{' '}
|
|
<Text style={[pal.link, s.ml5]}>{toShortUrl(url)}</Text>
|
|
</Text>
|
|
</TouchableOpacity>
|
|
))}
|
|
</View>
|
|
) : null}
|
|
<SuggestedLanguage text={richtext.text} />
|
|
<View style={[pal.border, styles.bottomBar]}>
|
|
{canSelectImages ? (
|
|
<>
|
|
<SelectPhotoBtn gallery={gallery} />
|
|
<OpenCameraBtn gallery={gallery} />
|
|
</>
|
|
) : null}
|
|
{!isMobile ? (
|
|
<Pressable
|
|
onPress={onEmojiButtonPress}
|
|
accessibilityRole="button"
|
|
accessibilityLabel={_(msg`Open emoji picker`)}
|
|
accessibilityHint={_(msg`Open emoji picker`)}>
|
|
<FontAwesomeIcon
|
|
icon={['far', 'face-smile']}
|
|
color={pal.colors.link}
|
|
size={22}
|
|
/>
|
|
</Pressable>
|
|
) : null}
|
|
<View style={s.flex1} />
|
|
<SelectLangBtn />
|
|
<CharProgress count={graphemeLength} />
|
|
</View>
|
|
</View>
|
|
|
|
<Prompt.Basic
|
|
control={discardPromptControl}
|
|
title={_(msg`Discard draft?`)}
|
|
description={_(msg`Are you sure you'd like to discard this draft?`)}
|
|
onConfirm={onClose}
|
|
confirmButtonCta={_(msg`Discard`)}
|
|
confirmButtonColor="negative"
|
|
/>
|
|
</KeyboardAvoidingView>
|
|
)
|
|
})
|
|
|
|
const styles = StyleSheet.create({
|
|
outer: {
|
|
flexDirection: 'column',
|
|
flex: 1,
|
|
height: '100%',
|
|
},
|
|
topbar: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
paddingTop: 6,
|
|
paddingBottom: 4,
|
|
paddingHorizontal: 20,
|
|
height: 55,
|
|
gap: 4,
|
|
},
|
|
topbarDesktop: {
|
|
paddingTop: 10,
|
|
paddingBottom: 10,
|
|
},
|
|
postBtn: {
|
|
borderRadius: 20,
|
|
paddingHorizontal: 20,
|
|
paddingVertical: 6,
|
|
marginLeft: 12,
|
|
},
|
|
errorLine: {
|
|
flexDirection: 'row',
|
|
backgroundColor: colors.red1,
|
|
borderRadius: 6,
|
|
marginHorizontal: 15,
|
|
paddingHorizontal: 8,
|
|
paddingVertical: 6,
|
|
marginVertical: 6,
|
|
},
|
|
reminderLine: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
borderRadius: 6,
|
|
marginHorizontal: 15,
|
|
paddingHorizontal: 8,
|
|
paddingVertical: 6,
|
|
marginBottom: 6,
|
|
},
|
|
errorIcon: {
|
|
borderWidth: 1,
|
|
borderColor: colors.red4,
|
|
color: colors.red4,
|
|
borderRadius: 30,
|
|
width: 16,
|
|
height: 16,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
marginRight: 5,
|
|
},
|
|
scrollView: {
|
|
flex: 1,
|
|
paddingHorizontal: 15,
|
|
},
|
|
textInputLayout: {
|
|
flexDirection: 'row',
|
|
borderTopWidth: 1,
|
|
paddingTop: 16,
|
|
},
|
|
textInputLayoutMobile: {
|
|
flex: 1,
|
|
},
|
|
addExtLinkBtn: {
|
|
borderWidth: 1,
|
|
borderRadius: 24,
|
|
paddingHorizontal: 16,
|
|
paddingVertical: 12,
|
|
marginHorizontal: 10,
|
|
marginBottom: 4,
|
|
},
|
|
bottomBar: {
|
|
flexDirection: 'row',
|
|
paddingVertical: 10,
|
|
paddingLeft: 15,
|
|
paddingRight: 20,
|
|
alignItems: 'center',
|
|
borderTopWidth: 1,
|
|
},
|
|
})
|