diff --git a/assets/icons/pin_filled_stroke2_corner0_rounded.svg b/assets/icons/pin_filled_stroke2_corner0_rounded.svg
new file mode 100644
index 0000000000..a2e71b967c
--- /dev/null
+++ b/assets/icons/pin_filled_stroke2_corner0_rounded.svg
@@ -0,0 +1 @@
+
diff --git a/package.json b/package.json
index bbe46aa7c3..23e2bb324a 100644
--- a/package.json
+++ b/package.json
@@ -54,7 +54,7 @@
"icons:optimize": "svgo -f ./assets/icons"
},
"dependencies": {
- "@atproto/api": "^0.13.18",
+ "@atproto/api": "^0.13.20",
"@bitdrift/react-native": "0.4.0",
"@braintree/sanitize-url": "^6.0.2",
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
@@ -206,7 +206,7 @@
"zod": "^3.20.2"
},
"devDependencies": {
- "@atproto/dev-env": "^0.3.64",
+ "@atproto/dev-env": "^0.3.67",
"@babel/core": "^7.26.0",
"@babel/preset-env": "^7.26.0",
"@babel/runtime": "^7.26.0",
diff --git a/src/Navigation.tsx b/src/Navigation.tsx
index cf00215264..7443128d2c 100644
--- a/src/Navigation.tsx
+++ b/src/Navigation.tsx
@@ -55,7 +55,6 @@ import {NotificationsScreen} from '#/view/screens/Notifications'
import {PostThreadScreen} from '#/view/screens/PostThread'
import {PrivacyPolicyScreen} from '#/view/screens/PrivacyPolicy'
import {ProfileScreen} from '#/view/screens/Profile'
-import {ProfileFeedScreen} from '#/view/screens/ProfileFeed'
import {ProfileFeedLikedByScreen} from '#/view/screens/ProfileFeedLikedBy'
import {ProfileListScreen} from '#/view/screens/ProfileList'
import {SavedFeeds} from '#/view/screens/SavedFeeds'
@@ -75,6 +74,7 @@ import {PostLikedByScreen} from '#/screens/Post/PostLikedBy'
import {PostQuotesScreen} from '#/screens/Post/PostQuotes'
import {PostRepostedByScreen} from '#/screens/Post/PostRepostedBy'
import {ProfileKnownFollowersScreen} from '#/screens/Profile/KnownFollowers'
+import {ProfileFeedScreen} from '#/screens/Profile/ProfileFeed'
import {ProfileFollowersScreen} from '#/screens/Profile/ProfileFollowers'
import {ProfileFollowsScreen} from '#/screens/Profile/ProfileFollows'
import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy'
diff --git a/src/alf/themes.ts b/src/alf/themes.ts
index 0cfe09aadc..cb97a7065b 100644
--- a/src/alf/themes.ts
+++ b/src/alf/themes.ts
@@ -60,6 +60,7 @@ export function createThemes({
dim: Theme
} {
const color = {
+ like: '#ec4899',
trueBlack: '#000000',
gray_0: `hsl(${hues.primary}, 20%, ${defaultScale[14]}%)`,
@@ -124,6 +125,7 @@ export function createThemes({
const lightPalette = {
white: color.gray_0,
black: color.gray_1000,
+ like: color.like,
contrast_25: color.gray_25,
contrast_50: color.gray_50,
@@ -185,6 +187,7 @@ export function createThemes({
const darkPalette: Palette = {
white: color.gray_25,
black: color.trueBlack,
+ like: color.like,
contrast_25: color.gray_975,
contrast_50: color.gray_950,
@@ -246,6 +249,7 @@ export function createThemes({
const dimPalette: Palette = {
...darkPalette,
black: `hsl(${hues.primary}, 28%, ${dimScale[0]}%)`,
+ like: color.like,
contrast_25: `hsl(${hues.primary}, 28%, ${dimScale[1]}%)`,
contrast_50: `hsl(${hues.primary}, 28%, ${dimScale[2]}%)`,
diff --git a/src/alf/types.ts b/src/alf/types.ts
index 08ec593927..5bac690e2b 100644
--- a/src/alf/types.ts
+++ b/src/alf/types.ts
@@ -12,6 +12,7 @@ export type ThemeName = 'light' | 'dim' | 'dark'
export type Palette = {
white: string
black: string
+ like: string
contrast_25: string
contrast_50: string
diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx
index d08505fbfd..8532cbbb49 100644
--- a/src/components/Layout/index.tsx
+++ b/src/components/Layout/index.tsx
@@ -21,6 +21,7 @@ export * as Header from '#/components/Layout/Header'
export type ScreenProps = React.ComponentProps & {
style?: StyleProp
+ noInsetTop?: boolean
}
/**
@@ -28,6 +29,7 @@ export type ScreenProps = React.ComponentProps & {
*/
export const Screen = React.memo(function Screen({
style,
+ noInsetTop,
...props
}: ScreenProps) {
const {top} = useSafeAreaInsets()
@@ -35,7 +37,7 @@ export const Screen = React.memo(function Screen({
<>
{isWeb && }
>
diff --git a/src/components/icons/Pin.tsx b/src/components/icons/Pin.tsx
index 03dbbac90e..d1c37f39a3 100644
--- a/src/components/icons/Pin.tsx
+++ b/src/components/icons/Pin.tsx
@@ -3,3 +3,7 @@ import {createSinglePathSVG} from './TEMPLATE'
export const Pin_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M6.5 3a1 1 0 0 1 1-1h9a1 1 0 0 1 1 1v3.997a6.25 6.25 0 0 0 1.83 4.42l.377.376A1 1 0 0 1 20 12.5V15a1 1 0 0 1-1 1h-6v5a1 1 0 1 1-2 0v-5H5a1 1 0 0 1-1-1v-2.5a1 1 0 0 1 .293-.707l.376-.377A6.25 6.25 0 0 0 6.5 6.996V3.001Zm2 1v2.997a8.25 8.25 0 0 1-2.416 5.834L6 12.914V14h12v-1.086l-.084-.083A8.25 8.25 0 0 1 15.5 6.997V4h-7Z',
})
+
+export const Pin_Filled_Corner0_Rounded = createSinglePathSVG({
+ path: 'M7.5 2a1 1 0 0 0-1 1v3.997a6.25 6.25 0 0 1-1.83 4.42l-.377.376A1 1 0 0 0 4 12.5V15a1 1 0 0 0 1 1h6v5a1 1 0 1 0 2 0v-5h6a1 1 0 0 0 1-1v-2.5a1 1 0 0 0-.293-.707l-.376-.377a6.25 6.25 0 0 1-1.831-4.42V3.001a1 1 0 0 0-1-1h-9Z',
+})
diff --git a/src/lib/hooks/useNotificationHandler.ts b/src/lib/hooks/useNotificationHandler.ts
index 69ae536d02..2ec3fcb799 100644
--- a/src/lib/hooks/useNotificationHandler.ts
+++ b/src/lib/hooks/useNotificationHandler.ts
@@ -239,14 +239,21 @@ export function useNotificationsHandler() {
)
logEvent('notifications:openApp', {})
invalidateCachedUnreadPage()
- truncateAndInvalidate(queryClient, RQKEY_NOTIFS())
+ const payload = e.notification.request.trigger
+ .payload as NotificationPayload
+ truncateAndInvalidate(queryClient, RQKEY_NOTIFS('all'))
+ if (
+ payload.reason === 'mention' ||
+ payload.reason === 'quote' ||
+ payload.reason === 'reply'
+ ) {
+ truncateAndInvalidate(queryClient, RQKEY_NOTIFS('mentions'))
+ }
logger.debug('Notifications: handleNotification', {
content: e.notification.request.content,
payload: e.notification.request.trigger.payload,
})
- handleNotification(
- e.notification.request.trigger.payload as NotificationPayload,
- )
+ handleNotification(payload)
Notifications.dismissAllNotificationsAsync()
}
})
diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts
index 9e3407261d..238e4be4c3 100644
--- a/src/lib/routes/types.ts
+++ b/src/lib/routes/types.ts
@@ -75,7 +75,7 @@ export type SearchTabNavigatorParams = CommonNavigatorParams & {
}
export type NotificationsTabNavigatorParams = CommonNavigatorParams & {
- Notifications: {show?: 'all'}
+ Notifications: undefined
}
export type MyProfileTabNavigatorParams = CommonNavigatorParams & {
@@ -90,7 +90,7 @@ export type FlatNavigatorParams = CommonNavigatorParams & {
Home: undefined
Search: {q?: string}
Feeds: undefined
- Notifications: {show?: 'all'}
+ Notifications: undefined
Hashtag: {tag: string; author?: string}
Messages: {pushToConversation?: string; animation?: 'push' | 'pop'}
}
@@ -102,7 +102,7 @@ export type AllNavigatorParams = CommonNavigatorParams & {
Search: {q?: string}
Feeds: undefined
NotificationsTab: undefined
- Notifications: {show?: 'all'}
+ Notifications: undefined
MyProfileTab: undefined
Hashtag: {tag: string; author?: string}
MessagesTab: undefined
diff --git a/src/screens/Hashtag.tsx b/src/screens/Hashtag.tsx
index a87487150c..3e98f364bf 100644
--- a/src/screens/Hashtag.tsx
+++ b/src/screens/Hashtag.tsx
@@ -107,7 +107,7 @@ export default function HashtagScreen({
return (
-
+
{headerTitle}
diff --git a/src/screens/Profile/ProfileFeed/index.tsx b/src/screens/Profile/ProfileFeed/index.tsx
new file mode 100644
index 0000000000..7d48b5ac15
--- /dev/null
+++ b/src/screens/Profile/ProfileFeed/index.tsx
@@ -0,0 +1,227 @@
+import React, {useCallback, useMemo} from 'react'
+import {StyleSheet, View} from 'react-native'
+import {useAnimatedRef} from 'react-native-reanimated'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useIsFocused, useNavigation} from '@react-navigation/native'
+import {NativeStackScreenProps} from '@react-navigation/native-stack'
+import {useQueryClient} from '@tanstack/react-query'
+
+import {usePalette} from '#/lib/hooks/usePalette'
+import {useSetTitle} from '#/lib/hooks/useSetTitle'
+import {ComposeIcon2} from '#/lib/icons'
+import {CommonNavigatorParams} from '#/lib/routes/types'
+import {NavigationProp} from '#/lib/routes/types'
+import {makeRecordUri} from '#/lib/strings/url-helpers'
+import {s} from '#/lib/styles'
+import {isNative} from '#/platform/detection'
+import {listenSoftReset} from '#/state/events'
+import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
+import {FeedSourceFeedInfo, useFeedSourceInfoQuery} from '#/state/queries/feed'
+import {FeedDescriptor} from '#/state/queries/post-feed'
+import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
+import {
+ usePreferencesQuery,
+ UsePreferencesQueryResponse,
+} from '#/state/queries/preferences'
+import {useResolveUriQuery} from '#/state/queries/resolve-uri'
+import {truncateAndInvalidate} from '#/state/queries/util'
+import {useSession} from '#/state/session'
+import {useComposerControls} from '#/state/shell/composer'
+import {PostFeed} from '#/view/com/posts/PostFeed'
+import {EmptyState} from '#/view/com/util/EmptyState'
+import {FAB} from '#/view/com/util/fab/FAB'
+import {Button} from '#/view/com/util/forms/Button'
+import {ListRef} from '#/view/com/util/List'
+import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn'
+import {LoadingScreen} from '#/view/com/util/LoadingScreen'
+import {Text} from '#/view/com/util/text/Text'
+import {ProfileFeedHeader} from '#/screens/Profile/components/ProfileFeedHeader'
+import * as Layout from '#/components/Layout'
+
+type Props = NativeStackScreenProps
+export function ProfileFeedScreen(props: Props) {
+ const {rkey, name: handleOrDid} = props.route.params
+
+ const pal = usePalette('default')
+ const {_} = useLingui()
+ const navigation = useNavigation()
+
+ const uri = useMemo(
+ () => makeRecordUri(handleOrDid, 'app.bsky.feed.generator', rkey),
+ [rkey, handleOrDid],
+ )
+ const {error, data: resolvedUri} = useResolveUriQuery(uri)
+
+ const onPressBack = React.useCallback(() => {
+ if (navigation.canGoBack()) {
+ navigation.goBack()
+ } else {
+ navigation.navigate('Home')
+ }
+ }, [navigation])
+
+ if (error) {
+ return (
+
+
+
+
+ Could not load feed
+
+
+ {error.toString()}
+
+
+
+
+
+
+
+
+ )
+ }
+
+ return resolvedUri ? (
+
+
+
+ ) : (
+
+
+
+ )
+}
+
+function ProfileFeedScreenIntermediate({feedUri}: {feedUri: string}) {
+ const {data: preferences} = usePreferencesQuery()
+ const {data: info} = useFeedSourceInfoQuery({uri: feedUri})
+
+ if (!preferences || !info) {
+ return
+ }
+
+ return (
+
+ )
+}
+
+export function ProfileFeedScreenInner({
+ feedInfo,
+}: {
+ preferences: UsePreferencesQueryResponse
+ feedInfo: FeedSourceFeedInfo
+}) {
+ const {_} = useLingui()
+ const {hasSession} = useSession()
+ const {openComposer} = useComposerControls()
+ const isScreenFocused = useIsFocused()
+
+ useSetTitle(feedInfo?.displayName)
+
+ const feed = `feedgen|${feedInfo.uri}` as FeedDescriptor
+
+ const [hasNew, setHasNew] = React.useState(false)
+ const [isScrolledDown, setIsScrolledDown] = React.useState(false)
+ const queryClient = useQueryClient()
+ const feedFeedback = useFeedFeedback(feed, hasSession)
+ const scrollElRef = useAnimatedRef() as ListRef
+
+ const onScrollToTop = useCallback(() => {
+ scrollElRef.current?.scrollToOffset({
+ animated: isNative,
+ offset: 0, // -headerHeight,
+ })
+ truncateAndInvalidate(queryClient, FEED_RQKEY(feed))
+ setHasNew(false)
+ }, [scrollElRef, queryClient, feed, setHasNew])
+
+ React.useEffect(() => {
+ if (!isScreenFocused) {
+ return
+ }
+ return listenSoftReset(onScrollToTop)
+ }, [onScrollToTop, isScreenFocused])
+
+ const renderPostsEmpty = useCallback(() => {
+ return
+ }, [_])
+
+ return (
+ <>
+
+
+
+
+
+
+ {(isScrolledDown || hasNew) && (
+
+ )}
+
+ {hasSession && (
+ openComposer({})}
+ icon={
+
+ }
+ accessibilityRole="button"
+ accessibilityLabel={_(msg`New post`)}
+ accessibilityHint=""
+ />
+ )}
+ >
+ )
+}
+
+const styles = StyleSheet.create({
+ btn: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 6,
+ paddingVertical: 7,
+ paddingHorizontal: 14,
+ borderRadius: 50,
+ marginLeft: 6,
+ },
+ notFoundContainer: {
+ margin: 10,
+ paddingHorizontal: 18,
+ paddingVertical: 14,
+ borderRadius: 6,
+ },
+ aboutSectionContainer: {
+ paddingVertical: 4,
+ paddingHorizontal: 16,
+ gap: 12,
+ },
+})
diff --git a/src/screens/Profile/components/ProfileFeedHeader.tsx b/src/screens/Profile/components/ProfileFeedHeader.tsx
new file mode 100644
index 0000000000..afaa849819
--- /dev/null
+++ b/src/screens/Profile/components/ProfileFeedHeader.tsx
@@ -0,0 +1,539 @@
+import React from 'react'
+import {View} from 'react-native'
+import {useSafeAreaInsets} from 'react-native-safe-area-context'
+import {AtUri} from '@atproto/api'
+import {msg, Plural, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {useHaptics} from '#/lib/haptics'
+import {makeProfileLink} from '#/lib/routes/links'
+import {makeCustomFeedLink} from '#/lib/routes/links'
+import {shareUrl} from '#/lib/sharing'
+import {sanitizeHandle} from '#/lib/strings/handles'
+import {toShareUrl} from '#/lib/strings/url-helpers'
+import {logger} from '#/logger'
+import {FeedSourceFeedInfo} from '#/state/queries/feed'
+import {useLikeMutation, useUnlikeMutation} from '#/state/queries/like'
+import {
+ useAddSavedFeedsMutation,
+ usePreferencesQuery,
+ useRemoveFeedMutation,
+ useUpdateSavedFeedsMutation,
+} from '#/state/queries/preferences'
+import {useSession} from '#/state/session'
+import {formatCount} from '#/view/com/util/numeric/format'
+import * as Toast from '#/view/com/util/Toast'
+import {UserAvatar} from '#/view/com/util/UserAvatar'
+import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
+import {Button, ButtonIcon, ButtonText} from '#/components/Button'
+import * as Dialog from '#/components/Dialog'
+import {Divider} from '#/components/Divider'
+import {useRichText} from '#/components/hooks/useRichText'
+import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox'
+import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron'
+import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
+import {
+ Heart2_Filled_Stroke2_Corner0_Rounded as HeartFilled,
+ Heart2_Stroke2_Corner0_Rounded as Heart,
+} from '#/components/icons/Heart2'
+import {
+ Pin_Filled_Corner0_Rounded as PinFilled,
+ Pin_Stroke2_Corner0_Rounded as Pin,
+} from '#/components/icons/Pin'
+import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
+import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
+import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
+import * as Layout from '#/components/Layout'
+import {InlineLinkText} from '#/components/Link'
+import {Loader} from '#/components/Loader'
+import * as Menu from '#/components/Menu'
+import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
+import {RichText} from '#/components/RichText'
+import {Text} from '#/components/Typography'
+
+export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) {
+ const t = useTheme()
+ const {_, i18n} = useLingui()
+ const {hasSession} = useSession()
+ const {gtPhone, gtMobile} = useBreakpoints()
+ const {top} = useSafeAreaInsets()
+ const infoControl = Dialog.useDialogControl()
+ const playHaptic = useHaptics()
+
+ const {data: preferences} = usePreferencesQuery()
+
+ const [likeUri, setLikeUri] = React.useState(info.likeUri || '')
+ const isLiked = !!likeUri
+ const likeCount =
+ isLiked && likeUri ? (info.likeCount || 0) + 1 : info.likeCount || 0
+
+ const {mutateAsync: addSavedFeeds, isPending: isAddSavedFeedPending} =
+ useAddSavedFeedsMutation()
+ const {mutateAsync: removeFeed, isPending: isRemovePending} =
+ useRemoveFeedMutation()
+ const {mutateAsync: updateSavedFeeds, isPending: isUpdateFeedPending} =
+ useUpdateSavedFeedsMutation()
+
+ const isFeedStateChangePending =
+ isAddSavedFeedPending || isRemovePending || isUpdateFeedPending
+ const savedFeedConfig = preferences?.savedFeeds?.find(
+ f => f.value === info.uri,
+ )
+ const isSaved = Boolean(savedFeedConfig)
+ const isPinned = Boolean(savedFeedConfig?.pinned)
+
+ const onToggleSaved = React.useCallback(async () => {
+ try {
+ playHaptic()
+
+ if (savedFeedConfig) {
+ await removeFeed(savedFeedConfig)
+ Toast.show(_(msg`Removed from your feeds`))
+ } else {
+ await addSavedFeeds([
+ {
+ type: 'feed',
+ value: info.uri,
+ pinned: false,
+ },
+ ])
+ Toast.show(_(msg`Saved to your feeds`))
+ }
+ } catch (err) {
+ Toast.show(
+ _(
+ msg`There was an issue updating your feeds, please check your internet connection and try again.`,
+ ),
+ 'xmark',
+ )
+ logger.error('Failed to update feeds', {message: err})
+ }
+ }, [_, playHaptic, info, removeFeed, addSavedFeeds, savedFeedConfig])
+
+ const onTogglePinned = React.useCallback(async () => {
+ try {
+ playHaptic()
+
+ if (savedFeedConfig) {
+ const pinned = !savedFeedConfig.pinned
+ await updateSavedFeeds([
+ {
+ ...savedFeedConfig,
+ pinned,
+ },
+ ])
+
+ if (pinned) {
+ Toast.show(_(msg`Pinned ${info.displayName} to Home`))
+ } else {
+ Toast.show(_(msg`Un-pinned ${info.displayName} from Home`))
+ }
+ } else {
+ await addSavedFeeds([
+ {
+ type: 'feed',
+ value: info.uri,
+ pinned: true,
+ },
+ ])
+ Toast.show(_(msg`Pinned ${info.displayName} to Home`))
+ }
+ } catch (e) {
+ Toast.show(_(msg`There was an issue contacting the server`), 'xmark')
+ logger.error('Failed to toggle pinned feed', {message: e})
+ }
+ }, [playHaptic, info, _, savedFeedConfig, updateSavedFeeds, addSavedFeeds])
+
+ return (
+ <>
+
+
+
+
+
+
+
+ {hasSession && (
+
+ {isPinned ? (
+
+
+ {({props}) => {
+ return (
+
+ )
+ }}
+
+
+
+
+ {_(msg`Unpin from home`)}
+
+
+
+
+ {isSaved
+ ? _(msg`Remove from my feeds`)
+ : _(msg`Save to my feeds`)}
+
+
+
+
+
+ ) : (
+
+ )}
+
+ )}
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
+
+function DialogInner({
+ info,
+ likeUri,
+ setLikeUri,
+ likeCount,
+ isPinned,
+ onTogglePinned,
+ isFeedStateChangePending,
+}: {
+ info: FeedSourceFeedInfo
+ likeUri: string
+ setLikeUri: (uri: string) => void
+ likeCount: number
+ isPinned: boolean
+ onTogglePinned: () => void
+ isFeedStateChangePending: boolean
+}) {
+ const t = useTheme()
+ const {_} = useLingui()
+ const {hasSession} = useSession()
+ const playHaptic = useHaptics()
+ const control = Dialog.useDialogContext()
+ const reportDialogControl = useReportDialogControl()
+ const [rt, loading] = useRichText(info.description.text)
+ const {mutateAsync: likeFeed, isPending: isLikePending} = useLikeMutation()
+ const {mutateAsync: unlikeFeed, isPending: isUnlikePending} =
+ useUnlikeMutation()
+
+ const isLiked = !!likeUri
+ const feedRkey = React.useMemo(() => new AtUri(info.uri).rkey, [info.uri])
+
+ const onToggleLiked = React.useCallback(async () => {
+ try {
+ playHaptic()
+
+ if (isLiked && likeUri) {
+ await unlikeFeed({uri: likeUri})
+ setLikeUri('')
+ } else {
+ const res = await likeFeed({uri: info.uri, cid: info.cid})
+ setLikeUri(res.uri)
+ }
+ } catch (err) {
+ Toast.show(
+ _(
+ msg`There was an issue contacting the server, please check your internet connection and try again.`,
+ ),
+ 'xmark',
+ )
+ logger.error('Failed to toggle like', {message: err})
+ }
+ }, [playHaptic, isLiked, likeUri, unlikeFeed, setLikeUri, likeFeed, info, _])
+
+ const onPressShare = React.useCallback(() => {
+ playHaptic()
+ const url = toShareUrl(info.route.href)
+ shareUrl(url)
+ }, [info, playHaptic])
+
+ const onPressReport = React.useCallback(() => {
+ reportDialogControl.open()
+ }, [reportDialogControl])
+
+ return loading ? (
+
+ ) : (
+
+
+
+
+
+
+ {info.displayName}
+
+
+ By{' '}
+ control.close()}>
+ {sanitizeHandle(info.creatorHandle, '@')}
+
+
+
+
+
+
+
+
+
+
+ {typeof likeCount === 'number' && (
+ control.close()}>
+
+ Liked by
+
+
+ )}
+
+
+ {hasSession && (
+ <>
+
+
+
+
+
+
+
+
+
+
+ Something wrong? Let us know.
+
+
+
+
+
+
+
+ >
+ )}
+
+ )
+}
diff --git a/src/screens/Settings/NotificationSettings.tsx b/src/screens/Settings/NotificationSettings.tsx
index 1c77b31489..ebb230c2ca 100644
--- a/src/screens/Settings/NotificationSettings.tsx
+++ b/src/screens/Settings/NotificationSettings.tsx
@@ -18,7 +18,13 @@ type Props = NativeStackScreenProps
export function NotificationSettingsScreen({}: Props) {
const {_} = useLingui()
- const {data, isError: isQueryError, refetch} = useNotificationFeedQuery()
+ const {
+ data,
+ isError: isQueryError,
+ refetch,
+ } = useNotificationFeedQuery({
+ filter: 'all',
+ })
const serverPriority = data?.pages.at(0)?.priority
const {
diff --git a/src/state/queries/notifications/feed.ts b/src/state/queries/notifications/feed.ts
index 19a92fc3c0..72100a6245 100644
--- a/src/state/queries/notifications/feed.ts
+++ b/src/state/queries/notifications/feed.ts
@@ -52,25 +52,22 @@ const PAGE_SIZE = 30
type RQPageParam = string | undefined
const RQKEY_ROOT = 'notification-feed'
-export function RQKEY(priority?: false) {
- return [RQKEY_ROOT, priority]
+export function RQKEY(filter: 'all' | 'mentions') {
+ return [RQKEY_ROOT, filter]
}
-export function useNotificationFeedQuery(opts?: {
+export function useNotificationFeedQuery(opts: {
enabled?: boolean
- overridePriorityNotifications?: boolean
+ filter: 'all' | 'mentions'
}) {
const agent = useAgent()
const queryClient = useQueryClient()
const moderationOpts = useModerationOpts()
const unreads = useUnreadNotificationsApi()
- const enabled = opts?.enabled !== false
+ const enabled = opts.enabled !== false
+ const filter = opts.filter
const {uris: hiddenReplyUris} = useThreadgateHiddenReplyUris()
- // false: force showing all notifications
- // undefined: let the server decide
- const priority = opts?.overridePriorityNotifications ? false : undefined
-
const selectArgs = useMemo(() => {
return {
moderationOpts,
@@ -91,14 +88,23 @@ export function useNotificationFeedQuery(opts?: {
RQPageParam
>({
staleTime: STALE.INFINITY,
- queryKey: RQKEY(priority),
+ queryKey: RQKEY(filter),
async queryFn({pageParam}: {pageParam: RQPageParam}) {
let page
- if (!pageParam) {
+ if (filter === 'all' && !pageParam) {
// for the first page, we check the cached page held by the unread-checker first
page = unreads.getCachedUnreadPage()
}
if (!page) {
+ let reasons: string[] = []
+ if (filter === 'mentions') {
+ reasons = [
+ // Anything that's a post
+ 'mention',
+ 'reply',
+ 'quote',
+ ]
+ }
const {page: fetchedPage} = await fetchPage({
agent,
limit: PAGE_SIZE,
@@ -106,13 +112,13 @@ export function useNotificationFeedQuery(opts?: {
queryClient,
moderationOpts,
fetchAdditionalData: true,
- priority,
+ reasons,
})
page = fetchedPage
}
- // if the first page has an unread, mark all read
- if (!pageParam) {
+ if (filter === 'all' && !pageParam) {
+ // if the first page has an unread, mark all read
unreads.markAllRead()
}
diff --git a/src/state/queries/notifications/settings.ts b/src/state/queries/notifications/settings.ts
index a17fce8326..e552b65202 100644
--- a/src/state/queries/notifications/settings.ts
+++ b/src/state/queries/notifications/settings.ts
@@ -45,7 +45,8 @@ export function useNotificationSettingsMutation() {
},
onSettled: () => {
invalidateCachedUnreadPage()
- queryClient.invalidateQueries({queryKey: RQKEY_NOTIFS()})
+ queryClient.invalidateQueries({queryKey: RQKEY_NOTIFS('all')})
+ queryClient.invalidateQueries({queryKey: RQKEY_NOTIFS('mentions')})
},
})
}
@@ -54,7 +55,7 @@ function eagerlySetCachedPriority(
queryClient: ReturnType,
enabled: boolean,
) {
- queryClient.setQueryData(RQKEY_NOTIFS(), (old: any) => {
+ function updateData(old: any) {
if (!old) return old
return {
...old,
@@ -65,5 +66,7 @@ function eagerlySetCachedPriority(
}
}),
}
- })
+ }
+ queryClient.setQueryData(RQKEY_NOTIFS('all'), updateData)
+ queryClient.setQueryData(RQKEY_NOTIFS('mentions'), updateData)
}
diff --git a/src/state/queries/notifications/unread.tsx b/src/state/queries/notifications/unread.tsx
index 2ade04246f..ba2377a784 100644
--- a/src/state/queries/notifications/unread.tsx
+++ b/src/state/queries/notifications/unread.tsx
@@ -2,7 +2,7 @@
* A kind of companion API to ./feed.ts. See that file for more info.
*/
-import React from 'react'
+import React, {useRef} from 'react'
import {AppState} from 'react-native'
import {useQueryClient} from '@tanstack/react-query'
import EventEmitter from 'eventemitter3'
@@ -105,6 +105,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
}
}, [setNumUnread])
+ const isFetchingRef = useRef(false)
+
// create API
const api = React.useMemo(() => {
return {
@@ -138,6 +140,12 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
}
}
+ if (isFetchingRef.current) {
+ return
+ }
+ // Do not move this without ensuring it gets a symmetrical reset in the finally block.
+ isFetchingRef.current = true
+
// count
const {page, indexedAt: lastIndexed} = await fetchPage({
agent,
@@ -145,6 +153,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
limit: 40,
queryClient,
moderationOpts,
+ reasons: [],
// only fetch subjects when the page is going to be used
// in the notifications query, otherwise skip it
@@ -174,11 +183,14 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
// update & broadcast
setNumUnread(unreadCountStr)
if (invalidate) {
- truncateAndInvalidate(queryClient, RQKEY_NOTIFS())
+ truncateAndInvalidate(queryClient, RQKEY_NOTIFS('all'))
+ truncateAndInvalidate(queryClient, RQKEY_NOTIFS('mentions'))
}
broadcast.postMessage({event: unreadCountStr})
} catch (e) {
logger.warn('Failed to check unread notifications', {error: e})
+ } finally {
+ isFetchingRef.current = false
}
},
diff --git a/src/state/queries/notifications/util.ts b/src/state/queries/notifications/util.ts
index a251d170ec..0d72e9e929 100644
--- a/src/state/queries/notifications/util.ts
+++ b/src/state/queries/notifications/util.ts
@@ -31,6 +31,7 @@ export async function fetchPage({
queryClient,
moderationOpts,
fetchAdditionalData,
+ reasons,
}: {
agent: BskyAgent
cursor: string | undefined
@@ -38,7 +39,7 @@ export async function fetchPage({
queryClient: QueryClient
moderationOpts: ModerationOpts | undefined
fetchAdditionalData: boolean
- priority?: boolean
+ reasons: string[]
}): Promise<{
page: FeedPage
indexedAt: string | undefined
@@ -46,7 +47,7 @@ export async function fetchPage({
const res = await agent.listNotifications({
limit,
cursor,
- // priority,
+ reasons,
})
const indexedAt = res.data.notifications[0]?.indexedAt
diff --git a/src/state/queries/util.ts b/src/state/queries/util.ts
index 0d6a8e99ac..887c1df0ac 100644
--- a/src/state/queries/util.ts
+++ b/src/state/queries/util.ts
@@ -8,7 +8,7 @@ import {
} from '@atproto/api'
import {InfiniteData, QueryClient, QueryKey} from '@tanstack/react-query'
-export function truncateAndInvalidate(
+export async function truncateAndInvalidate(
queryClient: QueryClient,
queryKey: QueryKey,
) {
@@ -21,7 +21,7 @@ export function truncateAndInvalidate(
}
return data
})
- queryClient.invalidateQueries({queryKey})
+ return queryClient.invalidateQueries({queryKey})
}
// Given an AtUri, this function will check if the AtUri matches a
diff --git a/src/view/com/notifications/NotificationFeed.tsx b/src/view/com/notifications/NotificationFeed.tsx
index 5168933aeb..0b814e68dc 100644
--- a/src/view/com/notifications/NotificationFeed.tsx
+++ b/src/view/com/notifications/NotificationFeed.tsx
@@ -9,13 +9,11 @@ import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
-import {usePalette} from '#/lib/hooks/usePalette'
import {cleanError} from '#/lib/strings/errors'
import {s} from '#/lib/styles'
import {logger} from '#/logger'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useNotificationFeedQuery} from '#/state/queries/notifications/feed'
-import {useUnreadNotificationsApi} from '#/state/queries/notifications/unread'
import {EmptyState} from '#/view/com/util/EmptyState'
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
import {List, ListRef} from '#/view/com/util/List'
@@ -28,26 +26,26 @@ const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
const LOADING_ITEM = {_reactKey: '__loading__'}
export function NotificationFeed({
+ filter,
+ enabled,
scrollElRef,
onPressTryAgain,
onScrolledDownChange,
ListHeaderComponent,
- overridePriorityNotifications,
+ refreshNotifications,
}: {
+ filter: 'all' | 'mentions'
+ enabled: boolean
scrollElRef?: ListRef
onPressTryAgain?: () => void
onScrolledDownChange: (isScrolledDown: boolean) => void
ListHeaderComponent?: () => JSX.Element
- overridePriorityNotifications?: boolean
+ refreshNotifications: () => Promise
}) {
const initialNumToRender = useInitialNumToRender()
-
const [isPTRing, setIsPTRing] = React.useState(false)
- const pal = usePalette('default')
-
const {_} = useLingui()
const moderationOpts = useModerationOpts()
- const {checkUnread} = useUnreadNotificationsApi()
const {
data,
isFetching,
@@ -58,8 +56,8 @@ export function NotificationFeed({
isFetchingNextPage,
fetchNextPage,
} = useNotificationFeedQuery({
- enabled: !!moderationOpts,
- overridePriorityNotifications,
+ enabled: enabled && !!moderationOpts,
+ filter,
})
const isEmpty = !isFetching && !data?.pages[0]?.items.length
@@ -85,7 +83,7 @@ export function NotificationFeed({
const onRefresh = React.useCallback(async () => {
try {
setIsPTRing(true)
- await checkUnread({invalidate: true})
+ await refreshNotifications()
} catch (err) {
logger.error('Failed to refresh notifications feed', {
message: err,
@@ -93,7 +91,7 @@ export function NotificationFeed({
} finally {
setIsPTRing(false)
}
- }, [checkUnread, setIsPTRing])
+ }, [refreshNotifications, setIsPTRing])
const onEndReached = React.useCallback(async () => {
if (isFetching || !hasNextPage || isError) return
@@ -129,21 +127,18 @@ export function NotificationFeed({
/>
)
} else if (item === LOADING_ITEM) {
- return (
-
-
-
- )
+ return
}
return (
)
},
- [moderationOpts, _, onPressRetryLoadMore, pal.border],
+ [moderationOpts, _, onPressRetryLoadMore, filter],
)
const FeedFooter = React.useCallback(
diff --git a/src/view/com/notifications/NotificationFeedItem.tsx b/src/view/com/notifications/NotificationFeedItem.tsx
index 4902e66bc7..1267ce0894 100644
--- a/src/view/com/notifications/NotificationFeedItem.tsx
+++ b/src/view/com/notifications/NotificationFeedItem.tsx
@@ -79,10 +79,12 @@ interface Author {
let NotificationFeedItem = ({
item,
moderationOpts,
+ highlightUnread,
hideTopBorder,
}: {
item: FeedNotification
moderationOpts: ModerationOpts
+ highlightUnread: boolean
hideTopBorder?: boolean
}): React.ReactNode => {
const queryClient = useQueryClient()
@@ -151,6 +153,7 @@ let NotificationFeedItem = ({
if (!item.subject) {
return null
}
+ const isHighlighted = highlightUnread && !item.notification.isRead
return (
diff --git a/src/view/com/pager/Pager.tsx b/src/view/com/pager/Pager.tsx
index 2c0bbee520..b3f936ddc4 100644
--- a/src/view/com/pager/Pager.tsx
+++ b/src/view/com/pager/Pager.tsx
@@ -136,12 +136,14 @@ export const Pager = forwardRef>(
return (
- {renderTabBar({
- selectedPage,
- onSelect: onTabBarSelect,
- dragProgress,
- dragState,
- })}
+
+ {renderTabBar({
+ selectedPage,
+ onSelect: onTabBarSelect,
+ dragProgress,
+ dragState,
+ })}
+
([])
+ const textLayouts = useSharedValue<{width: number}[]>([])
const itemsLength = items.length
const scrollToOffsetJS = useCallback(
@@ -211,21 +212,40 @@ export function TabBar({
[layouts],
)
+ const onTextLayout = useCallback(
+ (i: number, layout: {width: number}) => {
+ 'worklet'
+ textLayouts.modify(ls => {
+ ls[i] = layout
+ return ls
+ })
+ },
+ [textLayouts],
+ )
+
const indicatorStyle = useAnimatedStyle(() => {
if (!_WORKLET) {
return {opacity: 0}
}
const layoutsValue = layouts.get()
+ const textLayoutsValue = textLayouts.get()
if (
layoutsValue.length !== itemsLength ||
- layoutsValue.some(l => l === undefined)
+ textLayoutsValue.length !== itemsLength
) {
return {
opacity: 0,
}
}
- if (layoutsValue.length === 1) {
- return {opacity: 1}
+ if (textLayoutsValue.length === 1) {
+ return {
+ opacity: 1,
+ transform: [
+ {
+ scaleX: textLayoutsValue[0].width / contentSize.get(),
+ },
+ ],
+ }
}
return {
opacity: 1,
@@ -240,10 +260,8 @@ export function TabBar({
{
scaleX: interpolate(
dragProgress.get(),
- layoutsValue.map((l, i) => i),
- layoutsValue.map(
- l => (l.width - ITEM_PADDING * 2) / contentSize.get(),
- ),
+ textLayoutsValue.map((l, i) => i),
+ textLayoutsValue.map(l => l.width / contentSize.get()),
),
},
],
@@ -287,7 +305,7 @@ export function TabBar({
onLayout={e => {
contentSize.set(e.nativeEvent.layout.width)
}}
- style={{flexDirection: 'row'}}>
+ style={{flexDirection: 'row', flexGrow: 1}}>
{items.map((item, i) => {
return (
)
})}
@@ -328,6 +347,7 @@ function TabBarItem({
item,
onPressItem,
onItemLayout,
+ onTextLayout,
}: {
index: number
testID: string | undefined
@@ -335,6 +355,7 @@ function TabBarItem({
item: string
onPressItem: (index: number) => void
onItemLayout: (index: number, layout: {x: number; width: number}) => void
+ onTextLayout: (index: number, layout: {width: number}) => void
}) {
const t = useTheme()
const style = useAnimatedStyle(() => {
@@ -358,8 +379,15 @@ function TabBarItem({
[index, onItemLayout],
)
+ const handleTextLayout = useCallback(
+ (e: LayoutChangeEvent) => {
+ runOnUI(onTextLayout)(index, e.nativeEvent.layout)
+ },
+ [index, onTextLayout],
+ )
+
return (
-
+
+ style={[styles.itemText, t.atoms.text, a.text_md, a.font_bold]}
+ onLayout={handleTextLayout}>
{item}
@@ -381,19 +410,28 @@ function TabBarItem({
const styles = StyleSheet.create({
contentContainer: {
+ flexGrow: 1,
backgroundColor: 'transparent',
paddingHorizontal: CONTENT_PADDING,
},
item: {
+ flexGrow: 1,
paddingTop: 10,
paddingHorizontal: ITEM_PADDING,
justifyContent: 'center',
},
itemInner: {
+ alignItems: 'center',
+ flexGrow: 1,
paddingBottom: 10,
borderBottomWidth: 3,
borderBottomColor: 'transparent',
},
+ itemText: {
+ lineHeight: 20,
+ minWidth: 45,
+ textAlign: 'center',
+ },
outerBottomBorder: {
position: 'absolute',
left: 0,
diff --git a/src/view/com/pager/TabBar.web.tsx b/src/view/com/pager/TabBar.web.tsx
index 789f88e753..f44e03368e 100644
--- a/src/view/com/pager/TabBar.web.tsx
+++ b/src/view/com/pager/TabBar.web.tsx
@@ -115,12 +115,14 @@ export function TabBar({
hoverStyle={t.atoms.bg_contrast_25}
onPress={() => onPressItem(i)}
accessibilityRole="tab">
-
+
)
}
- return
+ return (
+
+ )
}
function MockAccountCard({
diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx
index 70ab32db0d..82c68dde64 100644
--- a/src/view/screens/Notifications.tsx
+++ b/src/view/screens/Notifications.tsx
@@ -13,7 +13,7 @@ import {
} from '#/lib/routes/types'
import {s} from '#/lib/styles'
import {logger} from '#/logger'
-import {isNative, isWeb} from '#/platform/detection'
+import {isNative} from '#/platform/detection'
import {emitSoftReset, listenSoftReset} from '#/state/events'
import {RQKEY as NOTIFS_RQKEY} from '#/state/queries/notifications/feed'
import {
@@ -24,35 +24,173 @@ import {truncateAndInvalidate} from '#/state/queries/util'
import {useSetMinimalShellMode} from '#/state/shell'
import {useComposerControls} from '#/state/shell/composer'
import {NotificationFeed} from '#/view/com/notifications/NotificationFeed'
+import {Pager} from '#/view/com/pager/Pager'
+import {TabBar} from '#/view/com/pager/TabBar'
import {FAB} from '#/view/com/util/fab/FAB'
import {ListMethods} from '#/view/com/util/List'
import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn'
import {MainScrollProvider} from '#/view/com/util/MainScrollProvider'
-import {atoms as a, useBreakpoints, useTheme} from '#/alf'
-import {Button, ButtonIcon} from '#/components/Button'
+import {atoms as a} from '#/alf'
+import {web} from '#/alf'
+import {ButtonIcon} from '#/components/Button'
import {SettingsGear2_Stroke2_Corner0_Rounded as SettingsIcon} from '#/components/icons/SettingsGear2'
import * as Layout from '#/components/Layout'
import {Link} from '#/components/Link'
import {Loader} from '#/components/Loader'
+// We don't currently persist this across reloads since
+// you gotta visit All to clear the badge anyway.
+// But let's at least persist it during the sesssion.
+let lastActiveTab = 0
+
type Props = NativeStackScreenProps<
NotificationsTabNavigatorParams,
'Notifications'
>
-export function NotificationsScreen({route: {params}}: Props) {
- const t = useTheme()
- const {gtTablet} = useBreakpoints()
+export function NotificationsScreen({}: Props) {
+ const {_} = useLingui()
+ const {openComposer} = useComposerControls()
+ const unreadNotifs = useUnreadNotifications()
+ const hasNew = !!unreadNotifs
+ const {checkUnread: checkUnreadAll} = useUnreadNotificationsApi()
+ const [isLoadingAll, setIsLoadingAll] = React.useState(false)
+ const [isLoadingMentions, setIsLoadingMentions] = React.useState(false)
+ const initialActiveTab = lastActiveTab
+ const [activeTab, setActiveTab] = React.useState(initialActiveTab)
+ const isLoading = activeTab === 0 ? isLoadingAll : isLoadingMentions
+
+ const onPageSelected = React.useCallback(
+ (index: number) => {
+ setActiveTab(index)
+ lastActiveTab = index
+ },
+ [setActiveTab],
+ )
+
+ const queryClient = useQueryClient()
+ const checkUnreadMentions = React.useCallback(
+ async ({invalidate}: {invalidate: boolean}) => {
+ if (invalidate) {
+ return truncateAndInvalidate(queryClient, NOTIFS_RQKEY('mentions'))
+ } else {
+ // Background polling is not implemented for the mentions tab.
+ // Just ignore it.
+ }
+ },
+ [queryClient],
+ )
+
+ const sections = React.useMemo(() => {
+ return [
+ {
+ title: _(msg`All`),
+ component: (
+
+ ),
+ },
+ {
+ title: _(msg`Mentions`),
+ component: (
+
+ ),
+ },
+ ]
+ }, [
+ _,
+ hasNew,
+ checkUnreadAll,
+ checkUnreadMentions,
+ activeTab,
+ isLoadingAll,
+ isLoadingMentions,
+ ])
+
+ return (
+
+
+
+
+
+ Notifications
+
+
+
+
+
+
+
+
+ (
+
+ section.title)}
+ onPressSelected={() => emitSoftReset()}
+ />
+
+ )}
+ initialPage={initialActiveTab}>
+ {sections.map((section, i) => (
+ {section.component}
+ ))}
+
+ openComposer({})}
+ icon={}
+ accessibilityRole="button"
+ accessibilityLabel={_(msg`New post`)}
+ accessibilityHint=""
+ />
+
+ )
+}
+
+function NotificationsTab({
+ filter,
+ isActive,
+ isLoading,
+ hasNew,
+ checkUnread,
+ setIsLoadingLatest,
+}: {
+ filter: 'all' | 'mentions'
+ isActive: boolean
+ isLoading: boolean
+ hasNew: boolean
+ checkUnread: ({invalidate}: {invalidate: boolean}) => Promise
+ setIsLoadingLatest: (v: boolean) => void
+}) {
const {_} = useLingui()
const setMinimalShellMode = useSetMinimalShellMode()
const [isScrolledDown, setIsScrolledDown] = React.useState(false)
- const [isLoadingLatest, setIsLoadingLatest] = React.useState(false)
const scrollElRef = React.useRef(null)
const queryClient = useQueryClient()
- const unreadNotifs = useUnreadNotifications()
- const unreadApi = useUnreadNotificationsApi()
- const hasNew = !!unreadNotifs
const isScreenFocused = useIsFocused()
- const {openComposer} = useComposerControls()
+ const isFocusedAndActive = isScreenFocused && isActive
// event handlers
// =
@@ -65,16 +203,23 @@ export function NotificationsScreen({route: {params}}: Props) {
scrollToTop()
if (hasNew) {
// render what we have now
- truncateAndInvalidate(queryClient, NOTIFS_RQKEY())
- } else {
+ truncateAndInvalidate(queryClient, NOTIFS_RQKEY(filter))
+ } else if (!isLoading) {
// check with the server
setIsLoadingLatest(true)
- unreadApi
- .checkUnread({invalidate: true})
+ checkUnread({invalidate: true})
.catch(() => undefined)
.then(() => setIsLoadingLatest(false))
}
- }, [scrollToTop, queryClient, unreadApi, hasNew, setIsLoadingLatest])
+ }, [
+ scrollToTop,
+ queryClient,
+ checkUnread,
+ hasNew,
+ isLoading,
+ setIsLoadingLatest,
+ filter,
+ ])
const onFocusCheckLatest = useNonReactiveCallback(() => {
// on focus, check for latest, but only invalidate if the user
@@ -87,79 +232,36 @@ export function NotificationsScreen({route: {params}}: Props) {
// we're just going to look it up synchronously.
currentIsScrolledDown = window.scrollY > 200
}
- unreadApi.checkUnread({invalidate: !currentIsScrolledDown})
+ checkUnread({invalidate: !currentIsScrolledDown})
})
// on-visible setup
// =
useFocusEffect(
React.useCallback(() => {
- setMinimalShellMode(false)
- logger.debug('NotificationsScreen: Focus')
- onFocusCheckLatest()
- }, [setMinimalShellMode, onFocusCheckLatest]),
+ if (isFocusedAndActive) {
+ setMinimalShellMode(false)
+ logger.debug('NotificationsScreen: Focus')
+ onFocusCheckLatest()
+ }
+ }, [setMinimalShellMode, onFocusCheckLatest, isFocusedAndActive]),
)
React.useEffect(() => {
- if (!isScreenFocused) {
+ if (!isFocusedAndActive) {
return
}
return listenSoftReset(onPressLoadLatest)
- }, [onPressLoadLatest, isScreenFocused])
+ }, [onPressLoadLatest, isFocusedAndActive])
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
+ <>
checkUnread({invalidate: true})}
onScrolledDownChange={setIsScrolledDown}
scrollElRef={scrollElRef}
- overridePriorityNotifications={params?.show === 'all'}
/>
{(isScrolledDown || hasNew) && (
@@ -169,14 +271,6 @@ export function NotificationsScreen({route: {params}}: Props) {
showIndicator={hasNew}
/>
)}
- openComposer({})}
- icon={}
- accessibilityRole="button"
- accessibilityLabel={_(msg`New post`)}
- accessibilityHint=""
- />
-
+ >
)
}
diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx
deleted file mode 100644
index c3f98c067f..0000000000
--- a/src/view/screens/ProfileFeed.tsx
+++ /dev/null
@@ -1,621 +0,0 @@
-import React, {useCallback, useMemo} from 'react'
-import {Pressable, StyleSheet, View} from 'react-native'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {msg, Plural, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {useIsFocused, useNavigation} from '@react-navigation/native'
-import {NativeStackScreenProps} from '@react-navigation/native-stack'
-import {useQueryClient} from '@tanstack/react-query'
-
-import {HITSLOP_20} from '#/lib/constants'
-import {useHaptics} from '#/lib/haptics'
-import {usePalette} from '#/lib/hooks/usePalette'
-import {useSetTitle} from '#/lib/hooks/useSetTitle'
-import {ComposeIcon2} from '#/lib/icons'
-import {makeCustomFeedLink} from '#/lib/routes/links'
-import {CommonNavigatorParams} from '#/lib/routes/types'
-import {NavigationProp} from '#/lib/routes/types'
-import {shareUrl} from '#/lib/sharing'
-import {makeRecordUri} from '#/lib/strings/url-helpers'
-import {toShareUrl} from '#/lib/strings/url-helpers'
-import {s} from '#/lib/styles'
-import {logger} from '#/logger'
-import {isNative} from '#/platform/detection'
-import {listenSoftReset} from '#/state/events'
-import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
-import {FeedSourceFeedInfo, useFeedSourceInfoQuery} from '#/state/queries/feed'
-import {useLikeMutation, useUnlikeMutation} from '#/state/queries/like'
-import {FeedDescriptor} from '#/state/queries/post-feed'
-import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
-import {
- useAddSavedFeedsMutation,
- usePreferencesQuery,
- UsePreferencesQueryResponse,
- useRemoveFeedMutation,
- useUpdateSavedFeedsMutation,
-} from '#/state/queries/preferences'
-import {useResolveUriQuery} from '#/state/queries/resolve-uri'
-import {truncateAndInvalidate} from '#/state/queries/util'
-import {useSession} from '#/state/session'
-import {useComposerControls} from '#/state/shell/composer'
-import {PagerWithHeader} from '#/view/com/pager/PagerWithHeader'
-import {PostFeed} from '#/view/com/posts/PostFeed'
-import {ProfileSubpageHeader} from '#/view/com/profile/ProfileSubpageHeader'
-import {EmptyState} from '#/view/com/util/EmptyState'
-import {FAB} from '#/view/com/util/fab/FAB'
-import {Button} from '#/view/com/util/forms/Button'
-import {ListRef} from '#/view/com/util/List'
-import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn'
-import {LoadingScreen} from '#/view/com/util/LoadingScreen'
-import {Text} from '#/view/com/util/text/Text'
-import * as Toast from '#/view/com/util/Toast'
-import {atoms as a, useTheme} from '#/alf'
-import {Button as NewButton, ButtonText} from '#/components/Button'
-import {useRichText} from '#/components/hooks/useRichText'
-import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox'
-import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
-import {
- Heart2_Filled_Stroke2_Corner0_Rounded as HeartFilled,
- Heart2_Stroke2_Corner0_Rounded as HeartOutline,
-} from '#/components/icons/Heart2'
-import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
-import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
-import * as Layout from '#/components/Layout'
-import {InlineLinkText} from '#/components/Link'
-import * as Menu from '#/components/Menu'
-import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
-import {RichText} from '#/components/RichText'
-
-const SECTION_TITLES = ['Posts']
-
-interface SectionRef {
- scrollToTop: () => void
-}
-
-type Props = NativeStackScreenProps
-export function ProfileFeedScreen(props: Props) {
- const {rkey, name: handleOrDid} = props.route.params
-
- const pal = usePalette('default')
- const {_} = useLingui()
- const navigation = useNavigation()
-
- const uri = useMemo(
- () => makeRecordUri(handleOrDid, 'app.bsky.feed.generator', rkey),
- [rkey, handleOrDid],
- )
- const {error, data: resolvedUri} = useResolveUriQuery(uri)
-
- const onPressBack = React.useCallback(() => {
- if (navigation.canGoBack()) {
- navigation.goBack()
- } else {
- navigation.navigate('Home')
- }
- }, [navigation])
-
- if (error) {
- return (
-
-
-
-
- Could not load feed
-
-
- {error.toString()}
-
-
-
-
-
-
-
-
- )
- }
-
- return resolvedUri ? (
-
-
-
- ) : (
-
-
-
- )
-}
-
-function ProfileFeedScreenIntermediate({feedUri}: {feedUri: string}) {
- const {data: preferences} = usePreferencesQuery()
- const {data: info} = useFeedSourceInfoQuery({uri: feedUri})
-
- if (!preferences || !info) {
- return
- }
-
- return (
-
- )
-}
-
-export function ProfileFeedScreenInner({
- preferences,
- feedInfo,
-}: {
- preferences: UsePreferencesQueryResponse
- feedInfo: FeedSourceFeedInfo
-}) {
- const {_} = useLingui()
- const t = useTheme()
- const {hasSession, currentAccount} = useSession()
- const reportDialogControl = useReportDialogControl()
- const {openComposer} = useComposerControls()
- const playHaptic = useHaptics()
- const feedSectionRef = React.useRef(null)
- const isScreenFocused = useIsFocused()
-
- const {mutateAsync: addSavedFeeds, isPending: isAddSavedFeedPending} =
- useAddSavedFeedsMutation()
- const {mutateAsync: removeFeed, isPending: isRemovePending} =
- useRemoveFeedMutation()
- const {mutateAsync: updateSavedFeeds, isPending: isUpdateFeedPending} =
- useUpdateSavedFeedsMutation()
-
- const isPending =
- isAddSavedFeedPending || isRemovePending || isUpdateFeedPending
- const savedFeedConfig = preferences.savedFeeds.find(
- f => f.value === feedInfo.uri,
- )
- const isSaved = Boolean(savedFeedConfig)
- const isPinned = Boolean(savedFeedConfig?.pinned)
-
- useSetTitle(feedInfo?.displayName)
-
- // event handlers
- //
-
- const onToggleSaved = React.useCallback(async () => {
- try {
- playHaptic()
-
- if (savedFeedConfig) {
- await removeFeed(savedFeedConfig)
- Toast.show(_(msg`Removed from your feeds`))
- } else {
- await addSavedFeeds([
- {
- type: 'feed',
- value: feedInfo.uri,
- pinned: false,
- },
- ])
- Toast.show(_(msg`Saved to your feeds`))
- }
- } catch (err) {
- Toast.show(
- _(
- msg`There was an issue updating your feeds, please check your internet connection and try again.`,
- ),
- 'xmark',
- )
- logger.error('Failed to update feeds', {message: err})
- }
- }, [_, playHaptic, feedInfo, removeFeed, addSavedFeeds, savedFeedConfig])
-
- const onTogglePinned = React.useCallback(async () => {
- try {
- playHaptic()
-
- if (savedFeedConfig) {
- await updateSavedFeeds([
- {
- ...savedFeedConfig,
- pinned: !savedFeedConfig.pinned,
- },
- ])
- } else {
- await addSavedFeeds([
- {
- type: 'feed',
- value: feedInfo.uri,
- pinned: true,
- },
- ])
- }
- } catch (e) {
- Toast.show(_(msg`There was an issue contacting the server`), 'xmark')
- logger.error('Failed to toggle pinned feed', {message: e})
- }
- }, [
- playHaptic,
- feedInfo,
- _,
- savedFeedConfig,
- updateSavedFeeds,
- addSavedFeeds,
- ])
-
- const onPressShare = React.useCallback(() => {
- const url = toShareUrl(feedInfo.route.href)
- shareUrl(url)
- }, [feedInfo])
-
- const onPressReport = React.useCallback(() => {
- reportDialogControl.open()
- }, [reportDialogControl])
-
- const onCurrentPageSelected = React.useCallback(
- (index: number) => {
- if (index === 0) {
- feedSectionRef.current?.scrollToTop()
- }
- },
- [feedSectionRef],
- )
-
- const renderHeader = useCallback(() => {
- return (
- <>
-
-
- {feedInfo && hasSession && (
-
-
- {isPinned ? _(msg`Unpin`) : _(msg`Pin to Home`)}
-
-
- )}
-
-
- {({props, state}) => {
- return (
-
-
-
- )
- }}
-
-
-
-
- {hasSession && (
- <>
-
-
- {isSaved
- ? _(msg`Remove from my feeds`)
- : _(msg`Save to my feeds`)}
-
-
-
-
-
- {_(msg`Report feed`)}
-
-
- >
- )}
-
-
- {_(msg`Share feed`)}
-
-
-
-
-
-
-
-
- >
- )
- }, [
- _,
- hasSession,
- feedInfo,
- isPinned,
- onTogglePinned,
- onToggleSaved,
- currentAccount?.did,
- isSaved,
- onPressReport,
- onPressShare,
- t,
- isPending,
- ])
-
- return (
- <>
-
-
- {({headerHeight, scrollElRef, isFocused}) => (
-
- )}
-
- {hasSession && (
- openComposer({})}
- icon={
-
- }
- accessibilityRole="button"
- accessibilityLabel={_(msg`New post`)}
- accessibilityHint=""
- />
- )}
- >
- )
-}
-
-interface FeedSectionProps {
- feed: FeedDescriptor
- headerHeight: number
- scrollElRef: ListRef
- isFocused: boolean
-}
-const FeedSection = React.forwardRef(
- function FeedSectionImpl({feed, headerHeight, scrollElRef, isFocused}, ref) {
- const {_} = useLingui()
- const [hasNew, setHasNew] = React.useState(false)
- const [isScrolledDown, setIsScrolledDown] = React.useState(false)
- const queryClient = useQueryClient()
- const isScreenFocused = useIsFocused()
- const {hasSession} = useSession()
- const feedFeedback = useFeedFeedback(feed, hasSession)
-
- const onScrollToTop = useCallback(() => {
- scrollElRef.current?.scrollToOffset({
- animated: isNative,
- offset: -headerHeight,
- })
- truncateAndInvalidate(queryClient, FEED_RQKEY(feed))
- setHasNew(false)
- }, [scrollElRef, headerHeight, queryClient, feed, setHasNew])
-
- React.useImperativeHandle(ref, () => ({
- scrollToTop: onScrollToTop,
- }))
-
- React.useEffect(() => {
- if (!isScreenFocused) {
- return
- }
- return listenSoftReset(onScrollToTop)
- }, [onScrollToTop, isScreenFocused])
-
- const renderPostsEmpty = useCallback(() => {
- return
- }, [_])
-
- return (
-
-
-
-
- {(isScrolledDown || hasNew) && (
-
- )}
-
- )
- },
-)
-
-function AboutSection({
- feedOwnerDid,
- feedRkey,
- feedInfo,
-}: {
- feedOwnerDid: string
- feedRkey: string
- feedInfo: FeedSourceFeedInfo
-}) {
- const t = useTheme()
- const pal = usePalette('default')
- const {_} = useLingui()
- const [likeUri, setLikeUri] = React.useState(feedInfo.likeUri)
- const {hasSession} = useSession()
- const playHaptic = useHaptics()
- const {mutateAsync: likeFeed, isPending: isLikePending} = useLikeMutation()
- const {mutateAsync: unlikeFeed, isPending: isUnlikePending} =
- useUnlikeMutation()
- const [resolvedRT] = useRichText(feedInfo.description.text || '')
-
- const isLiked = !!likeUri
- const likeCount =
- isLiked && likeUri ? (feedInfo.likeCount || 0) + 1 : feedInfo.likeCount
-
- const onToggleLiked = React.useCallback(async () => {
- try {
- playHaptic()
-
- if (isLiked && likeUri) {
- await unlikeFeed({uri: likeUri})
- setLikeUri('')
- } else {
- const res = await likeFeed({uri: feedInfo.uri, cid: feedInfo.cid})
- setLikeUri(res.uri)
- }
- } catch (err) {
- Toast.show(
- _(
- msg`There was an issue contacting the server, please check your internet connection and try again.`,
- ),
- 'xmark',
- )
- logger.error('Failed to toggle like', {message: err})
- }
- }, [playHaptic, isLiked, likeUri, unlikeFeed, likeFeed, feedInfo, _])
-
- return (
-
-
- {feedInfo.description ? (
-
- ) : (
-
- No description
-
- )}
-
-
-
-
- {isLiked ? (
-
- ) : (
-
- )}
-
- {typeof likeCount === 'number' && (
-
-
- Liked by
-
-
- )}
-
-
- )
-}
-
-const styles = StyleSheet.create({
- btn: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 6,
- paddingVertical: 7,
- paddingHorizontal: 14,
- borderRadius: 50,
- marginLeft: 6,
- },
- notFoundContainer: {
- margin: 10,
- paddingHorizontal: 18,
- paddingVertical: 14,
- borderRadius: 6,
- },
- aboutSectionContainer: {
- paddingVertical: 4,
- paddingHorizontal: 16,
- gap: 12,
- },
-})
diff --git a/yarn.lock b/yarn.lock
index 16fe9579a4..9e2259e0d2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -20,21 +20,21 @@
"@jridgewell/gen-mapping" "^0.3.0"
"@jridgewell/trace-mapping" "^0.3.9"
-"@atproto-labs/fetch-node@0.1.3":
- version "0.1.3"
- resolved "https://registry.yarnpkg.com/@atproto-labs/fetch-node/-/fetch-node-0.1.3.tgz#2581bf4710a4f957c74c75d959961de3304b3595"
- integrity sha512-KX3ogPJt6dXNppWImQ9omfhrc8t73WrJaxHMphRAqQL8jXxKW5NBCTjSuwroBkJ1pj1aValBrc5NpdYu+H/9Qg==
+"@atproto-labs/fetch-node@0.1.4":
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/@atproto-labs/fetch-node/-/fetch-node-0.1.4.tgz#03859a39556eab936e2b3bec2d087585c6408cb3"
+ integrity sha512-hwYx0XpgIl2zydRy13DtWvywruuHk1EX+yCjqjgUIezUm8fi35ZN4QvR6INEm0MpN2MD/kQsImPbd8ZftzZ3zw==
dependencies:
- "@atproto-labs/fetch" "0.1.1"
+ "@atproto-labs/fetch" "0.1.2"
"@atproto-labs/pipe" "0.1.0"
ipaddr.js "^2.1.0"
psl "^1.9.0"
undici "^6.14.1"
-"@atproto-labs/fetch@0.1.1":
- version "0.1.1"
- resolved "https://registry.yarnpkg.com/@atproto-labs/fetch/-/fetch-0.1.1.tgz#10e7f8c06cf01a63f58e130b95d9ee0d4171902c"
- integrity sha512-X1zO1MDoJzEurbWXMAe1H8EZ995Xam/aXdxhGVrXmOMyPDuvBa1oxwh/kQNZRCKcMQUbiwkk+Jfq6ZkTuvGbww==
+"@atproto-labs/fetch@0.1.2":
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/@atproto-labs/fetch/-/fetch-0.1.2.tgz#e1b9354205fb76f106ae3e1c6b56e7865a39600f"
+ integrity sha512-7mQQIRtVenqtdBQKCqoLjyAhPS2aA56EGEjyz5zB3sramM3qkrvzyusr55GAzGDS0tvB6cy9cDEtSLmfK7LUnA==
dependencies:
"@atproto-labs/pipe" "0.1.0"
optionalDependencies:
@@ -58,28 +58,28 @@
resolved "https://registry.yarnpkg.com/@atproto-labs/simple-store/-/simple-store-0.1.1.tgz#e743a2722b5d8732166f0a72aca8bd10e9bff106"
integrity sha512-WKILW2b3QbAYKh+w5U2x6p5FqqLl0nAeLwGeDY+KjX01K4Dq3vQTR9b/qNp0jZm48CabPQVrqCv0PPU9LgRRRg==
-"@atproto/api@^0.13.18":
- version "0.13.18"
- resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.13.18.tgz#cc537cc3b4c8d03f258a373f4d893fea11a77cdd"
- integrity sha512-rrl5HhzGYWZ7fiC965TPBUOVItq9M4dxMb6qz8IvAVQliSkrJrKc7UD0QWL89QiiXaOBuX8w+4i5r4wrfBGddg==
+"@atproto/api@^0.13.20":
+ version "0.13.20"
+ resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.13.20.tgz#5140db303c3b0981958dfe6a5fa6d7d1cd7bb3cc"
+ integrity sha512-z/+CvG6BEttRHf856tKSe1AeUQNfrobRJldaHAthGmFk7O3wLZQyfcI9DUmBJQ9+4wAt0dZwvKWVGLZOV9eLHA==
dependencies:
"@atproto/common-web" "^0.3.1"
- "@atproto/lexicon" "^0.4.3"
+ "@atproto/lexicon" "^0.4.4"
"@atproto/syntax" "^0.3.1"
- "@atproto/xrpc" "^0.6.4"
+ "@atproto/xrpc" "^0.6.5"
await-lock "^2.2.2"
multiformats "^9.9.0"
tlds "^1.234.0"
zod "^3.23.8"
-"@atproto/aws@^0.2.9":
- version "0.2.9"
- resolved "https://registry.yarnpkg.com/@atproto/aws/-/aws-0.2.9.tgz#3539b281b725914b769451ee4afc62315dff1afc"
- integrity sha512-sc9aXUePcqItkJSOJJnGNVthVfAKjhn3zMDG+RRLzKUBye6Yutrlhpt1yxNZLHQiqIK5fy2Cuc4EX3p3jeWUYw==
+"@atproto/aws@^0.2.10":
+ version "0.2.10"
+ resolved "https://registry.yarnpkg.com/@atproto/aws/-/aws-0.2.10.tgz#e0b888fd50308cc24b7086cf3ec209587c13bbe4"
+ integrity sha512-zQElKk6wGTQo5aKdXtmx/dINjkVgbJU9+C/xOVTs+M88I8IrrBxPvo1dASLJcMtRb9VjXh5snLJeAjgyx6qC6Q==
dependencies:
- "@atproto/common" "^0.4.4"
+ "@atproto/common" "^0.4.5"
"@atproto/crypto" "^0.4.2"
- "@atproto/repo" "^0.5.5"
+ "@atproto/repo" "^0.6.0"
"@aws-sdk/client-cloudfront" "^3.261.0"
"@aws-sdk/client-kms" "^3.196.0"
"@aws-sdk/client-s3" "^3.224.0"
@@ -89,20 +89,20 @@
multiformats "^9.9.0"
uint8arrays "3.0.0"
-"@atproto/bsky@^0.0.96":
- version "0.0.96"
- resolved "https://registry.yarnpkg.com/@atproto/bsky/-/bsky-0.0.96.tgz#b89abf2828f57738357beb4efd05539667dd14b3"
- integrity sha512-Tk0ppiPMKdcnPU3x+uBAVRn92vroznhr2OlqinNSy/PZ39qWViRlKAhG3CLJsU2gjSHxsNfaIwulj7tPvKCmSw==
+"@atproto/bsky@^0.0.98":
+ version "0.0.98"
+ resolved "https://registry.yarnpkg.com/@atproto/bsky/-/bsky-0.0.98.tgz#4c4746e588568df1878647ae80cf4b963bc95924"
+ integrity sha512-Y+un2pD1W1H0s0IWdY6S4vLy8rgR8cpqThz9onn4wDppmGWvOBNXeD8AjNzIWC0iFlYcfR4rwCKSoccUXYzxNg==
dependencies:
- "@atproto/api" "^0.13.18"
- "@atproto/common" "^0.4.4"
+ "@atproto/api" "^0.13.20"
+ "@atproto/common" "^0.4.5"
"@atproto/crypto" "^0.4.2"
"@atproto/identity" "^0.4.3"
- "@atproto/lexicon" "^0.4.3"
- "@atproto/repo" "^0.5.5"
- "@atproto/sync" "^0.1.6"
+ "@atproto/lexicon" "^0.4.4"
+ "@atproto/repo" "^0.6.0"
+ "@atproto/sync" "^0.1.7"
"@atproto/syntax" "^0.3.1"
- "@atproto/xrpc-server" "^0.7.3"
+ "@atproto/xrpc-server" "^0.7.4"
"@bufbuild/protobuf" "^1.5.0"
"@connectrpc/connect" "^1.1.4"
"@connectrpc/connect-express" "^1.1.4"
@@ -129,12 +129,12 @@
typed-emitter "^2.1.0"
uint8arrays "3.0.0"
-"@atproto/bsync@^0.0.9":
- version "0.0.9"
- resolved "https://registry.yarnpkg.com/@atproto/bsync/-/bsync-0.0.9.tgz#7a6d58ef776404893d3c1139bdfe606fef483612"
- integrity sha512-N0+TnYOoJz4hTo6/h1jJKh6QzdbwkFuOQ1bdwugzST7ZkwMtjs5FX8o/uqgiD4gSHSqfQSRrew7+qYEHUT61Aw==
+"@atproto/bsync@^0.0.10":
+ version "0.0.10"
+ resolved "https://registry.yarnpkg.com/@atproto/bsync/-/bsync-0.0.10.tgz#fa16acfaf67112449b703778a20c785226c94189"
+ integrity sha512-qviPMyYade/sqhX/9X9eTT4KaQ+FLvOyz+140LCDk/0vbZUCZPuYSEXZDCQkL5nlEXzScsQ3iyVeoYCGvV5kYw==
dependencies:
- "@atproto/common" "^0.4.4"
+ "@atproto/common" "^0.4.5"
"@atproto/syntax" "^0.3.1"
"@bufbuild/protobuf" "^1.5.0"
"@connectrpc/connect" "^1.1.4"
@@ -175,10 +175,10 @@
pino "^8.6.1"
zod "^3.14.2"
-"@atproto/common@^0.4.4":
- version "0.4.4"
- resolved "https://registry.yarnpkg.com/@atproto/common/-/common-0.4.4.tgz#79096aef920f5ad7cda5c682d7ed7416d0581e1a"
- integrity sha512-58tMbn6A1Zu296s/l3uIj8z9d7IRHpZvLOfsFRikaQaYrzhJpL2aPY4uFQ8GJcxnsxeUnxBCrQz9we5jVVJI5Q==
+"@atproto/common@^0.4.5":
+ version "0.4.5"
+ resolved "https://registry.yarnpkg.com/@atproto/common/-/common-0.4.5.tgz#28fd176a9b5527c723828e725586bc0be9fa9516"
+ integrity sha512-LFAGqHcxCI5+b31Xgk+VQQtZU258iGPpHJzNeHVcdh6teIKZi4C2l6YV+m+3CEz+yYcfP7jjUmgqesx7l9Arsg==
dependencies:
"@atproto/common-web" "^0.3.1"
"@ipld/dag-cbor" "^7.0.3"
@@ -207,23 +207,23 @@
"@noble/hashes" "^1.3.1"
uint8arrays "3.0.0"
-"@atproto/dev-env@^0.3.64":
- version "0.3.64"
- resolved "https://registry.yarnpkg.com/@atproto/dev-env/-/dev-env-0.3.64.tgz#148537785b6a86b0a56d0988e63a1ff8ea7c84e9"
- integrity sha512-s7mdppgp2BS0uy5ASZwqJ3J8dez14pDGI9uqTGbsOYF/qTCbBGZKw/Vkqjci5bY1UaW+o6n787q63ECDtljM8A==
+"@atproto/dev-env@^0.3.67":
+ version "0.3.67"
+ resolved "https://registry.yarnpkg.com/@atproto/dev-env/-/dev-env-0.3.67.tgz#4f6a20f0aafa8125ed9ec715abceedd11580882e"
+ integrity sha512-7Ize4Y5vdjQjyrxTwjBPbkxKXQdE02KpE7AJLJt6Xpvowd2vbn8l8rDXfha+LtVi6t/613U4s+Slo5c1YD3x9A==
dependencies:
- "@atproto/api" "^0.13.18"
- "@atproto/bsky" "^0.0.96"
- "@atproto/bsync" "^0.0.9"
+ "@atproto/api" "^0.13.20"
+ "@atproto/bsky" "^0.0.98"
+ "@atproto/bsync" "^0.0.10"
"@atproto/common-web" "^0.3.1"
"@atproto/crypto" "^0.4.2"
"@atproto/identity" "^0.4.3"
- "@atproto/lexicon" "^0.4.3"
- "@atproto/ozone" "^0.1.57"
- "@atproto/pds" "^0.4.73"
- "@atproto/sync" "^0.1.6"
+ "@atproto/lexicon" "^0.4.4"
+ "@atproto/ozone" "^0.1.59"
+ "@atproto/pds" "^0.4.76"
+ "@atproto/sync" "^0.1.7"
"@atproto/syntax" "^0.3.1"
- "@atproto/xrpc-server" "^0.7.3"
+ "@atproto/xrpc-server" "^0.7.4"
"@did-plc/lib" "^0.0.1"
"@did-plc/server" "^0.0.1"
axios "^0.27.2"
@@ -258,10 +258,10 @@
multiformats "^9.9.0"
zod "^3.23.8"
-"@atproto/lexicon@^0.4.3":
- version "0.4.3"
- resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.4.3.tgz#d69f6bb363a6326df7766c48132bfa30e22622d9"
- integrity sha512-lFVZXe1S1pJP0dcxvJuHP3r/a+EAIBwwU7jUK+r8iLhIja+ml6NmYv8KeFHmIJATh03spEQ9s02duDmFVdCoXg==
+"@atproto/lexicon@^0.4.4":
+ version "0.4.4"
+ resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.4.4.tgz#0d97314bb57b693b76f2495fa5e02872469dd93a"
+ integrity sha512-QFEmr3rpj/RoAmfX9ALU/asBG/rsVtQZnw+9nOB1/AuIwoxXd+ZyndR6lVUc2+DL4GEjl6W2yvBru5xbQIZWyA==
dependencies:
"@atproto/common-web" "^0.3.1"
"@atproto/syntax" "^0.3.1"
@@ -269,20 +269,20 @@
multiformats "^9.9.0"
zod "^3.23.8"
-"@atproto/oauth-provider@^0.2.7":
- version "0.2.7"
- resolved "https://registry.yarnpkg.com/@atproto/oauth-provider/-/oauth-provider-0.2.7.tgz#38a211c197ee1ce4e92a5b59a92f2e15fcacee0b"
- integrity sha512-T/cEr7TGs36SqTW8JzLAt9EchumYY48zuI4rqoAepYT29eGpP37SxK+5X0+fQHOKJPKWUGlYocR9fDm4CdzAPQ==
+"@atproto/oauth-provider@^0.2.10":
+ version "0.2.10"
+ resolved "https://registry.yarnpkg.com/@atproto/oauth-provider/-/oauth-provider-0.2.10.tgz#f9820d7f82c33d3b74e81a75873f50e1e654b901"
+ integrity sha512-cF42lo0+Mj+Zq2RXwS2NxmobmtL7YL1vXlYcN6iKflZ8pQ5WvpR/cZKsKEZOT9cEBBTw5MARKTYxbr8CPDKlHg==
dependencies:
- "@atproto-labs/fetch" "0.1.1"
- "@atproto-labs/fetch-node" "0.1.3"
+ "@atproto-labs/fetch" "0.1.2"
+ "@atproto-labs/fetch-node" "0.1.4"
"@atproto-labs/pipe" "0.1.0"
"@atproto-labs/simple-store" "0.1.1"
"@atproto-labs/simple-store-memory" "0.1.1"
- "@atproto/common" "^0.4.4"
+ "@atproto/common" "^0.4.5"
"@atproto/jwk" "0.1.1"
"@atproto/jwk-jose" "0.1.2"
- "@atproto/oauth-types" "0.2.0"
+ "@atproto/oauth-types" "0.2.1"
"@hapi/accept" "^6.0.3"
"@hapi/bourne" "^3.0.0"
"@hapi/content" "^6.0.0"
@@ -294,27 +294,27 @@
psl "^1.9.0"
zod "^3.23.8"
-"@atproto/oauth-types@0.2.0":
- version "0.2.0"
- resolved "https://registry.yarnpkg.com/@atproto/oauth-types/-/oauth-types-0.2.0.tgz#28bc861b56cba093e6c52603cec1d3d38cd2a1e7"
- integrity sha512-v/4ht6eRh0yOu2iuuWujZdnJBamPKimdy8k0Xan8cVZ+a2i83UkhIIU+S/XUbbvJ4a64wLPZrS9IDd0K5XYYTQ==
+"@atproto/oauth-types@0.2.1":
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/@atproto/oauth-types/-/oauth-types-0.2.1.tgz#a7ace557cc91817fcde6195f023e4e1838e4aef6"
+ integrity sha512-hDisUXzcq5KU1HMuCYZ8Kcz7BePl7V11bFjjgZvND3mdSphiyBpJ8MCNn3QzAa6cXpFo0w9PDcYMAlCCRZHdVw==
dependencies:
"@atproto/jwk" "0.1.1"
zod "^3.23.8"
-"@atproto/ozone@^0.1.57":
- version "0.1.57"
- resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.1.57.tgz#141d66b213710575c7859d691586fd44c731f7ca"
- integrity sha512-P2YKeRFPbxKc2e2yftUoMTTcWYuFV0qU1/Nkd4GxuHnBnDJcbtMPglXd7kyLf0p8plCCFau/wZ8QdY8KSDLM9Q==
+"@atproto/ozone@^0.1.59":
+ version "0.1.59"
+ resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.1.59.tgz#219984a46617b0ac039f2f02767290eaa0b4cfc3"
+ integrity sha512-AD03Ocb3fZW+grxO/VwMld5iNdCLgbahFzku6xh1qEw0tLOBKp3GXSfepVd9XWu5fb1yPhGPd2JgjApV5hbJvw==
dependencies:
- "@atproto/api" "^0.13.18"
- "@atproto/common" "^0.4.4"
+ "@atproto/api" "^0.13.20"
+ "@atproto/common" "^0.4.5"
"@atproto/crypto" "^0.4.2"
"@atproto/identity" "^0.4.3"
- "@atproto/lexicon" "^0.4.3"
+ "@atproto/lexicon" "^0.4.4"
"@atproto/syntax" "^0.3.1"
- "@atproto/xrpc" "^0.6.4"
- "@atproto/xrpc-server" "^0.7.3"
+ "@atproto/xrpc" "^0.6.5"
+ "@atproto/xrpc-server" "^0.7.4"
"@did-plc/lib" "^0.0.1"
axios "^1.6.7"
compression "^1.7.4"
@@ -331,29 +331,30 @@
typed-emitter "^2.1.0"
uint8arrays "3.0.0"
-"@atproto/pds@^0.4.73":
- version "0.4.73"
- resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.4.73.tgz#49b7625d9b40a5a24be1cdd7cdb56faab9e25707"
- integrity sha512-fzrKlgKVF5JvTTmhfvofXT9Ok1KFTfAjCzTrLJivbOcqQSqBagNTuz5CiQxAAAo/JTlSxmnyr3e7OrlJdrph1w==
+"@atproto/pds@^0.4.76":
+ version "0.4.76"
+ resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.4.76.tgz#cd7b3f13359a7c31dc9362a5e4309419512c4102"
+ integrity sha512-+cFVpqlgpCS0BuGac5fCQPZUugpS1r7ghnSQLVdjnTnvQJCqLRA++BlJWYbGgRP6FJrumCY2jtuwG8t59Rjt8Q==
dependencies:
- "@atproto-labs/fetch-node" "0.1.3"
- "@atproto/api" "^0.13.18"
- "@atproto/aws" "^0.2.9"
- "@atproto/common" "^0.4.4"
+ "@atproto-labs/fetch-node" "0.1.4"
+ "@atproto/api" "^0.13.20"
+ "@atproto/aws" "^0.2.10"
+ "@atproto/common" "^0.4.5"
"@atproto/crypto" "^0.4.2"
"@atproto/identity" "^0.4.3"
- "@atproto/lexicon" "^0.4.3"
- "@atproto/oauth-provider" "^0.2.7"
- "@atproto/repo" "^0.5.5"
+ "@atproto/lexicon" "^0.4.4"
+ "@atproto/oauth-provider" "^0.2.10"
+ "@atproto/repo" "^0.6.0"
"@atproto/syntax" "^0.3.1"
- "@atproto/xrpc" "^0.6.4"
- "@atproto/xrpc-server" "^0.7.3"
+ "@atproto/xrpc" "^0.6.5"
+ "@atproto/xrpc-server" "^0.7.4"
"@did-plc/lib" "^0.0.4"
+ "@hapi/address" "^5.1.1"
better-sqlite3 "^10.0.0"
bytes "^3.1.2"
compression "^1.7.4"
cors "^2.8.5"
- disposable-email "^0.2.3"
+ disposable-email-domains-js "^1.5.0"
express "^4.17.2"
express-async-errors "^3.1.1"
file-type "^16.5.4"
@@ -376,49 +377,50 @@
undici "^6.19.8"
zod "^3.23.8"
-"@atproto/repo@^0.5.5":
- version "0.5.5"
- resolved "https://registry.yarnpkg.com/@atproto/repo/-/repo-0.5.5.tgz#73eaf1a0b35cfc4fc1c837f4e3ddeb6768d29c20"
- integrity sha512-Zu1tw42KBVyFzIh1XYSIvm8V+V9oEKWJR7NnHBgeSMwCc9QwM32jO7uqgvEjZYEXgdYKanGhv/YHLyxtZa5Ckg==
+"@atproto/repo@^0.6.0":
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/@atproto/repo/-/repo-0.6.0.tgz#29e698731e6df63636b0f7c91ce106a9de50ad19"
+ integrity sha512-6YGVhjiHKmqCW5Ce4oY49E3NCEfbvAGowJ5ETXX2sx2l4D2bOL7a2hn5zWqsPHYpWSLjrPfnj7PVpApK0kmL7A==
dependencies:
- "@atproto/common" "^0.4.4"
+ "@atproto/common" "^0.4.5"
"@atproto/common-web" "^0.3.1"
"@atproto/crypto" "^0.4.2"
- "@atproto/lexicon" "^0.4.3"
+ "@atproto/lexicon" "^0.4.4"
"@ipld/car" "^3.2.3"
"@ipld/dag-cbor" "^7.0.0"
multiformats "^9.9.0"
uint8arrays "3.0.0"
zod "^3.23.8"
-"@atproto/sync@^0.1.6":
- version "0.1.6"
- resolved "https://registry.yarnpkg.com/@atproto/sync/-/sync-0.1.6.tgz#fb3e61147c05caf2c3d1cd597ff94fef68abbc02"
- integrity sha512-9lqe6E6fIns28TJyQufLCVefMxmK3bvEfQBhmXJBGZMHuKlH8+F5P9DfnHv6vs6ygfmHIUIjYDWqJu/rpt8pzw==
+"@atproto/sync@^0.1.7":
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/@atproto/sync/-/sync-0.1.7.tgz#c7f78d99bb40eacf93ca13fdd04134a0985bf421"
+ integrity sha512-liJH2EsD4AbWA8G0oRDURgbHW6Uq4NnM2rNfbrTlqgtj0kyGRY3FcVEyqeRcaQYfCuscChIg5DQKHqY421/7Mw==
dependencies:
- "@atproto/common" "^0.4.4"
+ "@atproto/common" "^0.4.5"
"@atproto/identity" "^0.4.3"
- "@atproto/lexicon" "^0.4.3"
- "@atproto/repo" "^0.5.5"
+ "@atproto/lexicon" "^0.4.4"
+ "@atproto/repo" "^0.6.0"
"@atproto/syntax" "^0.3.1"
- "@atproto/xrpc-server" "^0.7.3"
+ "@atproto/xrpc-server" "^0.7.4"
multiformats "^9.9.0"
p-queue "^6.6.2"
+ ws "^8.12.0"
"@atproto/syntax@^0.3.1":
version "0.3.1"
resolved "https://registry.yarnpkg.com/@atproto/syntax/-/syntax-0.3.1.tgz#4346418728f9643d783d2ffcf7c77e132e1f53d4"
integrity sha512-fzW0Mg1QUOVCWUD3RgEsDt6d1OZ6DdFmbKcDdbzUfh0t4rhtRAC05KbZYmxuMPWDAiJ4BbbQ5dkAc/mNypMXkw==
-"@atproto/xrpc-server@^0.7.3":
- version "0.7.3"
- resolved "https://registry.yarnpkg.com/@atproto/xrpc-server/-/xrpc-server-0.7.3.tgz#d09b36d00edb7aacca48675d1ebb7fa796fa11bd"
- integrity sha512-x0qegkN6snrbXJO3v9h2kuh9e90g6ZZkDXv3COiraGS3yRTzIm6i4bMvDSfCI50+0xCNtPKOkpn8taRoRgkyiw==
+"@atproto/xrpc-server@^0.7.4":
+ version "0.7.4"
+ resolved "https://registry.yarnpkg.com/@atproto/xrpc-server/-/xrpc-server-0.7.4.tgz#dfac8f7276c1c971a35eaba627eb6372088441c3"
+ integrity sha512-MrAwxfJBQm/kCol3D8qc+vpQzBMzLqvtUbauSSfVVJ10PlGtxg4LlXqcjkAuhrjyrqp3dQH9LHuhDpgVQK+G3w==
dependencies:
- "@atproto/common" "^0.4.4"
+ "@atproto/common" "^0.4.5"
"@atproto/crypto" "^0.4.2"
- "@atproto/lexicon" "^0.4.3"
- "@atproto/xrpc" "^0.6.4"
+ "@atproto/lexicon" "^0.4.4"
+ "@atproto/xrpc" "^0.6.5"
cbor-x "^1.5.1"
express "^4.17.2"
http-errors "^2.0.0"
@@ -428,12 +430,12 @@
ws "^8.12.0"
zod "^3.23.8"
-"@atproto/xrpc@^0.6.4":
- version "0.6.4"
- resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.6.4.tgz#4cf59774f7c72e5bc821bc5f1d57f0a6ae2014db"
- integrity sha512-9ZAJ8nsXTqC4XFyS0E1Wlg7bAvonhXQNQ3Ocs1L1LIwFLXvsw/4fNpIHXxvXvqTCVeyHLbImOnE9UiO1c/qIYA==
+"@atproto/xrpc@^0.6.5":
+ version "0.6.5"
+ resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.6.5.tgz#8b180fc5f6b8374fd00c41b9e4cd7b24ead48e6b"
+ integrity sha512-t6u8iPEVbWge5RhzKZDahSzNDYIAxUtop6Q/X/apAZY1rgreVU0/1sSvvRoRFH19d3UIKjYdLuwFqMi9w8nY3Q==
dependencies:
- "@atproto/lexicon" "^0.4.3"
+ "@atproto/lexicon" "^0.4.4"
zod "^3.23.8"
"@aws-crypto/crc32@3.0.0":
@@ -4378,6 +4380,13 @@
"@hapi/boom" "^10.0.1"
"@hapi/hoek" "^11.0.2"
+"@hapi/address@^5.1.1":
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/@hapi/address/-/address-5.1.1.tgz#e9925fc1b65f5cc3fbea821f2b980e4652e84cb6"
+ integrity sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==
+ dependencies:
+ "@hapi/hoek" "^11.0.2"
+
"@hapi/boom@^10.0.0", "@hapi/boom@^10.0.1":
version "10.0.1"
resolved "https://registry.yarnpkg.com/@hapi/boom/-/boom-10.0.1.tgz#ebb14688275ae150aa6af788dbe482e6a6062685"
@@ -9593,10 +9602,10 @@ dir-glob@^3.0.1:
dependencies:
path-type "^4.0.0"
-disposable-email@^0.2.3:
- version "0.2.3"
- resolved "https://registry.yarnpkg.com/disposable-email/-/disposable-email-0.2.3.tgz#a21a49717f6034a8ff777dc8eae3b4d994a7b988"
- integrity sha512-gkBQQ5Res431ZXqLlAafrXHizG7/1FWmi8U2RTtriD78Vc10HhBUvdJun3R4eSF0KRIQQJs+wHlxjkED/Hr1EQ==
+disposable-email-domains-js@^1.5.0:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/disposable-email-domains-js/-/disposable-email-domains-js-1.7.0.tgz#2bf859bccf7a2eb697025577e18f0434409713ec"
+ integrity sha512-qcIJcnXjDvH8EEt0tyAesk1sZVGU5ZFtW6Wys2wKCAcbUf5nJYfwZfT7Z0PVA/LBMlqd/Xgk9dXN2Q3fx7NFAg==
dns-equal@^1.0.0:
version "1.0.0"