Compare commits
32 Commits
ruby-v
...
fix-top-border
| Author | SHA1 | Date | |
|---|---|---|---|
| dfd038c3ba | |||
| 2808f8b73d | |||
| ffc63dc85f | |||
| 4b32b0a71b | |||
| f8cdd6b9ae | |||
| 10e241e7eb | |||
| 88166926fa | |||
| 704e36c280 | |||
| 20ac42a5a3 | |||
| c8745a9ab0 | |||
| 084905c146 | |||
| ddeadc21a0 | |||
| e49dad2889 | |||
| 69f22b9dba | |||
| 48346edde6 | |||
| 89c6ca94fe | |||
| 7db5882ea2 | |||
| c55063d262 | |||
| b081f8f8d9 | |||
| de15f8e2d3 | |||
| 48c5341644 | |||
| 86155986af | |||
| bee50c3954 | |||
| 6308e91de6 | |||
| b7ddb07d90 | |||
| d00879e628 | |||
| e052f5e198 | |||
| f34e8d8cdf | |||
| 46e1e5cee6 | |||
| fec3352b68 | |||
| bc4b4a3cfe | |||
| 63c0c7e621 |
@@ -1,5 +1,6 @@
|
||||
# Copy this to `.env` and `.env.test` files
|
||||
|
||||
BITDRIFT_API_KEY=
|
||||
SENTRY_AUTH_TOKEN=
|
||||
EXPO_PUBLIC_ENV=development
|
||||
EXPO_PUBLIC_LOG_LEVEL=debug
|
||||
|
||||
+3
-2
@@ -222,6 +222,7 @@ module.exports = function (config) {
|
||||
},
|
||||
],
|
||||
'react-native-compressor',
|
||||
'@bitdrift/react-native',
|
||||
'./plugins/starterPackAppClipExtension/withStarterPackAppClip.js',
|
||||
'./plugins/withAndroidManifestPlugin.js',
|
||||
'./plugins/withAndroidManifestFCMIconPlugin.js',
|
||||
@@ -234,8 +235,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.
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M7.5 2a1 1 0 0 0-1 1v3.997a6.25 6.25 0 0 1-1.83 4.42l-.377.376A1 1 0 0 0 4 12.5V15a1 1 0 0 0 1 1h6v5a1 1 0 1 0 2 0v-5h6a1 1 0 0 0 1-1v-2.5a1 1 0 0 0-.293-.707l-.376-.377a6.25 6.25 0 0 1-1.831-4.42V3.001a1 1 0 0 0-1-1h-9Z"/></svg>
|
||||
|
After Width: | Height: | Size: 323 B |
@@ -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.
|
||||
|
||||
@@ -87,6 +87,12 @@ However, if you're a part of the Bluesky team and want to enable Sentry, fill in
|
||||
|
||||
If you change `SENTRY_AUTH_TOKEN`, you need to do `yarn prebuild` before running `yarn ios` or `yarn android` again.
|
||||
|
||||
### Adding bitdrift
|
||||
|
||||
Adding bitdirft is NOT required. You can keep `BITDRIFT_API_KEY=` in `.env` which will avoid initializing bitdrift during startup.
|
||||
|
||||
However, if you're a part of the Bluesky team and want to enable bitdrift, fill in `BITDRIFT_API_KEY` in your `.env` to enable bitdrift.
|
||||
|
||||
### Adding and Updating Locales
|
||||
|
||||
- `yarn intl:build` -> you will also need to run this anytime `./src/locale/{locale}/messages.po` change
|
||||
|
||||
@@ -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.
|
||||
|
||||
+7
-6
@@ -54,7 +54,8 @@
|
||||
"icons:optimize": "svgo -f ./assets/icons"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "^0.13.18",
|
||||
"@atproto/api": "^0.13.20",
|
||||
"@bitdrift/react-native": "0.4.0",
|
||||
"@braintree/sanitize-url": "^6.0.2",
|
||||
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
@@ -62,9 +63,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",
|
||||
@@ -181,7 +182,7 @@
|
||||
"react-native-picker-select": "^9.3.1",
|
||||
"react-native-progress": "bluesky-social/react-native-progress",
|
||||
"react-native-qrcode-styled": "^0.3.3",
|
||||
"react-native-reanimated": "^3.16.3",
|
||||
"react-native-reanimated": "3.17.0-nightly-20241211-17e89ca24",
|
||||
"react-native-root-siblings": "^4.1.1",
|
||||
"react-native-safe-area-context": "4.14.0",
|
||||
"react-native-screens": "~4.3.0",
|
||||
@@ -205,7 +206,7 @@
|
||||
"zod": "^3.20.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@atproto/dev-env": "^0.3.64",
|
||||
"@atproto/dev-env": "^0.3.67",
|
||||
"@babel/core": "^7.26.0",
|
||||
"@babel/preset-env": "^7.26.0",
|
||||
"@babel/runtime": "^7.26.0",
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
## expo-modules-core Patch
|
||||
|
||||
This patch fixes an issue where bitdrift's API stream gets blocked by the Expo interceptor used to power the devtools
|
||||
@@ -0,0 +1,15 @@
|
||||
diff --git a/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt b/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt
|
||||
index 47c4d15..afe138d 100644
|
||||
--- a/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt
|
||||
+++ b/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt
|
||||
@@ -125,6 +125,10 @@ internal fun peekResponseBody(
|
||||
}
|
||||
|
||||
internal fun shouldParseBody(response: Response): Boolean {
|
||||
+ if (response.request.url.encodedPath == "/bitdrift_public.protobuf.client.v1.ApiService/Mux") {
|
||||
+ return false
|
||||
+ }
|
||||
+
|
||||
// Check for Content-Type
|
||||
val skipContentTypes = listOf(
|
||||
"text/event-stream", // Server Sent Events
|
||||
@@ -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>
|
||||
);
|
||||
+5
-1
@@ -1,5 +1,6 @@
|
||||
import 'react-native-url-polyfill/auto'
|
||||
import '#/lib/sentry' // must be near top
|
||||
import '#/lib/bitdrift' // must be near top
|
||||
import '#/view/icons'
|
||||
|
||||
import React, {useEffect, useState} from 'react'
|
||||
@@ -53,6 +54,7 @@ import {
|
||||
import {readLastActiveAccount} from '#/state/session/util'
|
||||
import {Provider as ShellStateProvider} from '#/state/shell'
|
||||
import {Provider as ComposerProvider} from '#/state/shell/composer'
|
||||
import {Provider as LightStatusBarProvider} from '#/state/shell/light-status-bar'
|
||||
import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out'
|
||||
import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide'
|
||||
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
|
||||
@@ -208,7 +210,9 @@ function App() {
|
||||
<SafeAreaProvider
|
||||
initialMetrics={initialWindowMetrics}>
|
||||
<IntentDialogProvider>
|
||||
<InnerApp />
|
||||
<LightStatusBarProvider>
|
||||
<InnerApp />
|
||||
</LightStatusBarProvider>
|
||||
</IntentDialogProvider>
|
||||
</SafeAreaProvider>
|
||||
</StarterPackProvider>
|
||||
|
||||
+4
-1
@@ -40,6 +40,7 @@ import {
|
||||
import {readLastActiveAccount} from '#/state/session/util'
|
||||
import {Provider as ShellStateProvider} from '#/state/shell'
|
||||
import {Provider as ComposerProvider} from '#/state/shell/composer'
|
||||
import {Provider as LightStatusBarProvider} from '#/state/shell/light-status-bar'
|
||||
import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out'
|
||||
import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide'
|
||||
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
|
||||
@@ -181,7 +182,9 @@ function App() {
|
||||
<PortalProvider>
|
||||
<StarterPackProvider>
|
||||
<IntentDialogProvider>
|
||||
<InnerApp />
|
||||
<LightStatusBarProvider>
|
||||
<InnerApp />
|
||||
</LightStatusBarProvider>
|
||||
</IntentDialogProvider>
|
||||
</StarterPackProvider>
|
||||
</PortalProvider>
|
||||
|
||||
+3
-3
@@ -55,10 +55,7 @@ import {NotificationsScreen} from '#/view/screens/Notifications'
|
||||
import {PostThreadScreen} from '#/view/screens/PostThread'
|
||||
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 +74,9 @@ 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 {ProfileFeedScreen} from '#/screens/Profile/ProfileFeed'
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ export function createThemes({
|
||||
dim: Theme
|
||||
} {
|
||||
const color = {
|
||||
like: '#ec4899',
|
||||
trueBlack: '#000000',
|
||||
|
||||
gray_0: `hsl(${hues.primary}, 20%, ${defaultScale[14]}%)`,
|
||||
@@ -124,6 +125,7 @@ export function createThemes({
|
||||
const lightPalette = {
|
||||
white: color.gray_0,
|
||||
black: color.gray_1000,
|
||||
like: color.like,
|
||||
|
||||
contrast_25: color.gray_25,
|
||||
contrast_50: color.gray_50,
|
||||
@@ -185,6 +187,7 @@ export function createThemes({
|
||||
const darkPalette: Palette = {
|
||||
white: color.gray_25,
|
||||
black: color.trueBlack,
|
||||
like: color.like,
|
||||
|
||||
contrast_25: color.gray_975,
|
||||
contrast_50: color.gray_950,
|
||||
@@ -246,6 +249,7 @@ export function createThemes({
|
||||
const dimPalette: Palette = {
|
||||
...darkPalette,
|
||||
black: `hsl(${hues.primary}, 28%, ${dimScale[0]}%)`,
|
||||
like: color.like,
|
||||
|
||||
contrast_25: `hsl(${hues.primary}, 28%, ${dimScale[1]}%)`,
|
||||
contrast_50: `hsl(${hues.primary}, 28%, ${dimScale[2]}%)`,
|
||||
|
||||
@@ -12,6 +12,7 @@ export type ThemeName = 'light' | 'dim' | 'dark'
|
||||
export type Palette = {
|
||||
white: string
|
||||
black: string
|
||||
like: string
|
||||
|
||||
contrast_25: string
|
||||
contrast_50: string
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
AtUri,
|
||||
RichText as RichTextApi,
|
||||
} from '@atproto/api'
|
||||
import {msg, plural, Trans} from '@lingui/macro'
|
||||
import {msg, Plural, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
@@ -210,10 +210,9 @@ export function Likes({count}: {count: number}) {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
|
||||
{plural(count || 0, {
|
||||
one: 'Liked by # user',
|
||||
other: 'Liked by # users',
|
||||
})}
|
||||
<Trans>
|
||||
Liked by <Plural value={count || 0} one="# user" other="# users" />
|
||||
</Trans>
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||
import {NavigationProp} from '#/lib/routes/types'
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {logger} from '#/logger'
|
||||
@@ -16,7 +15,6 @@ import {FeedDescriptor} from '#/state/queries/post-feed'
|
||||
import {useProfilesQuery} from '#/state/queries/profile'
|
||||
import {useSuggestedFollowsByActorQuery} from '#/state/queries/suggested-follows'
|
||||
import {useSession} from '#/state/session'
|
||||
import {useProgressGuide} from '#/state/shell/progress-guide'
|
||||
import * as userActionHistory from '#/state/userActionHistory'
|
||||
import {SeenPost} from '#/state/userActionHistory'
|
||||
import {atoms as a, useBreakpoints, useTheme, ViewStyleProp, web} from '#/alf'
|
||||
@@ -506,23 +504,9 @@ export function SuggestedFeeds() {
|
||||
|
||||
export function ProgressGuide() {
|
||||
const t = useTheme()
|
||||
const {isDesktop} = useWebMediaQueries()
|
||||
const guide = useProgressGuide('like-10-and-follow-7')
|
||||
|
||||
if (isDesktop) {
|
||||
return null
|
||||
}
|
||||
|
||||
return guide ? (
|
||||
<View
|
||||
style={[
|
||||
a.border_t,
|
||||
t.atoms.border_contrast_low,
|
||||
a.px_lg,
|
||||
a.py_lg,
|
||||
a.pb_lg,
|
||||
]}>
|
||||
return (
|
||||
<View style={[t.atoms.border_contrast_low, a.px_lg, a.py_lg, a.pb_lg]}>
|
||||
<ProgressGuideList />
|
||||
</View>
|
||||
) : null
|
||||
)
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ export function RegionalNotice() {
|
||||
)
|
||||
}
|
||||
|
||||
export function LikeCount({count}: {count: number}) {
|
||||
export function LikeCount({likeCount}: {likeCount: number}) {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<Text
|
||||
@@ -93,7 +93,9 @@ export function LikeCount({count}: {count: number}) {
|
||||
t.atoms.text_contrast_medium,
|
||||
{fontWeight: '600'},
|
||||
]}>
|
||||
<Plural value={count} one="Liked by # user" other="Liked by # users" />
|
||||
<Trans>
|
||||
Liked by <Plural value={likeCount} one="# user" other="# users" />
|
||||
</Trans>
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
@@ -138,7 +140,7 @@ export function Default({
|
||||
value={labeler.creator.description}
|
||||
handle={labeler.creator.handle}
|
||||
/>
|
||||
{labeler.likeCount ? <LikeCount count={labeler.likeCount} /> : null}
|
||||
{labeler.likeCount ? <LikeCount likeCount={labeler.likeCount} /> : null}
|
||||
</Content>
|
||||
</Outer>
|
||||
)
|
||||
|
||||
@@ -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}],
|
||||
@@ -175,7 +175,8 @@ export function TitleText({
|
||||
gtMobile && a.text_xl,
|
||||
style,
|
||||
]}
|
||||
numberOfLines={2}>
|
||||
numberOfLines={2}
|
||||
emoji>
|
||||
{children}
|
||||
</Text>
|
||||
)
|
||||
|
||||
@@ -21,6 +21,7 @@ export * as Header from '#/components/Layout/Header'
|
||||
|
||||
export type ScreenProps = React.ComponentProps<typeof View> & {
|
||||
style?: StyleProp<ViewStyle>
|
||||
noInsetTop?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -28,6 +29,7 @@ export type ScreenProps = React.ComponentProps<typeof View> & {
|
||||
*/
|
||||
export const Screen = React.memo(function Screen({
|
||||
style,
|
||||
noInsetTop,
|
||||
...props
|
||||
}: ScreenProps) {
|
||||
const {top} = useSafeAreaInsets()
|
||||
@@ -35,7 +37,7 @@ export const Screen = React.memo(function Screen({
|
||||
<>
|
||||
{isWeb && <WebCenterBorders />}
|
||||
<View
|
||||
style={[a.util_screen_outer, {paddingTop: top}, style]}
|
||||
style={[a.util_screen_outer, {paddingTop: noInsetTop ? 0 : top}, style]}
|
||||
{...props}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -3,3 +3,7 @@ import {createSinglePathSVG} from './TEMPLATE'
|
||||
export const Pin_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M6.5 3a1 1 0 0 1 1-1h9a1 1 0 0 1 1 1v3.997a6.25 6.25 0 0 0 1.83 4.42l.377.376A1 1 0 0 1 20 12.5V15a1 1 0 0 1-1 1h-6v5a1 1 0 1 1-2 0v-5H5a1 1 0 0 1-1-1v-2.5a1 1 0 0 1 .293-.707l.376-.377A6.25 6.25 0 0 0 6.5 6.996V3.001Zm2 1v2.997a8.25 8.25 0 0 1-2.416 5.834L6 12.914V14h12v-1.086l-.084-.083A8.25 8.25 0 0 1 15.5 6.997V4h-7Z',
|
||||
})
|
||||
|
||||
export const Pin_Filled_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M7.5 2a1 1 0 0 0-1 1v3.997a6.25 6.25 0 0 1-1.83 4.42l-.377.376A1 1 0 0 0 4 12.5V15a1 1 0 0 0 1 1h6v5a1 1 0 1 0 2 0v-5h6a1 1 0 0 0 1-1v-2.5a1 1 0 0 0-.293-.707l-.376-.377a6.25 6.25 0 0 1-1.831-4.42V3.001a1 1 0 0 0-1-1h-9Z',
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {StyleProp, View, ViewStyle} from 'react-native'
|
||||
import {AppBskyFeedDefs, ComAtprotoLabelDefs} from '@atproto/api'
|
||||
import {msg, Plural} from '@lingui/macro'
|
||||
import {msg, Plural, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useSession} from '#/state/session'
|
||||
@@ -50,17 +50,23 @@ export function LabelsOnMe({
|
||||
<ButtonIcon position="left" icon={CircleInfo} />
|
||||
<ButtonText style={[a.leading_snug]}>
|
||||
{type === 'account' ? (
|
||||
<Plural
|
||||
value={labels.length}
|
||||
one="# label has been placed on this account"
|
||||
other="# labels have been placed on this account"
|
||||
/>
|
||||
<Trans>
|
||||
<Plural
|
||||
value={labels.length}
|
||||
one="# label has"
|
||||
other="# labels have"
|
||||
/>{' '}
|
||||
been placed on this account
|
||||
</Trans>
|
||||
) : (
|
||||
<Plural
|
||||
value={labels.length}
|
||||
one="# label has been placed on this content"
|
||||
other="# labels have been placed on this content"
|
||||
/>
|
||||
<Trans>
|
||||
<Plural
|
||||
value={labels.length}
|
||||
one="# label has"
|
||||
other="# labels have"
|
||||
/>{' '}
|
||||
been placed on this content
|
||||
</Trans>
|
||||
)}
|
||||
</ButtonText>
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import {init} from '@bitdrift/react-native'
|
||||
|
||||
const BITDRIFT_API_KEY = process.env.BITDRIFT_API_KEY
|
||||
|
||||
if (BITDRIFT_API_KEY) {
|
||||
init(BITDRIFT_API_KEY, {url: 'https://api-bsky.bitdrift.io'})
|
||||
}
|
||||
@@ -239,14 +239,21 @@ export function useNotificationsHandler() {
|
||||
)
|
||||
logEvent('notifications:openApp', {})
|
||||
invalidateCachedUnreadPage()
|
||||
truncateAndInvalidate(queryClient, RQKEY_NOTIFS())
|
||||
const payload = e.notification.request.trigger
|
||||
.payload as NotificationPayload
|
||||
truncateAndInvalidate(queryClient, RQKEY_NOTIFS('all'))
|
||||
if (
|
||||
payload.reason === 'mention' ||
|
||||
payload.reason === 'quote' ||
|
||||
payload.reason === 'reply'
|
||||
) {
|
||||
truncateAndInvalidate(queryClient, RQKEY_NOTIFS('mentions'))
|
||||
}
|
||||
logger.debug('Notifications: handleNotification', {
|
||||
content: e.notification.request.content,
|
||||
payload: e.notification.request.trigger.payload,
|
||||
})
|
||||
handleNotification(
|
||||
e.notification.request.trigger.payload as NotificationPayload,
|
||||
)
|
||||
handleNotification(payload)
|
||||
Notifications.dismissAllNotificationsAsync()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -75,7 +75,7 @@ export type SearchTabNavigatorParams = CommonNavigatorParams & {
|
||||
}
|
||||
|
||||
export type NotificationsTabNavigatorParams = CommonNavigatorParams & {
|
||||
Notifications: {show?: 'all'}
|
||||
Notifications: undefined
|
||||
}
|
||||
|
||||
export type MyProfileTabNavigatorParams = CommonNavigatorParams & {
|
||||
@@ -90,7 +90,7 @@ export type FlatNavigatorParams = CommonNavigatorParams & {
|
||||
Home: undefined
|
||||
Search: {q?: string}
|
||||
Feeds: undefined
|
||||
Notifications: {show?: 'all'}
|
||||
Notifications: undefined
|
||||
Hashtag: {tag: string; author?: string}
|
||||
Messages: {pushToConversation?: string; animation?: 'push' | 'pop'}
|
||||
}
|
||||
@@ -102,7 +102,7 @@ export type AllNavigatorParams = CommonNavigatorParams & {
|
||||
Search: {q?: string}
|
||||
Feeds: undefined
|
||||
NotificationsTab: undefined
|
||||
Notifications: {show?: 'all'}
|
||||
Notifications: undefined
|
||||
MyProfileTab: undefined
|
||||
Hashtag: {tag: string; author?: string}
|
||||
MessagesTab: undefined
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import {
|
||||
debug as bdDebug,
|
||||
error as bdError,
|
||||
info as bdInfo,
|
||||
warn as bdWarn,
|
||||
} from '@bitdrift/react-native'
|
||||
|
||||
import {LogLevel, Transport} from './types'
|
||||
|
||||
export function createBitdriftTransport(): Transport {
|
||||
const logFunctions = {
|
||||
[LogLevel.Debug]: bdDebug,
|
||||
[LogLevel.Info]: bdInfo,
|
||||
[LogLevel.Log]: bdInfo,
|
||||
[LogLevel.Warn]: bdWarn,
|
||||
[LogLevel.Error]: bdError,
|
||||
} as const
|
||||
|
||||
return (level, message) => {
|
||||
const log = logFunctions[level]
|
||||
log(message.toString())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import {Transport} from './index'
|
||||
|
||||
export function createBitdriftTransport(): Transport {
|
||||
return (_level, _message) => {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
+9
-67
@@ -6,74 +6,12 @@ import {DebugContext} from '#/logger/debugContext'
|
||||
import {add} from '#/logger/logDump'
|
||||
import {Sentry} from '#/logger/sentry'
|
||||
import * as env from '#/env'
|
||||
import {createBitdriftTransport} from './bitdriftTransport'
|
||||
import {Metadata} from './types'
|
||||
import {ConsoleTransportEntry, LogLevel, Transport} from './types'
|
||||
|
||||
export enum LogLevel {
|
||||
Debug = 'debug',
|
||||
Info = 'info',
|
||||
Log = 'log',
|
||||
Warn = 'warn',
|
||||
Error = 'error',
|
||||
}
|
||||
|
||||
type Transport = (
|
||||
level: LogLevel,
|
||||
message: string | Error,
|
||||
metadata: Metadata,
|
||||
timestamp: number,
|
||||
) => void
|
||||
|
||||
/**
|
||||
* A union of some of Sentry's breadcrumb properties as well as Sentry's
|
||||
* `captureException` parameter, `CaptureContext`.
|
||||
*/
|
||||
type Metadata = {
|
||||
/**
|
||||
* Applied as Sentry breadcrumb types. Defaults to `default`.
|
||||
*
|
||||
* @see https://develop.sentry.dev/sdk/event-payloads/breadcrumbs/#breadcrumb-types
|
||||
*/
|
||||
type?:
|
||||
| 'default'
|
||||
| 'debug'
|
||||
| 'error'
|
||||
| 'navigation'
|
||||
| 'http'
|
||||
| 'info'
|
||||
| 'query'
|
||||
| 'transaction'
|
||||
| 'ui'
|
||||
| 'user'
|
||||
|
||||
/**
|
||||
* Passed through to `Sentry.captureException`
|
||||
*
|
||||
* @see https://github.com/getsentry/sentry-javascript/blob/903addf9a1a1534a6cb2ba3143654b918a86f6dd/packages/types/src/misc.ts#L65
|
||||
*/
|
||||
tags?: {
|
||||
[key: string]:
|
||||
| number
|
||||
| string
|
||||
| boolean
|
||||
| bigint
|
||||
| symbol
|
||||
| null
|
||||
| undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Any additional data, passed through to Sentry as `extra` param on
|
||||
* exceptions, or the `data` param on breadcrumbs.
|
||||
*/
|
||||
[key: string]: unknown
|
||||
} & Parameters<typeof Sentry.captureException>[1]
|
||||
|
||||
export type ConsoleTransportEntry = {
|
||||
id: string
|
||||
timestamp: number
|
||||
level: LogLevel
|
||||
message: string | Error
|
||||
metadata: Metadata
|
||||
}
|
||||
export {LogLevel}
|
||||
export type {ConsoleTransportEntry, Transport}
|
||||
|
||||
const enabledLogLevels: {
|
||||
[key in LogLevel]: LogLevel[]
|
||||
@@ -328,6 +266,10 @@ export class Logger {
|
||||
*/
|
||||
export const logger = new Logger()
|
||||
|
||||
if (!env.IS_TEST) {
|
||||
logger.addTransport(createBitdriftTransport())
|
||||
}
|
||||
|
||||
if (env.IS_DEV && !env.IS_TEST) {
|
||||
logger.addTransport(consoleTransport)
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import type {Sentry} from '#/logger/sentry'
|
||||
|
||||
export enum LogLevel {
|
||||
Debug = 'debug',
|
||||
Info = 'info',
|
||||
Log = 'log',
|
||||
Warn = 'warn',
|
||||
Error = 'error',
|
||||
}
|
||||
|
||||
export type Transport = (
|
||||
level: LogLevel,
|
||||
message: string | Error,
|
||||
metadata: Metadata,
|
||||
timestamp: number,
|
||||
) => void
|
||||
|
||||
/**
|
||||
* A union of some of Sentry's breadcrumb properties as well as Sentry's
|
||||
* `captureException` parameter, `CaptureContext`.
|
||||
*/
|
||||
export type Metadata = {
|
||||
/**
|
||||
* Applied as Sentry breadcrumb types. Defaults to `default`.
|
||||
*
|
||||
* @see https://develop.sentry.dev/sdk/event-payloads/breadcrumbs/#breadcrumb-types
|
||||
*/
|
||||
type?:
|
||||
| 'default'
|
||||
| 'debug'
|
||||
| 'error'
|
||||
| 'navigation'
|
||||
| 'http'
|
||||
| 'info'
|
||||
| 'query'
|
||||
| 'transaction'
|
||||
| 'ui'
|
||||
| 'user'
|
||||
|
||||
/**
|
||||
* Passed through to `Sentry.captureException`
|
||||
*
|
||||
* @see https://github.com/getsentry/sentry-javascript/blob/903addf9a1a1534a6cb2ba3143654b918a86f6dd/packages/types/src/misc.ts#L65
|
||||
*/
|
||||
tags?: {
|
||||
[key: string]:
|
||||
| number
|
||||
| string
|
||||
| boolean
|
||||
| bigint
|
||||
| symbol
|
||||
| null
|
||||
| undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Any additional data, passed through to Sentry as `extra` param on
|
||||
* exceptions, or the `data` param on breadcrumbs.
|
||||
*/
|
||||
[key: string]: unknown
|
||||
} & Parameters<typeof Sentry.captureException>[1]
|
||||
|
||||
export type ConsoleTransportEntry = {
|
||||
id: string
|
||||
timestamp: number
|
||||
level: LogLevel
|
||||
message: string | Error
|
||||
metadata: Metadata
|
||||
}
|
||||
@@ -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(() => {
|
||||
@@ -109,7 +107,7 @@ export default function HashtagScreen({
|
||||
|
||||
return (
|
||||
<Layout.Screen>
|
||||
<Layout.Header.Outer>
|
||||
<Layout.Header.Outer noBottomBorder>
|
||||
<Layout.Header.BackButton />
|
||||
<Layout.Header.Content>
|
||||
<Layout.Header.TitleText>{headerTitle}</Layout.Header.TitleText>
|
||||
|
||||
@@ -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="# repost"
|
||||
other="# reposts"
|
||||
/>
|
||||
</Layout.Header.SubtitleText>
|
||||
</>
|
||||
)}
|
||||
</Layout.Header.Content>
|
||||
<Layout.Header.Slot />
|
||||
</Layout.Header.Outer>
|
||||
<PostRepostedByComponent uri={uri} />
|
||||
</Layout.Screen>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import Animated, {
|
||||
useAnimatedReaction,
|
||||
useAnimatedStyle,
|
||||
} from 'react-native-reanimated'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {BlurView} from 'expo-blur'
|
||||
import {useIsFetching} from '@tanstack/react-query'
|
||||
|
||||
@@ -32,7 +33,7 @@ export function GrowableBanner({
|
||||
}) {
|
||||
const pagerContext = usePagerHeaderContext()
|
||||
|
||||
// pagerContext should only be present on iOS, but better safe than sorry
|
||||
// plain non-growable mode for Android/Web
|
||||
if (!pagerContext || !isIOS) {
|
||||
return (
|
||||
<View style={[a.w_full, a.h_full]}>
|
||||
@@ -60,6 +61,7 @@ function GrowableBannerInner({
|
||||
backButton?: React.ReactNode
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const {top: topInset} = useSafeAreaInsets()
|
||||
const isFetching = useIsProfileFetching()
|
||||
const animateSpinner = useShouldAnimateSpinner({isFetching, scrollY})
|
||||
|
||||
@@ -104,7 +106,7 @@ function GrowableBannerInner({
|
||||
const animatedBackButtonStyle = useAnimatedStyle(() => ({
|
||||
transform: [
|
||||
{
|
||||
translateY: interpolate(scrollY.get(), [-150, 60], [-150, 60], {
|
||||
translateY: interpolate(scrollY.get(), [-150, 10], [-150, 10], {
|
||||
extrapolateRight: Extrapolation.CLAMP,
|
||||
}),
|
||||
},
|
||||
@@ -128,7 +130,14 @@ function GrowableBannerInner({
|
||||
animatedProps={animatedBlurViewProps}
|
||||
/>
|
||||
</Animated.View>
|
||||
<View style={[a.absolute, a.inset_0, a.justify_center, a.align_center]}>
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.inset_0,
|
||||
{top: topInset - (isIOS ? 15 : 0)},
|
||||
a.justify_center,
|
||||
a.align_center,
|
||||
]}>
|
||||
<Animated.View style={[animatedSpinnerStyle]}>
|
||||
<ActivityIndicator
|
||||
key={animateSpinner ? 'spin' : 'stop'}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -291,10 +291,12 @@ let ProfileHeaderLabeler = ({
|
||||
},
|
||||
}}
|
||||
size="tiny"
|
||||
label={plural(likeCount, {
|
||||
one: 'Liked by # user',
|
||||
other: 'Liked by # users',
|
||||
})}>
|
||||
label={_(
|
||||
msg`Liked by ${plural(likeCount, {
|
||||
one: '# user',
|
||||
other: '# users',
|
||||
})}`,
|
||||
)}>
|
||||
{({hovered, focused, pressed}) => (
|
||||
<Text
|
||||
style={[
|
||||
@@ -304,11 +306,14 @@ let ProfileHeaderLabeler = ({
|
||||
(hovered || focused || pressed) &&
|
||||
t.atoms.text_contrast_high,
|
||||
]}>
|
||||
<Plural
|
||||
value={likeCount}
|
||||
one="Liked by # user"
|
||||
other="Liked by # users"
|
||||
/>
|
||||
<Trans>
|
||||
Liked by{' '}
|
||||
<Plural
|
||||
value={likeCount}
|
||||
one="# user"
|
||||
other="# users"
|
||||
/>
|
||||
</Trans>
|
||||
</Text>
|
||||
)}
|
||||
</Link>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import React, {memo} from 'react'
|
||||
import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native'
|
||||
import {MeasuredDimensions, runOnJS, runOnUI} from 'react-native-reanimated'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {AppBskyActorDefs, ModerationDecision} from '@atproto/api'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {BACK_HITSLOP} from '#/lib/constants'
|
||||
import {measureHandle, useHandleRef} from '#/lib/hooks/useHandleRef'
|
||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||
import {NavigationProp} from '#/lib/routes/types'
|
||||
import {isIOS} from '#/platform/detection'
|
||||
import {Shadow} from '#/state/cache/types'
|
||||
@@ -18,11 +17,13 @@ import {useSession} from '#/state/session'
|
||||
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {UserBanner} from '#/view/com/util/UserBanner'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {atoms as a, platform, useTheme} from '#/alf'
|
||||
import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeftIcon} from '#/components/icons/Arrow'
|
||||
import {LabelsOnMe} from '#/components/moderation/LabelsOnMe'
|
||||
import {ProfileHeaderAlerts} from '#/components/moderation/ProfileHeaderAlerts'
|
||||
import {GrowableAvatar} from './GrowableAvatar'
|
||||
import {GrowableBanner} from './GrowableBanner'
|
||||
import {StatusBarShadow} from './StatusBarShadow'
|
||||
|
||||
interface Props {
|
||||
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>
|
||||
@@ -43,7 +44,8 @@ let ProfileHeaderShell = ({
|
||||
const {_} = useLingui()
|
||||
const {openLightbox} = useLightboxControls()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const {isDesktop} = useWebMediaQueries()
|
||||
const {top: topInset} = useSafeAreaInsets()
|
||||
|
||||
const aviRef = useHandleRef()
|
||||
|
||||
const onPressBack = React.useCallback(() => {
|
||||
@@ -100,10 +102,11 @@ let ProfileHeaderShell = ({
|
||||
<View
|
||||
pointerEvents={isIOS ? 'auto' : 'box-none'}
|
||||
style={[a.relative, {height: 150}]}>
|
||||
<StatusBarShadow />
|
||||
<GrowableBanner
|
||||
backButton={
|
||||
<>
|
||||
{!isDesktop && !hideBackButton && (
|
||||
{!hideBackButton && (
|
||||
<TouchableWithoutFeedback
|
||||
testID="profileHeaderBackBtn"
|
||||
onPress={onPressBack}
|
||||
@@ -111,12 +114,17 @@ let ProfileHeaderShell = ({
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Back`)}
|
||||
accessibilityHint="">
|
||||
<View style={styles.backBtnWrapper}>
|
||||
<FontAwesomeIcon
|
||||
size={18}
|
||||
icon="angle-left"
|
||||
color="white"
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
styles.backBtnWrapper,
|
||||
{
|
||||
top: platform({
|
||||
web: 10,
|
||||
default: topInset,
|
||||
}),
|
||||
},
|
||||
]}>
|
||||
<ArrowLeftIcon size="lg" fill="white" />
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
)}
|
||||
@@ -186,7 +194,6 @@ export {ProfileHeaderShell}
|
||||
const styles = StyleSheet.create({
|
||||
backBtnWrapper: {
|
||||
position: 'absolute',
|
||||
top: 10,
|
||||
left: 10,
|
||||
width: 30,
|
||||
height: 30,
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import Animated, {SharedValue, useAnimatedStyle} from 'react-native-reanimated'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {LinearGradient} from 'expo-linear-gradient'
|
||||
|
||||
import {isIOS} from '#/platform/detection'
|
||||
import {usePagerHeaderContext} from '#/view/com/pager/PagerHeaderContext'
|
||||
import {atoms as a} from '#/alf'
|
||||
|
||||
const AnimatedLinearGradient = Animated.createAnimatedComponent(LinearGradient)
|
||||
|
||||
export function StatusBarShadow() {
|
||||
const {top: topInset} = useSafeAreaInsets()
|
||||
const pagerContext = usePagerHeaderContext()
|
||||
|
||||
if (isIOS && pagerContext) {
|
||||
const {scrollY} = pagerContext
|
||||
return <StatusBarShadowInnner scrollY={scrollY} />
|
||||
}
|
||||
|
||||
return (
|
||||
<LinearGradient
|
||||
colors={['rgba(0,0,0,0.5)', 'rgba(0,0,0,0)']}
|
||||
style={[
|
||||
a.absolute,
|
||||
a.z_10,
|
||||
{height: topInset, top: 0, left: 0, right: 0},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function StatusBarShadowInnner({scrollY}: {scrollY: SharedValue<number>}) {
|
||||
const {top: topInset} = useSafeAreaInsets()
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
transform: [
|
||||
{
|
||||
translateY: Math.min(0, scrollY.get()),
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<AnimatedLinearGradient
|
||||
colors={['rgba(0,0,0,0.5)', 'rgba(0,0,0,0)']}
|
||||
style={[
|
||||
animatedStyle,
|
||||
a.absolute,
|
||||
a.z_10,
|
||||
{height: topInset, top: 0, left: 0, right: 0},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export function StatusBarShadow() {
|
||||
return null
|
||||
}
|
||||
@@ -1,14 +1,25 @@
|
||||
import React, {memo} from 'react'
|
||||
import {StyleSheet, View} from 'react-native'
|
||||
import React, {memo, useState} from 'react'
|
||||
import {LayoutChangeEvent, StyleSheet, View} from 'react-native'
|
||||
import Animated, {
|
||||
runOnJS,
|
||||
useAnimatedReaction,
|
||||
useAnimatedStyle,
|
||||
withTiming,
|
||||
} from 'react-native-reanimated'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {
|
||||
AppBskyActorDefs,
|
||||
AppBskyLabelerDefs,
|
||||
ModerationOpts,
|
||||
RichText as RichTextAPI,
|
||||
} from '@atproto/api'
|
||||
import {useIsFocused} from '@react-navigation/native'
|
||||
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {useSetLightStatusBar} from '#/state/shell/light-status-bar'
|
||||
import {usePagerHeaderContext} from '#/view/com/pager/PagerHeaderContext'
|
||||
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||
import {useTheme} from '#/alf'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {ProfileHeaderLabeler} from './ProfileHeaderLabeler'
|
||||
import {ProfileHeaderStandard} from './ProfileHeaderStandard'
|
||||
|
||||
@@ -43,20 +54,114 @@ interface Props {
|
||||
moderationOpts: ModerationOpts
|
||||
hideBackButton?: boolean
|
||||
isPlaceholderProfile?: boolean
|
||||
setMinimumHeight: (height: number) => void
|
||||
}
|
||||
|
||||
let ProfileHeader = (props: Props): React.ReactNode => {
|
||||
let ProfileHeader = ({setMinimumHeight, ...props}: Props): React.ReactNode => {
|
||||
let content
|
||||
if (props.profile.associated?.labeler) {
|
||||
if (!props.labeler) {
|
||||
return <ProfileHeaderLoading />
|
||||
content = <ProfileHeaderLoading />
|
||||
} else {
|
||||
content = <ProfileHeaderLabeler {...props} labeler={props.labeler} />
|
||||
}
|
||||
return <ProfileHeaderLabeler {...props} labeler={props.labeler} />
|
||||
} else {
|
||||
content = <ProfileHeaderStandard {...props} />
|
||||
}
|
||||
return <ProfileHeaderStandard {...props} />
|
||||
|
||||
return (
|
||||
<>
|
||||
{isNative && (
|
||||
<MinimalHeader
|
||||
onLayout={evt => setMinimumHeight(evt.nativeEvent.layout.height)}
|
||||
profile={props.profile}
|
||||
hideBackButton={props.hideBackButton}
|
||||
/>
|
||||
)}
|
||||
{content}
|
||||
</>
|
||||
)
|
||||
}
|
||||
ProfileHeader = memo(ProfileHeader)
|
||||
export {ProfileHeader}
|
||||
|
||||
const MinimalHeader = React.memo(function MinimalHeader({
|
||||
onLayout,
|
||||
}: {
|
||||
onLayout: (e: LayoutChangeEvent) => void
|
||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||
hideBackButton?: boolean
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const insets = useSafeAreaInsets()
|
||||
const ctx = usePagerHeaderContext()
|
||||
const [visible, setVisible] = useState(false)
|
||||
const [minimalHeaderHeight, setMinimalHeaderHeight] = React.useState(0)
|
||||
const isScreenFocused = useIsFocused()
|
||||
if (!ctx) throw new Error('MinimalHeader cannot be used on web')
|
||||
const {scrollY, headerHeight} = ctx
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => {
|
||||
// if we don't yet have the min header height in JS, hide
|
||||
if (!_WORKLET || minimalHeaderHeight === 0) {
|
||||
return {
|
||||
opacity: 0,
|
||||
}
|
||||
}
|
||||
const pastThreshold = scrollY.get() > 100
|
||||
return {
|
||||
opacity: pastThreshold
|
||||
? withTiming(1, {duration: 75})
|
||||
: withTiming(0, {duration: 75}),
|
||||
transform: [
|
||||
{
|
||||
translateY: Math.min(
|
||||
scrollY.get(),
|
||||
headerHeight - minimalHeaderHeight,
|
||||
),
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
|
||||
useAnimatedReaction(
|
||||
() => scrollY.get() > 100,
|
||||
(value, prev) => {
|
||||
if (prev !== value) {
|
||||
runOnJS(setVisible)(value)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
useSetLightStatusBar(isScreenFocused && !visible)
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
pointerEvents={visible ? 'auto' : 'none'}
|
||||
aria-hidden={!visible}
|
||||
accessibilityElementsHidden={!visible}
|
||||
importantForAccessibility={visible ? 'auto' : 'no-hide-descendants'}
|
||||
onLayout={evt => {
|
||||
setMinimalHeaderHeight(evt.nativeEvent.layout.height)
|
||||
onLayout(evt)
|
||||
}}
|
||||
style={[
|
||||
a.absolute,
|
||||
a.z_50,
|
||||
t.atoms.bg,
|
||||
{
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
paddingTop: insets.top,
|
||||
},
|
||||
animatedStyle,
|
||||
]}
|
||||
/>
|
||||
)
|
||||
})
|
||||
MinimalHeader.displayName = 'MinimalHeader'
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
avi: {
|
||||
position: 'absolute',
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
import React, {useCallback, useMemo} from 'react'
|
||||
import {StyleSheet, View} from 'react-native'
|
||||
import {useAnimatedRef} from 'react-native-reanimated'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useIsFocused, useNavigation} from '@react-navigation/native'
|
||||
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {useSetTitle} from '#/lib/hooks/useSetTitle'
|
||||
import {ComposeIcon2} from '#/lib/icons'
|
||||
import {CommonNavigatorParams} from '#/lib/routes/types'
|
||||
import {NavigationProp} from '#/lib/routes/types'
|
||||
import {makeRecordUri} from '#/lib/strings/url-helpers'
|
||||
import {s} from '#/lib/styles'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {listenSoftReset} from '#/state/events'
|
||||
import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
|
||||
import {FeedSourceFeedInfo, useFeedSourceInfoQuery} from '#/state/queries/feed'
|
||||
import {FeedDescriptor} from '#/state/queries/post-feed'
|
||||
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
|
||||
import {
|
||||
usePreferencesQuery,
|
||||
UsePreferencesQueryResponse,
|
||||
} from '#/state/queries/preferences'
|
||||
import {useResolveUriQuery} from '#/state/queries/resolve-uri'
|
||||
import {truncateAndInvalidate} from '#/state/queries/util'
|
||||
import {useSession} from '#/state/session'
|
||||
import {useComposerControls} from '#/state/shell/composer'
|
||||
import {PostFeed} from '#/view/com/posts/PostFeed'
|
||||
import {EmptyState} from '#/view/com/util/EmptyState'
|
||||
import {FAB} from '#/view/com/util/fab/FAB'
|
||||
import {Button} from '#/view/com/util/forms/Button'
|
||||
import {ListRef} from '#/view/com/util/List'
|
||||
import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn'
|
||||
import {LoadingScreen} from '#/view/com/util/LoadingScreen'
|
||||
import {Text} from '#/view/com/util/text/Text'
|
||||
import {ProfileFeedHeader} from '#/screens/Profile/components/ProfileFeedHeader'
|
||||
import * as Layout from '#/components/Layout'
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFeed'>
|
||||
export function ProfileFeedScreen(props: Props) {
|
||||
const {rkey, name: handleOrDid} = props.route.params
|
||||
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
|
||||
const uri = useMemo(
|
||||
() => makeRecordUri(handleOrDid, 'app.bsky.feed.generator', rkey),
|
||||
[rkey, handleOrDid],
|
||||
)
|
||||
const {error, data: resolvedUri} = useResolveUriQuery(uri)
|
||||
|
||||
const onPressBack = React.useCallback(() => {
|
||||
if (navigation.canGoBack()) {
|
||||
navigation.goBack()
|
||||
} else {
|
||||
navigation.navigate('Home')
|
||||
}
|
||||
}, [navigation])
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Layout.Screen testID="profileFeedScreenError">
|
||||
<Layout.Content>
|
||||
<View style={[pal.view, pal.border, styles.notFoundContainer]}>
|
||||
<Text type="title-lg" style={[pal.text, s.mb10]}>
|
||||
<Trans>Could not load feed</Trans>
|
||||
</Text>
|
||||
<Text type="md" style={[pal.text, s.mb20]}>
|
||||
{error.toString()}
|
||||
</Text>
|
||||
|
||||
<View style={{flexDirection: 'row'}}>
|
||||
<Button
|
||||
type="default"
|
||||
accessibilityLabel={_(msg`Go back`)}
|
||||
accessibilityHint={_(msg`Returns to previous page`)}
|
||||
onPress={onPressBack}
|
||||
style={{flexShrink: 1}}>
|
||||
<Text type="button" style={pal.text}>
|
||||
<Trans>Go Back</Trans>
|
||||
</Text>
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</Layout.Content>
|
||||
</Layout.Screen>
|
||||
)
|
||||
}
|
||||
|
||||
return resolvedUri ? (
|
||||
<Layout.Screen noInsetTop>
|
||||
<ProfileFeedScreenIntermediate feedUri={resolvedUri.uri} />
|
||||
</Layout.Screen>
|
||||
) : (
|
||||
<Layout.Screen>
|
||||
<LoadingScreen />
|
||||
</Layout.Screen>
|
||||
)
|
||||
}
|
||||
|
||||
function ProfileFeedScreenIntermediate({feedUri}: {feedUri: string}) {
|
||||
const {data: preferences} = usePreferencesQuery()
|
||||
const {data: info} = useFeedSourceInfoQuery({uri: feedUri})
|
||||
|
||||
if (!preferences || !info) {
|
||||
return <LoadingScreen />
|
||||
}
|
||||
|
||||
return (
|
||||
<ProfileFeedScreenInner
|
||||
preferences={preferences}
|
||||
feedInfo={info as FeedSourceFeedInfo}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function ProfileFeedScreenInner({
|
||||
feedInfo,
|
||||
}: {
|
||||
preferences: UsePreferencesQueryResponse
|
||||
feedInfo: FeedSourceFeedInfo
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const {hasSession} = useSession()
|
||||
const {openComposer} = useComposerControls()
|
||||
const isScreenFocused = useIsFocused()
|
||||
|
||||
useSetTitle(feedInfo?.displayName)
|
||||
|
||||
const feed = `feedgen|${feedInfo.uri}` as FeedDescriptor
|
||||
|
||||
const [hasNew, setHasNew] = React.useState(false)
|
||||
const [isScrolledDown, setIsScrolledDown] = React.useState(false)
|
||||
const queryClient = useQueryClient()
|
||||
const feedFeedback = useFeedFeedback(feed, hasSession)
|
||||
const scrollElRef = useAnimatedRef() as ListRef
|
||||
|
||||
const onScrollToTop = useCallback(() => {
|
||||
scrollElRef.current?.scrollToOffset({
|
||||
animated: isNative,
|
||||
offset: 0, // -headerHeight,
|
||||
})
|
||||
truncateAndInvalidate(queryClient, FEED_RQKEY(feed))
|
||||
setHasNew(false)
|
||||
}, [scrollElRef, queryClient, feed, setHasNew])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isScreenFocused) {
|
||||
return
|
||||
}
|
||||
return listenSoftReset(onScrollToTop)
|
||||
}, [onScrollToTop, isScreenFocused])
|
||||
|
||||
const renderPostsEmpty = useCallback(() => {
|
||||
return <EmptyState icon="hashtag" message={_(msg`This feed is empty.`)} />
|
||||
}, [_])
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProfileFeedHeader info={feedInfo} />
|
||||
|
||||
<FeedFeedbackProvider value={feedFeedback}>
|
||||
<PostFeed
|
||||
feed={feed}
|
||||
pollInterval={60e3}
|
||||
disablePoll={hasNew}
|
||||
onHasNew={setHasNew}
|
||||
scrollElRef={scrollElRef}
|
||||
onScrolledDownChange={setIsScrolledDown}
|
||||
renderEmptyState={renderPostsEmpty}
|
||||
/>
|
||||
</FeedFeedbackProvider>
|
||||
|
||||
{(isScrolledDown || hasNew) && (
|
||||
<LoadLatestBtn
|
||||
onPress={onScrollToTop}
|
||||
label={_(msg`Load new posts`)}
|
||||
showIndicator={hasNew}
|
||||
/>
|
||||
)}
|
||||
|
||||
{hasSession && (
|
||||
<FAB
|
||||
testID="composeFAB"
|
||||
onPress={() => openComposer({})}
|
||||
icon={
|
||||
<ComposeIcon2
|
||||
strokeWidth={1.5}
|
||||
size={29}
|
||||
style={{color: 'white'}}
|
||||
/>
|
||||
}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`New post`)}
|
||||
accessibilityHint=""
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
btn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
paddingVertical: 7,
|
||||
paddingHorizontal: 14,
|
||||
borderRadius: 50,
|
||||
marginLeft: 6,
|
||||
},
|
||||
notFoundContainer: {
|
||||
margin: 10,
|
||||
paddingHorizontal: 18,
|
||||
paddingVertical: 14,
|
||||
borderRadius: 6,
|
||||
},
|
||||
aboutSectionContainer: {
|
||||
paddingVertical: 4,
|
||||
paddingHorizontal: 16,
|
||||
gap: 12,
|
||||
},
|
||||
})
|
||||
@@ -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.followsCount ?? 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}
|
||||
|
||||
@@ -0,0 +1,534 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {AtUri} from '@atproto/api'
|
||||
import {msg, Plural, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useHaptics} from '#/lib/haptics'
|
||||
import {makeProfileLink} from '#/lib/routes/links'
|
||||
import {makeCustomFeedLink} from '#/lib/routes/links'
|
||||
import {shareUrl} from '#/lib/sharing'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {toShareUrl} from '#/lib/strings/url-helpers'
|
||||
import {logger} from '#/logger'
|
||||
import {FeedSourceFeedInfo} from '#/state/queries/feed'
|
||||
import {useLikeMutation, useUnlikeMutation} from '#/state/queries/like'
|
||||
import {
|
||||
useAddSavedFeedsMutation,
|
||||
usePreferencesQuery,
|
||||
useRemoveFeedMutation,
|
||||
useUpdateSavedFeedsMutation,
|
||||
} from '#/state/queries/preferences'
|
||||
import {useSession} from '#/state/session'
|
||||
import {formatCount} from '#/view/com/util/numeric/format'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {Divider} from '#/components/Divider'
|
||||
import {useRichText} from '#/components/hooks/useRichText'
|
||||
import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox'
|
||||
import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron'
|
||||
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
|
||||
import {
|
||||
Heart2_Filled_Stroke2_Corner0_Rounded as HeartFilled,
|
||||
Heart2_Stroke2_Corner0_Rounded as Heart,
|
||||
} from '#/components/icons/Heart2'
|
||||
import {
|
||||
Pin_Filled_Corner0_Rounded as PinFilled,
|
||||
Pin_Stroke2_Corner0_Rounded as Pin,
|
||||
} from '#/components/icons/Pin'
|
||||
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
||||
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||
import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
|
||||
import * as Layout from '#/components/Layout'
|
||||
import {InlineLinkText} from '#/components/Link'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import * as Menu from '#/components/Menu'
|
||||
import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
|
||||
import {RichText} from '#/components/RichText'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) {
|
||||
const t = useTheme()
|
||||
const {_, i18n} = useLingui()
|
||||
const {hasSession} = useSession()
|
||||
const {gtPhone, gtMobile} = useBreakpoints()
|
||||
const {top} = useSafeAreaInsets()
|
||||
const infoControl = Dialog.useDialogControl()
|
||||
const playHaptic = useHaptics()
|
||||
|
||||
const {data: preferences} = usePreferencesQuery()
|
||||
|
||||
const [likeUri, setLikeUri] = React.useState(info.likeUri || '')
|
||||
const isLiked = !!likeUri
|
||||
const likeCount =
|
||||
isLiked && likeUri ? (info.likeCount || 0) + 1 : info.likeCount || 0
|
||||
|
||||
const {mutateAsync: addSavedFeeds, isPending: isAddSavedFeedPending} =
|
||||
useAddSavedFeedsMutation()
|
||||
const {mutateAsync: removeFeed, isPending: isRemovePending} =
|
||||
useRemoveFeedMutation()
|
||||
const {mutateAsync: updateSavedFeeds, isPending: isUpdateFeedPending} =
|
||||
useUpdateSavedFeedsMutation()
|
||||
|
||||
const isFeedStateChangePending =
|
||||
isAddSavedFeedPending || isRemovePending || isUpdateFeedPending
|
||||
const savedFeedConfig = preferences?.savedFeeds?.find(
|
||||
f => f.value === info.uri,
|
||||
)
|
||||
const isSaved = Boolean(savedFeedConfig)
|
||||
const isPinned = Boolean(savedFeedConfig?.pinned)
|
||||
|
||||
const onToggleSaved = React.useCallback(async () => {
|
||||
try {
|
||||
playHaptic()
|
||||
|
||||
if (savedFeedConfig) {
|
||||
await removeFeed(savedFeedConfig)
|
||||
Toast.show(_(msg`Removed from your feeds`))
|
||||
} else {
|
||||
await addSavedFeeds([
|
||||
{
|
||||
type: 'feed',
|
||||
value: info.uri,
|
||||
pinned: false,
|
||||
},
|
||||
])
|
||||
Toast.show(_(msg`Saved to your feeds`))
|
||||
}
|
||||
} catch (err) {
|
||||
Toast.show(
|
||||
_(
|
||||
msg`There was an issue updating your feeds, please check your internet connection and try again.`,
|
||||
),
|
||||
'xmark',
|
||||
)
|
||||
logger.error('Failed to update feeds', {message: err})
|
||||
}
|
||||
}, [_, playHaptic, info, removeFeed, addSavedFeeds, savedFeedConfig])
|
||||
|
||||
const onTogglePinned = React.useCallback(async () => {
|
||||
try {
|
||||
playHaptic()
|
||||
|
||||
if (savedFeedConfig) {
|
||||
const pinned = !savedFeedConfig.pinned
|
||||
await updateSavedFeeds([
|
||||
{
|
||||
...savedFeedConfig,
|
||||
pinned,
|
||||
},
|
||||
])
|
||||
|
||||
if (pinned) {
|
||||
Toast.show(_(msg`Pinned ${info.displayName} to Home`))
|
||||
} else {
|
||||
Toast.show(_(msg`Unpinned ${info.displayName} from Home`))
|
||||
}
|
||||
} else {
|
||||
await addSavedFeeds([
|
||||
{
|
||||
type: 'feed',
|
||||
value: info.uri,
|
||||
pinned: true,
|
||||
},
|
||||
])
|
||||
Toast.show(_(msg`Pinned ${info.displayName} to Home`))
|
||||
}
|
||||
} catch (e) {
|
||||
Toast.show(_(msg`There was an issue contacting the server`), 'xmark')
|
||||
logger.error('Failed to toggle pinned feed', {message: e})
|
||||
}
|
||||
}, [playHaptic, info, _, savedFeedConfig, updateSavedFeeds, addSavedFeeds])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Layout.Center
|
||||
style={[
|
||||
t.atoms.bg,
|
||||
a.z_10,
|
||||
{paddingTop: top},
|
||||
web([a.sticky, a.z_10, {top: 0}]),
|
||||
]}>
|
||||
<Layout.Header.Outer>
|
||||
<Layout.Header.BackButton />
|
||||
<Layout.Header.Content align="left">
|
||||
<Button
|
||||
label={_(msg`Open feed info screen`)}
|
||||
style={[
|
||||
a.justify_start,
|
||||
{
|
||||
paddingVertical: 6,
|
||||
paddingHorizontal: 8,
|
||||
paddingRight: 12,
|
||||
},
|
||||
]}
|
||||
onPress={() => {
|
||||
playHaptic()
|
||||
infoControl.open()
|
||||
}}>
|
||||
{({hovered, pressed}) => (
|
||||
<>
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.inset_0,
|
||||
a.rounded_sm,
|
||||
a.transition_transform,
|
||||
t.atoms.bg_contrast_25,
|
||||
pressed && t.atoms.bg_contrast_50,
|
||||
hovered && {
|
||||
transform: [{scaleX: 1.01}, {scaleY: 1.1}],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
|
||||
{info.avatar && (
|
||||
<UserAvatar size={32} type="algo" avatar={info.avatar} />
|
||||
)}
|
||||
|
||||
<View style={[a.flex_1]}>
|
||||
<Text
|
||||
style={[
|
||||
a.text_md,
|
||||
a.font_heavy,
|
||||
a.leading_tight,
|
||||
gtMobile && a.text_xl,
|
||||
]}
|
||||
numberOfLines={2}>
|
||||
{info.displayName}
|
||||
</Text>
|
||||
<View style={[a.flex_row, {gap: 6}]}>
|
||||
<Text
|
||||
style={[
|
||||
a.flex_shrink,
|
||||
a.text_xs,
|
||||
a.leading_snug,
|
||||
t.atoms.text_contrast_medium,
|
||||
gtPhone && a.text_sm,
|
||||
]}
|
||||
numberOfLines={1}>
|
||||
{sanitizeHandle(info.creatorHandle, '@')}
|
||||
</Text>
|
||||
<View style={[a.flex_row, a.align_center, {gap: 2}]}>
|
||||
<HeartFilled
|
||||
size="xs"
|
||||
fill={
|
||||
likeUri
|
||||
? t.palette.like
|
||||
: t.atoms.text_contrast_low.color
|
||||
}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
a.text_xs,
|
||||
a.leading_snug,
|
||||
t.atoms.text_contrast_medium,
|
||||
gtPhone && a.text_sm,
|
||||
]}
|
||||
numberOfLines={1}>
|
||||
{formatCount(i18n, likeCount)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<ChevronDown
|
||||
size="md"
|
||||
fill={t.atoms.text_contrast_low.color}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</Layout.Header.Content>
|
||||
|
||||
{hasSession && (
|
||||
<Layout.Header.Slot>
|
||||
{isPinned ? (
|
||||
<Menu.Root>
|
||||
<Menu.Trigger label={_(msg`Open feed options menu`)}>
|
||||
{({props}) => {
|
||||
return (
|
||||
<Button
|
||||
{...props}
|
||||
label={_(msg`Open feed options menu`)}
|
||||
size="small"
|
||||
variant="ghost"
|
||||
shape="square"
|
||||
color="secondary">
|
||||
<PinFilled size="lg" fill={t.palette.primary_500} />
|
||||
</Button>
|
||||
)
|
||||
}}
|
||||
</Menu.Trigger>
|
||||
|
||||
<Menu.Outer>
|
||||
<Menu.Item
|
||||
disabled={isFeedStateChangePending}
|
||||
label={_(msg`Unpin from home`)}
|
||||
onPress={onTogglePinned}>
|
||||
<Menu.ItemText>{_(msg`Unpin from home`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={X} position="right" />
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
disabled={isFeedStateChangePending}
|
||||
label={
|
||||
isSaved
|
||||
? _(msg`Remove from my feeds`)
|
||||
: _(msg`Save to my feeds`)
|
||||
}
|
||||
onPress={onToggleSaved}>
|
||||
<Menu.ItemText>
|
||||
{isSaved
|
||||
? _(msg`Remove from my feeds`)
|
||||
: _(msg`Save to my feeds`)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon
|
||||
icon={isSaved ? Trash : Plus}
|
||||
position="right"
|
||||
/>
|
||||
</Menu.Item>
|
||||
</Menu.Outer>
|
||||
</Menu.Root>
|
||||
) : (
|
||||
<Button
|
||||
label={_(msg`Pin to Home`)}
|
||||
size="small"
|
||||
variant="ghost"
|
||||
shape="square"
|
||||
color="secondary"
|
||||
onPress={onTogglePinned}>
|
||||
<ButtonIcon icon={Pin} size="lg" />
|
||||
</Button>
|
||||
)}
|
||||
</Layout.Header.Slot>
|
||||
)}
|
||||
</Layout.Header.Outer>
|
||||
</Layout.Center>
|
||||
|
||||
<Dialog.Outer control={infoControl}>
|
||||
<Dialog.Handle />
|
||||
<Dialog.ScrollableInner
|
||||
label={_(msg`Feed menu`)}
|
||||
style={[gtMobile ? {width: 'auto', minWidth: 450} : a.w_full]}>
|
||||
<DialogInner
|
||||
info={info}
|
||||
likeUri={likeUri}
|
||||
setLikeUri={setLikeUri}
|
||||
likeCount={likeCount}
|
||||
isPinned={isPinned}
|
||||
onTogglePinned={onTogglePinned}
|
||||
isFeedStateChangePending={isFeedStateChangePending}
|
||||
/>
|
||||
</Dialog.ScrollableInner>
|
||||
</Dialog.Outer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogInner({
|
||||
info,
|
||||
likeUri,
|
||||
setLikeUri,
|
||||
likeCount,
|
||||
isPinned,
|
||||
onTogglePinned,
|
||||
isFeedStateChangePending,
|
||||
}: {
|
||||
info: FeedSourceFeedInfo
|
||||
likeUri: string
|
||||
setLikeUri: (uri: string) => void
|
||||
likeCount: number
|
||||
isPinned: boolean
|
||||
onTogglePinned: () => void
|
||||
isFeedStateChangePending: boolean
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const {hasSession} = useSession()
|
||||
const playHaptic = useHaptics()
|
||||
const control = Dialog.useDialogContext()
|
||||
const reportDialogControl = useReportDialogControl()
|
||||
const [rt, loading] = useRichText(info.description.text)
|
||||
const {mutateAsync: likeFeed, isPending: isLikePending} = useLikeMutation()
|
||||
const {mutateAsync: unlikeFeed, isPending: isUnlikePending} =
|
||||
useUnlikeMutation()
|
||||
|
||||
const isLiked = !!likeUri
|
||||
const feedRkey = React.useMemo(() => new AtUri(info.uri).rkey, [info.uri])
|
||||
|
||||
const onToggleLiked = React.useCallback(async () => {
|
||||
try {
|
||||
playHaptic()
|
||||
|
||||
if (isLiked && likeUri) {
|
||||
await unlikeFeed({uri: likeUri})
|
||||
setLikeUri('')
|
||||
} else {
|
||||
const res = await likeFeed({uri: info.uri, cid: info.cid})
|
||||
setLikeUri(res.uri)
|
||||
}
|
||||
} catch (err) {
|
||||
Toast.show(
|
||||
_(
|
||||
msg`There was an issue contacting the server, please check your internet connection and try again.`,
|
||||
),
|
||||
'xmark',
|
||||
)
|
||||
logger.error('Failed to toggle like', {message: err})
|
||||
}
|
||||
}, [playHaptic, isLiked, likeUri, unlikeFeed, setLikeUri, likeFeed, info, _])
|
||||
|
||||
const onPressShare = React.useCallback(() => {
|
||||
playHaptic()
|
||||
const url = toShareUrl(info.route.href)
|
||||
shareUrl(url)
|
||||
}, [info, playHaptic])
|
||||
|
||||
const onPressReport = React.useCallback(() => {
|
||||
reportDialogControl.open()
|
||||
}, [reportDialogControl])
|
||||
|
||||
return loading ? (
|
||||
<Loader size="xl" />
|
||||
) : (
|
||||
<View style={[a.gap_md]}>
|
||||
<View style={[a.flex_row, a.align_center, a.gap_md]}>
|
||||
<UserAvatar type="algo" size={48} avatar={info.avatar} />
|
||||
|
||||
<View style={[a.flex_1, a.gap_2xs]}>
|
||||
<Text
|
||||
style={[a.text_2xl, a.font_heavy, a.leading_tight]}
|
||||
numberOfLines={2}>
|
||||
{info.displayName}
|
||||
</Text>
|
||||
<Text
|
||||
style={[a.text_sm, a.leading_tight, t.atoms.text_contrast_medium]}
|
||||
numberOfLines={1}>
|
||||
<Trans>
|
||||
By{' '}
|
||||
<InlineLinkText
|
||||
label={_(msg`View ${info.creatorHandle}'s profile`)}
|
||||
to={makeProfileLink({
|
||||
did: info.creatorDid,
|
||||
handle: info.creatorHandle,
|
||||
})}
|
||||
style={[
|
||||
a.text_sm,
|
||||
a.leading_tight,
|
||||
a.underline,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}
|
||||
numberOfLines={1}
|
||||
onPress={() => control.close()}>
|
||||
{sanitizeHandle(info.creatorHandle, '@')}
|
||||
</InlineLinkText>
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Button
|
||||
label={_(msg`Share this feed`)}
|
||||
size="small"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
shape="round"
|
||||
onPress={onPressShare}>
|
||||
<ButtonIcon icon={Share} size="lg" />
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
<RichText value={rt} style={[a.text_md, a.leading_snug]} />
|
||||
|
||||
<View style={[a.flex_row, a.gap_sm, a.align_center]}>
|
||||
{typeof likeCount === 'number' && (
|
||||
<InlineLinkText
|
||||
label={_(msg`View users who like this feed`)}
|
||||
to={makeCustomFeedLink(info.creatorDid, feedRkey, 'liked-by')}
|
||||
style={[a.underline, t.atoms.text_contrast_medium]}
|
||||
onPress={() => control.close()}>
|
||||
<Trans>
|
||||
Liked by <Plural value={likeCount} one="# user" other="# users" />
|
||||
</Trans>
|
||||
</InlineLinkText>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{hasSession && (
|
||||
<>
|
||||
<View style={[a.flex_row, a.gap_sm, a.align_center, a.pt_sm]}>
|
||||
<Button
|
||||
disabled={isLikePending || isUnlikePending}
|
||||
label={_(msg`Like feed`)}
|
||||
size="small"
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
onPress={onToggleLiked}
|
||||
style={[a.flex_1]}>
|
||||
{isLiked ? (
|
||||
<HeartFilled size="sm" fill={t.palette.like} />
|
||||
) : (
|
||||
<ButtonIcon icon={Heart} position="left" />
|
||||
)}
|
||||
|
||||
<ButtonText>
|
||||
{isLiked ? <Trans>Unlike</Trans> : <Trans>Like</Trans>}
|
||||
</ButtonText>
|
||||
</Button>
|
||||
<Button
|
||||
disabled={isFeedStateChangePending}
|
||||
label={isPinned ? _(msg`Unpin feed`) : _(msg`Pin feed`)}
|
||||
size="small"
|
||||
variant="solid"
|
||||
color={isPinned ? 'secondary' : 'primary'}
|
||||
onPress={onTogglePinned}
|
||||
style={[a.flex_1]}>
|
||||
<ButtonText>
|
||||
{isPinned ? <Trans>Unpin feed</Trans> : <Trans>Pin feed</Trans>}
|
||||
</ButtonText>
|
||||
<ButtonIcon icon={Pin} position="right" />
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
<View style={[a.pt_xs, a.gap_lg]}>
|
||||
<Divider />
|
||||
|
||||
<View
|
||||
style={[a.flex_row, a.align_center, a.gap_sm, a.justify_between]}>
|
||||
<Text style={[a.italic, t.atoms.text_contrast_medium]}>
|
||||
Something wrong? Let us know.
|
||||
</Text>
|
||||
|
||||
<Button
|
||||
label={_(msg`Report feed`)}
|
||||
size="small"
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
onPress={onPressReport}>
|
||||
<ButtonText>
|
||||
<Trans>Report feed</Trans>
|
||||
</ButtonText>
|
||||
<ButtonIcon icon={CircleInfo} position="right" />
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
<ReportDialog
|
||||
control={reportDialogControl}
|
||||
params={{
|
||||
type: 'feedgen',
|
||||
uri: info.uri,
|
||||
cid: info.cid,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -18,7 +18,13 @@ type Props = NativeStackScreenProps<AllNavigatorParams, 'NotificationSettings'>
|
||||
export function NotificationSettingsScreen({}: Props) {
|
||||
const {_} = useLingui()
|
||||
|
||||
const {data, isError: isQueryError, refetch} = useNotificationFeedQuery()
|
||||
const {
|
||||
data,
|
||||
isError: isQueryError,
|
||||
refetch,
|
||||
} = useNotificationFeedQuery({
|
||||
filter: 'all',
|
||||
})
|
||||
const serverPriority = data?.pages.at(0)?.priority
|
||||
|
||||
const {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user