diff --git a/assets/icons/arrowBottom_stroke2_corner0_rounded.svg b/assets/icons/arrowBottom_stroke2_corner0_rounded.svg
new file mode 100644
index 0000000000..5f4a11e09a
--- /dev/null
+++ b/assets/icons/arrowBottom_stroke2_corner0_rounded.svg
@@ -0,0 +1 @@
+
diff --git a/package.json b/package.json
index 5f9a898fc9..e08aa9d29c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "bsky.app",
- "version": "1.86.0",
+ "version": "1.87.0",
"private": true,
"engines": {
"node": ">=18"
diff --git a/patches/expo-haptics+12.8.1.patch b/patches/expo-haptics+13.0.1.patch
similarity index 96%
rename from patches/expo-haptics+12.8.1.patch
rename to patches/expo-haptics+13.0.1.patch
index a95b56f3be..9c7b9a6663 100644
--- a/patches/expo-haptics+12.8.1.patch
+++ b/patches/expo-haptics+13.0.1.patch
@@ -1,9 +1,9 @@
diff --git a/node_modules/expo-haptics/android/src/main/java/expo/modules/haptics/HapticsModule.kt b/node_modules/expo-haptics/android/src/main/java/expo/modules/haptics/HapticsModule.kt
-index 26c52af..b949a4c 100644
+index 1520465..6ea988a 100644
--- a/node_modules/expo-haptics/android/src/main/java/expo/modules/haptics/HapticsModule.kt
+++ b/node_modules/expo-haptics/android/src/main/java/expo/modules/haptics/HapticsModule.kt
@@ -42,7 +42,7 @@ class HapticsModule : Module() {
-
+
private fun vibrate(type: HapticsVibrationType) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- vibrator.vibrate(VibrationEffect.createWaveform(type.timings, type.amplitudes, -1))
diff --git a/patches/expo-updates+0.25.11.patch b/patches/expo-updates+0.25.14.patch
similarity index 96%
rename from patches/expo-updates+0.25.11.patch
rename to patches/expo-updates+0.25.14.patch
index 5f9eceef48..6fc4fc5fcf 100644
--- a/patches/expo-updates+0.25.11.patch
+++ b/patches/expo-updates+0.25.14.patch
@@ -1,11 +1,11 @@
diff --git a/node_modules/expo-updates/ios/EXUpdates/Update/ExpoUpdatesUpdate.swift b/node_modules/expo-updates/ios/EXUpdates/Update/ExpoUpdatesUpdate.swift
-index b85291e..07a5d3c 100644
+index b85291e..546709d 100644
--- a/node_modules/expo-updates/ios/EXUpdates/Update/ExpoUpdatesUpdate.swift
+++ b/node_modules/expo-updates/ios/EXUpdates/Update/ExpoUpdatesUpdate.swift
@@ -78,13 +78,20 @@ public final class ExpoUpdatesUpdate: Update {
status = UpdateStatus.StatusPending
}
-
+
+ // Instead of relying on various hacks to get the correct format for the specific
+ // platform on the backend, we can just add this little patch..
+ let dateFormatter = DateFormatter()
diff --git a/src/components/FeedCard.tsx b/src/components/FeedCard.tsx
new file mode 100644
index 0000000000..2745ed7c9f
--- /dev/null
+++ b/src/components/FeedCard.tsx
@@ -0,0 +1,198 @@
+import React from 'react'
+import {GestureResponderEvent, View} from 'react-native'
+import {AppBskyActorDefs, AppBskyFeedDefs, AtUri} from '@atproto/api'
+import {msg, plural, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {logger} from '#/logger'
+import {
+ useAddSavedFeedsMutation,
+ usePreferencesQuery,
+ useRemoveFeedMutation,
+} from '#/state/queries/preferences'
+import {sanitizeHandle} from 'lib/strings/handles'
+import {UserAvatar} from '#/view/com/util/UserAvatar'
+import * as Toast from 'view/com/util/Toast'
+import {useTheme} from '#/alf'
+import {atoms as a} from '#/alf'
+import {Button, ButtonIcon} from '#/components/Button'
+import {useRichText} from '#/components/hooks/useRichText'
+import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
+import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
+import {Link as InternalLink} from '#/components/Link'
+import {Loader} from '#/components/Loader'
+import * as Prompt from '#/components/Prompt'
+import {RichText} from '#/components/RichText'
+import {Text} from '#/components/Typography'
+
+export function Default({feed}: {feed: AppBskyFeedDefs.GeneratorView}) {
+ return (
+
+
+
+
+
+
+
+ )
+}
+
+export function Link({
+ children,
+ feed,
+}: {
+ children: React.ReactElement
+ feed: AppBskyFeedDefs.GeneratorView
+}) {
+ const href = React.useMemo(() => {
+ const urip = new AtUri(feed.uri)
+ const handleOrDid = feed.creator.handle || feed.creator.did
+ return `/profile/${handleOrDid}/feed/${urip.rkey}`
+ }, [feed])
+ return {children}
+}
+
+export function Outer({children}: {children: React.ReactNode}) {
+ return {children}
+}
+
+export function Header({children}: {children: React.ReactNode}) {
+ return {children}
+}
+
+export function Avatar({src}: {src: string | undefined}) {
+ return
+}
+
+export function TitleAndByline({
+ title,
+ creator,
+}: {
+ title: string
+ creator: AppBskyActorDefs.ProfileViewBasic
+}) {
+ const t = useTheme()
+
+ return (
+
+
+ {title}
+
+
+ Feed by {sanitizeHandle(creator.handle, '@')}
+
+
+ )
+}
+
+export function Description({description}: {description?: string}) {
+ const [rt, isResolving] = useRichText(description || '')
+ if (!description) return null
+ return isResolving ? (
+
+ ) : (
+
+ )
+}
+
+export function Likes({count}: {count: number}) {
+ const t = useTheme()
+ return (
+
+ {plural(count || 0, {
+ one: 'Liked by # user',
+ other: 'Liked by # users',
+ })}
+
+ )
+}
+
+export function Action({uri, pin}: {uri: string; pin?: boolean}) {
+ const {_} = useLingui()
+ const {data: preferences} = usePreferencesQuery()
+ const {isPending: isAddSavedFeedPending, mutateAsync: saveFeeds} =
+ useAddSavedFeedsMutation()
+ const {isPending: isRemovePending, mutateAsync: removeFeed} =
+ useRemoveFeedMutation()
+ const savedFeedConfig = React.useMemo(() => {
+ return preferences?.savedFeeds?.find(
+ feed => feed.type === 'feed' && feed.value === uri,
+ )
+ }, [preferences?.savedFeeds, uri])
+ const removePromptControl = Prompt.usePromptControl()
+ const isPending = isAddSavedFeedPending || isRemovePending
+
+ const toggleSave = React.useCallback(
+ async (e: GestureResponderEvent) => {
+ e.preventDefault()
+ e.stopPropagation()
+
+ try {
+ if (savedFeedConfig) {
+ await removeFeed(savedFeedConfig)
+ } else {
+ await saveFeeds([
+ {
+ type: 'feed',
+ value: uri,
+ pinned: pin || false,
+ },
+ ])
+ }
+ Toast.show(_(msg`Feeds updated!`))
+ } catch (e: any) {
+ logger.error(e, {context: `FeedCard: failed to update feeds`, pin})
+ Toast.show(_(msg`Failed to update feeds`))
+ }
+ },
+ [_, pin, saveFeeds, removeFeed, uri, savedFeedConfig],
+ )
+
+ const onPrompRemoveFeed = React.useCallback(
+ async (e: GestureResponderEvent) => {
+ e.preventDefault()
+ e.stopPropagation()
+
+ removePromptControl.open()
+ },
+ [removePromptControl],
+ )
+
+ return (
+ <>
+
+
+
+ >
+ )
+}
diff --git a/src/components/KnownFollowers.tsx b/src/components/KnownFollowers.tsx
index a8bdb763d7..63f61ce856 100644
--- a/src/components/KnownFollowers.tsx
+++ b/src/components/KnownFollowers.tsx
@@ -100,7 +100,7 @@ function KnownFollowersInner({
moderation,
}
})
- const count = cachedKnownFollowers.count - Math.min(slice.length, 2)
+ const count = cachedKnownFollowers.count
return (
void
+ onPress: ButtonProps['onPress']
color?: ButtonColor
/**
* Optional i18n string. If undefined, it will default to "Confirm".
@@ -147,9 +147,12 @@ export function Action({
const {_} = useLingui()
const {gtMobile} = useBreakpoints()
const {close} = Dialog.useDialogContext()
- const handleOnPress = React.useCallback(() => {
- close(onPress)
- }, [close, onPress])
+ const handleOnPress = React.useCallback(
+ (e: GestureResponderEvent) => {
+ close(() => onPress?.(e))
+ },
+ [close, onPress],
+ )
return (