Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 68cead4013 | |||
| 36a3dd149d | |||
| 86c2fc8e1c | |||
| 64eed158b7 | |||
| c323f628ae | |||
| 4f7bc284f7 | |||
| 289efece86 | |||
| 90e22f86ab |
+2
-2
@@ -69,7 +69,7 @@
|
||||
"icons:optimize": "svgo -f ./assets/icons"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "^0.15.7",
|
||||
"@atproto/api": "^0.15.8",
|
||||
"@bitdrift/react-native": "^0.6.8",
|
||||
"@braintree/sanitize-url": "^6.0.2",
|
||||
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
||||
@@ -219,7 +219,7 @@
|
||||
"zod": "^3.20.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@atproto/dev-env": "^0.3.131",
|
||||
"@atproto/dev-env": "^0.3.132",
|
||||
"@babel/core": "^7.26.0",
|
||||
"@babel/preset-env": "^7.26.0",
|
||||
"@babel/runtime": "^7.26.0",
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
## react-native-view-shot patch
|
||||
|
||||
Temporary patch for web, where `view`'s type has changed.
|
||||
@@ -0,0 +1,28 @@
|
||||
diff --git a/node_modules/react-native-view-shot/src/index.js b/node_modules/react-native-view-shot/src/index.js
|
||||
index fa76d7e..018b686 100644
|
||||
--- a/node_modules/react-native-view-shot/src/index.js
|
||||
+++ b/node_modules/react-native-view-shot/src/index.js
|
||||
@@ -125,13 +125,17 @@ export function captureRef<T: React$ElementType>(
|
||||
}
|
||||
}
|
||||
if (typeof view !== "number") {
|
||||
- const node = findNodeHandle(view);
|
||||
- if (!node) {
|
||||
- return Promise.reject(
|
||||
- new Error("findNodeHandle failed to resolve view=" + String(view))
|
||||
- );
|
||||
+ if (Platform.OS == 'web') {
|
||||
+ view = view;
|
||||
+ } else {
|
||||
+ const node = findNodeHandle(view);
|
||||
+ if (!node) {
|
||||
+ return Promise.reject(
|
||||
+ new Error("findNodeHandle failed to resolve view=" + String(view))
|
||||
+ );
|
||||
+ }
|
||||
+ view = node;
|
||||
}
|
||||
- view = node;
|
||||
}
|
||||
const { options, errors } = validateOptions(optionsObject);
|
||||
if (__DEV__ && errors.length > 0) {
|
||||
@@ -1,13 +1,13 @@
|
||||
import React from 'react'
|
||||
import {StyleProp, View, ViewStyle} from 'react-native'
|
||||
import {createContext, useContext} from 'react'
|
||||
import {type StyleProp, View, type ViewStyle} from 'react-native'
|
||||
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
import {Button as BaseButton, ButtonProps} from '#/components/Button'
|
||||
import {Button as BaseButton, type ButtonProps} from '#/components/Button'
|
||||
import {CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon} from '#/components/icons/CircleInfo'
|
||||
import {Eye_Stroke2_Corner0_Rounded as InfoIcon} from '#/components/icons/Eye'
|
||||
import {Leaf_Stroke2_Corner0_Rounded as TipIcon} from '#/components/icons/Leaf'
|
||||
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
|
||||
import {Text as BaseText, TextProps} from '#/components/Typography'
|
||||
import {Text as BaseText, type TextProps} from '#/components/Typography'
|
||||
|
||||
export const colors = {
|
||||
warning: {
|
||||
@@ -20,13 +20,13 @@ type Context = {
|
||||
type: 'info' | 'tip' | 'warning' | 'error'
|
||||
}
|
||||
|
||||
const Context = React.createContext<Context>({
|
||||
const Context = createContext<Context>({
|
||||
type: 'info',
|
||||
})
|
||||
|
||||
export function Icon() {
|
||||
const t = useTheme()
|
||||
const {type} = React.useContext(Context)
|
||||
const {type} = useContext(Context)
|
||||
const Icon = {
|
||||
info: InfoIcon,
|
||||
tip: TipIcon,
|
||||
|
||||
@@ -381,8 +381,8 @@ let Card = ({
|
||||
t.atoms.bg,
|
||||
t.atoms.border_contrast_low,
|
||||
t.atoms.shadow_lg,
|
||||
a.w_full,
|
||||
{maxWidth: status.isActive ? 500 : 300},
|
||||
{width: status.isActive ? 350 : 300},
|
||||
a.max_w_full,
|
||||
]}>
|
||||
{data && moderationOpts ? (
|
||||
status.isActive ? (
|
||||
@@ -396,7 +396,13 @@ let Card = ({
|
||||
<Inner profile={data} moderationOpts={moderationOpts} hide={hide} />
|
||||
)
|
||||
) : (
|
||||
<View style={[a.justify_center, a.align_center, {minHeight: 200}]}>
|
||||
<View
|
||||
style={[
|
||||
a.justify_center,
|
||||
a.align_center,
|
||||
{minHeight: 200},
|
||||
a.w_full,
|
||||
]}>
|
||||
<Loader size="xl" />
|
||||
</View>
|
||||
)}
|
||||
|
||||
@@ -4,7 +4,7 @@ import type ViewShot from 'react-native-view-shot'
|
||||
import {requestMediaLibraryPermissionsAsync} from 'expo-image-picker'
|
||||
import {createAssetAsync} from 'expo-media-library'
|
||||
import * as Sharing from 'expo-sharing'
|
||||
import {AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api'
|
||||
import {type AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
@@ -15,7 +15,7 @@ import * as Toast from '#/view/com/util/Toast'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {DialogControlProps} from '#/components/Dialog'
|
||||
import {type DialogControlProps} from '#/components/Dialog'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {QrCode} from '#/components/StarterPack/QrCode'
|
||||
import * as bsky from '#/types/bsky'
|
||||
@@ -55,7 +55,7 @@ export function QrCodeDialog({
|
||||
if (isNative) {
|
||||
const res = await requestMediaLibraryPermissionsAsync()
|
||||
|
||||
if (!res) {
|
||||
if (!res.granted) {
|
||||
Toast.show(
|
||||
_(
|
||||
msg`You must grant access to your photo library to save a QR code`,
|
||||
@@ -155,6 +155,7 @@ export function QrCodeDialog({
|
||||
|
||||
return (
|
||||
<Dialog.Outer control={control}>
|
||||
<Dialog.Handle />
|
||||
<Dialog.ScrollableInner
|
||||
label={_(msg`Create a QR code for a starter pack`)}>
|
||||
<View style={[a.flex_1, a.align_center, a.gap_5xl]}>
|
||||
@@ -197,6 +198,7 @@ export function QrCodeDialog({
|
||||
)}
|
||||
</React.Suspense>
|
||||
</View>
|
||||
<Dialog.Close />
|
||||
</Dialog.ScrollableInner>
|
||||
</Dialog.Outer>
|
||||
)
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import {View} from 'react-native'
|
||||
import {Image} from 'expo-image'
|
||||
import {requestMediaLibraryPermissionsAsync} from 'expo-image-picker'
|
||||
import {type AppBskyGraphDefs} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||
import {saveImageToMediaLibrary} from '#/lib/media/manip'
|
||||
import {useSaveImageToMediaLibrary} from '#/lib/media/save-image'
|
||||
import {shareUrl} from '#/lib/sharing'
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {getStarterPackOgCard} from '#/lib/strings/starter-pack'
|
||||
import {logger} from '#/logger'
|
||||
import {isNative, isWeb} from '#/platform/detection'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import {type DialogControlProps} from '#/components/Dialog'
|
||||
@@ -60,26 +57,10 @@ function ShareDialogInner({
|
||||
control.close()
|
||||
}
|
||||
|
||||
const saveImageToAlbum = useSaveImageToMediaLibrary()
|
||||
|
||||
const onSave = async () => {
|
||||
const res = await requestMediaLibraryPermissionsAsync()
|
||||
|
||||
if (!res) {
|
||||
Toast.show(
|
||||
_(msg`You must grant access to your photo library to save the image.`),
|
||||
'xmark',
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await saveImageToMediaLibrary({uri: imageUrl})
|
||||
Toast.show(_(msg`Image saved`))
|
||||
control.close()
|
||||
} catch (e: unknown) {
|
||||
Toast.show(_(msg`An error occurred while saving the QR code!`), 'xmark')
|
||||
logger.error('Failed to save QR code', {error: e})
|
||||
return
|
||||
}
|
||||
await saveImageToAlbum(imageUrl)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -161,6 +142,7 @@ function ShareDialogInner({
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
<Dialog.Close />
|
||||
</Dialog.ScrollableInner>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -33,9 +33,7 @@ export function EmojiReactionPicker({
|
||||
return (
|
||||
<Menu.Root>
|
||||
<Menu.Trigger label={_(msg`Add emoji reaction`)}>{children}</Menu.Trigger>
|
||||
<Menu.Outer>
|
||||
<MenuInner message={message} onEmojiSelect={onEmojiSelect} />
|
||||
</Menu.Outer>
|
||||
<MenuInner message={message} onEmojiSelect={onEmojiSelect} />
|
||||
</Menu.Root>
|
||||
)
|
||||
}
|
||||
@@ -55,6 +53,15 @@ function MenuInner({
|
||||
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
|
||||
const [prevOpen, setPrevOpen] = useState(control.isOpen)
|
||||
|
||||
if (control.isOpen !== prevOpen) {
|
||||
setPrevOpen(control.isOpen)
|
||||
if (!control.isOpen) {
|
||||
setExpanded(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleEmojiPickerResponse = (emoji: Emoji) => {
|
||||
handleEmojiSelect(emoji.native)
|
||||
}
|
||||
@@ -67,7 +74,16 @@ function MenuInner({
|
||||
const limitReacted = hasReachedReactionLimit(message, currentAccount?.did)
|
||||
|
||||
return expanded ? (
|
||||
<EmojiPicker onEmojiSelect={handleEmojiPickerResponse} autoFocus={true} />
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content
|
||||
sideOffset={5}
|
||||
collisionPadding={{left: 5, right: 5, bottom: 5}}>
|
||||
<EmojiPicker
|
||||
onEmojiSelect={handleEmojiPickerResponse}
|
||||
autoFocus={true}
|
||||
/>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
) : (
|
||||
<Menu.Outer style={[a.rounded_full]}>
|
||||
<View style={[a.flex_row, a.gap_xs]}>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {useMemo, useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {Image} from 'expo-image'
|
||||
import {
|
||||
type AppBskyActorDefs,
|
||||
AppBskyActorStatus,
|
||||
@@ -8,28 +7,23 @@ import {
|
||||
} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useQuery} from '@tanstack/react-query'
|
||||
import {differenceInMinutes} from 'date-fns'
|
||||
|
||||
import {getLinkMeta} from '#/lib/link-meta/link-meta'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {toNiceDomain} from '#/lib/strings/url-helpers'
|
||||
import {definitelyUrl} from '#/lib/strings/url-helpers'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {useTickEveryMinute} from '#/state/shell'
|
||||
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||
import {atoms as a, platform, useTheme, web} from '#/alf'
|
||||
import {Admonition} from '#/components/Admonition'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import * as TextField from '#/components/forms/TextField'
|
||||
import {CircleX_Stroke2_Corner0_Rounded as CircleXIcon} from '#/components/icons/CircleX'
|
||||
import {Clock_Stroke2_Corner0_Rounded as ClockIcon} from '#/components/icons/Clock'
|
||||
import {Globe_Stroke2_Corner0_Rounded as GlobeIcon} from '#/components/icons/Globe'
|
||||
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {LinkPreview} from './LinkPreview'
|
||||
import {
|
||||
useLiveLinkMetaQuery,
|
||||
useRemoveLiveStatusMutation,
|
||||
useUpsertLiveStatusMutation,
|
||||
} from './queries'
|
||||
@@ -62,10 +56,9 @@ function DialogInner({
|
||||
const control = Dialog.useDialogContext()
|
||||
const {_, i18n} = useLingui()
|
||||
const t = useTheme()
|
||||
const agent = useAgent()
|
||||
|
||||
const [liveLink, setLiveLink] = useState(embed.external.uri)
|
||||
const [liveLinkError, setLiveLinkError] = useState('')
|
||||
const [imageLoadError, setImageLoadError] = useState(false)
|
||||
const tick = useTickEveryMinute()
|
||||
|
||||
const liveLinkUrl = definitelyUrl(liveLink)
|
||||
@@ -78,14 +71,7 @@ function DialogInner({
|
||||
isSuccess: hasValidLinkMeta,
|
||||
isLoading: linkMetaLoading,
|
||||
error: linkMetaError,
|
||||
} = useQuery({
|
||||
enabled: !!debouncedUrl,
|
||||
queryKey: ['link-meta', debouncedUrl],
|
||||
queryFn: async () => {
|
||||
if (!debouncedUrl) return null
|
||||
return getLinkMeta(agent, debouncedUrl)
|
||||
},
|
||||
})
|
||||
} = useLiveLinkMetaQuery(debouncedUrl)
|
||||
|
||||
const record = useMemo(() => {
|
||||
if (!AppBskyActorStatus.isRecord(status.record)) return null
|
||||
@@ -208,85 +194,7 @@ function DialogInner({
|
||||
</View>
|
||||
)}
|
||||
|
||||
{(linkMeta || linkMetaLoading) && (
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
a.border,
|
||||
t.atoms.border_contrast_low,
|
||||
t.atoms.bg,
|
||||
a.flex_row,
|
||||
a.rounded_sm,
|
||||
a.overflow_hidden,
|
||||
a.align_stretch,
|
||||
]}>
|
||||
{(!linkMeta || linkMeta.image) && (
|
||||
<View
|
||||
style={[
|
||||
t.atoms.bg_contrast_25,
|
||||
{minHeight: 64, width: 114},
|
||||
a.justify_center,
|
||||
a.align_center,
|
||||
]}>
|
||||
{linkMeta?.image && (
|
||||
<Image
|
||||
source={linkMeta.image}
|
||||
accessibilityIgnoresInvertColors
|
||||
transition={200}
|
||||
style={[a.absolute, a.inset_0]}
|
||||
contentFit="cover"
|
||||
onLoad={() => setImageLoadError(false)}
|
||||
onError={() => setImageLoadError(true)}
|
||||
/>
|
||||
)}
|
||||
{linkMeta && imageLoadError && (
|
||||
<CircleXIcon
|
||||
style={[t.atoms.text_contrast_low]}
|
||||
size="xl"
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.justify_center,
|
||||
a.py_sm,
|
||||
a.gap_xs,
|
||||
a.px_md,
|
||||
]}>
|
||||
{linkMeta ? (
|
||||
<>
|
||||
<Text
|
||||
numberOfLines={2}
|
||||
style={[a.leading_snug, a.font_bold, a.text_md]}>
|
||||
{linkMeta.title || linkMeta.url}
|
||||
</Text>
|
||||
<View style={[a.flex_row, a.align_center, a.gap_2xs]}>
|
||||
<GlobeIcon
|
||||
size="xs"
|
||||
style={[t.atoms.text_contrast_low]}
|
||||
/>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={[
|
||||
a.text_xs,
|
||||
a.leading_snug,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}>
|
||||
{toNiceDomain(linkMeta.url)}
|
||||
</Text>
|
||||
</View>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<LoadingPlaceholder height={16} width={128} />
|
||||
<LoadingPlaceholder height={12} width={72} />
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
<LinkPreview linkMeta={linkMeta} loading={linkMetaLoading} />
|
||||
</View>
|
||||
|
||||
{goLiveError && (
|
||||
|
||||
@@ -1,33 +1,25 @@
|
||||
import {useCallback, useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {Image} from 'expo-image'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useQuery} from '@tanstack/react-query'
|
||||
|
||||
import {getLinkMeta} from '#/lib/link-meta/link-meta'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {toNiceDomain} from '#/lib/strings/url-helpers'
|
||||
import {definitelyUrl} from '#/lib/strings/url-helpers'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useLiveNowConfig} from '#/state/service-config'
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
import {useTickEveryMinute} from '#/state/shell'
|
||||
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||
import {atoms as a, ios, native, platform, useTheme, web} from '#/alf'
|
||||
import {Admonition} from '#/components/Admonition'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import * as TextField from '#/components/forms/TextField'
|
||||
import {CircleX_Stroke2_Corner0_Rounded as CircleXIcon} from '#/components/icons/CircleX'
|
||||
import {Globe_Stroke2_Corner0_Rounded as GlobeIcon} from '#/components/icons/Globe'
|
||||
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import * as ProfileCard from '#/components/ProfileCard'
|
||||
import * as Select from '#/components/Select'
|
||||
import {Text} from '#/components/Typography'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
import {useUpsertLiveStatusMutation} from './queries'
|
||||
import {LinkPreview} from './LinkPreview'
|
||||
import {useLiveLinkMetaQuery, useUpsertLiveStatusMutation} from './queries'
|
||||
import {displayDuration, useDebouncedValue} from './utils'
|
||||
|
||||
export function GoLiveDialog({
|
||||
@@ -52,17 +44,11 @@ function DialogInner({profile}: {profile: bsky.profile.AnyProfileView}) {
|
||||
const control = Dialog.useDialogContext()
|
||||
const {_, i18n} = useLingui()
|
||||
const t = useTheme()
|
||||
const agent = useAgent()
|
||||
const [liveLink, setLiveLink] = useState('')
|
||||
const [liveLinkError, setLiveLinkError] = useState('')
|
||||
const [imageLoadError, setImageLoadError] = useState(false)
|
||||
const [duration, setDuration] = useState(60)
|
||||
const moderationOpts = useModerationOpts()
|
||||
const tick = useTickEveryMinute()
|
||||
const liveNowConfig = useLiveNowConfig()
|
||||
const {currentAccount} = useSession()
|
||||
|
||||
const config = liveNowConfig.find(cfg => cfg.did === currentAccount?.did)
|
||||
|
||||
const time = useCallback(
|
||||
(offset: number) => {
|
||||
@@ -90,21 +76,7 @@ function DialogInner({profile}: {profile: bsky.profile.AnyProfileView}) {
|
||||
isSuccess: hasValidLinkMeta,
|
||||
isLoading: linkMetaLoading,
|
||||
error: linkMetaError,
|
||||
} = useQuery({
|
||||
enabled: !!debouncedUrl,
|
||||
queryKey: ['link-meta', debouncedUrl],
|
||||
queryFn: async () => {
|
||||
if (!debouncedUrl) return null
|
||||
if (!config) throw new Error(_(msg`You are not allowed to go live`))
|
||||
|
||||
const urlp = new URL(debouncedUrl)
|
||||
if (!config.domains.includes(urlp.hostname)) {
|
||||
throw new Error(_(msg`${urlp.hostname} is not a valid URL`))
|
||||
}
|
||||
|
||||
return getLinkMeta(agent, debouncedUrl)
|
||||
},
|
||||
})
|
||||
} = useLiveLinkMetaQuery(debouncedUrl)
|
||||
|
||||
const {
|
||||
mutate: goLive,
|
||||
@@ -193,85 +165,7 @@ function DialogInner({profile}: {profile: bsky.profile.AnyProfileView}) {
|
||||
</View>
|
||||
)}
|
||||
|
||||
{(linkMeta || linkMetaLoading) && (
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
a.border,
|
||||
t.atoms.border_contrast_low,
|
||||
t.atoms.bg,
|
||||
a.flex_row,
|
||||
a.rounded_sm,
|
||||
a.overflow_hidden,
|
||||
a.align_stretch,
|
||||
]}>
|
||||
{(!linkMeta || linkMeta.image) && (
|
||||
<View
|
||||
style={[
|
||||
t.atoms.bg_contrast_25,
|
||||
{minHeight: 64, width: 114},
|
||||
a.justify_center,
|
||||
a.align_center,
|
||||
]}>
|
||||
{linkMeta?.image && (
|
||||
<Image
|
||||
source={linkMeta.image}
|
||||
accessibilityIgnoresInvertColors
|
||||
transition={200}
|
||||
style={[a.absolute, a.inset_0]}
|
||||
contentFit="cover"
|
||||
onLoad={() => setImageLoadError(false)}
|
||||
onError={() => setImageLoadError(true)}
|
||||
/>
|
||||
)}
|
||||
{linkMeta && imageLoadError && (
|
||||
<CircleXIcon
|
||||
style={[t.atoms.text_contrast_low]}
|
||||
size="xl"
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.justify_center,
|
||||
a.py_sm,
|
||||
a.gap_xs,
|
||||
a.px_md,
|
||||
]}>
|
||||
{linkMeta ? (
|
||||
<>
|
||||
<Text
|
||||
numberOfLines={2}
|
||||
style={[a.leading_snug, a.font_bold, a.text_md]}>
|
||||
{linkMeta.title || linkMeta.url}
|
||||
</Text>
|
||||
<View style={[a.flex_row, a.align_center, a.gap_2xs]}>
|
||||
<GlobeIcon
|
||||
size="xs"
|
||||
style={[t.atoms.text_contrast_low]}
|
||||
/>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={[
|
||||
a.text_xs,
|
||||
a.leading_snug,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}>
|
||||
{toNiceDomain(linkMeta.url)}
|
||||
</Text>
|
||||
</View>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<LoadingPlaceholder height={16} width={128} />
|
||||
<LoadingPlaceholder height={12} width={72} />
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
<LinkPreview linkMeta={linkMeta} loading={linkMetaLoading} />
|
||||
</View>
|
||||
|
||||
{hasLink && (
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import {useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {Image} from 'expo-image'
|
||||
import {Trans} from '@lingui/macro'
|
||||
|
||||
import {type LinkMeta} from '#/lib/link-meta/link-meta'
|
||||
import {toNiceDomain} from '#/lib/strings/url-helpers'
|
||||
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Globe_Stroke2_Corner0_Rounded as GlobeIcon} from '#/components/icons/Globe'
|
||||
import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function LinkPreview({
|
||||
linkMeta,
|
||||
loading,
|
||||
}: {
|
||||
linkMeta?: LinkMeta
|
||||
loading: boolean
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const [imageLoadError, setImageLoadError] = useState(false)
|
||||
|
||||
if (!linkMeta && !loading) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
a.border,
|
||||
t.atoms.border_contrast_low,
|
||||
t.atoms.bg,
|
||||
a.flex_row,
|
||||
a.rounded_sm,
|
||||
a.overflow_hidden,
|
||||
a.align_stretch,
|
||||
]}>
|
||||
<View
|
||||
style={[
|
||||
t.atoms.bg_contrast_25,
|
||||
{minHeight: 64, width: 114},
|
||||
a.justify_center,
|
||||
a.align_center,
|
||||
a.gap_xs,
|
||||
]}>
|
||||
{linkMeta?.image && (
|
||||
<Image
|
||||
source={linkMeta.image}
|
||||
accessibilityIgnoresInvertColors
|
||||
transition={200}
|
||||
style={[a.absolute, a.inset_0]}
|
||||
contentFit="cover"
|
||||
onLoad={() => setImageLoadError(false)}
|
||||
onError={() => setImageLoadError(true)}
|
||||
/>
|
||||
)}
|
||||
{linkMeta && (!linkMeta.image || imageLoadError) && (
|
||||
<>
|
||||
<ImageIcon style={[t.atoms.text_contrast_low]} />
|
||||
<Text style={[t.atoms.text_contrast_low, a.text_xs, a.text_center]}>
|
||||
<Trans>No image</Trans>
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
<View style={[a.flex_1, a.justify_center, a.py_sm, a.gap_xs, a.px_md]}>
|
||||
{linkMeta ? (
|
||||
<>
|
||||
<Text
|
||||
numberOfLines={2}
|
||||
style={[a.leading_snug, a.font_bold, a.text_md]}>
|
||||
{linkMeta.title || linkMeta.url}
|
||||
</Text>
|
||||
<View style={[a.flex_row, a.align_center, a.gap_2xs]}>
|
||||
<GlobeIcon size="xs" style={[t.atoms.text_contrast_low]} />
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={[
|
||||
a.text_xs,
|
||||
a.leading_snug,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}>
|
||||
{toNiceDomain(linkMeta.url)}
|
||||
</Text>
|
||||
</View>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<LoadingPlaceholder height={16} width={128} />
|
||||
<LoadingPlaceholder height={12} width={72} />
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -134,7 +134,7 @@ export function LiveStatus({
|
||||
? [a.px_xl, !embed.external.thumb ? a.pt_2xl : a.pt_lg]
|
||||
: a.p_lg,
|
||||
]}>
|
||||
<View style={[a.flex_1, a.justify_center, a.gap_2xs]}>
|
||||
<View style={[a.w_full, a.justify_center, a.gap_2xs]}>
|
||||
<Text
|
||||
numberOfLines={3}
|
||||
style={[a.leading_snug, a.font_bold, a.text_xl]}>
|
||||
|
||||
@@ -7,17 +7,43 @@ import {
|
||||
import {retry} from '@atproto/common-web'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useMutation, useQueryClient} from '@tanstack/react-query'
|
||||
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {uploadBlob} from '#/lib/api'
|
||||
import {imageToThumb} from '#/lib/api/resolve'
|
||||
import {type LinkMeta} from '#/lib/link-meta/link-meta'
|
||||
import {getLinkMeta, type LinkMeta} from '#/lib/link-meta/link-meta'
|
||||
import {logger} from '#/logger'
|
||||
import {updateProfileShadow} from '#/state/cache/profile-shadow'
|
||||
import {useLiveNowConfig} from '#/state/service-config'
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {useDialogContext} from '#/components/Dialog'
|
||||
|
||||
export function useLiveLinkMetaQuery(url: string | null) {
|
||||
const liveNowConfig = useLiveNowConfig()
|
||||
const {currentAccount} = useSession()
|
||||
const {_} = useLingui()
|
||||
|
||||
const agent = useAgent()
|
||||
return useQuery({
|
||||
enabled: !!url,
|
||||
queryKey: ['link-meta', url],
|
||||
queryFn: async () => {
|
||||
if (!url) return undefined
|
||||
const config = liveNowConfig.find(cfg => cfg.did === currentAccount?.did)
|
||||
|
||||
if (!config) throw new Error(_(msg`You are not allowed to go live`))
|
||||
|
||||
const urlp = new URL(url)
|
||||
if (!config.domains.includes(urlp.hostname)) {
|
||||
throw new Error(_(msg`${urlp.hostname} is not a valid URL`))
|
||||
}
|
||||
|
||||
return await getLinkMeta(agent, url)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useUpsertLiveStatusMutation(
|
||||
duration: number,
|
||||
linkMeta: LinkMeta | null | undefined,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
AppBskyActorDefs,
|
||||
type AppBskyActorDefs,
|
||||
AppBskyEmbedRecord,
|
||||
AppBskyEmbedRecordWithMedia,
|
||||
AppBskyFeedDefs,
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
import * as bsky from '#/types/bsky'
|
||||
import {isPostInLanguage} from '../../locale/helpers'
|
||||
import {FALLBACK_MARKER_POST} from './feed/home'
|
||||
import {ReasonFeedSource} from './feed/types'
|
||||
import {type ReasonFeedSource} from './feed/types'
|
||||
|
||||
type FeedViewPost = AppBskyFeedDefs.FeedViewPost
|
||||
|
||||
@@ -187,6 +187,10 @@ export class FeedViewPostsSlice {
|
||||
return this._feedPost.feedContext
|
||||
}
|
||||
|
||||
get reqId() {
|
||||
return this._feedPost.reqId
|
||||
}
|
||||
|
||||
get isRepost() {
|
||||
const reason = this._feedPost.reason
|
||||
return AppBskyFeedDefs.isReasonRepost(reason)
|
||||
|
||||
@@ -5,7 +5,6 @@ import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
|
||||
import {type NavigationProp} from '#/lib/routes/types'
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {Logger} from '#/logger'
|
||||
import {isAndroid} from '#/platform/detection'
|
||||
import {useCurrentConvoId} from '#/state/messages/current-convo-id'
|
||||
@@ -17,7 +16,7 @@ import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
||||
import {useCloseAllActiveElements} from '#/state/util'
|
||||
import {resetToTab} from '#/Navigation'
|
||||
|
||||
type NotificationReason =
|
||||
export type NotificationReason =
|
||||
| 'like'
|
||||
| 'repost'
|
||||
| 'follow'
|
||||
@@ -229,15 +228,18 @@ export function useNotificationsHandler() {
|
||||
'type' in e.notification.request.trigger &&
|
||||
e.notification.request.trigger.type === 'push'
|
||||
) {
|
||||
const payload = e.notification.request.trigger
|
||||
.payload as NotificationPayload
|
||||
|
||||
logger.debug(
|
||||
'User pressed a notification, opening notifications tab',
|
||||
{},
|
||||
)
|
||||
logEvent('notifications:openApp', {})
|
||||
logger.metric('notifications:openApp', {reason: payload.reason})
|
||||
|
||||
invalidateCachedUnreadPage()
|
||||
const payload = e.notification.request.trigger
|
||||
.payload as NotificationPayload
|
||||
truncateAndInvalidate(queryClient, RQKEY_NOTIFS('all'))
|
||||
|
||||
if (
|
||||
payload.reason === 'mention' ||
|
||||
payload.reason === 'quote' ||
|
||||
@@ -245,10 +247,12 @@ export function useNotificationsHandler() {
|
||||
) {
|
||||
truncateAndInvalidate(queryClient, RQKEY_NOTIFS('mentions'))
|
||||
}
|
||||
|
||||
logger.debug('Notifications: handleNotification', {
|
||||
content: e.notification.request.content,
|
||||
payload: e.notification.request.trigger.payload,
|
||||
})
|
||||
|
||||
handleNotification(payload)
|
||||
Notifications.dismissAllNotificationsAsync()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import {useCallback} from 'react'
|
||||
import * as MediaLibrary from 'expo-media-library'
|
||||
import {t} from '@lingui/macro'
|
||||
|
||||
import {isNative} from '#/platform/detection'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {saveImageToMediaLibrary} from './manip'
|
||||
|
||||
/**
|
||||
* Same as `saveImageToMediaLibrary`, but also handles permissions and toasts
|
||||
*/
|
||||
export function useSaveImageToMediaLibrary() {
|
||||
const [permissionResponse, requestPermission, getPermission] =
|
||||
MediaLibrary.usePermissions({
|
||||
granularPermissions: ['photo'],
|
||||
})
|
||||
return useCallback(
|
||||
async (uri: string) => {
|
||||
if (!isNative) {
|
||||
throw new Error('useSaveImageToMediaLibrary is native only')
|
||||
}
|
||||
|
||||
async function save() {
|
||||
try {
|
||||
await saveImageToMediaLibrary({uri})
|
||||
Toast.show(t`Image saved`)
|
||||
} catch (e: any) {
|
||||
Toast.show(t`Failed to save image: ${String(e)}`, 'xmark')
|
||||
}
|
||||
}
|
||||
|
||||
const permission = permissionResponse ?? (await getPermission())
|
||||
|
||||
if (permission.granted) {
|
||||
await save()
|
||||
} else {
|
||||
if (permission.canAskAgain) {
|
||||
// request again once
|
||||
const askAgain = await requestPermission()
|
||||
if (askAgain.granted) {
|
||||
await save()
|
||||
} else {
|
||||
// since we've been explicitly denied, show a toast.
|
||||
Toast.show(
|
||||
t`Images cannot be saved unless permission is granted to access your photo library.`,
|
||||
'xmark',
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Toast.show(
|
||||
t`Permission to access your photo library was denied. Please enable it in your system settings.`,
|
||||
'xmark',
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
[permissionResponse, requestPermission, getPermission],
|
||||
)
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import {type NotificationReason} from '#/lib/hooks/useNotificationHandler'
|
||||
import {type FeedDescriptor} from '#/state/queries/post-feed'
|
||||
|
||||
export type MetricEvents = {
|
||||
@@ -23,7 +24,9 @@ export type MetricEvents = {
|
||||
| 'Takendown'
|
||||
scope: 'current' | 'every'
|
||||
}
|
||||
'notifications:openApp': {}
|
||||
'notifications:openApp': {
|
||||
reason: NotificationReason
|
||||
}
|
||||
'notifications:request': {
|
||||
context: 'StartOnboarding' | 'AfterOnboarding' | 'Login' | 'Home'
|
||||
status: 'granted' | 'denied' | 'undetermined'
|
||||
|
||||
@@ -882,6 +882,7 @@ export function Explore({
|
||||
record={subItem.record}
|
||||
reason={indexInSlice === 0 ? slice.reason : undefined}
|
||||
feedContext={slice.feedContext}
|
||||
reqId={slice.reqId}
|
||||
moderation={subItem.moderation}
|
||||
parentAuthor={subItem.parentAuthor}
|
||||
showReplyTo={item.showReplyTo}
|
||||
|
||||
@@ -178,6 +178,7 @@ type VideoItem = {
|
||||
post: AppBskyFeedDefs.PostView
|
||||
video: AppBskyEmbedVideo.View
|
||||
feedContext: string | undefined
|
||||
reqId: string | undefined
|
||||
}
|
||||
|
||||
function Feed() {
|
||||
@@ -216,6 +217,7 @@ function Feed() {
|
||||
post: AppBskyFeedDefs.PostView
|
||||
video: AppBskyEmbedVideo.View
|
||||
feedContext: string | undefined
|
||||
reqId: string | undefined
|
||||
}[] = []
|
||||
for (const slice of page.slices) {
|
||||
const feedPost = slice.items.find(
|
||||
@@ -228,6 +230,7 @@ function Feed() {
|
||||
post: feedPost.post,
|
||||
video: feedPost.post.embed,
|
||||
feedContext: slice.feedContext,
|
||||
reqId: slice.reqId,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -274,6 +277,7 @@ function Feed() {
|
||||
moderation={item.moderation}
|
||||
scrollGesture={scrollGesture}
|
||||
feedContext={item.feedContext}
|
||||
reqId={item.reqId}
|
||||
/>
|
||||
)
|
||||
},
|
||||
@@ -470,6 +474,7 @@ let VideoItem = ({
|
||||
scrollGesture,
|
||||
moderation,
|
||||
feedContext,
|
||||
reqId,
|
||||
}: {
|
||||
player?: VideoPlayer
|
||||
post: AppBskyFeedDefs.PostView
|
||||
@@ -479,6 +484,7 @@ let VideoItem = ({
|
||||
scrollGesture: NativeGesture
|
||||
moderation?: ModerationDecision
|
||||
feedContext: string | undefined
|
||||
reqId: string | undefined
|
||||
}): React.ReactNode => {
|
||||
const postShadow = usePostShadow(post)
|
||||
const {width, height} = useSafeAreaFrame()
|
||||
@@ -490,9 +496,10 @@ let VideoItem = ({
|
||||
item: post.uri,
|
||||
event: 'app.bsky.feed.defs#interactionSeen',
|
||||
feedContext,
|
||||
reqId,
|
||||
})
|
||||
}
|
||||
}, [active, post.uri, feedContext, sendInteraction])
|
||||
}, [active, post.uri, feedContext, reqId, sendInteraction])
|
||||
|
||||
// TODO: high-performance android phones should also
|
||||
// be capable of rendering 3 video players, but currently
|
||||
@@ -537,6 +544,7 @@ let VideoItem = ({
|
||||
scrollGesture={scrollGesture}
|
||||
moderation={moderation}
|
||||
feedContext={feedContext}
|
||||
reqId={reqId}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
@@ -682,6 +690,7 @@ function Overlay({
|
||||
scrollGesture,
|
||||
moderation,
|
||||
feedContext,
|
||||
reqId,
|
||||
}: {
|
||||
player?: VideoPlayer
|
||||
post: Shadow<AppBskyFeedDefs.PostView>
|
||||
@@ -690,6 +699,7 @@ function Overlay({
|
||||
scrollGesture: NativeGesture
|
||||
moderation: ModerationDecision
|
||||
feedContext: string | undefined
|
||||
reqId: string | undefined
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
@@ -760,6 +770,7 @@ function Overlay({
|
||||
player={player}
|
||||
post={post}
|
||||
feedContext={feedContext}
|
||||
reqId={reqId}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
@@ -1002,10 +1013,12 @@ function PlayPauseTapArea({
|
||||
player,
|
||||
post,
|
||||
feedContext,
|
||||
reqId,
|
||||
}: {
|
||||
player: VideoPlayer
|
||||
post: Shadow<AppBskyFeedDefs.PostView>
|
||||
feedContext: string | undefined
|
||||
reqId: string | undefined
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const doubleTapRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
@@ -1036,6 +1049,7 @@ function PlayPauseTapArea({
|
||||
item: post.uri,
|
||||
event: 'app.bsky.feed.defs#interactionLike',
|
||||
feedContext,
|
||||
reqId,
|
||||
})
|
||||
} else {
|
||||
doubleTapRef.current = setTimeout(togglePlayPause, 200)
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import React from 'react'
|
||||
import {AppState, AppStateStatus} from 'react-native'
|
||||
import {AppBskyFeedDefs} from '@atproto/api'
|
||||
import {AppState, type AppStateStatus} from 'react-native'
|
||||
import {type AppBskyFeedDefs} from '@atproto/api'
|
||||
import throttle from 'lodash.throttle'
|
||||
|
||||
import {FEEDBACK_FEEDS, STAGING_FEEDS} from '#/lib/constants'
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {logger} from '#/logger'
|
||||
import {FeedDescriptor, FeedPostSliceItem} from '#/state/queries/post-feed'
|
||||
import {
|
||||
type FeedDescriptor,
|
||||
type FeedPostSliceItem,
|
||||
} from '#/state/queries/post-feed'
|
||||
import {getItemsForFeedback} from '#/view/com/posts/PostFeed'
|
||||
import {useAgent} from './session'
|
||||
|
||||
@@ -103,7 +106,7 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
|
||||
return
|
||||
}
|
||||
const items = getItemsForFeedback(feedItem)
|
||||
for (const {item: postItem, feedContext} of items) {
|
||||
for (const {item: postItem, feedContext, reqId} of items) {
|
||||
if (!history.current.has(postItem)) {
|
||||
history.current.add(postItem)
|
||||
queue.current.add(
|
||||
@@ -111,6 +114,7 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
|
||||
item: postItem.uri,
|
||||
event: 'app.bsky.feed.defs#interactionSeen',
|
||||
feedContext,
|
||||
reqId,
|
||||
}),
|
||||
)
|
||||
sendToFeed()
|
||||
@@ -164,12 +168,12 @@ function isDiscoverFeed(feed: FeedDescriptor) {
|
||||
function toString(interaction: AppBskyFeedDefs.Interaction): string {
|
||||
return `${interaction.item}|${interaction.event}|${
|
||||
interaction.feedContext || ''
|
||||
}`
|
||||
}|${interaction.reqId || ''}`
|
||||
}
|
||||
|
||||
function toInteraction(str: string): AppBskyFeedDefs.Interaction {
|
||||
const [item, event, feedContext] = str.split('|')
|
||||
return {item, event, feedContext}
|
||||
const [item, event, feedContext, reqId] = str.split('|')
|
||||
return {item, event, feedContext, reqId}
|
||||
}
|
||||
|
||||
type AggregatedStats = {
|
||||
|
||||
@@ -215,6 +215,7 @@ export function useFeedPreviews(
|
||||
isFallbackMarker: false,
|
||||
isIncompleteThread: item.isIncompleteThread,
|
||||
feedContext: item.feedContext,
|
||||
reqId: item.reqId,
|
||||
reason: item.reason,
|
||||
feedPostUri: item.feedPostUri,
|
||||
items: item.items
|
||||
|
||||
@@ -92,6 +92,7 @@ export interface FeedPostSlice {
|
||||
isIncompleteThread: boolean
|
||||
isFallbackMarker: boolean
|
||||
feedContext: string | undefined
|
||||
reqId: string | undefined
|
||||
feedPostUri: string
|
||||
reason?:
|
||||
| AppBskyFeedDefs.ReasonRepost
|
||||
@@ -316,6 +317,7 @@ export function usePostFeedQuery(
|
||||
userActionHistory.seen(
|
||||
slice.items.map(item => ({
|
||||
feedContext: slice.feedContext,
|
||||
reqId: slice.reqId,
|
||||
likeCount: item.post.likeCount ?? 0,
|
||||
repostCount: item.post.repostCount ?? 0,
|
||||
replyCount: item.post.replyCount ?? 0,
|
||||
@@ -333,6 +335,7 @@ export function usePostFeedQuery(
|
||||
isIncompleteThread: slice.isIncompleteThread,
|
||||
isFallbackMarker: slice.isFallbackMarker,
|
||||
feedContext: slice.feedContext,
|
||||
reqId: slice.reqId,
|
||||
reason: slice.reason,
|
||||
feedPostUri: slice.feedPostUri,
|
||||
items: slice.items.map((item, i) => {
|
||||
|
||||
@@ -1,59 +1,25 @@
|
||||
import React from 'react'
|
||||
import * as MediaLibrary from 'expo-media-library'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useCallback} from 'react'
|
||||
|
||||
import {saveImageToMediaLibrary, shareImageModal} from '#/lib/media/manip'
|
||||
import {shareImageModal} from '#/lib/media/manip'
|
||||
import {useSaveImageToMediaLibrary} from '#/lib/media/save-image'
|
||||
import {useLightbox, useLightboxControls} from '#/state/lightbox'
|
||||
import * as Toast from '../util/Toast'
|
||||
import ImageView from './ImageViewing'
|
||||
|
||||
export function Lightbox() {
|
||||
const {activeLightbox} = useLightbox()
|
||||
const {closeLightbox} = useLightboxControls()
|
||||
|
||||
const onClose = React.useCallback(() => {
|
||||
const onClose = useCallback(() => {
|
||||
closeLightbox()
|
||||
}, [closeLightbox])
|
||||
|
||||
const {_} = useLingui()
|
||||
const [permissionResponse, requestPermission] = MediaLibrary.usePermissions({
|
||||
granularPermissions: ['photo'],
|
||||
})
|
||||
const saveImageToAlbumWithToasts = React.useCallback(
|
||||
async (uri: string) => {
|
||||
if (!permissionResponse || permissionResponse.granted === false) {
|
||||
Toast.show(
|
||||
_(msg`Permission to access camera roll is required.`),
|
||||
'info',
|
||||
)
|
||||
if (permissionResponse?.canAskAgain) {
|
||||
requestPermission()
|
||||
} else {
|
||||
Toast.show(
|
||||
_(
|
||||
msg`Permission to access camera roll was denied. Please enable it in your system settings.`,
|
||||
),
|
||||
'xmark',
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
try {
|
||||
await saveImageToMediaLibrary({uri})
|
||||
Toast.show(_(msg`Image saved`))
|
||||
} catch (e: any) {
|
||||
Toast.show(_(msg`Failed to save image: ${String(e)}`), 'xmark')
|
||||
}
|
||||
},
|
||||
[permissionResponse, requestPermission, _],
|
||||
)
|
||||
const saveImageToAlbum = useSaveImageToMediaLibrary()
|
||||
|
||||
return (
|
||||
<ImageView
|
||||
lightbox={activeLightbox}
|
||||
onRequestClose={onClose}
|
||||
onPressSave={saveImageToAlbumWithToasts}
|
||||
onPressSave={saveImageToAlbum}
|
||||
onPressShare={uri => shareImageModal({uri})}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -103,6 +103,7 @@ type FeedRow =
|
||||
items: FeedPostSliceItem[]
|
||||
sourceFeedUri: string
|
||||
feedContexts: (string | undefined)[]
|
||||
reqIds: (string | undefined)[]
|
||||
}
|
||||
| {
|
||||
type: 'sliceViewFullThread'
|
||||
@@ -134,16 +135,19 @@ export function getItemsForFeedback(feedRow: FeedRow):
|
||||
| {
|
||||
item: FeedPostSliceItem
|
||||
feedContext: string | undefined
|
||||
reqId: string | undefined
|
||||
}[] {
|
||||
if (feedRow.type === 'sliceItem') {
|
||||
return feedRow.slice.items.map(item => ({
|
||||
item,
|
||||
feedContext: feedRow.slice.feedContext,
|
||||
reqId: feedRow.slice.reqId,
|
||||
}))
|
||||
} else if (feedRow.type === 'videoGridRow') {
|
||||
return feedRow.items.map((item, i) => ({
|
||||
item,
|
||||
feedContext: feedRow.feedContexts[i],
|
||||
reqId: feedRow.reqIds[i],
|
||||
}))
|
||||
} else {
|
||||
return []
|
||||
@@ -398,6 +402,7 @@ let PostFeed = ({
|
||||
const videos: {
|
||||
item: FeedPostSliceItem
|
||||
feedContext: string | undefined
|
||||
reqId: string | undefined
|
||||
}[] = []
|
||||
for (const page of data.pages) {
|
||||
for (const slice of page.slices) {
|
||||
@@ -405,7 +410,11 @@ let PostFeed = ({
|
||||
item => item.uri === slice.feedPostUri,
|
||||
)
|
||||
if (item && AppBskyEmbedVideo.isView(item.post.embed)) {
|
||||
videos.push({item, feedContext: slice.feedContext})
|
||||
videos.push({
|
||||
item,
|
||||
feedContext: slice.feedContext,
|
||||
reqId: slice.reqId,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -413,12 +422,17 @@ let PostFeed = ({
|
||||
const rows: {
|
||||
item: FeedPostSliceItem
|
||||
feedContext: string | undefined
|
||||
reqId: string | undefined
|
||||
}[][] = []
|
||||
for (let i = 0; i < videos.length; i++) {
|
||||
const video = videos[i]
|
||||
const item = video.item
|
||||
const cols = gtMobile ? 3 : 2
|
||||
const rowItem = {item, feedContext: video.feedContext}
|
||||
const rowItem = {
|
||||
item,
|
||||
feedContext: video.feedContext,
|
||||
reqId: video.reqId,
|
||||
}
|
||||
if (i % cols === 0) {
|
||||
rows.push([rowItem])
|
||||
} else {
|
||||
@@ -434,6 +448,7 @@ let PostFeed = ({
|
||||
items: row.map(r => r.item),
|
||||
sourceFeedUri: feedUriOrActorDid,
|
||||
feedContexts: row.map(r => r.feedContext),
|
||||
reqIds: row.map(r => r.reqId),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
@@ -685,6 +700,7 @@ let PostFeed = ({
|
||||
record={item.record}
|
||||
reason={indexInSlice === 0 ? slice.reason : undefined}
|
||||
feedContext={slice.feedContext}
|
||||
reqId={slice.reqId}
|
||||
moderation={item.moderation}
|
||||
parentAuthor={item.parentAuthor}
|
||||
showReplyTo={row.showReplyTo}
|
||||
|
||||
@@ -70,6 +70,7 @@ interface FeedItemProps {
|
||||
isThreadLastChild?: boolean
|
||||
isThreadParent?: boolean
|
||||
feedContext: string | undefined
|
||||
reqId: string | undefined
|
||||
hideTopBorder?: boolean
|
||||
isParentBlocked?: boolean
|
||||
isParentNotFound?: boolean
|
||||
@@ -80,6 +81,7 @@ export function PostFeedItem({
|
||||
record,
|
||||
reason,
|
||||
feedContext,
|
||||
reqId,
|
||||
moderation,
|
||||
parentAuthor,
|
||||
showReplyTo,
|
||||
@@ -117,6 +119,7 @@ export function PostFeedItem({
|
||||
record={record}
|
||||
reason={reason}
|
||||
feedContext={feedContext}
|
||||
reqId={reqId}
|
||||
richText={richText}
|
||||
parentAuthor={parentAuthor}
|
||||
showReplyTo={showReplyTo}
|
||||
@@ -140,6 +143,7 @@ let FeedItemInner = ({
|
||||
record,
|
||||
reason,
|
||||
feedContext,
|
||||
reqId,
|
||||
richText,
|
||||
moderation,
|
||||
parentAuthor,
|
||||
@@ -171,11 +175,12 @@ let FeedItemInner = ({
|
||||
}, [post.uri, post.author])
|
||||
const {sendInteraction} = useFeedFeedbackContext()
|
||||
|
||||
const onPressReply = useCallback(() => {
|
||||
const onPressReply = () => {
|
||||
sendInteraction({
|
||||
item: post.uri,
|
||||
event: 'app.bsky.feed.defs#interactionReply',
|
||||
feedContext,
|
||||
reqId,
|
||||
})
|
||||
openComposer({
|
||||
replyTo: {
|
||||
@@ -187,40 +192,44 @@ let FeedItemInner = ({
|
||||
moderation,
|
||||
},
|
||||
})
|
||||
}, [post, record, openComposer, moderation, sendInteraction, feedContext])
|
||||
}
|
||||
|
||||
const onOpenAuthor = useCallback(() => {
|
||||
const onOpenAuthor = () => {
|
||||
sendInteraction({
|
||||
item: post.uri,
|
||||
event: 'app.bsky.feed.defs#clickthroughAuthor',
|
||||
feedContext,
|
||||
reqId,
|
||||
})
|
||||
}, [sendInteraction, post, feedContext])
|
||||
}
|
||||
|
||||
const onOpenReposter = useCallback(() => {
|
||||
const onOpenReposter = () => {
|
||||
sendInteraction({
|
||||
item: post.uri,
|
||||
event: 'app.bsky.feed.defs#clickthroughReposter',
|
||||
feedContext,
|
||||
reqId,
|
||||
})
|
||||
}, [sendInteraction, post, feedContext])
|
||||
}
|
||||
|
||||
const onOpenEmbed = useCallback(() => {
|
||||
const onOpenEmbed = () => {
|
||||
sendInteraction({
|
||||
item: post.uri,
|
||||
event: 'app.bsky.feed.defs#clickthroughEmbed',
|
||||
feedContext,
|
||||
reqId,
|
||||
})
|
||||
}, [sendInteraction, post, feedContext])
|
||||
}
|
||||
|
||||
const onBeforePress = useCallback(() => {
|
||||
const onBeforePress = () => {
|
||||
sendInteraction({
|
||||
item: post.uri,
|
||||
event: 'app.bsky.feed.defs#clickthroughItem',
|
||||
feedContext,
|
||||
reqId,
|
||||
})
|
||||
precacheProfile(queryClient, post.author)
|
||||
}, [queryClient, post, sendInteraction, feedContext])
|
||||
}
|
||||
|
||||
const outerStyles = [
|
||||
styles.outer,
|
||||
@@ -437,6 +446,7 @@ let FeedItemInner = ({
|
||||
onPressReply={onPressReply}
|
||||
logContext="FeedItem"
|
||||
feedContext={feedContext}
|
||||
reqId={reqId}
|
||||
threadgateRecord={threadgateRecord}
|
||||
onShowLess={onShowLess}
|
||||
/>
|
||||
|
||||
@@ -28,6 +28,7 @@ let PostDropdownBtn = ({
|
||||
testID,
|
||||
post,
|
||||
postFeedContext,
|
||||
postReqId,
|
||||
record,
|
||||
richText,
|
||||
style,
|
||||
@@ -40,6 +41,7 @@ let PostDropdownBtn = ({
|
||||
testID: string
|
||||
post: Shadow<AppBskyFeedDefs.PostView>
|
||||
postFeedContext: string | undefined
|
||||
postReqId: string | undefined
|
||||
record: AppBskyFeedPost.Record
|
||||
richText: RichTextAPI
|
||||
style?: StyleProp<ViewStyle>
|
||||
@@ -99,6 +101,7 @@ let PostDropdownBtn = ({
|
||||
testID={testID}
|
||||
post={post}
|
||||
postFeedContext={postFeedContext}
|
||||
postReqId={postReqId}
|
||||
record={record}
|
||||
richText={richText}
|
||||
timestamp={timestamp}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, {memo, useCallback} from 'react'
|
||||
import React, {memo} from 'react'
|
||||
import {
|
||||
Platform,
|
||||
type PressableProps,
|
||||
@@ -97,6 +97,7 @@ import * as Toast from '../Toast'
|
||||
let PostDropdownMenuItems = ({
|
||||
post,
|
||||
postFeedContext,
|
||||
postReqId,
|
||||
record,
|
||||
richText,
|
||||
timestamp,
|
||||
@@ -106,6 +107,7 @@ let PostDropdownMenuItems = ({
|
||||
testID: string
|
||||
post: Shadow<AppBskyFeedDefs.PostView>
|
||||
postFeedContext: string | undefined
|
||||
postReqId: string | undefined
|
||||
record: AppBskyFeedPost.Record
|
||||
richText: RichTextAPI
|
||||
style?: StyleProp<ViewStyle>
|
||||
@@ -189,7 +191,7 @@ let PostDropdownMenuItems = ({
|
||||
langPrefs.primaryLanguage,
|
||||
)
|
||||
|
||||
const onDeletePost = React.useCallback(() => {
|
||||
const onDeletePost = () => {
|
||||
deletePostMutate({uri: postUri}).then(
|
||||
() => {
|
||||
Toast.show(_(msg({message: 'Post deleted', context: 'toast'})))
|
||||
@@ -215,18 +217,9 @@ let PostDropdownMenuItems = ({
|
||||
Toast.show(_(msg`Failed to delete post, please try again`), 'xmark')
|
||||
},
|
||||
)
|
||||
}, [
|
||||
navigation,
|
||||
postUri,
|
||||
deletePostMutate,
|
||||
postAuthor,
|
||||
currentAccount,
|
||||
isAuthor,
|
||||
href,
|
||||
_,
|
||||
])
|
||||
}
|
||||
|
||||
const onToggleThreadMute = React.useCallback(() => {
|
||||
const onToggleThreadMute = () => {
|
||||
try {
|
||||
if (isThreadMuted) {
|
||||
unmuteThread()
|
||||
@@ -246,16 +239,16 @@ let PostDropdownMenuItems = ({
|
||||
)
|
||||
}
|
||||
}
|
||||
}, [isThreadMuted, unmuteThread, _, muteThread])
|
||||
}
|
||||
|
||||
const onCopyPostText = React.useCallback(() => {
|
||||
const onCopyPostText = () => {
|
||||
const str = richTextToString(richText, true)
|
||||
|
||||
Clipboard.setStringAsync(str)
|
||||
Toast.show(_(msg`Copied to clipboard`), 'clipboard-check')
|
||||
}, [_, richText])
|
||||
}
|
||||
|
||||
const onPressTranslate = React.useCallback(async () => {
|
||||
const onPressTranslate = async () => {
|
||||
await openLink(translatorUrl, true)
|
||||
|
||||
if (
|
||||
@@ -270,40 +263,40 @@ let PostDropdownMenuItems = ({
|
||||
textLength: post.record.text.length,
|
||||
})
|
||||
}
|
||||
}, [openLink, translatorUrl, langPrefs, post])
|
||||
}
|
||||
|
||||
const onHidePost = React.useCallback(() => {
|
||||
const onHidePost = () => {
|
||||
hidePost({uri: postUri})
|
||||
}, [postUri, hidePost])
|
||||
}
|
||||
|
||||
const hideInPWI = React.useMemo(() => {
|
||||
return !!postAuthor.labels?.find(
|
||||
label => label.val === '!no-unauthenticated',
|
||||
)
|
||||
}, [postAuthor])
|
||||
const hideInPWI = !!postAuthor.labels?.find(
|
||||
label => label.val === '!no-unauthenticated',
|
||||
)
|
||||
|
||||
const showLoggedOutWarning =
|
||||
postAuthor.did !== currentAccount?.did && hideInPWI
|
||||
|
||||
const onSharePost = React.useCallback(() => {
|
||||
const onSharePost = () => {
|
||||
const url = toShareUrl(href)
|
||||
shareUrl(url)
|
||||
}, [href])
|
||||
}
|
||||
|
||||
const onPressShowMore = React.useCallback(() => {
|
||||
const onPressShowMore = () => {
|
||||
feedFeedback.sendInteraction({
|
||||
event: 'app.bsky.feed.defs#requestMore',
|
||||
item: postUri,
|
||||
feedContext: postFeedContext,
|
||||
reqId: postReqId,
|
||||
})
|
||||
Toast.show(_(msg({message: 'Feedback sent!', context: 'toast'})))
|
||||
}, [feedFeedback, postUri, postFeedContext, _])
|
||||
}
|
||||
|
||||
const onPressShowLess = React.useCallback(() => {
|
||||
const onPressShowLess = () => {
|
||||
feedFeedback.sendInteraction({
|
||||
event: 'app.bsky.feed.defs#requestLess',
|
||||
item: postUri,
|
||||
feedContext: postFeedContext,
|
||||
reqId: postReqId,
|
||||
})
|
||||
if (onShowLess) {
|
||||
onShowLess({
|
||||
@@ -313,19 +306,16 @@ let PostDropdownMenuItems = ({
|
||||
} else {
|
||||
Toast.show(_(msg({message: 'Feedback sent!', context: 'toast'})))
|
||||
}
|
||||
}, [feedFeedback, postUri, postFeedContext, _, onShowLess])
|
||||
}
|
||||
|
||||
const onSelectChatToShareTo = React.useCallback(
|
||||
(conversation: string) => {
|
||||
navigation.navigate('MessagesConversation', {
|
||||
conversation,
|
||||
embed: postUri,
|
||||
})
|
||||
},
|
||||
[navigation, postUri],
|
||||
)
|
||||
const onSelectChatToShareTo = (conversation: string) => {
|
||||
navigation.navigate('MessagesConversation', {
|
||||
conversation,
|
||||
embed: postUri,
|
||||
})
|
||||
}
|
||||
|
||||
const onToggleQuotePostAttachment = React.useCallback(async () => {
|
||||
const onToggleQuotePostAttachment = async () => {
|
||||
if (!quoteEmbed) return
|
||||
|
||||
const action = quoteEmbed.isDetached ? 'reattach' : 'detach'
|
||||
@@ -348,7 +338,7 @@ let PostDropdownMenuItems = ({
|
||||
)
|
||||
logger.error(`Failed to ${action} quote`, {safeMessage: e.message})
|
||||
}
|
||||
}, [_, quoteEmbed, post, toggleQuoteDetachment])
|
||||
}
|
||||
|
||||
const canHidePostForMe = !isAuthor && !isPostHidden
|
||||
const canEmbed = isWeb && gtMobile && !hideInPWI
|
||||
@@ -356,7 +346,7 @@ let PostDropdownMenuItems = ({
|
||||
!isAuthor && isRootPostAuthor && !isPostHidden && isReply
|
||||
const canDetachQuote = quoteEmbed && quoteEmbed.isOwnedByViewer
|
||||
|
||||
const onToggleReplyVisibility = React.useCallback(async () => {
|
||||
const onToggleReplyVisibility = async () => {
|
||||
// TODO no threadgate?
|
||||
if (!canHideReplyForEveryone) return
|
||||
|
||||
@@ -380,25 +370,18 @@ let PostDropdownMenuItems = ({
|
||||
)
|
||||
logger.error(`Failed to ${action} reply`, {safeMessage: e.message})
|
||||
}
|
||||
}, [
|
||||
_,
|
||||
isReplyHiddenByThreadgate,
|
||||
rootUri,
|
||||
postUri,
|
||||
canHideReplyForEveryone,
|
||||
toggleReplyVisibility,
|
||||
])
|
||||
}
|
||||
|
||||
const onPressPin = useCallback(() => {
|
||||
const onPressPin = () => {
|
||||
logEvent(isPinned ? 'post:unpin' : 'post:pin', {})
|
||||
pinPostMutate({
|
||||
postUri,
|
||||
postCid,
|
||||
action: isPinned ? 'unpin' : 'pin',
|
||||
})
|
||||
}, [isPinned, pinPostMutate, postCid, postUri])
|
||||
}
|
||||
|
||||
const onBlockAuthor = useCallback(async () => {
|
||||
const onBlockAuthor = async () => {
|
||||
try {
|
||||
await queueBlock()
|
||||
Toast.show(_(msg({message: 'Account blocked', context: 'toast'})))
|
||||
@@ -408,9 +391,9 @@ let PostDropdownMenuItems = ({
|
||||
Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
|
||||
}
|
||||
}
|
||||
}, [_, queueBlock])
|
||||
}
|
||||
|
||||
const onMuteAuthor = useCallback(async () => {
|
||||
const onMuteAuthor = async () => {
|
||||
if (postAuthor.viewer?.muted) {
|
||||
try {
|
||||
await queueUnmute()
|
||||
@@ -432,22 +415,22 @@ let PostDropdownMenuItems = ({
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [_, queueMute, queueUnmute, postAuthor.viewer?.muted])
|
||||
}
|
||||
|
||||
const onShareATURI = useCallback(() => {
|
||||
const onShareATURI = () => {
|
||||
shareText(postUri)
|
||||
}, [postUri])
|
||||
}
|
||||
|
||||
const onShareAuthorDID = useCallback(() => {
|
||||
const onShareAuthorDID = () => {
|
||||
shareText(postAuthor.did)
|
||||
}, [postAuthor.did])
|
||||
}
|
||||
|
||||
const onReportMisclassification = useCallback(() => {
|
||||
const onReportMisclassification = () => {
|
||||
const url = `https://docs.google.com/forms/d/e/1FAIpQLSd0QPqhNFksDQf1YyOos7r1ofCLvmrKAH1lU042TaS3GAZaWQ/viewform?entry.1756031717=${toShareUrl(
|
||||
href,
|
||||
)}`
|
||||
openLink(url)
|
||||
}, [href, openLink])
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, {memo, useCallback} from 'react'
|
||||
import React, {memo} from 'react'
|
||||
import {
|
||||
Pressable,
|
||||
type PressableStateCallbackType,
|
||||
@@ -55,6 +55,7 @@ let PostCtrls = ({
|
||||
record,
|
||||
richText,
|
||||
feedContext,
|
||||
reqId,
|
||||
style,
|
||||
onPressReply,
|
||||
onPostReply,
|
||||
@@ -67,6 +68,7 @@ let PostCtrls = ({
|
||||
record: AppBskyFeedPost.Record
|
||||
richText: RichTextAPI
|
||||
feedContext?: string | undefined
|
||||
reqId?: string | undefined
|
||||
style?: StyleProp<ViewStyle>
|
||||
onPressReply: () => void
|
||||
onPostReply?: (postUri: string | undefined) => void
|
||||
@@ -117,7 +119,7 @@ let PostCtrls = ({
|
||||
const [hasLikeIconBeenToggled, setHasLikeIconBeenToggled] =
|
||||
React.useState(false)
|
||||
|
||||
const onPressToggleLike = React.useCallback(async () => {
|
||||
const onPressToggleLike = async () => {
|
||||
if (isBlocked) {
|
||||
Toast.show(
|
||||
_(msg`Cannot interact with a blocked user`),
|
||||
@@ -134,6 +136,7 @@ let PostCtrls = ({
|
||||
item: post.uri,
|
||||
event: 'app.bsky.feed.defs#interactionLike',
|
||||
feedContext,
|
||||
reqId,
|
||||
})
|
||||
captureAction(ProgressGuideAction.Like)
|
||||
await queueLike()
|
||||
@@ -145,20 +148,9 @@ let PostCtrls = ({
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}, [
|
||||
_,
|
||||
playHaptic,
|
||||
post.uri,
|
||||
post.viewer?.like,
|
||||
queueLike,
|
||||
queueUnlike,
|
||||
sendInteraction,
|
||||
captureAction,
|
||||
feedContext,
|
||||
isBlocked,
|
||||
])
|
||||
}
|
||||
|
||||
const onRepost = useCallback(async () => {
|
||||
const onRepost = async () => {
|
||||
if (isBlocked) {
|
||||
Toast.show(
|
||||
_(msg`Cannot interact with a blocked user`),
|
||||
@@ -173,6 +165,7 @@ let PostCtrls = ({
|
||||
item: post.uri,
|
||||
event: 'app.bsky.feed.defs#interactionRepost',
|
||||
feedContext,
|
||||
reqId,
|
||||
})
|
||||
await queueRepost()
|
||||
} else {
|
||||
@@ -183,18 +176,9 @@ let PostCtrls = ({
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}, [
|
||||
_,
|
||||
post.uri,
|
||||
post.viewer?.repost,
|
||||
queueRepost,
|
||||
queueUnrepost,
|
||||
sendInteraction,
|
||||
feedContext,
|
||||
isBlocked,
|
||||
])
|
||||
}
|
||||
|
||||
const onQuote = useCallback(() => {
|
||||
const onQuote = () => {
|
||||
if (isBlocked) {
|
||||
Toast.show(
|
||||
_(msg`Cannot interact with a blocked user`),
|
||||
@@ -207,22 +191,15 @@ let PostCtrls = ({
|
||||
item: post.uri,
|
||||
event: 'app.bsky.feed.defs#interactionQuote',
|
||||
feedContext,
|
||||
reqId,
|
||||
})
|
||||
openComposer({
|
||||
quote: post,
|
||||
onPost: onPostReply,
|
||||
})
|
||||
}, [
|
||||
_,
|
||||
sendInteraction,
|
||||
post,
|
||||
feedContext,
|
||||
openComposer,
|
||||
onPostReply,
|
||||
isBlocked,
|
||||
])
|
||||
}
|
||||
|
||||
const onShare = useCallback(() => {
|
||||
const onShare = () => {
|
||||
const urip = new AtUri(post.uri)
|
||||
const href = makeProfileLink(post.author, 'post', urip.rkey)
|
||||
const url = toShareUrl(href)
|
||||
@@ -231,8 +208,9 @@ let PostCtrls = ({
|
||||
item: post.uri,
|
||||
event: 'app.bsky.feed.defs#interactionShare',
|
||||
feedContext,
|
||||
reqId,
|
||||
})
|
||||
}, [post.uri, post.author, sendInteraction, feedContext])
|
||||
}
|
||||
|
||||
const btnStyle = React.useCallback(
|
||||
({pressed, hovered}: PressableStateCallbackType) => [
|
||||
@@ -374,6 +352,7 @@ let PostCtrls = ({
|
||||
testID="postDropdownBtn"
|
||||
post={post}
|
||||
postFeedContext={feedContext}
|
||||
postReqId={reqId}
|
||||
record={record}
|
||||
richText={richText}
|
||||
style={{padding: 5}}
|
||||
|
||||
@@ -829,6 +829,7 @@ function MockPostFeedItem({
|
||||
showReplyTo={false}
|
||||
reason={undefined}
|
||||
feedContext={''}
|
||||
reqId={undefined}
|
||||
rootPost={post}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -20,28 +20,27 @@
|
||||
"@jridgewell/gen-mapping" "^0.3.0"
|
||||
"@jridgewell/trace-mapping" "^0.3.9"
|
||||
|
||||
"@atproto-labs/fetch-node@0.1.8":
|
||||
version "0.1.8"
|
||||
resolved "https://registry.yarnpkg.com/@atproto-labs/fetch-node/-/fetch-node-0.1.8.tgz#687fc8be6107f10a4247c17989792862affd838b"
|
||||
integrity sha512-OOTIhZNPEDDm7kaYU8iYRgzM+D5n3mP2iiBSyKuLakKTaZBL5WwYlUsJVsqX26SnUXtGEroOJEVJ6f66OcG80w==
|
||||
"@atproto-labs/fetch-node@0.1.9":
|
||||
version "0.1.9"
|
||||
resolved "https://registry.yarnpkg.com/@atproto-labs/fetch-node/-/fetch-node-0.1.9.tgz#5df902413cc2ebfff914999ad3fbbc13b20e1dd0"
|
||||
integrity sha512-8sHDDXZEzQptLu8ddUU/8U+THS6dumgPynVX0/1PjUYd4S/FWyPcz6yMIiVChTfzKnZvYRRz47+qvOKhydrHQw==
|
||||
dependencies:
|
||||
"@atproto-labs/fetch" "0.2.2"
|
||||
"@atproto-labs/pipe" "0.1.0"
|
||||
"@atproto-labs/fetch" "0.2.3"
|
||||
"@atproto-labs/pipe" "0.1.1"
|
||||
ipaddr.js "^2.1.0"
|
||||
psl "^1.9.0"
|
||||
undici "^6.14.1"
|
||||
|
||||
"@atproto-labs/fetch@0.2.2":
|
||||
version "0.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@atproto-labs/fetch/-/fetch-0.2.2.tgz#c65acfd7b2265a8fe7d4ba3997126cce07bafe26"
|
||||
integrity sha512-QyafkedbFeVaN20DYUpnY2hcArYxjdThPXbYMqOSoZhcvkrUqaw4xDND4wZB5TBD9cq2yqe9V6mcw9P4XQKQuQ==
|
||||
"@atproto-labs/fetch@0.2.3":
|
||||
version "0.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@atproto-labs/fetch/-/fetch-0.2.3.tgz#d47afec078f630c50e291c56264cc0ff13d0c6cc"
|
||||
integrity sha512-NZtbJOCbxKUFRFKMpamT38PUQMY0hX0p7TG5AEYOPhZKZEP7dHZ1K2s1aB8MdVH0qxmqX7nQleNrrvLf09Zfdw==
|
||||
dependencies:
|
||||
"@atproto-labs/pipe" "0.1.0"
|
||||
"@atproto-labs/pipe" "0.1.1"
|
||||
|
||||
"@atproto-labs/pipe@0.1.0":
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@atproto-labs/pipe/-/pipe-0.1.0.tgz#c8d86923b6d8e900d39efe6fdcdf0d897c434086"
|
||||
integrity sha512-ghOqHFyJlQVFPESzlVHjKroP0tPzbmG5Jms0dNI9yLDEfL8xp4OFPWLX4f6T8mRq69wWs4nIDM3sSsFbFqLa1w==
|
||||
"@atproto-labs/pipe@0.1.1":
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@atproto-labs/pipe/-/pipe-0.1.1.tgz#1c4232d16bf95f251e993cb6ee440f9aa4e87ce6"
|
||||
integrity sha512-hdNw2oUs2B6BN1lp+32pF7cp8EMKuIN5Qok2Vvv/aOpG/3tNSJ9YkvfI0k6Zd188LeDDYRUpYpxcoFIcGH/FNg==
|
||||
|
||||
"@atproto-labs/simple-store-memory@0.1.3":
|
||||
version "0.1.3"
|
||||
@@ -64,10 +63,10 @@
|
||||
"@atproto/xrpc" "^0.7.0"
|
||||
"@atproto/xrpc-server" "^0.7.18"
|
||||
|
||||
"@atproto/api@^0.15.7":
|
||||
version "0.15.7"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.15.7.tgz#8436162d9fa5dac627bdd5c0f5c9598309ec1383"
|
||||
integrity sha512-YRETLcOwDCYfGs7Sl9ObqPwhOlVWrPkw4f1AYGIrXLQS58WHe/vz1lZbqOqMsC6gvCnyZnOuKlhsRHZ14rBLzg==
|
||||
"@atproto/api@^0.15.8":
|
||||
version "0.15.8"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.15.8.tgz#f284a9c225191ebd35b46f5695932ab649c04a61"
|
||||
integrity sha512-PsCgmV4zPjN8VuJMruxqauhn88PuS0b8t2Xsjl4617+bCPpY513jVlxgNH/XExxO7TSVvJM7EzdLY4o3fqh/xQ==
|
||||
dependencies:
|
||||
"@atproto/common-web" "^0.4.2"
|
||||
"@atproto/lexicon" "^0.4.11"
|
||||
@@ -95,14 +94,14 @@
|
||||
multiformats "^9.9.0"
|
||||
uint8arrays "3.0.0"
|
||||
|
||||
"@atproto/bsky@^0.0.149":
|
||||
version "0.0.149"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/bsky/-/bsky-0.0.149.tgz#3e9cfb999b9958e9a61776eddb72d424905ec3be"
|
||||
integrity sha512-7j2KgWHm1nOTQDmtEcNwtldTArS9WwZS3M+aw7OmGH8wCa8vEljNxP6HETjtktDMNTrSipHmmyqh25+Rc5+Ziw==
|
||||
"@atproto/bsky@^0.0.150":
|
||||
version "0.0.150"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/bsky/-/bsky-0.0.150.tgz#6626095875d805d0d3f38fa4e184b9f7d274c80f"
|
||||
integrity sha512-dn1jzP1EId842+g78Q6EMdOmgEZxa9bSq20HMdd5/R8uu559mPs8zigFuddqCoT1fRaJXFC8ZP7Jk5asvBQhrA==
|
||||
dependencies:
|
||||
"@atproto-labs/fetch-node" "0.1.8"
|
||||
"@atproto-labs/fetch-node" "0.1.9"
|
||||
"@atproto-labs/xrpc-utils" "0.0.14"
|
||||
"@atproto/api" "^0.15.7"
|
||||
"@atproto/api" "^0.15.8"
|
||||
"@atproto/common" "^0.4.11"
|
||||
"@atproto/crypto" "^0.4.4"
|
||||
"@atproto/did" "^0.1.5"
|
||||
@@ -219,20 +218,20 @@
|
||||
"@noble/hashes" "^1.6.1"
|
||||
uint8arrays "3.0.0"
|
||||
|
||||
"@atproto/dev-env@^0.3.131":
|
||||
version "0.3.131"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/dev-env/-/dev-env-0.3.131.tgz#b3b4cee5f367766d542515b1713523423ecb5a71"
|
||||
integrity sha512-Tijqc/vq7qKGTpgoKm1BwyvP2QfoOQRjNm9Ro5CDAMXsKqHfXxPiytxYqxj6QR/PptC27aDUqgmexluZN6XbWg==
|
||||
"@atproto/dev-env@^0.3.132":
|
||||
version "0.3.132"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/dev-env/-/dev-env-0.3.132.tgz#78d55ef08a368a752c55b1ee7b7c08a41f27b5ac"
|
||||
integrity sha512-RFd/9kgvmbP859N6NLu/FxCzLsj01iq22P9jNpL+dQNXbWXHYwGMUa6edf/ZrljNi3dFBNxabdDZJ2q+8uvBJQ==
|
||||
dependencies:
|
||||
"@atproto/api" "^0.15.7"
|
||||
"@atproto/bsky" "^0.0.149"
|
||||
"@atproto/api" "^0.15.8"
|
||||
"@atproto/bsky" "^0.0.150"
|
||||
"@atproto/bsync" "^0.0.19"
|
||||
"@atproto/common-web" "^0.4.2"
|
||||
"@atproto/crypto" "^0.4.4"
|
||||
"@atproto/identity" "^0.4.8"
|
||||
"@atproto/lexicon" "^0.4.11"
|
||||
"@atproto/ozone" "^0.1.110"
|
||||
"@atproto/pds" "^0.4.137"
|
||||
"@atproto/ozone" "^0.1.111"
|
||||
"@atproto/pds" "^0.4.138"
|
||||
"@atproto/sync" "^0.1.23"
|
||||
"@atproto/syntax" "^0.4.0"
|
||||
"@atproto/xrpc-server" "^0.7.18"
|
||||
@@ -302,21 +301,21 @@
|
||||
optionalDependencies:
|
||||
"@atproto/oauth-provider-api" "0.1.2"
|
||||
|
||||
"@atproto/oauth-provider-ui@0.1.4":
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-ui/-/oauth-provider-ui-0.1.4.tgz#5e092d30afa583fdab54fc78371aecb1cbfa017d"
|
||||
integrity sha512-GTQnB7OUBFSeXcdRseAGYzKe9UUFB/kGjRcIA8+pO5pCMD7JdXI+WliUhsbdmQ2I+OK78aAlCrmygNWpLtpZgg==
|
||||
"@atproto/oauth-provider-ui@0.1.5":
|
||||
version "0.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-ui/-/oauth-provider-ui-0.1.5.tgz#b080c5e814975821689c5976c27ac1081211106f"
|
||||
integrity sha512-pW0Vx3kvIWH1Mu3SOImNHP9JbmhSj2e3ChDvtfXCWI1oC03fiaMlJfdxrx9Plq5Z+DajnCzPzrf1Lvbopjf94Q==
|
||||
optionalDependencies:
|
||||
"@atproto/oauth-provider-api" "0.1.2"
|
||||
|
||||
"@atproto/oauth-provider@^0.7.6":
|
||||
version "0.7.6"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/oauth-provider/-/oauth-provider-0.7.6.tgz#68bc37303611d548bae9f653d41bc89bd8890152"
|
||||
integrity sha512-4YcnddACznmpuRmHlt9G+kccdv2Gct5qQOF9Yyjse8cl2Td+Rg1gkchpRdWUnyr9fgZzmCsSBYzEfVXge3eUiQ==
|
||||
"@atproto/oauth-provider@^0.7.7":
|
||||
version "0.7.7"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/oauth-provider/-/oauth-provider-0.7.7.tgz#dbbdeb405ab1d239fd926340f83fb41e13455011"
|
||||
integrity sha512-ElphzmOjw1hr42HN4dD6sMAQFtpTkaJ8bBDAsbL9YBVJDEGhmHsF3Ye8mDUO4nhEdg7PUTWiCzXyqnaorAjiTA==
|
||||
dependencies:
|
||||
"@atproto-labs/fetch" "0.2.2"
|
||||
"@atproto-labs/fetch-node" "0.1.8"
|
||||
"@atproto-labs/pipe" "0.1.0"
|
||||
"@atproto-labs/fetch" "0.2.3"
|
||||
"@atproto-labs/fetch-node" "0.1.9"
|
||||
"@atproto-labs/pipe" "0.1.1"
|
||||
"@atproto-labs/simple-store" "0.2.0"
|
||||
"@atproto-labs/simple-store-memory" "0.1.3"
|
||||
"@atproto/common" "^0.4.11"
|
||||
@@ -324,7 +323,7 @@
|
||||
"@atproto/jwk-jose" "0.1.6"
|
||||
"@atproto/oauth-provider-api" "0.1.2"
|
||||
"@atproto/oauth-provider-frontend" "0.1.4"
|
||||
"@atproto/oauth-provider-ui" "0.1.4"
|
||||
"@atproto/oauth-provider-ui" "0.1.5"
|
||||
"@atproto/oauth-types" "0.2.7"
|
||||
"@atproto/syntax" "0.4.0"
|
||||
"@hapi/accept" "^6.0.3"
|
||||
@@ -337,7 +336,6 @@
|
||||
http-errors "^2.0.0"
|
||||
ioredis "^5.3.2"
|
||||
jose "^5.2.0"
|
||||
psl "^1.9.0"
|
||||
zod "^3.23.8"
|
||||
|
||||
"@atproto/oauth-types@0.2.7":
|
||||
@@ -348,12 +346,12 @@
|
||||
"@atproto/jwk" "0.1.5"
|
||||
zod "^3.23.8"
|
||||
|
||||
"@atproto/ozone@^0.1.110":
|
||||
version "0.1.110"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.1.110.tgz#78ad57961b4699c8aa3e6f7d5b6f215d7760a723"
|
||||
integrity sha512-X7VU7QAkwJrwpgmAuAHqvVDX9CEW0Ts5R4ovATgEt2lbxyxtJtYIm1dG346fAlOfC9f3RGN+HI8vBMWrrrLKAQ==
|
||||
"@atproto/ozone@^0.1.111":
|
||||
version "0.1.111"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.1.111.tgz#7ef4a02f1af045ab44254fb9d44ab0e50fd94ba9"
|
||||
integrity sha512-NY+Cn/3dY4tPFkMUoJR1KMZN/v9ZIxjx6EQBMwn/nqTiHk0E3rtGEbyL2jLQ7x+FxpPTjDgpnn3K725+8XUaAg==
|
||||
dependencies:
|
||||
"@atproto/api" "^0.15.7"
|
||||
"@atproto/api" "^0.15.8"
|
||||
"@atproto/common" "^0.4.11"
|
||||
"@atproto/crypto" "^0.4.4"
|
||||
"@atproto/identity" "^0.4.8"
|
||||
@@ -378,20 +376,20 @@
|
||||
undici "^6.14.1"
|
||||
ws "^8.12.0"
|
||||
|
||||
"@atproto/pds@^0.4.137":
|
||||
version "0.4.137"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.4.137.tgz#87468703b02bf42681ddd50049ee906331655731"
|
||||
integrity sha512-DRUck9CgOdK0cP6B6/1Cku2gb5t31Vhh9su2TcqF9eymZP1dNSI6nfTIEp+cuwpW/VpDeu7AfHCSgYfnJeZ5yg==
|
||||
"@atproto/pds@^0.4.138":
|
||||
version "0.4.138"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.4.138.tgz#437d785c83f710bf37bef8baf687b0a46ce9dc68"
|
||||
integrity sha512-WLzDhmguTgs2wQNKoGxCbpKNegDnRiemSslenMbPrB7kSiXYj+XZobLyoIXHv1EnAd2pbThwNEL8z8EfkM0mDg==
|
||||
dependencies:
|
||||
"@atproto-labs/fetch-node" "0.1.8"
|
||||
"@atproto-labs/fetch-node" "0.1.9"
|
||||
"@atproto-labs/xrpc-utils" "0.0.14"
|
||||
"@atproto/api" "^0.15.7"
|
||||
"@atproto/api" "^0.15.8"
|
||||
"@atproto/aws" "^0.2.21"
|
||||
"@atproto/common" "^0.4.11"
|
||||
"@atproto/crypto" "^0.4.4"
|
||||
"@atproto/identity" "^0.4.8"
|
||||
"@atproto/lexicon" "^0.4.11"
|
||||
"@atproto/oauth-provider" "^0.7.6"
|
||||
"@atproto/oauth-provider" "^0.7.7"
|
||||
"@atproto/repo" "^0.8.1"
|
||||
"@atproto/syntax" "^0.4.0"
|
||||
"@atproto/xrpc" "^0.7.0"
|
||||
|
||||
Reference in New Issue
Block a user