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/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 ed76c753b5..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()
@@ -107,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 a64988f380..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)
@@ -92,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 9e9b49443a..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}
diff --git a/src/alf/atoms.ts b/src/alf/atoms.ts
index 650997c919..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 = {
/*
@@ -284,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/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 82d675a8e4..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}
)
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/Signup/StepInfo/Policies.tsx b/src/screens/Signup/StepInfo/Policies.tsx
index f25bda274f..a3a0672223 100644
--- a/src/screens/Signup/StepInfo/Policies.tsx
+++ b/src/screens/Signup/StepInfo/Policies.tsx
@@ -45,14 +45,20 @@ export const Policies = ({
const els = []
if (tos) {
els.push(
-
+
{_(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/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..569c85c3ad 100644
--- a/src/state/queries/post-feed.ts
+++ b/src/state/queries/post-feed.ts
@@ -31,7 +31,7 @@ import {LikesFeedAPI} from 'lib/api/feed/likes'
import {ListFeedAPI} from 'lib/api/feed/list'
import {MergeFeedAPI} from 'lib/api/feed/merge'
import {FeedAPI, ReasonFeedSource} from 'lib/api/feed/types'
-import {FeedTuner, FeedTunerFn, NoopFeedTuner} from 'lib/api/feed-manip'
+import {FeedTuner, FeedTunerFn} from 'lib/api/feed-manip'
import {BSKY_FEED_OWNER_DIDS} from 'lib/constants'
import {KnownError} from '#/view/com/posts/FeedErrorMessage'
import {useFeedTuners} from '../preferences/feed-tuners'
@@ -61,7 +61,6 @@ export type FeedDescriptor =
| `list|${ListUri}`
| `list|${ListUri}|${ListFilter}`
export interface FeedParams {
- disableTuner?: boolean
mergeFeedEnabled?: boolean
mergeFeedSources?: string[]
}
@@ -78,7 +77,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
@@ -102,7 +104,7 @@ export interface FeedPageUnselected {
export interface FeedPage {
api: FeedAPI
- tuner: FeedTuner | NoopFeedTuner
+ tuner: FeedTuner
cursor: string | undefined
slices: FeedPostSlice[]
fetchedAt: number
@@ -139,18 +141,11 @@ export function usePostFeedQuery(
const selectArgs = React.useMemo(
() => ({
feedTuners,
- disableTuner: params?.disableTuner,
moderationOpts,
ignoreFilterFor: opts?.ignoreFilterFor,
isDiscover,
}),
- [
- feedTuners,
- params?.disableTuner,
- moderationOpts,
- opts?.ignoreFilterFor,
- isDiscover,
- ],
+ [feedTuners, moderationOpts, opts?.ignoreFilterFor, isDiscover],
)
const query = useInfiniteQuery<
@@ -229,17 +224,10 @@ export function usePostFeedQuery(
(data: InfiniteData) => {
// If the selection depends on some data, that data should
// be included in the selectArgs object and read here.
- const {
- feedTuners,
- disableTuner,
- moderationOpts,
- ignoreFilterFor,
- isDiscover,
- } = selectArgs
+ const {feedTuners, moderationOpts, ignoreFilterFor, isDiscover} =
+ selectArgs
- const tuner = disableTuner
- ? new NoopFeedTuner()
- : new FeedTuner(feedTuners)
+ const tuner = new FeedTuner(feedTuners)
// Keep track of the last run and whether we can reuse
// some already selected pages from there.
@@ -311,7 +299,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 +311,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 +329,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`), 'xmark')
- 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 c44353c8bf..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,
@@ -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/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/UserAddRemoveLists.tsx b/src/view/com/modals/UserAddRemoveLists.tsx
index 16c366d8af..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 (
@@ -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..e4294eaa5f 100644
--- a/src/view/com/notifications/FeedItem.tsx
+++ b/src/view/com/notifications/FeedItem.tsx
@@ -13,27 +13,37 @@ import {
AppBskyEmbedRecordWithMedia,
AppBskyFeedDefs,
AppBskyFeedPost,
+ AppBskyGraphFollow,
moderateProfile,
ModerationDecision,
ModerationOpts,
} from '@atproto/api'
import {AtUri} from '@atproto/api'
+import {TID} from '@atproto/common-web'
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 +51,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 +64,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 +170,6 @@ let FeedItem = ({
)
}
- let isFollowBack = false
let action = ''
let icon = (
} else if (item.type === 'follow') {
+ let isFollowBack = false
+
if (
item.notification.author.viewer?.following &&
- gate('ungroup_follow_backs')
+ AppBskyGraphFollow.isRecord(item.notification.record)
) {
- isFollowBack = true
+ let followingTimestamp
+ try {
+ const rkey = new AtUri(item.notification.author.viewer.following).rkey
+ followingTimestamp = TID.fromStr(rkey).timestamp()
+ } catch (e) {
+ // For some reason the following URI was invalid. Default to it not being a follow back.
+ console.error('Invalid following URI')
+ }
+ if (followingTimestamp) {
+ const followedTimestamp =
+ new Date(item.notification.record.createdAt).getTime() * 1000
+ isFollowBack = followedTimestamp > followingTimestamp
+ }
+ }
+
+ if (isFollowBack && gate('ungroup_follow_backs')) {
action = _(msg`followed you back`)
} else {
action = _(msg`followed you`)
@@ -211,7 +226,22 @@ let FeedItem = ({
return null
}
- let formattedCount = authors.length > 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 +304,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 +337,7 @@ let FeedItem = ({
{({timeElapsed}) => (
+ title={niceTimestamp}>
{' ' + timeElapsed}
)}
@@ -458,7 +489,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 +506,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/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/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 36b1706cb1..f12ecf12b6 100644
--- a/src/view/com/posts/FeedShutdownMsg.tsx
+++ b/src/view/com/posts/FeedShutdownMsg.tsx
@@ -99,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/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/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/List.web.tsx b/src/view/com/util/List.web.tsx
index 12d223db03..5aa699356d 100644
--- a/src/view/com/util/List.web.tsx
+++ b/src/view/com/util/List.web.tsx
@@ -344,10 +344,11 @@ function ListImpl(
style={[styles.aboveTheFoldDetector, {height: headerOffset}]}
/>
{onStartReached && !isEmpty && (
-
)}
{headerComponent}
@@ -368,11 +369,11 @@ function ListImpl(
)
})}
{onEndReached && !isEmpty && (
-
)}
{footerComponent}
@@ -381,6 +382,34 @@ function ListImpl(
)
}
+function EdgeVisibility({
+ root,
+ topMargin,
+ bottomMargin,
+ containerRef,
+ onVisibleChange,
+}: {
+ root?: React.RefObject | null
+ topMargin?: string
+ bottomMargin?: string
+ containerRef: React.RefObject
+ onVisibleChange: (isVisible: boolean) => void
+}) {
+ const [containerHeight, setContainerHeight] = React.useState(0)
+ useResizeObserver(containerRef, (w, h) => {
+ setContainerHeight(h)
+ })
+ return (
+
+ )
+}
+
function useResizeObserver(
ref: React.RefObject,
onResize: undefined | ((w: number, h: number) => void),
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 d510eed87f..f7c6bc2c91 100644
--- a/src/view/com/util/Toast.tsx
+++ b/src/view/com/util/Toast.tsx
@@ -12,7 +12,7 @@ import {atoms as a, useTheme} from '#/alf'
import {Text} from '#/components/Typography'
import {IS_TEST} from '#/env'
-const TIMEOUT = 3.7e3
+const TIMEOUT = 2e3
export function show(
message: string,
diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx
index b727234093..8862bd0e4a 100644
--- a/src/view/com/util/UserAvatar.tsx
+++ b/src/view/com/util/UserAvatar.tsx
@@ -55,7 +55,6 @@ interface PreviewableUserAvatarProps extends BaseUserAvatarProps {
profile: AppBskyActorDefs.ProfileViewBasic
disableHoverCard?: boolean
onBeforePress?: () => void
- accessible?: boolean
}
const BLUR_AMOUNT = isWeb ? 5 : 100
@@ -412,7 +411,6 @@ let PreviewableUserAvatar = ({
profile,
disableHoverCard,
onBeforePress,
- accessible = true,
...rest
}: PreviewableUserAvatarProps): React.ReactNode => {
const {_} = useLingui()
@@ -426,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/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-embeds/ActiveVideoContext.tsx b/src/view/com/util/post-embeds/ActiveVideoContext.tsx
new file mode 100644
index 0000000000..6804436a7e
--- /dev/null
+++ b/src/view/com/util/post-embeds/ActiveVideoContext.tsx
@@ -0,0 +1,48 @@
+import React, {useCallback, useId, useMemo, useState} from 'react'
+
+import {VideoPlayerProvider} from './VideoPlayerContext'
+
+const ActiveVideoContext = React.createContext<{
+ activeViewId: string | null
+ setActiveView: (viewId: string, src: string) => 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 71c72b4978..0462212fbd 100644
--- a/src/view/com/util/post-embeds/index.tsx
+++ b/src/view/com/util/post-embeds/index.tsx
@@ -21,19 +21,18 @@ import {
} from '@atproto/api'
import {ImagesLightbox, useLightboxControls} from '#/state/lightbox'
+import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
+import {useModerationOpts} from '#/state/preferences/moderation-opts'
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 {useModerationOpts} from '#/state/preferences/moderation-opts'
-import {Embed as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
type Embed =
| AppBskyEmbedRecord.View
@@ -231,7 +230,7 @@ const styles = StyleSheet.create({
fontWeight: 'bold',
},
customFeedOuter: {
- borderWidth: hairlineWidth,
+ borderWidth: StyleSheet.hairlineWidth,
borderRadius: 8,
marginTop: 4,
paddingHorizontal: 12,
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/ProfileList.tsx b/src/view/screens/ProfileList.tsx
index abb1600f83..bf13791ae6 100644
--- a/src/view/screens/ProfileList.tsx
+++ b/src/view/screens/ProfileList.tsx
@@ -73,7 +73,6 @@ import {ScreenHider} from '#/components/moderation/ScreenHider'
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']
@@ -838,7 +837,7 @@ const AboutSection = React.forwardRef(
diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx
index ef76cdcb22..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 && (
-
+
)}
@@ -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 0daa3c8c97..a6ddb38204 100644
--- a/src/view/screens/Settings/ExportCarDialog.tsx
+++ b/src/view/screens/Settings/ExportCarDialog.tsx
@@ -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/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"