Compare commits
22 Commits
ruby-v
...
fix-web-env
| Author | SHA1 | Date | |
|---|---|---|---|
| cdf13ae7fa | |||
| 080033a011 | |||
| e49dad2889 | |||
| 69f22b9dba | |||
| 48346edde6 | |||
| 89c6ca94fe | |||
| 7db5882ea2 | |||
| c55063d262 | |||
| b081f8f8d9 | |||
| de15f8e2d3 | |||
| 48c5341644 | |||
| 86155986af | |||
| bee50c3954 | |||
| 6308e91de6 | |||
| b7ddb07d90 | |||
| d00879e628 | |||
| e052f5e198 | |||
| f34e8d8cdf | |||
| 46e1e5cee6 | |||
| fec3352b68 | |||
| bc4b4a3cfe | |||
| 63c0c7e621 |
@@ -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
@@ -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.
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -5,3 +5,7 @@
|
||||
.break-word {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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"]
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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'
|
||||
|
||||
@@ -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
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
@@ -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])
|
||||
}
|
||||
@@ -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}],
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Vendored
+1
-1
@@ -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'
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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}) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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/",
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
+3
-3
@@ -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}
|
||||
+3
-3
@@ -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,
|
||||
@@ -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>
|
||||
)
|
||||
},
|
||||
|
||||
@@ -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},
|
||||
+1
-1
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -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',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user