Compare commits

...

22 Commits

Author SHA1 Message Date
Dan Abramov cdf13ae7fa Simpler fix 2024-12-12 04:19:38 +00:00
Dan Abramov 080033a011 Make sure local web builds set debug environment flag 2024-12-12 03:30:19 +00:00
Samuel Newman e49dad2889 Nicer app icon screen (#6972)
* wip exploration

* list format instead of grid

* fix normalising default name

* adjust margins

* Rework the app icon link

* Decrease app icon size

* Adjust some spacing

* Move some things around to fix web errors

* Fix pathname

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
Co-authored-by: Eric Bailey <git@esb.lol>
2024-12-11 22:33:45 +00:00
dan 69f22b9dba Point "Continue thread" at last shown post (#7060) 2024-12-11 22:29:57 +00:00
Samuel Newman 48346edde6 More exact counts (#7059)
* post liked by

* rm unnecessary layout.center

* do post quotes/reposts

* set height of header to prevent layout shifts
2024-12-11 22:24:22 +00:00
Samuel Newman 89c6ca94fe Followers/following exact count (#7057)
* followers exact count in heading

* exact count for following

* move files to new dir

* use <Plural>
2024-12-11 21:12:58 +00:00
dan 7db5882ea2 Merge pull request #7058 from bluesky-social/fix-profile-empty-space-nobio 2024-12-11 21:01:56 +00:00
Dan Abramov c55063d262 Reduce excessive empty space on profile with no bio 2024-12-11 20:35:13 +00:00
Samuel Newman b081f8f8d9 Remove rounding mode hack (#7055)
* bump formatjs and remove hack

* maybe fix tests?

* Revert "maybe fix tests?"

This reverts commit f1b448e5b8.

* rm test
2024-12-11 19:13:09 +00:00
かっこかり de15f8e2d3 feat(embed): Add support for dark mode (#6912)
* feat(embed): Support dark mode (wip)

* finishing up the implementation

* fix tailwind color selector

* tweak design

* refactor: unify types

* fix

* fix english grammar

* refactor: unify types

* tweak design

* remove the customization part
2024-12-11 16:31:29 +00:00
dan 48c5341644 Fix iOS build (#7042) 2024-12-10 22:17:27 +00:00
dan 86155986af Fix composer shortcut detaching a reply (#7041) 2024-12-10 21:19:53 +00:00
Barry Pollard bee50c3954 Font loading improvements (#6993)
* Font loading improvements

* Reconvert with missing files

* Update bskyweb/templates/base.html

Co-authored-by: Rick Viscomi <rviscomi@users.noreply.github.com>

* Update bskyweb/templates/base.html

Co-authored-by: Rick Viscomi <rviscomi@users.noreply.github.com>

* Run web-build to update hashes

* Revert Android to otf

* Use Inter woff2 files

---------

Co-authored-by: Rick Viscomi <rviscomi@users.noreply.github.com>
2024-12-10 21:17:58 +00:00
rcombs 6308e91de6 LoginForm: enable autocomplete on 2FA code field (#7019)
This should (fingers crossed) work in Safari, at least.
2024-12-10 21:16:45 +00:00
Renan Mav b7ddb07d90 fix: typo in filename list-converations (#7036) 2024-12-10 20:59:35 +00:00
dan d00879e628 Disambiguate feed component naming (#7040)
* Rename posts/Feed* -> posts/PostFeed*

* Rename notifications/Feed* -> notifications/NotificationFeed*
2024-12-10 20:57:53 +00:00
Eric Bailey e052f5e198 Refactor sidebar (#6971)
* Refactor RightNav

(cherry picked from commit 96bb02acfd2d7452df18a0e7410e6a7169a583ed)

* Better gutter handling

* Clean up styles

* Memoize breakpoints

* Format

* Comment

* Loosen spacing, handle overflow, smaller text to match prod

* Fix circular imports on native

* Return 0 instead of undefined for easier calculations

* Re-assign

* Fix

* Port over fix from subs/base

* Space out right nav feeds, widen sidebar to match prod

* Fix lost padding on home header

* Fix perf by not actually linking to new URL

* Remove underline on focus

* Foramt

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
2024-12-10 20:52:30 +00:00
dan f34e8d8cdf Limit feed prefetching to native (#7038) 2024-12-10 18:23:23 +00:00
dan 46e1e5cee6 Fix drawer swipe (#7007)
* Fix drawer swipe

* Remove existing setDrawerSwipeDisabled management

This is already pretty error-prone. And with tracking whether we're idle it's going to get more complicated. Let's pause and think.

* Move setDrawerSwipeDisabled logic into Pager

* Remove win/2 threshold

It feels super arbitrary and breaks muscle memory. If the gesture is reliable, we shouldn't need it.

* Maybe work around iOS freeze

* Tweak gestures, add comments

* Tune gestures
2024-12-10 04:40:40 +00:00
Dmitry Grachikov fec3352b68 Access content of extended block properly (block.Super instead of super()) (#7026) 2024-12-09 22:23:36 +00:00
Eric Bailey bc4b4a3cfe Add isSelfHosted to SessionAccount (#7028)
* Add isSelfHosted to SessionAccount

* Update snaps
2024-12-09 13:43:16 -06:00
Eric Bailey 63c0c7e621 Add back button to desktop threads (#7010) 2024-12-08 14:44:01 -06:00
115 changed files with 2115 additions and 934 deletions
-1
View File
@@ -1,7 +1,6 @@
# Copy this to `.env` and `.env.test` files
SENTRY_AUTH_TOKEN=
EXPO_PUBLIC_ENV=development
EXPO_PUBLIC_LOG_LEVEL=debug
EXPO_PUBLIC_LOG_DEBUG=
EXPO_PUBLIC_BUNDLE_IDENTIFIER=
+2 -2
View File
@@ -234,8 +234,8 @@ module.exports = function (config) {
'expo-font',
{
fonts: [
'./assets/fonts/inter/InterVariable.ttf',
'./assets/fonts/inter/InterVariable-Italic.ttf',
'./assets/fonts/inter/InterVariable.woff2',
'./assets/fonts/inter/InterVariable-Italic.woff2',
// Android only
'./assets/fonts/inter/Inter-Regular.otf',
'./assets/fonts/inter/Inter-Italic.otf',
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3 -1
View File
@@ -20,6 +20,7 @@ window.addEventListener('message', event => {
return
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const id = (event.data as {id: string}).id
if (!id) {
return
@@ -33,6 +34,7 @@ window.addEventListener('message', event => {
return
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const height = (event.data as {height: number}).height
if (height) {
embed.style.height = `${height}px`
@@ -47,7 +49,7 @@ window.addEventListener('message', event => {
* @returns
*/
function scan(node = document) {
const embeds = node.querySelectorAll('[data-bluesky-uri]')
const embeds = node.querySelectorAll<HTMLIFrameElement>('[data-bluesky-uri]')
for (let i = 0; i < embeds.length; i++) {
const id = String(Math.random()).slice(2)
+17
View File
@@ -0,0 +1,17 @@
export function applyTheme(theme: 'light' | 'dark') {
document.documentElement.classList.remove('light', 'dark')
document.documentElement.classList.add(theme)
}
export function initColorMode() {
applyTheme(
window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light',
)
window
.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', mql => {
applyTheme(mql.matches ? 'dark' : 'light')
})
}
+1 -1
View File
@@ -37,7 +37,7 @@ export function Container({
return (
<div
ref={ref}
className="w-full bg-white hover:bg-neutral-50 relative transition-colors max-w-[600px] min-w-[300px] flex border rounded-xl"
className="w-full bg-white text-black hover:bg-neutral-50 dark:bg-dimmedBg dark:hover:bg-dimmedBgLighten relative transition-colors max-w-[600px] min-w-[300px] flex border dark:border-slate-600 dark:text-slate-200 rounded-xl"
onClick={() => {
if (ref.current && href) {
// forwardRef requires preact/compat - let's keep it simple
+20 -14
View File
@@ -78,9 +78,9 @@ export function Embed({
return (
<Link
href={`/profile/${record.author.did}/post/${getRkey(record)}`}
className="transition-colors hover:bg-neutral-100 border rounded-lg p-2 gap-1.5 w-full flex flex-col">
className="transition-colors hover:bg-neutral-100 dark:hover:bg-slate-700 border dark:border-slate-600 rounded-lg p-2 gap-1.5 w-full flex flex-col">
<div className="flex gap-1.5 items-center">
<div className="w-4 h-4 overflow-hidden rounded-full bg-neutral-300 shrink-0">
<div className="w-4 h-4 overflow-hidden rounded-full bg-neutral-300 dark:bg-slate-700 shrink-0">
<img
src={record.author.avatar}
style={isAuthorLabeled ? {filter: 'blur(1.5px)'} : undefined}
@@ -88,7 +88,7 @@ export function Embed({
</div>
<p className="line-clamp-1 text-sm">
<span className="font-bold">{record.author.displayName}</span>
<span className="text-textLight ml-1">
<span className="text-textLight dark:text-textDimmed ml-1">
@{record.author.handle}
</span>
</p>
@@ -209,7 +209,7 @@ function Info({children}: {children: ComponentChildren}) {
return (
<div className="w-full rounded-lg border py-2 px-2.5 flex-row flex gap-2 bg-neutral-50">
<img src={infoIcon} className="w-4 h-4 shrink-0 mt-0.5" />
<p className="text-sm text-textLight">{children}</p>
<p className="text-sm text-textLight dark:text-textDimmed">{children}</p>
</div>
)
}
@@ -308,7 +308,7 @@ function ExternalEmbed({
return (
<Link
href={content.external.uri}
className="w-full rounded-lg overflow-hidden border flex flex-col items-stretch"
className="w-full rounded-lg overflow-hidden border dark:border-slate-600 flex flex-col items-stretch"
disableTracking>
{content.external.thumb && (
<img
@@ -317,11 +317,11 @@ function ExternalEmbed({
/>
)}
<div className="py-3 px-4">
<p className="text-sm text-textLight line-clamp-1">
<p className="text-sm text-textLight dark:text-textDimmed line-clamp-1">
{toNiceDomain(content.external.uri)}
</p>
<p className="font-semibold line-clamp-3">{content.external.title}</p>
<p className="text-sm text-textLight line-clamp-2 mt-0.5">
<p className="text-sm text-textLight dark:text-textDimmed line-clamp-2 mt-0.5">
{content.external.description}
</p>
</div>
@@ -345,23 +345,29 @@ function GenericWithImageEmbed({
return (
<Link
href={href}
className="w-full rounded-lg border py-2 px-3 flex flex-col gap-2">
className="w-full rounded-lg border dark:border-slate-600 py-2 px-3 flex flex-col gap-2">
<div className="flex gap-2.5 items-center">
{image ? (
<img
src={image}
alt={title}
className="w-8 h-8 rounded-md bg-neutral-300 shrink-0"
className="w-8 h-8 rounded-md bg-neutral-300 dark:bg-slate-700 shrink-0"
/>
) : (
<div className="w-8 h-8 rounded-md bg-brand shrink-0" />
)}
<div className="flex-1">
<p className="font-bold text-sm">{title}</p>
<p className="text-textLight text-sm">{subtitle}</p>
<p className="text-textLight dark:text-textDimmed text-sm">
{subtitle}
</p>
</div>
</div>
{description && <p className="text-textLight text-sm">{description}</p>}
{description && (
<p className="text-textLight dark:text-textDimmed text-sm">
{description}
</p>
)}
</Link>
)
}
@@ -406,7 +412,7 @@ function StarterPackEmbed({
return (
<Link
href={starterPackHref}
className="w-full rounded-lg overflow-hidden border flex flex-col items-stretch">
className="w-full rounded-lg overflow-hidden border dark:border-slate-600 flex flex-col items-stretch">
<img src={imageUri} className="aspect-[1.91/1] object-cover" />
<div className="py-3 px-4">
<div className="flex space-x-2 items-center">
@@ -415,7 +421,7 @@ function StarterPackEmbed({
<p className="font-semibold leading-[21px]">
{content.record.name}
</p>
<p className="text-sm text-textLight line-clamp-2 leading-[18px]">
<p className="text-sm text-textLight dark:text-textDimmed line-clamp-2 leading-[18px]">
Starter pack by{' '}
{content.creator.displayName || `@${content.creator.handle}`}
</p>
@@ -425,7 +431,7 @@ function StarterPackEmbed({
<p className="text-sm mt-1">{content.record.description}</p>
)}
{!!content.joinedAllTimeCount && content.joinedAllTimeCount > 50 && (
<p className="text-sm font-semibold text-textLight mt-1">
<p className="text-sm font-semibold text-textLight dark:text-textDimmed mt-1">
{content.joinedAllTimeCount} users have joined!
</p>
)}
+10 -8
View File
@@ -38,7 +38,7 @@ export function Post({thread}: Props) {
<div className="flex-1 flex-col flex gap-2" lang={record?.langs?.[0]}>
<div className="flex gap-2.5 items-center cursor-pointer">
<Link href={`/profile/${post.author.did}`} className="rounded-full">
<div className="w-10 h-10 overflow-hidden rounded-full bg-neutral-300 shrink-0">
<div className="w-10 h-10 overflow-hidden rounded-full bg-neutral-300 dark:bg-slate-700 shrink-0">
<img
src={post.author.avatar}
style={isAuthorLabeled ? {filter: 'blur(2.5px)'} : undefined}
@@ -53,7 +53,7 @@ export function Post({thread}: Props) {
</Link>
<Link
href={`/profile/${post.author.did}`}
className="text-[15px] text-textLight hover:underline line-clamp-1">
className="text-[15px] text-textLight dark:text-textDimmed hover:underline line-clamp-1">
<p>@{post.author.handle}</p>
</Link>
</div>
@@ -69,15 +69,15 @@ export function Post({thread}: Props) {
<Link href={href}>
<time
datetime={new Date(post.indexedAt).toISOString()}
className="text-textLight mt-1 text-sm hover:underline">
className="text-textLight dark:text-textDimmed mt-1 text-sm hover:underline">
{niceDate(post.indexedAt)}
</time>
</Link>
<div className="border-t w-full pt-2.5 flex items-center gap-5 text-sm cursor-pointer">
<div className="border-t dark:border-slate-600 w-full pt-2.5 flex items-center gap-5 text-sm cursor-pointer">
{!!post.likeCount && (
<div className="flex items-center gap-2 cursor-pointer">
<img src={likeIcon} className="w-5 h-5" />
<p className="font-bold text-neutral-500 mb-px">
<p className="font-bold text-neutral-500 dark:text-neutral-300 mb-px">
{prettyNumber(post.likeCount)}
</p>
</div>
@@ -85,17 +85,19 @@ export function Post({thread}: Props) {
{!!post.repostCount && (
<div className="flex items-center gap-2 cursor-pointer">
<img src={repostIcon} className="w-5 h-5" />
<p className="font-bold text-neutral-500 mb-px">
<p className="font-bold text-neutral-500 dark:text-neutral-300 mb-px">
{prettyNumber(post.repostCount)}
</p>
</div>
)}
<div className="flex items-center gap-2 cursor-pointer">
<img src={replyIcon} className="w-5 h-5" />
<p className="font-bold text-neutral-500 mb-px">Reply</p>
<p className="font-bold text-neutral-500 dark:text-neutral-300 mb-px">
Reply
</p>
</div>
<div className="flex-1" />
<p className="cursor-pointer text-brand font-bold hover:underline hidden min-[450px]:inline">
<p className="cursor-pointer text-brand dark:text-brandLighten font-bold hover:underline hidden min-[450px]:inline">
{post.replyCount
? `Read ${prettyNumber(post.replyCount)} ${
post.replyCount > 1 ? 'replies' : 'reply'
+4
View File
@@ -5,3 +5,7 @@
.break-word {
word-break: break-word;
}
:root {
color-scheme: light dark;
}
+18 -13
View File
@@ -6,6 +6,7 @@ import {useEffect, useMemo, useRef, useState} from 'preact/hooks'
import arrowBottom from '../../assets/arrowBottom_stroke2_corner0_rounded.svg'
import logo from '../../assets/logo.svg'
import {initColorMode} from '../color-mode'
import {Container} from '../components/container'
import {Link} from '../components/link'
import {Post} from '../components/post'
@@ -21,6 +22,8 @@ export const EMBED_SCRIPT = `${EMBED_SERVICE}/static/embed.js`
const root = document.getElementById('app')
if (!root) throw new Error('No root element')
initColorMode()
const agent = new BskyAgent({
service: 'https://public.api.bsky.app',
})
@@ -108,7 +111,7 @@ function LandingPage() {
}, [uri])
return (
<main className="w-full min-h-screen flex flex-col items-center gap-8 py-14 px-4 md:pt-32">
<main className="w-full min-h-screen flex flex-col items-center gap-8 py-14 px-4 md:pt-32 dark:bg-dimmedBgDarken dark:text-slate-200">
<Link
href="https://bsky.social/about"
className="transition-transform hover:scale-110">
@@ -121,20 +124,22 @@ function LandingPage() {
type="text"
value={uri}
onInput={e => setUri(e.currentTarget.value)}
className="border rounded-lg py-3 w-full max-w-[600px] px-4"
className="border rounded-lg py-3 w-full max-w-[600px] px-4 dark:bg-dimmedBg dark:border-slate-500"
placeholder={DEFAULT_POST}
/>
<img src={arrowBottom} className="w-6" />
<img src={arrowBottom} className="w-6 dark:invert" />
{loading ? (
<Skeleton />
<div className="w-full max-w-[600px]">
<Skeleton />
</div>
) : (
<div className="w-full max-w-[600px] gap-8 flex flex-col">
{!error && thread && uri && <Snippet thread={thread} />}
{!error && thread && <Post thread={thread} key={thread.post.uri} />}
{error && (
<div className="w-full border border-red-500 bg-red-50 px-4 py-3 rounded-lg">
<div className="w-full border border-red-500 bg-red-500/10 px-4 py-3 rounded-lg">
<p className="text-red-500 text-center">{error}</p>
</div>
)}
@@ -149,15 +154,15 @@ function Skeleton() {
<Container>
<div className="flex-1 flex-col flex gap-2 pb-8">
<div className="flex gap-2.5 items-center">
<div className="w-10 h-10 overflow-hidden rounded-full bg-neutral-100 shrink-0 animate-pulse" />
<div className="w-10 h-10 overflow-hidden rounded-full bg-neutral-100 dark:bg-slate-700 shrink-0 animate-pulse" />
<div className="flex-1">
<div className="bg-neutral-100 animate-pulse w-64 h-4 rounded" />
<div className="bg-neutral-100 animate-pulse w-32 h-3 mt-1 rounded" />
<div className="bg-neutral-100 dark:bg-slate-700 animate-pulse w-64 h-4 rounded" />
<div className="bg-neutral-100 dark:bg-slate-700 animate-pulse w-32 h-3 mt-1 rounded" />
</div>
</div>
<div className="w-full h-4 mt-2 bg-neutral-100 rounded animate-pulse" />
<div className="w-5/6 h-4 bg-neutral-100 rounded animate-pulse" />
<div className="w-3/4 h-4 bg-neutral-100 rounded animate-pulse" />
<div className="w-full h-4 mt-2 bg-neutral-100 dark:bg-slate-700 rounded animate-pulse" />
<div className="w-5/6 h-4 bg-neutral-100 dark:bg-slate-700 rounded animate-pulse" />
<div className="w-3/4 h-4 bg-neutral-100 dark:bg-slate-700 rounded animate-pulse" />
</div>
</Container>
)
@@ -220,7 +225,7 @@ function Snippet({thread}: {thread: AppBskyFeedDefs.ThreadViewPost}) {
ref={ref}
type="text"
value={snippet}
className="border rounded-lg py-3 w-full px-4"
className="border rounded-lg py-3 w-full px-4 dark:bg-dimmedBg dark:border-slate-500"
readOnly
autoFocus
onFocus={() => {
@@ -228,7 +233,7 @@ function Snippet({thread}: {thread: AppBskyFeedDefs.ThreadViewPost}) {
}}
/>
<button
className="rounded-lg bg-brand text-white color-white py-3 px-4 whitespace-nowrap min-w-28"
className="rounded-lg bg-brand text-white py-3 px-4 whitespace-nowrap min-w-28"
onClick={() => {
ref.current?.focus()
ref.current?.select()
+6 -3
View File
@@ -4,6 +4,7 @@ import {AppBskyFeedDefs, AtpAgent} from '@atproto/api'
import {h, render} from 'preact'
import logo from '../../assets/logo.svg'
import {initColorMode} from '../color-mode'
import {Container} from '../components/container'
import {Link} from '../components/link'
import {Post} from '../components/post'
@@ -21,6 +22,8 @@ if (!uri) {
throw new Error('No uri in path')
}
initColorMode()
agent
.getPostThread({
uri,
@@ -55,13 +58,13 @@ function PwiOptOut({thread}: {thread: AppBskyFeedDefs.ThreadViewPost}) {
<img src={logo} className="h-6" />
</Link>
<div className="w-full py-12 gap-4 flex flex-col items-center">
<p className="max-w-80 text-center w-full text-textLight">
<p className="max-w-80 text-center w-full text-textLight dark:text-textDimmed">
The author of this post has requested their posts not be displayed on
external sites.
</p>
<Link
href={href}
className="max-w-80 rounded-lg bg-brand text-white color-white text-center py-1 px-4 w-full mx-auto">
className="max-w-80 rounded-lg bg-brand text-white text-center py-1 px-4 w-full mx-auto">
View on Bluesky
</Link>
</div>
@@ -77,7 +80,7 @@ function ErrorMessage() {
className="transition-transform hover:scale-110 absolute top-4 right-4">
<img src={logo} className="h-6" />
</Link>
<p className="my-16 text-center w-full text-textLight">
<p className="my-16 text-center w-full text-textLight dark:text-textDimmed">
Post not found, it may have been deleted.
</p>
</Container>
+8
View File
@@ -1,11 +1,19 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
darkMode: ['variant', [
'&:is(.dark *):not(:is(.dark .light *))',
]],
theme: {
extend: {
colors: {
brand: 'rgb(10,122,255)',
brandLighten: 'rgb(32,139,254)',
textLight: 'rgb(66,87,108)',
textDimmed: 'rgb(174,187,201)',
dimmedBgLighten: 'rgb(30,41,54)',
dimmedBg: 'rgb(22,30,39)',
dimmedBgDarken: 'rgb(18,25,32)',
},
},
},
+1 -2
View File
@@ -1,4 +1,3 @@
{
"compilerOptions": {
"target": "ES5",
@@ -20,5 +19,5 @@
"jsxFragmentFactory": "Fragment",
"downlevelIteration": true
},
"include": ["src", "vite.config.ts"]
"include": ["src", "snippet", "vite.config.ts"]
}
+3 -4
View File
@@ -13,8 +13,7 @@
<!-- Hello Humans! API docs at https://atproto.com -->
<link rel="preload" as="font" type="font/ttf" href="{{ staticCDNHost }}/static/media/InterVariable.c9f788f6e7ebaec75d7c.ttf">
<link rel="preload" as="font" type="font/ttf" href="{{ staticCDNHost }}/static/media/InterVariable-Italic.55d6a3f35e9b605ba6f4.ttf">
<link rel="preload" as="font" type="font/ttf" href="{{ staticCDNHost }}/static/media/InterVariable.c504db5c06caaf7cdfba.woff2">
<style>
/**
@@ -26,14 +25,14 @@
*/
@font-face {
font-family: 'InterVariable';
src: url("{{ staticCDNHost }}/static/media/InterVariable.c9f788f6e7ebaec75d7c.ttf") format('truetype');
src: url("{{ staticCDNHost }}/static/media/InterVariable.c504db5c06caaf7cdfba.woff2") format('woff2');
font-weight: 300 1000;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'InterVariableItalic';
src: url("{{ staticCDNHost }}/static/media/InterVariable-Italic.55d6a3f35e9b605ba6f4.ttf") format('truetype');
src: url("{{ staticCDNHost }}/static/media/InterVariable-Italic.01dcbad1bac635f9c9cd.woff2") format('woff2');
font-weight: 300 1000;
font-style: italic;
font-display: swap;
+2 -2
View File
@@ -12,14 +12,14 @@
{# don't include the bundle on non-404 error pages #}
{% block head_bundle %}
{% if statusCode == 404 %}
{{ super() }}
{{ block.Super }}
{% else %}
{% endif %}
{% endblock %}
{%- block body_all %}
{% if statusCode == 404 %}
{{ super() }}
{{ block.Super }}
{% else %}
<h1>{{ statusCode }}: Server Error</h1>
<p>Sorry about that! Our <a href="https://bluesky.statuspage.io/">Status Page</a> might have more context.
+2
View File
@@ -14,6 +14,8 @@ if (process.env.BSKY_PROFILE) {
cfg.cacheVersion += ':PROFILE'
}
cfg.resolver.assetExts = [...cfg.resolver.assetExts, 'woff2']
cfg.resolver.resolveRequest = (context, moduleName, platform) => {
// HACK: manually resolve a few packages that use `exports` in `package.json`.
// A proper solution is to enable `unstable_enablePackageExports` but this needs careful testing.
+3 -3
View File
@@ -62,9 +62,9 @@
"@expo/webpack-config": "^19.0.0",
"@floating-ui/dom": "^1.6.3",
"@floating-ui/react-dom": "^2.0.8",
"@formatjs/intl-locale": "^4.0.0",
"@formatjs/intl-numberformat": "^8.10.3",
"@formatjs/intl-pluralrules": "^5.2.14",
"@formatjs/intl-locale": "^4.2.8",
"@formatjs/intl-numberformat": "^8.15.1",
"@formatjs/intl-pluralrules": "^5.4.1",
"@fortawesome/fontawesome-svg-core": "^6.1.1",
"@fortawesome/free-regular-svg-icons": "^6.1.1",
"@fortawesome/free-solid-svg-icons": "^6.1.1",
@@ -0,0 +1,876 @@
diff --git a/node_modules/react-native-drawer-layout/lib/commonjs/index.js b/node_modules/react-native-drawer-layout/lib/commonjs/index.js
index 3dce76e..6c4b3e5 100644
--- a/node_modules/react-native-drawer-layout/lib/commonjs/index.js
+++ b/node_modules/react-native-drawer-layout/lib/commonjs/index.js
@@ -9,6 +9,12 @@ Object.defineProperty(exports, "Drawer", {
return _Drawer.Drawer;
}
});
+Object.defineProperty(exports, "DrawerGestureContext", {
+ enumerable: true,
+ get: function () {
+ return _DrawerGestureContext.DrawerGestureContext;
+ }
+});
Object.defineProperty(exports, "DrawerProgressContext", {
enumerable: true,
get: function () {
@@ -21,6 +27,7 @@ Object.defineProperty(exports, "useDrawerProgress", {
return _useDrawerProgress.useDrawerProgress;
}
});
+var _DrawerGestureContext = require("./utils/DrawerGestureContext.js");
var _DrawerProgressContext = require("./utils/DrawerProgressContext.js");
var _useDrawerProgress = require("./utils/useDrawerProgress.js");
var _Drawer = require("./views/Drawer");
diff --git a/node_modules/react-native-drawer-layout/lib/commonjs/utils/DrawerGestureContext.js b/node_modules/react-native-drawer-layout/lib/commonjs/utils/DrawerGestureContext.js
new file mode 100644
index 0000000..de6d793
--- /dev/null
+++ b/node_modules/react-native-drawer-layout/lib/commonjs/utils/DrawerGestureContext.js
@@ -0,0 +1,11 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.DrawerGestureContext = void 0;
+var React = _interopRequireWildcard(require("react"));
+function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
+function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
+const DrawerGestureContext = exports.DrawerGestureContext = /*#__PURE__*/React.createContext(undefined);
+//# sourceMappingURL=DrawerGestureContext.js.map
\ No newline at end of file
diff --git a/node_modules/react-native-drawer-layout/lib/commonjs/views/Drawer.native.js b/node_modules/react-native-drawer-layout/lib/commonjs/views/Drawer.native.js
index fd65ab8..86a9a6a 100644
--- a/node_modules/react-native-drawer-layout/lib/commonjs/views/Drawer.native.js
+++ b/node_modules/react-native-drawer-layout/lib/commonjs/views/Drawer.native.js
@@ -8,6 +8,7 @@ var React = _interopRequireWildcard(require("react"));
var _reactNative = require("react-native");
var _reactNativeReanimated = _interopRequireWildcard(require("react-native-reanimated"));
var _useLatestCallback = _interopRequireDefault(require("use-latest-callback"));
+var _DrawerGestureContext = require("../utils/DrawerGestureContext.js");
var _DrawerProgressContext = require("../utils/DrawerProgressContext.js");
var _getDrawerWidth = require("../utils/getDrawerWidth.js");
var _GestureHandler = require("./GestureHandler");
@@ -79,38 +80,38 @@ function Drawer({
return () => hideStatusBar(false);
}, [isOpen, hideStatusBarOnOpen, statusBarAnimation, hideStatusBar]);
const interactionHandleRef = React.useRef(null);
- const startInteraction = () => {
+ const startInteraction = React.useCallback(() => {
interactionHandleRef.current = _reactNative.InteractionManager.createInteractionHandle();
- };
- const endInteraction = () => {
+ }, []);
+ const endInteraction = React.useCallback(() => {
if (interactionHandleRef.current != null) {
_reactNative.InteractionManager.clearInteractionHandle(interactionHandleRef.current);
interactionHandleRef.current = null;
}
- };
- const hideKeyboard = () => {
+ }, []);
+ const hideKeyboard = React.useCallback(() => {
if (keyboardDismissMode === 'on-drag') {
_reactNative.Keyboard.dismiss();
}
- };
- const onGestureBegin = () => {
+ }, [keyboardDismissMode]);
+ const onGestureBegin = React.useCallback(() => {
onGestureStart?.();
startInteraction();
hideKeyboard();
hideStatusBar(true);
- };
- const onGestureFinish = () => {
+ }, [onGestureStart, startInteraction, hideKeyboard, hideStatusBar]);
+ const onGestureFinish = React.useCallback(() => {
onGestureEnd?.();
endInteraction();
- };
- const onGestureAbort = () => {
+ }, [onGestureEnd, endInteraction]);
+ const onGestureAbort = React.useCallback(() => {
onGestureCancel?.();
endInteraction();
- };
+ }, [onGestureCancel, endInteraction]);
// FIXME: Currently hitSlop is broken when on Android when drawer is on right
// https://github.com/software-mansion/react-native-gesture-handler/issues/569
- const hitSlop = isRight ?
+ const hitSlop = React.useMemo(() => isRight ?
// Extend hitSlop to the side of the screen when drawer is closed
// This lets the user drag the drawer from the side of the screen
{
@@ -119,7 +120,7 @@ function Drawer({
} : {
left: 0,
width: isOpen ? undefined : swipeEdgeWidth
- };
+ }, [isRight, isOpen, swipeEdgeWidth]);
const touchStartX = (0, _reactNativeReanimated.useSharedValue)(0);
const touchX = (0, _reactNativeReanimated.useSharedValue)(0);
const translationX = (0, _reactNativeReanimated.useSharedValue)(getDrawerTranslationX(open));
@@ -158,40 +159,43 @@ function Drawer({
}, [getDrawerTranslationX, handleAnimationEnd, handleAnimationStart, onClose, onOpen, touchStartX, touchX, translationX]);
React.useEffect(() => toggleDrawer(open), [open, toggleDrawer]);
const startX = (0, _reactNativeReanimated.useSharedValue)(0);
- let pan = _GestureHandler.Gesture?.Pan().onBegin(event => {
- 'worklet';
+ const pan = React.useMemo(() => {
+ let panGesture = _GestureHandler.Gesture?.Pan().onBegin(event => {
+ 'worklet';
- startX.value = translationX.value;
- gestureState.value = event.state;
- touchStartX.value = event.x;
- }).onStart(() => {
- 'worklet';
+ startX.value = translationX.value;
+ gestureState.value = event.state;
+ touchStartX.value = event.x;
+ }).onStart(() => {
+ 'worklet';
- (0, _reactNativeReanimated.runOnJS)(onGestureBegin)();
- }).onChange(event => {
- 'worklet';
+ (0, _reactNativeReanimated.runOnJS)(onGestureBegin)();
+ }).onChange(event => {
+ 'worklet';
- touchX.value = event.x;
- translationX.value = startX.value + event.translationX;
- gestureState.value = event.state;
- }).onEnd((event, success) => {
- 'worklet';
+ touchX.value = event.x;
+ translationX.value = startX.value + event.translationX;
+ gestureState.value = event.state;
+ }).onEnd((event, success) => {
+ 'worklet';
- gestureState.value = event.state;
- if (!success) {
- (0, _reactNativeReanimated.runOnJS)(onGestureAbort)();
+ gestureState.value = event.state;
+ if (!success) {
+ (0, _reactNativeReanimated.runOnJS)(onGestureAbort)();
+ }
+ const nextOpen = Math.abs(event.translationX) > SWIPE_MIN_OFFSET && Math.abs(event.translationX) > swipeMinVelocity || Math.abs(event.translationX) > swipeMinDistance ? drawerPosition === 'left' ?
+ // If swiped to right, open the drawer, otherwise close it
+ (event.velocityX === 0 ? event.translationX : event.velocityX) > 0 :
+ // If swiped to left, open the drawer, otherwise close it
+ (event.velocityX === 0 ? event.translationX : event.velocityX) < 0 : open;
+ toggleDrawer(nextOpen, event.velocityX);
+ (0, _reactNativeReanimated.runOnJS)(onGestureFinish)();
+ }).activeOffsetX([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET]).failOffsetY([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET]).hitSlop(hitSlop).enabled(drawerType !== 'permanent' && swipeEnabled);
+ if (panGesture && configureGestureHandler) {
+ panGesture = configureGestureHandler(panGesture);
}
- const nextOpen = Math.abs(event.translationX) > SWIPE_MIN_OFFSET && Math.abs(event.translationX) > swipeMinVelocity || Math.abs(event.translationX) > swipeMinDistance ? drawerPosition === 'left' ?
- // If swiped to right, open the drawer, otherwise close it
- (event.velocityX === 0 ? event.translationX : event.velocityX) > 0 :
- // If swiped to left, open the drawer, otherwise close it
- (event.velocityX === 0 ? event.translationX : event.velocityX) < 0 : open;
- toggleDrawer(nextOpen, event.velocityX);
- (0, _reactNativeReanimated.runOnJS)(onGestureFinish)();
- }).activeOffsetX([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET]).failOffsetY([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET]).hitSlop(hitSlop).enabled(drawerType !== 'permanent' && swipeEnabled);
- if (pan && configureGestureHandler) {
- pan = configureGestureHandler(pan);
- }
+ return panGesture;
+ }, [configureGestureHandler, drawerPosition, drawerType, gestureState, hitSlop, onGestureBegin, onGestureAbort, onGestureFinish, open, startX, swipeEnabled, swipeMinDistance, swipeMinVelocity, toggleDrawer, touchStartX, touchX, translationX]);
const translateX = (0, _reactNativeReanimated.useDerivedValue)(() => {
// Comment stolen from react-native-gesture-handler/DrawerLayout
//
@@ -254,35 +258,38 @@ function Drawer({
style: [styles.container, style],
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_DrawerProgressContext.DrawerProgressContext.Provider, {
value: progress,
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_GestureHandler.GestureDetector, {
- gesture: pan,
- children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNativeReanimated.default.View, {
- style: [styles.main, {
- flexDirection: drawerType === 'permanent' ? isRight && direction === 'ltr' || !isRight && direction === 'rtl' ? 'row' : 'row-reverse' : 'row'
- }],
- children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNativeReanimated.default.View, {
- style: [styles.content, contentAnimatedStyle],
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
- accessibilityElementsHidden: isOpen && drawerType !== 'permanent',
- importantForAccessibility: isOpen && drawerType !== 'permanent' ? 'no-hide-descendants' : 'auto',
- style: styles.content,
- children: children
- }), drawerType !== 'permanent' ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_Overlay.Overlay, {
- open: open,
- progress: progress,
- onPress: () => toggleDrawer(false),
- style: overlayStyle,
- accessibilityLabel: overlayAccessibilityLabel
- }) : null]
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, {
- removeClippedSubviews: _reactNative.Platform.OS !== 'ios',
- style: [styles.drawer, {
- width: drawerWidth,
- position: drawerType === 'permanent' ? 'relative' : 'absolute',
- zIndex: drawerType === 'back' ? -1 : 0
- }, drawerAnimatedStyle, drawerStyle],
- children: renderDrawerContent()
- })]
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_DrawerGestureContext.DrawerGestureContext.Provider, {
+ value: pan,
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_GestureHandler.GestureDetector, {
+ gesture: pan,
+ children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNativeReanimated.default.View, {
+ style: [styles.main, {
+ flexDirection: drawerType === 'permanent' ? isRight && direction === 'ltr' || !isRight && direction === 'rtl' ? 'row' : 'row-reverse' : 'row'
+ }],
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNativeReanimated.default.View, {
+ style: [styles.content, contentAnimatedStyle],
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
+ accessibilityElementsHidden: isOpen && drawerType !== 'permanent',
+ importantForAccessibility: isOpen && drawerType !== 'permanent' ? 'no-hide-descendants' : 'auto',
+ style: styles.content,
+ children: children
+ }), drawerType !== 'permanent' ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_Overlay.Overlay, {
+ open: open,
+ progress: progress,
+ onPress: () => toggleDrawer(false),
+ style: overlayStyle,
+ accessibilityLabel: overlayAccessibilityLabel
+ }) : null]
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, {
+ removeClippedSubviews: _reactNative.Platform.OS !== 'ios',
+ style: [styles.drawer, {
+ width: drawerWidth,
+ position: drawerType === 'permanent' ? 'relative' : 'absolute',
+ zIndex: drawerType === 'back' ? -1 : 0
+ }, drawerAnimatedStyle, drawerStyle],
+ children: renderDrawerContent()
+ })]
+ })
})
})
})
diff --git a/node_modules/react-native-drawer-layout/lib/module/index.js b/node_modules/react-native-drawer-layout/lib/module/index.js
index a600e51..f08275c 100644
--- a/node_modules/react-native-drawer-layout/lib/module/index.js
+++ b/node_modules/react-native-drawer-layout/lib/module/index.js
@@ -1,5 +1,6 @@
"use strict";
+export { DrawerGestureContext } from "./utils/DrawerGestureContext.js";
export { DrawerProgressContext } from "./utils/DrawerProgressContext.js";
export { useDrawerProgress } from "./utils/useDrawerProgress.js";
export { Drawer } from './views/Drawer';
diff --git a/node_modules/react-native-drawer-layout/lib/module/utils/DrawerGestureContext.js b/node_modules/react-native-drawer-layout/lib/module/utils/DrawerGestureContext.js
new file mode 100644
index 0000000..1adaa9c
--- /dev/null
+++ b/node_modules/react-native-drawer-layout/lib/module/utils/DrawerGestureContext.js
@@ -0,0 +1,5 @@
+"use strict";
+
+import * as React from 'react';
+export const DrawerGestureContext = /*#__PURE__*/React.createContext(undefined);
+//# sourceMappingURL=DrawerGestureContext.js.map
\ No newline at end of file
diff --git a/node_modules/react-native-drawer-layout/lib/module/views/Drawer.native.js b/node_modules/react-native-drawer-layout/lib/module/views/Drawer.native.js
index 6d07126..981d9f8 100644
--- a/node_modules/react-native-drawer-layout/lib/module/views/Drawer.native.js
+++ b/node_modules/react-native-drawer-layout/lib/module/views/Drawer.native.js
@@ -4,6 +4,7 @@ import * as React from 'react';
import { I18nManager, InteractionManager, Keyboard, Platform, StatusBar, StyleSheet, useWindowDimensions, View } from 'react-native';
import Animated, { interpolate, ReduceMotion, runOnJS, useAnimatedStyle, useDerivedValue, useSharedValue, withSpring } from 'react-native-reanimated';
import useLatestCallback from 'use-latest-callback';
+import { DrawerGestureContext } from "../utils/DrawerGestureContext.js";
import { DrawerProgressContext } from "../utils/DrawerProgressContext.js";
import { getDrawerWidth } from "../utils/getDrawerWidth.js";
import { Gesture, GestureDetector, GestureHandlerRootView, GestureState } from './GestureHandler';
@@ -72,38 +73,38 @@ export function Drawer({
return () => hideStatusBar(false);
}, [isOpen, hideStatusBarOnOpen, statusBarAnimation, hideStatusBar]);
const interactionHandleRef = React.useRef(null);
- const startInteraction = () => {
+ const startInteraction = React.useCallback(() => {
interactionHandleRef.current = InteractionManager.createInteractionHandle();
- };
- const endInteraction = () => {
+ }, []);
+ const endInteraction = React.useCallback(() => {
if (interactionHandleRef.current != null) {
InteractionManager.clearInteractionHandle(interactionHandleRef.current);
interactionHandleRef.current = null;
}
- };
- const hideKeyboard = () => {
+ }, []);
+ const hideKeyboard = React.useCallback(() => {
if (keyboardDismissMode === 'on-drag') {
Keyboard.dismiss();
}
- };
- const onGestureBegin = () => {
+ }, [keyboardDismissMode]);
+ const onGestureBegin = React.useCallback(() => {
onGestureStart?.();
startInteraction();
hideKeyboard();
hideStatusBar(true);
- };
- const onGestureFinish = () => {
+ }, [onGestureStart, startInteraction, hideKeyboard, hideStatusBar]);
+ const onGestureFinish = React.useCallback(() => {
onGestureEnd?.();
endInteraction();
- };
- const onGestureAbort = () => {
+ }, [onGestureEnd, endInteraction]);
+ const onGestureAbort = React.useCallback(() => {
onGestureCancel?.();
endInteraction();
- };
+ }, [onGestureCancel, endInteraction]);
// FIXME: Currently hitSlop is broken when on Android when drawer is on right
// https://github.com/software-mansion/react-native-gesture-handler/issues/569
- const hitSlop = isRight ?
+ const hitSlop = React.useMemo(() => isRight ?
// Extend hitSlop to the side of the screen when drawer is closed
// This lets the user drag the drawer from the side of the screen
{
@@ -112,7 +113,7 @@ export function Drawer({
} : {
left: 0,
width: isOpen ? undefined : swipeEdgeWidth
- };
+ }, [isRight, isOpen, swipeEdgeWidth]);
const touchStartX = useSharedValue(0);
const touchX = useSharedValue(0);
const translationX = useSharedValue(getDrawerTranslationX(open));
@@ -151,40 +152,43 @@ export function Drawer({
}, [getDrawerTranslationX, handleAnimationEnd, handleAnimationStart, onClose, onOpen, touchStartX, touchX, translationX]);
React.useEffect(() => toggleDrawer(open), [open, toggleDrawer]);
const startX = useSharedValue(0);
- let pan = Gesture?.Pan().onBegin(event => {
- 'worklet';
+ const pan = React.useMemo(() => {
+ let panGesture = Gesture?.Pan().onBegin(event => {
+ 'worklet';
- startX.value = translationX.value;
- gestureState.value = event.state;
- touchStartX.value = event.x;
- }).onStart(() => {
- 'worklet';
+ startX.value = translationX.value;
+ gestureState.value = event.state;
+ touchStartX.value = event.x;
+ }).onStart(() => {
+ 'worklet';
- runOnJS(onGestureBegin)();
- }).onChange(event => {
- 'worklet';
+ runOnJS(onGestureBegin)();
+ }).onChange(event => {
+ 'worklet';
- touchX.value = event.x;
- translationX.value = startX.value + event.translationX;
- gestureState.value = event.state;
- }).onEnd((event, success) => {
- 'worklet';
+ touchX.value = event.x;
+ translationX.value = startX.value + event.translationX;
+ gestureState.value = event.state;
+ }).onEnd((event, success) => {
+ 'worklet';
- gestureState.value = event.state;
- if (!success) {
- runOnJS(onGestureAbort)();
+ gestureState.value = event.state;
+ if (!success) {
+ runOnJS(onGestureAbort)();
+ }
+ const nextOpen = Math.abs(event.translationX) > SWIPE_MIN_OFFSET && Math.abs(event.translationX) > swipeMinVelocity || Math.abs(event.translationX) > swipeMinDistance ? drawerPosition === 'left' ?
+ // If swiped to right, open the drawer, otherwise close it
+ (event.velocityX === 0 ? event.translationX : event.velocityX) > 0 :
+ // If swiped to left, open the drawer, otherwise close it
+ (event.velocityX === 0 ? event.translationX : event.velocityX) < 0 : open;
+ toggleDrawer(nextOpen, event.velocityX);
+ runOnJS(onGestureFinish)();
+ }).activeOffsetX([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET]).failOffsetY([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET]).hitSlop(hitSlop).enabled(drawerType !== 'permanent' && swipeEnabled);
+ if (panGesture && configureGestureHandler) {
+ panGesture = configureGestureHandler(panGesture);
}
- const nextOpen = Math.abs(event.translationX) > SWIPE_MIN_OFFSET && Math.abs(event.translationX) > swipeMinVelocity || Math.abs(event.translationX) > swipeMinDistance ? drawerPosition === 'left' ?
- // If swiped to right, open the drawer, otherwise close it
- (event.velocityX === 0 ? event.translationX : event.velocityX) > 0 :
- // If swiped to left, open the drawer, otherwise close it
- (event.velocityX === 0 ? event.translationX : event.velocityX) < 0 : open;
- toggleDrawer(nextOpen, event.velocityX);
- runOnJS(onGestureFinish)();
- }).activeOffsetX([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET]).failOffsetY([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET]).hitSlop(hitSlop).enabled(drawerType !== 'permanent' && swipeEnabled);
- if (pan && configureGestureHandler) {
- pan = configureGestureHandler(pan);
- }
+ return panGesture;
+ }, [configureGestureHandler, drawerPosition, drawerType, gestureState, hitSlop, onGestureBegin, onGestureAbort, onGestureFinish, open, startX, swipeEnabled, swipeMinDistance, swipeMinVelocity, toggleDrawer, touchStartX, touchX, translationX]);
const translateX = useDerivedValue(() => {
// Comment stolen from react-native-gesture-handler/DrawerLayout
//
@@ -247,35 +251,38 @@ export function Drawer({
style: [styles.container, style],
children: /*#__PURE__*/_jsx(DrawerProgressContext.Provider, {
value: progress,
- children: /*#__PURE__*/_jsx(GestureDetector, {
- gesture: pan,
- children: /*#__PURE__*/_jsxs(Animated.View, {
- style: [styles.main, {
- flexDirection: drawerType === 'permanent' ? isRight && direction === 'ltr' || !isRight && direction === 'rtl' ? 'row' : 'row-reverse' : 'row'
- }],
- children: [/*#__PURE__*/_jsxs(Animated.View, {
- style: [styles.content, contentAnimatedStyle],
- children: [/*#__PURE__*/_jsx(View, {
- accessibilityElementsHidden: isOpen && drawerType !== 'permanent',
- importantForAccessibility: isOpen && drawerType !== 'permanent' ? 'no-hide-descendants' : 'auto',
- style: styles.content,
- children: children
- }), drawerType !== 'permanent' ? /*#__PURE__*/_jsx(Overlay, {
- open: open,
- progress: progress,
- onPress: () => toggleDrawer(false),
- style: overlayStyle,
- accessibilityLabel: overlayAccessibilityLabel
- }) : null]
- }), /*#__PURE__*/_jsx(Animated.View, {
- removeClippedSubviews: Platform.OS !== 'ios',
- style: [styles.drawer, {
- width: drawerWidth,
- position: drawerType === 'permanent' ? 'relative' : 'absolute',
- zIndex: drawerType === 'back' ? -1 : 0
- }, drawerAnimatedStyle, drawerStyle],
- children: renderDrawerContent()
- })]
+ children: /*#__PURE__*/_jsx(DrawerGestureContext.Provider, {
+ value: pan,
+ children: /*#__PURE__*/_jsx(GestureDetector, {
+ gesture: pan,
+ children: /*#__PURE__*/_jsxs(Animated.View, {
+ style: [styles.main, {
+ flexDirection: drawerType === 'permanent' ? isRight && direction === 'ltr' || !isRight && direction === 'rtl' ? 'row' : 'row-reverse' : 'row'
+ }],
+ children: [/*#__PURE__*/_jsxs(Animated.View, {
+ style: [styles.content, contentAnimatedStyle],
+ children: [/*#__PURE__*/_jsx(View, {
+ accessibilityElementsHidden: isOpen && drawerType !== 'permanent',
+ importantForAccessibility: isOpen && drawerType !== 'permanent' ? 'no-hide-descendants' : 'auto',
+ style: styles.content,
+ children: children
+ }), drawerType !== 'permanent' ? /*#__PURE__*/_jsx(Overlay, {
+ open: open,
+ progress: progress,
+ onPress: () => toggleDrawer(false),
+ style: overlayStyle,
+ accessibilityLabel: overlayAccessibilityLabel
+ }) : null]
+ }), /*#__PURE__*/_jsx(Animated.View, {
+ removeClippedSubviews: Platform.OS !== 'ios',
+ style: [styles.drawer, {
+ width: drawerWidth,
+ position: drawerType === 'permanent' ? 'relative' : 'absolute',
+ zIndex: drawerType === 'back' ? -1 : 0
+ }, drawerAnimatedStyle, drawerStyle],
+ children: renderDrawerContent()
+ })]
+ })
})
})
})
diff --git a/node_modules/react-native-drawer-layout/lib/typescript/commonjs/src/index.d.ts b/node_modules/react-native-drawer-layout/lib/typescript/commonjs/src/index.d.ts
index 7e978f0..a8bce18 100644
--- a/node_modules/react-native-drawer-layout/lib/typescript/commonjs/src/index.d.ts
+++ b/node_modules/react-native-drawer-layout/lib/typescript/commonjs/src/index.d.ts
@@ -1,3 +1,4 @@
+export { DrawerGestureContext } from './utils/DrawerGestureContext';
export { DrawerProgressContext } from './utils/DrawerProgressContext';
export { useDrawerProgress } from './utils/useDrawerProgress';
export { Drawer } from './views/Drawer';
diff --git a/node_modules/react-native-drawer-layout/lib/typescript/commonjs/src/utils/DrawerGestureContext.d.ts b/node_modules/react-native-drawer-layout/lib/typescript/commonjs/src/utils/DrawerGestureContext.d.ts
new file mode 100644
index 0000000..33ffbeb
--- /dev/null
+++ b/node_modules/react-native-drawer-layout/lib/typescript/commonjs/src/utils/DrawerGestureContext.d.ts
@@ -0,0 +1,3 @@
+import * as React from 'react';
+export declare const DrawerGestureContext: React.Context<import("react-native-gesture-handler/lib/typescript/handlers/gestures/panGesture").PanGesture | undefined>;
+//# sourceMappingURL=DrawerGestureContext.d.ts.map
\ No newline at end of file
diff --git a/node_modules/react-native-drawer-layout/lib/typescript/module/src/index.d.ts b/node_modules/react-native-drawer-layout/lib/typescript/module/src/index.d.ts
index 7e978f0..a8bce18 100644
--- a/node_modules/react-native-drawer-layout/lib/typescript/module/src/index.d.ts
+++ b/node_modules/react-native-drawer-layout/lib/typescript/module/src/index.d.ts
@@ -1,3 +1,4 @@
+export { DrawerGestureContext } from './utils/DrawerGestureContext';
export { DrawerProgressContext } from './utils/DrawerProgressContext';
export { useDrawerProgress } from './utils/useDrawerProgress';
export { Drawer } from './views/Drawer';
diff --git a/node_modules/react-native-drawer-layout/lib/typescript/module/src/utils/DrawerGestureContext.d.ts b/node_modules/react-native-drawer-layout/lib/typescript/module/src/utils/DrawerGestureContext.d.ts
new file mode 100644
index 0000000..33ffbeb
--- /dev/null
+++ b/node_modules/react-native-drawer-layout/lib/typescript/module/src/utils/DrawerGestureContext.d.ts
@@ -0,0 +1,3 @@
+import * as React from 'react';
+export declare const DrawerGestureContext: React.Context<import("react-native-gesture-handler/lib/typescript/handlers/gestures/panGesture").PanGesture | undefined>;
+//# sourceMappingURL=DrawerGestureContext.d.ts.map
\ No newline at end of file
diff --git a/node_modules/react-native-drawer-layout/src/index.tsx b/node_modules/react-native-drawer-layout/src/index.tsx
index 0aa6f53..61a37cf 100644
--- a/node_modules/react-native-drawer-layout/src/index.tsx
+++ b/node_modules/react-native-drawer-layout/src/index.tsx
@@ -1,3 +1,4 @@
+export { DrawerGestureContext } from './utils/DrawerGestureContext';
export { DrawerProgressContext } from './utils/DrawerProgressContext';
export { useDrawerProgress } from './utils/useDrawerProgress';
export { Drawer } from './views/Drawer';
diff --git a/node_modules/react-native-drawer-layout/src/utils/DrawerGestureContext.tsx b/node_modules/react-native-drawer-layout/src/utils/DrawerGestureContext.tsx
new file mode 100644
index 0000000..3aac957
--- /dev/null
+++ b/node_modules/react-native-drawer-layout/src/utils/DrawerGestureContext.tsx
@@ -0,0 +1,6 @@
+import * as React from 'react';
+import type { PanGesture } from 'react-native-gesture-handler';
+
+export const DrawerGestureContext = React.createContext<PanGesture | undefined>(
+ undefined
+);
diff --git a/node_modules/react-native-drawer-layout/src/views/Drawer.native.tsx b/node_modules/react-native-drawer-layout/src/views/Drawer.native.tsx
index 9c40f41..ee5d075 100644
--- a/node_modules/react-native-drawer-layout/src/views/Drawer.native.tsx
+++ b/node_modules/react-native-drawer-layout/src/views/Drawer.native.tsx
@@ -21,6 +21,7 @@ import Animated, {
import useLatestCallback from 'use-latest-callback';
import type { DrawerProps } from '../types';
+import { DrawerGestureContext } from '../utils/DrawerGestureContext';
import { DrawerProgressContext } from '../utils/DrawerProgressContext';
import { getDrawerWidth } from '../utils/getDrawerWidth';
import {
@@ -110,47 +111,51 @@ export function Drawer({
const interactionHandleRef = React.useRef<number | null>(null);
- const startInteraction = () => {
+ const startInteraction = React.useCallback(() => {
interactionHandleRef.current = InteractionManager.createInteractionHandle();
- };
+ }, []);
- const endInteraction = () => {
+ const endInteraction = React.useCallback(() => {
if (interactionHandleRef.current != null) {
InteractionManager.clearInteractionHandle(interactionHandleRef.current);
interactionHandleRef.current = null;
}
- };
+ }, []);
- const hideKeyboard = () => {
+ const hideKeyboard = React.useCallback(() => {
if (keyboardDismissMode === 'on-drag') {
Keyboard.dismiss();
}
- };
+ }, [keyboardDismissMode]);
- const onGestureBegin = () => {
+ const onGestureBegin = React.useCallback(() => {
onGestureStart?.();
startInteraction();
hideKeyboard();
hideStatusBar(true);
- };
+ }, [onGestureStart, startInteraction, hideKeyboard, hideStatusBar]);
- const onGestureFinish = () => {
+ const onGestureFinish = React.useCallback(() => {
onGestureEnd?.();
endInteraction();
- };
+ }, [onGestureEnd, endInteraction]);
- const onGestureAbort = () => {
+ const onGestureAbort = React.useCallback(() => {
onGestureCancel?.();
endInteraction();
- };
+ }, [onGestureCancel, endInteraction]);
// FIXME: Currently hitSlop is broken when on Android when drawer is on right
// https://github.com/software-mansion/react-native-gesture-handler/issues/569
- const hitSlop = isRight
- ? // Extend hitSlop to the side of the screen when drawer is closed
- // This lets the user drag the drawer from the side of the screen
- { right: 0, width: isOpen ? undefined : swipeEdgeWidth }
- : { left: 0, width: isOpen ? undefined : swipeEdgeWidth };
+ const hitSlop = React.useMemo(
+ () =>
+ isRight
+ ? // Extend hitSlop to the side of the screen when drawer is closed
+ // This lets the user drag the drawer from the side of the screen
+ { right: 0, width: isOpen ? undefined : swipeEdgeWidth }
+ : { left: 0, width: isOpen ? undefined : swipeEdgeWidth },
+ [isRight, isOpen, swipeEdgeWidth]
+ );
const touchStartX = useSharedValue(0);
const touchX = useSharedValue(0);
@@ -217,53 +222,76 @@ export function Drawer({
const startX = useSharedValue(0);
- let pan = Gesture?.Pan()
- .onBegin((event) => {
- 'worklet';
- startX.value = translationX.value;
- gestureState.value = event.state;
- touchStartX.value = event.x;
- })
- .onStart(() => {
- 'worklet';
- runOnJS(onGestureBegin)();
- })
- .onChange((event) => {
- 'worklet';
- touchX.value = event.x;
- translationX.value = startX.value + event.translationX;
- gestureState.value = event.state;
- })
- .onEnd((event, success) => {
- 'worklet';
- gestureState.value = event.state;
-
- if (!success) {
- runOnJS(onGestureAbort)();
- }
-
- const nextOpen =
- (Math.abs(event.translationX) > SWIPE_MIN_OFFSET &&
- Math.abs(event.translationX) > swipeMinVelocity) ||
- Math.abs(event.translationX) > swipeMinDistance
- ? drawerPosition === 'left'
- ? // If swiped to right, open the drawer, otherwise close it
- (event.velocityX === 0 ? event.translationX : event.velocityX) > 0
- : // If swiped to left, open the drawer, otherwise close it
- (event.velocityX === 0 ? event.translationX : event.velocityX) < 0
- : open;
-
- toggleDrawer(nextOpen, event.velocityX);
- runOnJS(onGestureFinish)();
- })
- .activeOffsetX([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET])
- .failOffsetY([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET])
- .hitSlop(hitSlop)
- .enabled(drawerType !== 'permanent' && swipeEnabled);
-
- if (pan && configureGestureHandler) {
- pan = configureGestureHandler(pan);
- }
+ const pan = React.useMemo(() => {
+ let panGesture = Gesture?.Pan()
+ .onBegin((event) => {
+ 'worklet';
+ startX.value = translationX.value;
+ gestureState.value = event.state;
+ touchStartX.value = event.x;
+ })
+ .onStart(() => {
+ 'worklet';
+ runOnJS(onGestureBegin)();
+ })
+ .onChange((event) => {
+ 'worklet';
+ touchX.value = event.x;
+ translationX.value = startX.value + event.translationX;
+ gestureState.value = event.state;
+ })
+ .onEnd((event, success) => {
+ 'worklet';
+ gestureState.value = event.state;
+
+ if (!success) {
+ runOnJS(onGestureAbort)();
+ }
+
+ const nextOpen =
+ (Math.abs(event.translationX) > SWIPE_MIN_OFFSET &&
+ Math.abs(event.translationX) > swipeMinVelocity) ||
+ Math.abs(event.translationX) > swipeMinDistance
+ ? drawerPosition === 'left'
+ ? // If swiped to right, open the drawer, otherwise close it
+ (event.velocityX === 0 ? event.translationX : event.velocityX) >
+ 0
+ : // If swiped to left, open the drawer, otherwise close it
+ (event.velocityX === 0 ? event.translationX : event.velocityX) <
+ 0
+ : open;
+
+ toggleDrawer(nextOpen, event.velocityX);
+ runOnJS(onGestureFinish)();
+ })
+ .activeOffsetX([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET])
+ .failOffsetY([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET])
+ .hitSlop(hitSlop)
+ .enabled(drawerType !== 'permanent' && swipeEnabled);
+
+ if (panGesture && configureGestureHandler) {
+ panGesture = configureGestureHandler(panGesture);
+ }
+ return panGesture;
+ }, [
+ configureGestureHandler,
+ drawerPosition,
+ drawerType,
+ gestureState,
+ hitSlop,
+ onGestureBegin,
+ onGestureAbort,
+ onGestureFinish,
+ open,
+ startX,
+ swipeEnabled,
+ swipeMinDistance,
+ swipeMinVelocity,
+ toggleDrawer,
+ touchStartX,
+ touchX,
+ translationX,
+ ]);
const translateX = useDerivedValue(() => {
// Comment stolen from react-native-gesture-handler/DrawerLayout
@@ -376,64 +404,66 @@ export function Drawer({
return (
<GestureHandlerRootView style={[styles.container, style]}>
<DrawerProgressContext.Provider value={progress}>
- <GestureDetector gesture={pan}>
- {/* Immediate child of gesture handler needs to be an Animated.View */}
- <Animated.View
- style={[
- styles.main,
- {
- flexDirection:
- drawerType === 'permanent'
- ? (isRight && direction === 'ltr') ||
- (!isRight && direction === 'rtl')
- ? 'row'
- : 'row-reverse'
- : 'row',
- },
- ]}
- >
- <Animated.View style={[styles.content, contentAnimatedStyle]}>
- <View
- accessibilityElementsHidden={
- isOpen && drawerType !== 'permanent'
- }
- importantForAccessibility={
- isOpen && drawerType !== 'permanent'
- ? 'no-hide-descendants'
- : 'auto'
- }
- style={styles.content}
- >
- {children}
- </View>
- {drawerType !== 'permanent' ? (
- <Overlay
- open={open}
- progress={progress}
- onPress={() => toggleDrawer(false)}
- style={overlayStyle}
- accessibilityLabel={overlayAccessibilityLabel}
- />
- ) : null}
- </Animated.View>
+ <DrawerGestureContext.Provider value={pan}>
+ <GestureDetector gesture={pan}>
+ {/* Immediate child of gesture handler needs to be an Animated.View */}
<Animated.View
- removeClippedSubviews={Platform.OS !== 'ios'}
style={[
- styles.drawer,
+ styles.main,
{
- width: drawerWidth,
- position:
- drawerType === 'permanent' ? 'relative' : 'absolute',
- zIndex: drawerType === 'back' ? -1 : 0,
+ flexDirection:
+ drawerType === 'permanent'
+ ? (isRight && direction === 'ltr') ||
+ (!isRight && direction === 'rtl')
+ ? 'row'
+ : 'row-reverse'
+ : 'row',
},
- drawerAnimatedStyle,
- drawerStyle,
]}
>
- {renderDrawerContent()}
+ <Animated.View style={[styles.content, contentAnimatedStyle]}>
+ <View
+ accessibilityElementsHidden={
+ isOpen && drawerType !== 'permanent'
+ }
+ importantForAccessibility={
+ isOpen && drawerType !== 'permanent'
+ ? 'no-hide-descendants'
+ : 'auto'
+ }
+ style={styles.content}
+ >
+ {children}
+ </View>
+ {drawerType !== 'permanent' ? (
+ <Overlay
+ open={open}
+ progress={progress}
+ onPress={() => toggleDrawer(false)}
+ style={overlayStyle}
+ accessibilityLabel={overlayAccessibilityLabel}
+ />
+ ) : null}
+ </Animated.View>
+ <Animated.View
+ removeClippedSubviews={Platform.OS !== 'ios'}
+ style={[
+ styles.drawer,
+ {
+ width: drawerWidth,
+ position:
+ drawerType === 'permanent' ? 'relative' : 'absolute',
+ zIndex: drawerType === 'back' ? -1 : 0,
+ },
+ drawerAnimatedStyle,
+ drawerStyle,
+ ]}
+ >
+ {renderDrawerContent()}
+ </Animated.View>
</Animated.View>
- </Animated.View>
- </GestureDetector>
+ </GestureDetector>
+ </DrawerGestureContext.Provider>
</DrawerProgressContext.Provider>
</GestureHandlerRootView>
);
+2 -2
View File
@@ -57,8 +57,6 @@ import {PrivacyPolicyScreen} from '#/view/screens/PrivacyPolicy'
import {ProfileScreen} from '#/view/screens/Profile'
import {ProfileFeedScreen} from '#/view/screens/ProfileFeed'
import {ProfileFeedLikedByScreen} from '#/view/screens/ProfileFeedLikedBy'
import {ProfileFollowersScreen} from '#/view/screens/ProfileFollowers'
import {ProfileFollowsScreen} from '#/view/screens/ProfileFollows'
import {ProfileListScreen} from '#/view/screens/ProfileList'
import {SavedFeeds} from '#/view/screens/SavedFeeds'
import {SearchScreen} from '#/view/screens/Search'
@@ -77,6 +75,8 @@ import {PostLikedByScreen} from '#/screens/Post/PostLikedBy'
import {PostQuotesScreen} from '#/screens/Post/PostQuotes'
import {PostRepostedByScreen} from '#/screens/Post/PostRepostedBy'
import {ProfileKnownFollowersScreen} from '#/screens/Profile/KnownFollowers'
import {ProfileFollowersScreen} from '#/screens/Profile/ProfileFollowers'
import {ProfileFollowsScreen} from '#/screens/Profile/ProfileFollows'
import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy'
import {AppearanceSettingsScreen} from '#/screens/Settings/AppearanceSettings'
import {AppIconSettingsScreen} from '#/screens/Settings/AppIconSettings'
+28
View File
@@ -0,0 +1,28 @@
import {useMemo} from 'react'
import {useMediaQuery} from 'react-responsive'
export type Breakpoint = 'gtPhone' | 'gtMobile' | 'gtTablet'
export function useBreakpoints(): Record<Breakpoint, boolean> & {
activeBreakpoint: Breakpoint | undefined
} {
const gtPhone = useMediaQuery({minWidth: 500})
const gtMobile = useMediaQuery({minWidth: 800})
const gtTablet = useMediaQuery({minWidth: 1300})
return useMemo(() => {
let active: Breakpoint | undefined
if (gtTablet) {
active = 'gtTablet'
} else if (gtMobile) {
active = 'gtMobile'
} else if (gtPhone) {
active = 'gtPhone'
}
return {
activeBreakpoint: active,
gtPhone,
gtMobile,
gtTablet,
}
}, [gtPhone, gtMobile, gtTablet])
}
+2 -13
View File
@@ -1,5 +1,4 @@
import React from 'react'
import {useMediaQuery} from 'react-responsive'
import {
computeFontScaleMultiplier,
@@ -14,13 +13,14 @@ import {BLUE_HUE, GREEN_HUE, RED_HUE} from '#/alf/util/colorGeneration'
import {Device} from '#/storage'
export {atoms} from '#/alf/atoms'
export * from '#/alf/breakpoints'
export * from '#/alf/fonts'
export * as tokens from '#/alf/tokens'
export * from '#/alf/types'
export * from '#/alf/util/flatten'
export * from '#/alf/util/platform'
export * from '#/alf/util/themeSelector'
export * from '#/alf/util/useGutterStyles'
export * from '#/alf/util/useGutters'
export type Alf = {
themeName: ThemeName
@@ -142,14 +142,3 @@ export function useTheme(theme?: ThemeName) {
return theme ? alf.themes[theme] : alf.theme
}, [theme, alf])
}
export function useBreakpoints() {
const gtPhone = useMediaQuery({minWidth: 500})
const gtMobile = useMediaQuery({minWidth: 800})
const gtTablet = useMediaQuery({minWidth: 1300})
return {
gtPhone,
gtMobile,
gtTablet,
}
}
+2 -2
View File
@@ -9,7 +9,7 @@ import {useFonts} from 'expo-font'
*/
export function DO_NOT_USE() {
return useFonts({
InterVariable: require('../../../assets/fonts/inter/InterVariable.ttf'),
'InterVariable-Italic': require('../../../assets/fonts/inter/InterVariable-Italic.ttf'),
InterVariable: require('../../../assets/fonts/inter/InterVariable.woff2'),
'InterVariable-Italic': require('../../../assets/fonts/inter/InterVariable-Italic.woff2'),
})
}
-21
View File
@@ -1,21 +0,0 @@
import React from 'react'
import {atoms as a, useBreakpoints, ViewStyleProp} from '#/alf'
export function useGutterStyles({
top,
bottom,
}: {
top?: boolean
bottom?: boolean
} = {}) {
const {gtMobile} = useBreakpoints()
return React.useMemo<ViewStyleProp['style']>(() => {
return [
a.px_lg,
top && a.pt_md,
bottom && a.pb_md,
gtMobile && [a.px_xl, top && a.pt_lg, bottom && a.pb_lg],
]
}, [gtMobile, top, bottom])
}
+66
View File
@@ -0,0 +1,66 @@
import React from 'react'
import {Breakpoint, useBreakpoints} from '#/alf/breakpoints'
import * as tokens from '#/alf/tokens'
type Gutter = 'compact' | 'base' | 'wide' | 0
const gutters: Record<
Exclude<Gutter, 0>,
Record<Breakpoint | 'default', number>
> = {
compact: {
default: tokens.space.sm,
gtPhone: tokens.space.sm,
gtMobile: tokens.space.md,
gtTablet: tokens.space.md,
},
base: {
default: tokens.space.lg,
gtPhone: tokens.space.lg,
gtMobile: tokens.space.xl,
gtTablet: tokens.space.xl,
},
wide: {
default: tokens.space.xl,
gtPhone: tokens.space.xl,
gtMobile: tokens.space._3xl,
gtTablet: tokens.space._3xl,
},
}
type Gutters = {
paddingTop: number
paddingRight: number
paddingBottom: number
paddingLeft: number
}
export function useGutters([all]: [Gutter]): Gutters
export function useGutters([vertical, horizontal]: [Gutter, Gutter]): Gutters
export function useGutters([top, right, bottom, left]: [
Gutter,
Gutter,
Gutter,
Gutter,
]): Gutters
export function useGutters([top, right, bottom, left]: Gutter[]) {
const {activeBreakpoint} = useBreakpoints()
if (right === undefined) {
right = bottom = left = top
} else if (bottom === undefined) {
bottom = top
left = right
}
return React.useMemo(() => {
return {
paddingTop: top === 0 ? 0 : gutters[top][activeBreakpoint || 'default'],
paddingRight:
right === 0 ? 0 : gutters[right][activeBreakpoint || 'default'],
paddingBottom:
bottom === 0 ? 0 : gutters[bottom][activeBreakpoint || 'default'],
paddingLeft:
left === 0 ? 0 : gutters[left][activeBreakpoint || 'default'],
}
}, [activeBreakpoint, top, right, bottom, left])
}
+5 -5
View File
@@ -13,7 +13,7 @@ import {
platform,
TextStyleProp,
useBreakpoints,
useGutterStyles,
useGutters,
useTheme,
} from '#/alf'
import {Button, ButtonIcon, ButtonProps} from '#/components/Button'
@@ -34,7 +34,7 @@ export function Outer({
noBottomBorder?: boolean
}) {
const t = useTheme()
const gutter = useGutterStyles()
const gutters = useGutters([0, 'base'])
const {gtMobile} = useBreakpoints()
const {isWithinOffsetView} = useContext(ScrollbarOffsetContext)
@@ -46,10 +46,10 @@ export function Outer({
a.flex_row,
a.align_center,
a.gap_sm,
gutter,
gutters,
platform({
native: [a.pb_sm, a.pt_xs],
web: [a.py_sm],
native: [a.pb_xs, {minHeight: 48}],
web: [a.py_xs, {minHeight: 52}],
}),
t.atoms.border_contrast_low,
gtMobile && [a.mx_auto, {maxWidth: 600}],
+4 -5
View File
@@ -237,7 +237,9 @@ export function Link({
}
export type InlineLinkProps = React.PropsWithChildren<
BaseLinkProps & TextStyleProp & Pick<TextProps, 'selectable'>
BaseLinkProps &
TextStyleProp &
Pick<TextProps, 'selectable' | 'numberOfLines'>
> &
Pick<ButtonProps, 'label'> & {
disableUnderline?: boolean
@@ -273,7 +275,6 @@ export function InlineLinkText({
onIn: onHoverIn,
onOut: onHoverOut,
} = useInteractionState()
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
const flattenedStyle = flatten(style) || {}
return (
@@ -284,7 +285,7 @@ export function InlineLinkText({
{...rest}
style={[
{color: t.palette.primary_500},
(hovered || focused) &&
hovered &&
!disableUnderline && {
...web({
outline: 0,
@@ -298,8 +299,6 @@ export function InlineLinkText({
role="link"
onPress={download ? undefined : onPress}
onLongPress={onLongPress}
onFocus={onFocus}
onBlur={onBlur}
onMouseEnter={onHoverIn}
onMouseLeave={onHoverOut}
accessibilityRole="link"
@@ -5,7 +5,7 @@ import {useLingui} from '@lingui/react'
import {isNative} from '#/platform/detection'
import {FeedDescriptor} from '#/state/queries/post-feed'
import {Feed} from '#/view/com/posts/Feed'
import {PostFeed} from '#/view/com/posts/PostFeed'
import {EmptyState} from '#/view/com/util/EmptyState'
import {ListRef} from '#/view/com/util/List'
import {SectionRef} from '#/screens/Profile/Sections/types'
@@ -38,7 +38,7 @@ export const PostsList = React.forwardRef<SectionRef, ProfilesListProps>(
return (
<View>
<Feed
<PostFeed
feed={feed}
pollInterval={60e3}
scrollElRef={scrollElRef}
@@ -15,7 +15,7 @@ import {sanitizeHandle} from '#/lib/strings/handles'
import {isWeb} from '#/platform/detection'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
import {useListConvosQuery} from '#/state/queries/messages/list-converations'
import {useListConvosQuery} from '#/state/queries/messages/list-conversations'
import {useProfileFollowsQuery} from '#/state/queries/profile-follows'
import {useSession} from '#/state/session'
import {ListMethods} from '#/view/com/util/List'
+2 -2
View File
@@ -1,7 +1,7 @@
import {nativeApplicationVersion, nativeBuildVersion} from 'expo-application'
export const BUILD_ENV = process.env.EXPO_PUBLIC_ENV
export const IS_DEV = process.env.EXPO_PUBLIC_ENV === 'development'
export const BUILD_ENV = process.env.EXPO_PUBLIC_ENV || 'development'
export const IS_DEV = BUILD_ENV === 'development'
export const IS_TESTFLIGHT = process.env.EXPO_PUBLIC_ENV === 'testflight'
export const IS_INTERNAL = IS_DEV || IS_TESTFLIGHT
+2 -2
View File
@@ -1,7 +1,7 @@
import {version} from '../../package.json'
export const BUILD_ENV = process.env.EXPO_PUBLIC_ENV
export const IS_DEV = process.env.EXPO_PUBLIC_ENV === 'development'
export const BUILD_ENV = process.env.EXPO_PUBLIC_ENV || 'development'
export const IS_DEV = BUILD_ENV === 'development'
export const IS_TESTFLIGHT = false
export const IS_INTERNAL = IS_DEV
+2 -4
View File
@@ -14,7 +14,7 @@ import {cleanError} from '#/lib/strings/errors'
import {sanitizeHandle} from '#/lib/strings/handles'
import {enforceLen} from '#/lib/strings/helpers'
import {useSearchPostsQuery} from '#/state/queries/search-posts'
import {useSetDrawerSwipeDisabled, useSetMinimalShellMode} from '#/state/shell'
import {useSetMinimalShellMode} from '#/state/shell'
import {Pager} from '#/view/com/pager/Pager'
import {TabBar} from '#/view/com/pager/TabBar'
import {Post} from '#/view/com/post/Post'
@@ -63,7 +63,6 @@ export default function HashtagScreen({
const [activeTab, setActiveTab] = React.useState(0)
const setMinimalShellMode = useSetMinimalShellMode()
const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
useFocusEffect(
React.useCallback(() => {
@@ -74,10 +73,9 @@ export default function HashtagScreen({
const onPageSelected = React.useCallback(
(index: number) => {
setMinimalShellMode(false)
setDrawerSwipeDisabled(index > 0)
setActiveTab(index)
},
[setDrawerSwipeDisabled, setMinimalShellMode],
[setMinimalShellMode],
)
const sections = React.useMemo(() => {
+1 -1
View File
@@ -257,7 +257,7 @@ export const LoginForm = ({
autoCapitalize="none"
autoFocus
autoCorrect={false}
autoComplete="off"
autoComplete="one-time-code"
returnKeyType="done"
textContentType="username"
blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field
+1 -1
View File
@@ -14,7 +14,7 @@ import {logger} from '#/logger'
import {isNative} from '#/platform/detection'
import {MESSAGE_SCREEN_POLL_INTERVAL} from '#/state/messages/convo/const'
import {useMessagesEventBus} from '#/state/messages/events'
import {useListConvosQuery} from '#/state/queries/messages/list-converations'
import {useListConvosQuery} from '#/state/queries/messages/list-conversations'
import {List} from '#/view/com/util/List'
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
+24 -5
View File
@@ -1,13 +1,12 @@
import React from 'react'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {Plural, Trans} from '@lingui/macro'
import {useFocusEffect} from '@react-navigation/native'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {makeRecordUri} from '#/lib/strings/url-helpers'
import {usePostThreadQuery} from '#/state/queries/post-thread'
import {useSetMinimalShellMode} from '#/state/shell'
import {PostLikedBy as PostLikedByComponent} from '#/view/com/post-thread/PostLikedBy'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import * as Layout from '#/components/Layout'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostLikedBy'>
@@ -15,7 +14,12 @@ export const PostLikedByScreen = ({route}: Props) => {
const setMinimalShellMode = useSetMinimalShellMode()
const {name, rkey} = route.params
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
const {_} = useLingui()
const {data: post} = usePostThreadQuery(uri)
let likeCount
if (post?.thread.type === 'post') {
likeCount = post.thread.post.likeCount
}
useFocusEffect(
React.useCallback(() => {
@@ -25,7 +29,22 @@ export const PostLikedByScreen = ({route}: Props) => {
return (
<Layout.Screen>
<ViewHeader title={_(msg`Liked By`)} />
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
{post && (
<>
<Layout.Header.TitleText>
<Trans>Liked By</Trans>
</Layout.Header.TitleText>
<Layout.Header.SubtitleText>
<Plural value={likeCount ?? 0} one="# like" other="# likes" />
</Layout.Header.SubtitleText>
</>
)}
</Layout.Header.Content>
<Layout.Header.Slot />
</Layout.Header.Outer>
<PostLikedByComponent uri={uri} />
</Layout.Screen>
)
+29 -10
View File
@@ -1,15 +1,12 @@
import React from 'react'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {Plural, Trans} from '@lingui/macro'
import {useFocusEffect} from '@react-navigation/native'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {makeRecordUri} from '#/lib/strings/url-helpers'
import {isWeb} from '#/platform/detection'
import {usePostThreadQuery} from '#/state/queries/post-thread'
import {useSetMinimalShellMode} from '#/state/shell'
import {PostQuotes as PostQuotesComponent} from '#/view/com/post-thread/PostQuotes'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {CenteredView} from '#/view/com/util/Views'
import * as Layout from '#/components/Layout'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostQuotes'>
@@ -17,7 +14,12 @@ export const PostQuotesScreen = ({route}: Props) => {
const setMinimalShellMode = useSetMinimalShellMode()
const {name, rkey} = route.params
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
const {_} = useLingui()
const {data: post} = usePostThreadQuery(uri)
let quoteCount
if (post?.thread.type === 'post') {
quoteCount = post.thread.post.quoteCount
}
useFocusEffect(
React.useCallback(() => {
@@ -27,10 +29,27 @@ export const PostQuotesScreen = ({route}: Props) => {
return (
<Layout.Screen>
<CenteredView sideBorders={true}>
<ViewHeader title={_(msg`Quotes`)} showBorder={!isWeb} />
<PostQuotesComponent uri={uri} />
</CenteredView>
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
{post && (
<>
<Layout.Header.TitleText>
<Trans>Quotes</Trans>
</Layout.Header.TitleText>
<Layout.Header.SubtitleText>
<Plural
value={quoteCount ?? 0}
one="# quote"
other="# quotes"
/>
</Layout.Header.SubtitleText>
</>
)}
</Layout.Header.Content>
<Layout.Header.Slot />
</Layout.Header.Outer>
<PostQuotesComponent uri={uri} />
</Layout.Screen>
)
}
+29 -10
View File
@@ -1,15 +1,12 @@
import React from 'react'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {Plural, Trans} from '@lingui/macro'
import {useFocusEffect} from '@react-navigation/native'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {makeRecordUri} from '#/lib/strings/url-helpers'
import {isWeb} from '#/platform/detection'
import {usePostThreadQuery} from '#/state/queries/post-thread'
import {useSetMinimalShellMode} from '#/state/shell'
import {PostRepostedBy as PostRepostedByComponent} from '#/view/com/post-thread/PostRepostedBy'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {CenteredView} from '#/view/com/util/Views'
import * as Layout from '#/components/Layout'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostRepostedBy'>
@@ -17,7 +14,12 @@ export const PostRepostedByScreen = ({route}: Props) => {
const {name, rkey} = route.params
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
const setMinimalShellMode = useSetMinimalShellMode()
const {_} = useLingui()
const {data: post} = usePostThreadQuery(uri)
let quoteCount
if (post?.thread.type === 'post') {
quoteCount = post.thread.post.repostCount
}
useFocusEffect(
React.useCallback(() => {
@@ -27,10 +29,27 @@ export const PostRepostedByScreen = ({route}: Props) => {
return (
<Layout.Screen>
<CenteredView sideBorders={true}>
<ViewHeader title={_(msg`Reposted By`)} showBorder={!isWeb} />
<PostRepostedByComponent uri={uri} />
</CenteredView>
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
{post && (
<>
<Layout.Header.TitleText>
<Trans>Reposted By</Trans>
</Layout.Header.TitleText>
<Layout.Header.SubtitleText>
<Plural
value={quoteCount ?? 0}
one="# reposts"
other="# reposts"
/>
</Layout.Header.SubtitleText>
</>
)}
</Layout.Header.Content>
<Layout.Header.Slot />
</Layout.Header.Outer>
<PostRepostedByComponent uri={uri} />
</Layout.Screen>
)
}
+1 -1
View File
@@ -30,7 +30,7 @@ export function ProfileHeaderMetrics({
return (
<View
style={[a.flex_row, a.gap_sm, a.align_center, a.pb_md]}
style={[a.flex_row, a.gap_sm, a.align_center]}
pointerEvents="box-none">
<InlineLinkText
testID="profileHeaderFollowersButton"
@@ -244,7 +244,7 @@ let ProfileHeaderStandard = ({
<ProfileHeaderHandle profile={profile} />
</View>
{!isPlaceholderProfile && !isBlockedUser && (
<>
<View style={a.gap_md}>
<ProfileHeaderMetrics profile={profile} />
{descriptionRT && !moderation.ui('profileView').blur ? (
<View pointerEvents="auto">
@@ -262,14 +262,14 @@ let ProfileHeaderStandard = ({
{!isMe &&
!isBlockedUser &&
shouldShowKnownFollowers(profile.viewer?.knownFollowers) && (
<View style={[a.flex_row, a.align_center, a.gap_sm, a.pt_md]}>
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
<KnownFollowers
profile={profile}
moderationOpts={moderationOpts}
/>
</View>
)}
</>
</View>
)}
</View>
<Prompt.Basic
+54
View File
@@ -0,0 +1,54 @@
import React from 'react'
import {Plural} from '@lingui/macro'
import {useFocusEffect} from '@react-navigation/native'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {useProfileQuery} from '#/state/queries/profile'
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
import {useSetMinimalShellMode} from '#/state/shell'
import {ProfileFollowers as ProfileFollowersComponent} from '#/view/com/profile/ProfileFollowers'
import * as Layout from '#/components/Layout'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFollowers'>
export const ProfileFollowersScreen = ({route}: Props) => {
const {name} = route.params
const setMinimalShellMode = useSetMinimalShellMode()
const {data: resolvedDid} = useResolveDidQuery(name)
const {data: profile} = useProfileQuery({
did: resolvedDid,
})
useFocusEffect(
React.useCallback(() => {
setMinimalShellMode(false)
}, [setMinimalShellMode]),
)
return (
<Layout.Screen testID="profileFollowersScreen">
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
{profile && (
<>
<Layout.Header.TitleText>
{sanitizeDisplayName(profile.displayName || profile.handle)}
</Layout.Header.TitleText>
<Layout.Header.SubtitleText>
<Plural
value={profile.followersCount ?? 0}
one="# follower"
other="# followers"
/>
</Layout.Header.SubtitleText>
</>
)}
</Layout.Header.Content>
<Layout.Header.Slot />
</Layout.Header.Outer>
<ProfileFollowersComponent name={name} />
</Layout.Screen>
)
}
+54
View File
@@ -0,0 +1,54 @@
import React from 'react'
import {Plural} from '@lingui/macro'
import {useFocusEffect} from '@react-navigation/native'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {useProfileQuery} from '#/state/queries/profile'
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
import {useSetMinimalShellMode} from '#/state/shell'
import {ProfileFollows as ProfileFollowsComponent} from '#/view/com/profile/ProfileFollows'
import * as Layout from '#/components/Layout'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFollows'>
export const ProfileFollowsScreen = ({route}: Props) => {
const {name} = route.params
const setMinimalShellMode = useSetMinimalShellMode()
const {data: resolvedDid} = useResolveDidQuery(name)
const {data: profile} = useProfileQuery({
did: resolvedDid,
})
useFocusEffect(
React.useCallback(() => {
setMinimalShellMode(false)
}, [setMinimalShellMode]),
)
return (
<Layout.Screen testID="profileFollowsScreen">
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
{profile && (
<>
<Layout.Header.TitleText>
{sanitizeDisplayName(profile.displayName || profile.handle)}
</Layout.Header.TitleText>
<Layout.Header.SubtitleText>
<Plural
value={profile.followersCount ?? 0}
one="# following"
other="# following"
/>
</Layout.Header.SubtitleText>
</>
)}
</Layout.Header.Content>
<Layout.Header.Slot />
</Layout.Header.Outer>
<ProfileFollowsComponent name={name} />
</Layout.Screen>
)
}
+2 -2
View File
@@ -10,7 +10,7 @@ import {isNative} from '#/platform/detection'
import {FeedDescriptor} from '#/state/queries/post-feed'
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
import {truncateAndInvalidate} from '#/state/queries/util'
import {Feed} from '#/view/com/posts/Feed'
import {PostFeed} from '#/view/com/posts/PostFeed'
import {EmptyState} from '#/view/com/util/EmptyState'
import {ListRef} from '#/view/com/util/List'
import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn'
@@ -74,7 +74,7 @@ export const ProfileFeedSection = React.forwardRef<
return (
<View>
<Feed
<PostFeed
testID="postsFeed"
enabled={isFocused}
feed={feed}
-260
View File
@@ -1,260 +0,0 @@
import React from 'react'
import {Alert, View} from 'react-native'
import {Image} from 'expo-image'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import * as AppIcon from '@mozzius/expo-dynamic-app-icon'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {PressableScale} from '#/lib/custom-animations/PressableScale'
import {CommonNavigatorParams} from '#/lib/routes/types'
import {isAndroid} from '#/platform/detection'
import {atoms as a, platform} from '#/alf'
import * as Layout from '#/components/Layout'
import {Text} from '#/components/Typography'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppIconSettings'>
export function AppIconSettingsScreen({}: Props) {
const {_} = useLingui()
const sets = useAppIconSets()
return (
<Layout.Screen>
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
<Layout.Header.TitleText>
<Trans>App Icon</Trans>
</Layout.Header.TitleText>
</Layout.Header.Content>
<Layout.Header.Slot />
</Layout.Header.Outer>
<Layout.Content
contentContainerStyle={[a.py_2xl, a.px_xl, {paddingBottom: 100}]}>
<Text style={[a.text_lg, a.font_heavy]}>Defaults</Text>
<View style={[a.flex_row, a.flex_wrap]}>
{sets.defaults.map(icon => (
<View
style={[{width: '50%'}, a.py_lg, a.px_xs, a.align_center]}
key={icon.id}>
<PressableScale
accessibilityLabel={icon.name}
accessibilityHint={_(msg`Tap to change app icon`)}
targetScale={0.95}
onPress={() => AppIcon.setAppIcon(icon.id)}>
<Image
source={platform({
ios: icon.iosImage(),
android: icon.androidImage(),
})}
style={[
{width: 100, height: 100},
platform({
ios: {borderRadius: 20},
android: a.rounded_full,
}),
a.curve_continuous,
]}
accessibilityIgnoresInvertColors
/>
</PressableScale>
<Text style={[a.text_center, a.font_bold, a.text_md, a.mt_md]}>
{icon.name}
</Text>
</View>
))}
</View>
<Text style={[a.text_lg, a.font_heavy]}>Bluesky+</Text>
<View style={[a.flex_row, a.flex_wrap]}>
{sets.core.map(icon => (
<View
style={[{width: '50%'}, a.py_lg, a.px_xs, a.align_center]}
key={icon.id}>
<PressableScale
accessibilityLabel={icon.name}
accessibilityHint={_(msg`Tap to change app icon`)}
targetScale={0.95}
onPress={() => {
if (isAndroid) {
Alert.alert(
_(msg`Change app icon to "${icon.name}"`),
_(msg`The app will be restarted`),
[
{
text: _(msg`Cancel`),
style: 'cancel',
},
{
text: _(msg`OK`),
onPress: () => {
AppIcon.setAppIcon(icon.id)
},
style: 'default',
},
],
)
} else {
AppIcon.setAppIcon(icon.id)
}
}}>
<Image
source={platform({
ios: icon.iosImage(),
android: icon.androidImage(),
})}
style={[
{width: 100, height: 100},
platform({
ios: {borderRadius: 20},
android: a.rounded_full,
}),
a.curve_continuous,
a.shadow_lg,
]}
accessibilityIgnoresInvertColors
/>
</PressableScale>
<Text
style={[a.text_center, a.font_bold, a.text_md, a.mt_md]}
// for Classic™
emoji>
{icon.name}
</Text>
</View>
))}
</View>
</Layout.Content>
</Layout.Screen>
)
}
function useAppIconSets() {
const {_} = useLingui()
return React.useMemo(() => {
const defaults = [
{
id: 'default_light',
name: _('Light'),
iosImage: () => {
return require(`../../../assets/app-icons/ios_icon_default_light.png`)
},
androidImage: () => {
return require(`../../../assets/app-icons/android_icon_default_light.png`)
},
},
{
id: 'default_dark',
name: _('Dark'),
iosImage: () => {
return require(`../../../assets/app-icons/ios_icon_default_dark.png`)
},
androidImage: () => {
return require(`../../../assets/app-icons/android_icon_default_dark.png`)
},
},
]
/**
* Bluesky+
*/
const core = [
{
id: 'core_aurora',
name: _('Aurora'),
iosImage: () => {
return require(`../../../assets/app-icons/ios_icon_core_aurora.png`)
},
androidImage: () => {
return require(`../../../assets/app-icons/android_icon_core_aurora.png`)
},
},
// {
// id: 'core_bonfire',
// name: _('Bonfire'),
// iosImage: () => {
// return require(`../../../assets/app-icons/ios_icon_core_bonfire.png`)
// },
// androidImage: () => {
// return require(`../../../assets/app-icons/android_icon_core_bonfire.png`)
// },
// },
{
id: 'core_sunrise',
name: _('Sunrise'),
iosImage: () => {
return require(`../../../assets/app-icons/ios_icon_core_sunrise.png`)
},
androidImage: () => {
return require(`../../../assets/app-icons/android_icon_core_sunrise.png`)
},
},
{
id: 'core_sunset',
name: _('Sunset'),
iosImage: () => {
return require(`../../../assets/app-icons/ios_icon_core_sunset.png`)
},
androidImage: () => {
return require(`../../../assets/app-icons/android_icon_core_sunset.png`)
},
},
{
id: 'core_midnight',
name: _('Midnight'),
iosImage: () => {
return require(`../../../assets/app-icons/ios_icon_core_midnight.png`)
},
androidImage: () => {
return require(`../../../assets/app-icons/android_icon_core_midnight.png`)
},
},
{
id: 'core_flat_blue',
name: _('Flat Blue'),
iosImage: () => {
return require(`../../../assets/app-icons/ios_icon_core_flat_blue.png`)
},
androidImage: () => {
return require(`../../../assets/app-icons/android_icon_core_flat_blue.png`)
},
},
{
id: 'core_flat_white',
name: _('Flat White'),
iosImage: () => {
return require(`../../../assets/app-icons/ios_icon_core_flat_white.png`)
},
androidImage: () => {
return require(`../../../assets/app-icons/android_icon_core_flat_white.png`)
},
},
{
id: 'core_flat_black',
name: _('Flat Black'),
iosImage: () => {
return require(`../../../assets/app-icons/ios_icon_core_flat_black.png`)
},
androidImage: () => {
return require(`../../../assets/app-icons/android_icon_core_flat_black.png`)
},
},
{
id: 'core_classic',
name: _('Bluesky Classic™'),
iosImage: () => {
return require(`../../../assets/app-icons/ios_icon_core_classic.png`)
},
androidImage: () => {
return require(`../../../assets/app-icons/android_icon_core_classic.png`)
},
},
]
return {
defaults,
core,
}
}, [_])
}
@@ -0,0 +1,33 @@
import {Image} from 'expo-image'
import {AppIconSet} from '#/screens/Settings/AppIconSettings/types'
import {atoms as a, platform, useTheme} from '#/alf'
export function AppIconImage({
icon,
size = 50,
}: {
icon: AppIconSet
size: number
}) {
const t = useTheme()
return (
<Image
source={platform({
ios: icon.iosImage(),
android: icon.androidImage(),
})}
style={[
{width: size, height: size},
platform({
ios: {borderRadius: size / 5},
android: a.rounded_full,
}),
a.curve_continuous,
t.atoms.border_contrast_medium,
a.border,
]}
accessibilityIgnoresInvertColors
/>
)
}
@@ -0,0 +1,29 @@
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {AppIconImage} from '#/screens/Settings/AppIconSettings/AppIconImage'
import {useCurrentAppIcon} from '#/screens/Settings/AppIconSettings/useCurrentAppIcon'
import * as SettingsList from '#/screens/Settings/components/SettingsList'
import {atoms as a} from '#/alf'
import {Shapes_Stroke2_Corner0_Rounded as Shapes} from '#/components/icons/Shapes'
export function SettingsListItem() {
const {_} = useLingui()
const icon = useCurrentAppIcon()
return (
<SettingsList.LinkItem
to="/settings/app-icon"
label={_(msg`App Icon`)}
contentContainerStyle={[a.align_start]}>
<SettingsList.ItemIcon icon={Shapes} />
<View style={[a.flex_1]}>
<SettingsList.ItemText style={[a.pt_xs, a.pb_md]}>
<Trans>App Icon</Trans>
</SettingsList.ItemText>
<AppIconImage icon={icon} size={60} />
</View>
</SettingsList.LinkItem>
)
}
@@ -0,0 +1 @@
export function SettingsListItem() {}
@@ -0,0 +1,244 @@
import {useState} from 'react'
import {Alert, View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import * as DynamicAppIcon from '@mozzius/expo-dynamic-app-icon'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {DISCOVER_DEBUG_DIDS} from '#/lib/constants'
import {PressableScale} from '#/lib/custom-animations/PressableScale'
import {CommonNavigatorParams} from '#/lib/routes/types'
import {isAndroid} from '#/platform/detection'
import {useSession} from '#/state/session'
import {AppIconImage} from '#/screens/Settings/AppIconSettings/AppIconImage'
import {AppIconSet} from '#/screens/Settings/AppIconSettings/types'
import {useAppIconSets} from '#/screens/Settings/AppIconSettings/useAppIconSets'
import {atoms as a, useTheme} from '#/alf'
import * as Toggle from '#/components/forms/Toggle'
import * as Layout from '#/components/Layout'
import {Text} from '#/components/Typography'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppIconSettings'>
export function AppIconSettingsScreen({}: Props) {
const t = useTheme()
const {_} = useLingui()
const sets = useAppIconSets()
const {currentAccount} = useSession()
const [currentAppIcon, setCurrentAppIcon] = useState(() =>
getAppIconName(DynamicAppIcon.getAppIcon()),
)
const onSetAppIcon = (icon: string) => {
if (isAndroid) {
const next =
sets.defaults.find(i => i.id === icon) ??
sets.core.find(i => i.id === icon)
Alert.alert(
next
? _(msg`Change app icon to "${next.name}"`)
: _(msg`Change app icon`),
// to determine - can we stop this happening? -sfn
_(msg`The app will be restarted`),
[
{
text: _(msg`Cancel`),
style: 'cancel',
},
{
text: _(msg`OK`),
onPress: () => {
setCurrentAppIcon(setAppIcon(icon))
},
style: 'default',
},
],
)
} else {
setCurrentAppIcon(setAppIcon(icon))
}
}
return (
<Layout.Screen>
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
<Layout.Header.TitleText>
<Trans>App Icon</Trans>
</Layout.Header.TitleText>
</Layout.Header.Content>
<Layout.Header.Slot />
</Layout.Header.Outer>
<Layout.Content contentContainerStyle={[a.p_lg]}>
<Group
label={_(msg`Default icons`)}
value={currentAppIcon}
onChange={onSetAppIcon}>
{sets.defaults.map((icon, i) => (
<Row
key={icon.id}
icon={icon}
isEnd={i === sets.defaults.length - 1}>
<AppIcon icon={icon} key={icon.id} size={40} />
<RowText>{icon.name}</RowText>
</Row>
))}
</Group>
{DISCOVER_DEBUG_DIDS[currentAccount?.did ?? ''] && (
<>
<Text
style={[
a.text_md,
a.mt_xl,
a.mb_sm,
a.font_bold,
t.atoms.text_contrast_medium,
]}>
<Trans>Bluesky+</Trans>
</Text>
<Group
label={_(msg`Bluesky+ icons`)}
value={currentAppIcon}
onChange={onSetAppIcon}>
{sets.core.map((icon, i) => (
<Row
key={icon.id}
icon={icon}
isEnd={i === sets.core.length - 1}>
<AppIcon icon={icon} key={icon.id} size={40} />
<RowText>{icon.name}</RowText>
</Row>
))}
</Group>
</>
)}
</Layout.Content>
</Layout.Screen>
)
}
function setAppIcon(icon: string) {
if (icon === 'default_light') {
return getAppIconName(DynamicAppIcon.setAppIcon(null))
} else {
return getAppIconName(DynamicAppIcon.setAppIcon(icon))
}
}
function getAppIconName(icon: string | false) {
if (!icon || icon === 'DEFAULT') {
return 'default_light'
} else {
return icon
}
}
function Group({
children,
label,
value,
onChange,
}: {
children: React.ReactNode
label: string
value: string
onChange: (value: string) => void
}) {
return (
<Toggle.Group
type="radio"
label={label}
values={[value]}
maxSelections={1}
onChange={vals => {
if (vals[0]) onChange(vals[0])
}}>
<View style={[a.flex_1, a.rounded_md, a.overflow_hidden]}>
{children}
</View>
</Toggle.Group>
)
}
function Row({
icon,
children,
isEnd,
}: {
icon: AppIconSet
children: React.ReactNode
isEnd: boolean
}) {
const t = useTheme()
const {_} = useLingui()
return (
<Toggle.Item label={_(msg`Set app icon to ${icon.name}`)} name={icon.id}>
{({hovered, pressed}) => (
<View
style={[
a.flex_1,
a.p_md,
a.flex_row,
a.gap_md,
a.align_center,
t.atoms.bg_contrast_25,
(hovered || pressed) && t.atoms.bg_contrast_50,
t.atoms.border_contrast_high,
!isEnd && a.border_b,
]}>
{children}
<Toggle.Radio />
</View>
)}
</Toggle.Item>
)
}
function RowText({children}: {children: React.ReactNode}) {
const t = useTheme()
return (
<Text
style={[a.text_md, a.font_bold, a.flex_1, t.atoms.text_contrast_medium]}
emoji>
{children}
</Text>
)
}
function AppIcon({icon, size = 50}: {icon: AppIconSet; size: number}) {
const {_} = useLingui()
return (
<PressableScale
accessibilityLabel={icon.name}
accessibilityHint={_(msg`Tap to change app icon`)}
targetScale={0.95}
onPress={() => {
if (isAndroid) {
Alert.alert(
_(msg`Change app icon to "${icon.name}"`),
_(msg`The app will be restarted`),
[
{
text: _(msg`Cancel`),
style: 'cancel',
},
{
text: _(msg`OK`),
onPress: () => {
DynamicAppIcon.setAppIcon(icon.id)
},
style: 'default',
},
],
)
} else {
DynamicAppIcon.setAppIcon(icon.id)
}
}}>
<AppIconImage icon={icon} size={size} />
</PressableScale>
)
}
@@ -0,0 +1,8 @@
import {ImageSourcePropType} from 'react-native'
export type AppIconSet = {
id: string
name: string
iosImage: () => ImageSourcePropType
androidImage: () => ImageSourcePropType
}
@@ -0,0 +1,134 @@
import {useMemo} from 'react'
import {useLingui} from '@lingui/react'
import {AppIconSet} from '#/screens/Settings/AppIconSettings/types'
export function useAppIconSets() {
const {_} = useLingui()
return useMemo(() => {
const defaults = [
{
id: 'default_light',
name: _('Light'),
iosImage: () => {
return require(`../../../../assets/app-icons/ios_icon_default_light.png`)
},
androidImage: () => {
return require(`../../../../assets/app-icons/android_icon_default_light.png`)
},
},
{
id: 'default_dark',
name: _('Dark'),
iosImage: () => {
return require(`../../../../assets/app-icons/ios_icon_default_dark.png`)
},
androidImage: () => {
return require(`../../../../assets/app-icons/android_icon_default_dark.png`)
},
},
] satisfies AppIconSet[]
/**
* Bluesky+
*/
const core = [
{
id: 'core_aurora',
name: _('Aurora'),
iosImage: () => {
return require(`../../../../assets/app-icons/ios_icon_core_aurora.png`)
},
androidImage: () => {
return require(`../../../../assets/app-icons/android_icon_core_aurora.png`)
},
},
// {
// id: 'core_bonfire',
// name: _('Bonfire'),
// iosImage: () => {
// return require(`../../../../assets/app-icons/ios_icon_core_bonfire.png`)
// },
// androidImage: () => {
// return require(`../../../../assets/app-icons/android_icon_core_bonfire.png`)
// },
// },
{
id: 'core_sunrise',
name: _('Sunrise'),
iosImage: () => {
return require(`../../../../assets/app-icons/ios_icon_core_sunrise.png`)
},
androidImage: () => {
return require(`../../../../assets/app-icons/android_icon_core_sunrise.png`)
},
},
{
id: 'core_sunset',
name: _('Sunset'),
iosImage: () => {
return require(`../../../../assets/app-icons/ios_icon_core_sunset.png`)
},
androidImage: () => {
return require(`../../../../assets/app-icons/android_icon_core_sunset.png`)
},
},
{
id: 'core_midnight',
name: _('Midnight'),
iosImage: () => {
return require(`../../../../assets/app-icons/ios_icon_core_midnight.png`)
},
androidImage: () => {
return require(`../../../../assets/app-icons/android_icon_core_midnight.png`)
},
},
{
id: 'core_flat_blue',
name: _('Flat Blue'),
iosImage: () => {
return require(`../../../../assets/app-icons/ios_icon_core_flat_blue.png`)
},
androidImage: () => {
return require(`../../../../assets/app-icons/android_icon_core_flat_blue.png`)
},
},
{
id: 'core_flat_white',
name: _('Flat White'),
iosImage: () => {
return require(`../../../../assets/app-icons/ios_icon_core_flat_white.png`)
},
androidImage: () => {
return require(`../../../../assets/app-icons/android_icon_core_flat_white.png`)
},
},
{
id: 'core_flat_black',
name: _('Flat Black'),
iosImage: () => {
return require(`../../../../assets/app-icons/ios_icon_core_flat_black.png`)
},
androidImage: () => {
return require(`../../../../assets/app-icons/android_icon_core_flat_black.png`)
},
},
{
id: 'core_classic',
name: _('Bluesky Classic™'),
iosImage: () => {
return require(`../../../../assets/app-icons/ios_icon_core_classic.png`)
},
androidImage: () => {
return require(`../../../../assets/app-icons/android_icon_core_classic.png`)
},
},
] satisfies AppIconSet[]
return {
defaults,
core,
}
}, [_])
}
@@ -0,0 +1,27 @@
import {useCallback, useMemo, useState} from 'react'
import * as DynamicAppIcon from '@mozzius/expo-dynamic-app-icon'
import {useFocusEffect} from '@react-navigation/native'
import {useAppIconSets} from '#/screens/Settings/AppIconSettings/useAppIconSets'
export function useCurrentAppIcon() {
const appIconSets = useAppIconSets()
const [currentAppIcon, setCurrentAppIcon] = useState(() =>
DynamicAppIcon.getAppIcon(),
)
// refresh current icon when screen is focused
useFocusEffect(
useCallback(() => {
setCurrentAppIcon(DynamicAppIcon.getAppIcon())
}, []),
)
return useMemo(() => {
return (
appIconSets.defaults.find(i => i.id === currentAppIcon) ??
appIconSets.core.find(i => i.id === currentAppIcon) ??
appIconSets.defaults[0]
)
}, [appIconSets, currentAppIcon])
}
+2 -10
View File
@@ -13,7 +13,7 @@ import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {isNative} from '#/platform/detection'
import {useSession} from '#/state/session'
import {useSetThemePrefs, useThemePrefs} from '#/state/shell'
import {Logo} from '#/view/icons/Logo'
import {SettingsListItem as AppIconSettingsListItem} from '#/screens/Settings/AppIconSettings/SettingsListItem'
import {atoms as a, native, useAlf, useTheme} from '#/alf'
import * as ToggleButton from '#/components/forms/ToggleButton'
import {Props as SVGIconProps} from '#/components/icons/common'
@@ -181,15 +181,7 @@ export function AppearanceSettingsScreen({}: Props) {
{isNative && DISCOVER_DEBUG_DIDS[currentAccount?.did ?? ''] && (
<>
<SettingsList.Divider />
<SettingsList.LinkItem
to="/settings/app-icon"
label={_(msg`App Icon`)}>
<SettingsList.ItemIcon icon={Logo} />
<SettingsList.ItemText>
<Trans>App Icon</Trans>
</SettingsList.ItemText>
</SettingsList.LinkItem>
<AppIconSettingsListItem />
</>
)}
</Animated.View>
+1 -1
View File
@@ -7,7 +7,7 @@ import {batchedUpdates} from '#/lib/batchedUpdates'
import {findAllProfilesInQueryData as findAllProfilesInActorSearchQueryData} from '../queries/actor-search'
import {findAllProfilesInQueryData as findAllProfilesInKnownFollowersQueryData} from '../queries/known-followers'
import {findAllProfilesInQueryData as findAllProfilesInListMembersQueryData} from '../queries/list-members'
import {findAllProfilesInQueryData as findAllProfilesInListConvosQueryData} from '../queries/messages/list-converations'
import {findAllProfilesInQueryData as findAllProfilesInListConvosQueryData} from '../queries/messages/list-conversations'
import {findAllProfilesInQueryData as findAllProfilesInMyBlockedAccountsQueryData} from '../queries/my-blocked-accounts'
import {findAllProfilesInQueryData as findAllProfilesInMyMutedAccountsQueryData} from '../queries/my-muted-accounts'
import {findAllProfilesInQueryData as findAllProfilesInFeedsQueryData} from '../queries/post-feed'
+1 -1
View File
@@ -7,7 +7,7 @@ import {PROD_DEFAULT_FEED} from '#/lib/constants'
import {logEvent} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {FeedDescriptor, FeedPostSliceItem} from '#/state/queries/post-feed'
import {getFeedPostSlice} from '#/view/com/posts/Feed'
import {getFeedPostSlice} from '#/view/com/posts/PostFeed'
import {useAgent} from './session'
type StateContext = {
+1 -1
View File
@@ -15,7 +15,7 @@ import {
import {isConvoActive} from '#/state/messages/convo/util'
import {useMessagesEventBus} from '#/state/messages/events'
import {useMarkAsReadMutation} from '#/state/queries/messages/conversation'
import {RQKEY as ListConvosQueryKey} from '#/state/queries/messages/list-converations'
import {RQKEY as ListConvosQueryKey} from '#/state/queries/messages/list-conversations'
import {RQKEY as createProfileQueryKey} from '#/state/queries/profile'
import {useAgent} from '#/state/session'
+1 -1
View File
@@ -2,7 +2,7 @@ import React from 'react'
import {CurrentConvoIdProvider} from '#/state/messages/current-convo-id'
import {MessagesEventBusProvider} from '#/state/messages/events'
import {ListConvosProvider} from '#/state/queries/messages/list-converations'
import {ListConvosProvider} from '#/state/queries/messages/list-conversations'
import {MessageDraftsProvider} from './message-drafts'
export function MessagesProvider({children}: {children: React.ReactNode}) {
+1
View File
@@ -28,6 +28,7 @@ const accountSchema = z.object({
*/
status: z.string().optional(),
pdsUrl: z.string().optional(),
isSelfHosted: z.boolean().optional(),
})
export type PersistedAccount = z.infer<typeof accountSchema>
+2 -2
View File
@@ -3,13 +3,13 @@ import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
import {STALE} from '#/state/queries'
import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const'
import {useOnMarkAsRead} from '#/state/queries/messages/list-converations'
import {useOnMarkAsRead} from '#/state/queries/messages/list-conversations'
import {useAgent} from '#/state/session'
import {
ConvoListQueryData,
getConvoFromQueryData,
RQKEY as LIST_CONVOS_KEY,
} from './list-converations'
} from './list-conversations'
const RQKEY_ROOT = 'convo'
export const RQKEY = (convoId: string) => [RQKEY_ROOT, convoId]
@@ -4,7 +4,7 @@ import {useMutation, useQueryClient} from '@tanstack/react-query'
import {logger} from '#/logger'
import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const'
import {useAgent} from '#/state/session'
import {RQKEY as CONVO_LIST_KEY} from './list-converations'
import {RQKEY as CONVO_LIST_KEY} from './list-conversations'
export function useLeaveConvo(
convoId: string | undefined,
@@ -8,7 +8,7 @@ import {InfiniteData, useMutation, useQueryClient} from '@tanstack/react-query'
import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const'
import {useAgent} from '#/state/session'
import {RQKEY as CONVO_KEY} from './conversation'
import {RQKEY as CONVO_LIST_KEY} from './list-converations'
import {RQKEY as CONVO_LIST_KEY} from './list-conversations'
export function useMuteConvo(
convoId: string | undefined,
+1 -1
View File
@@ -33,7 +33,7 @@ import {STALE} from '#/state/queries'
import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences/const'
import {useAgent} from '#/state/session'
import * as userActionHistory from '#/state/userActionHistory'
import {KnownError} from '#/view/com/posts/FeedErrorMessage'
import {KnownError} from '#/view/com/posts/PostFeedErrorMessage'
import {useFeedTuners} from '../preferences/feed-tuners'
import {useModerationOpts} from '../preferences/moderation-opts'
import {usePreferencesQuery} from './preferences'
+1 -1
View File
@@ -30,7 +30,7 @@ import {
ProgressGuideAction,
useProgressGuideControls,
} from '../shell/progress-guide'
import {RQKEY as RQKEY_LIST_CONVOS} from './messages/list-converations'
import {RQKEY as RQKEY_LIST_CONVOS} from './messages/list-conversations'
import {RQKEY as RQKEY_MY_BLOCKED} from './my-blocked-accounts'
import {RQKEY as RQKEY_MY_MUTED} from './my-muted-accounts'
@@ -61,6 +61,7 @@ describe('session', () => {
"emailAuthFactor": false,
"emailConfirmed": false,
"handle": "alice.test",
"isSelfHosted": true,
"pdsUrl": undefined,
"refreshJwt": "alice-refresh-jwt-1",
"service": "https://alice.com/",
@@ -100,6 +101,7 @@ describe('session', () => {
"emailAuthFactor": false,
"emailConfirmed": false,
"handle": "alice.test",
"isSelfHosted": true,
"pdsUrl": undefined,
"refreshJwt": undefined,
"service": "https://alice.com/",
@@ -152,6 +154,7 @@ describe('session', () => {
"emailAuthFactor": false,
"emailConfirmed": false,
"handle": "alice.test",
"isSelfHosted": true,
"pdsUrl": undefined,
"refreshJwt": "alice-refresh-jwt-1",
"service": "https://alice.com/",
@@ -202,6 +205,7 @@ describe('session', () => {
"emailAuthFactor": false,
"emailConfirmed": false,
"handle": "bob.test",
"isSelfHosted": true,
"pdsUrl": undefined,
"refreshJwt": "bob-refresh-jwt-1",
"service": "https://bob.com/",
@@ -216,6 +220,7 @@ describe('session', () => {
"emailAuthFactor": false,
"emailConfirmed": false,
"handle": "alice.test",
"isSelfHosted": true,
"pdsUrl": undefined,
"refreshJwt": "alice-refresh-jwt-1",
"service": "https://alice.com/",
@@ -266,6 +271,7 @@ describe('session', () => {
"emailAuthFactor": false,
"emailConfirmed": false,
"handle": "alice-updated.test",
"isSelfHosted": true,
"pdsUrl": undefined,
"refreshJwt": "alice-refresh-jwt-2",
"service": "https://alice.com/",
@@ -280,6 +286,7 @@ describe('session', () => {
"emailAuthFactor": false,
"emailConfirmed": false,
"handle": "bob.test",
"isSelfHosted": true,
"pdsUrl": undefined,
"refreshJwt": "bob-refresh-jwt-1",
"service": "https://bob.com/",
@@ -328,6 +335,7 @@ describe('session', () => {
"emailAuthFactor": false,
"emailConfirmed": false,
"handle": "jay.test",
"isSelfHosted": true,
"pdsUrl": undefined,
"refreshJwt": "jay-refresh-jwt-1",
"service": "https://jay.com/",
@@ -342,6 +350,7 @@ describe('session', () => {
"emailAuthFactor": false,
"emailConfirmed": false,
"handle": "alice-updated.test",
"isSelfHosted": true,
"pdsUrl": undefined,
"refreshJwt": "alice-refresh-jwt-2",
"service": "https://alice.com/",
@@ -356,6 +365,7 @@ describe('session', () => {
"emailAuthFactor": false,
"emailConfirmed": false,
"handle": "bob.test",
"isSelfHosted": true,
"pdsUrl": undefined,
"refreshJwt": "bob-refresh-jwt-1",
"service": "https://bob.com/",
@@ -399,6 +409,7 @@ describe('session', () => {
"emailAuthFactor": false,
"emailConfirmed": false,
"handle": "jay.test",
"isSelfHosted": true,
"pdsUrl": undefined,
"refreshJwt": undefined,
"service": "https://jay.com/",
@@ -413,6 +424,7 @@ describe('session', () => {
"emailAuthFactor": false,
"emailConfirmed": false,
"handle": "alice-updated.test",
"isSelfHosted": true,
"pdsUrl": undefined,
"refreshJwt": undefined,
"service": "https://alice.com/",
@@ -427,6 +439,7 @@ describe('session', () => {
"emailAuthFactor": false,
"emailConfirmed": false,
"handle": "bob.test",
"isSelfHosted": true,
"pdsUrl": undefined,
"refreshJwt": undefined,
"service": "https://bob.com/",
@@ -488,6 +501,7 @@ describe('session', () => {
"emailAuthFactor": false,
"emailConfirmed": false,
"handle": "alice.test",
"isSelfHosted": true,
"pdsUrl": undefined,
"refreshJwt": undefined,
"service": "https://alice.com/",
@@ -535,6 +549,7 @@ describe('session', () => {
"emailAuthFactor": false,
"emailConfirmed": false,
"handle": "alice.test",
"isSelfHosted": true,
"pdsUrl": undefined,
"refreshJwt": "alice-refresh-jwt-2",
"service": "https://alice.com/",
@@ -651,6 +666,7 @@ describe('session', () => {
"emailAuthFactor": false,
"emailConfirmed": false,
"handle": "bob.test",
"isSelfHosted": true,
"pdsUrl": undefined,
"refreshJwt": "bob-refresh-jwt-1",
"service": "https://bob.com/",
@@ -743,6 +759,7 @@ describe('session', () => {
"emailAuthFactor": false,
"emailConfirmed": false,
"handle": "bob.test",
"isSelfHosted": true,
"pdsUrl": undefined,
"refreshJwt": undefined,
"service": "https://bob.com/",
@@ -757,6 +774,7 @@ describe('session', () => {
"emailAuthFactor": false,
"emailConfirmed": false,
"handle": "alice.test",
"isSelfHosted": true,
"pdsUrl": undefined,
"refreshJwt": "alice-refresh-jwt-1",
"service": "https://alice.com/",
@@ -832,6 +850,7 @@ describe('session', () => {
"emailAuthFactor": false,
"emailConfirmed": false,
"handle": "alice-updated.test",
"isSelfHosted": true,
"pdsUrl": undefined,
"refreshJwt": "alice-refresh-jwt-2",
"service": "https://alice.com/",
@@ -885,6 +904,7 @@ describe('session', () => {
"emailAuthFactor": true,
"emailConfirmed": true,
"handle": "alice-updated.test",
"isSelfHosted": true,
"pdsUrl": undefined,
"refreshJwt": "alice-refresh-jwt-3",
"service": "https://alice.com/",
@@ -938,6 +958,7 @@ describe('session', () => {
"emailAuthFactor": false,
"emailConfirmed": false,
"handle": "alice-updated.test",
"isSelfHosted": true,
"pdsUrl": undefined,
"refreshJwt": "alice-refresh-jwt-4",
"service": "https://alice.com/",
@@ -1102,6 +1123,7 @@ describe('session', () => {
"emailAuthFactor": false,
"emailConfirmed": false,
"handle": "bob.test",
"isSelfHosted": true,
"pdsUrl": undefined,
"refreshJwt": "bob-refresh-jwt-1",
"service": "https://bob.com/",
@@ -1116,6 +1138,7 @@ describe('session', () => {
"emailAuthFactor": false,
"emailConfirmed": false,
"handle": "alice-updated.test",
"isSelfHosted": true,
"pdsUrl": undefined,
"refreshJwt": "alice-refresh-jwt-2",
"service": "https://alice.com/",
@@ -1167,6 +1190,7 @@ describe('session', () => {
"emailAuthFactor": false,
"emailConfirmed": false,
"handle": "bob-updated.test",
"isSelfHosted": true,
"pdsUrl": undefined,
"refreshJwt": "bob-refresh-jwt-2",
"service": "https://bob.com/",
@@ -1181,6 +1205,7 @@ describe('session', () => {
"emailAuthFactor": false,
"emailConfirmed": false,
"handle": "alice-updated.test",
"isSelfHosted": true,
"pdsUrl": undefined,
"refreshJwt": "alice-refresh-jwt-2",
"service": "https://alice.com/",
@@ -1332,6 +1357,7 @@ describe('session', () => {
"emailAuthFactor": false,
"emailConfirmed": false,
"handle": "alice.test",
"isSelfHosted": true,
"pdsUrl": undefined,
"refreshJwt": "alice-refresh-jwt-1",
"service": "https://alice.com/",
@@ -1397,6 +1423,7 @@ describe('session', () => {
"emailAuthFactor": false,
"emailConfirmed": false,
"handle": "alice.test",
"isSelfHosted": true,
"pdsUrl": undefined,
"refreshJwt": undefined,
"service": "https://alice.com/",
@@ -1462,6 +1489,7 @@ describe('session', () => {
"emailAuthFactor": false,
"emailConfirmed": false,
"handle": "alice.test",
"isSelfHosted": true,
"pdsUrl": undefined,
"refreshJwt": undefined,
"service": "https://alice.com/",
@@ -1559,6 +1587,7 @@ describe('session', () => {
"emailAuthFactor": false,
"emailConfirmed": false,
"handle": "jay.test",
"isSelfHosted": true,
"pdsUrl": undefined,
"refreshJwt": "jay-refresh-jwt-1",
"service": "https://jay.com/",
@@ -1573,6 +1602,7 @@ describe('session', () => {
"emailAuthFactor": false,
"emailConfirmed": false,
"handle": "bob.test",
"isSelfHosted": true,
"pdsUrl": undefined,
"refreshJwt": "bob-refresh-jwt-2",
"service": "https://alice.com/",
@@ -1622,6 +1652,7 @@ describe('session', () => {
"emailAuthFactor": false,
"emailConfirmed": false,
"handle": "clarence.test",
"isSelfHosted": true,
"pdsUrl": undefined,
"refreshJwt": "clarence-refresh-jwt-2",
"service": "https://clarence.com/",
+2
View File
@@ -3,6 +3,7 @@ import {TID} from '@atproto/common-web'
import {networkRetry} from '#/lib/async/retry'
import {
BSKY_SERVICE,
DISCOVER_SAVED_FEED,
IS_PROD_SERVICE,
PUBLIC_BSKY_SERVICE,
@@ -204,6 +205,7 @@ export function agentToSessionAccount(
active: agent.session.active,
status: agent.session.status as SessionAccount['status'],
pdsUrl: agent.pdsUrl?.toString(),
isSelfHosted: !agent.serviceUrl.toString().startsWith(BSKY_SERVICE),
}
}
+7 -1
View File
@@ -83,7 +83,13 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
'exclamation-circle',
)
} else {
setState(opts)
setState(prevOpts => {
if (prevOpts) {
// Never replace an already open composer.
return prevOpts
}
return opts
})
}
})
+4 -3
View File
@@ -21,7 +21,7 @@ import {useSession} from '#/state/session'
import {useSetMinimalShellMode} from '#/state/shell'
import {useComposerControls} from '#/state/shell/composer'
import {useHeaderOffset} from '#/components/hooks/useHeaderOffset'
import {Feed} from '../posts/Feed'
import {PostFeed} from '../posts/PostFeed'
import {FAB} from '../util/fab/FAB'
import {ListMethods} from '../util/List'
import {LoadLatestBtn} from '../util/load-latest/LoadLatestBtn'
@@ -107,13 +107,14 @@ export function FeedPage({
})
}, [scrollToTop, feed, queryClient, setHasNew])
const shouldPrefetch = isNative && isPageAdjacent
return (
<View testID={testID}>
<MainScrollProvider>
<FeedFeedbackProvider value={feedFeedback}>
<Feed
<PostFeed
testID={testID ? `${testID}-feed` : undefined}
enabled={isPageFocused || isPageAdjacent}
enabled={isPageFocused || shouldPrefetch}
feed={feed}
feedParams={feedParams}
pollInterval={POLL_FREQ}
+3 -3
View File
@@ -10,7 +10,7 @@ import {useSession} from '#/state/session'
import {useShellLayout} from '#/state/shell/shell-layout'
import {HomeHeaderLayoutMobile} from '#/view/com/home/HomeHeaderLayoutMobile'
import {Logo} from '#/view/icons/Logo'
import {atoms as a, useBreakpoints, useGutterStyles, useTheme} from '#/alf'
import {atoms as a, useBreakpoints, useGutters, useTheme} from '#/alf'
import {ButtonIcon} from '#/components/Button'
import {Hashtag_Stroke2_Corner0_Rounded as FeedsIcon} from '#/components/icons/Hashtag'
import * as Layout from '#/components/Layout'
@@ -41,14 +41,14 @@ function HomeHeaderLayoutDesktopAndTablet({
const {hasSession} = useSession()
const {_} = useLingui()
const kawaii = useKawaiiMode()
const gutter = useGutterStyles()
const gutters = useGutters([0, 'base'])
return (
<>
{hasSession && (
<Layout.Center>
<View
style={[a.flex_row, a.align_center, a.pt_md, gutter, t.atoms.bg]}>
style={[a.flex_row, a.align_center, gutters, a.pt_md, t.atoms.bg]}>
<View style={{width: 34}} />
<View style={[a.flex_1, a.align_center, a.justify_center]}>
<Logo width={kawaii ? 60 : 28} />
@@ -21,13 +21,13 @@ import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
import {List, ListRef} from '#/view/com/util/List'
import {NotificationFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn'
import {FeedItem} from './FeedItem'
import {NotificationFeedItem} from './NotificationFeedItem'
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
const LOADING_ITEM = {_reactKey: '__loading__'}
export function Feed({
export function NotificationFeed({
scrollElRef,
onPressTryAgain,
onScrolledDownChange,
@@ -136,7 +136,7 @@ export function Feed({
)
}
return (
<FeedItem
<NotificationFeedItem
item={item}
moderationOpts={moderationOpts!}
hideTopBorder={index === 0}
@@ -76,7 +76,7 @@ interface Author {
moderation: ModerationDecision
}
let FeedItem = ({
let NotificationFeedItem = ({
item,
moderationOpts,
hideTopBorder,
@@ -494,8 +494,8 @@ let FeedItem = ({
</Link>
)
}
FeedItem = memo(FeedItem)
export {FeedItem}
NotificationFeedItem = memo(NotificationFeedItem)
export {NotificationFeedItem}
function ExpandListPressable({
hasMultipleAuthors,
+31 -8
View File
@@ -1,5 +1,7 @@
import React, {forwardRef} from 'react'
import React, {forwardRef, useCallback, useContext} from 'react'
import {View} from 'react-native'
import {DrawerGestureContext} from 'react-native-drawer-layout'
import {Gesture, GestureDetector} from 'react-native-gesture-handler'
import PagerView, {
PagerViewOnPageScrollEventData,
PagerViewOnPageSelectedEvent,
@@ -13,7 +15,9 @@ import Animated, {
useHandler,
useSharedValue,
} from 'react-native-reanimated'
import {useFocusEffect} from '@react-navigation/native'
import {useSetDrawerSwipeDisabled} from '#/state/shell'
import {atoms as a, native} from '#/alf'
export type PageSelectedEvent = PagerViewOnPageSelectedEvent
@@ -58,6 +62,18 @@ export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
const [selectedPage, setSelectedPage] = React.useState(initialPage)
const pagerView = React.useRef<PagerView>(null)
const [isIdle, setIsIdle] = React.useState(true)
const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
useFocusEffect(
useCallback(() => {
const canSwipeDrawer = selectedPage === 0 && isIdle
setDrawerSwipeDisabled(!canSwipeDrawer)
return () => {
setDrawerSwipeDisabled(false)
}
}, [setDrawerSwipeDisabled, selectedPage, isIdle]),
)
React.useImperativeHandle(ref, () => ({
setPage: (index: number) => {
pagerView.current?.setPage(index)
@@ -96,6 +112,7 @@ export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
},
onPageScrollStateChanged(e: PageScrollStateChangedNativeEventData) {
'worklet'
runOnJS(setIsIdle)(e.pageScrollState === 'idle')
if (dragState.get() === 'idle' && e.pageScrollState === 'settling') {
// This is a programmatic scroll on Android.
// Stay "idle" to match iOS and avoid confusing downstream code.
@@ -113,6 +130,10 @@ export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
[parentOnPageScrollStateChanged],
)
const drawerGesture = useContext(DrawerGestureContext)!
const nativeGesture =
Gesture.Native().requireExternalGestureToFail(drawerGesture)
return (
<View testID={testID} style={[a.flex_1, native(a.overflow_hidden)]}>
{renderTabBar({
@@ -121,13 +142,15 @@ export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
dragProgress,
dragState,
})}
<AnimatedPagerView
ref={pagerView}
style={[a.flex_1]}
initialPage={initialPage}
onPageScroll={handlePageScroll}>
{children}
</AnimatedPagerView>
<GestureDetector gesture={nativeGesture}>
<AnimatedPagerView
ref={pagerView}
style={[a.flex_1]}
initialPage={initialPage}
onPageScroll={handlePageScroll}>
{children}
</AnimatedPagerView>
</GestureDetector>
</View>
)
},
+6 -6
View File
@@ -89,7 +89,7 @@ export function PostThread({uri}: {uri: string | undefined}) {
const {hasSession, currentAccount} = useSession()
const {_} = useLingui()
const t = useTheme()
const {isMobile, isTabletOrMobile} = useWebMediaQueries()
const {isMobile} = useWebMediaQueries()
const initialNumToRender = useInitialNumToRender()
const {height: windowHeight} = useWindowDimensions()
const [hiddenRepliesState, setHiddenRepliesState] = React.useState(
@@ -367,8 +367,7 @@ export function PostThread({uri}: {uri: string | undefined}) {
skeleton?.highlightedPost?.type === 'post' &&
(skeleton.highlightedPost.ctx.isParentLoading ||
Boolean(skeleton?.parents && skeleton.parents.length > 0))
const showHeader =
isNative || (isTabletOrMobile && (!hasParents || !isFetching))
const showHeader = isNative || !hasParents || !isFetching
const renderItem = ({item, index}: {item: RowItem; index: number}) => {
if (item === REPLY_PROMPT && hasSession) {
@@ -422,9 +421,6 @@ export function PostThread({uri}: {uri: string | undefined}) {
</View>
)
} else if (isThreadPost(item)) {
if (!treeView && item.ctx.hasMoreSelfThread) {
return <PostThreadLoadMore post={item.post} />
}
const prev = isThreadPost(posts[index - 1])
? (posts[index - 1] as ThreadPost)
: undefined
@@ -437,6 +433,10 @@ export function PostThread({uri}: {uri: string | undefined}) {
const hasUnrevealedParents =
index === 0 && skeleton?.parents && maxParents < skeleton.parents.length
if (!treeView && prev && item.ctx.hasMoreSelfThread) {
return <PostThreadLoadMore post={prev.post} />
}
return (
<View
ref={item.ctx.isHighlightedPost ? highlightedPostRef : undefined}
@@ -37,9 +37,9 @@ import {List, ListRef} from '../util/List'
import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
import {DiscoverFallbackHeader} from './DiscoverFallbackHeader'
import {FeedErrorMessage} from './FeedErrorMessage'
import {FeedItem} from './FeedItem'
import {FeedShutdownMsg} from './FeedShutdownMsg'
import {PostFeedErrorMessage} from './PostFeedErrorMessage'
import {PostFeedItem} from './PostFeedItem'
import {ViewFullThread} from './ViewFullThread'
type FeedRow =
@@ -101,7 +101,7 @@ export function getFeedPostSlice(feedRow: FeedRow): FeedPostSlice | null {
// const REFRESH_AFTER = STALE.HOURS.ONE
const CHECK_LATEST_AFTER = STALE.SECONDS.THIRTY
let Feed = ({
let PostFeed = ({
feed,
feedParams,
ignoreFilterFor,
@@ -444,7 +444,7 @@ let Feed = ({
return renderEmptyState()
} else if (row.type === 'error') {
return (
<FeedErrorMessage
<PostFeedErrorMessage
feedDesc={feed}
error={error ?? undefined}
onPressTryAgain={onPressTryAgain}
@@ -480,7 +480,7 @@ let Feed = ({
const indexInSlice = row.indexInSlice
const item = slice.items[indexInSlice]
return (
<FeedItem
<PostFeedItem
post={item.post}
record={item.record}
reason={indexInSlice === 0 ? slice.reason : undefined}
@@ -576,8 +576,8 @@ let Feed = ({
</View>
)
}
Feed = memo(Feed)
export {Feed}
PostFeed = memo(PostFeed)
export {PostFeed}
const styles = StyleSheet.create({
feedFooter: {paddingTop: 20},
@@ -30,7 +30,7 @@ export enum KnownError {
Unknown = 'Unknown',
}
export function FeedErrorMessage({
export function PostFeedErrorMessage({
feedDesc,
error,
onPressTryAgain,
@@ -70,7 +70,7 @@ interface FeedItemProps {
isParentNotFound?: boolean
}
export function FeedItem({
export function PostFeedItem({
post,
record,
reason,
@@ -1,92 +0,0 @@
import {describe, expect, it} from '@jest/globals'
import {APP_LANGUAGES} from '#/locale/languages'
import {formatCount} from '../format'
const formatCountRound = (locale: string, num: number) => {
const options: Intl.NumberFormatOptions = {
notation: 'compact',
maximumFractionDigits: 1,
}
return new Intl.NumberFormat(locale, options).format(num)
}
const formatCountTrunc = (locale: string, num: number) => {
const options: Intl.NumberFormatOptions = {
notation: 'compact',
maximumFractionDigits: 1,
// @ts-ignore
roundingMode: 'trunc',
}
return new Intl.NumberFormat(locale, options).format(num)
}
// prettier-ignore
const testNums = [
1,
5,
9,
11,
55,
99,
111,
555,
999,
1111,
5555,
9999,
11111,
55555,
99999,
111111,
555555,
999999,
1111111,
5555555,
9999999,
11111111,
55555555,
99999999,
111111111,
555555555,
999999999,
1111111111,
5555555555,
9999999999,
11111111111,
55555555555,
99999999999,
111111111111,
555555555555,
999999999999,
1111111111111,
5555555555555,
9999999999999,
11111111111111,
55555555555555,
99999999999999,
111111111111111,
555555555555555,
999999999999999,
1111111111111111,
5555555555555555,
]
describe('formatCount', () => {
for (const appLanguage of APP_LANGUAGES) {
const locale = appLanguage.code2
it('truncates for ' + locale, () => {
const mockI8nn = {
locale,
number(num: number) {
return formatCountRound(locale, num)
},
}
for (const num of testNums) {
const formatManual = formatCount(mockI8nn as any, num)
const formatOriginal = formatCountTrunc(locale, num)
expect(formatManual).toEqual(formatOriginal)
}
})
}
})
+3 -43
View File
@@ -1,50 +1,10 @@
import {I18n} from '@lingui/core'
const truncateRounding = (num: number, factors: Array<number>): number => {
for (let i = factors.length - 1; i >= 0; i--) {
let factor = factors[i]
if (num >= 10 ** factor) {
if (factor === 10) {
// CA and ES abruptly jump from "9999,9 M" to "10 mil M"
factor--
}
const precision = 1
const divisor = 10 ** (factor - precision)
return Math.floor(num / divisor) * divisor
}
}
return num
}
const koFactors = [3, 4, 8, 12]
const hiFactors = [3, 5, 7, 9, 11, 13]
const esCaFactors = [3, 6, 10, 12]
const itDeFactors = [6, 9, 12]
const jaZhFactors = [4, 8, 12]
const glFactors = [6, 12]
const restFactors = [3, 6, 9, 12]
export const formatCount = (i18n: I18n, num: number) => {
const locale = i18n.locale
let truncatedNum: number
if (locale === 'hi') {
truncatedNum = truncateRounding(num, hiFactors)
} else if (locale === 'ko') {
truncatedNum = truncateRounding(num, koFactors)
} else if (locale === 'es' || locale === 'ca') {
truncatedNum = truncateRounding(num, esCaFactors)
} else if (locale === 'ja' || locale === 'zh-CN' || locale === 'zh-TW') {
truncatedNum = truncateRounding(num, jaZhFactors)
} else if (locale === 'it' || locale === 'de') {
truncatedNum = truncateRounding(num, itDeFactors)
} else if (locale === 'gl') {
truncatedNum = truncateRounding(num, glFactors)
} else {
truncatedNum = truncateRounding(num, restFactors)
}
return i18n.number(truncatedNum, {
return i18n.number(num, {
notation: 'compact',
maximumFractionDigits: 1,
// Ideally we'd use roundingMode: 'trunc' but it isn't supported on RN.
// @ts-expect-error - roundingMode not in the types
roundingMode: 'trunc',
})
}
+4 -4
View File
@@ -44,9 +44,9 @@ import {
import * as Layout from '#/components/Layout'
import {H1, H3, P, Text} from '#/components/Typography'
import {ScreenHider} from '../../components/moderation/ScreenHider'
import {FeedItem as NotifFeedItem} from '../com/notifications/FeedItem'
import {NotificationFeedItem} from '../com/notifications/NotificationFeedItem'
import {PostThreadItem} from '../com/post-thread/PostThreadItem'
import {FeedItem} from '../com/posts/FeedItem'
import {PostFeedItem} from '../com/posts/PostFeedItem'
import {ProfileCard} from '../com/profile/ProfileCard'
const LABEL_VALUES: (keyof typeof LABELS)[] = Object.keys(
@@ -817,7 +817,7 @@ function MockPostFeedItem({
)
}
return (
<FeedItem
<PostFeedItem
post={post}
record={post.record as AppBskyFeedPost.Record}
moderation={moderation}
@@ -872,7 +872,7 @@ function MockNotifItem({
</P>
)
}
return <NotifFeedItem item={notif} moderationOpts={moderationOpts} />
return <NotificationFeedItem item={notif} moderationOpts={moderationOpts} />
}
function MockAccountCard({
+3 -9
View File
@@ -19,7 +19,7 @@ import {FeedParams} from '#/state/queries/post-feed'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
import {useSession} from '#/state/session'
import {useSetDrawerSwipeDisabled, useSetMinimalShellMode} from '#/state/shell'
import {useSetMinimalShellMode} from '#/state/shell'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {useSelectedFeed, useSetSelectedFeed} from '#/state/shell/selected-feed'
import {FeedPage} from '#/view/com/feeds/FeedPage'
@@ -127,15 +127,10 @@ function HomeScreenReady({
const {hasSession} = useSession()
const setMinimalShellMode = useSetMinimalShellMode()
const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
useFocusEffect(
React.useCallback(() => {
setMinimalShellMode(false)
setDrawerSwipeDisabled(selectedIndex > 0)
return () => {
setDrawerSwipeDisabled(false)
}
}, [setDrawerSwipeDisabled, selectedIndex, setMinimalShellMode]),
}, [setMinimalShellMode]),
)
useFocusEffect(
@@ -154,7 +149,6 @@ function HomeScreenReady({
const onPageSelected = React.useCallback(
(index: number) => {
setMinimalShellMode(false)
setDrawerSwipeDisabled(index > 0)
const feed = allFeeds[index]
// Mutate the ref before setting state to avoid the imperative syncing effect
// above from starting a loop on Android when swiping back and forth.
@@ -166,7 +160,7 @@ function HomeScreenReady({
feedUrl: feed,
})
},
[setDrawerSwipeDisabled, setSelectedFeed, setMinimalShellMode, allFeeds],
[setSelectedFeed, setMinimalShellMode, allFeeds],
)
const onPressSelected = React.useCallback(() => {
+2 -2
View File
@@ -23,7 +23,7 @@ import {
import {truncateAndInvalidate} from '#/state/queries/util'
import {useSetMinimalShellMode} from '#/state/shell'
import {useComposerControls} from '#/state/shell/composer'
import {Feed} from '#/view/com/notifications/Feed'
import {NotificationFeed} from '#/view/com/notifications/NotificationFeed'
import {FAB} from '#/view/com/util/fab/FAB'
import {ListMethods} from '#/view/com/util/List'
import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn'
@@ -156,7 +156,7 @@ export function NotificationsScreen({route: {params}}: Props) {
</Layout.Header.Outer>
<MainScrollProvider>
<Feed
<NotificationFeed
onScrolledDownChange={setIsScrolledDown}
scrollElRef={scrollElRef}
overridePriorityNotifications={params?.show === 'all'}

Some files were not shown because too many files have changed in this diff Show More