Compare commits

...

32 Commits

Author SHA1 Message Date
Dan Abramov dfd038c3ba Fix double border due to progress guide 2024-12-12 20:08:44 +00:00
Eric Bailey 2808f8b73d New profile feed header (#7056)
* Init hacking

* Lil baby button checkpoint

* Playing around

* Revert "Playing around"

This reverts commit f58a7fafa1.

* Mostly there

* Cleanups

* Cleanup

* Fix report dialog nesting

* Remove transform on native

* Rename header

* Fix layout, overflowing FAB buttons

* Remove hack

* Couple of fixes

* Keep Pin primary CTA (#7061)

* Update src/screens/Profile/components/ProfileFeedHeader.tsx

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Simplify, use old string

* Wrap Trans better

---------

Co-authored-by: dan <dan.abramov@gmail.com>
Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
2024-12-12 11:48:26 -06:00
Samuel Newman ffc63dc85f [Layout] Bleed profile banner into safe area (#6967)
* bleed profile banner into safe area

(cherry picked from commit 50b3a4d0c6fd94b583ffe4efa65de35c81ae7f4e)

* pointer events none when hidden

(cherry picked from commit bae2c7b2dd6d7f858a98812196628308c0877755)

* fix web

(cherry picked from commit e3f9597170375f2903b6e567b963f008ec95aed1)

* add status bar shadow

* rm log

* rm mini header

* speed up animation

* pass bool rather than int in light status bar
2024-12-12 17:46:19 +00:00
Samuel Newman 4b32b0a71b Increase scrubber height if touch device (#7081)
* further increase target area of scrubber

* make scrubber bigger if touch device

---------

Co-authored-by: Antonio Sarcevic <sarcevicantonio@gmail.com>
2024-12-12 17:42:36 +00:00
dan f8cdd6b9ae [Notifications] Add a Mentions tab (#7044)
* Split out NotificationsTab

* Remove unused route parameter

* Refine the split between components

* Hoist some logic out of NotificationFeed

* Remove unused option

* Add all|conversations to query, hardcode "all"

* Add a Conversations tab

* Rename to Mentions

* Bump packages

* Rename fields

* Fix oopsie

* Simplify header

* Track active tab

* Fix types

* Separate logic for tabs

* Better border for first unread

* Highlight unread for all only

* Fix spinner races

* Fix fetchPage races

* Fix bottom bar border being obscured by glimmer

* Remember last tab within the session

* One tab at a time

* Fix TS

* Handle all RQKEY usages

* Nit
2024-12-12 17:37:07 +00:00
Samuel Newman 10e241e7eb followersCount -> followsCount (#7080) 2024-12-12 17:21:28 +00:00
dan 88166926fa Make the pager take full width (#7066)
* Wide tabs for web

* Wide tabs on mobile

* Tweak min for profile

* Driveby border fix

* Fix single tab indicator
2024-12-12 15:35:56 +00:00
dan 704e36c280 Fix web build (#7069)
* Fix web build

* Fix circular deps

* Hoist log functions
2024-12-12 05:00:09 +00:00
Snow Pettersen 20ac42a5a3 configure the bitdrift Capture SDK (#6979)
* configure bitdrift Capture SDK

* remove console log

* use the new url

* add patch to fix devtool interaction

* update patches

* fix patch

* add BITDRIFT_API_KEY to .env.example
2024-12-12 04:28:15 +00:00
dan c8745a9ab0 Cache Reanimated worklets (#7068)
* Undo perf hackfix

* Bump Reanimated to include https://github.com/software-mansion/react-native-reanimated/pull/6758

* Bump to 3.17.0-nightly-20241211-17e89ca24
2024-12-12 04:20:22 +00:00
Minseo Lee 084905c146 refine plural marks (#7065) 2024-12-12 03:48:02 +00:00
Frudrax Cheng ddeadc21a0 Update PostRepostedBy.tsx (#7064) 2024-12-12 01:56:43 +00:00
Samuel Newman e49dad2889 Nicer app icon screen (#6972)
* wip exploration

* list format instead of grid

* fix normalising default name

* adjust margins

* Rework the app icon link

* Decrease app icon size

* Adjust some spacing

* Move some things around to fix web errors

* Fix pathname

---------

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

* rm unnecessary layout.center

* do post quotes/reposts

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

* exact count for following

* move files to new dir

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

* maybe fix tests?

* Revert "maybe fix tests?"

This reverts commit f1b448e5b8.

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

* finishing up the implementation

* fix tailwind color selector

* tweak design

* refactor: unify types

* fix

* fix english grammar

* refactor: unify types

* tweak design

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

* Reconvert with missing files

* Update bskyweb/templates/base.html

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

* Update bskyweb/templates/base.html

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

* Run web-build to update hashes

* Revert Android to otf

* Use Inter woff2 files

---------

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

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

(cherry picked from commit 96bb02acfd2d7452df18a0e7410e6a7169a583ed)

* Better gutter handling

* Clean up styles

* Memoize breakpoints

* Format

* Comment

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

* Fix circular imports on native

* Return 0 instead of undefined for easier calculations

* Re-assign

* Fix

* Port over fix from subs/base

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

* Fix lost padding on home header

* Fix perf by not actually linking to new URL

* Remove underline on focus

* Foramt

---------

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

* Remove existing setDrawerSwipeDisabled management

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

* Move setDrawerSwipeDisabled logic into Pager

* Remove win/2 threshold

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

* Maybe work around iOS freeze

* Tweak gestures, add comments

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

* Update snaps
2024-12-09 13:43:16 -06:00
Eric Bailey 63c0c7e621 Add back button to desktop threads (#7010) 2024-12-08 14:44:01 -06:00
159 changed files with 3939 additions and 2039 deletions
+1
View File
@@ -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
View File
@@ -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

+3 -1
View File
@@ -20,6 +20,7 @@ window.addEventListener('message', event => {
return
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const id = (event.data as {id: string}).id
if (!id) {
return
@@ -33,6 +34,7 @@ window.addEventListener('message', event => {
return
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const height = (event.data as {height: number}).height
if (height) {
embed.style.height = `${height}px`
@@ -47,7 +49,7 @@ window.addEventListener('message', event => {
* @returns
*/
function scan(node = document) {
const embeds = node.querySelectorAll('[data-bluesky-uri]')
const embeds = node.querySelectorAll<HTMLIFrameElement>('[data-bluesky-uri]')
for (let i = 0; i < embeds.length; i++) {
const id = String(Math.random()).slice(2)
+17
View File
@@ -0,0 +1,17 @@
export function applyTheme(theme: 'light' | 'dark') {
document.documentElement.classList.remove('light', 'dark')
document.documentElement.classList.add(theme)
}
export function initColorMode() {
applyTheme(
window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light',
)
window
.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', mql => {
applyTheme(mql.matches ? 'dark' : 'light')
})
}
+1 -1
View File
@@ -37,7 +37,7 @@ export function Container({
return (
<div
ref={ref}
className="w-full bg-white hover:bg-neutral-50 relative transition-colors max-w-[600px] min-w-[300px] flex border rounded-xl"
className="w-full bg-white text-black hover:bg-neutral-50 dark:bg-dimmedBg dark:hover:bg-dimmedBgLighten relative transition-colors max-w-[600px] min-w-[300px] flex border dark:border-slate-600 dark:text-slate-200 rounded-xl"
onClick={() => {
if (ref.current && href) {
// forwardRef requires preact/compat - let's keep it simple
+20 -14
View File
@@ -78,9 +78,9 @@ export function Embed({
return (
<Link
href={`/profile/${record.author.did}/post/${getRkey(record)}`}
className="transition-colors hover:bg-neutral-100 border rounded-lg p-2 gap-1.5 w-full flex flex-col">
className="transition-colors hover:bg-neutral-100 dark:hover:bg-slate-700 border dark:border-slate-600 rounded-lg p-2 gap-1.5 w-full flex flex-col">
<div className="flex gap-1.5 items-center">
<div className="w-4 h-4 overflow-hidden rounded-full bg-neutral-300 shrink-0">
<div className="w-4 h-4 overflow-hidden rounded-full bg-neutral-300 dark:bg-slate-700 shrink-0">
<img
src={record.author.avatar}
style={isAuthorLabeled ? {filter: 'blur(1.5px)'} : undefined}
@@ -88,7 +88,7 @@ export function Embed({
</div>
<p className="line-clamp-1 text-sm">
<span className="font-bold">{record.author.displayName}</span>
<span className="text-textLight ml-1">
<span className="text-textLight dark:text-textDimmed ml-1">
@{record.author.handle}
</span>
</p>
@@ -209,7 +209,7 @@ function Info({children}: {children: ComponentChildren}) {
return (
<div className="w-full rounded-lg border py-2 px-2.5 flex-row flex gap-2 bg-neutral-50">
<img src={infoIcon} className="w-4 h-4 shrink-0 mt-0.5" />
<p className="text-sm text-textLight">{children}</p>
<p className="text-sm text-textLight dark:text-textDimmed">{children}</p>
</div>
)
}
@@ -308,7 +308,7 @@ function ExternalEmbed({
return (
<Link
href={content.external.uri}
className="w-full rounded-lg overflow-hidden border flex flex-col items-stretch"
className="w-full rounded-lg overflow-hidden border dark:border-slate-600 flex flex-col items-stretch"
disableTracking>
{content.external.thumb && (
<img
@@ -317,11 +317,11 @@ function ExternalEmbed({
/>
)}
<div className="py-3 px-4">
<p className="text-sm text-textLight line-clamp-1">
<p className="text-sm text-textLight dark:text-textDimmed line-clamp-1">
{toNiceDomain(content.external.uri)}
</p>
<p className="font-semibold line-clamp-3">{content.external.title}</p>
<p className="text-sm text-textLight line-clamp-2 mt-0.5">
<p className="text-sm text-textLight dark:text-textDimmed line-clamp-2 mt-0.5">
{content.external.description}
</p>
</div>
@@ -345,23 +345,29 @@ function GenericWithImageEmbed({
return (
<Link
href={href}
className="w-full rounded-lg border py-2 px-3 flex flex-col gap-2">
className="w-full rounded-lg border dark:border-slate-600 py-2 px-3 flex flex-col gap-2">
<div className="flex gap-2.5 items-center">
{image ? (
<img
src={image}
alt={title}
className="w-8 h-8 rounded-md bg-neutral-300 shrink-0"
className="w-8 h-8 rounded-md bg-neutral-300 dark:bg-slate-700 shrink-0"
/>
) : (
<div className="w-8 h-8 rounded-md bg-brand shrink-0" />
)}
<div className="flex-1">
<p className="font-bold text-sm">{title}</p>
<p className="text-textLight text-sm">{subtitle}</p>
<p className="text-textLight dark:text-textDimmed text-sm">
{subtitle}
</p>
</div>
</div>
{description && <p className="text-textLight text-sm">{description}</p>}
{description && (
<p className="text-textLight dark:text-textDimmed text-sm">
{description}
</p>
)}
</Link>
)
}
@@ -406,7 +412,7 @@ function StarterPackEmbed({
return (
<Link
href={starterPackHref}
className="w-full rounded-lg overflow-hidden border flex flex-col items-stretch">
className="w-full rounded-lg overflow-hidden border dark:border-slate-600 flex flex-col items-stretch">
<img src={imageUri} className="aspect-[1.91/1] object-cover" />
<div className="py-3 px-4">
<div className="flex space-x-2 items-center">
@@ -415,7 +421,7 @@ function StarterPackEmbed({
<p className="font-semibold leading-[21px]">
{content.record.name}
</p>
<p className="text-sm text-textLight line-clamp-2 leading-[18px]">
<p className="text-sm text-textLight dark:text-textDimmed line-clamp-2 leading-[18px]">
Starter pack by{' '}
{content.creator.displayName || `@${content.creator.handle}`}
</p>
@@ -425,7 +431,7 @@ function StarterPackEmbed({
<p className="text-sm mt-1">{content.record.description}</p>
)}
{!!content.joinedAllTimeCount && content.joinedAllTimeCount > 50 && (
<p className="text-sm font-semibold text-textLight mt-1">
<p className="text-sm font-semibold text-textLight dark:text-textDimmed mt-1">
{content.joinedAllTimeCount} users have joined!
</p>
)}
+10 -8
View File
@@ -38,7 +38,7 @@ export function Post({thread}: Props) {
<div className="flex-1 flex-col flex gap-2" lang={record?.langs?.[0]}>
<div className="flex gap-2.5 items-center cursor-pointer">
<Link href={`/profile/${post.author.did}`} className="rounded-full">
<div className="w-10 h-10 overflow-hidden rounded-full bg-neutral-300 shrink-0">
<div className="w-10 h-10 overflow-hidden rounded-full bg-neutral-300 dark:bg-slate-700 shrink-0">
<img
src={post.author.avatar}
style={isAuthorLabeled ? {filter: 'blur(2.5px)'} : undefined}
@@ -53,7 +53,7 @@ export function Post({thread}: Props) {
</Link>
<Link
href={`/profile/${post.author.did}`}
className="text-[15px] text-textLight hover:underline line-clamp-1">
className="text-[15px] text-textLight dark:text-textDimmed hover:underline line-clamp-1">
<p>@{post.author.handle}</p>
</Link>
</div>
@@ -69,15 +69,15 @@ export function Post({thread}: Props) {
<Link href={href}>
<time
datetime={new Date(post.indexedAt).toISOString()}
className="text-textLight mt-1 text-sm hover:underline">
className="text-textLight dark:text-textDimmed mt-1 text-sm hover:underline">
{niceDate(post.indexedAt)}
</time>
</Link>
<div className="border-t w-full pt-2.5 flex items-center gap-5 text-sm cursor-pointer">
<div className="border-t dark:border-slate-600 w-full pt-2.5 flex items-center gap-5 text-sm cursor-pointer">
{!!post.likeCount && (
<div className="flex items-center gap-2 cursor-pointer">
<img src={likeIcon} className="w-5 h-5" />
<p className="font-bold text-neutral-500 mb-px">
<p className="font-bold text-neutral-500 dark:text-neutral-300 mb-px">
{prettyNumber(post.likeCount)}
</p>
</div>
@@ -85,17 +85,19 @@ export function Post({thread}: Props) {
{!!post.repostCount && (
<div className="flex items-center gap-2 cursor-pointer">
<img src={repostIcon} className="w-5 h-5" />
<p className="font-bold text-neutral-500 mb-px">
<p className="font-bold text-neutral-500 dark:text-neutral-300 mb-px">
{prettyNumber(post.repostCount)}
</p>
</div>
)}
<div className="flex items-center gap-2 cursor-pointer">
<img src={replyIcon} className="w-5 h-5" />
<p className="font-bold text-neutral-500 mb-px">Reply</p>
<p className="font-bold text-neutral-500 dark:text-neutral-300 mb-px">
Reply
</p>
</div>
<div className="flex-1" />
<p className="cursor-pointer text-brand font-bold hover:underline hidden min-[450px]:inline">
<p className="cursor-pointer text-brand dark:text-brandLighten font-bold hover:underline hidden min-[450px]:inline">
{post.replyCount
? `Read ${prettyNumber(post.replyCount)} ${
post.replyCount > 1 ? 'replies' : 'reply'
+4
View File
@@ -5,3 +5,7 @@
.break-word {
word-break: break-word;
}
:root {
color-scheme: light dark;
}
+18 -13
View File
@@ -6,6 +6,7 @@ import {useEffect, useMemo, useRef, useState} from 'preact/hooks'
import arrowBottom from '../../assets/arrowBottom_stroke2_corner0_rounded.svg'
import logo from '../../assets/logo.svg'
import {initColorMode} from '../color-mode'
import {Container} from '../components/container'
import {Link} from '../components/link'
import {Post} from '../components/post'
@@ -21,6 +22,8 @@ export const EMBED_SCRIPT = `${EMBED_SERVICE}/static/embed.js`
const root = document.getElementById('app')
if (!root) throw new Error('No root element')
initColorMode()
const agent = new BskyAgent({
service: 'https://public.api.bsky.app',
})
@@ -108,7 +111,7 @@ function LandingPage() {
}, [uri])
return (
<main className="w-full min-h-screen flex flex-col items-center gap-8 py-14 px-4 md:pt-32">
<main className="w-full min-h-screen flex flex-col items-center gap-8 py-14 px-4 md:pt-32 dark:bg-dimmedBgDarken dark:text-slate-200">
<Link
href="https://bsky.social/about"
className="transition-transform hover:scale-110">
@@ -121,20 +124,22 @@ function LandingPage() {
type="text"
value={uri}
onInput={e => setUri(e.currentTarget.value)}
className="border rounded-lg py-3 w-full max-w-[600px] px-4"
className="border rounded-lg py-3 w-full max-w-[600px] px-4 dark:bg-dimmedBg dark:border-slate-500"
placeholder={DEFAULT_POST}
/>
<img src={arrowBottom} className="w-6" />
<img src={arrowBottom} className="w-6 dark:invert" />
{loading ? (
<Skeleton />
<div className="w-full max-w-[600px]">
<Skeleton />
</div>
) : (
<div className="w-full max-w-[600px] gap-8 flex flex-col">
{!error && thread && uri && <Snippet thread={thread} />}
{!error && thread && <Post thread={thread} key={thread.post.uri} />}
{error && (
<div className="w-full border border-red-500 bg-red-50 px-4 py-3 rounded-lg">
<div className="w-full border border-red-500 bg-red-500/10 px-4 py-3 rounded-lg">
<p className="text-red-500 text-center">{error}</p>
</div>
)}
@@ -149,15 +154,15 @@ function Skeleton() {
<Container>
<div className="flex-1 flex-col flex gap-2 pb-8">
<div className="flex gap-2.5 items-center">
<div className="w-10 h-10 overflow-hidden rounded-full bg-neutral-100 shrink-0 animate-pulse" />
<div className="w-10 h-10 overflow-hidden rounded-full bg-neutral-100 dark:bg-slate-700 shrink-0 animate-pulse" />
<div className="flex-1">
<div className="bg-neutral-100 animate-pulse w-64 h-4 rounded" />
<div className="bg-neutral-100 animate-pulse w-32 h-3 mt-1 rounded" />
<div className="bg-neutral-100 dark:bg-slate-700 animate-pulse w-64 h-4 rounded" />
<div className="bg-neutral-100 dark:bg-slate-700 animate-pulse w-32 h-3 mt-1 rounded" />
</div>
</div>
<div className="w-full h-4 mt-2 bg-neutral-100 rounded animate-pulse" />
<div className="w-5/6 h-4 bg-neutral-100 rounded animate-pulse" />
<div className="w-3/4 h-4 bg-neutral-100 rounded animate-pulse" />
<div className="w-full h-4 mt-2 bg-neutral-100 dark:bg-slate-700 rounded animate-pulse" />
<div className="w-5/6 h-4 bg-neutral-100 dark:bg-slate-700 rounded animate-pulse" />
<div className="w-3/4 h-4 bg-neutral-100 dark:bg-slate-700 rounded animate-pulse" />
</div>
</Container>
)
@@ -220,7 +225,7 @@ function Snippet({thread}: {thread: AppBskyFeedDefs.ThreadViewPost}) {
ref={ref}
type="text"
value={snippet}
className="border rounded-lg py-3 w-full px-4"
className="border rounded-lg py-3 w-full px-4 dark:bg-dimmedBg dark:border-slate-500"
readOnly
autoFocus
onFocus={() => {
@@ -228,7 +233,7 @@ function Snippet({thread}: {thread: AppBskyFeedDefs.ThreadViewPost}) {
}}
/>
<button
className="rounded-lg bg-brand text-white color-white py-3 px-4 whitespace-nowrap min-w-28"
className="rounded-lg bg-brand text-white py-3 px-4 whitespace-nowrap min-w-28"
onClick={() => {
ref.current?.focus()
ref.current?.select()
+6 -3
View File
@@ -4,6 +4,7 @@ import {AppBskyFeedDefs, AtpAgent} from '@atproto/api'
import {h, render} from 'preact'
import logo from '../../assets/logo.svg'
import {initColorMode} from '../color-mode'
import {Container} from '../components/container'
import {Link} from '../components/link'
import {Post} from '../components/post'
@@ -21,6 +22,8 @@ if (!uri) {
throw new Error('No uri in path')
}
initColorMode()
agent
.getPostThread({
uri,
@@ -55,13 +58,13 @@ function PwiOptOut({thread}: {thread: AppBskyFeedDefs.ThreadViewPost}) {
<img src={logo} className="h-6" />
</Link>
<div className="w-full py-12 gap-4 flex flex-col items-center">
<p className="max-w-80 text-center w-full text-textLight">
<p className="max-w-80 text-center w-full text-textLight dark:text-textDimmed">
The author of this post has requested their posts not be displayed on
external sites.
</p>
<Link
href={href}
className="max-w-80 rounded-lg bg-brand text-white color-white text-center py-1 px-4 w-full mx-auto">
className="max-w-80 rounded-lg bg-brand text-white text-center py-1 px-4 w-full mx-auto">
View on Bluesky
</Link>
</div>
@@ -77,7 +80,7 @@ function ErrorMessage() {
className="transition-transform hover:scale-110 absolute top-4 right-4">
<img src={logo} className="h-6" />
</Link>
<p className="my-16 text-center w-full text-textLight">
<p className="my-16 text-center w-full text-textLight dark:text-textDimmed">
Post not found, it may have been deleted.
</p>
</Container>
+8
View File
@@ -1,11 +1,19 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
darkMode: ['variant', [
'&:is(.dark *):not(:is(.dark .light *))',
]],
theme: {
extend: {
colors: {
brand: 'rgb(10,122,255)',
brandLighten: 'rgb(32,139,254)',
textLight: 'rgb(66,87,108)',
textDimmed: 'rgb(174,187,201)',
dimmedBgLighten: 'rgb(30,41,54)',
dimmedBg: 'rgb(22,30,39)',
dimmedBgDarken: 'rgb(18,25,32)',
},
},
},
+1 -2
View File
@@ -1,4 +1,3 @@
{
"compilerOptions": {
"target": "ES5",
@@ -20,5 +19,5 @@
"jsxFragmentFactory": "Fragment",
"downlevelIteration": true
},
"include": ["src", "vite.config.ts"]
"include": ["src", "snippet", "vite.config.ts"]
}
+3 -4
View File
@@ -13,8 +13,7 @@
<!-- Hello Humans! API docs at https://atproto.com -->
<link rel="preload" as="font" type="font/ttf" href="{{ staticCDNHost }}/static/media/InterVariable.c9f788f6e7ebaec75d7c.ttf">
<link rel="preload" as="font" type="font/ttf" href="{{ staticCDNHost }}/static/media/InterVariable-Italic.55d6a3f35e9b605ba6f4.ttf">
<link rel="preload" as="font" type="font/ttf" href="{{ staticCDNHost }}/static/media/InterVariable.c504db5c06caaf7cdfba.woff2">
<style>
/**
@@ -26,14 +25,14 @@
*/
@font-face {
font-family: 'InterVariable';
src: url("{{ staticCDNHost }}/static/media/InterVariable.c9f788f6e7ebaec75d7c.ttf") format('truetype');
src: url("{{ staticCDNHost }}/static/media/InterVariable.c504db5c06caaf7cdfba.woff2") format('woff2');
font-weight: 300 1000;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'InterVariableItalic';
src: url("{{ staticCDNHost }}/static/media/InterVariable-Italic.55d6a3f35e9b605ba6f4.ttf") format('truetype');
src: url("{{ staticCDNHost }}/static/media/InterVariable-Italic.01dcbad1bac635f9c9cd.woff2") format('woff2');
font-weight: 300 1000;
font-style: italic;
font-display: swap;
+2 -2
View File
@@ -12,14 +12,14 @@
{# don't include the bundle on non-404 error pages #}
{% block head_bundle %}
{% if statusCode == 404 %}
{{ super() }}
{{ block.Super }}
{% else %}
{% endif %}
{% endblock %}
{%- block body_all %}
{% if statusCode == 404 %}
{{ super() }}
{{ block.Super }}
{% else %}
<h1>{{ statusCode }}: Server Error</h1>
<p>Sorry about that! Our <a href="https://bluesky.statuspage.io/">Status Page</a> might have more context.
+6
View File
@@ -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
+2
View File
@@ -14,6 +14,8 @@ if (process.env.BSKY_PROFILE) {
cfg.cacheVersion += ':PROFILE'
}
cfg.resolver.assetExts = [...cfg.resolver.assetExts, 'woff2']
cfg.resolver.resolveRequest = (context, moduleName, platform) => {
// HACK: manually resolve a few packages that use `exports` in `package.json`.
// A proper solution is to enable `unstable_enablePackageExports` but this needs careful testing.
+7 -6
View File
@@ -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",
+3
View File
@@ -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
+15
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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'
+28
View File
@@ -0,0 +1,28 @@
import {useMemo} from 'react'
import {useMediaQuery} from 'react-responsive'
export type Breakpoint = 'gtPhone' | 'gtMobile' | 'gtTablet'
export function useBreakpoints(): Record<Breakpoint, boolean> & {
activeBreakpoint: Breakpoint | undefined
} {
const gtPhone = useMediaQuery({minWidth: 500})
const gtMobile = useMediaQuery({minWidth: 800})
const gtTablet = useMediaQuery({minWidth: 1300})
return useMemo(() => {
let active: Breakpoint | undefined
if (gtTablet) {
active = 'gtTablet'
} else if (gtMobile) {
active = 'gtMobile'
} else if (gtPhone) {
active = 'gtPhone'
}
return {
activeBreakpoint: active,
gtPhone,
gtMobile,
gtTablet,
}
}, [gtPhone, gtMobile, gtTablet])
}
+2 -13
View File
@@ -1,5 +1,4 @@
import React from 'react'
import {useMediaQuery} from 'react-responsive'
import {
computeFontScaleMultiplier,
@@ -14,13 +13,14 @@ import {BLUE_HUE, GREEN_HUE, RED_HUE} from '#/alf/util/colorGeneration'
import {Device} from '#/storage'
export {atoms} from '#/alf/atoms'
export * from '#/alf/breakpoints'
export * from '#/alf/fonts'
export * as tokens from '#/alf/tokens'
export * from '#/alf/types'
export * from '#/alf/util/flatten'
export * from '#/alf/util/platform'
export * from '#/alf/util/themeSelector'
export * from '#/alf/util/useGutterStyles'
export * from '#/alf/util/useGutters'
export type Alf = {
themeName: ThemeName
@@ -142,14 +142,3 @@ export function useTheme(theme?: ThemeName) {
return theme ? alf.themes[theme] : alf.theme
}, [theme, alf])
}
export function useBreakpoints() {
const gtPhone = useMediaQuery({minWidth: 500})
const gtMobile = useMediaQuery({minWidth: 800})
const gtTablet = useMediaQuery({minWidth: 1300})
return {
gtPhone,
gtMobile,
gtTablet,
}
}
+4
View File
@@ -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]}%)`,
+1
View File
@@ -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
+2 -2
View File
@@ -9,7 +9,7 @@ import {useFonts} from 'expo-font'
*/
export function DO_NOT_USE() {
return useFonts({
InterVariable: require('../../../assets/fonts/inter/InterVariable.ttf'),
'InterVariable-Italic': require('../../../assets/fonts/inter/InterVariable-Italic.ttf'),
InterVariable: require('../../../assets/fonts/inter/InterVariable.woff2'),
'InterVariable-Italic': require('../../../assets/fonts/inter/InterVariable-Italic.woff2'),
})
}
-21
View File
@@ -1,21 +0,0 @@
import React from 'react'
import {atoms as a, useBreakpoints, ViewStyleProp} from '#/alf'
export function useGutterStyles({
top,
bottom,
}: {
top?: boolean
bottom?: boolean
} = {}) {
const {gtMobile} = useBreakpoints()
return React.useMemo<ViewStyleProp['style']>(() => {
return [
a.px_lg,
top && a.pt_md,
bottom && a.pb_md,
gtMobile && [a.px_xl, top && a.pt_lg, bottom && a.pb_lg],
]
}, [gtMobile, top, bottom])
}
+66
View File
@@ -0,0 +1,66 @@
import React from 'react'
import {Breakpoint, useBreakpoints} from '#/alf/breakpoints'
import * as tokens from '#/alf/tokens'
type Gutter = 'compact' | 'base' | 'wide' | 0
const gutters: Record<
Exclude<Gutter, 0>,
Record<Breakpoint | 'default', number>
> = {
compact: {
default: tokens.space.sm,
gtPhone: tokens.space.sm,
gtMobile: tokens.space.md,
gtTablet: tokens.space.md,
},
base: {
default: tokens.space.lg,
gtPhone: tokens.space.lg,
gtMobile: tokens.space.xl,
gtTablet: tokens.space.xl,
},
wide: {
default: tokens.space.xl,
gtPhone: tokens.space.xl,
gtMobile: tokens.space._3xl,
gtTablet: tokens.space._3xl,
},
}
type Gutters = {
paddingTop: number
paddingRight: number
paddingBottom: number
paddingLeft: number
}
export function useGutters([all]: [Gutter]): Gutters
export function useGutters([vertical, horizontal]: [Gutter, Gutter]): Gutters
export function useGutters([top, right, bottom, left]: [
Gutter,
Gutter,
Gutter,
Gutter,
]): Gutters
export function useGutters([top, right, bottom, left]: Gutter[]) {
const {activeBreakpoint} = useBreakpoints()
if (right === undefined) {
right = bottom = left = top
} else if (bottom === undefined) {
bottom = top
left = right
}
return React.useMemo(() => {
return {
paddingTop: top === 0 ? 0 : gutters[top][activeBreakpoint || 'default'],
paddingRight:
right === 0 ? 0 : gutters[right][activeBreakpoint || 'default'],
paddingBottom:
bottom === 0 ? 0 : gutters[bottom][activeBreakpoint || 'default'],
paddingLeft:
left === 0 ? 0 : gutters[left][activeBreakpoint || 'default'],
}
}, [activeBreakpoint, top, right, bottom, left])
}
+4 -5
View File
@@ -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>
)
}
+3 -19
View File
@@ -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
)
}
+5 -3
View File
@@ -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>
)
+7 -6
View File
@@ -13,7 +13,7 @@ import {
platform,
TextStyleProp,
useBreakpoints,
useGutterStyles,
useGutters,
useTheme,
} from '#/alf'
import {Button, ButtonIcon, ButtonProps} from '#/components/Button'
@@ -34,7 +34,7 @@ export function Outer({
noBottomBorder?: boolean
}) {
const t = useTheme()
const gutter = useGutterStyles()
const gutters = useGutters([0, 'base'])
const {gtMobile} = useBreakpoints()
const {isWithinOffsetView} = useContext(ScrollbarOffsetContext)
@@ -46,10 +46,10 @@ export function Outer({
a.flex_row,
a.align_center,
a.gap_sm,
gutter,
gutters,
platform({
native: [a.pb_sm, a.pt_xs],
web: [a.py_sm],
native: [a.pb_xs, {minHeight: 48}],
web: [a.py_xs, {minHeight: 52}],
}),
t.atoms.border_contrast_low,
gtMobile && [a.mx_auto, {maxWidth: 600}],
@@ -175,7 +175,8 @@ export function TitleText({
gtMobile && a.text_xl,
style,
]}
numberOfLines={2}>
numberOfLines={2}
emoji>
{children}
</Text>
)
+3 -1
View File
@@ -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}
/>
</>
+4 -5
View File
@@ -237,7 +237,9 @@ export function Link({
}
export type InlineLinkProps = React.PropsWithChildren<
BaseLinkProps & TextStyleProp & Pick<TextProps, 'selectable'>
BaseLinkProps &
TextStyleProp &
Pick<TextProps, 'selectable' | 'numberOfLines'>
> &
Pick<ButtonProps, 'label'> & {
disableUnderline?: boolean
@@ -273,7 +275,6 @@ export function InlineLinkText({
onIn: onHoverIn,
onOut: onHoverOut,
} = useInteractionState()
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
const flattenedStyle = flatten(style) || {}
return (
@@ -284,7 +285,7 @@ export function InlineLinkText({
{...rest}
style={[
{color: t.palette.primary_500},
(hovered || focused) &&
hovered &&
!disableUnderline && {
...web({
outline: 0,
@@ -298,8 +299,6 @@ export function InlineLinkText({
role="link"
onPress={download ? undefined : onPress}
onLongPress={onLongPress}
onFocus={onFocus}
onBlur={onBlur}
onMouseEnter={onHoverIn}
onMouseLeave={onHoverOut}
accessibilityRole="link"
@@ -5,7 +5,7 @@ import {useLingui} from '@lingui/react'
import {isNative} from '#/platform/detection'
import {FeedDescriptor} from '#/state/queries/post-feed'
import {Feed} from '#/view/com/posts/Feed'
import {PostFeed} from '#/view/com/posts/PostFeed'
import {EmptyState} from '#/view/com/util/EmptyState'
import {ListRef} from '#/view/com/util/List'
import {SectionRef} from '#/screens/Profile/Sections/types'
@@ -38,7 +38,7 @@ export const PostsList = React.forwardRef<SectionRef, ProfilesListProps>(
return (
<View>
<Feed
<PostFeed
feed={feed}
pollInterval={60e3}
scrollElRef={scrollElRef}
@@ -15,7 +15,7 @@ import {sanitizeHandle} from '#/lib/strings/handles'
import {isWeb} from '#/platform/detection'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
import {useListConvosQuery} from '#/state/queries/messages/list-converations'
import {useListConvosQuery} from '#/state/queries/messages/list-conversations'
import {useProfileFollowsQuery} from '#/state/queries/profile-follows'
import {useSession} from '#/state/session'
import {ListMethods} from '#/view/com/util/List'
+4
View File
@@ -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',
})
+17 -11
View File
@@ -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>
+7
View File
@@ -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'})
}
+11 -4
View File
@@ -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()
}
})
+3 -3
View File
@@ -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
+23
View File
@@ -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())
}
}
+7
View File
@@ -0,0 +1,7 @@
import {Transport} from './index'
export function createBitdriftTransport(): Transport {
return (_level, _message) => {
// noop
}
}
+9 -67
View File
@@ -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)
+69
View File
@@ -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
}
+3 -5
View File
@@ -14,7 +14,7 @@ import {cleanError} from '#/lib/strings/errors'
import {sanitizeHandle} from '#/lib/strings/handles'
import {enforceLen} from '#/lib/strings/helpers'
import {useSearchPostsQuery} from '#/state/queries/search-posts'
import {useSetDrawerSwipeDisabled, useSetMinimalShellMode} from '#/state/shell'
import {useSetMinimalShellMode} from '#/state/shell'
import {Pager} from '#/view/com/pager/Pager'
import {TabBar} from '#/view/com/pager/TabBar'
import {Post} from '#/view/com/post/Post'
@@ -63,7 +63,6 @@ export default function HashtagScreen({
const [activeTab, setActiveTab] = React.useState(0)
const setMinimalShellMode = useSetMinimalShellMode()
const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
useFocusEffect(
React.useCallback(() => {
@@ -74,10 +73,9 @@ export default function HashtagScreen({
const onPageSelected = React.useCallback(
(index: number) => {
setMinimalShellMode(false)
setDrawerSwipeDisabled(index > 0)
setActiveTab(index)
},
[setDrawerSwipeDisabled, setMinimalShellMode],
[setMinimalShellMode],
)
const sections = React.useMemo(() => {
@@ -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>
+1 -1
View File
@@ -257,7 +257,7 @@ export const LoginForm = ({
autoCapitalize="none"
autoFocus
autoCorrect={false}
autoComplete="off"
autoComplete="one-time-code"
returnKeyType="done"
textContentType="username"
blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field
+1 -1
View File
@@ -14,7 +14,7 @@ import {logger} from '#/logger'
import {isNative} from '#/platform/detection'
import {MESSAGE_SCREEN_POLL_INTERVAL} from '#/state/messages/convo/const'
import {useMessagesEventBus} from '#/state/messages/events'
import {useListConvosQuery} from '#/state/queries/messages/list-converations'
import {useListConvosQuery} from '#/state/queries/messages/list-conversations'
import {List} from '#/view/com/util/List'
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
+24 -5
View File
@@ -1,13 +1,12 @@
import React from 'react'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {Plural, Trans} from '@lingui/macro'
import {useFocusEffect} from '@react-navigation/native'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {makeRecordUri} from '#/lib/strings/url-helpers'
import {usePostThreadQuery} from '#/state/queries/post-thread'
import {useSetMinimalShellMode} from '#/state/shell'
import {PostLikedBy as PostLikedByComponent} from '#/view/com/post-thread/PostLikedBy'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import * as Layout from '#/components/Layout'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostLikedBy'>
@@ -15,7 +14,12 @@ export const PostLikedByScreen = ({route}: Props) => {
const setMinimalShellMode = useSetMinimalShellMode()
const {name, rkey} = route.params
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
const {_} = useLingui()
const {data: post} = usePostThreadQuery(uri)
let likeCount
if (post?.thread.type === 'post') {
likeCount = post.thread.post.likeCount
}
useFocusEffect(
React.useCallback(() => {
@@ -25,7 +29,22 @@ export const PostLikedByScreen = ({route}: Props) => {
return (
<Layout.Screen>
<ViewHeader title={_(msg`Liked By`)} />
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
{post && (
<>
<Layout.Header.TitleText>
<Trans>Liked By</Trans>
</Layout.Header.TitleText>
<Layout.Header.SubtitleText>
<Plural value={likeCount ?? 0} one="# like" other="# likes" />
</Layout.Header.SubtitleText>
</>
)}
</Layout.Header.Content>
<Layout.Header.Slot />
</Layout.Header.Outer>
<PostLikedByComponent uri={uri} />
</Layout.Screen>
)
+29 -10
View File
@@ -1,15 +1,12 @@
import React from 'react'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {Plural, Trans} from '@lingui/macro'
import {useFocusEffect} from '@react-navigation/native'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {makeRecordUri} from '#/lib/strings/url-helpers'
import {isWeb} from '#/platform/detection'
import {usePostThreadQuery} from '#/state/queries/post-thread'
import {useSetMinimalShellMode} from '#/state/shell'
import {PostQuotes as PostQuotesComponent} from '#/view/com/post-thread/PostQuotes'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {CenteredView} from '#/view/com/util/Views'
import * as Layout from '#/components/Layout'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostQuotes'>
@@ -17,7 +14,12 @@ export const PostQuotesScreen = ({route}: Props) => {
const setMinimalShellMode = useSetMinimalShellMode()
const {name, rkey} = route.params
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
const {_} = useLingui()
const {data: post} = usePostThreadQuery(uri)
let quoteCount
if (post?.thread.type === 'post') {
quoteCount = post.thread.post.quoteCount
}
useFocusEffect(
React.useCallback(() => {
@@ -27,10 +29,27 @@ export const PostQuotesScreen = ({route}: Props) => {
return (
<Layout.Screen>
<CenteredView sideBorders={true}>
<ViewHeader title={_(msg`Quotes`)} showBorder={!isWeb} />
<PostQuotesComponent uri={uri} />
</CenteredView>
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
{post && (
<>
<Layout.Header.TitleText>
<Trans>Quotes</Trans>
</Layout.Header.TitleText>
<Layout.Header.SubtitleText>
<Plural
value={quoteCount ?? 0}
one="# quote"
other="# quotes"
/>
</Layout.Header.SubtitleText>
</>
)}
</Layout.Header.Content>
<Layout.Header.Slot />
</Layout.Header.Outer>
<PostQuotesComponent uri={uri} />
</Layout.Screen>
)
}
+29 -10
View File
@@ -1,15 +1,12 @@
import React from 'react'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {Plural, Trans} from '@lingui/macro'
import {useFocusEffect} from '@react-navigation/native'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {makeRecordUri} from '#/lib/strings/url-helpers'
import {isWeb} from '#/platform/detection'
import {usePostThreadQuery} from '#/state/queries/post-thread'
import {useSetMinimalShellMode} from '#/state/shell'
import {PostRepostedBy as PostRepostedByComponent} from '#/view/com/post-thread/PostRepostedBy'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {CenteredView} from '#/view/com/util/Views'
import * as Layout from '#/components/Layout'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostRepostedBy'>
@@ -17,7 +14,12 @@ export const PostRepostedByScreen = ({route}: Props) => {
const {name, rkey} = route.params
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
const setMinimalShellMode = useSetMinimalShellMode()
const {_} = useLingui()
const {data: post} = usePostThreadQuery(uri)
let quoteCount
if (post?.thread.type === 'post') {
quoteCount = post.thread.post.repostCount
}
useFocusEffect(
React.useCallback(() => {
@@ -27,10 +29,27 @@ export const PostRepostedByScreen = ({route}: Props) => {
return (
<Layout.Screen>
<CenteredView sideBorders={true}>
<ViewHeader title={_(msg`Reposted By`)} showBorder={!isWeb} />
<PostRepostedByComponent uri={uri} />
</CenteredView>
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
{post && (
<>
<Layout.Header.TitleText>
<Trans>Reposted By</Trans>
</Layout.Header.TitleText>
<Layout.Header.SubtitleText>
<Plural
value={quoteCount ?? 0}
one="# repost"
other="# reposts"
/>
</Layout.Header.SubtitleText>
</>
)}
</Layout.Header.Content>
<Layout.Header.Slot />
</Layout.Header.Outer>
<PostRepostedByComponent uri={uri} />
</Layout.Screen>
)
}
+12 -3
View File
@@ -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'}
+1 -1
View File
@@ -30,7 +30,7 @@ export function ProfileHeaderMetrics({
return (
<View
style={[a.flex_row, a.gap_sm, a.align_center, a.pb_md]}
style={[a.flex_row, a.gap_sm, a.align_center]}
pointerEvents="box-none">
<InlineLinkText
testID="profileHeaderFollowersButton"
@@ -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
+19 -12
View File
@@ -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
}
+112 -7
View File
@@ -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',
+227
View File
@@ -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,
},
})
+54
View File
@@ -0,0 +1,54 @@
import React from 'react'
import {Plural} from '@lingui/macro'
import {useFocusEffect} from '@react-navigation/native'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {useProfileQuery} from '#/state/queries/profile'
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
import {useSetMinimalShellMode} from '#/state/shell'
import {ProfileFollowers as ProfileFollowersComponent} from '#/view/com/profile/ProfileFollowers'
import * as Layout from '#/components/Layout'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFollowers'>
export const ProfileFollowersScreen = ({route}: Props) => {
const {name} = route.params
const setMinimalShellMode = useSetMinimalShellMode()
const {data: resolvedDid} = useResolveDidQuery(name)
const {data: profile} = useProfileQuery({
did: resolvedDid,
})
useFocusEffect(
React.useCallback(() => {
setMinimalShellMode(false)
}, [setMinimalShellMode]),
)
return (
<Layout.Screen testID="profileFollowersScreen">
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
{profile && (
<>
<Layout.Header.TitleText>
{sanitizeDisplayName(profile.displayName || profile.handle)}
</Layout.Header.TitleText>
<Layout.Header.SubtitleText>
<Plural
value={profile.followersCount ?? 0}
one="# follower"
other="# followers"
/>
</Layout.Header.SubtitleText>
</>
)}
</Layout.Header.Content>
<Layout.Header.Slot />
</Layout.Header.Outer>
<ProfileFollowersComponent name={name} />
</Layout.Screen>
)
}
+54
View File
@@ -0,0 +1,54 @@
import React from 'react'
import {Plural} from '@lingui/macro'
import {useFocusEffect} from '@react-navigation/native'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {useProfileQuery} from '#/state/queries/profile'
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
import {useSetMinimalShellMode} from '#/state/shell'
import {ProfileFollows as ProfileFollowsComponent} from '#/view/com/profile/ProfileFollows'
import * as Layout from '#/components/Layout'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFollows'>
export const ProfileFollowsScreen = ({route}: Props) => {
const {name} = route.params
const setMinimalShellMode = useSetMinimalShellMode()
const {data: resolvedDid} = useResolveDidQuery(name)
const {data: profile} = useProfileQuery({
did: resolvedDid,
})
useFocusEffect(
React.useCallback(() => {
setMinimalShellMode(false)
}, [setMinimalShellMode]),
)
return (
<Layout.Screen testID="profileFollowsScreen">
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
{profile && (
<>
<Layout.Header.TitleText>
{sanitizeDisplayName(profile.displayName || profile.handle)}
</Layout.Header.TitleText>
<Layout.Header.SubtitleText>
<Plural
value={profile.followsCount ?? 0}
one="# following"
other="# following"
/>
</Layout.Header.SubtitleText>
</>
)}
</Layout.Header.Content>
<Layout.Header.Slot />
</Layout.Header.Outer>
<ProfileFollowsComponent name={name} />
</Layout.Screen>
)
}
+2 -2
View File
@@ -10,7 +10,7 @@ import {isNative} from '#/platform/detection'
import {FeedDescriptor} from '#/state/queries/post-feed'
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
import {truncateAndInvalidate} from '#/state/queries/util'
import {Feed} from '#/view/com/posts/Feed'
import {PostFeed} from '#/view/com/posts/PostFeed'
import {EmptyState} from '#/view/com/util/EmptyState'
import {ListRef} from '#/view/com/util/List'
import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn'
@@ -74,7 +74,7 @@ export const ProfileFeedSection = React.forwardRef<
return (
<View>
<Feed
<PostFeed
testID="postsFeed"
enabled={isFocused}
feed={feed}
@@ -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>
)
}
-260
View File
@@ -1,260 +0,0 @@
import React from 'react'
import {Alert, View} from 'react-native'
import {Image} from 'expo-image'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import * as AppIcon from '@mozzius/expo-dynamic-app-icon'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {PressableScale} from '#/lib/custom-animations/PressableScale'
import {CommonNavigatorParams} from '#/lib/routes/types'
import {isAndroid} from '#/platform/detection'
import {atoms as a, platform} from '#/alf'
import * as Layout from '#/components/Layout'
import {Text} from '#/components/Typography'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppIconSettings'>
export function AppIconSettingsScreen({}: Props) {
const {_} = useLingui()
const sets = useAppIconSets()
return (
<Layout.Screen>
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
<Layout.Header.TitleText>
<Trans>App Icon</Trans>
</Layout.Header.TitleText>
</Layout.Header.Content>
<Layout.Header.Slot />
</Layout.Header.Outer>
<Layout.Content
contentContainerStyle={[a.py_2xl, a.px_xl, {paddingBottom: 100}]}>
<Text style={[a.text_lg, a.font_heavy]}>Defaults</Text>
<View style={[a.flex_row, a.flex_wrap]}>
{sets.defaults.map(icon => (
<View
style={[{width: '50%'}, a.py_lg, a.px_xs, a.align_center]}
key={icon.id}>
<PressableScale
accessibilityLabel={icon.name}
accessibilityHint={_(msg`Tap to change app icon`)}
targetScale={0.95}
onPress={() => AppIcon.setAppIcon(icon.id)}>
<Image
source={platform({
ios: icon.iosImage(),
android: icon.androidImage(),
})}
style={[
{width: 100, height: 100},
platform({
ios: {borderRadius: 20},
android: a.rounded_full,
}),
a.curve_continuous,
]}
accessibilityIgnoresInvertColors
/>
</PressableScale>
<Text style={[a.text_center, a.font_bold, a.text_md, a.mt_md]}>
{icon.name}
</Text>
</View>
))}
</View>
<Text style={[a.text_lg, a.font_heavy]}>Bluesky+</Text>
<View style={[a.flex_row, a.flex_wrap]}>
{sets.core.map(icon => (
<View
style={[{width: '50%'}, a.py_lg, a.px_xs, a.align_center]}
key={icon.id}>
<PressableScale
accessibilityLabel={icon.name}
accessibilityHint={_(msg`Tap to change app icon`)}
targetScale={0.95}
onPress={() => {
if (isAndroid) {
Alert.alert(
_(msg`Change app icon to "${icon.name}"`),
_(msg`The app will be restarted`),
[
{
text: _(msg`Cancel`),
style: 'cancel',
},
{
text: _(msg`OK`),
onPress: () => {
AppIcon.setAppIcon(icon.id)
},
style: 'default',
},
],
)
} else {
AppIcon.setAppIcon(icon.id)
}
}}>
<Image
source={platform({
ios: icon.iosImage(),
android: icon.androidImage(),
})}
style={[
{width: 100, height: 100},
platform({
ios: {borderRadius: 20},
android: a.rounded_full,
}),
a.curve_continuous,
a.shadow_lg,
]}
accessibilityIgnoresInvertColors
/>
</PressableScale>
<Text
style={[a.text_center, a.font_bold, a.text_md, a.mt_md]}
// for Classic™
emoji>
{icon.name}
</Text>
</View>
))}
</View>
</Layout.Content>
</Layout.Screen>
)
}
function useAppIconSets() {
const {_} = useLingui()
return React.useMemo(() => {
const defaults = [
{
id: 'default_light',
name: _('Light'),
iosImage: () => {
return require(`../../../assets/app-icons/ios_icon_default_light.png`)
},
androidImage: () => {
return require(`../../../assets/app-icons/android_icon_default_light.png`)
},
},
{
id: 'default_dark',
name: _('Dark'),
iosImage: () => {
return require(`../../../assets/app-icons/ios_icon_default_dark.png`)
},
androidImage: () => {
return require(`../../../assets/app-icons/android_icon_default_dark.png`)
},
},
]
/**
* Bluesky+
*/
const core = [
{
id: 'core_aurora',
name: _('Aurora'),
iosImage: () => {
return require(`../../../assets/app-icons/ios_icon_core_aurora.png`)
},
androidImage: () => {
return require(`../../../assets/app-icons/android_icon_core_aurora.png`)
},
},
// {
// id: 'core_bonfire',
// name: _('Bonfire'),
// iosImage: () => {
// return require(`../../../assets/app-icons/ios_icon_core_bonfire.png`)
// },
// androidImage: () => {
// return require(`../../../assets/app-icons/android_icon_core_bonfire.png`)
// },
// },
{
id: 'core_sunrise',
name: _('Sunrise'),
iosImage: () => {
return require(`../../../assets/app-icons/ios_icon_core_sunrise.png`)
},
androidImage: () => {
return require(`../../../assets/app-icons/android_icon_core_sunrise.png`)
},
},
{
id: 'core_sunset',
name: _('Sunset'),
iosImage: () => {
return require(`../../../assets/app-icons/ios_icon_core_sunset.png`)
},
androidImage: () => {
return require(`../../../assets/app-icons/android_icon_core_sunset.png`)
},
},
{
id: 'core_midnight',
name: _('Midnight'),
iosImage: () => {
return require(`../../../assets/app-icons/ios_icon_core_midnight.png`)
},
androidImage: () => {
return require(`../../../assets/app-icons/android_icon_core_midnight.png`)
},
},
{
id: 'core_flat_blue',
name: _('Flat Blue'),
iosImage: () => {
return require(`../../../assets/app-icons/ios_icon_core_flat_blue.png`)
},
androidImage: () => {
return require(`../../../assets/app-icons/android_icon_core_flat_blue.png`)
},
},
{
id: 'core_flat_white',
name: _('Flat White'),
iosImage: () => {
return require(`../../../assets/app-icons/ios_icon_core_flat_white.png`)
},
androidImage: () => {
return require(`../../../assets/app-icons/android_icon_core_flat_white.png`)
},
},
{
id: 'core_flat_black',
name: _('Flat Black'),
iosImage: () => {
return require(`../../../assets/app-icons/ios_icon_core_flat_black.png`)
},
androidImage: () => {
return require(`../../../assets/app-icons/android_icon_core_flat_black.png`)
},
},
{
id: 'core_classic',
name: _('Bluesky Classic™'),
iosImage: () => {
return require(`../../../assets/app-icons/ios_icon_core_classic.png`)
},
androidImage: () => {
return require(`../../../assets/app-icons/android_icon_core_classic.png`)
},
},
]
return {
defaults,
core,
}
}, [_])
}
@@ -0,0 +1,33 @@
import {Image} from 'expo-image'
import {AppIconSet} from '#/screens/Settings/AppIconSettings/types'
import {atoms as a, platform, useTheme} from '#/alf'
export function AppIconImage({
icon,
size = 50,
}: {
icon: AppIconSet
size: number
}) {
const t = useTheme()
return (
<Image
source={platform({
ios: icon.iosImage(),
android: icon.androidImage(),
})}
style={[
{width: size, height: size},
platform({
ios: {borderRadius: size / 5},
android: a.rounded_full,
}),
a.curve_continuous,
t.atoms.border_contrast_medium,
a.border,
]}
accessibilityIgnoresInvertColors
/>
)
}
@@ -0,0 +1,29 @@
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {AppIconImage} from '#/screens/Settings/AppIconSettings/AppIconImage'
import {useCurrentAppIcon} from '#/screens/Settings/AppIconSettings/useCurrentAppIcon'
import * as SettingsList from '#/screens/Settings/components/SettingsList'
import {atoms as a} from '#/alf'
import {Shapes_Stroke2_Corner0_Rounded as Shapes} from '#/components/icons/Shapes'
export function SettingsListItem() {
const {_} = useLingui()
const icon = useCurrentAppIcon()
return (
<SettingsList.LinkItem
to="/settings/app-icon"
label={_(msg`App Icon`)}
contentContainerStyle={[a.align_start]}>
<SettingsList.ItemIcon icon={Shapes} />
<View style={[a.flex_1]}>
<SettingsList.ItemText style={[a.pt_xs, a.pb_md]}>
<Trans>App Icon</Trans>
</SettingsList.ItemText>
<AppIconImage icon={icon} size={60} />
</View>
</SettingsList.LinkItem>
)
}
@@ -0,0 +1 @@
export function SettingsListItem() {}
@@ -0,0 +1,244 @@
import {useState} from 'react'
import {Alert, View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import * as DynamicAppIcon from '@mozzius/expo-dynamic-app-icon'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {DISCOVER_DEBUG_DIDS} from '#/lib/constants'
import {PressableScale} from '#/lib/custom-animations/PressableScale'
import {CommonNavigatorParams} from '#/lib/routes/types'
import {isAndroid} from '#/platform/detection'
import {useSession} from '#/state/session'
import {AppIconImage} from '#/screens/Settings/AppIconSettings/AppIconImage'
import {AppIconSet} from '#/screens/Settings/AppIconSettings/types'
import {useAppIconSets} from '#/screens/Settings/AppIconSettings/useAppIconSets'
import {atoms as a, useTheme} from '#/alf'
import * as Toggle from '#/components/forms/Toggle'
import * as Layout from '#/components/Layout'
import {Text} from '#/components/Typography'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppIconSettings'>
export function AppIconSettingsScreen({}: Props) {
const t = useTheme()
const {_} = useLingui()
const sets = useAppIconSets()
const {currentAccount} = useSession()
const [currentAppIcon, setCurrentAppIcon] = useState(() =>
getAppIconName(DynamicAppIcon.getAppIcon()),
)
const onSetAppIcon = (icon: string) => {
if (isAndroid) {
const next =
sets.defaults.find(i => i.id === icon) ??
sets.core.find(i => i.id === icon)
Alert.alert(
next
? _(msg`Change app icon to "${next.name}"`)
: _(msg`Change app icon`),
// to determine - can we stop this happening? -sfn
_(msg`The app will be restarted`),
[
{
text: _(msg`Cancel`),
style: 'cancel',
},
{
text: _(msg`OK`),
onPress: () => {
setCurrentAppIcon(setAppIcon(icon))
},
style: 'default',
},
],
)
} else {
setCurrentAppIcon(setAppIcon(icon))
}
}
return (
<Layout.Screen>
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
<Layout.Header.TitleText>
<Trans>App Icon</Trans>
</Layout.Header.TitleText>
</Layout.Header.Content>
<Layout.Header.Slot />
</Layout.Header.Outer>
<Layout.Content contentContainerStyle={[a.p_lg]}>
<Group
label={_(msg`Default icons`)}
value={currentAppIcon}
onChange={onSetAppIcon}>
{sets.defaults.map((icon, i) => (
<Row
key={icon.id}
icon={icon}
isEnd={i === sets.defaults.length - 1}>
<AppIcon icon={icon} key={icon.id} size={40} />
<RowText>{icon.name}</RowText>
</Row>
))}
</Group>
{DISCOVER_DEBUG_DIDS[currentAccount?.did ?? ''] && (
<>
<Text
style={[
a.text_md,
a.mt_xl,
a.mb_sm,
a.font_bold,
t.atoms.text_contrast_medium,
]}>
<Trans>Bluesky+</Trans>
</Text>
<Group
label={_(msg`Bluesky+ icons`)}
value={currentAppIcon}
onChange={onSetAppIcon}>
{sets.core.map((icon, i) => (
<Row
key={icon.id}
icon={icon}
isEnd={i === sets.core.length - 1}>
<AppIcon icon={icon} key={icon.id} size={40} />
<RowText>{icon.name}</RowText>
</Row>
))}
</Group>
</>
)}
</Layout.Content>
</Layout.Screen>
)
}
function setAppIcon(icon: string) {
if (icon === 'default_light') {
return getAppIconName(DynamicAppIcon.setAppIcon(null))
} else {
return getAppIconName(DynamicAppIcon.setAppIcon(icon))
}
}
function getAppIconName(icon: string | false) {
if (!icon || icon === 'DEFAULT') {
return 'default_light'
} else {
return icon
}
}
function Group({
children,
label,
value,
onChange,
}: {
children: React.ReactNode
label: string
value: string
onChange: (value: string) => void
}) {
return (
<Toggle.Group
type="radio"
label={label}
values={[value]}
maxSelections={1}
onChange={vals => {
if (vals[0]) onChange(vals[0])
}}>
<View style={[a.flex_1, a.rounded_md, a.overflow_hidden]}>
{children}
</View>
</Toggle.Group>
)
}
function Row({
icon,
children,
isEnd,
}: {
icon: AppIconSet
children: React.ReactNode
isEnd: boolean
}) {
const t = useTheme()
const {_} = useLingui()
return (
<Toggle.Item label={_(msg`Set app icon to ${icon.name}`)} name={icon.id}>
{({hovered, pressed}) => (
<View
style={[
a.flex_1,
a.p_md,
a.flex_row,
a.gap_md,
a.align_center,
t.atoms.bg_contrast_25,
(hovered || pressed) && t.atoms.bg_contrast_50,
t.atoms.border_contrast_high,
!isEnd && a.border_b,
]}>
{children}
<Toggle.Radio />
</View>
)}
</Toggle.Item>
)
}
function RowText({children}: {children: React.ReactNode}) {
const t = useTheme()
return (
<Text
style={[a.text_md, a.font_bold, a.flex_1, t.atoms.text_contrast_medium]}
emoji>
{children}
</Text>
)
}
function AppIcon({icon, size = 50}: {icon: AppIconSet; size: number}) {
const {_} = useLingui()
return (
<PressableScale
accessibilityLabel={icon.name}
accessibilityHint={_(msg`Tap to change app icon`)}
targetScale={0.95}
onPress={() => {
if (isAndroid) {
Alert.alert(
_(msg`Change app icon to "${icon.name}"`),
_(msg`The app will be restarted`),
[
{
text: _(msg`Cancel`),
style: 'cancel',
},
{
text: _(msg`OK`),
onPress: () => {
DynamicAppIcon.setAppIcon(icon.id)
},
style: 'default',
},
],
)
} else {
DynamicAppIcon.setAppIcon(icon.id)
}
}}>
<AppIconImage icon={icon} size={size} />
</PressableScale>
)
}
@@ -0,0 +1,8 @@
import {ImageSourcePropType} from 'react-native'
export type AppIconSet = {
id: string
name: string
iosImage: () => ImageSourcePropType
androidImage: () => ImageSourcePropType
}
@@ -0,0 +1,134 @@
import {useMemo} from 'react'
import {useLingui} from '@lingui/react'
import {AppIconSet} from '#/screens/Settings/AppIconSettings/types'
export function useAppIconSets() {
const {_} = useLingui()
return useMemo(() => {
const defaults = [
{
id: 'default_light',
name: _('Light'),
iosImage: () => {
return require(`../../../../assets/app-icons/ios_icon_default_light.png`)
},
androidImage: () => {
return require(`../../../../assets/app-icons/android_icon_default_light.png`)
},
},
{
id: 'default_dark',
name: _('Dark'),
iosImage: () => {
return require(`../../../../assets/app-icons/ios_icon_default_dark.png`)
},
androidImage: () => {
return require(`../../../../assets/app-icons/android_icon_default_dark.png`)
},
},
] satisfies AppIconSet[]
/**
* Bluesky+
*/
const core = [
{
id: 'core_aurora',
name: _('Aurora'),
iosImage: () => {
return require(`../../../../assets/app-icons/ios_icon_core_aurora.png`)
},
androidImage: () => {
return require(`../../../../assets/app-icons/android_icon_core_aurora.png`)
},
},
// {
// id: 'core_bonfire',
// name: _('Bonfire'),
// iosImage: () => {
// return require(`../../../../assets/app-icons/ios_icon_core_bonfire.png`)
// },
// androidImage: () => {
// return require(`../../../../assets/app-icons/android_icon_core_bonfire.png`)
// },
// },
{
id: 'core_sunrise',
name: _('Sunrise'),
iosImage: () => {
return require(`../../../../assets/app-icons/ios_icon_core_sunrise.png`)
},
androidImage: () => {
return require(`../../../../assets/app-icons/android_icon_core_sunrise.png`)
},
},
{
id: 'core_sunset',
name: _('Sunset'),
iosImage: () => {
return require(`../../../../assets/app-icons/ios_icon_core_sunset.png`)
},
androidImage: () => {
return require(`../../../../assets/app-icons/android_icon_core_sunset.png`)
},
},
{
id: 'core_midnight',
name: _('Midnight'),
iosImage: () => {
return require(`../../../../assets/app-icons/ios_icon_core_midnight.png`)
},
androidImage: () => {
return require(`../../../../assets/app-icons/android_icon_core_midnight.png`)
},
},
{
id: 'core_flat_blue',
name: _('Flat Blue'),
iosImage: () => {
return require(`../../../../assets/app-icons/ios_icon_core_flat_blue.png`)
},
androidImage: () => {
return require(`../../../../assets/app-icons/android_icon_core_flat_blue.png`)
},
},
{
id: 'core_flat_white',
name: _('Flat White'),
iosImage: () => {
return require(`../../../../assets/app-icons/ios_icon_core_flat_white.png`)
},
androidImage: () => {
return require(`../../../../assets/app-icons/android_icon_core_flat_white.png`)
},
},
{
id: 'core_flat_black',
name: _('Flat Black'),
iosImage: () => {
return require(`../../../../assets/app-icons/ios_icon_core_flat_black.png`)
},
androidImage: () => {
return require(`../../../../assets/app-icons/android_icon_core_flat_black.png`)
},
},
{
id: 'core_classic',
name: _('Bluesky Classic™'),
iosImage: () => {
return require(`../../../../assets/app-icons/ios_icon_core_classic.png`)
},
androidImage: () => {
return require(`../../../../assets/app-icons/android_icon_core_classic.png`)
},
},
] satisfies AppIconSet[]
return {
defaults,
core,
}
}, [_])
}
@@ -0,0 +1,27 @@
import {useCallback, useMemo, useState} from 'react'
import * as DynamicAppIcon from '@mozzius/expo-dynamic-app-icon'
import {useFocusEffect} from '@react-navigation/native'
import {useAppIconSets} from '#/screens/Settings/AppIconSettings/useAppIconSets'
export function useCurrentAppIcon() {
const appIconSets = useAppIconSets()
const [currentAppIcon, setCurrentAppIcon] = useState(() =>
DynamicAppIcon.getAppIcon(),
)
// refresh current icon when screen is focused
useFocusEffect(
useCallback(() => {
setCurrentAppIcon(DynamicAppIcon.getAppIcon())
}, []),
)
return useMemo(() => {
return (
appIconSets.defaults.find(i => i.id === currentAppIcon) ??
appIconSets.core.find(i => i.id === currentAppIcon) ??
appIconSets.defaults[0]
)
}, [appIconSets, currentAppIcon])
}
+2 -10
View File
@@ -13,7 +13,7 @@ import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {isNative} from '#/platform/detection'
import {useSession} from '#/state/session'
import {useSetThemePrefs, useThemePrefs} from '#/state/shell'
import {Logo} from '#/view/icons/Logo'
import {SettingsListItem as AppIconSettingsListItem} from '#/screens/Settings/AppIconSettings/SettingsListItem'
import {atoms as a, native, useAlf, useTheme} from '#/alf'
import * as ToggleButton from '#/components/forms/ToggleButton'
import {Props as SVGIconProps} from '#/components/icons/common'
@@ -181,15 +181,7 @@ export function AppearanceSettingsScreen({}: Props) {
{isNative && DISCOVER_DEBUG_DIDS[currentAccount?.did ?? ''] && (
<>
<SettingsList.Divider />
<SettingsList.LinkItem
to="/settings/app-icon"
label={_(msg`App Icon`)}>
<SettingsList.ItemIcon icon={Logo} />
<SettingsList.ItemText>
<Trans>App Icon</Trans>
</SettingsList.ItemText>
</SettingsList.LinkItem>
<AppIconSettingsListItem />
</>
)}
</Animated.View>
@@ -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