Draft previews (#9803)
* Send deviceId and platform
* Add deviceId and deviceName to drafts, skip loading media for other devies
* WIP new preview
* show rich text in drafts list
(cherry picked from commit fb70d53d59)
* New draft preview UI
* Tighten up spacing in draft list
* Add i18n comments
---------
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
This commit is contained in:
@@ -119,6 +119,7 @@ module.exports = function (_config) {
|
|||||||
'com.apple.developer.kernel.increased-memory-limit': true,
|
'com.apple.developer.kernel.increased-memory-limit': true,
|
||||||
'com.apple.developer.kernel.extended-virtual-addressing': true,
|
'com.apple.developer.kernel.extended-virtual-addressing': true,
|
||||||
'com.apple.security.application-groups': 'group.app.bsky',
|
'com.apple.security.application-groups': 'group.app.bsky',
|
||||||
|
'com.apple.developer.device-information.user-assigned-device-name': true,
|
||||||
},
|
},
|
||||||
privacyManifests: {
|
privacyManifests: {
|
||||||
NSPrivacyCollectedDataTypes: [
|
NSPrivacyCollectedDataTypes: [
|
||||||
|
|||||||
+1
-1
@@ -73,7 +73,7 @@
|
|||||||
"icons:optimize": "svgo -f ./assets/icons"
|
"icons:optimize": "svgo -f ./assets/icons"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@atproto/api": "^0.18.18",
|
"@atproto/api": "^0.18.20",
|
||||||
"@bitdrift/react-native": "^0.6.8",
|
"@bitdrift/react-native": "^0.6.8",
|
||||||
"@braintree/sanitize-url": "^6.0.2",
|
"@braintree/sanitize-url": "^6.0.2",
|
||||||
"@bsky.app/alf": "^0.1.6",
|
"@bsky.app/alf": "^0.1.6",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import {useMemo} from 'react'
|
||||||
import {type StyleProp, type TextStyle} from 'react-native'
|
import {type StyleProp, type TextStyle} from 'react-native'
|
||||||
import {AppBskyRichtextFacet, RichText as RichTextAPI} from '@atproto/api'
|
import {AppBskyRichtextFacet, RichText as RichTextAPI} from '@atproto/api'
|
||||||
|
|
||||||
@@ -27,6 +27,16 @@ export type RichTextProps = TextStyleProp &
|
|||||||
interactiveStyle?: StyleProp<TextStyle>
|
interactiveStyle?: StyleProp<TextStyle>
|
||||||
emojiMultiplier?: number
|
emojiMultiplier?: number
|
||||||
shouldProxyLinks?: boolean
|
shouldProxyLinks?: boolean
|
||||||
|
/**
|
||||||
|
* DANGEROUS: Disable facet lexicon validation
|
||||||
|
*
|
||||||
|
* `detectFacetsWithoutResolution()` generates technically invalid facets,
|
||||||
|
* with a handle in place of the DID. This means that RichText that uses it
|
||||||
|
* won't be able to render links.
|
||||||
|
*
|
||||||
|
* Use with care - only use if you're rendering facets you're generating yourself.
|
||||||
|
*/
|
||||||
|
disableMentionFacetValidation?: true
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RichText({
|
export function RichText({
|
||||||
@@ -44,12 +54,17 @@ export function RichText({
|
|||||||
onLayout,
|
onLayout,
|
||||||
onTextLayout,
|
onTextLayout,
|
||||||
shouldProxyLinks,
|
shouldProxyLinks,
|
||||||
|
disableMentionFacetValidation,
|
||||||
}: RichTextProps) {
|
}: RichTextProps) {
|
||||||
const richText = React.useMemo(
|
const richText = useMemo(() => {
|
||||||
() =>
|
if (value instanceof RichTextAPI) {
|
||||||
value instanceof RichTextAPI ? value : new RichTextAPI({text: value}),
|
return value
|
||||||
[value],
|
} else {
|
||||||
)
|
const rt = new RichTextAPI({text: value})
|
||||||
|
rt.detectFacetsWithoutResolution()
|
||||||
|
return rt
|
||||||
|
}
|
||||||
|
}, [value])
|
||||||
|
|
||||||
const plainStyles = [a.leading_snug, style]
|
const plainStyles = [a.leading_snug, style]
|
||||||
const interactiveStyles = [plainStyles, interactiveStyle]
|
const interactiveStyles = [plainStyles, interactiveStyle]
|
||||||
@@ -98,9 +113,11 @@ export function RichText({
|
|||||||
const link = segment.link
|
const link = segment.link
|
||||||
const mention = segment.mention
|
const mention = segment.mention
|
||||||
const tag = segment.tag
|
const tag = segment.tag
|
||||||
|
|
||||||
if (
|
if (
|
||||||
mention &&
|
mention &&
|
||||||
AppBskyRichtextFacet.validateMention(mention).success &&
|
(disableMentionFacetValidation ||
|
||||||
|
AppBskyRichtextFacet.validateMention(mention).success) &&
|
||||||
!disableLinks
|
!disableLinks
|
||||||
) {
|
) {
|
||||||
els.push(
|
els.push(
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import * as Device from 'expo-device'
|
||||||
|
|
||||||
|
import * as env from '#/env'
|
||||||
|
|
||||||
|
export const FALLBACK_ANDROID = 'Android'
|
||||||
|
export const FALLBACK_IOS = 'iOS'
|
||||||
|
export const FALLBACK_WEB = 'Web'
|
||||||
|
|
||||||
|
export function getDeviceName(): string {
|
||||||
|
const deviceName = Device.deviceName
|
||||||
|
if (env.IS_ANDROID) {
|
||||||
|
return deviceName || FALLBACK_ANDROID
|
||||||
|
} else if (env.IS_IOS) {
|
||||||
|
return deviceName || FALLBACK_IOS
|
||||||
|
} else {
|
||||||
|
return FALLBACK_WEB // could append browser info here
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -138,7 +138,7 @@ import {
|
|||||||
type RestoredVideo,
|
type RestoredVideo,
|
||||||
} from './drafts/state/api'
|
} from './drafts/state/api'
|
||||||
import {
|
import {
|
||||||
loadDraft,
|
loadDraftMedia,
|
||||||
useCleanupPublishedDraftMutation,
|
useCleanupPublishedDraftMutation,
|
||||||
useSaveDraftMutation,
|
useSaveDraftMutation,
|
||||||
} from './drafts/state/queries'
|
} from './drafts/state/queries'
|
||||||
@@ -482,7 +482,7 @@ export const ComposePost = ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Load local media files for the draft
|
// Load local media files for the draft
|
||||||
const {loadedMedia} = await loadDraft(draftSummary.draft)
|
const {loadedMedia} = await loadDraftMedia(draftSummary.draft)
|
||||||
|
|
||||||
// Extract original localRefs for orphan detection on save
|
// Extract original localRefs for orphan detection on save
|
||||||
const originalLocalRefs = extractLocalRefs(draftSummary.draft)
|
const originalLocalRefs = extractLocalRefs(draftSummary.draft)
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
import {useCallback, useEffect, useState} from 'react'
|
import {useCallback, useEffect, useMemo, useState} from 'react'
|
||||||
import {Pressable, View} from 'react-native'
|
import {Pressable, View} from 'react-native'
|
||||||
import * as VideoThumbnails from 'expo-video-thumbnails'
|
import * as VideoThumbnails from 'expo-video-thumbnails'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, plural} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
|
import * as device from '#/lib/deviceName'
|
||||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
|
||||||
import {useCurrentAccountProfile} from '#/state/queries/useCurrentAccountProfile'
|
|
||||||
import {logger} from '#/view/com/composer/drafts/state/logger'
|
import {logger} from '#/view/com/composer/drafts/state/logger'
|
||||||
import {TimeElapsed} from '#/view/com/util/TimeElapsed'
|
import {TimeElapsed} from '#/view/com/util/TimeElapsed'
|
||||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
import {atoms as a, select, useTheme} from '#/alf'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {Button} from '#/components/Button'
|
||||||
import {Button, ButtonIcon} from '#/components/Button'
|
import {CirclePlus_Stroke2_Corner0_Rounded as CirclePlusIcon} from '#/components/icons/CirclePlus'
|
||||||
|
import {type Props as SVGIconProps} from '#/components/icons/common'
|
||||||
import {DotGrid_Stroke2_Corner0_Rounded as DotsIcon} from '#/components/icons/DotGrid'
|
import {DotGrid_Stroke2_Corner0_Rounded as DotsIcon} from '#/components/icons/DotGrid'
|
||||||
|
import {CloseQuote_Stroke2_Corner0_Rounded as CloseQuoteIcon} from '#/components/icons/Quote'
|
||||||
|
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
|
||||||
import * as MediaPreview from '#/components/MediaPreview'
|
import * as MediaPreview from '#/components/MediaPreview'
|
||||||
import * as Prompt from '#/components/Prompt'
|
import * as Prompt from '#/components/Prompt'
|
||||||
|
import {RichText} from '#/components/RichText'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {IS_WEB} from '#/env'
|
import {IS_WEB} from '#/env'
|
||||||
import {type DraftPostDisplay, type DraftSummary} from './state/schema'
|
import {type DraftPostDisplay, type DraftSummary} from './state/schema'
|
||||||
@@ -32,6 +34,34 @@ export function DraftItem({
|
|||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const discardPromptControl = Prompt.usePromptControl()
|
const discardPromptControl = Prompt.usePromptControl()
|
||||||
|
const post = draft.posts[0]
|
||||||
|
|
||||||
|
const mediaExistsOnOtherDevice =
|
||||||
|
!draft.meta.isOriginatingDevice && draft.meta.hasMissingMedia
|
||||||
|
const mediaIsMissing =
|
||||||
|
draft.meta.isOriginatingDevice && draft.meta.hasMissingMedia
|
||||||
|
const hasMetadata =
|
||||||
|
draft.meta.replyCount > 0 ||
|
||||||
|
mediaExistsOnOtherDevice ||
|
||||||
|
draft.meta.hasQuotes
|
||||||
|
|
||||||
|
const deviceName = useMemo(() => {
|
||||||
|
const raw = draft.draft.deviceName
|
||||||
|
let name = raw
|
||||||
|
switch (raw) {
|
||||||
|
case device.FALLBACK_IOS:
|
||||||
|
case device.FALLBACK_ANDROID:
|
||||||
|
case device.FALLBACK_WEB:
|
||||||
|
name = _(
|
||||||
|
msg({
|
||||||
|
message: `another device`,
|
||||||
|
comment: `Prefixed with "This media is stored on...". Example: "This media is stored on another device"`,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}, [_, draft])
|
||||||
|
|
||||||
const handleDelete = useCallback(() => {
|
const handleDelete = useCallback(() => {
|
||||||
onDelete(draft)
|
onDelete(draft)
|
||||||
@@ -39,6 +69,7 @@ export function DraftItem({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<View style={[a.relative]}>
|
||||||
<Pressable
|
<Pressable
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel={_(msg`Open draft`)}
|
accessibilityLabel={_(msg`Open draft`)}
|
||||||
@@ -46,42 +77,162 @@ export function DraftItem({
|
|||||||
onPress={() => onSelect(draft)}
|
onPress={() => onSelect(draft)}
|
||||||
style={({pressed, hovered}) => [
|
style={({pressed, hovered}) => [
|
||||||
a.rounded_md,
|
a.rounded_md,
|
||||||
a.overflow_hidden,
|
|
||||||
a.border,
|
a.border,
|
||||||
t.atoms.bg,
|
|
||||||
t.atoms.border_contrast_low,
|
|
||||||
t.atoms.shadow_sm,
|
t.atoms.shadow_sm,
|
||||||
(pressed || hovered) && t.atoms.bg_contrast_25,
|
pressed || hovered
|
||||||
|
? t.atoms.border_contrast_medium
|
||||||
|
: t.atoms.border_contrast_low,
|
||||||
|
{
|
||||||
|
backgroundColor: select(t.name, {
|
||||||
|
light: t.atoms.bg.backgroundColor,
|
||||||
|
dark: t.atoms.bg_contrast_25.backgroundColor,
|
||||||
|
dim: t.atoms.bg_contrast_25.backgroundColor,
|
||||||
|
}),
|
||||||
|
},
|
||||||
]}>
|
]}>
|
||||||
<View style={[a.p_md, a.gap_sm]}>
|
|
||||||
{draft.hasMissingMedia && (
|
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
a.rounded_sm,
|
a.rounded_md,
|
||||||
a.px_sm,
|
a.overflow_hidden,
|
||||||
a.py_xs,
|
a.p_lg,
|
||||||
a.mb_xs,
|
a.pb_md,
|
||||||
t.atoms.bg_contrast_50,
|
a.gap_sm,
|
||||||
|
{
|
||||||
|
paddingTop: 20 + a.pt_md.paddingTop,
|
||||||
|
},
|
||||||
]}>
|
]}>
|
||||||
<Text style={[a.text_xs, t.atoms.text_contrast_medium]}>
|
<RichText
|
||||||
<Trans>Some media unavailable (saved on another device)</Trans>
|
style={[a.text_md, a.leading_snug, a.pointer_events_none]}
|
||||||
</Text>
|
value={post.text}
|
||||||
|
enableTags
|
||||||
|
disableMentionFacetValidation
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!mediaExistsOnOtherDevice && <DraftMediaPreview post={post} />}
|
||||||
|
|
||||||
|
{hasMetadata && (
|
||||||
|
<View style={[a.gap_xs]}>
|
||||||
|
{mediaExistsOnOtherDevice && (
|
||||||
|
<DraftMetadataTag
|
||||||
|
icon={WarningIcon}
|
||||||
|
text={_(
|
||||||
|
msg({
|
||||||
|
message: `Media stored on ${deviceName}`,
|
||||||
|
comment: `This media is stored on... Example: "This media is stored on John's iPhone"`,
|
||||||
|
}),
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{mediaIsMissing && (
|
||||||
|
<DraftMetadataTag
|
||||||
|
display="warning"
|
||||||
|
icon={WarningIcon}
|
||||||
|
text={_(msg`Missing media`)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{draft.meta.hasQuotes && (
|
||||||
|
<DraftMetadataTag
|
||||||
|
icon={CloseQuoteIcon}
|
||||||
|
text={_(msg`Quote post`)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{draft.meta.replyCount > 0 && (
|
||||||
|
<DraftMetadataTag
|
||||||
|
icon={CirclePlusIcon}
|
||||||
|
text={plural(draft.meta.replyCount, {
|
||||||
|
one: '1 more post',
|
||||||
|
other: '# more posts',
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{draft.posts.map((post, index) => (
|
|
||||||
<DraftPostRow
|
|
||||||
key={post.id}
|
|
||||||
post={post}
|
|
||||||
isFirst={index === 0}
|
|
||||||
isLast={index === draft.posts.length - 1}
|
|
||||||
timestamp={draft.updatedAt}
|
|
||||||
discardPromptControl={discardPromptControl}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</View>
|
</View>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
|
||||||
|
{/* Timestamp */}
|
||||||
|
<View
|
||||||
|
pointerEvents="none"
|
||||||
|
style={[
|
||||||
|
a.absolute,
|
||||||
|
a.pointer_events_none,
|
||||||
|
{
|
||||||
|
top: a.pt_md.paddingTop,
|
||||||
|
left: a.pl_lg.paddingLeft,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<TimeElapsed timestamp={draft.updatedAt}>
|
||||||
|
{({timeElapsed}) => (
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
a.text_sm,
|
||||||
|
t.atoms.text_contrast_medium,
|
||||||
|
a.leading_tight,
|
||||||
|
]}
|
||||||
|
numberOfLines={1}>
|
||||||
|
{timeElapsed}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</TimeElapsed>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Menu button */}
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.absolute,
|
||||||
|
{
|
||||||
|
top: a.pt_md.paddingTop,
|
||||||
|
right: a.pr_md.paddingRight,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<Button
|
||||||
|
label={_(msg`More options`)}
|
||||||
|
hitSlop={8}
|
||||||
|
onPress={e => {
|
||||||
|
e.stopPropagation()
|
||||||
|
discardPromptControl.open()
|
||||||
|
}}
|
||||||
|
style={[
|
||||||
|
a.pointer,
|
||||||
|
a.rounded_full,
|
||||||
|
{
|
||||||
|
height: 20,
|
||||||
|
width: 20,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
{({pressed, hovered}) => (
|
||||||
|
<>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.absolute,
|
||||||
|
a.rounded_full,
|
||||||
|
{
|
||||||
|
top: -4,
|
||||||
|
bottom: -4,
|
||||||
|
left: -4,
|
||||||
|
right: -4,
|
||||||
|
backgroundColor:
|
||||||
|
pressed || hovered
|
||||||
|
? select(t.name, {
|
||||||
|
light: t.atoms.bg_contrast_50.backgroundColor,
|
||||||
|
dark: t.atoms.bg_contrast_100.backgroundColor,
|
||||||
|
dim: t.atoms.bg_contrast_100.backgroundColor,
|
||||||
|
})
|
||||||
|
: 'transparent',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<DotsIcon
|
||||||
|
width={16}
|
||||||
|
fill={t.atoms.text_contrast_low.color}
|
||||||
|
style={[a.z_20]}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
<Prompt.Basic
|
<Prompt.Basic
|
||||||
control={discardPromptControl}
|
control={discardPromptControl}
|
||||||
title={_(msg`Discard draft?`)}
|
title={_(msg`Discard draft?`)}
|
||||||
@@ -94,125 +245,28 @@ export function DraftItem({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function DraftPostRow({
|
function DraftMetadataTag({
|
||||||
post,
|
display = 'info',
|
||||||
isFirst,
|
icon: Icon,
|
||||||
isLast,
|
text,
|
||||||
timestamp,
|
|
||||||
discardPromptControl,
|
|
||||||
}: {
|
}: {
|
||||||
post: DraftPostDisplay
|
display?: 'info' | 'warning'
|
||||||
isFirst: boolean
|
icon: React.ComponentType<SVGIconProps>
|
||||||
isLast: boolean
|
text: string
|
||||||
timestamp: string
|
|
||||||
discardPromptControl: Prompt.PromptControlProps
|
|
||||||
}) {
|
}) {
|
||||||
const {_} = useLingui()
|
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const profile = useCurrentAccountProfile()
|
const color = {
|
||||||
|
info: t.atoms.text_contrast_medium.color,
|
||||||
|
warning: select(t.name, {
|
||||||
|
light: '#C99A00',
|
||||||
|
dark: '#FFC404',
|
||||||
|
dim: '#FFC404',
|
||||||
|
}),
|
||||||
|
}[display]
|
||||||
return (
|
return (
|
||||||
<View style={[a.flex_row, a.gap_sm]}>
|
|
||||||
<View style={[a.align_center]}>
|
|
||||||
<UserAvatar type="user" size={42} avatar={profile?.avatar} />
|
|
||||||
{!isLast && (
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.flex_1,
|
|
||||||
a.mt_xs,
|
|
||||||
{
|
|
||||||
width: 2,
|
|
||||||
backgroundColor: t.palette.contrast_100,
|
|
||||||
minHeight: 8,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={[a.flex_1, a.gap_2xs]}>
|
|
||||||
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
|
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
|
||||||
<View style={[a.flex_row, a.align_center, a.flex_1, a.gap_xs]}>
|
<Icon size="sm" fill={color} />
|
||||||
{profile && (
|
<Text style={[a.text_sm, a.leading_tight, {color}]}>{text}</Text>
|
||||||
<>
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
a.text_md,
|
|
||||||
a.font_semi_bold,
|
|
||||||
t.atoms.text,
|
|
||||||
a.leading_snug,
|
|
||||||
]}
|
|
||||||
numberOfLines={1}>
|
|
||||||
{createSanitizedDisplayName(profile)}
|
|
||||||
</Text>
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
a.text_md,
|
|
||||||
t.atoms.text_contrast_medium,
|
|
||||||
a.leading_snug,
|
|
||||||
]}
|
|
||||||
numberOfLines={1}>
|
|
||||||
{sanitizeHandle(profile.handle)}
|
|
||||||
</Text>
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
a.text_md,
|
|
||||||
t.atoms.text_contrast_medium,
|
|
||||||
a.leading_snug,
|
|
||||||
]}>
|
|
||||||
·
|
|
||||||
</Text>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<TimeElapsed timestamp={timestamp}>
|
|
||||||
{({timeElapsed}) => (
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
a.text_md,
|
|
||||||
t.atoms.text_contrast_medium,
|
|
||||||
a.leading_snug,
|
|
||||||
]}
|
|
||||||
numberOfLines={1}>
|
|
||||||
{timeElapsed}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</TimeElapsed>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{isFirst && (
|
|
||||||
<Button
|
|
||||||
label={_(msg`More options`)}
|
|
||||||
variant="ghost"
|
|
||||||
color="secondary"
|
|
||||||
shape="round"
|
|
||||||
size="tiny"
|
|
||||||
onPress={e => {
|
|
||||||
e.stopPropagation()
|
|
||||||
discardPromptControl.open()
|
|
||||||
}}>
|
|
||||||
<ButtonIcon icon={DotsIcon} />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{post.text ? (
|
|
||||||
<Text style={[a.text_md, a.leading_snug, t.atoms.text]}>
|
|
||||||
{post.text}
|
|
||||||
</Text>
|
|
||||||
) : (
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
a.text_md,
|
|
||||||
a.leading_snug,
|
|
||||||
t.atoms.text_contrast_medium,
|
|
||||||
a.italic,
|
|
||||||
]}>
|
|
||||||
<Trans>(No text)</Trans>
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<DraftMediaPreview post={post} />
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -271,7 +325,7 @@ function DraftMediaPreview({post}: {post: DraftPostDisplay}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MediaPreview.Outer style={[a.pt_xs]}>
|
<MediaPreview.Outer>
|
||||||
{loadedImages.map((image, i) => (
|
{loadedImages.map((image, i) => (
|
||||||
<MediaPreview.ImageItem key={i} thumbnail={image.url} alt={image.alt} />
|
<MediaPreview.ImageItem key={i} thumbnail={image.url} alt={image.alt} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {useLingui} from '@lingui/react'
|
|||||||
|
|
||||||
import {useCallOnce} from '#/lib/once'
|
import {useCallOnce} from '#/lib/once'
|
||||||
import {EmptyState} from '#/view/com/util/EmptyState'
|
import {EmptyState} from '#/view/com/util/EmptyState'
|
||||||
import {atoms as a, useTheme, web} from '#/alf'
|
import {atoms as a, select, useBreakpoints, useTheme, web} from '#/alf'
|
||||||
import {Button, ButtonText} from '#/components/Button'
|
import {Button, ButtonText} from '#/components/Button'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
import {PageX_Stroke2_Corner0_Rounded_Large as PageXIcon} from '#/components/icons/PageX'
|
import {PageX_Stroke2_Corner0_Rounded_Large as PageXIcon} from '#/components/icons/PageX'
|
||||||
@@ -26,6 +26,7 @@ export function DraftsListDialog({
|
|||||||
}) {
|
}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
const {gtPhone} = useBreakpoints()
|
||||||
const ax = useAnalytics()
|
const ax = useAnalytics()
|
||||||
const {data, isLoading, hasNextPage, isFetchingNextPage, fetchNextPage} =
|
const {data, isLoading, hasNextPage, isFetchingNextPage, fetchNextPage} =
|
||||||
useDraftsQuery()
|
useDraftsQuery()
|
||||||
@@ -91,7 +92,7 @@ export function DraftsListDialog({
|
|||||||
const renderItem = useCallback(
|
const renderItem = useCallback(
|
||||||
({item}: {item: DraftSummary}) => {
|
({item}: {item: DraftSummary}) => {
|
||||||
return (
|
return (
|
||||||
<View style={[a.px_lg, a.mt_lg]}>
|
<View style={[gtPhone ? [a.px_md, a.pt_md] : [a.px_sm, a.pt_sm]]}>
|
||||||
<DraftItem
|
<DraftItem
|
||||||
draft={item}
|
draft={item}
|
||||||
onSelect={handleSelectDraft}
|
onSelect={handleSelectDraft}
|
||||||
@@ -100,7 +101,7 @@ export function DraftsListDialog({
|
|||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
[handleSelectDraft, handleDeleteDraft],
|
[handleSelectDraft, handleDeleteDraft, gtPhone],
|
||||||
)
|
)
|
||||||
|
|
||||||
const header = useMemo(
|
const header = useMemo(
|
||||||
@@ -162,7 +163,17 @@ export function DraftsListDialog({
|
|||||||
ListFooterComponent={footerComponent}
|
ListFooterComponent={footerComponent}
|
||||||
onEndReached={onEndReached}
|
onEndReached={onEndReached}
|
||||||
onEndReachedThreshold={0.5}
|
onEndReachedThreshold={0.5}
|
||||||
style={[t.atoms.bg_contrast_50, a.px_0, web({minHeight: 500})]}
|
style={[
|
||||||
|
a.px_0,
|
||||||
|
web({minHeight: 500}),
|
||||||
|
{
|
||||||
|
backgroundColor: select(t.name, {
|
||||||
|
light: t.palette.contrast_50,
|
||||||
|
dark: t.palette.contrast_0,
|
||||||
|
dim: '#000000',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]}
|
||||||
webInnerContentContainerStyle={[a.py_0]}
|
webInnerContentContainerStyle={[a.py_0]}
|
||||||
contentContainerStyle={[a.pb_xl]}
|
contentContainerStyle={[a.pb_xl]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {type AppBskyDraftDefs, AtUri, RichText} from '@atproto/api'
|
|||||||
import {nanoid} from 'nanoid/non-secure'
|
import {nanoid} from 'nanoid/non-secure'
|
||||||
|
|
||||||
import {resolveLink} from '#/lib/api/resolve'
|
import {resolveLink} from '#/lib/api/resolve'
|
||||||
|
import {getDeviceName} from '#/lib/deviceName'
|
||||||
import {getImageDim} from '#/lib/media/manip'
|
import {getImageDim} from '#/lib/media/manip'
|
||||||
import {mimeToExt} from '#/lib/media/video/util'
|
import {mimeToExt} from '#/lib/media/video/util'
|
||||||
import {type ComposerImage} from '#/state/gallery'
|
import {type ComposerImage} from '#/state/gallery'
|
||||||
@@ -17,8 +18,11 @@ import {
|
|||||||
type PostDraft,
|
type PostDraft,
|
||||||
} from '#/view/com/composer/state/composer'
|
} from '#/view/com/composer/state/composer'
|
||||||
import {type VideoState} from '#/view/com/composer/state/video'
|
import {type VideoState} from '#/view/com/composer/state/video'
|
||||||
|
import {type AnalyticsContextType} from '#/analytics'
|
||||||
|
import {getDeviceId} from '#/analytics/identifiers'
|
||||||
import {logger} from './logger'
|
import {logger} from './logger'
|
||||||
import {type DraftPostDisplay, type DraftSummary} from './schema'
|
import {type DraftPostDisplay, type DraftSummary} from './schema'
|
||||||
|
import * as storage from './storage'
|
||||||
|
|
||||||
const TENOR_HOSTNAME = 'media.tenor.com'
|
const TENOR_HOSTNAME = 'media.tenor.com'
|
||||||
|
|
||||||
@@ -66,6 +70,8 @@ export async function composerStateToDraft(state: ComposerState): Promise<{
|
|||||||
|
|
||||||
const draft: AppBskyDraftDefs.Draft = {
|
const draft: AppBskyDraftDefs.Draft = {
|
||||||
$type: 'app.bsky.draft.defs#draft',
|
$type: 'app.bsky.draft.defs#draft',
|
||||||
|
deviceId: getDeviceId(),
|
||||||
|
deviceName: getDeviceName().slice(0, 100), // max length of 100 in lex
|
||||||
posts,
|
posts,
|
||||||
threadgateAllow: threadgateAllowUISettingToAllowRecordValue(
|
threadgateAllow: threadgateAllowUISettingToAllowRecordValue(
|
||||||
state.thread.threadgate,
|
state.thread.threadgate,
|
||||||
@@ -265,16 +271,24 @@ function serializeGif(gifMedia: {
|
|||||||
* Convert server DraftView to DraftSummary for list display.
|
* Convert server DraftView to DraftSummary for list display.
|
||||||
* Also checks which media files exist locally.
|
* Also checks which media files exist locally.
|
||||||
*/
|
*/
|
||||||
export function draftViewToSummary(
|
export function draftViewToSummary({
|
||||||
view: AppBskyDraftDefs.DraftView,
|
view,
|
||||||
localMediaExists: (path: string) => boolean,
|
analytics,
|
||||||
): DraftSummary {
|
}: {
|
||||||
const firstPost = view.draft.posts[0]
|
view: AppBskyDraftDefs.DraftView
|
||||||
const previewText = firstPost?.text?.slice(0, 100) || ''
|
analytics: AnalyticsContextType
|
||||||
|
}): DraftSummary {
|
||||||
let mediaCount = 0
|
const meta = {
|
||||||
let hasMedia = false
|
isOriginatingDevice: view.draft.deviceId === getDeviceId(),
|
||||||
let hasMissingMedia = false
|
postCount: view.draft.posts.length,
|
||||||
|
// minus anchor post
|
||||||
|
replyCount: view.draft.posts.length - 1,
|
||||||
|
hasMedia: false,
|
||||||
|
hasMissingMedia: false,
|
||||||
|
mediaCount: 0,
|
||||||
|
hasQuotes: false,
|
||||||
|
quoteCount: 0,
|
||||||
|
}
|
||||||
|
|
||||||
const posts: DraftPostDisplay[] = view.draft.posts.map((post, index) => {
|
const posts: DraftPostDisplay[] = view.draft.posts.map((post, index) => {
|
||||||
const images: DraftPostDisplay['images'] = []
|
const images: DraftPostDisplay['images'] = []
|
||||||
@@ -284,11 +298,11 @@ export function draftViewToSummary(
|
|||||||
// Process images
|
// Process images
|
||||||
if (post.embedImages) {
|
if (post.embedImages) {
|
||||||
for (const img of post.embedImages) {
|
for (const img of post.embedImages) {
|
||||||
mediaCount++
|
meta.mediaCount++
|
||||||
hasMedia = true
|
meta.hasMedia = true
|
||||||
const exists = localMediaExists(img.localRef.path)
|
const exists = storage.mediaExists(img.localRef.path)
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
hasMissingMedia = true
|
meta.hasMissingMedia = true
|
||||||
}
|
}
|
||||||
images.push({
|
images.push({
|
||||||
localPath: img.localRef.path,
|
localPath: img.localRef.path,
|
||||||
@@ -301,11 +315,11 @@ export function draftViewToSummary(
|
|||||||
// Process videos
|
// Process videos
|
||||||
if (post.embedVideos) {
|
if (post.embedVideos) {
|
||||||
for (const vid of post.embedVideos) {
|
for (const vid of post.embedVideos) {
|
||||||
mediaCount++
|
meta.mediaCount++
|
||||||
hasMedia = true
|
meta.hasMedia = true
|
||||||
const exists = localMediaExists(vid.localRef.path)
|
const exists = storage.mediaExists(vid.localRef.path)
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
hasMissingMedia = true
|
meta.hasMissingMedia = true
|
||||||
}
|
}
|
||||||
videos.push({
|
videos.push({
|
||||||
localPath: vid.localRef.path,
|
localPath: vid.localRef.path,
|
||||||
@@ -320,13 +334,18 @@ export function draftViewToSummary(
|
|||||||
for (const ext of post.embedExternals) {
|
for (const ext of post.embedExternals) {
|
||||||
const gifData = parseGifFromUrl(ext.uri)
|
const gifData = parseGifFromUrl(ext.uri)
|
||||||
if (gifData) {
|
if (gifData) {
|
||||||
mediaCount++
|
meta.mediaCount++
|
||||||
hasMedia = true
|
meta.hasMedia = true
|
||||||
gif = gifData
|
gif = gifData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (post.embedRecords && post.embedRecords.length > 0) {
|
||||||
|
meta.quoteCount += post.embedRecords.length
|
||||||
|
meta.hasQuotes = true
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: `post-${index}`,
|
id: `post-${index}`,
|
||||||
text: post.text || '',
|
text: post.text || '',
|
||||||
@@ -336,17 +355,17 @@ export function draftViewToSummary(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (meta.isOriginatingDevice && meta.hasMissingMedia) {
|
||||||
|
analytics.logger.warn(`Draft is missing media on originating device`, {})
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: view.id,
|
id: view.id,
|
||||||
draft: view.draft,
|
|
||||||
previewText,
|
|
||||||
hasMedia,
|
|
||||||
hasMissingMedia,
|
|
||||||
mediaCount,
|
|
||||||
postCount: view.draft.posts.length,
|
|
||||||
createdAt: view.createdAt,
|
createdAt: view.createdAt,
|
||||||
updatedAt: view.updatedAt,
|
updatedAt: view.updatedAt,
|
||||||
|
draft: view.draft,
|
||||||
posts,
|
posts,
|
||||||
|
meta,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import {
|
|||||||
import {isNetworkError} from '#/lib/strings/errors'
|
import {isNetworkError} from '#/lib/strings/errors'
|
||||||
import {useAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
import {type ComposerState} from '#/view/com/composer/state/composer'
|
import {type ComposerState} from '#/view/com/composer/state/composer'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
|
import {getDeviceId} from '#/analytics/identifiers'
|
||||||
import {composerStateToDraft, draftViewToSummary} from './api'
|
import {composerStateToDraft, draftViewToSummary} from './api'
|
||||||
import {logger} from './logger'
|
import {logger} from './logger'
|
||||||
import * as storage from './storage'
|
import * as storage from './storage'
|
||||||
@@ -19,6 +21,7 @@ const DRAFTS_QUERY_KEY = ['drafts']
|
|||||||
*/
|
*/
|
||||||
export function useDraftsQuery() {
|
export function useDraftsQuery() {
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
|
const ax = useAnalytics()
|
||||||
|
|
||||||
return useInfiniteQuery({
|
return useInfiniteQuery({
|
||||||
queryKey: DRAFTS_QUERY_KEY,
|
queryKey: DRAFTS_QUERY_KEY,
|
||||||
@@ -29,7 +32,10 @@ export function useDraftsQuery() {
|
|||||||
return {
|
return {
|
||||||
cursor: res.data.cursor,
|
cursor: res.data.cursor,
|
||||||
drafts: res.data.drafts.map(view =>
|
drafts: res.data.drafts.map(view =>
|
||||||
draftViewToSummary(view, path => storage.mediaExists(path)),
|
draftViewToSummary({
|
||||||
|
view,
|
||||||
|
analytics: ax,
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -42,11 +48,17 @@ export function useDraftsQuery() {
|
|||||||
* Load a draft's local media for editing.
|
* Load a draft's local media for editing.
|
||||||
* Takes the full Draft object (from DraftSummary) to avoid re-fetching.
|
* Takes the full Draft object (from DraftSummary) to avoid re-fetching.
|
||||||
*/
|
*/
|
||||||
export async function loadDraft(draft: AppBskyDraftDefs.Draft): Promise<{
|
export async function loadDraftMedia(draft: AppBskyDraftDefs.Draft): Promise<{
|
||||||
loadedMedia: Map<string, string>
|
loadedMedia: Map<string, string>
|
||||||
}> {
|
}> {
|
||||||
// Load local media files
|
// Load local media files
|
||||||
const loadedMedia = new Map<string, string>()
|
const loadedMedia = new Map<string, string>()
|
||||||
|
|
||||||
|
// can't load media from another device
|
||||||
|
if (draft.deviceId && draft.deviceId !== getDeviceId()) {
|
||||||
|
return {loadedMedia}
|
||||||
|
}
|
||||||
|
|
||||||
for (const post of draft.posts) {
|
for (const post of draft.posts) {
|
||||||
// Load images
|
// Load images
|
||||||
if (post.embedImages) {
|
if (post.embedImages) {
|
||||||
@@ -54,10 +66,10 @@ export async function loadDraft(draft: AppBskyDraftDefs.Draft): Promise<{
|
|||||||
try {
|
try {
|
||||||
const url = await storage.loadMediaFromLocal(img.localRef.path)
|
const url = await storage.loadMediaFromLocal(img.localRef.path)
|
||||||
loadedMedia.set(img.localRef.path, url)
|
loadedMedia.set(img.localRef.path, url)
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
logger.debug('Failed to load draft image', {
|
logger.error('Failed to load draft image', {
|
||||||
path: img.localRef.path,
|
path: img.localRef.path,
|
||||||
error: e,
|
safeMessage: e.message,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -68,10 +80,10 @@ export async function loadDraft(draft: AppBskyDraftDefs.Draft): Promise<{
|
|||||||
try {
|
try {
|
||||||
const url = await storage.loadMediaFromLocal(vid.localRef.path)
|
const url = await storage.loadMediaFromLocal(vid.localRef.path)
|
||||||
loadedMedia.set(vid.localRef.path, url)
|
loadedMedia.set(vid.localRef.path, url)
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
logger.debug('Failed to load draft video', {
|
logger.error('Failed to load draft video', {
|
||||||
path: vid.localRef.path,
|
path: vid.localRef.path,
|
||||||
error: e,
|
safeMessage: e.message,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,22 +50,31 @@ export type DraftPostDisplay = {
|
|||||||
*/
|
*/
|
||||||
export type DraftSummary = {
|
export type DraftSummary = {
|
||||||
id: string
|
id: string
|
||||||
|
/** ISO timestamp of creation */
|
||||||
|
createdAt: string
|
||||||
|
/** ISO timestamp of last update */
|
||||||
|
updatedAt: string
|
||||||
/** The full draft data from the server */
|
/** The full draft data from the server */
|
||||||
draft: AppBskyDraftDefs.Draft
|
draft: AppBskyDraftDefs.Draft
|
||||||
/** First ~100 chars of first post */
|
/** All posts in the draft for full display */
|
||||||
previewText: string
|
posts: DraftPostDisplay[]
|
||||||
|
/** Metadata about the draft for display purposes */
|
||||||
|
meta: {
|
||||||
|
/** Whether this device is the originating device for the draft */
|
||||||
|
isOriginatingDevice: boolean
|
||||||
|
/** Number of posts in thread */
|
||||||
|
postCount: number
|
||||||
|
/** Number of replies to anchor post */
|
||||||
|
replyCount: number
|
||||||
/** Whether the draft has media */
|
/** Whether the draft has media */
|
||||||
hasMedia: boolean
|
hasMedia: boolean
|
||||||
/** Whether some media is missing (saved on another device) */
|
/** Whether some media is missing (saved on another device) */
|
||||||
hasMissingMedia?: boolean
|
hasMissingMedia?: boolean
|
||||||
/** Number of media items */
|
/** Number of media items */
|
||||||
mediaCount: number
|
mediaCount: number
|
||||||
/** Number of posts in thread */
|
/** Whether any posts in the draft has quotes */
|
||||||
postCount: number
|
hasQuotes: boolean
|
||||||
/** ISO timestamp of creation */
|
/** Number of quotes in the draft */
|
||||||
createdAt: string
|
quoteCount: number
|
||||||
/** ISO timestamp of last update */
|
}
|
||||||
updatedAt: string
|
|
||||||
/** All posts in the draft for full display */
|
|
||||||
posts: DraftPostDisplay[]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,6 +96,20 @@
|
|||||||
tlds "^1.234.0"
|
tlds "^1.234.0"
|
||||||
zod "^3.23.8"
|
zod "^3.23.8"
|
||||||
|
|
||||||
|
"@atproto/api@^0.18.20":
|
||||||
|
version "0.18.20"
|
||||||
|
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.18.20.tgz#3fdbb7b7fae90bd59101970c2b56cc31e8cf417d"
|
||||||
|
integrity sha512-BZYZkh2VJIFCXEnc/vzKwAwWjAQQTgbNJ8FBxpBK+z+KYh99O0uPCsRYKoCQsRrnkgrhzdU9+g2G+7zanTIGbw==
|
||||||
|
dependencies:
|
||||||
|
"@atproto/common-web" "^0.4.15"
|
||||||
|
"@atproto/lexicon" "^0.6.1"
|
||||||
|
"@atproto/syntax" "^0.4.3"
|
||||||
|
"@atproto/xrpc" "^0.7.7"
|
||||||
|
await-lock "^2.2.2"
|
||||||
|
multiformats "^9.9.0"
|
||||||
|
tlds "^1.234.0"
|
||||||
|
zod "^3.23.8"
|
||||||
|
|
||||||
"@atproto/aws@^0.2.31":
|
"@atproto/aws@^0.2.31":
|
||||||
version "0.2.31"
|
version "0.2.31"
|
||||||
resolved "https://registry.yarnpkg.com/@atproto/aws/-/aws-0.2.31.tgz#e46d7db34ee57c4f9817269f1e73a7eddba2b9b8"
|
resolved "https://registry.yarnpkg.com/@atproto/aws/-/aws-0.2.31.tgz#e46d7db34ee57c4f9817269f1e73a7eddba2b9b8"
|
||||||
@@ -190,6 +204,16 @@
|
|||||||
"@atproto/syntax" "0.4.3"
|
"@atproto/syntax" "0.4.3"
|
||||||
zod "^3.23.8"
|
zod "^3.23.8"
|
||||||
|
|
||||||
|
"@atproto/common-web@^0.4.15":
|
||||||
|
version "0.4.15"
|
||||||
|
resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.4.15.tgz#1fffedf62d69b8c96f7b360e11c4180446a2cc82"
|
||||||
|
integrity sha512-A4l9gyqUNez8CjZp/Trypz/D3WIQsNj8dN05WR6+RoBbvwc9JhWjKPrm+WoVYc/F16RPdXHLkE3BEJlGIyYIiA==
|
||||||
|
dependencies:
|
||||||
|
"@atproto/lex-data" "^0.0.10"
|
||||||
|
"@atproto/lex-json" "^0.0.10"
|
||||||
|
"@atproto/syntax" "^0.4.3"
|
||||||
|
zod "^3.23.8"
|
||||||
|
|
||||||
"@atproto/common@0.1.0":
|
"@atproto/common@0.1.0":
|
||||||
version "0.1.0"
|
version "0.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@atproto/common/-/common-0.1.0.tgz#4216a8fef5b985ab62ac21252a0f8ca0f4a0f210"
|
resolved "https://registry.yarnpkg.com/@atproto/common/-/common-0.1.0.tgz#4216a8fef5b985ab62ac21252a0f8ca0f4a0f210"
|
||||||
@@ -327,6 +351,16 @@
|
|||||||
uint8arrays "3.0.0"
|
uint8arrays "3.0.0"
|
||||||
unicode-segmenter "^0.14.0"
|
unicode-segmenter "^0.14.0"
|
||||||
|
|
||||||
|
"@atproto/lex-data@^0.0.10":
|
||||||
|
version "0.0.10"
|
||||||
|
resolved "https://registry.yarnpkg.com/@atproto/lex-data/-/lex-data-0.0.10.tgz#091c012af817a869951ce0e61eb757bd6c29797a"
|
||||||
|
integrity sha512-FDbcy8VIUVzS9Mi1F8SMxbkL/jOUmRRpqbeM1xB4A0fMxeZJTxf6naAbFt4gYF3quu/+TPJGmio6/7cav05FqQ==
|
||||||
|
dependencies:
|
||||||
|
multiformats "^9.9.0"
|
||||||
|
tslib "^2.8.1"
|
||||||
|
uint8arrays "3.0.0"
|
||||||
|
unicode-segmenter "^0.14.0"
|
||||||
|
|
||||||
"@atproto/lex-document@0.0.11":
|
"@atproto/lex-document@0.0.11":
|
||||||
version "0.0.11"
|
version "0.0.11"
|
||||||
resolved "https://registry.yarnpkg.com/@atproto/lex-document/-/lex-document-0.0.11.tgz#8cfdd6ab5b5befac4d1409c76e2d5a310845c1dc"
|
resolved "https://registry.yarnpkg.com/@atproto/lex-document/-/lex-document-0.0.11.tgz#8cfdd6ab5b5befac4d1409c76e2d5a310845c1dc"
|
||||||
@@ -344,6 +378,14 @@
|
|||||||
"@atproto/lex-data" "0.0.9"
|
"@atproto/lex-data" "0.0.9"
|
||||||
tslib "^2.8.1"
|
tslib "^2.8.1"
|
||||||
|
|
||||||
|
"@atproto/lex-json@^0.0.10":
|
||||||
|
version "0.0.10"
|
||||||
|
resolved "https://registry.yarnpkg.com/@atproto/lex-json/-/lex-json-0.0.10.tgz#18de1c8cc3ba564e5412bce1f8e22c198c956e64"
|
||||||
|
integrity sha512-L6MyXU17C5ODMeob8myQ2F3xvgCTvJUtM0ew8qSApnN//iDasB/FDGgd7ty4UVNmx4NQ/rtvz8xV94YpG6kneQ==
|
||||||
|
dependencies:
|
||||||
|
"@atproto/lex-data" "^0.0.10"
|
||||||
|
tslib "^2.8.1"
|
||||||
|
|
||||||
"@atproto/lex-resolver@0.0.12":
|
"@atproto/lex-resolver@0.0.12":
|
||||||
version "0.0.12"
|
version "0.0.12"
|
||||||
resolved "https://registry.yarnpkg.com/@atproto/lex-resolver/-/lex-resolver-0.0.12.tgz#fb6cd78c78c0acfc9a92d9e42abe7ff18b4c3a41"
|
resolved "https://registry.yarnpkg.com/@atproto/lex-resolver/-/lex-resolver-0.0.12.tgz#fb6cd78c78c0acfc9a92d9e42abe7ff18b4c3a41"
|
||||||
|
|||||||
Reference in New Issue
Block a user