diff --git a/__e2e__/flows/curate-lists.yml b/__e2e__/flows/curate-lists.yml
index e497898b27..a37bc07778 100644
--- a/__e2e__/flows/curate-lists.yml
+++ b/__e2e__/flows/curate-lists.yml
@@ -132,7 +132,8 @@ appId: xyz.blueskyweb.app
id: "feedItem-by-bob.test"
- tapOn:
id: "e2eGotoFeeds"
-- tapOn: "Good Ppl"
+- tapOn:
+ id: "saved-feed-Good Ppl"
- assertVisible:
id: "feedItem-by-bob.test"
- tapOn:
@@ -168,7 +169,7 @@ appId: xyz.blueskyweb.app
id: "profilePager-selector"
direction: LEFT
- tapOn:
- id: "profilePager-selector-5"
+ id: "profilePager-selector-6"
- tapOn: "Good Ppl"
- tapOn:
diff --git a/assets/icons/play_filled_corner2_rounded.svg b/assets/icons/play_filled_corner2_rounded.svg
new file mode 100644
index 0000000000..e25e8d4628
--- /dev/null
+++ b/assets/icons/play_filled_corner2_rounded.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/play_stroke2_corner2_rounded.svg b/assets/icons/play_stroke2_corner2_rounded.svg
new file mode 100644
index 0000000000..54bba91fb9
--- /dev/null
+++ b/assets/icons/play_stroke2_corner2_rounded.svg
@@ -0,0 +1 @@
+
diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go
index d7e41a4ca8..61a524a70b 100644
--- a/bskyweb/cmd/bskyweb/server.go
+++ b/bskyweb/cmd/bskyweb/server.go
@@ -197,6 +197,7 @@ func serve(cctx *cli.Context) error {
e.GET("/search", server.WebGeneric)
e.GET("/feeds", server.WebGeneric)
e.GET("/notifications", server.WebGeneric)
+ e.GET("/notifications/settings", server.WebGeneric)
e.GET("/lists", server.WebGeneric)
e.GET("/moderation", server.WebGeneric)
e.GET("/moderation/modlists", server.WebGeneric)
diff --git a/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/referrer/ExpoBlueskyReferrerModule.kt b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/referrer/ExpoBlueskyReferrerModule.kt
index bac555233e..52b4daa303 100644
--- a/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/referrer/ExpoBlueskyReferrerModule.kt
+++ b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/referrer/ExpoBlueskyReferrerModule.kt
@@ -23,7 +23,7 @@ class ExpoBlueskyReferrerModule : Module() {
activityReferrer = appContext.currentActivity?.referrer
}
- AsyncFunction("getReferrerInfoAsync") {
+ Function("getReferrerInfo") {
val intentReferrer =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent?.getParcelableExtra(Intent.EXTRA_REFERRER, Uri::class.java)
@@ -40,7 +40,7 @@ class ExpoBlueskyReferrerModule : Module() {
"hostname" to intentReferrer.host,
)
intent = null
- return@AsyncFunction res
+ return@Function res
}
// In all other cases, we'll just record the app that sent the intent.
@@ -52,10 +52,10 @@ class ExpoBlueskyReferrerModule : Module() {
"hostname" to (activityReferrer?.host ?: ""),
)
activityReferrer = null
- return@AsyncFunction res
+ return@Function res
}
- return@AsyncFunction null
+ return@Function null
}
AsyncFunction("getGooglePlayReferrerInfoAsync") { promise: Promise ->
diff --git a/modules/expo-bluesky-swiss-army/src/Referrer/index.android.ts b/modules/expo-bluesky-swiss-army/src/Referrer/index.android.ts
index ec2bcb57db..795c6146c9 100644
--- a/modules/expo-bluesky-swiss-army/src/Referrer/index.android.ts
+++ b/modules/expo-bluesky-swiss-army/src/Referrer/index.android.ts
@@ -8,6 +8,6 @@ export function getGooglePlayReferrerInfoAsync(): Promise {
- return NativeModule.getReferrerInfoAsync()
+export function getReferrerInfo(): Promise {
+ return NativeModule.getReferrerInfo()
}
diff --git a/modules/expo-bluesky-swiss-army/src/Referrer/index.ios.ts b/modules/expo-bluesky-swiss-army/src/Referrer/index.ios.ts
index 2bf1497a0a..e76893d308 100644
--- a/modules/expo-bluesky-swiss-army/src/Referrer/index.ios.ts
+++ b/modules/expo-bluesky-swiss-army/src/Referrer/index.ios.ts
@@ -6,7 +6,7 @@ export function getGooglePlayReferrerInfoAsync(): Promise {
+export function getReferrerInfo(): ReferrerInfo | null {
const referrer = SharedPrefs.getString('referrer')
if (referrer) {
SharedPrefs.removeValue('referrer')
@@ -19,7 +19,7 @@ export function getReferrerInfoAsync(): Promise {
} catch (e) {
return {
referrer,
- hostname: undefined,
+ hostname: referrer,
}
}
}
diff --git a/modules/expo-bluesky-swiss-army/src/Referrer/index.ts b/modules/expo-bluesky-swiss-army/src/Referrer/index.ts
index a60f7b6db8..ac594402b8 100644
--- a/modules/expo-bluesky-swiss-army/src/Referrer/index.ts
+++ b/modules/expo-bluesky-swiss-army/src/Referrer/index.ts
@@ -5,6 +5,6 @@ export function getGooglePlayReferrerInfoAsync(): Promise {
+export function getReferrerInfo(): ReferrerInfo | null {
throw new NotImplementedError()
}
diff --git a/modules/expo-bluesky-swiss-army/src/Referrer/index.web.ts b/modules/expo-bluesky-swiss-army/src/Referrer/index.web.ts
index c092a67e73..cfd42642d9 100644
--- a/modules/expo-bluesky-swiss-army/src/Referrer/index.web.ts
+++ b/modules/expo-bluesky-swiss-army/src/Referrer/index.web.ts
@@ -7,7 +7,7 @@ export function getGooglePlayReferrerInfoAsync(): Promise {
+export function getReferrerInfo(): ReferrerInfo | null {
if (
Platform.OS === 'web' &&
// for ssr
diff --git a/package.json b/package.json
index b56dd9e39b..91b427ae91 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "bsky.app",
- "version": "1.89.0",
+ "version": "1.90.0",
"private": true,
"engines": {
"node": ">=18"
@@ -52,7 +52,7 @@
"open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web"
},
"dependencies": {
- "@atproto/api": "^0.12.23",
+ "@atproto/api": "0.12.25",
"@bam.tech/react-native-image-resizer": "^3.0.4",
"@braintree/sanitize-url": "^6.0.2",
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
@@ -143,6 +143,7 @@
"expo-web-browser": "~13.0.3",
"fast-text-encoding": "^1.0.6",
"history": "^5.3.0",
+ "hls.js": "^1.5.11",
"js-sha256": "^0.9.0",
"jwt-decode": "^4.0.0",
"lande": "^1.0.10",
@@ -269,7 +270,7 @@
"react-scripts": "^5.0.1",
"react-test-renderer": "18.2.0",
"ts-node": "^10.9.1",
- "typescript": "^5.3.3",
+ "typescript": "^5.5.4",
"url-loader": "^4.1.1",
"webpack": "^5.75.0",
"webpack-bundle-analyzer": "^4.10.1",
diff --git a/patches/expo-modules-core+1.12.11.patch b/patches/expo-modules-core+1.12.11.patch
index 4878bb9f7e..a4ee027c81 100644
--- a/patches/expo-modules-core+1.12.11.patch
+++ b/patches/expo-modules-core+1.12.11.patch
@@ -12,3 +12,15 @@ index bb74e80..0aa0202 100644
Map constants = new HashMap<>(3);
constants.put(MODULES_CONSTANTS_KEY, new HashMap<>());
+diff --git a/node_modules/expo-modules-core/build/uuid/uuid.js b/node_modules/expo-modules-core/build/uuid/uuid.js
+index 109d3fe..c7fce9e 100644
+--- a/node_modules/expo-modules-core/build/uuid/uuid.js
++++ b/node_modules/expo-modules-core/build/uuid/uuid.js
+@@ -1,5 +1,7 @@
+ import bytesToUuid from './lib/bytesToUuid';
+ import { Uuidv5Namespace } from './uuid.types';
++import { ensureNativeModulesAreInstalled } from '../ensureNativeModulesAreInstalled';
++ensureNativeModulesAreInstalled();
+ const nativeUuidv4 = globalThis?.expo?.uuidv4;
+ const nativeUuidv5 = globalThis?.expo?.uuidv5;
+ function uuidv4() {
diff --git a/patches/expo-video+1.1.10.patch b/patches/expo-video+1.1.10.patch
new file mode 100644
index 0000000000..b183be9d41
--- /dev/null
+++ b/patches/expo-video+1.1.10.patch
@@ -0,0 +1,20 @@
+--- a/node_modules/expo-video/android/src/main/java/expo/modules/video/PlayerViewExtension.kt
++++ b/node_modules/expo-video/android/src/main/java/expo/modules/video/PlayerViewExtension.kt
+@@ -11,6 +11,7 @@ internal fun PlayerView.applyRequiresLinearPlayback(requireLinearPlayback: Boole
+ setShowPreviousButton(!requireLinearPlayback)
+ setShowNextButton(!requireLinearPlayback)
+ setTimeBarInteractive(requireLinearPlayback)
++ setShowSubtitleButton(true)
+ }
+
+ @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
+@@ -27,7 +28,8 @@ internal fun PlayerView.setTimeBarInteractive(interactive: Boolean) {
+
+ @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
+ internal fun PlayerView.setFullscreenButtonVisibility(visible: Boolean) {
+- val fullscreenButton = findViewById(androidx.media3.ui.R.id.exo_fullscreen)
++ val fullscreenButton =
++ findViewById(androidx.media3.ui.R.id.exo_fullscreen)
+ fullscreenButton?.visibility = if (visible) {
+ android.view.View.VISIBLE
+ } else {
diff --git a/src/App.native.tsx b/src/App.native.tsx
index f0dde6ee1a..d2c20fc8e7 100644
--- a/src/App.native.tsx
+++ b/src/App.native.tsx
@@ -23,10 +23,12 @@ import {
} from '#/lib/statsig/statsig'
import {s} from '#/lib/styles'
import {ThemeProvider} from '#/lib/ThemeContext'
+import I18nProvider from '#/locale/i18nProvider'
import {logger} from '#/logger'
import {Provider as A11yProvider} from '#/state/a11y'
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
import {Provider as DialogStateProvider} from '#/state/dialogs'
+import {listenSessionDropped} from '#/state/events'
import {Provider as InvitesStateProvider} from '#/state/invites'
import {Provider as LightboxStateProvider} from '#/state/lightbox'
import {MessagesProvider} from '#/state/messages'
@@ -49,6 +51,7 @@ import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide'
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
import {TestCtrls} from '#/view/com/testing/TestCtrls'
+import {ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoContext'
import * as Toast from '#/view/com/util/Toast'
import {Shell} from '#/view/shell'
import {ThemeProvider as Alf} from '#/alf'
@@ -58,8 +61,6 @@ import {Provider as PortalProvider} from '#/components/Portal'
import {Splash} from '#/Splash'
import {Provider as TourProvider} from '#/tours'
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
-import I18nProvider from './locale/i18nProvider'
-import {listenSessionDropped} from './state/events'
SplashScreen.preventAutoHideAsync()
@@ -95,7 +96,10 @@ function InnerApp() {
useEffect(() => {
return listenSessionDropped(() => {
- Toast.show(_(msg`Sorry! Your session expired. Please log in again.`))
+ Toast.show(
+ _(msg`Sorry! Your session expired. Please log in again.`),
+ 'info',
+ )
})
}, [_])
@@ -104,42 +108,44 @@ function InnerApp() {
-
-
-
-
-
- {/* LabelDefsProvider MUST come before ModerationOptsProvider */}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+ {/* LabelDefsProvider MUST come before ModerationOptsProvider */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/App.web.tsx b/src/App.web.tsx
index eb4a925d07..df6fbf2449 100644
--- a/src/App.web.tsx
+++ b/src/App.web.tsx
@@ -12,10 +12,12 @@ import {useIntentHandler} from '#/lib/hooks/useIntentHandler'
import {QueryProvider} from '#/lib/react-query'
import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
import {ThemeProvider} from '#/lib/ThemeContext'
+import I18nProvider from '#/locale/i18nProvider'
import {logger} from '#/logger'
import {Provider as A11yProvider} from '#/state/a11y'
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
import {Provider as DialogStateProvider} from '#/state/dialogs'
+import {listenSessionDropped} from '#/state/events'
import {Provider as InvitesStateProvider} from '#/state/invites'
import {Provider as LightboxStateProvider} from '#/state/lightbox'
import {MessagesProvider} from '#/state/messages'
@@ -37,6 +39,7 @@ import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out'
import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide'
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
+import {ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoContext'
import * as Toast from '#/view/com/util/Toast'
import {ToastContainer} from '#/view/com/util/Toast.web'
import {Shell} from '#/view/shell/index'
@@ -46,8 +49,6 @@ import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
import {Provider as PortalProvider} from '#/components/Portal'
import {Provider as TourProvider} from '#/tours'
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
-import I18nProvider from './locale/i18nProvider'
-import {listenSessionDropped} from './state/events'
function InnerApp() {
const [isReady, setIsReady] = React.useState(false)
@@ -77,7 +78,10 @@ function InnerApp() {
useEffect(() => {
return listenSessionDropped(() => {
- Toast.show(_(msg`Sorry! Your session expired. Please log in again.`))
+ Toast.show(
+ _(msg`Sorry! Your session expired. Please log in again.`),
+ 'info',
+ )
})
}, [_])
@@ -89,39 +93,41 @@ function InnerApp() {
-
-
-
-
- {/* LabelDefsProvider MUST come before ModerationOptsProvider */}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+ {/* LabelDefsProvider MUST come before ModerationOptsProvider */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Navigation.tsx b/src/Navigation.tsx
index 8c815a3fe2..8646577c8b 100644
--- a/src/Navigation.tsx
+++ b/src/Navigation.tsx
@@ -76,6 +76,7 @@ import {LogScreen} from './view/screens/Log'
import {ModerationModlistsScreen} from './view/screens/ModerationModlists'
import {NotFoundScreen} from './view/screens/NotFound'
import {NotificationsScreen} from './view/screens/Notifications'
+import {NotificationsSettingsScreen} from './view/screens/NotificationsSettings'
import {PostLikedByScreen} from './view/screens/PostLikedBy'
import {PostRepostedByScreen} from './view/screens/PostRepostedBy'
import {PostThreadScreen} from './view/screens/PostThread'
@@ -324,6 +325,11 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
getComponent={() => MessagesSettingsScreen}
options={{title: title(msg`Chat settings`), requireAuth: true}}
/>
+ NotificationsSettingsScreen}
+ options={{title: title(msg`Notification settings`), requireAuth: true}}
+ />
FeedsScreen}
@@ -771,15 +777,14 @@ function logModuleInitTime() {
})
if (isWeb) {
- Referrer.getReferrerInfoAsync().then(info => {
- if (info && info.hostname !== 'bsky.app') {
- logEvent('deepLink:referrerReceived', {
- to: window.location.href,
- referrer: info?.referrer,
- hostname: info?.hostname,
- })
- }
- })
+ const referrerInfo = Referrer.getReferrerInfo()
+ if (referrerInfo && referrerInfo.hostname !== 'bsky.app') {
+ logEvent('deepLink:referrerReceived', {
+ to: window.location.href,
+ referrer: referrerInfo?.referrer,
+ hostname: referrerInfo?.hostname,
+ })
+ }
}
if (__DEV__) {
diff --git a/src/alf/atoms.ts b/src/alf/atoms.ts
index 1dc2dfa7b0..e918e370d8 100644
--- a/src/alf/atoms.ts
+++ b/src/alf/atoms.ts
@@ -2,7 +2,6 @@ import {Platform, StyleSheet} from 'react-native'
import * as tokens from '#/alf/tokens'
import {native, web} from '#/alf/util/platform'
-import hairlineWidth = StyleSheet.hairlineWidth
export const atoms = {
/*
@@ -145,6 +144,9 @@ export const atoms = {
flex_shrink: {
flexShrink: 1,
},
+ flex_shrink_0: {
+ flexShrink: 0,
+ },
justify_start: {
justifyContent: 'flex-start',
},
@@ -281,19 +283,19 @@ export const atoms = {
borderWidth: 0,
},
border: {
- borderWidth: hairlineWidth,
+ borderWidth: StyleSheet.hairlineWidth,
},
border_t: {
- borderTopWidth: hairlineWidth,
+ borderTopWidth: StyleSheet.hairlineWidth,
},
border_b: {
- borderBottomWidth: hairlineWidth,
+ borderBottomWidth: StyleSheet.hairlineWidth,
},
border_l: {
- borderLeftWidth: hairlineWidth,
+ borderLeftWidth: StyleSheet.hairlineWidth,
},
border_r: {
- borderRightWidth: hairlineWidth,
+ borderRightWidth: StyleSheet.hairlineWidth,
},
/*
diff --git a/src/alf/themes.ts b/src/alf/themes.ts
index b430fa65e1..ba18ee0072 100644
--- a/src/alf/themes.ts
+++ b/src/alf/themes.ts
@@ -261,23 +261,23 @@ export function createThemes({
contrast_950: `hsl(${hues.primary}, 20%, ${dimScale[12]}%)`,
contrast_975: `hsl(${hues.primary}, 20%, ${dimScale[13]}%)`,
- primary_25: `hsl(${hues.primary}, 99%, ${dimScale[1]}%)`,
- primary_50: `hsl(${hues.primary}, 99%, ${dimScale[2]}%)`,
- primary_100: `hsl(${hues.primary}, 99%, ${dimScale[3]}%)`,
- primary_200: `hsl(${hues.primary}, 99%, ${dimScale[4]}%)`,
- primary_300: `hsl(${hues.primary}, 99%, ${dimScale[5]}%)`,
- primary_400: `hsl(${hues.primary}, 99%, ${dimScale[6]}%)`,
+ primary_25: `hsl(${hues.primary}, 50%, ${dimScale[1]}%)`,
+ primary_50: `hsl(${hues.primary}, 60%, ${dimScale[2]}%)`,
+ primary_100: `hsl(${hues.primary}, 70%, ${dimScale[3]}%)`,
+ primary_200: `hsl(${hues.primary}, 82%, ${dimScale[4]}%)`,
+ primary_300: `hsl(${hues.primary}, 90%, ${dimScale[5]}%)`,
+ primary_400: `hsl(${hues.primary}, 95%, ${dimScale[6]}%)`,
primary_500: `hsl(${hues.primary}, 99%, ${dimScale[7]}%)`,
- primary_600: `hsl(${hues.primary}, 95%, ${dimScale[8]}%)`,
- primary_700: `hsl(${hues.primary}, 90%, ${dimScale[9]}%)`,
- primary_800: `hsl(${hues.primary}, 82%, ${dimScale[10]}%)`,
- primary_900: `hsl(${hues.primary}, 70%, ${dimScale[11]}%)`,
- primary_950: `hsl(${hues.primary}, 60%, ${dimScale[12]}%)`,
- primary_975: `hsl(${hues.primary}, 50%, ${dimScale[13]}%)`,
+ primary_600: `hsl(${hues.primary}, 99%, ${dimScale[8]}%)`,
+ primary_700: `hsl(${hues.primary}, 99%, ${dimScale[9]}%)`,
+ primary_800: `hsl(${hues.primary}, 99%, ${dimScale[10]}%)`,
+ primary_900: `hsl(${hues.primary}, 99%, ${dimScale[11]}%)`,
+ primary_950: `hsl(${hues.primary}, 99%, ${dimScale[12]}%)`,
+ primary_975: `hsl(${hues.primary}, 99%, ${dimScale[13]}%)`,
- positive_25: `hsl(${hues.positive}, 82%, ${dimScale[1]}%)`,
- positive_50: `hsl(${hues.positive}, 82%, ${dimScale[2]}%)`,
- positive_100: `hsl(${hues.positive}, 82%, ${dimScale[3]}%)`,
+ positive_25: `hsl(${hues.positive}, 50%, ${dimScale[1]}%)`,
+ positive_50: `hsl(${hues.positive}, 60%, ${dimScale[2]}%)`,
+ positive_100: `hsl(${hues.positive}, 70%, ${dimScale[3]}%)`,
positive_200: `hsl(${hues.positive}, 82%, ${dimScale[4]}%)`,
positive_300: `hsl(${hues.positive}, 82%, ${dimScale[5]}%)`,
positive_400: `hsl(${hues.positive}, 82%, ${dimScale[6]}%)`,
@@ -285,23 +285,23 @@ export function createThemes({
positive_600: `hsl(${hues.positive}, 82%, ${dimScale[8]}%)`,
positive_700: `hsl(${hues.positive}, 82%, ${dimScale[9]}%)`,
positive_800: `hsl(${hues.positive}, 82%, ${dimScale[10]}%)`,
- positive_900: `hsl(${hues.positive}, 70%, ${dimScale[11]}%)`,
- positive_950: `hsl(${hues.positive}, 60%, ${dimScale[12]}%)`,
- positive_975: `hsl(${hues.positive}, 50%, ${dimScale[13]}%)`,
+ positive_900: `hsl(${hues.positive}, 82%, ${dimScale[11]}%)`,
+ positive_950: `hsl(${hues.positive}, 82%, ${dimScale[12]}%)`,
+ positive_975: `hsl(${hues.positive}, 82%, ${dimScale[13]}%)`,
- negative_25: `hsl(${hues.negative}, 91%, ${dimScale[1]}%)`,
- negative_50: `hsl(${hues.negative}, 91%, ${dimScale[2]}%)`,
- negative_100: `hsl(${hues.negative}, 91%, ${dimScale[3]}%)`,
- negative_200: `hsl(${hues.negative}, 91%, ${dimScale[4]}%)`,
+ negative_25: `hsl(${hues.negative}, 70%, ${dimScale[1]}%)`,
+ negative_50: `hsl(${hues.negative}, 80%, ${dimScale[2]}%)`,
+ negative_100: `hsl(${hues.negative}, 84%, ${dimScale[3]}%)`,
+ negative_200: `hsl(${hues.negative}, 88%, ${dimScale[4]}%)`,
negative_300: `hsl(${hues.negative}, 91%, ${dimScale[5]}%)`,
negative_400: `hsl(${hues.negative}, 91%, ${dimScale[6]}%)`,
negative_500: `hsl(${hues.negative}, 91%, ${dimScale[7]}%)`,
negative_600: `hsl(${hues.negative}, 91%, ${dimScale[8]}%)`,
negative_700: `hsl(${hues.negative}, 91%, ${dimScale[9]}%)`,
- negative_800: `hsl(${hues.negative}, 88%, ${dimScale[10]}%)`,
- negative_900: `hsl(${hues.negative}, 84%, ${dimScale[11]}%)`,
- negative_950: `hsl(${hues.negative}, 80%, ${dimScale[12]}%)`,
- negative_975: `hsl(${hues.negative}, 70%, ${dimScale[13]}%)`,
+ negative_800: `hsl(${hues.negative}, 91%, ${dimScale[10]}%)`,
+ negative_900: `hsl(${hues.negative}, 91%, ${dimScale[11]}%)`,
+ negative_950: `hsl(${hues.negative}, 91%, ${dimScale[12]}%)`,
+ negative_975: `hsl(${hues.negative}, 91%, ${dimScale[13]}%)`,
} as const
const light: Theme = {
diff --git a/src/components/Button.tsx b/src/components/Button.tsx
index 457164d111..4fe0ab4b12 100644
--- a/src/components/Button.tsx
+++ b/src/components/Button.tsx
@@ -70,6 +70,9 @@ export type ButtonProps = Pick<
AccessibilityProps &
VariantProps & {
testID?: string
+ /**
+ * For a11y, try to make this descriptive and clear
+ */
label: string
style?: StyleProp
hoverStyle?: StyleProp
diff --git a/src/components/FeedCard.tsx b/src/components/FeedCard.tsx
index 5e50f3c483..e6d664cfda 100644
--- a/src/components/FeedCard.tsx
+++ b/src/components/FeedCard.tsx
@@ -40,7 +40,7 @@ type Props = {
export function Default(props: Props) {
const {view} = props
return (
-
+
@@ -58,7 +58,7 @@ export function Link({
view,
children,
...props
-}: Props & Omit) {
+}: Props & Omit) {
const queryClient = useQueryClient()
const href = React.useMemo(() => {
@@ -70,7 +70,11 @@ export function Link({
}, [view, queryClient])
return (
-
+
{children}
)
@@ -264,9 +268,9 @@ function SaveButtonInner({
])
}
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`))
+ } catch (err: any) {
+ logger.error(err, {context: `FeedCard: failed to update feeds`, pin})
+ Toast.show(_(msg`Failed to update feeds`), 'xmark')
}
},
[_, pin, saveFeeds, removeFeed, uri, savedFeedConfig, type],
diff --git a/src/components/FeedInterstitials.tsx b/src/components/FeedInterstitials.tsx
index 53c1fac0b9..2e8724143d 100644
--- a/src/components/FeedInterstitials.tsx
+++ b/src/components/FeedInterstitials.tsx
@@ -190,7 +190,7 @@ export function SuggestedFollows() {
{profiles.slice(0, maxLength).map(profile => (
{
logEvent('feed:interstitial:profileCard:press', {})
}}
@@ -266,7 +266,10 @@ export function SuggestedFollows() {
a.pt_xs,
a.gap_md,
]}>
-
+
Browse more suggestions
@@ -396,7 +399,10 @@ export function SuggestedFeeds() {
a.pt_xs,
a.gap_md,
]}>
-
+
Browse more suggestions
diff --git a/src/components/Link.tsx b/src/components/Link.tsx
index a8b478be78..6c25faffb8 100644
--- a/src/components/Link.tsx
+++ b/src/components/Link.tsx
@@ -40,11 +40,6 @@ type BaseLinkProps = Pick<
> & {
testID?: string
- /**
- * Label for a11y. Defaults to the href.
- */
- label?: string
-
/**
* The React Navigation `StackAction` to perform when the link is pressed.
*/
@@ -197,7 +192,7 @@ export function useLink({
}
export type LinkProps = Omit &
- Omit
+ Omit
/**
* A interactive element that renders as a `` tag on the web. On mobile it
@@ -224,7 +219,6 @@ export function Link({
return (
-
+
{state => (
-
+
{state => (
-
+
{state => (
Adult content can only be enabled via the Web at{' '}
{
evt.preventDefault()
@@ -569,7 +579,9 @@ function PwiOptOut() {
-
+
Learn more about what is public on Bluesky.
diff --git a/src/screens/Profile/Header/ProfileHeaderLabeler.tsx b/src/screens/Profile/Header/ProfileHeaderLabeler.tsx
index d266decb32..a807c70dd9 100644
--- a/src/screens/Profile/Header/ProfileHeaderLabeler.tsx
+++ b/src/screens/Profile/Header/ProfileHeaderLabeler.tsx
@@ -10,6 +10,8 @@ import {
import {msg, Plural, plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+import {MAX_LABELERS} from '#/lib/constants'
import {isAppLabeler} from '#/lib/moderation'
import {logger} from '#/logger'
import {Shadow} from '#/state/cache/types'
@@ -75,14 +77,14 @@ let ProfileHeaderLabeler = ({
[profile, moderationOpts],
)
const {data: preferences} = usePreferencesQuery()
- const {mutateAsync: toggleSubscription, variables} =
- useLabelerSubscriptionMutation()
+ const {
+ mutateAsync: toggleSubscription,
+ variables,
+ reset,
+ } = useLabelerSubscriptionMutation()
const isSubscribed =
variables?.subscribe ??
preferences?.moderationPrefs.labelers.find(l => l.did === profile.did)
- const canSubscribe =
- isSubscribed ||
- (preferences ? preferences?.moderationPrefs.labelers.length <= 20 : false)
const {mutateAsync: likeMod, isPending: isLikePending} = useLikeMutation()
const {mutateAsync: unlikeMod, isPending: isUnlikePending} =
useUnlikeMutation()
@@ -114,6 +116,7 @@ let ProfileHeaderLabeler = ({
_(
msg`There was an an issue contacting the server, please check your internet connection and try again.`,
),
+ 'xmark',
)
logger.error(`Failed to toggle labeler like`, {message: e.message})
}
@@ -130,17 +133,17 @@ let ProfileHeaderLabeler = ({
const onPressSubscribe = React.useCallback(
() =>
requireAuth(async (): Promise => {
- if (!canSubscribe) {
- cantSubscribePrompt.open()
- return
- }
try {
await toggleSubscription({
did: profile.did,
subscribe: !isSubscribed,
})
} catch (e: any) {
- // setSubscriptionError(e.message)
+ reset()
+ if (e.message === 'MAX_LABELERS') {
+ cantSubscribePrompt.open()
+ return
+ }
logger.error(`Failed to subscribe to labeler`, {message: e.message})
}
}),
@@ -149,8 +152,8 @@ let ProfileHeaderLabeler = ({
toggleSubscription,
isSubscribed,
profile,
- canSubscribe,
cantSubscribePrompt,
+ reset,
],
)
@@ -199,14 +202,13 @@ let ProfileHeaderLabeler = ({
style={[
{
paddingVertical: gtMobile ? 12 : 10,
- backgroundColor:
- isSubscribed || !canSubscribe
- ? state.hovered || state.pressed
- ? t.palette.contrast_50
- : t.palette.contrast_25
- : state.hovered || state.pressed
- ? tokens.color.temp_purple_dark
- : tokens.color.temp_purple,
+ backgroundColor: isSubscribed
+ ? state.hovered || state.pressed
+ ? t.palette.contrast_50
+ : t.palette.contrast_25
+ : state.hovered || state.pressed
+ ? tokens.color.temp_purple_dark
+ : tokens.color.temp_purple,
},
a.px_lg,
a.rounded_sm,
@@ -215,11 +217,9 @@ let ProfileHeaderLabeler = ({
+
{_(msg`Terms of Service`)}
,
)
}
if (pp) {
els.push(
-
+
{_(msg`Privacy Policy`)}
,
)
diff --git a/src/screens/Signup/index.tsx b/src/screens/Signup/index.tsx
index da0383884b..189760460e 100644
--- a/src/screens/Signup/index.tsx
+++ b/src/screens/Signup/index.tsx
@@ -166,6 +166,7 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
Having trouble?{' '}
Contact support
diff --git a/src/screens/StarterPack/StarterPackScreen.tsx b/src/screens/StarterPack/StarterPackScreen.tsx
index 680b7c455e..595e185274 100644
--- a/src/screens/StarterPack/StarterPackScreen.tsx
+++ b/src/screens/StarterPack/StarterPackScreen.tsx
@@ -359,7 +359,7 @@ function Header({
captureAction(ProgressGuideAction.Follow, dids.length)
Toast.show(_(msg`All accounts have been followed!`))
} catch (e) {
- Toast.show(_(msg`An error occurred while trying to follow all`))
+ Toast.show(_(msg`An error occurred while trying to follow all`), 'xmark')
} finally {
setIsProcessing(false)
}
@@ -672,7 +672,7 @@ function InvalidStarterPack({rkey}: {rkey: string}) {
onError: e => {
setIsProcessing(false)
logger.error('Failed to delete invalid starter pack', {safeMessage: e})
- Toast.show(_(msg`Failed to delete starter pack`))
+ Toast.show(_(msg`Failed to delete starter pack`), 'xmark')
},
})
diff --git a/src/screens/StarterPack/Wizard/State.tsx b/src/screens/StarterPack/Wizard/State.tsx
index ea9bbf9d33..ba5bb147c8 100644
--- a/src/screens/StarterPack/Wizard/State.tsx
+++ b/src/screens/StarterPack/Wizard/State.tsx
@@ -74,7 +74,10 @@ function reducer(state: State, action: Action): State {
break
case 'AddProfile':
if (state.profiles.length >= 51) {
- Toast.show(msg`You may only add up to 50 profiles`.message ?? '')
+ Toast.show(
+ msg`You may only add up to 50 profiles`.message ?? '',
+ 'info',
+ )
} else {
updatedState = {...state, profiles: [...state.profiles, action.profile]}
}
@@ -89,7 +92,7 @@ function reducer(state: State, action: Action): State {
break
case 'AddFeed':
if (state.feeds.length >= 50) {
- Toast.show(msg`You may only add up to 50 feeds`.message ?? '')
+ Toast.show(msg`You may only add up to 50 feeds`.message ?? '', 'info')
} else {
updatedState = {...state, feeds: [...state.feeds, action.feed]}
}
diff --git a/src/screens/StarterPack/Wizard/index.tsx b/src/screens/StarterPack/Wizard/index.tsx
index 3f0499a1da..8d9bb165b9 100644
--- a/src/screens/StarterPack/Wizard/index.tsx
+++ b/src/screens/StarterPack/Wizard/index.tsx
@@ -227,7 +227,7 @@ function WizardInner({
onError: e => {
logger.error('Failed to create starter pack', {safeMessage: e})
dispatch({type: 'SetProcessing', processing: false})
- Toast.show(_(msg`Failed to create starter pack`))
+ Toast.show(_(msg`Failed to create starter pack`), 'xmark')
},
})
const {mutate: editStarterPack} = useEditStarterPackMutation({
@@ -235,7 +235,7 @@ function WizardInner({
onError: e => {
logger.error('Failed to edit starter pack', {safeMessage: e})
dispatch({type: 'SetProcessing', processing: false})
- Toast.show(_(msg`Failed to create starter pack`))
+ Toast.show(_(msg`Failed to create starter pack`), 'xmark')
},
})
diff --git a/src/state/queries/actor-autocomplete.ts b/src/state/queries/actor-autocomplete.ts
index 7e997ea016..abf78da3ce 100644
--- a/src/state/queries/actor-autocomplete.ts
+++ b/src/state/queries/actor-autocomplete.ts
@@ -45,9 +45,13 @@ export function useActorAutocompleteQuery(
},
select: React.useCallback(
(data: AppBskyActorDefs.ProfileViewBasic[]) => {
- return computeSuggestions(data, moderationOpts || DEFAULT_MOD_OPTS)
+ return computeSuggestions({
+ q: prefix,
+ searched: data,
+ moderationOpts: moderationOpts || DEFAULT_MOD_OPTS,
+ })
},
- [moderationOpts],
+ [prefix, moderationOpts],
),
placeholderData: maintainData ? keepPreviousData : undefined,
})
@@ -81,19 +85,25 @@ export function useActorAutocompleteFn() {
}
}
- return computeSuggestions(
- res?.data.actors,
- moderationOpts || DEFAULT_MOD_OPTS,
- )
+ return computeSuggestions({
+ q: query,
+ searched: res?.data.actors,
+ moderationOpts: moderationOpts || DEFAULT_MOD_OPTS,
+ })
},
[queryClient, moderationOpts, agent],
)
}
-function computeSuggestions(
- searched: AppBskyActorDefs.ProfileViewBasic[] = [],
- moderationOpts: ModerationOpts,
-) {
+function computeSuggestions({
+ q,
+ searched = [],
+ moderationOpts,
+}: {
+ q?: string
+ searched?: AppBskyActorDefs.ProfileViewBasic[]
+ moderationOpts: ModerationOpts
+}) {
let items: AppBskyActorDefs.ProfileViewBasic[] = []
for (const item of searched) {
if (!items.find(item2 => item2.handle === item.handle)) {
@@ -102,6 +112,7 @@ function computeSuggestions(
}
return items.filter(profile => {
const modui = moderateProfile(profile, moderationOpts).ui('profileList')
- return !modui.filter || isJustAMute(modui)
+ const isExactMatch = q && profile.handle.toLowerCase() === q
+ return isExactMatch || !modui.filter || isJustAMute(modui)
})
}
diff --git a/src/state/queries/labeler.ts b/src/state/queries/labeler.ts
index 058e8fcdee..53e923a857 100644
--- a/src/state/queries/labeler.ts
+++ b/src/state/queries/labeler.ts
@@ -2,9 +2,13 @@ import {AppBskyLabelerDefs} from '@atproto/api'
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
import {z} from 'zod'
+import {MAX_LABELERS} from '#/lib/constants'
import {labelersDetailedInfoQueryKeyRoot} from '#/lib/react-query'
import {STALE} from '#/state/queries'
-import {preferencesQueryKey} from '#/state/queries/preferences'
+import {
+ preferencesQueryKey,
+ usePreferencesQuery,
+} from '#/state/queries/preferences'
import {useAgent} from '#/state/session'
const labelerInfoQueryKeyRoot = 'labeler-info'
@@ -77,6 +81,7 @@ export function useLabelersDetailedInfoQuery({dids}: {dids: string[]}) {
export function useLabelerSubscriptionMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
+ const preferences = usePreferencesQuery()
return useMutation({
async mutationFn({did, subscribe}: {did: string; subscribe: boolean}) {
@@ -86,14 +91,52 @@ export function useLabelerSubscriptionMutation() {
subscribe: z.boolean(),
}).parse({did, subscribe})
+ /**
+ * If a user has invalid/takendown/deactivated labelers, we need to
+ * remove them. We don't have a great way to do this atm on the server,
+ * so we do it here.
+ *
+ * We also need to push validation into this method, since we need to
+ * check {@link MAX_LABELERS} _after_ we've removed invalid or takendown
+ * labelers.
+ */
+ const labelerDids = (
+ preferences.data?.moderationPrefs?.labelers ?? []
+ ).map(l => l.did)
+ const invalidLabelers: string[] = []
+ if (labelerDids.length) {
+ const profiles = await agent.getProfiles({actors: labelerDids})
+ if (profiles.data) {
+ for (const did of labelerDids) {
+ const exists = profiles.data.profiles.find(p => p.did === did)
+ if (exists) {
+ // profile came back but it's not a valid labeler
+ if (exists.associated && !exists.associated.labeler) {
+ invalidLabelers.push(did)
+ }
+ } else {
+ // no response came back, might be deactivated or takendown
+ invalidLabelers.push(did)
+ }
+ }
+ }
+ }
+ if (invalidLabelers.length) {
+ await Promise.all(invalidLabelers.map(did => agent.removeLabeler(did)))
+ }
+
if (subscribe) {
+ const labelerCount = labelerDids.length - invalidLabelers.length
+ if (labelerCount >= MAX_LABELERS) {
+ throw new Error('MAX_LABELERS')
+ }
await agent.addLabeler(did)
} else {
await agent.removeLabeler(did)
}
},
- onSuccess() {
- queryClient.invalidateQueries({
+ async onSuccess() {
+ await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
})
},
diff --git a/src/state/queries/notifications/feed.ts b/src/state/queries/notifications/feed.ts
index 17ee90929c..3cafcb7168 100644
--- a/src/state/queries/notifications/feed.ts
+++ b/src/state/queries/notifications/feed.ts
@@ -46,11 +46,14 @@ const PAGE_SIZE = 30
type RQPageParam = string | undefined
const RQKEY_ROOT = 'notification-feed'
-export function RQKEY() {
- return [RQKEY_ROOT]
+export function RQKEY(priority?: false) {
+ return [RQKEY_ROOT, priority]
}
-export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
+export function useNotificationFeedQuery(opts?: {
+ enabled?: boolean
+ overridePriorityNotifications?: boolean
+}) {
const agent = useAgent()
const queryClient = useQueryClient()
const moderationOpts = useModerationOpts()
@@ -59,6 +62,10 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
const lastPageCountRef = useRef(0)
const gate = useGate()
+ // false: force showing all notifications
+ // undefined: let the server decide
+ const priority = opts?.overridePriorityNotifications ? false : undefined
+
const query = useInfiniteQuery<
FeedPage,
Error,
@@ -67,7 +74,7 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
RQPageParam
>({
staleTime: STALE.INFINITY,
- queryKey: RQKEY(),
+ queryKey: RQKEY(priority),
async queryFn({pageParam}: {pageParam: RQPageParam}) {
let page
if (!pageParam) {
@@ -75,17 +82,17 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
page = unreads.getCachedUnreadPage()
}
if (!page) {
- page = (
- await fetchPage({
- agent,
- limit: PAGE_SIZE,
- cursor: pageParam,
- queryClient,
- moderationOpts,
- fetchAdditionalData: true,
- shouldUngroupFollowBacks: () => gate('ungroup_follow_backs'),
- })
- ).page
+ const {page: fetchedPage} = await fetchPage({
+ agent,
+ limit: PAGE_SIZE,
+ cursor: pageParam,
+ queryClient,
+ moderationOpts,
+ fetchAdditionalData: true,
+ shouldUngroupFollowBacks: () => gate('ungroup_follow_backs'),
+ priority,
+ })
+ page = fetchedPage
}
// if the first page has an unread, mark all read
diff --git a/src/state/queries/notifications/settings.ts b/src/state/queries/notifications/settings.ts
new file mode 100644
index 0000000000..bfc449d17b
--- /dev/null
+++ b/src/state/queries/notifications/settings.ts
@@ -0,0 +1,69 @@
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useMutation, useQueryClient} from '@tanstack/react-query'
+
+import {until} from '#/lib/async/until'
+import {logger} from '#/logger'
+import {RQKEY as RQKEY_NOTIFS} from '#/state/queries/notifications/feed'
+import {invalidateCachedUnreadPage} from '#/state/queries/notifications/unread'
+import {useAgent} from '#/state/session'
+import * as Toast from '#/view/com/util/Toast'
+
+export function useNotificationsSettingsMutation() {
+ const {_} = useLingui()
+ const agent = useAgent()
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: async (keys: string[]) => {
+ const enabled = keys[0] === 'enabled'
+
+ await agent.api.app.bsky.notification.putPreferences({
+ priority: enabled,
+ })
+
+ await until(
+ 5, // 5 tries
+ 1e3, // 1s delay between tries
+ res => res.data.priority === enabled,
+ () => agent.api.app.bsky.notification.listNotifications({limit: 1}),
+ )
+
+ eagerlySetCachedPriority(queryClient, enabled)
+ },
+ onError: err => {
+ logger.error('Failed to save notification preferences', {
+ safeMessage: err,
+ })
+ Toast.show(
+ _(msg`Failed to save notification preferences, please try again`),
+ 'xmark',
+ )
+ },
+ onSuccess: () => {
+ Toast.show(_(msg`Preference saved`))
+ },
+ onSettled: () => {
+ invalidateCachedUnreadPage()
+ queryClient.invalidateQueries({queryKey: RQKEY_NOTIFS()})
+ },
+ })
+}
+
+function eagerlySetCachedPriority(
+ queryClient: ReturnType,
+ enabled: boolean,
+) {
+ queryClient.setQueryData(RQKEY_NOTIFS(), (old: any) => {
+ if (!old) return old
+ return {
+ ...old,
+ pages: old.pages.map((page: any) => {
+ return {
+ ...page,
+ priority: enabled,
+ }
+ }),
+ }
+ })
+}
diff --git a/src/state/queries/notifications/types.ts b/src/state/queries/notifications/types.ts
index d40a07b12f..c96374eb8e 100644
--- a/src/state/queries/notifications/types.ts
+++ b/src/state/queries/notifications/types.ts
@@ -22,6 +22,7 @@ export interface FeedPage {
cursor: string | undefined
seenAt: Date
items: FeedNotification[]
+ priority: boolean
}
export interface CachedFeedPage {
diff --git a/src/state/queries/notifications/util.ts b/src/state/queries/notifications/util.ts
index 2f2c242d82..7651e414a4 100644
--- a/src/state/queries/notifications/util.ts
+++ b/src/state/queries/notifications/util.ts
@@ -39,10 +39,15 @@ export async function fetchPage({
moderationOpts: ModerationOpts | undefined
fetchAdditionalData: boolean
shouldUngroupFollowBacks?: () => boolean
-}): Promise<{page: FeedPage; indexedAt: string | undefined}> {
+ priority?: boolean
+}): Promise<{
+ page: FeedPage
+ indexedAt: string | undefined
+}> {
const res = await agent.listNotifications({
limit,
cursor,
+ // priority,
})
const indexedAt = res.data.notifications[0]?.indexedAt
@@ -88,6 +93,7 @@ export async function fetchPage({
cursor: res.data.cursor,
seenAt,
items: notifsGrouped,
+ priority: res.data.priority ?? false,
},
indexedAt,
}
diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts
index 315c9cfadd..1d6ec80d91 100644
--- a/src/state/queries/post-feed.ts
+++ b/src/state/queries/post-feed.ts
@@ -78,7 +78,10 @@ export interface FeedPostSliceItem {
uri: string
post: AppBskyFeedDefs.PostView
record: AppBskyFeedPost.Record
- reason?: AppBskyFeedDefs.ReasonRepost | ReasonFeedSource
+ reason?:
+ | AppBskyFeedDefs.ReasonRepost
+ | ReasonFeedSource
+ | {[k: string]: unknown; $type: string}
feedContext: string | undefined
moderation: ModerationDecision
parentAuthor?: AppBskyActorDefs.ProfileViewBasic
@@ -311,7 +314,7 @@ export function usePostFeedQuery(
if (isDiscover) {
userActionHistory.seen(
slice.items.map(item => ({
- feedContext: item.feedContext,
+ feedContext: slice.feedContext,
likeCount: item.post.likeCount ?? 0,
repostCount: item.post.repostCount ?? 0,
replyCount: item.post.replyCount ?? 0,
@@ -323,10 +326,10 @@ export function usePostFeedQuery(
)
}
- return {
+ const feedPostSlice: FeedPostSlice = {
_reactKey: slice._reactKey,
_isFeedPostSlice: true,
- rootUri: slice.rootItem.post.uri,
+ rootUri: slice.uri,
isThread:
slice.items.length > 1 &&
slice.items.every(
@@ -341,35 +344,42 @@ export function usePostFeedQuery(
AppBskyFeedPost.validateRecord(item.post.record)
.success
) {
- const parentAuthor =
- item.reply?.parent?.author ??
- slice.items[i + 1]?.reply?.grandparentAuthor
+ const parent = item.reply?.parent
+ let parentAuthor:
+ | AppBskyActorDefs.ProfileViewBasic
+ | undefined
+ if (AppBskyFeedDefs.isPostView(parent)) {
+ parentAuthor = parent.author
+ }
+ if (!parentAuthor) {
+ parentAuthor =
+ slice.items[i + 1]?.reply?.grandparentAuthor
+ }
const replyRef = item.reply
const isParentBlocked = AppBskyFeedDefs.isBlockedPost(
replyRef?.parent,
)
- return {
+ const feedPostSliceItem: FeedPostSliceItem = {
_reactKey: `${slice._reactKey}-${i}-${item.post.uri}`,
uri: item.post.uri,
post: item.post,
record: item.post.record,
- reason:
- i === 0 && slice.source
- ? slice.source
- : item.reason,
- feedContext: item.feedContext || slice.feedContext,
+ reason: slice.reason,
+ feedContext: slice.feedContext,
moderation: moderations[i],
parentAuthor,
isParentBlocked,
}
+ return feedPostSliceItem
}
return undefined
})
- .filter(Boolean) as FeedPostSliceItem[],
+ .filter(n => !!n),
}
+ return feedPostSlice
})
- .filter(Boolean) as FeedPostSlice[],
+ .filter(n => !!n),
})),
],
}
diff --git a/src/state/queries/threadgate.ts b/src/state/queries/threadgate.ts
index 67c6f8c084..8b6aeba6c1 100644
--- a/src/state/queries/threadgate.ts
+++ b/src/state/queries/threadgate.ts
@@ -4,7 +4,7 @@ export type ThreadgateSetting =
| {type: 'nobody'}
| {type: 'mention'}
| {type: 'following'}
- | {type: 'list'; list: string}
+ | {type: 'list'; list: unknown}
export function threadgateViewToSettings(
threadgate: AppBskyFeedDefs.ThreadgateView | undefined,
@@ -21,18 +21,18 @@ export function threadgateViewToSettings(
if (!record.allow?.length) {
return [{type: 'nobody'}]
}
- return record.allow
+ const settings: ThreadgateSetting[] = record.allow
.map(allow => {
+ let setting: ThreadgateSetting | undefined
if (allow.$type === 'app.bsky.feed.threadgate#mentionRule') {
- return {type: 'mention'}
+ setting = {type: 'mention'}
+ } else if (allow.$type === 'app.bsky.feed.threadgate#followingRule') {
+ setting = {type: 'following'}
+ } else if (allow.$type === 'app.bsky.feed.threadgate#listRule') {
+ setting = {type: 'list', list: allow.list}
}
- if (allow.$type === 'app.bsky.feed.threadgate#followingRule') {
- return {type: 'following'}
- }
- if (allow.$type === 'app.bsky.feed.threadgate#listRule') {
- return {type: 'list', list: allow.list}
- }
- return undefined
+ return setting
})
- .filter(Boolean) as ThreadgateSetting[]
+ .filter(n => !!n)
+ return settings
}
diff --git a/src/state/queries/video/compress-video.ts b/src/state/queries/video/compress-video.ts
new file mode 100644
index 0000000000..a2c739cfde
--- /dev/null
+++ b/src/state/queries/video/compress-video.ts
@@ -0,0 +1,31 @@
+import {ImagePickerAsset} from 'expo-image-picker'
+import {useMutation} from '@tanstack/react-query'
+
+import {CompressedVideo, compressVideo} from 'lib/media/video/compress'
+
+export function useCompressVideoMutation({
+ onProgress,
+ onSuccess,
+ onError,
+}: {
+ onProgress: (progress: number) => void
+ onError: (e: any) => void
+ onSuccess: (video: CompressedVideo) => void
+}) {
+ return useMutation({
+ mutationFn: async (asset: ImagePickerAsset) => {
+ return await compressVideo(asset.uri, {
+ onProgress: num => onProgress(trunc2dp(num)),
+ })
+ },
+ onError,
+ onSuccess,
+ onMutate: () => {
+ onProgress(0)
+ },
+ })
+}
+
+function trunc2dp(num: number) {
+ return Math.trunc(num * 100) / 100
+}
diff --git a/src/state/queries/video/util.ts b/src/state/queries/video/util.ts
new file mode 100644
index 0000000000..266d8aee37
--- /dev/null
+++ b/src/state/queries/video/util.ts
@@ -0,0 +1,15 @@
+const UPLOAD_ENDPOINT = process.env.EXPO_PUBLIC_VIDEO_ROOT_ENDPOINT ?? ''
+
+export const createVideoEndpointUrl = (
+ route: string,
+ params?: Record,
+) => {
+ const url = new URL(`${UPLOAD_ENDPOINT}`)
+ url.pathname = route
+ if (params) {
+ for (const key in params) {
+ url.searchParams.set(key, params[key])
+ }
+ }
+ return url.href
+}
diff --git a/src/state/queries/video/video-upload.ts b/src/state/queries/video/video-upload.ts
new file mode 100644
index 0000000000..4d7f7995c5
--- /dev/null
+++ b/src/state/queries/video/video-upload.ts
@@ -0,0 +1,59 @@
+import {createUploadTask, FileSystemUploadType} from 'expo-file-system'
+import {useMutation} from '@tanstack/react-query'
+import {nanoid} from 'nanoid/non-secure'
+
+import {CompressedVideo} from 'lib/media/video/compress'
+import {UploadVideoResponse} from 'lib/media/video/types'
+import {createVideoEndpointUrl} from 'state/queries/video/util'
+import {useSession} from 'state/session'
+const UPLOAD_HEADER = process.env.EXPO_PUBLIC_VIDEO_HEADER ?? ''
+
+export const useUploadVideoMutation = ({
+ onSuccess,
+ onError,
+ setProgress,
+}: {
+ onSuccess: (response: UploadVideoResponse) => void
+ onError: (e: any) => void
+ setProgress: (progress: number) => void
+}) => {
+ const {currentAccount} = useSession()
+
+ return useMutation({
+ mutationFn: async (video: CompressedVideo) => {
+ const uri = createVideoEndpointUrl('/upload', {
+ did: currentAccount!.did,
+ name: `${nanoid(12)}.mp4`, // @TODO what are we limiting this to?
+ })
+
+ const uploadTask = createUploadTask(
+ uri,
+ video.uri,
+ {
+ headers: {
+ 'dev-key': UPLOAD_HEADER,
+ 'content-type': 'video/mp4', // @TODO same question here. does the compression step always output mp4?
+ },
+ httpMethod: 'POST',
+ uploadType: FileSystemUploadType.BINARY_CONTENT,
+ },
+ p => {
+ setProgress(p.totalBytesSent / p.totalBytesExpectedToSend)
+ },
+ )
+ const res = await uploadTask.uploadAsync()
+
+ if (!res?.body) {
+ throw new Error('No response')
+ }
+
+ // @TODO rm, useful for debugging/getting video cid
+ console.log('[VIDEO]', res.body)
+ const responseBody = JSON.parse(res.body) as UploadVideoResponse
+ onSuccess(responseBody)
+ return responseBody
+ },
+ onError,
+ onSuccess,
+ })
+}
diff --git a/src/state/queries/video/video-upload.web.ts b/src/state/queries/video/video-upload.web.ts
new file mode 100644
index 0000000000..b5b9e93bf9
--- /dev/null
+++ b/src/state/queries/video/video-upload.web.ts
@@ -0,0 +1,66 @@
+import {useMutation} from '@tanstack/react-query'
+import {nanoid} from 'nanoid/non-secure'
+
+import {CompressedVideo} from 'lib/media/video/compress'
+import {UploadVideoResponse} from 'lib/media/video/types'
+import {createVideoEndpointUrl} from 'state/queries/video/util'
+import {useSession} from 'state/session'
+const UPLOAD_HEADER = process.env.EXPO_PUBLIC_VIDEO_HEADER ?? ''
+
+export const useUploadVideoMutation = ({
+ onSuccess,
+ onError,
+ setProgress,
+}: {
+ onSuccess: (response: UploadVideoResponse) => void
+ onError: (e: any) => void
+ setProgress: (progress: number) => void
+}) => {
+ const {currentAccount} = useSession()
+
+ return useMutation({
+ mutationFn: async (video: CompressedVideo) => {
+ const uri = createVideoEndpointUrl('/upload', {
+ did: currentAccount!.did,
+ name: `${nanoid(12)}.mp4`, // @TODO what are we limiting this to?
+ })
+
+ const bytes = await fetch(video.uri).then(res => res.arrayBuffer())
+
+ const xhr = new XMLHttpRequest()
+ const res = (await new Promise((resolve, reject) => {
+ xhr.upload.addEventListener('progress', e => {
+ const progress = e.loaded / e.total
+ setProgress(progress)
+ })
+ xhr.onloadend = () => {
+ if (xhr.readyState === 4) {
+ const uploadRes = JSON.parse(
+ xhr.responseText,
+ ) as UploadVideoResponse
+ resolve(uploadRes)
+ onSuccess(uploadRes)
+ } else {
+ reject()
+ onError(new Error('Failed to upload video'))
+ }
+ }
+ xhr.onerror = () => {
+ reject()
+ onError(new Error('Failed to upload video'))
+ }
+ xhr.open('POST', uri)
+ xhr.setRequestHeader('Content-Type', 'video/mp4') // @TODO how we we set the proper content type?
+ // @TODO remove this header for prod
+ xhr.setRequestHeader('dev-key', UPLOAD_HEADER)
+ xhr.send(bytes)
+ })) as UploadVideoResponse
+
+ // @TODO rm for prod
+ console.log('[VIDEO]', res)
+ return res
+ },
+ onError,
+ onSuccess,
+ })
+}
diff --git a/src/state/queries/video/video.ts b/src/state/queries/video/video.ts
new file mode 100644
index 0000000000..295db38b43
--- /dev/null
+++ b/src/state/queries/video/video.ts
@@ -0,0 +1,212 @@
+import React from 'react'
+import {ImagePickerAsset} from 'expo-image-picker'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useQuery} from '@tanstack/react-query'
+
+import {logger} from '#/logger'
+import {CompressedVideo} from 'lib/media/video/compress'
+import {VideoTooLargeError} from 'lib/media/video/errors'
+import {JobState, JobStatus} from 'lib/media/video/types'
+import {useCompressVideoMutation} from 'state/queries/video/compress-video'
+import {createVideoEndpointUrl} from 'state/queries/video/util'
+import {useUploadVideoMutation} from 'state/queries/video/video-upload'
+
+type Status = 'idle' | 'compressing' | 'processing' | 'uploading' | 'done'
+
+type Action =
+ | {
+ type: 'SetStatus'
+ status: Status
+ }
+ | {
+ type: 'SetProgress'
+ progress: number
+ }
+ | {
+ type: 'SetError'
+ error: string | undefined
+ }
+ | {type: 'Reset'}
+ | {type: 'SetAsset'; asset: ImagePickerAsset}
+ | {type: 'SetVideo'; video: CompressedVideo}
+ | {type: 'SetJobStatus'; jobStatus: JobStatus}
+
+export interface State {
+ status: Status
+ progress: number
+ asset?: ImagePickerAsset
+ video: CompressedVideo | null
+ jobStatus?: JobStatus
+ error?: string
+}
+
+function reducer(state: State, action: Action): State {
+ let updatedState = state
+ if (action.type === 'SetStatus') {
+ updatedState = {...state, status: action.status}
+ } else if (action.type === 'SetProgress') {
+ updatedState = {...state, progress: action.progress}
+ } else if (action.type === 'SetError') {
+ updatedState = {...state, error: action.error}
+ } else if (action.type === 'Reset') {
+ updatedState = {
+ status: 'idle',
+ progress: 0,
+ video: null,
+ }
+ } else if (action.type === 'SetAsset') {
+ updatedState = {...state, asset: action.asset}
+ } else if (action.type === 'SetVideo') {
+ updatedState = {...state, video: action.video}
+ } else if (action.type === 'SetJobStatus') {
+ updatedState = {...state, jobStatus: action.jobStatus}
+ }
+ return updatedState
+}
+
+export function useUploadVideo({
+ setStatus,
+ onSuccess,
+}: {
+ setStatus: (status: string) => void
+ onSuccess: () => void
+}) {
+ const {_} = useLingui()
+ const [state, dispatch] = React.useReducer(reducer, {
+ status: 'idle',
+ progress: 0,
+ video: null,
+ })
+
+ const {setJobId} = useUploadStatusQuery({
+ onStatusChange: (status: JobStatus) => {
+ // This might prove unuseful, most of the job status steps happen too quickly to even be displayed to the user
+ // Leaving it for now though
+ dispatch({
+ type: 'SetJobStatus',
+ jobStatus: status,
+ })
+ setStatus(status.state.toString())
+ },
+ onSuccess: () => {
+ dispatch({
+ type: 'SetStatus',
+ status: 'idle',
+ })
+ onSuccess()
+ },
+ })
+
+ const {mutate: onVideoCompressed} = useUploadVideoMutation({
+ onSuccess: response => {
+ dispatch({
+ type: 'SetStatus',
+ status: 'processing',
+ })
+ setJobId(response.job_id)
+ },
+ onError: e => {
+ dispatch({
+ type: 'SetError',
+ error: _(msg`An error occurred while uploading the video.`),
+ })
+ logger.error('Error uploading video', {safeMessage: e})
+ },
+ setProgress: p => {
+ dispatch({type: 'SetProgress', progress: p})
+ },
+ })
+
+ const {mutate: onSelectVideo} = useCompressVideoMutation({
+ onProgress: p => {
+ dispatch({type: 'SetProgress', progress: p})
+ },
+ onError: e => {
+ if (e instanceof VideoTooLargeError) {
+ dispatch({
+ type: 'SetError',
+ error: _(msg`The selected video is larger than 100MB.`),
+ })
+ } else {
+ dispatch({
+ type: 'SetError',
+ // @TODO better error message from server, left untranslated on purpose
+ error: 'An error occurred while compressing the video.',
+ })
+ logger.error('Error compressing video', {safeMessage: e})
+ }
+ },
+ onSuccess: (video: CompressedVideo) => {
+ dispatch({
+ type: 'SetVideo',
+ video,
+ })
+ dispatch({
+ type: 'SetStatus',
+ status: 'uploading',
+ })
+ onVideoCompressed(video)
+ },
+ })
+
+ const selectVideo = (asset: ImagePickerAsset) => {
+ dispatch({
+ type: 'SetAsset',
+ asset,
+ })
+ dispatch({
+ type: 'SetStatus',
+ status: 'compressing',
+ })
+ onSelectVideo(asset)
+ }
+
+ const clearVideo = () => {
+ // @TODO cancel any running jobs
+ dispatch({type: 'Reset'})
+ }
+
+ return {
+ state,
+ dispatch,
+ selectVideo,
+ clearVideo,
+ }
+}
+
+const useUploadStatusQuery = ({
+ onStatusChange,
+ onSuccess,
+}: {
+ onStatusChange: (status: JobStatus) => void
+ onSuccess: () => void
+}) => {
+ const [enabled, setEnabled] = React.useState(true)
+ const [jobId, setJobId] = React.useState()
+
+ const {isLoading, isError} = useQuery({
+ queryKey: ['video-upload'],
+ queryFn: async () => {
+ const url = createVideoEndpointUrl(`/job/${jobId}/status`)
+ const res = await fetch(url)
+ const status = (await res.json()) as JobStatus
+ if (status.state === JobState.JOB_STATE_COMPLETED) {
+ setEnabled(false)
+ onSuccess()
+ }
+ onStatusChange(status)
+ return status
+ },
+ enabled: Boolean(jobId && enabled),
+ refetchInterval: 1500,
+ })
+
+ return {
+ isLoading,
+ isError,
+ setJobId: (_jobId: string) => {
+ setJobId(_jobId)
+ },
+ }
+}
diff --git a/src/state/shell/post-progress.tsx b/src/state/shell/post-progress.tsx
new file mode 100644
index 0000000000..0df2a6be4a
--- /dev/null
+++ b/src/state/shell/post-progress.tsx
@@ -0,0 +1,18 @@
+import React from 'react'
+
+interface PostProgressState {
+ progress: number
+ status: 'pending' | 'success' | 'error' | 'idle'
+ error?: string
+}
+
+const PostProgressContext = React.createContext({
+ progress: 0,
+ status: 'idle',
+})
+
+export function Provider() {}
+
+export function usePostProgress() {
+ return React.useContext(PostProgressContext)
+}
diff --git a/src/view/com/auth/SplashScreen.web.tsx b/src/view/com/auth/SplashScreen.web.tsx
index 6df4e439aa..9ffcbfb9df 100644
--- a/src/view/com/auth/SplashScreen.web.tsx
+++ b/src/view/com/auth/SplashScreen.web.tsx
@@ -132,6 +132,7 @@ export const SplashScreen = ({
function Footer() {
const t = useTheme()
+ const {_} = useLingui()
return (
-
+
Business
-
+
Blog
-
+
Jobs
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx
index eed5cb6c54..08ce4441f0 100644
--- a/src/view/com/composer/Composer.tsx
+++ b/src/view/com/composer/Composer.tsx
@@ -13,10 +13,16 @@ import {
Keyboard,
KeyboardAvoidingView,
LayoutChangeEvent,
+ StyleProp,
StyleSheet,
View,
+ ViewStyle,
} from 'react-native'
+// @ts-expect-error no type definition
+import ProgressCircle from 'react-native-progress/Circle'
import Animated, {
+ FadeIn,
+ FadeOut,
interpolateColor,
useAnimatedStyle,
useSharedValue,
@@ -55,6 +61,7 @@ import {
import {useProfileQuery} from '#/state/queries/profile'
import {Gif} from '#/state/queries/tenor'
import {ThreadgateSetting} from '#/state/queries/threadgate'
+import {useUploadVideo} from '#/state/queries/video/video'
import {useAgent, useSession} from '#/state/session'
import {useComposerControls} from '#/state/shell/composer'
import {useAnalytics} from 'lib/analytics/analytics'
@@ -70,6 +77,7 @@ import {colors, s} from 'lib/styles'
import {isAndroid, isIOS, isNative, isWeb} from 'platform/detection'
import {useDialogStateControlContext} from 'state/dialogs'
import {GalleryModel} from 'state/models/media/gallery'
+import {State as VideoUploadState} from 'state/queries/video/video'
import {ComposerOpts} from 'state/shell/composer'
import {ComposerReplyTo} from 'view/com/composer/ComposerReplyTo'
import {atoms as a, useTheme} from '#/alf'
@@ -96,10 +104,8 @@ import {TextInput, TextInputRef} from './text-input/TextInput'
import {ThreadgateBtn} from './threadgate/ThreadgateBtn'
import {useExternalLinkFetch} from './useExternalLinkFetch'
import {SelectVideoBtn} from './videos/SelectVideoBtn'
-import {useVideoState} from './videos/state'
import {VideoPreview} from './videos/VideoPreview'
import {VideoTranscodeProgress} from './videos/VideoTranscodeProgress'
-import hairlineWidth = StyleSheet.hairlineWidth
type CancelRef = {
onPressCancel: () => void
@@ -160,14 +166,21 @@ export const ComposePost = observer(function ComposePost({
const [quote, setQuote] = useState(
initQuote,
)
+
const {
- video,
- onSelectVideo,
- videoPending,
- videoProcessingData,
+ selectVideo,
clearVideo,
- videoProcessingProgress,
- } = useVideoState({setError})
+ state: videoUploadState,
+ } = useUploadVideo({
+ setStatus: (status: string) => setProcessingState(status),
+ onSuccess: () => {
+ if (publishOnUpload) {
+ onPressPublish(true)
+ }
+ },
+ })
+ const [publishOnUpload, setPublishOnUpload] = useState(false)
+
const {extLink, setExtLink} = useExternalLinkFetch({setQuote})
const [extGif, setExtGif] = useState()
const [labels, setLabels] = useState([])
@@ -275,7 +288,7 @@ export const ComposePost = observer(function ComposePost({
return false
}, [gallery.needsAltText, extLink, extGif, requireAltTextEnabled])
- const onPressPublish = async () => {
+ const onPressPublish = async (finishedUploading?: boolean) => {
if (isProcessing || graphemeLength > MAX_GRAPHEME_LENGTH) {
return
}
@@ -284,6 +297,15 @@ export const ComposePost = observer(function ComposePost({
return
}
+ if (
+ !finishedUploading &&
+ videoUploadState.status !== 'idle' &&
+ videoUploadState.asset
+ ) {
+ setPublishOnUpload(true)
+ return
+ }
+
setError('')
if (
@@ -388,8 +410,12 @@ export const ComposePost = observer(function ComposePost({
: _(msg`What's up?`)
const canSelectImages =
- gallery.size < 4 && !extLink && !video && !videoPending
- const hasMedia = gallery.size > 0 || Boolean(extLink) || Boolean(video)
+ gallery.size < 4 &&
+ !extLink &&
+ videoUploadState.status === 'idle' &&
+ !videoUploadState.video
+ const hasMedia =
+ gallery.size > 0 || Boolean(extLink) || Boolean(videoUploadState.video)
const onEmojiButtonPress = useCallback(() => {
openPicker?.(textInput.current?.getCursorPosition())
@@ -501,7 +527,10 @@ export const ComposePost = observer(function ComposePost({
shape="default"
size="small"
style={[a.rounded_full, a.py_sm]}
- onPress={onPressPublish}>
+ onPress={() => onPressPublish()}
+ disabled={
+ videoUploadState.status !== 'idle' && publishOnUpload
+ }>
{replyTo ? (
Reply
@@ -573,7 +602,7 @@ export const ComposePost = observer(function ComposePost({
autoFocus
setRichText={setRichText}
onPhotoPasted={onPhotoPasted}
- onPressPublish={onPressPublish}
+ onPressPublish={() => onPressPublish()}
onNewLink={onNewLink}
onError={setError}
accessible={true}
@@ -603,29 +632,33 @@ export const ComposePost = observer(function ComposePost({
)}
- {quote ? (
-
-
-
+
+ {quote ? (
+
+
+
+
+ {quote.uri !== initQuote?.uri && (
+ setQuote(undefined)} />
+ )}
- {quote.uri !== initQuote?.uri && (
- setQuote(undefined)} />
- )}
-
- ) : null}
- {videoPending && videoProcessingData ? (
-
- ) : (
- video && (
+ ) : null}
+ {videoUploadState.status === 'compressing' &&
+ videoUploadState.asset ? (
+
+ ) : videoUploadState.video ? (
// remove suspense when we get rid of lazy
-
+
- )
- )}
+ ) : null}
+
@@ -642,33 +675,37 @@ export const ComposePost = observer(function ComposePost({
t.atoms.border_contrast_medium,
styles.bottomBar,
]}>
-
-
- {gate('videos') && (
-
+ ) : (
+
+
+ {gate('videos') && (
+
+ )}
+
+
- )}
-
-
- {!isMobile ? (
-
- ) : null}
-
+ {!isMobile ? (
+
+ ) : null}
+
+ )}
@@ -763,7 +800,7 @@ function useAnimatedBorders() {
const topBarAnimatedStyle = useAnimatedStyle(() => {
return {
- borderBottomWidth: hairlineWidth,
+ borderBottomWidth: StyleSheet.hairlineWidth,
borderColor: interpolateColor(
hasScrolledTop.value,
[0, 1],
@@ -773,7 +810,7 @@ function useAnimatedBorders() {
})
const bottomBarAnimatedStyle = useAnimatedStyle(() => {
return {
- borderTopWidth: hairlineWidth,
+ borderTopWidth: StyleSheet.hairlineWidth,
borderColor: interpolateColor(
hasScrolledBottom.value,
[0, 1],
@@ -855,7 +892,7 @@ const styles = StyleSheet.create({
marginBottom: 8,
},
errorIcon: {
- borderWidth: hairlineWidth,
+ borderWidth: StyleSheet.hairlineWidth,
borderColor: colors.red4,
color: colors.red4,
borderRadius: 30,
@@ -891,6 +928,47 @@ const styles = StyleSheet.create({
paddingLeft: 7,
paddingRight: 16,
alignItems: 'center',
- borderTopWidth: hairlineWidth,
+ borderTopWidth: StyleSheet.hairlineWidth,
},
})
+
+function ToolbarWrapper({
+ style,
+ children,
+}: {
+ style: StyleProp
+ children: React.ReactNode
+}) {
+ if (isWeb) return children
+ return (
+
+ {children}
+
+ )
+}
+
+function VideoUploadToolbar({state}: {state: VideoUploadState}) {
+ const t = useTheme()
+
+ const progress =
+ state.status === 'compressing' || state.status === 'uploading'
+ ? state.progress
+ : state.jobStatus?.progress ?? 100
+
+ return (
+
+
+ {state.status}
+
+ )
+}
diff --git a/src/view/com/composer/Prompt.tsx b/src/view/com/composer/Prompt.tsx
index 54defb101c..20637c7e9f 100644
--- a/src/view/com/composer/Prompt.tsx
+++ b/src/view/com/composer/Prompt.tsx
@@ -9,7 +9,6 @@ import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {Text} from '../util/text/Text'
import {UserAvatar} from '../util/UserAvatar'
-import hairlineWidth = StyleSheet.hairlineWidth
export function ComposePrompt({onPressCompose}: {onPressCompose: () => void}) {
const {currentAccount} = useSession()
@@ -49,7 +48,7 @@ const styles = StyleSheet.create({
paddingBottom: 10,
flexDirection: 'row',
alignItems: 'center',
- borderTopWidth: hairlineWidth,
+ borderTopWidth: StyleSheet.hairlineWidth,
},
labelMobile: {
paddingLeft: 12,
diff --git a/src/view/com/composer/videos/VideoPreview.tsx b/src/view/com/composer/videos/VideoPreview.tsx
index b04cdf1c8b..8e2a22852d 100644
--- a/src/view/com/composer/videos/VideoPreview.tsx
+++ b/src/view/com/composer/videos/VideoPreview.tsx
@@ -17,6 +17,7 @@ export function VideoPreview({
const player = useVideoPlayer(video.uri, player => {
player.loop = true
player.play()
+ player.volume = 0
})
return (
diff --git a/src/view/com/composer/videos/VideoTranscodeProgress.tsx b/src/view/com/composer/videos/VideoTranscodeProgress.tsx
index 79407cd3ef..db58448a30 100644
--- a/src/view/com/composer/videos/VideoTranscodeProgress.tsx
+++ b/src/view/com/composer/videos/VideoTranscodeProgress.tsx
@@ -9,15 +9,15 @@ import {Text} from '#/components/Typography'
import {VideoTranscodeBackdrop} from './VideoTranscodeBackdrop'
export function VideoTranscodeProgress({
- input,
+ asset,
progress,
}: {
- input: ImagePickerAsset
+ asset: ImagePickerAsset
progress: number
}) {
const t = useTheme()
- const aspectRatio = input.width / input.height
+ const aspectRatio = asset.width / asset.height
return (
-
+
void}) {
- const {_} = useLingui()
- const [progress, setProgress] = useState(0)
-
- const {mutate, data, isPending, isError, reset, variables} = useMutation({
- mutationFn: async (asset: ImagePickerAsset) => {
- const compressed = await compressVideo(asset.uri, {
- onProgress: num => setProgress(trunc2dp(num)),
- })
-
- return compressed
- },
- onError: (e: any) => {
- // Don't log these errors in sentry, just let the user know
- if (e instanceof VideoTooLargeError) {
- Toast.show(_(msg`Videos cannot be larger than 100MB`))
- return
- }
- logger.error('Failed to compress video', {safeError: e})
- setError(_(msg`Could not compress video`))
- },
- onMutate: () => {
- setProgress(0)
- },
- })
-
- return {
- video: data,
- onSelectVideo: mutate,
- videoPending: isPending,
- videoProcessingData: variables,
- videoError: isError,
- clearVideo: reset,
- videoProcessingProgress: progress,
- }
-}
-
-function trunc2dp(num: number) {
- return Math.trunc(num * 100) / 100
-}
diff --git a/src/view/com/feeds/FeedSourceCard.tsx b/src/view/com/feeds/FeedSourceCard.tsx
index d216849c5f..68437c37a0 100644
--- a/src/view/com/feeds/FeedSourceCard.tsx
+++ b/src/view/com/feeds/FeedSourceCard.tsx
@@ -13,6 +13,7 @@ import {msg, Plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {logger} from '#/logger'
+import {shouldClickOpenNewTab} from '#/platform/urls'
import {FeedSourceInfo, useFeedSourceInfoQuery} from '#/state/queries/feed'
import {
useAddSavedFeedsMutation,
@@ -32,8 +33,6 @@ import * as Prompt from '#/components/Prompt'
import {RichText} from '#/components/RichText'
import {Text} from '../util/text/Text'
import {UserAvatar} from '../util/UserAvatar'
-import hairlineWidth = StyleSheet.hairlineWidth
-import {shouldClickOpenNewTab} from '#/platform/urls'
export function FeedSourceCard({
feedUri,
@@ -125,7 +124,7 @@ export function FeedSourceCardLoaded({
])
Toast.show(_(msg`Added to my feeds`))
} catch (e) {
- Toast.show(_(msg`There was an issue contacting your server`))
+ Toast.show(_(msg`There was an issue contacting your server`), 'xmark')
logger.error('Failed to save feed', {message: e})
}
}, [_, feed, pinOnSave, addSavedFeeds, isSaved])
@@ -138,7 +137,7 @@ export function FeedSourceCardLoaded({
// await item.unsave()
Toast.show(_(msg`Removed from my feeds`))
} catch (e) {
- Toast.show(_(msg`There was an issue contacting your server`))
+ Toast.show(_(msg`There was an issue contacting your server`), 'xmark')
logger.error('Failed to unsave feed', {message: e})
}
}, [_, removeFeed, savedFeedConfig])
@@ -209,7 +208,7 @@ export function FeedSourceCardLoaded({
styles.container,
pal.border,
style,
- {borderTopWidth: hideTopBorder ? 0 : hairlineWidth},
+ {borderTopWidth: hideTopBorder ? 0 : StyleSheet.hairlineWidth},
]}
onPress={e => {
const shouldOpenInNewTab = shouldClickOpenNewTab(e)
@@ -330,7 +329,7 @@ const styles = StyleSheet.create({
gap: 14,
},
border: {
- borderTopWidth: hairlineWidth,
+ borderTopWidth: StyleSheet.hairlineWidth,
},
headerContainer: {
flexDirection: 'row',
diff --git a/src/view/com/home/HomeHeaderLayoutMobile.tsx b/src/view/com/home/HomeHeaderLayoutMobile.tsx
index ed353cf168..e537abfaad 100644
--- a/src/view/com/home/HomeHeaderLayoutMobile.tsx
+++ b/src/view/com/home/HomeHeaderLayoutMobile.tsx
@@ -73,7 +73,7 @@ export function HomeHeaderLayoutMobile({
]}>
{IS_DEV && (
<>
-
+
>
diff --git a/src/view/com/lightbox/Lightbox.tsx b/src/view/com/lightbox/Lightbox.tsx
index 858116fdf7..fb31a0b9ff 100644
--- a/src/view/com/lightbox/Lightbox.tsx
+++ b/src/view/com/lightbox/Lightbox.tsx
@@ -66,7 +66,10 @@ function LightboxFooter({imageIndex}: {imageIndex: number}) {
const saveImageToAlbumWithToasts = React.useCallback(
async (uri: string) => {
if (!permissionResponse || permissionResponse.granted === false) {
- Toast.show(_(msg`Permission to access camera roll is required.`))
+ Toast.show(
+ _(msg`Permission to access camera roll is required.`),
+ 'info',
+ )
if (permissionResponse?.canAskAgain) {
requestPermission()
} else {
@@ -74,6 +77,7 @@ function LightboxFooter({imageIndex}: {imageIndex: number}) {
_(
msg`Permission to access camera roll was denied. Please enable it in your system settings.`,
),
+ 'xmark',
)
}
return
@@ -83,7 +87,7 @@ function LightboxFooter({imageIndex}: {imageIndex: number}) {
await saveImageToMediaLibrary({uri})
Toast.show(_(msg`Saved to your camera roll`))
} catch (e: any) {
- Toast.show(_(msg`Failed to save image: ${String(e)}`))
+ Toast.show(_(msg`Failed to save image: ${String(e)}`), 'xmark')
}
},
[permissionResponse, requestPermission, _],
diff --git a/src/view/com/lists/ListCard.tsx b/src/view/com/lists/ListCard.tsx
index 3adf5c09da..587885502b 100644
--- a/src/view/com/lists/ListCard.tsx
+++ b/src/view/com/lists/ListCard.tsx
@@ -14,7 +14,6 @@ import {RichText as RichTextCom} from '#/components/RichText'
import {Link} from '../util/Link'
import {Text} from '../util/text/Text'
import {UserAvatar} from '../util/UserAvatar'
-import hairlineWidth = StyleSheet.hairlineWidth
export const ListCard = ({
testID,
@@ -134,7 +133,7 @@ export const ListCard = ({
const styles = StyleSheet.create({
outer: {
- borderTopWidth: hairlineWidth,
+ borderTopWidth: StyleSheet.hairlineWidth,
paddingHorizontal: 6,
},
outerNoBorder: {
diff --git a/src/view/com/modals/AddAppPasswords.tsx b/src/view/com/modals/AddAppPasswords.tsx
index 92229e7b69..f7991f59bf 100644
--- a/src/view/com/modals/AddAppPasswords.tsx
+++ b/src/view/com/modals/AddAppPasswords.tsx
@@ -77,7 +77,7 @@ export function Component({}: {}) {
const onCopy = React.useCallback(() => {
if (appPassword) {
setStringAsync(appPassword)
- Toast.show(_(msg`Copied to clipboard`))
+ Toast.show(_(msg`Copied to clipboard`), 'clipboard-check')
setWasCopied(true)
}
}, [appPassword, _])
@@ -93,7 +93,7 @@ export function Component({}: {}) {
_(
msg`Please enter a name for your app password. All spaces is not allowed.`,
),
- 'times',
+ 'xmark',
)
return
}
@@ -101,13 +101,13 @@ export function Component({}: {}) {
if (name.length < 4) {
Toast.show(
_(msg`App Password names must be at least 4 characters long.`),
- 'times',
+ 'xmark',
)
return
}
if (passwords?.find(p => p.name === name)) {
- Toast.show(_(msg`This name is already in use`), 'times')
+ Toast.show(_(msg`This name is already in use`), 'xmark')
return
}
@@ -116,11 +116,11 @@ export function Component({}: {}) {
if (newPassword) {
setAppPassword(newPassword.password)
} else {
- Toast.show(_(msg`Failed to create app password.`), 'times')
+ Toast.show(_(msg`Failed to create app password.`), 'xmark')
// TODO: better error handling (?)
}
} catch (e) {
- Toast.show(_(msg`Failed to create app password.`), 'times')
+ Toast.show(_(msg`Failed to create app password.`), 'xmark')
logger.error('Failed to create app password', {message: e})
}
}
@@ -137,6 +137,7 @@ export function Component({}: {}) {
_(
msg`App Password names can only contain letters, numbers, spaces, dashes, and underscores.`,
),
+ 'xmark',
)
}
}
diff --git a/src/view/com/modals/ChangeHandle.tsx b/src/view/com/modals/ChangeHandle.tsx
index f2094ed75b..54750acf2e 100644
--- a/src/view/com/modals/ChangeHandle.tsx
+++ b/src/view/com/modals/ChangeHandle.tsx
@@ -317,7 +317,7 @@ function CustomHandleForm({
// =
const onPressCopy = React.useCallback(() => {
setStringAsync(isDNSForm ? `did=${currentAccount.did}` : currentAccount.did)
- Toast.show(_(msg`Copied to clipboard`))
+ Toast.show(_(msg`Copied to clipboard`), 'clipboard-check')
}, [currentAccount, isDNSForm, _])
const onChangeHandle = React.useCallback(
(v: string) => {
diff --git a/src/view/com/modals/InviteCodes.tsx b/src/view/com/modals/InviteCodes.tsx
index f8cebec3f6..44a6cc9ea4 100644
--- a/src/view/com/modals/InviteCodes.tsx
+++ b/src/view/com/modals/InviteCodes.tsx
@@ -150,7 +150,7 @@ function InviteCode({
const onPress = React.useCallback(() => {
setStringAsync(invite.code)
- Toast.show(_(msg`Copied to clipboard`))
+ Toast.show(_(msg`Copied to clipboard`), 'clipboard-check')
setInviteCopied(invite.code)
}, [setInviteCopied, invite, _])
diff --git a/src/view/com/modals/ListAddRemoveUsers.tsx b/src/view/com/modals/ListAddRemoveUsers.tsx
index 4715348dd8..e968252d29 100644
--- a/src/view/com/modals/ListAddRemoveUsers.tsx
+++ b/src/view/com/modals/ListAddRemoveUsers.tsx
@@ -7,32 +7,33 @@ import {
View,
} from 'react-native'
import {AppBskyActorDefs, AppBskyGraphDefs} from '@atproto/api'
-import {ScrollView, TextInput} from './util'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {Text} from '../util/text/Text'
-import {Button} from '../util/forms/Button'
-import {UserAvatar} from '../util/UserAvatar'
-import * as Toast from '../util/Toast'
-import {s, colors} from 'lib/styles'
-import {usePalette} from 'lib/hooks/usePalette'
-import {isWeb} from 'platform/detection'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {useIsKeyboardVisible} from 'lib/hooks/useIsKeyboardVisible'
-import {cleanError} from 'lib/strings/errors'
-import {sanitizeDisplayName} from 'lib/strings/display-names'
-import {sanitizeHandle} from 'lib/strings/handles'
-import {HITSLOP_20} from '#/lib/constants'
-import {Trans, msg} from '@lingui/macro'
+import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
+
+import {HITSLOP_20} from '#/lib/constants'
import {useModalControls} from '#/state/modals'
+import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
import {
- useDangerousListMembershipsQuery,
getMembership,
ListMembersip,
+ useDangerousListMembershipsQuery,
useListMembershipAddMutation,
useListMembershipRemoveMutation,
} from '#/state/queries/list-memberships'
-import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
+import {useIsKeyboardVisible} from 'lib/hooks/useIsKeyboardVisible'
+import {usePalette} from 'lib/hooks/usePalette'
+import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+import {sanitizeDisplayName} from 'lib/strings/display-names'
+import {cleanError} from 'lib/strings/errors'
+import {sanitizeHandle} from 'lib/strings/handles'
+import {colors, s} from 'lib/styles'
+import {isWeb} from 'platform/detection'
+import {Button} from '../util/forms/Button'
+import {Text} from '../util/text/Text'
+import * as Toast from '../util/Toast'
+import {UserAvatar} from '../util/UserAvatar'
+import {ScrollView, TextInput} from './util'
export const snapPoints = ['90%']
@@ -200,7 +201,7 @@ function UserResult({
onChange?.('remove', profile)
}
} catch (e) {
- Toast.show(cleanError(e))
+ Toast.show(cleanError(e), 'xmark')
} finally {
setIsProcessing(false)
}
diff --git a/src/view/com/modals/UserAddRemoveLists.tsx b/src/view/com/modals/UserAddRemoveLists.tsx
index 88506da570..f6db94ed85 100644
--- a/src/view/com/modals/UserAddRemoveLists.tsx
+++ b/src/view/com/modals/UserAddRemoveLists.tsx
@@ -29,7 +29,6 @@ import {Button} from '../util/forms/Button'
import {Text} from '../util/text/Text'
import * as Toast from '../util/Toast'
import {UserAvatar} from '../util/UserAvatar'
-import hairlineWidth = StyleSheet.hairlineWidth
export const snapPoints = ['fullscreen']
@@ -63,7 +62,7 @@ export function Component({
return [pal.border, {height: screenHeight / 1.5}]
}
- return [pal.border, {flex: 1, borderTopWidth: hairlineWidth}]
+ return [pal.border, {flex: 1, borderTopWidth: StyleSheet.hairlineWidth}]
}, [pal.border, screenHeight])
return (
@@ -166,7 +165,7 @@ function ListItem({
onRemove?.(list.uri)
}
} catch (e) {
- Toast.show(cleanError(e))
+ Toast.show(cleanError(e), 'xmark')
} finally {
setIsProcessing(false)
}
@@ -188,7 +187,7 @@ function ListItem({
style={[
styles.listItem,
pal.border,
- index !== 0 && {borderTopWidth: hairlineWidth},
+ index !== 0 && {borderTopWidth: StyleSheet.hairlineWidth},
]}>
@@ -248,7 +247,7 @@ const styles = StyleSheet.create({
gap: 10,
paddingTop: 10,
paddingBottom: isAndroid ? 10 : 0,
- borderTopWidth: hairlineWidth,
+ borderTopWidth: StyleSheet.hairlineWidth,
},
footerBtn: {
paddingHorizontal: 24,
diff --git a/src/view/com/notifications/Feed.tsx b/src/view/com/notifications/Feed.tsx
index e2f12e84f1..82755de1d3 100644
--- a/src/view/com/notifications/Feed.tsx
+++ b/src/view/com/notifications/Feed.tsx
@@ -24,7 +24,6 @@ import {NotificationFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceho
import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn'
import {CenteredView} from '#/view/com/util/Views'
import {FeedItem} from './FeedItem'
-import hairlineWidth = StyleSheet.hairlineWidth
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
@@ -35,11 +34,13 @@ export function Feed({
onPressTryAgain,
onScrolledDownChange,
ListHeaderComponent,
+ overridePriorityNotifications,
}: {
scrollElRef?: ListRef
onPressTryAgain?: () => void
onScrolledDownChange: (isScrolledDown: boolean) => void
ListHeaderComponent?: () => JSX.Element
+ overridePriorityNotifications?: boolean
}) {
const initialNumToRender = useInitialNumToRender()
@@ -59,7 +60,10 @@ export function Feed({
hasNextPage,
isFetchingNextPage,
fetchNextPage,
- } = useNotificationFeedQuery({enabled: !!moderationOpts})
+ } = useNotificationFeedQuery({
+ enabled: !!moderationOpts,
+ overridePriorityNotifications,
+ })
const isEmpty = !isFetching && !data?.pages[0]?.items.length
const items = React.useMemo(() => {
@@ -132,7 +136,7 @@ export function Feed({
diff --git a/src/view/com/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx
index d31962ff35..520a059ae2 100644
--- a/src/view/com/notifications/FeedItem.tsx
+++ b/src/view/com/notifications/FeedItem.tsx
@@ -20,20 +20,28 @@ import {
import {AtUri} from '@atproto/api'
import {msg, plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
+import {useNavigation} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
import {useGate} from '#/lib/statsig/statsig'
+import {parseTenorGif} from '#/lib/strings/embed-player'
+import {logger} from '#/logger'
import {FeedNotification} from '#/state/queries/notifications/feed'
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
import {usePalette} from 'lib/hooks/usePalette'
import {makeProfileLink} from 'lib/routes/links'
+import {NavigationProp} from 'lib/routes/types'
+import {forceLTR} from 'lib/strings/bidi'
import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles'
import {niceDate} from 'lib/strings/time'
import {colors, s} from 'lib/styles'
import {isWeb} from 'platform/detection'
+import {DM_SERVICE_HEADERS} from 'state/queries/messages/const'
import {precacheProfile} from 'state/queries/profile'
+import {useAgent} from 'state/session'
import {atoms as a, useTheme} from '#/alf'
+import {Button, ButtonText} from '#/components/Button'
import {
ChevronBottom_Stroke2_Corner0_Rounded as ChevronDownIcon,
ChevronTop_Stroke2_Corner0_Rounded as ChevronUpIcon,
@@ -41,8 +49,10 @@ import {
import {Heart2_Filled_Stroke2_Corner0_Rounded as HeartIconFilled} from '#/components/icons/Heart2'
import {PersonPlus_Filled_Stroke2_Corner0_Rounded as PersonPlusIcon} from '#/components/icons/Person'
import {Repost_Stroke2_Corner2_Rounded as RepostIcon} from '#/components/icons/Repost'
+import {StarterPack} from '#/components/icons/StarterPack'
import {Link as NewLink} from '#/components/Link'
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
+import {Notification as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
import {FeedSourceCard} from '../feeds/FeedSourceCard'
import {Post} from '../post/Post'
import {ImageHorzList} from '../util/images/ImageHorzList'
@@ -52,19 +62,6 @@ import {Text} from '../util/text/Text'
import {TimeElapsed} from '../util/TimeElapsed'
import {PreviewableUserAvatar, UserAvatar} from '../util/UserAvatar'
-import hairlineWidth = StyleSheet.hairlineWidth
-import {useNavigation} from '@react-navigation/native'
-
-import {parseTenorGif} from '#/lib/strings/embed-player'
-import {logger} from '#/logger'
-import {NavigationProp} from 'lib/routes/types'
-import {forceLTR} from 'lib/strings/bidi'
-import {DM_SERVICE_HEADERS} from 'state/queries/messages/const'
-import {useAgent} from 'state/session'
-import {Button, ButtonText} from '#/components/Button'
-import {StarterPack} from '#/components/icons/StarterPack'
-import {Notification as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
-
const MAX_AUTHORS = 5
const EXPANDED_AUTHOR_EL_HEIGHT = 35
@@ -171,7 +168,6 @@ let FeedItem = ({
)
}
- let isFollowBack = false
let action = ''
let icon = (
1 ? formatCount(authors.length - 1) : ''
+ const formattedCount =
+ authors.length > 1 ? formatCount(authors.length - 1) : ''
+ const firstAuthorName = sanitizeDisplayName(
+ authors[0].profile.displayName || authors[0].profile.handle,
+ )
+ const niceTimestamp = niceDate(item.notification.indexedAt)
+ const a11yLabelUsers =
+ authors.length > 1
+ ? _(msg` and `) +
+ plural(authors.length - 1, {
+ one: `${formattedCount} other`,
+ other: `${formattedCount} others`,
+ })
+ : ''
+ const a11yLabel = `${firstAuthorName}${a11yLabelUsers} ${action} ${niceTimestamp}`
+
return (
1
@@ -272,19 +284,18 @@ let FeedItem = ({
visible={!isAuthorsExpanded}
authors={authors}
onToggleAuthorsExpanded={onToggleAuthorsExpanded}
- showDmButton={item.type === 'starterpack-joined' || isFollowBack}
+ showDmButton={item.type === 'starterpack-joined'}
/>
-
+
{authors.length > 1 ? (
@@ -306,7 +317,7 @@ let FeedItem = ({
{({timeElapsed}) => (
+ title={niceTimestamp}>
{' ' + timeElapsed}
)}
@@ -458,7 +469,6 @@ function CondensedAuthorsList({
profile={authors[0].profile}
moderation={authors[0].moderation.ui('avatar')}
type={authors[0].profile.associated?.labeler ? 'labeler' : 'user'}
- accessible={false}
/>
{showDmButton ? : null}
@@ -476,7 +486,6 @@ function CondensedAuthorsList({
profile={author.profile}
moderation={author.moderation.ui('avatar')}
type={author.profile.associated?.labeler ? 'labeler' : 'user'}
- accessible={false}
/>
))}
diff --git a/src/view/com/pager/TabBar.tsx b/src/view/com/pager/TabBar.tsx
index 1c83ecd6e2..4801ae5e32 100644
--- a/src/view/com/pager/TabBar.tsx
+++ b/src/view/com/pager/TabBar.tsx
@@ -7,7 +7,6 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {PressableWithHover} from '../util/PressableWithHover'
import {Text} from '../util/text/Text'
import {DraggableScrollView} from './DraggableScrollView'
-import hairlineWidth = StyleSheet.hairlineWidth
export interface TabBarProps {
testID?: string
@@ -208,6 +207,6 @@ const mobileStyles = StyleSheet.create({
left: 0,
right: 0,
top: '100%',
- borderBottomWidth: hairlineWidth,
+ borderBottomWidth: StyleSheet.hairlineWidth,
},
})
diff --git a/src/view/com/post-thread/PostThreadFollowBtn.tsx b/src/view/com/post-thread/PostThreadFollowBtn.tsx
index 1f70f41c4a..6b6316eec2 100644
--- a/src/view/com/post-thread/PostThreadFollowBtn.tsx
+++ b/src/view/com/post-thread/PostThreadFollowBtn.tsx
@@ -96,7 +96,7 @@ function PostThreadFollowBtnLoaded({
} catch (e: any) {
if (e?.name !== 'AbortError') {
logger.error('Failed to follow', {message: String(e)})
- Toast.show(_(msg`There was an issue! ${e.toString()}`))
+ Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
}
}
})
@@ -108,7 +108,7 @@ function PostThreadFollowBtnLoaded({
} catch (e: any) {
if (e?.name !== 'AbortError') {
logger.error('Failed to unfollow', {message: String(e)})
- Toast.show(_(msg`There was an issue! ${e.toString()}`))
+ Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
}
}
})
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
index 4e6ed27bed..63dac300a8 100644
--- a/src/view/com/post-thread/PostThreadItem.tsx
+++ b/src/view/com/post-thread/PostThreadItem.tsx
@@ -44,7 +44,6 @@ import {PostEmbeds} from '../util/post-embeds'
import {PostMeta} from '../util/PostMeta'
import {Text} from '../util/text/Text'
import {PreviewableUserAvatar} from '../util/UserAvatar'
-import hairlineWidth = StyleSheet.hairlineWidth
export function PostThreadItem({
post,
@@ -604,7 +603,7 @@ function PostOuterWrapper({
{
flexDirection: 'row',
paddingHorizontal: isMobile ? 10 : 6,
- borderTopWidth: depth === 1 ? hairlineWidth : 0,
+ borderTopWidth: depth === 1 ? StyleSheet.hairlineWidth : 0,
},
]}>
{Array.from(Array(depth - 1)).map((_, n: number) => (
@@ -699,7 +698,7 @@ function getThreadAuthor(
const styles = StyleSheet.create({
outer: {
- borderTopWidth: hairlineWidth,
+ borderTopWidth: StyleSheet.hairlineWidth,
paddingLeft: 8,
},
outerHighlighted: {
@@ -709,7 +708,7 @@ const styles = StyleSheet.create({
paddingRight: 8,
},
outerHighlightedRoot: {
- borderTopWidth: hairlineWidth,
+ borderTopWidth: StyleSheet.hairlineWidth,
paddingTop: 16,
},
noTopBorder: {
@@ -761,8 +760,8 @@ const styles = StyleSheet.create({
expandedInfo: {
flexDirection: 'row',
padding: 10,
- borderTopWidth: hairlineWidth,
- borderBottomWidth: hairlineWidth,
+ borderTopWidth: StyleSheet.hairlineWidth,
+ borderBottomWidth: StyleSheet.hairlineWidth,
marginTop: 5,
marginBottom: 10,
},
diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx
index a05339d4dc..ade0e7f10f 100644
--- a/src/view/com/post/Post.tsx
+++ b/src/view/com/post/Post.tsx
@@ -37,7 +37,6 @@ import {PostMeta} from '../util/PostMeta'
import {Text} from '../util/text/Text'
import {PreviewableUserAvatar} from '../util/UserAvatar'
import {UserInfoText} from '../util/UserInfoText'
-import hairlineWidth = StyleSheet.hairlineWidth
export function Post({
post,
@@ -155,7 +154,7 @@ function PostInner({
style={[
styles.outer,
pal.border,
- !hideTopBorder && {borderTopWidth: hairlineWidth},
+ !hideTopBorder && {borderTopWidth: StyleSheet.hairlineWidth},
style,
]}
onBeforePress={onBeforePress}>
diff --git a/src/view/com/posts/FeedErrorMessage.tsx b/src/view/com/posts/FeedErrorMessage.tsx
index 2b77739d32..d123774619 100644
--- a/src/view/com/posts/FeedErrorMessage.tsx
+++ b/src/view/com/posts/FeedErrorMessage.tsx
@@ -144,6 +144,7 @@ function FeedgenErrorMessage({
_l(
msgLingui`There was an an issue removing this feed. Please check your internet connection and try again.`,
),
+ 'exclamation-circle',
)
logger.error('Failed to remove feed', {message: err})
}
diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx
index a59eeea52e..9ddc54a989 100644
--- a/src/view/com/posts/FeedItem.tsx
+++ b/src/view/com/posts/FeedItem.tsx
@@ -16,8 +16,10 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
+import {useGate} from '#/lib/statsig/statsig'
import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow'
import {useFeedFeedbackContext} from '#/state/feed-feedback'
+import {useSession} from '#/state/session'
import {useComposerControls} from '#/state/shell/composer'
import {isReasonFeedSource, ReasonFeedSource} from 'lib/api/feed/types'
import {MAX_POST_LINES} from 'lib/constants'
@@ -29,6 +31,7 @@ import {countLines} from 'lib/strings/helpers'
import {s} from 'lib/styles'
import {precacheProfile} from 'state/queries/profile'
import {atoms as a} from '#/alf'
+import {Repost_Stroke2_Corner2_Rounded as Repost} from '#/components/icons/Repost'
import {ContentHider} from '#/components/moderation/ContentHider'
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
import {RichText} from '#/components/RichText'
@@ -38,17 +41,19 @@ import {FeedNameText} from '../util/FeedInfoText'
import {Link, TextLink, TextLinkOnWebOnly} from '../util/Link'
import {PostCtrls} from '../util/post-ctrls/PostCtrls'
import {PostEmbeds} from '../util/post-embeds'
+import {VideoEmbed} from '../util/post-embeds/VideoEmbed'
import {PostMeta} from '../util/PostMeta'
import {Text} from '../util/text/Text'
import {PreviewableUserAvatar} from '../util/UserAvatar'
import {AviFollowButton} from './AviFollowButton'
-import hairlineWidth = StyleSheet.hairlineWidth
-import {useSession} from '#/state/session'
-import {Repost_Stroke2_Corner2_Rounded as Repost} from '#/components/icons/Repost'
interface FeedItemProps {
record: AppBskyFeedPost.Record
- reason: AppBskyFeedDefs.ReasonRepost | ReasonFeedSource | undefined
+ reason:
+ | AppBskyFeedDefs.ReasonRepost
+ | ReasonFeedSource
+ | {[k: string]: unknown; $type: string}
+ | undefined
moderation: ModerationDecision
parentAuthor: AppBskyActorDefs.ProfileViewBasic | undefined
showReplyTo: boolean
@@ -132,6 +137,8 @@ let FeedItemInner = ({
const {openComposer} = useComposerControls()
const pal = usePalette('default')
const {_} = useLingui()
+ const gate = useGate()
+
const href = useMemo(() => {
const urip = new AtUri(post.uri)
return makeProfileLink(post.author, 'post', urip.rkey)
@@ -197,7 +204,8 @@ let FeedItemInner = ({
isThreadLastChild || (!isThreadChild && !isThreadParent)
? 8
: undefined,
- borderTopWidth: hideTopBorder || isThreadChild ? 0 : hairlineWidth,
+ borderTopWidth:
+ hideTopBorder || isThreadChild ? 0 : StyleSheet.hairlineWidth,
},
]
@@ -337,9 +345,11 @@ let FeedItemInner = ({
postHref={href}
onOpenAuthor={onOpenAuthor}
/>
- {!isThreadChild && showReplyTo && parentAuthor && (
-
- )}
+ {!isThreadChild &&
+ showReplyTo &&
+ (parentAuthor || isParentBlocked) && (
+
+ )}
+ {__DEV__ && gate('videos') && (
+
+ )}
Reply to a blocked post
+ } else if (profile != null) {
+ const isMe = profile.did === currentAccount?.did
+ if (isMe) {
+ label = Reply to you
+ } else {
+ label = (
+
+ Reply to{' '}
+
+
+
+
+ )
+ }
+ }
+
+ if (!label) {
+ // Should not happen.
+ return null
+ }
return (
@@ -450,29 +497,7 @@ function ReplyToLabel({
style={[pal.textLight, s.mr2]}
lineHeight={1.2}
numberOfLines={1}>
- {isMe ? (
- Reply to you
- ) : blocked ? (
- Reply to a blocked post
- ) : (
-
- Reply to{' '}
-
-
-
-
- )}
+ {label}
)
diff --git a/src/view/com/posts/FeedShutdownMsg.tsx b/src/view/com/posts/FeedShutdownMsg.tsx
index 47f8941e2a..f12ecf12b6 100644
--- a/src/view/com/posts/FeedShutdownMsg.tsx
+++ b/src/view/com/posts/FeedShutdownMsg.tsx
@@ -51,6 +51,7 @@ export function FeedShutdownMsg({feedUri}: {feedUri: string}) {
_(
msg`There was an an issue updating your feeds, please check your internet connection and try again.`,
),
+ 'exclamation-circle',
)
logger.error('Failed up update feeds', {message: err})
}
@@ -69,6 +70,7 @@ export function FeedShutdownMsg({feedUri}: {feedUri: string}) {
_(
msg`There was an an issue updating your feeds, please check your internet connection and try again.`,
),
+ 'exclamation-circle',
)
logger.error('Failed up update feeds', {message: err})
}
@@ -97,6 +99,7 @@ export function FeedShutdownMsg({feedUri}: {feedUri: string}) {
This feed is no longer online. We are showing{' '}
Discover
diff --git a/src/view/com/posts/FeedSlice.tsx b/src/view/com/posts/FeedSlice.tsx
index 3e08f253cf..b8c9a1f2c7 100644
--- a/src/view/com/posts/FeedSlice.tsx
+++ b/src/view/com/posts/FeedSlice.tsx
@@ -56,12 +56,12 @@ let FeedSlice = ({
record={slice.items[last].record}
reason={slice.items[last].reason}
feedContext={slice.items[last].feedContext}
- parentAuthor={slice.items[2].parentAuthor}
+ parentAuthor={slice.items[last].parentAuthor}
showReplyTo={false}
moderation={slice.items[last].moderation}
isThreadParent={isThreadParentAt(slice.items, last)}
isThreadChild={isThreadChildAt(slice.items, last)}
- isParentBlocked={slice.items[2].isParentBlocked}
+ isParentBlocked={slice.items[last].isParentBlocked}
isThreadLastChild
/>
>
diff --git a/src/view/com/profile/FollowButton.tsx b/src/view/com/profile/FollowButton.tsx
index 8e63da85b4..42adea3cfc 100644
--- a/src/view/com/profile/FollowButton.tsx
+++ b/src/view/com/profile/FollowButton.tsx
@@ -33,7 +33,7 @@ export function FollowButton({
await queueFollow()
} catch (e: any) {
if (e?.name !== 'AbortError') {
- Toast.show(_(msg`An issue occurred, please try again.`))
+ Toast.show(_(msg`An issue occurred, please try again.`), 'xmark')
}
}
}
@@ -43,7 +43,7 @@ export function FollowButton({
await queueUnfollow()
} catch (e: any) {
if (e?.name !== 'AbortError') {
- Toast.show(_(msg`An issue occurred, please try again.`))
+ Toast.show(_(msg`An issue occurred, please try again.`), 'xmark')
}
}
}
diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx
index 9458fbaf8e..fd32e37a42 100644
--- a/src/view/com/profile/ProfileCard.tsx
+++ b/src/view/com/profile/ProfileCard.tsx
@@ -23,12 +23,11 @@ import {
KnownFollowers,
shouldShowKnownFollowers,
} from '#/components/KnownFollowers'
+import * as Pills from '#/components/Pills'
import {Link} from '../util/Link'
import {Text} from '../util/text/Text'
import {PreviewableUserAvatar} from '../util/UserAvatar'
import {FollowButton} from './FollowButton'
-import hairlineWidth = StyleSheet.hairlineWidth
-import * as Pills from '#/components/Pills'
export function ProfileCard({
testID,
@@ -217,7 +216,7 @@ export function ProfileCardWithFollowBtn({
const styles = StyleSheet.create({
outer: {
- borderTopWidth: hairlineWidth,
+ borderTopWidth: StyleSheet.hairlineWidth,
paddingHorizontal: 6,
paddingVertical: 4,
},
diff --git a/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx b/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx
index bb5ad2a636..c7df4d75be 100644
--- a/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx
+++ b/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx
@@ -185,7 +185,7 @@ function SuggestedFollow({
await queueFollow()
} catch (e: any) {
if (e?.name !== 'AbortError') {
- Toast.show(_(msg`An issue occurred, please try again.`))
+ Toast.show(_(msg`An issue occurred, please try again.`), 'xmark')
}
}
}, [queueFollow, track, _])
@@ -195,7 +195,7 @@ function SuggestedFollow({
await queueUnfollow()
} catch (e: any) {
if (e?.name !== 'AbortError') {
- Toast.show(_(msg`An issue occurred, please try again.`))
+ Toast.show(_(msg`An issue occurred, please try again.`), 'xmark')
}
}
}, [queueUnfollow, _])
diff --git a/src/view/com/profile/ProfileMenu.tsx b/src/view/com/profile/ProfileMenu.tsx
index f5e050d707..451c076742 100644
--- a/src/view/com/profile/ProfileMenu.tsx
+++ b/src/view/com/profile/ProfileMenu.tsx
@@ -108,7 +108,7 @@ let ProfileMenu = ({
} catch (e: any) {
if (e?.name !== 'AbortError') {
logger.error('Failed to unmute account', {message: e})
- Toast.show(_(msg`There was an issue! ${e.toString()}`))
+ Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
}
}
} else {
@@ -119,7 +119,7 @@ let ProfileMenu = ({
} catch (e: any) {
if (e?.name !== 'AbortError') {
logger.error('Failed to mute account', {message: e})
- Toast.show(_(msg`There was an issue! ${e.toString()}`))
+ Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
}
}
}
@@ -134,7 +134,7 @@ let ProfileMenu = ({
} catch (e: any) {
if (e?.name !== 'AbortError') {
logger.error('Failed to unblock account', {message: e})
- Toast.show(_(msg`There was an issue! ${e.toString()}`))
+ Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
}
}
} else {
@@ -145,7 +145,7 @@ let ProfileMenu = ({
} catch (e: any) {
if (e?.name !== 'AbortError') {
logger.error('Failed to block account', {message: e})
- Toast.show(_(msg`There was an issue! ${e.toString()}`))
+ Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
}
}
}
@@ -159,7 +159,7 @@ let ProfileMenu = ({
} catch (e: any) {
if (e?.name !== 'AbortError') {
logger.error('Failed to follow account', {message: e})
- Toast.show(_(msg`There was an issue! ${e.toString()}`))
+ Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
}
}
}, [_, queueFollow, track])
@@ -172,7 +172,7 @@ let ProfileMenu = ({
} catch (e: any) {
if (e?.name !== 'AbortError') {
logger.error('Failed to unfollow account', {message: e})
- Toast.show(_(msg`There was an issue! ${e.toString()}`))
+ Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
}
}
}, [_, queueUnfollow, track])
diff --git a/src/view/com/profile/ProfileSubpageHeader.tsx b/src/view/com/profile/ProfileSubpageHeader.tsx
index ac5febcda1..e07acef281 100644
--- a/src/view/com/profile/ProfileSubpageHeader.tsx
+++ b/src/view/com/profile/ProfileSubpageHeader.tsx
@@ -15,15 +15,13 @@ import {makeProfileLink} from 'lib/routes/links'
import {NavigationProp} from 'lib/routes/types'
import {sanitizeHandle} from 'lib/strings/handles'
import {isNative} from 'platform/detection'
+import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
+import {StarterPack} from '#/components/icons/StarterPack'
import {TextLink} from '../util/Link'
import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
import {Text} from '../util/text/Text'
import {UserAvatar, UserAvatarType} from '../util/UserAvatar'
import {CenteredView} from '../util/Views'
-import hairlineWidth = StyleSheet.hairlineWidth
-
-import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
-import {StarterPack} from '#/components/icons/StarterPack'
export function ProfileSubpageHeader({
isLoading,
@@ -84,7 +82,7 @@ export function ProfileSubpageHeader({
{
flexDirection: 'row',
alignItems: 'center',
- borderBottomWidth: hairlineWidth,
+ borderBottomWidth: StyleSheet.hairlineWidth,
paddingTop: isNative ? 0 : 8,
paddingBottom: 8,
paddingHorizontal: isMobile ? 12 : 14,
diff --git a/src/view/com/util/LoadingPlaceholder.tsx b/src/view/com/util/LoadingPlaceholder.tsx
index adeefd4ddb..6e75e88ca6 100644
--- a/src/view/com/util/LoadingPlaceholder.tsx
+++ b/src/view/com/util/LoadingPlaceholder.tsx
@@ -17,7 +17,6 @@ import {
Heart2_Stroke2_Corner0_Rounded as HeartIconOutline,
} from '#/components/icons/Heart2'
import {Repost_Stroke2_Corner2_Rounded as Repost} from '#/components/icons/Repost'
-import hairlineWidth = StyleSheet.hairlineWidth
export function LoadingPlaceholder({
width,
@@ -236,7 +235,7 @@ export function FeedLoadingPlaceholder({
{
paddingHorizontal: 12,
paddingVertical: 18,
- borderTopWidth: showTopBorder ? hairlineWidth : 0,
+ borderTopWidth: showTopBorder ? StyleSheet.hairlineWidth : 0,
},
pal.border,
style,
diff --git a/src/view/com/util/Toast.tsx b/src/view/com/util/Toast.tsx
index 5462505e1e..f7c6bc2c91 100644
--- a/src/view/com/util/Toast.tsx
+++ b/src/view/com/util/Toast.tsx
@@ -1,87 +1,93 @@
+import React, {useEffect, useState} from 'react'
+import {View} from 'react-native'
+import Animated, {FadeInUp, FadeOutUp} from 'react-native-reanimated'
import RootSiblings from 'react-native-root-siblings'
-import React from 'react'
-import {Animated, StyleSheet, View} from 'react-native'
-import {Props as FontAwesomeProps} from '@fortawesome/react-native-fontawesome'
-import {Text} from './text/Text'
-import {colors} from 'lib/styles'
-import {useTheme} from 'lib/ThemeContext'
-import {usePalette} from 'lib/hooks/usePalette'
-import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
+import {useSafeAreaInsets} from 'react-native-safe-area-context'
+import {
+ FontAwesomeIcon,
+ Props as FontAwesomeProps,
+} from '@fortawesome/react-native-fontawesome'
+
+import {atoms as a, useTheme} from '#/alf'
+import {Text} from '#/components/Typography'
import {IS_TEST} from '#/env'
-const TIMEOUT = 4e3
+const TIMEOUT = 2e3
export function show(
message: string,
- _icon: FontAwesomeProps['icon'] = 'check',
+ icon: FontAwesomeProps['icon'] = 'check',
) {
if (IS_TEST) return
- const item = new RootSiblings()
+ const item = new RootSiblings()
+ // timeout has some leeway to account for the animation
setTimeout(() => {
item.destroy()
- }, TIMEOUT)
+ }, TIMEOUT + 1e3)
}
-function Toast({message}: {message: string}) {
- const theme = useTheme()
- const pal = usePalette('default')
- const interp = useAnimatedValue(0)
+function Toast({
+ message,
+ icon,
+}: {
+ message: string
+ icon: FontAwesomeProps['icon']
+}) {
+ const t = useTheme()
+ const {top} = useSafeAreaInsets()
- React.useEffect(() => {
- Animated.sequence([
- Animated.timing(interp, {
- toValue: 1,
- duration: 150,
- useNativeDriver: true,
- }),
- Animated.delay(3700),
- Animated.timing(interp, {
- toValue: 0,
- duration: 150,
- useNativeDriver: true,
- }),
- ]).start()
- })
+ // for the exit animation to work on iOS the animated component
+ // must not be the root component
+ // so we need to wrap it in a view and unmount the toast ahead of time
+ const [alive, setAlive] = useState(true)
+
+ useEffect(() => {
+ setTimeout(() => {
+ setAlive(false)
+ }, TIMEOUT)
+ }, [])
- const opacityStyle = {opacity: interp}
return (
-
-
-
- {message}
-
-
+
+ {alive && (
+
+
+
+
+
+ {message}
+
+
+ )}
)
}
-
-const styles = StyleSheet.create({
- container: {
- position: 'absolute',
- top: 60,
- left: 0,
- right: 0,
- alignItems: 'center',
- },
- toast: {
- paddingHorizontal: 18,
- paddingVertical: 10,
- borderRadius: 24,
- borderWidth: 1,
- shadowColor: '#000',
- shadowOpacity: 0.1,
- shadowOffset: {width: 0, height: 4},
- marginHorizontal: 6,
- },
- toastDark: {
- backgroundColor: colors.gray6,
- shadowOpacity: 0.5,
- },
-})
diff --git a/src/view/com/util/Toast.web.tsx b/src/view/com/util/Toast.web.tsx
index d5a8435412..1f9eb479b6 100644
--- a/src/view/com/util/Toast.web.tsx
+++ b/src/view/com/util/Toast.web.tsx
@@ -2,7 +2,7 @@
* Note: the dataSet properties are used to leverage custom CSS in public/index.html
*/
-import React, {useState, useEffect} from 'react'
+import React, {useEffect, useState} from 'react'
import {StyleSheet, Text, View} from 'react-native'
import {
FontAwesomeIcon,
@@ -39,7 +39,7 @@ export const ToastContainer: React.FC = ({}) => {
{activeToast.text}
@@ -79,6 +79,7 @@ const styles = StyleSheet.create({
},
icon: {
color: '#fff',
+ flexShrink: 0,
},
text: {
color: '#fff',
diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx
index e9aa625806..8862bd0e4a 100644
--- a/src/view/com/util/UserAvatar.tsx
+++ b/src/view/com/util/UserAvatar.tsx
@@ -8,6 +8,7 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
+import {useGate} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {usePalette} from 'lib/hooks/usePalette'
import {
@@ -54,7 +55,6 @@ interface PreviewableUserAvatarProps extends BaseUserAvatarProps {
profile: AppBskyActorDefs.ProfileViewBasic
disableHoverCard?: boolean
onBeforePress?: () => void
- accessible?: boolean
}
const BLUR_AMOUNT = isWeb ? 5 : 100
@@ -179,6 +179,7 @@ let UserAvatar = ({
const pal = usePalette('default')
const backgroundColor = pal.colors.backgroundLight
const finalShape = overrideShape ?? (type === 'user' ? 'circle' : 'square')
+ const gate = useGate()
const aviStyle = useMemo(() => {
if (finalShape === 'square') {
@@ -221,7 +222,12 @@ let UserAvatar = ({
testID="userAvatarImage"
style={aviStyle}
resizeMode="cover"
- source={{uri: avatar}}
+ source={{
+ uri: hackModifyThumbnailPath(
+ avatar,
+ size < 90 && gate('small_avi_thumb'),
+ ),
+ }}
blurRadius={moderation?.blur ? BLUR_AMOUNT : 0}
/>
) : (
@@ -229,7 +235,12 @@ let UserAvatar = ({
testID="userAvatarImage"
style={aviStyle}
contentFit="cover"
- source={{uri: avatar}}
+ source={{
+ uri: hackModifyThumbnailPath(
+ avatar,
+ size < 90 && gate('small_avi_thumb'),
+ ),
+ }}
blurRadius={moderation?.blur ? BLUR_AMOUNT : 0}
/>
)}
@@ -400,7 +411,6 @@ let PreviewableUserAvatar = ({
profile,
disableHoverCard,
onBeforePress,
- accessible = true,
...rest
}: PreviewableUserAvatarProps): React.ReactNode => {
const {_} = useLingui()
@@ -414,12 +424,8 @@ let PreviewableUserAvatar = ({
return (
@@ -243,7 +242,7 @@ const styles = StyleSheet.create({
marginRight: 'auto',
},
border: {
- borderBottomWidth: hairlineWidth,
+ borderBottomWidth: StyleSheet.hairlineWidth,
},
titleContainer: {
marginLeft: 'auto',
diff --git a/src/view/com/util/Views.web.tsx b/src/view/com/util/Views.web.tsx
index 21998bfbdd..ed18c981a6 100644
--- a/src/view/com/util/Views.web.tsx
+++ b/src/view/com/util/Views.web.tsx
@@ -26,7 +26,6 @@ import Animated from 'react-native-reanimated'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {addStyle} from 'lib/styles'
-import hairlineWidth = StyleSheet.hairlineWidth
interface AddedProps {
desktopFixedHeight?: boolean | number
@@ -50,8 +49,8 @@ export const CenteredView = React.forwardRef(function CenteredView(
}
if (sideBorders) {
style = addStyle(style, {
- borderLeftWidth: hairlineWidth,
- borderRightWidth: hairlineWidth,
+ borderLeftWidth: StyleSheet.hairlineWidth,
+ borderRightWidth: StyleSheet.hairlineWidth,
})
style = addStyle(style, pal.border)
}
@@ -163,8 +162,8 @@ export const ScrollView = React.forwardRef(function ScrollViewImpl(
const styles = StyleSheet.create({
contentContainer: {
- borderLeftWidth: hairlineWidth,
- borderRightWidth: hairlineWidth,
+ borderLeftWidth: StyleSheet.hairlineWidth,
+ borderRightWidth: StyleSheet.hairlineWidth,
// @ts-ignore web only
minHeight: '100vh',
},
diff --git a/src/view/com/util/forms/PostDropdownBtn.tsx b/src/view/com/util/forms/PostDropdownBtn.tsx
index 45e00e58c7..6c82ec8cc2 100644
--- a/src/view/com/util/forms/PostDropdownBtn.tsx
+++ b/src/view/com/util/forms/PostDropdownBtn.tsx
@@ -149,7 +149,7 @@ let PostDropdownBtn = ({
},
e => {
logger.error('Failed to delete post', {message: e})
- Toast.show(_(msg`Failed to delete post, please try again`))
+ Toast.show(_(msg`Failed to delete post, please try again`), 'xmark')
},
)
}, [
@@ -177,7 +177,10 @@ let PostDropdownBtn = ({
} catch (e: any) {
if (e?.name !== 'AbortError') {
logger.error('Failed to toggle thread mute', {message: e})
- Toast.show(_(msg`Failed to toggle thread mute, please try again`))
+ Toast.show(
+ _(msg`Failed to toggle thread mute, please try again`),
+ 'xmark',
+ )
}
}
}, [isThreadMuted, unmuteThread, _, muteThread])
@@ -186,7 +189,7 @@ let PostDropdownBtn = ({
const str = richTextToString(richText, true)
Clipboard.setStringAsync(str)
- Toast.show(_(msg`Copied to clipboard`))
+ Toast.show(_(msg`Copied to clipboard`), 'clipboard-check')
}, [_, richText])
const onPressTranslate = React.useCallback(() => {
diff --git a/src/view/com/util/load-latest/LoadLatestBtn.tsx b/src/view/com/util/load-latest/LoadLatestBtn.tsx
index 9de03a4f33..2310b1f271 100644
--- a/src/view/com/util/load-latest/LoadLatestBtn.tsx
+++ b/src/view/com/util/load-latest/LoadLatestBtn.tsx
@@ -13,7 +13,6 @@ import {clamp} from '#/lib/numbers'
import {colors} from '#/lib/styles'
import {isWeb} from '#/platform/detection'
import {useSession} from '#/state/session'
-import hairlineWidth = StyleSheet.hairlineWidth
const AnimatedTouchableOpacity =
Animated.createAnimatedComponent(TouchableOpacity)
@@ -74,7 +73,7 @@ const styles = StyleSheet.create({
// @ts-ignore 'fixed' is web only -prf
position: isWeb ? 'fixed' : 'absolute',
left: 18,
- borderWidth: hairlineWidth,
+ borderWidth: StyleSheet.hairlineWidth,
width: 52,
height: 52,
borderRadius: 26,
diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx
index c3af3a61e8..478b8f0f89 100644
--- a/src/view/com/util/post-ctrls/PostCtrls.tsx
+++ b/src/view/com/util/post-ctrls/PostCtrls.tsx
@@ -354,7 +354,7 @@ let PostCtrls = ({
onPress={e => {
e.stopPropagation()
Clipboard.setStringAsync(feedContext)
- Toast.show(_(msg`Copied to clipboard`))
+ Toast.show(_(msg`Copied to clipboard`), 'clipboard-check')
}}>
void
+} | null>(null)
+
+export function ActiveVideoProvider({children}: {children: React.ReactNode}) {
+ const [activeViewId, setActiveViewId] = useState(null)
+ const [source, setSource] = useState(null)
+
+ const value = useMemo(
+ () => ({
+ activeViewId,
+ setActiveView: (viewId: string, src: string) => {
+ setActiveViewId(viewId)
+ setSource(src)
+ },
+ }),
+ [activeViewId],
+ )
+
+ return (
+
+
+ {children}
+
+
+ )
+}
+
+export function useActiveVideoView() {
+ const context = React.useContext(ActiveVideoContext)
+ if (!context) {
+ throw new Error('useActiveVideo must be used within a ActiveVideoProvider')
+ }
+ const id = useId()
+
+ return {
+ active: context.activeViewId === id,
+ setActive: useCallback(
+ (source: string) => context.setActiveView(id, source),
+ [context, id],
+ ),
+ }
+}
diff --git a/src/view/com/util/post-embeds/QuoteEmbed.tsx b/src/view/com/util/post-embeds/QuoteEmbed.tsx
index 4e2e19f587..20c05b692b 100644
--- a/src/view/com/util/post-embeds/QuoteEmbed.tsx
+++ b/src/view/com/util/post-embeds/QuoteEmbed.tsx
@@ -39,7 +39,6 @@ import {Link} from '../Link'
import {PostMeta} from '../PostMeta'
import {Text} from '../text/Text'
import {PostEmbeds} from '.'
-import hairlineWidth = StyleSheet.hairlineWidth
export function MaybeQuoteEmbed({
embed,
@@ -262,7 +261,7 @@ const styles = StyleSheet.create({
marginTop: 8,
paddingVertical: 12,
paddingHorizontal: 12,
- borderWidth: hairlineWidth,
+ borderWidth: StyleSheet.hairlineWidth,
},
errorContainer: {
flexDirection: 'row',
@@ -272,7 +271,7 @@ const styles = StyleSheet.create({
marginTop: 8,
paddingVertical: 14,
paddingHorizontal: 14,
- borderWidth: hairlineWidth,
+ borderWidth: StyleSheet.hairlineWidth,
},
alert: {
marginBottom: 6,
diff --git a/src/view/com/util/post-embeds/VideoEmbed.tsx b/src/view/com/util/post-embeds/VideoEmbed.tsx
new file mode 100644
index 0000000000..5e5293a553
--- /dev/null
+++ b/src/view/com/util/post-embeds/VideoEmbed.tsx
@@ -0,0 +1,44 @@
+import React, {useCallback} from 'react'
+import {View} from 'react-native'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {atoms as a, useTheme} from '#/alf'
+import {Button, ButtonIcon} from '#/components/Button'
+import {Play_Filled_Corner2_Rounded as PlayIcon} from '#/components/icons/Play'
+import {useActiveVideoView} from './ActiveVideoContext'
+import {VideoEmbedInner} from './VideoEmbedInner'
+
+export function VideoEmbed({source}: {source: string}) {
+ const t = useTheme()
+ const {active, setActive} = useActiveVideoView()
+ const {_} = useLingui()
+
+ const onPress = useCallback(() => setActive(source), [setActive, source])
+
+ return (
+
+ {active ? (
+
+ ) : (
+
+ )}
+
+ )
+}
diff --git a/src/view/com/util/post-embeds/VideoEmbedInner.tsx b/src/view/com/util/post-embeds/VideoEmbedInner.tsx
new file mode 100644
index 0000000000..ef06787097
--- /dev/null
+++ b/src/view/com/util/post-embeds/VideoEmbedInner.tsx
@@ -0,0 +1,138 @@
+import React, {useCallback, useEffect, useRef, useState} from 'react'
+import {Pressable, StyleSheet, useWindowDimensions, View} from 'react-native'
+import Animated, {
+ measure,
+ runOnJS,
+ useAnimatedRef,
+ useFrameCallback,
+ useSharedValue,
+} from 'react-native-reanimated'
+import {VideoPlayer, VideoView} from 'expo-video'
+
+import {atoms as a} from '#/alf'
+import {Text} from '#/components/Typography'
+import {useVideoPlayer} from './VideoPlayerContext'
+
+export const VideoEmbedInner = ({}: {source: string}) => {
+ const player = useVideoPlayer()
+ const aref = useAnimatedRef()
+ const {height: windowHeight} = useWindowDimensions()
+ const hasLeftView = useSharedValue(false)
+ const ref = useRef(null)
+
+ const onEnterView = useCallback(() => {
+ if (player.status === 'readyToPlay') {
+ player.play()
+ }
+ }, [player])
+
+ const onLeaveView = useCallback(() => {
+ player.pause()
+ }, [player])
+
+ const enterFullscreen = useCallback(() => {
+ if (ref.current) {
+ ref.current.enterFullscreen()
+ }
+ }, [])
+
+ useFrameCallback(() => {
+ const measurement = measure(aref)
+
+ if (measurement) {
+ if (hasLeftView.value) {
+ // Check if the video is in view
+ if (
+ measurement.pageY >= 0 &&
+ measurement.pageY + measurement.height <= windowHeight
+ ) {
+ runOnJS(onEnterView)()
+ hasLeftView.value = false
+ }
+ } else {
+ // Check if the video is out of view
+ if (
+ measurement.pageY + measurement.height < 0 ||
+ measurement.pageY > windowHeight
+ ) {
+ runOnJS(onLeaveView)()
+ hasLeftView.value = true
+ }
+ }
+ }
+ })
+
+ return (
+
+
+
+
+ )
+}
+
+function VideoControls({
+ player,
+ enterFullscreen,
+}: {
+ player: VideoPlayer
+ enterFullscreen: () => void
+}) {
+ const [currentTime, setCurrentTime] = useState(Math.floor(player.currentTime))
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ setCurrentTime(Math.floor(player.duration - player.currentTime))
+ // how often should we update the time?
+ // 1000 gets out of sync with the video time
+ }, 250)
+
+ return () => {
+ clearInterval(interval)
+ }
+ }, [player])
+
+ const minutes = Math.floor(currentTime / 60)
+ const seconds = String(currentTime % 60).padStart(2, '0')
+
+ return (
+
+
+
+ {minutes}:{seconds}
+
+
+
+
+ )
+}
+
+const styles = StyleSheet.create({
+ timeContainer: {
+ backgroundColor: 'rgba(0, 0, 0, 0.75)',
+ borderRadius: 6,
+ paddingHorizontal: 6,
+ paddingVertical: 3,
+ position: 'absolute',
+ left: 5,
+ bottom: 5,
+ },
+ timeElapsed: {
+ color: 'white',
+ fontSize: 12,
+ fontWeight: 'bold',
+ },
+})
diff --git a/src/view/com/util/post-embeds/VideoEmbedInner.web.tsx b/src/view/com/util/post-embeds/VideoEmbedInner.web.tsx
new file mode 100644
index 0000000000..cb02743c6f
--- /dev/null
+++ b/src/view/com/util/post-embeds/VideoEmbedInner.web.tsx
@@ -0,0 +1,52 @@
+import React, {useEffect, useRef} from 'react'
+import Hls from 'hls.js'
+
+import {atoms as a} from '#/alf'
+
+export const VideoEmbedInner = ({source}: {source: string}) => {
+ const ref = useRef(null)
+
+ // Use HLS.js to play HLS video
+ useEffect(() => {
+ if (ref.current) {
+ if (ref.current.canPlayType('application/vnd.apple.mpegurl')) {
+ ref.current.src = source
+ } else if (Hls.isSupported()) {
+ var hls = new Hls()
+ hls.loadSource(source)
+ hls.attachMedia(ref.current)
+ } else {
+ // TODO: fallback
+ }
+ }
+ }, [source])
+
+ useEffect(() => {
+ if (ref.current) {
+ const observer = new IntersectionObserver(
+ ([entry]) => {
+ if (ref.current) {
+ if (entry.isIntersecting) {
+ if (ref.current.paused) {
+ ref.current.play()
+ }
+ } else {
+ if (!ref.current.paused) {
+ ref.current.pause()
+ }
+ }
+ }
+ },
+ {threshold: 0},
+ )
+
+ observer.observe(ref.current)
+
+ return () => {
+ observer.disconnect()
+ }
+ }
+ }, [])
+
+ return
+}
diff --git a/src/view/com/util/post-embeds/VideoPlayerContext.tsx b/src/view/com/util/post-embeds/VideoPlayerContext.tsx
new file mode 100644
index 0000000000..bc5d9d370c
--- /dev/null
+++ b/src/view/com/util/post-embeds/VideoPlayerContext.tsx
@@ -0,0 +1,41 @@
+import React, {useContext, useEffect} from 'react'
+import type {VideoPlayer} from 'expo-video'
+import {useVideoPlayer as useExpoVideoPlayer} from 'expo-video'
+
+const VideoPlayerContext = React.createContext(null)
+
+export function VideoPlayerProvider({
+ viewId,
+ source,
+ children,
+}: {
+ viewId: string | null
+ source: string
+ children: React.ReactNode
+}) {
+ // eslint-disable-next-line @typescript-eslint/no-shadow
+ const player = useExpoVideoPlayer(source, player => {
+ player.loop = true
+ player.play()
+ })
+
+ // make sure we're playing every time the viewId changes
+ // this means the video is different
+ useEffect(() => {
+ player.play()
+ }, [viewId, player])
+
+ return (
+
+ {children}
+
+ )
+}
+
+export function useVideoPlayer() {
+ const context = useContext(VideoPlayerContext)
+ if (!context) {
+ throw new Error('useVideoPlayer must be used within a VideoPlayerProvider')
+ }
+ return context
+}
diff --git a/src/view/com/util/post-embeds/VideoPlayerContext.web.tsx b/src/view/com/util/post-embeds/VideoPlayerContext.web.tsx
new file mode 100644
index 0000000000..329fb1206e
--- /dev/null
+++ b/src/view/com/util/post-embeds/VideoPlayerContext.web.tsx
@@ -0,0 +1,9 @@
+import React from 'react'
+
+export function VideoPlayerProvider({children}: {children: React.ReactNode}) {
+ return children
+}
+
+export function useVideoPlayer() {
+ throw new Error('useVideoPlayer must not be used on web')
+}
diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx
index 3fd55f5c21..a0dc94e4d8 100644
--- a/src/view/com/util/post-embeds/index.tsx
+++ b/src/view/com/util/post-embeds/index.tsx
@@ -19,18 +19,17 @@ import {
} from '@atproto/api'
import {ImagesLightbox, useLightboxControls} from '#/state/lightbox'
+import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
import {usePalette} from 'lib/hooks/usePalette'
import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard'
import {atoms as a} from '#/alf'
+import {Embed as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
import {ContentHider} from '../../../../components/moderation/ContentHider'
import {AutoSizedImage} from '../images/AutoSizedImage'
import {ImageLayoutGrid} from '../images/ImageLayoutGrid'
import {ExternalLinkEmbed} from './ExternalLinkEmbed'
import {ListEmbed} from './ListEmbed'
import {MaybeQuoteEmbed} from './QuoteEmbed'
-import hairlineWidth = StyleSheet.hairlineWidth
-import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
-import {Embed as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
type Embed =
| AppBskyEmbedRecord.View
@@ -205,7 +204,7 @@ const styles = StyleSheet.create({
fontWeight: 'bold',
},
customFeedOuter: {
- borderWidth: hairlineWidth,
+ borderWidth: StyleSheet.hairlineWidth,
borderRadius: 8,
marginTop: 4,
paddingHorizontal: 12,
diff --git a/src/view/icons/index.tsx b/src/view/icons/index.tsx
index b4feed990c..beb31eca4e 100644
--- a/src/view/icons/index.tsx
+++ b/src/view/icons/index.tsx
@@ -51,6 +51,7 @@ import {faChevronRight} from '@fortawesome/free-solid-svg-icons/faChevronRight'
import {faCircleCheck} from '@fortawesome/free-solid-svg-icons/faCircleCheck'
import {faCircleDot} from '@fortawesome/free-solid-svg-icons/faCircleDot'
import {faCircleExclamation} from '@fortawesome/free-solid-svg-icons/faCircleExclamation'
+import {faClipboardCheck} from '@fortawesome/free-solid-svg-icons/faClipboardCheck'
import {faClone} from '@fortawesome/free-solid-svg-icons/faClone'
import {faCommentSlash} from '@fortawesome/free-solid-svg-icons/faCommentSlash'
import {faDownload} from '@fortawesome/free-solid-svg-icons/faDownload'
@@ -140,6 +141,7 @@ library.add(
faCircleExclamation,
faCirclePlay,
faCircleUser,
+ faClipboardCheck,
faClone,
farClone,
faComment,
diff --git a/src/view/screens/AccessibilitySettings.tsx b/src/view/screens/AccessibilitySettings.tsx
index 9ac9793367..abe1550762 100644
--- a/src/view/screens/AccessibilitySettings.tsx
+++ b/src/view/screens/AccessibilitySettings.tsx
@@ -36,7 +36,7 @@ export function AccessibilitySettingsScreen({}: Props) {
const pal = usePalette('default')
const setMinimalShellMode = useSetMinimalShellMode()
const {screen} = useAnalytics()
- const {isMobile} = useWebMediaQueries()
+ const {isMobile, isTabletOrMobile} = useWebMediaQueries()
const {_} = useLingui()
const requireAltTextEnabled = useRequireAltTextEnabled()
@@ -58,7 +58,7 @@ export function AccessibilitySettingsScreen({}: Props) {
return (
@@ -793,13 +792,13 @@ const styles = StyleSheet.create({
paddingHorizontal: 16,
paddingVertical: 14,
gap: 12,
- borderBottomWidth: hairlineWidth,
+ borderBottomWidth: StyleSheet.hairlineWidth,
},
savedFeedMobile: {
paddingVertical: 10,
},
offlineSlug: {
- borderWidth: hairlineWidth,
+ borderWidth: StyleSheet.hairlineWidth,
borderRadius: 4,
paddingHorizontal: 4,
paddingVertical: 2,
diff --git a/src/view/screens/Lists.tsx b/src/view/screens/Lists.tsx
index 12ea6f48be..9daeaba187 100644
--- a/src/view/screens/Lists.tsx
+++ b/src/view/screens/Lists.tsx
@@ -16,7 +16,6 @@ import {MyLists} from '#/view/com/lists/MyLists'
import {Button} from 'view/com/util/forms/Button'
import {SimpleViewHeader} from 'view/com/util/SimpleViewHeader'
import {Text} from 'view/com/util/text/Text'
-import hairlineWidth = StyleSheet.hairlineWidth
type Props = NativeStackScreenProps
export function ListsScreen({}: Props) {
@@ -55,8 +54,11 @@ export function ListsScreen({}: Props) {
style={[
pal.border,
isMobile
- ? {borderBottomWidth: hairlineWidth}
- : {borderLeftWidth: hairlineWidth, borderRightWidth: hairlineWidth},
+ ? {borderBottomWidth: StyleSheet.hairlineWidth}
+ : {
+ borderLeftWidth: StyleSheet.hairlineWidth,
+ borderRightWidth: StyleSheet.hairlineWidth,
+ },
]}>
diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx
index f1ae7945a7..073e91c45e 100644
--- a/src/view/screens/Notifications.tsx
+++ b/src/view/screens/Notifications.tsx
@@ -1,11 +1,19 @@
-import React from 'react'
+import React, {useCallback} from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useFocusEffect, useIsFocused} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
+import {useAnalytics} from '#/lib/analytics/analytics'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
+import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
+import {ComposeIcon2} from '#/lib/icons'
+import {
+ NativeStackScreenProps,
+ NotificationsTabNavigatorParams,
+} from '#/lib/routes/types'
+import {s} from '#/lib/styles'
import {logger} from '#/logger'
import {isNative} from '#/platform/detection'
import {emitSoftReset, listenSoftReset} from '#/state/events'
@@ -17,37 +25,32 @@ import {
import {truncateAndInvalidate} from '#/state/queries/util'
import {useSetMinimalShellMode} from '#/state/shell'
import {useComposerControls} from '#/state/shell/composer'
-import {useAnalytics} from 'lib/analytics/analytics'
-import {usePalette} from 'lib/hooks/usePalette'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {ComposeIcon2} from 'lib/icons'
-import {
- NativeStackScreenProps,
- NotificationsTabNavigatorParams,
-} from 'lib/routes/types'
-import {colors, s} from 'lib/styles'
-import {TextLink} from 'view/com/util/Link'
+import {Feed} from '#/view/com/notifications/Feed'
+import {FAB} from '#/view/com/util/fab/FAB'
+import {MainScrollProvider} from '#/view/com/util/MainScrollProvider'
+import {ViewHeader} from '#/view/com/util/ViewHeader'
import {ListMethods} from 'view/com/util/List'
import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn'
import {CenteredView} from 'view/com/util/Views'
+import {atoms as a, useTheme} from '#/alf'
+import {Button} from '#/components/Button'
+import {SettingsGear2_Stroke2_Corner0_Rounded as SettingsIcon} from '#/components/icons/SettingsGear2'
+import {Link} from '#/components/Link'
import {Loader} from '#/components/Loader'
-import {Feed} from '../com/notifications/Feed'
-import {FAB} from '../com/util/fab/FAB'
-import {MainScrollProvider} from '../com/util/MainScrollProvider'
-import {ViewHeader} from '../com/util/ViewHeader'
+import {Text} from '#/components/Typography'
type Props = NativeStackScreenProps<
NotificationsTabNavigatorParams,
'Notifications'
>
-export function NotificationsScreen({}: Props) {
+export function NotificationsScreen({route: {params}}: Props) {
const {_} = useLingui()
const setMinimalShellMode = useSetMinimalShellMode()
const [isScrolledDown, setIsScrolledDown] = React.useState(false)
const [isLoadingLatest, setIsLoadingLatest] = React.useState(false)
const scrollElRef = React.useRef(null)
const {screen} = useAnalytics()
- const pal = usePalette('default')
+ const t = useTheme()
const {isDesktop} = useWebMediaQueries()
const queryClient = useQueryClient()
const unreadNotifs = useUnreadNotifications()
@@ -109,56 +112,87 @@ export function NotificationsScreen({}: Props) {
return listenSoftReset(onPressLoadLatest)
}, [onPressLoadLatest, isScreenFocused])
+ const renderButton = useCallback(() => {
+ return (
+
+
+
+ )
+ }, [_, t])
+
const ListHeaderComponent = React.useCallback(() => {
if (isDesktop) {
return (
-
- Notifications{' '}
+
+
+ {isLoadingLatest ? : <>>}
+ {renderButton()}
+
)
}
return <>>
- }, [isDesktop, pal, hasNew, isLoadingLatest])
+ }, [isDesktop, t, hasNew, renderButton, _, isLoadingLatest])
const renderHeaderSpinner = React.useCallback(() => {
return (
-
+
{isLoadingLatest ? : <>>}
+ {renderButton()}
)
- }, [isLoadingLatest])
+ }, [renderButton, isLoadingLatest])
return (
{(isScrolledDown || hasNew) && (
diff --git a/src/view/screens/NotificationsSettings.tsx b/src/view/screens/NotificationsSettings.tsx
new file mode 100644
index 0000000000..8955119a6b
--- /dev/null
+++ b/src/view/screens/NotificationsSettings.tsx
@@ -0,0 +1,94 @@
+import React from 'react'
+import {View} from 'react-native'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {AllNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
+import {useNotificationFeedQuery} from '#/state/queries/notifications/feed'
+import {useNotificationsSettingsMutation} from '#/state/queries/notifications/settings'
+import {ViewHeader} from '#/view/com/util/ViewHeader'
+import {ScrollView} from '#/view/com/util/Views'
+import {atoms as a, useTheme} from '#/alf'
+import {Error} from '#/components/Error'
+import * as Toggle from '#/components/forms/Toggle'
+import {Loader} from '#/components/Loader'
+import {Text} from '#/components/Typography'
+
+type Props = NativeStackScreenProps
+export function NotificationsSettingsScreen({}: Props) {
+ const {_} = useLingui()
+ const t = useTheme()
+
+ const {data, isError: isQueryError, refetch} = useNotificationFeedQuery()
+ const serverPriority = data?.pages.at(0)?.priority
+
+ const {
+ mutate: onChangePriority,
+ isPending: isMutationPending,
+ variables,
+ } = useNotificationsSettingsMutation()
+
+ const priority = isMutationPending
+ ? variables[0] === 'enabled'
+ : serverPriority
+
+ return (
+
+
+ {isQueryError ? (
+
+ ) : (
+
+
+ {' '}
+ Notification filters
+
+
+
+
+
+ Enable priority notifications
+
+ {!data ? : }
+
+
+
+
+
+
+ Experimental: When this preference is enabled, you'll only
+ receive reply and quote notifications from users you follow.
+ We'll continue to add more controls here over time.
+
+
+
+
+ )}
+
+ )
+}
diff --git a/src/view/screens/PreferencesExternalEmbeds.tsx b/src/view/screens/PreferencesExternalEmbeds.tsx
index 5eec7e5077..57ca5e7653 100644
--- a/src/view/screens/PreferencesExternalEmbeds.tsx
+++ b/src/view/screens/PreferencesExternalEmbeds.tsx
@@ -30,7 +30,7 @@ export function PreferencesExternalEmbeds({}: Props) {
const pal = usePalette('default')
const setMinimalShellMode = useSetMinimalShellMode()
const {screen} = useAnalytics()
- const {isMobile} = useWebMediaQueries()
+ const {isTabletOrMobile} = useWebMediaQueries()
useFocusEffect(
React.useCallback(() => {
@@ -41,26 +41,23 @@ export function PreferencesExternalEmbeds({}: Props) {
return (
-
-
-
- External Media Preferences
-
-
- Customize media from external sites.
-
-
-
+ contentContainerStyle={[pal.viewLight, {paddingBottom: 75}]}>
+
+
+
+ External Media Preferences
+
+
+ Customize media from external sites.
+
+
+
+
diff --git a/src/view/screens/PreferencesFollowingFeed.tsx b/src/view/screens/PreferencesFollowingFeed.tsx
index b427a0f2b1..879c925fbf 100644
--- a/src/view/screens/PreferencesFollowingFeed.tsx
+++ b/src/view/screens/PreferencesFollowingFeed.tsx
@@ -1,24 +1,24 @@
import React, {useState} from 'react'
-import {ScrollView, StyleSheet, TouchableOpacity, View} from 'react-native'
+import {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 {Slider} from '@miblanchard/react-native-slider'
import debounce from 'lodash.debounce'
+import {usePalette} from '#/lib/hooks/usePalette'
+import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
+import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
+import {colors, s} from '#/lib/styles'
+import {isWeb} from '#/platform/detection'
import {
usePreferencesQuery,
useSetFeedViewPreferencesMutation,
} from '#/state/queries/preferences'
-import {usePalette} from 'lib/hooks/usePalette'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
-import {colors, s} from 'lib/styles'
-import {isWeb} from 'platform/detection'
-import {ToggleButton} from 'view/com/util/forms/ToggleButton'
-import {ViewHeader} from 'view/com/util/ViewHeader'
-import {CenteredView} from 'view/com/util/Views'
-import {Text} from '../com/util/text/Text'
+import {ToggleButton} from '#/view/com/util/forms/ToggleButton'
+import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader'
+import {Text} from '#/view/com/util/text/Text'
+import {ScrollView} from '#/view/com/util/Views'
function RepliesThresholdInput({
enabled,
@@ -79,10 +79,10 @@ type Props = NativeStackScreenProps<
CommonNavigatorParams,
'PreferencesFollowingFeed'
>
-export function PreferencesFollowingFeed({navigation}: Props) {
+export function PreferencesFollowingFeed({}: Props) {
const pal = usePalette('default')
const {_} = useLingui()
- const {isTabletOrDesktop} = useWebMediaQueries()
+ const {isTabletOrMobile} = useWebMediaQueries()
const {data: preferences} = usePreferencesQuery()
const {mutate: setFeedViewPref, variables} =
useSetFeedViewPreferencesMutation()
@@ -92,26 +92,25 @@ export function PreferencesFollowingFeed({navigation}: Props) {
)
return (
-
-
-
-
- Fine-tune the content you see on your Following feed.
-
-
-
-
+
+
+
+
+
+ Following Feed Preferences
+
+
+
+ Fine-tune the content you see on your Following feed.
+
+
+
+
@@ -253,7 +252,7 @@ export function PreferencesFollowingFeed({navigation}: Props) {
-
+ {' '}
Show Posts from My Feeds
@@ -288,42 +287,17 @@ export function PreferencesFollowingFeed({navigation}: Props) {
-
-
- {
- navigation.canGoBack()
- ? navigation.goBack()
- : navigation.navigate('Settings')
- }}
- style={[styles.btn, isTabletOrDesktop && styles.btnDesktop]}
- accessibilityRole="button"
- accessibilityLabel={_(msg`Confirm`)}
- accessibilityHint="">
-
- Done
-
-
-
-
+
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
- paddingBottom: 90,
},
desktopContainer: {
borderLeftWidth: 1,
borderRightWidth: 1,
- paddingBottom: 40,
},
titleSection: {
paddingBottom: 30,
@@ -338,6 +312,7 @@ const styles = StyleSheet.create({
},
cardsContainer: {
paddingHorizontal: 20,
+ paddingVertical: 16,
},
card: {
padding: 16,
diff --git a/src/view/screens/PreferencesThreads.tsx b/src/view/screens/PreferencesThreads.tsx
index 321c672936..3b09f0abb5 100644
--- a/src/view/screens/PreferencesThreads.tsx
+++ b/src/view/screens/PreferencesThreads.tsx
@@ -1,33 +1,29 @@
import React from 'react'
-import {
- ActivityIndicator,
- ScrollView,
- StyleSheet,
- TouchableOpacity,
- View,
-} from 'react-native'
+import {ActivityIndicator, StyleSheet, View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {Text} from '../com/util/text/Text'
-import {s, colors} from 'lib/styles'
-import {usePalette} from 'lib/hooks/usePalette'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {ToggleButton} from 'view/com/util/forms/ToggleButton'
-import {RadioGroup} from 'view/com/util/forms/RadioGroup'
-import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
-import {ViewHeader} from 'view/com/util/ViewHeader'
-import {CenteredView} from 'view/com/util/Views'
-import {Trans, msg} from '@lingui/macro'
+import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
+
+import {usePalette} from '#/lib/hooks/usePalette'
+import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
+import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
+import {colors, s} from '#/lib/styles'
import {
usePreferencesQuery,
useSetThreadViewPreferencesMutation,
} from '#/state/queries/preferences'
+import {RadioGroup} from '#/view/com/util/forms/RadioGroup'
+import {ToggleButton} from '#/view/com/util/forms/ToggleButton'
+import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader'
+import {Text} from '#/view/com/util/text/Text'
+import {ScrollView} from '#/view/com/util/Views'
+import {atoms as a} from '#/alf'
type Props = NativeStackScreenProps
-export function PreferencesThreads({navigation}: Props) {
+export function PreferencesThreads({}: Props) {
const pal = usePalette('default')
const {_} = useLingui()
- const {isTabletOrDesktop} = useWebMediaQueries()
+ const {isTabletOrMobile} = useWebMediaQueries()
const {data: preferences} = usePreferencesQuery()
const {mutate: setThreadViewPrefs, variables} =
useSetThreadViewPreferencesMutation()
@@ -42,27 +38,25 @@ export function PreferencesThreads({navigation}: Props) {
)
return (
-
-
-
-
- Fine-tune the discussion threads.
-
-
+
+
+
+
+
+ Thread Preferences
+
+
+ Fine-tune the discussion threads.
+
+
+
- {preferences ? (
-
+ {preferences ? (
@@ -136,46 +130,21 @@ export function PreferencesThreads({navigation}: Props) {
/>
-
- ) : (
-
- )}
-
-
- {
- navigation.canGoBack()
- ? navigation.goBack()
- : navigation.navigate('Settings')
- }}
- style={[styles.btn, isTabletOrDesktop && styles.btnDesktop]}
- accessibilityRole="button"
- accessibilityLabel={_(msg`Confirm`)}
- accessibilityHint="">
-
- Done
-
-
-
-
+ ) : (
+
+ )}
+
+
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
- paddingBottom: 90,
},
desktopContainer: {
borderLeftWidth: 1,
borderRightWidth: 1,
- paddingBottom: 40,
},
titleSection: {
paddingBottom: 30,
@@ -190,6 +159,7 @@ const styles = StyleSheet.create({
},
cardsContainer: {
paddingHorizontal: 20,
+ paddingVertical: 16,
},
card: {
padding: 16,
diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx
index 17d1214b2f..60e5193ff6 100644
--- a/src/view/screens/ProfileFeed.tsx
+++ b/src/view/screens/ProfileFeed.tsx
@@ -205,6 +205,7 @@ export function ProfileFeedScreenInner({
_(
msg`There was an an issue updating your feeds, please check your internet connection and try again.`,
),
+ 'xmark',
)
logger.error('Failed up update feeds', {message: err})
}
@@ -231,7 +232,7 @@ export function ProfileFeedScreenInner({
])
}
} catch (e) {
- Toast.show(_(msg`There was an issue contacting the server`))
+ Toast.show(_(msg`There was an issue contacting the server`), 'xmark')
logger.error('Failed to toggle pinned feed', {message: e})
}
}, [
@@ -543,6 +544,7 @@ function AboutSection({
_(
msg`There was an an issue contacting the server, please check your internet connection and try again.`,
),
+ 'xmark',
)
logger.error('Failed up toggle like', {message: err})
}
diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx
index 72de428f30..0ed44758d4 100644
--- a/src/view/screens/ProfileList.tsx
+++ b/src/view/screens/ProfileList.tsx
@@ -65,7 +65,6 @@ import {useDialogControl} from '#/components/Dialog'
import * as Prompt from '#/components/Prompt'
import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
import {RichText} from '#/components/RichText'
-import hairlineWidth = StyleSheet.hairlineWidth
const SECTION_TITLES_CURATE = ['Posts', 'About']
const SECTION_TITLES_MOD = ['About']
@@ -300,7 +299,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
Toast.show(_(msg`Saved to your feeds`))
}
} catch (e) {
- Toast.show(_(msg`There was an issue contacting the server`))
+ Toast.show(_(msg`There was an issue contacting the server`), 'xmark')
logger.error('Failed to toggle pinned feed', {message: e})
}
}, [
@@ -319,7 +318,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
await removeSavedFeed(savedFeedConfig)
Toast.show(_(msg`Removed from your feeds`))
} catch (e) {
- Toast.show(_(msg`There was an issue contacting the server`))
+ Toast.show(_(msg`There was an issue contacting the server`), 'xmark')
logger.error('Failed to remove pinned list', {message: e})
}
}, [playHaptic, removeSavedFeed, _, savedFeedConfig])
@@ -803,7 +802,7 @@ const AboutSection = React.forwardRef(
diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx
index d79c7708c6..3402db2c19 100644
--- a/src/view/screens/SavedFeeds.tsx
+++ b/src/view/screens/SavedFeeds.tsx
@@ -32,7 +32,6 @@ import {NoFollowingFeed} from '#/screens/Feeds/NoFollowingFeed'
import {NoSavedFeedsOfAnyType} from '#/screens/Feeds/NoSavedFeedsOfAnyType'
import {atoms as a, useTheme} from '#/alf'
import {FilterTimeline_Stroke2_Corner0_Rounded as FilterTimeline} from '#/components/icons/FilterTimeline'
-import hairlineWidth = StyleSheet.hairlineWidth
const HITSLOP_TOP = {
top: 20,
@@ -93,7 +92,8 @@ export function SavedFeeds({}: Props) {
{noSavedFeedsOfAnyType && (
-
+
)}
@@ -135,7 +135,8 @@ export function SavedFeeds({}: Props) {
)}
{noFollowingFeed && (
-
+
)}
@@ -234,7 +235,7 @@ function ListItem({
},
])
} catch (e) {
- Toast.show(_(msg`There was an issue contacting the server`))
+ Toast.show(_(msg`There was an issue contacting the server`), 'xmark')
logger.error('Failed to toggle pinned feed', {message: e})
}
}, [_, playHaptic, feed, updateSavedFeeds, resetSaveFeedsMutationState])
@@ -260,7 +261,7 @@ function ListItem({
index: nextIndex,
})
} catch (e) {
- Toast.show(_(msg`There was an issue contacting the server`))
+ Toast.show(_(msg`There was an issue contacting the server`), 'xmark')
logger.error('Failed to set pinned feed order', {message: e})
}
}, [feed, isPinned, overwriteSavedFeeds, currentFeeds, _])
@@ -286,7 +287,7 @@ function ListItem({
index: nextIndex,
})
} catch (e) {
- Toast.show(_(msg`There was an issue contacting the server`))
+ Toast.show(_(msg`There was an issue contacting the server`), 'xmark')
logger.error('Failed to set pinned feed order', {message: e})
}
}, [feed, isPinned, overwriteSavedFeeds, currentFeeds, _])
@@ -437,12 +438,12 @@ const styles = StyleSheet.create({
paddingHorizontal: 14,
paddingTop: 20,
paddingBottom: 10,
- borderBottomWidth: hairlineWidth,
+ borderBottomWidth: StyleSheet.hairlineWidth,
},
itemContainer: {
flexDirection: 'row',
alignItems: 'center',
- borderBottomWidth: hairlineWidth,
+ borderBottomWidth: StyleSheet.hairlineWidth,
},
footerText: {
paddingHorizontal: 26,
diff --git a/src/view/screens/Search/Explore.tsx b/src/view/screens/Search/Explore.tsx
index 05fd85effe..5510fbee25 100644
--- a/src/view/screens/Search/Explore.tsx
+++ b/src/view/screens/Search/Explore.tsx
@@ -75,17 +75,17 @@ function SuggestedItemsHeader({
)
}
-type LoadMoreItems =
+type LoadMoreItem =
| {
type: 'profile'
key: string
- avatar: string
+ avatar: string | undefined
moderation: ModerationDecision
}
| {
type: 'feed'
key: string
- avatar: string
+ avatar: string | undefined
moderation: undefined
}
@@ -98,27 +98,28 @@ function LoadMore({
}) {
const t = useTheme()
const {_} = useLingui()
- const items = React.useMemo(() => {
+ const items: LoadMoreItem[] = React.useMemo(() => {
return item.items
.map(_item => {
+ let loadMoreItem: LoadMoreItem | undefined
if (_item.type === 'profile') {
- return {
+ loadMoreItem = {
type: 'profile',
key: _item.profile.did,
avatar: _item.profile.avatar,
moderation: moderateProfile(_item.profile, moderationOpts!),
}
} else if (_item.type === 'feed') {
- return {
+ loadMoreItem = {
type: 'feed',
key: _item.feed.uri,
avatar: _item.feed.avatar,
moderation: undefined,
}
}
- return undefined
+ return loadMoreItem
})
- .filter(Boolean) as LoadMoreItems[]
+ .filter(n => !!n)
}, [item.items, moderationOpts])
if (items.length === 0) return null
diff --git a/src/view/screens/Settings/ExportCarDialog.tsx b/src/view/screens/Settings/ExportCarDialog.tsx
index 72d943bcf0..a6ddb38204 100644
--- a/src/view/screens/Settings/ExportCarDialog.tsx
+++ b/src/view/screens/Settings/ExportCarDialog.tsx
@@ -43,7 +43,7 @@ export function ExportCarDialog({
}
} catch (e) {
logger.error('Error occurred while downloading CAR file', {message: e})
- Toast.show(_(msg`Error occurred while saving file`))
+ Toast.show(_(msg`Error occurred while saving file`), 'xmark')
} finally {
setLoading(false)
control.close()
@@ -94,6 +94,7 @@ export function ExportCarDialog({
This feature is in beta. You can read more about repository
exports in{' '}
this blogpost
diff --git a/src/view/screens/Settings/index.tsx b/src/view/screens/Settings/index.tsx
index f59bfb1834..db74d5c0d5 100644
--- a/src/view/screens/Settings/index.tsx
+++ b/src/view/screens/Settings/index.tsx
@@ -62,13 +62,12 @@ import {UserAvatar} from 'view/com/util/UserAvatar'
import {ScrollView} from 'view/com/util/Views'
import {DeactivateAccountDialog} from '#/screens/Settings/components/DeactivateAccountDialog'
import {useTheme} from '#/alf'
+import {atoms as a} from '#/alf'
import {useDialogControl} from '#/components/Dialog'
import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings'
import {navigate, resetToTab} from '#/Navigation'
import {Email2FAToggle} from './Email2FAToggle'
import {ExportCarDialog} from './ExportCarDialog'
-import hairlineWidth = StyleSheet.hairlineWidth
-import {atoms as a} from '#/alf'
function SettingsAccountCard({
account,
@@ -326,7 +325,7 @@ export function SettingsScreen({}: Props) {
showBackButton={isMobile}
style={[
pal.border,
- {borderBottomWidth: hairlineWidth},
+ {borderBottomWidth: StyleSheet.hairlineWidth},
!isMobile && {borderLeftWidth: 1, borderRightWidth: 1},
]}>
diff --git a/src/view/screens/Storybook/Links.tsx b/src/view/screens/Storybook/Links.tsx
index d35db79bc4..465ce0d6f3 100644
--- a/src/view/screens/Storybook/Links.tsx
+++ b/src/view/screens/Storybook/Links.tsx
@@ -13,18 +13,20 @@ export function Links() {
Links
-
+
https://google.com
-
+
External with custom children (google.com)
Internal (bsky.social)
Internal (bsky.app)
diff --git a/src/view/shell/Drawer.tsx b/src/view/shell/Drawer.tsx
index e858ae2c45..4b765962a6 100644
--- a/src/view/shell/Drawer.tsx
+++ b/src/view/shell/Drawer.tsx
@@ -9,10 +9,7 @@ import {
View,
ViewStyle,
} from 'react-native'
-import {
- FontAwesomeIcon,
- FontAwesomeIconStyle,
-} from '@fortawesome/react-native-fontawesome'
+import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome'
import {msg, Plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {StackActions, useNavigation} from '@react-navigation/native'
@@ -37,6 +34,7 @@ import {formatCountShortOnly} from 'view/com/util/numeric/format'
import {Text} from 'view/com/util/text/Text'
import {UserAvatar} from 'view/com/util/UserAvatar'
import {useTheme as useAlfTheme} from '#/alf'
+import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {
Bell_Filled_Corner0_Rounded as BellFilled,
Bell_Stroke2_Corner0_Rounded as Bell,
@@ -52,6 +50,7 @@ import {
} from '#/components/icons/HomeOpen'
import {MagnifyingGlass_Filled_Stroke2_Corner0_Rounded as MagnifyingGlassFilled} from '#/components/icons/MagnifyingGlass'
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as MagnifyingGlass} from '#/components/icons/MagnifyingGlass2'
+import {Message_Stroke2_Corner0_Rounded as Message} from '#/components/icons/Message'
import {SettingsGear2_Stroke2_Corner0_Rounded as Settings} from '#/components/icons/SettingsGear2'
import {
UserCircle_Filled_Corner0_Rounded as UserCircleFilled,
@@ -319,42 +318,33 @@ let DrawerFooter = ({
onPressFeedback: () => void
onPressHelp: () => void
}): React.ReactNode => {
- const theme = useTheme()
- const pal = usePalette('default')
const {_} = useLingui()
return (
-
-
-
+
-
-
+
+
+
+
)
}
diff --git a/src/view/shell/bottom-bar/BottomBarStyles.tsx b/src/view/shell/bottom-bar/BottomBarStyles.tsx
index ba8743474f..c575e3d9b1 100644
--- a/src/view/shell/bottom-bar/BottomBarStyles.tsx
+++ b/src/view/shell/bottom-bar/BottomBarStyles.tsx
@@ -1,7 +1,6 @@
import {StyleSheet} from 'react-native'
import {colors} from 'lib/styles'
-import hairlineWidth = StyleSheet.hairlineWidth
export const styles = StyleSheet.create({
bottomBar: {
@@ -10,7 +9,7 @@ export const styles = StyleSheet.create({
left: 0,
right: 0,
flexDirection: 'row',
- borderTopWidth: hairlineWidth,
+ borderTopWidth: StyleSheet.hairlineWidth,
paddingLeft: 5,
paddingRight: 10,
},
diff --git a/src/view/shell/desktop/RightNav.tsx b/src/view/shell/desktop/RightNav.tsx
index 8dfa671cff..ed3d8212cf 100644
--- a/src/view/shell/desktop/RightNav.tsx
+++ b/src/view/shell/desktop/RightNav.tsx
@@ -11,10 +11,9 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {s} from 'lib/styles'
import {TextLink} from 'view/com/util/Link'
import {Text} from 'view/com/util/text/Text'
+import {ProgressGuideList} from '#/components/ProgressGuide/List'
import {DesktopFeeds} from './Feeds'
import {DesktopSearch} from './Search'
-import hairlineWidth = StyleSheet.hairlineWidth
-import {ProgressGuideList} from '#/components/ProgressGuide/List'
export function DesktopRightNav({routeName}: {routeName: string}) {
const pal = usePalette('default')
@@ -135,8 +134,8 @@ const styles = StyleSheet.create({
marginBottom: 10,
},
desktopFeedsContainer: {
- borderTopWidth: hairlineWidth,
- borderBottomWidth: hairlineWidth,
+ borderTopWidth: StyleSheet.hairlineWidth,
+ borderBottomWidth: StyleSheet.hairlineWidth,
marginTop: 18,
marginBottom: 18,
},
diff --git a/yarn.lock b/yarn.lock
index a83d41dba6..675fda4c2f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -34,10 +34,10 @@
jsonpointer "^5.0.0"
leven "^3.1.0"
-"@atproto/api@^0.12.23":
- version "0.12.23"
- resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.23.tgz#b3409817d0b981a64f30d16e8257f0fe261338af"
- integrity sha512-fgQ30u+q9smX5g41eep7fISSkSAhRkX0inc81PZ82QwcHbFkC8ePaha/KP0CoTaPWKi7EsC89Z/8BEBCJo0oBA==
+"@atproto/api@0.12.25":
+ version "0.12.25"
+ resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.25.tgz#9eeb51484106a5e07f89f124e505674a3574f93b"
+ integrity sha512-IV3vGPnDw9bmyP/JOd8YKbm8fOpRAgJpEUVnIZNVb/Vo8v+WOroOjrJxtzdHOcXTL9IEcTTyXSCc7yE7kwhN2A==
dependencies:
"@atproto/common-web" "^0.3.0"
"@atproto/lexicon" "^0.4.0"
@@ -13345,6 +13345,11 @@ history@^5.3.0:
dependencies:
"@babel/runtime" "^7.7.6"
+hls.js@^1.5.11:
+ version "1.5.11"
+ resolved "https://registry.yarnpkg.com/hls.js/-/hls.js-1.5.11.tgz#3941347df454983859ae8c75fe19e8818719a826"
+ integrity sha512-q3We1izi2+qkOO+TvZdHv+dx6aFzdtk3xc1/Qesrvto4thLTT/x/1FK85c5h1qZE4MmMBNgKg+MIW8nxQfxwBw==
+
hmac-drbg@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
@@ -21373,10 +21378,10 @@ typedarray-to-buffer@^3.1.5:
dependencies:
is-typedarray "^1.0.0"
-typescript@^5.3.3:
- version "5.3.3"
- resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37"
- integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==
+typescript@^5.5.4:
+ version "5.5.4"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba"
+ integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==
ua-parser-js@^0.7.33:
version "0.7.35"