Merge branch 'main' into new-profile-feed-header

This commit is contained in:
Dan Abramov
2024-12-12 05:04:35 +00:00
56 changed files with 1077 additions and 743 deletions
+1
View File
@@ -1,5 +1,6 @@
# Copy this to `.env` and `.env.test` files # Copy this to `.env` and `.env.test` files
BITDRIFT_API_KEY=
SENTRY_AUTH_TOKEN= SENTRY_AUTH_TOKEN=
EXPO_PUBLIC_ENV=development EXPO_PUBLIC_ENV=development
EXPO_PUBLIC_LOG_LEVEL=debug EXPO_PUBLIC_LOG_LEVEL=debug
+1
View File
@@ -222,6 +222,7 @@ module.exports = function (config) {
}, },
], ],
'react-native-compressor', 'react-native-compressor',
'@bitdrift/react-native',
'./plugins/starterPackAppClipExtension/withStarterPackAppClip.js', './plugins/starterPackAppClipExtension/withStarterPackAppClip.js',
'./plugins/withAndroidManifestPlugin.js', './plugins/withAndroidManifestPlugin.js',
'./plugins/withAndroidManifestFCMIconPlugin.js', './plugins/withAndroidManifestFCMIconPlugin.js',
+3 -1
View File
@@ -20,6 +20,7 @@ window.addEventListener('message', event => {
return return
} }
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const id = (event.data as {id: string}).id const id = (event.data as {id: string}).id
if (!id) { if (!id) {
return return
@@ -33,6 +34,7 @@ window.addEventListener('message', event => {
return return
} }
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const height = (event.data as {height: number}).height const height = (event.data as {height: number}).height
if (height) { if (height) {
embed.style.height = `${height}px` embed.style.height = `${height}px`
@@ -47,7 +49,7 @@ window.addEventListener('message', event => {
* @returns * @returns
*/ */
function scan(node = document) { 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++) { for (let i = 0; i < embeds.length; i++) {
const id = String(Math.random()).slice(2) 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 ( return (
<div <div
ref={ref} 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={() => { onClick={() => {
if (ref.current && href) { if (ref.current && href) {
// forwardRef requires preact/compat - let's keep it simple // forwardRef requires preact/compat - let's keep it simple
+20 -14
View File
@@ -78,9 +78,9 @@ export function Embed({
return ( return (
<Link <Link
href={`/profile/${record.author.did}/post/${getRkey(record)}`} 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="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 <img
src={record.author.avatar} src={record.author.avatar}
style={isAuthorLabeled ? {filter: 'blur(1.5px)'} : undefined} style={isAuthorLabeled ? {filter: 'blur(1.5px)'} : undefined}
@@ -88,7 +88,7 @@ export function Embed({
</div> </div>
<p className="line-clamp-1 text-sm"> <p className="line-clamp-1 text-sm">
<span className="font-bold">{record.author.displayName}</span> <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} @{record.author.handle}
</span> </span>
</p> </p>
@@ -209,7 +209,7 @@ function Info({children}: {children: ComponentChildren}) {
return ( return (
<div className="w-full rounded-lg border py-2 px-2.5 flex-row flex gap-2 bg-neutral-50"> <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" /> <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> </div>
) )
} }
@@ -308,7 +308,7 @@ function ExternalEmbed({
return ( return (
<Link <Link
href={content.external.uri} 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> disableTracking>
{content.external.thumb && ( {content.external.thumb && (
<img <img
@@ -317,11 +317,11 @@ function ExternalEmbed({
/> />
)} )}
<div className="py-3 px-4"> <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)} {toNiceDomain(content.external.uri)}
</p> </p>
<p className="font-semibold line-clamp-3">{content.external.title}</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} {content.external.description}
</p> </p>
</div> </div>
@@ -345,23 +345,29 @@ function GenericWithImageEmbed({
return ( return (
<Link <Link
href={href} 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"> <div className="flex gap-2.5 items-center">
{image ? ( {image ? (
<img <img
src={image} src={image}
alt={title} 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="w-8 h-8 rounded-md bg-brand shrink-0" />
)} )}
<div className="flex-1"> <div className="flex-1">
<p className="font-bold text-sm">{title}</p> <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>
</div> </div>
{description && <p className="text-textLight text-sm">{description}</p>} {description && (
<p className="text-textLight dark:text-textDimmed text-sm">
{description}
</p>
)}
</Link> </Link>
) )
} }
@@ -406,7 +412,7 @@ function StarterPackEmbed({
return ( return (
<Link <Link
href={starterPackHref} 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" /> <img src={imageUri} className="aspect-[1.91/1] object-cover" />
<div className="py-3 px-4"> <div className="py-3 px-4">
<div className="flex space-x-2 items-center"> <div className="flex space-x-2 items-center">
@@ -415,7 +421,7 @@ function StarterPackEmbed({
<p className="font-semibold leading-[21px]"> <p className="font-semibold leading-[21px]">
{content.record.name} {content.record.name}
</p> </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{' '} Starter pack by{' '}
{content.creator.displayName || `@${content.creator.handle}`} {content.creator.displayName || `@${content.creator.handle}`}
</p> </p>
@@ -425,7 +431,7 @@ function StarterPackEmbed({
<p className="text-sm mt-1">{content.record.description}</p> <p className="text-sm mt-1">{content.record.description}</p>
)} )}
{!!content.joinedAllTimeCount && content.joinedAllTimeCount > 50 && ( {!!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! {content.joinedAllTimeCount} users have joined!
</p> </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-1 flex-col flex gap-2" lang={record?.langs?.[0]}>
<div className="flex gap-2.5 items-center cursor-pointer"> <div className="flex gap-2.5 items-center cursor-pointer">
<Link href={`/profile/${post.author.did}`} className="rounded-full"> <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 <img
src={post.author.avatar} src={post.author.avatar}
style={isAuthorLabeled ? {filter: 'blur(2.5px)'} : undefined} style={isAuthorLabeled ? {filter: 'blur(2.5px)'} : undefined}
@@ -53,7 +53,7 @@ export function Post({thread}: Props) {
</Link> </Link>
<Link <Link
href={`/profile/${post.author.did}`} 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> <p>@{post.author.handle}</p>
</Link> </Link>
</div> </div>
@@ -69,15 +69,15 @@ export function Post({thread}: Props) {
<Link href={href}> <Link href={href}>
<time <time
datetime={new Date(post.indexedAt).toISOString()} 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)} {niceDate(post.indexedAt)}
</time> </time>
</Link> </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 && ( {!!post.likeCount && (
<div className="flex items-center gap-2 cursor-pointer"> <div className="flex items-center gap-2 cursor-pointer">
<img src={likeIcon} className="w-5 h-5" /> <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)} {prettyNumber(post.likeCount)}
</p> </p>
</div> </div>
@@ -85,17 +85,19 @@ export function Post({thread}: Props) {
{!!post.repostCount && ( {!!post.repostCount && (
<div className="flex items-center gap-2 cursor-pointer"> <div className="flex items-center gap-2 cursor-pointer">
<img src={repostIcon} className="w-5 h-5" /> <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)} {prettyNumber(post.repostCount)}
</p> </p>
</div> </div>
)} )}
<div className="flex items-center gap-2 cursor-pointer"> <div className="flex items-center gap-2 cursor-pointer">
<img src={replyIcon} className="w-5 h-5" /> <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>
<div className="flex-1" /> <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 {post.replyCount
? `Read ${prettyNumber(post.replyCount)} ${ ? `Read ${prettyNumber(post.replyCount)} ${
post.replyCount > 1 ? 'replies' : 'reply' post.replyCount > 1 ? 'replies' : 'reply'
+4
View File
@@ -5,3 +5,7 @@
.break-word { .break-word {
word-break: 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 arrowBottom from '../../assets/arrowBottom_stroke2_corner0_rounded.svg'
import logo from '../../assets/logo.svg' import logo from '../../assets/logo.svg'
import {initColorMode} from '../color-mode'
import {Container} from '../components/container' import {Container} from '../components/container'
import {Link} from '../components/link' import {Link} from '../components/link'
import {Post} from '../components/post' import {Post} from '../components/post'
@@ -21,6 +22,8 @@ export const EMBED_SCRIPT = `${EMBED_SERVICE}/static/embed.js`
const root = document.getElementById('app') const root = document.getElementById('app')
if (!root) throw new Error('No root element') if (!root) throw new Error('No root element')
initColorMode()
const agent = new BskyAgent({ const agent = new BskyAgent({
service: 'https://public.api.bsky.app', service: 'https://public.api.bsky.app',
}) })
@@ -108,7 +111,7 @@ function LandingPage() {
}, [uri]) }, [uri])
return ( 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 <Link
href="https://bsky.social/about" href="https://bsky.social/about"
className="transition-transform hover:scale-110"> className="transition-transform hover:scale-110">
@@ -121,20 +124,22 @@ function LandingPage() {
type="text" type="text"
value={uri} value={uri}
onInput={e => setUri(e.currentTarget.value)} 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} placeholder={DEFAULT_POST}
/> />
<img src={arrowBottom} className="w-6" /> <img src={arrowBottom} className="w-6 dark:invert" />
{loading ? ( {loading ? (
<Skeleton /> <div className="w-full max-w-[600px]">
<Skeleton />
</div>
) : ( ) : (
<div className="w-full max-w-[600px] gap-8 flex flex-col"> <div className="w-full max-w-[600px] gap-8 flex flex-col">
{!error && thread && uri && <Snippet thread={thread} />} {!error && thread && uri && <Snippet thread={thread} />}
{!error && thread && <Post thread={thread} key={thread.post.uri} />} {!error && thread && <Post thread={thread} key={thread.post.uri} />}
{error && ( {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> <p className="text-red-500 text-center">{error}</p>
</div> </div>
)} )}
@@ -149,15 +154,15 @@ function Skeleton() {
<Container> <Container>
<div className="flex-1 flex-col flex gap-2 pb-8"> <div className="flex-1 flex-col flex gap-2 pb-8">
<div className="flex gap-2.5 items-center"> <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="flex-1">
<div className="bg-neutral-100 animate-pulse w-64 h-4 rounded" /> <div className="bg-neutral-100 dark:bg-slate-700 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-32 h-3 mt-1 rounded" />
</div> </div>
</div> </div>
<div className="w-full h-4 mt-2 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 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 rounded animate-pulse" /> <div className="w-3/4 h-4 bg-neutral-100 dark:bg-slate-700 rounded animate-pulse" />
</div> </div>
</Container> </Container>
) )
@@ -220,7 +225,7 @@ function Snippet({thread}: {thread: AppBskyFeedDefs.ThreadViewPost}) {
ref={ref} ref={ref}
type="text" type="text"
value={snippet} 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 readOnly
autoFocus autoFocus
onFocus={() => { onFocus={() => {
@@ -228,7 +233,7 @@ function Snippet({thread}: {thread: AppBskyFeedDefs.ThreadViewPost}) {
}} }}
/> />
<button <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={() => { onClick={() => {
ref.current?.focus() ref.current?.focus()
ref.current?.select() ref.current?.select()
+6 -3
View File
@@ -4,6 +4,7 @@ import {AppBskyFeedDefs, AtpAgent} from '@atproto/api'
import {h, render} from 'preact' import {h, render} from 'preact'
import logo from '../../assets/logo.svg' import logo from '../../assets/logo.svg'
import {initColorMode} from '../color-mode'
import {Container} from '../components/container' import {Container} from '../components/container'
import {Link} from '../components/link' import {Link} from '../components/link'
import {Post} from '../components/post' import {Post} from '../components/post'
@@ -21,6 +22,8 @@ if (!uri) {
throw new Error('No uri in path') throw new Error('No uri in path')
} }
initColorMode()
agent agent
.getPostThread({ .getPostThread({
uri, uri,
@@ -55,13 +58,13 @@ function PwiOptOut({thread}: {thread: AppBskyFeedDefs.ThreadViewPost}) {
<img src={logo} className="h-6" /> <img src={logo} className="h-6" />
</Link> </Link>
<div className="w-full py-12 gap-4 flex flex-col items-center"> <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 The author of this post has requested their posts not be displayed on
external sites. external sites.
</p> </p>
<Link <Link
href={href} 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 View on Bluesky
</Link> </Link>
</div> </div>
@@ -77,7 +80,7 @@ function ErrorMessage() {
className="transition-transform hover:scale-110 absolute top-4 right-4"> className="transition-transform hover:scale-110 absolute top-4 right-4">
<img src={logo} className="h-6" /> <img src={logo} className="h-6" />
</Link> </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. Post not found, it may have been deleted.
</p> </p>
</Container> </Container>
+8
View File
@@ -1,11 +1,19 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
module.exports = { module.exports = {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
darkMode: ['variant', [
'&:is(.dark *):not(:is(.dark .light *))',
]],
theme: { theme: {
extend: { extend: {
colors: { colors: {
brand: 'rgb(10,122,255)', brand: 'rgb(10,122,255)',
brandLighten: 'rgb(32,139,254)',
textLight: 'rgb(66,87,108)', 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": { "compilerOptions": {
"target": "ES5", "target": "ES5",
@@ -20,5 +19,5 @@
"jsxFragmentFactory": "Fragment", "jsxFragmentFactory": "Fragment",
"downlevelIteration": true "downlevelIteration": true
}, },
"include": ["src", "vite.config.ts"] "include": ["src", "snippet", "vite.config.ts"]
} }
+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. 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 ### Adding and Updating Locales
- `yarn intl:build` -> you will also need to run this anytime `./src/locale/{locale}/messages.po` change - `yarn intl:build` -> you will also need to run this anytime `./src/locale/{locale}/messages.po` change
+5 -4
View File
@@ -55,6 +55,7 @@
}, },
"dependencies": { "dependencies": {
"@atproto/api": "^0.13.18", "@atproto/api": "^0.13.18",
"@bitdrift/react-native": "0.4.0",
"@braintree/sanitize-url": "^6.0.2", "@braintree/sanitize-url": "^6.0.2",
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet", "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
"@emoji-mart/react": "^1.1.1", "@emoji-mart/react": "^1.1.1",
@@ -62,9 +63,9 @@
"@expo/webpack-config": "^19.0.0", "@expo/webpack-config": "^19.0.0",
"@floating-ui/dom": "^1.6.3", "@floating-ui/dom": "^1.6.3",
"@floating-ui/react-dom": "^2.0.8", "@floating-ui/react-dom": "^2.0.8",
"@formatjs/intl-locale": "^4.0.0", "@formatjs/intl-locale": "^4.2.8",
"@formatjs/intl-numberformat": "^8.10.3", "@formatjs/intl-numberformat": "^8.15.1",
"@formatjs/intl-pluralrules": "^5.2.14", "@formatjs/intl-pluralrules": "^5.4.1",
"@fortawesome/fontawesome-svg-core": "^6.1.1", "@fortawesome/fontawesome-svg-core": "^6.1.1",
"@fortawesome/free-regular-svg-icons": "^6.1.1", "@fortawesome/free-regular-svg-icons": "^6.1.1",
"@fortawesome/free-solid-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-picker-select": "^9.3.1",
"react-native-progress": "bluesky-social/react-native-progress", "react-native-progress": "bluesky-social/react-native-progress",
"react-native-qrcode-styled": "^0.3.3", "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-root-siblings": "^4.1.1",
"react-native-safe-area-context": "4.14.0", "react-native-safe-area-context": "4.14.0",
"react-native-screens": "~4.3.0", "react-native-screens": "~4.3.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
+1
View File
@@ -1,5 +1,6 @@
import 'react-native-url-polyfill/auto' import 'react-native-url-polyfill/auto'
import '#/lib/sentry' // must be near top import '#/lib/sentry' // must be near top
import '#/lib/bitdrift' // must be near top
import '#/view/icons' import '#/view/icons'
import React, {useEffect, useState} from 'react' import React, {useEffect, useState} from 'react'
+2 -2
View File
@@ -56,8 +56,6 @@ import {PostThreadScreen} from '#/view/screens/PostThread'
import {PrivacyPolicyScreen} from '#/view/screens/PrivacyPolicy' import {PrivacyPolicyScreen} from '#/view/screens/PrivacyPolicy'
import {ProfileScreen} from '#/view/screens/Profile' import {ProfileScreen} from '#/view/screens/Profile'
import {ProfileFeedLikedByScreen} from '#/view/screens/ProfileFeedLikedBy' 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 {ProfileListScreen} from '#/view/screens/ProfileList'
import {SavedFeeds} from '#/view/screens/SavedFeeds' import {SavedFeeds} from '#/view/screens/SavedFeeds'
import {SearchScreen} from '#/view/screens/Search' import {SearchScreen} from '#/view/screens/Search'
@@ -77,6 +75,8 @@ import {PostQuotesScreen} from '#/screens/Post/PostQuotes'
import {PostRepostedByScreen} from '#/screens/Post/PostRepostedBy' import {PostRepostedByScreen} from '#/screens/Post/PostRepostedBy'
import {ProfileKnownFollowersScreen} from '#/screens/Profile/KnownFollowers' import {ProfileKnownFollowersScreen} from '#/screens/Profile/KnownFollowers'
import {ProfileFeedScreen} from '#/screens/Profile/ProfileFeed' 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 {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy'
import {AppearanceSettingsScreen} from '#/screens/Settings/AppearanceSettings' import {AppearanceSettingsScreen} from '#/screens/Settings/AppearanceSettings'
import {AppIconSettingsScreen} from '#/screens/Settings/AppIconSettings' import {AppIconSettingsScreen} from '#/screens/Settings/AppIconSettings'
+4 -5
View File
@@ -7,7 +7,7 @@ import {
AtUri, AtUri,
RichText as RichTextApi, RichText as RichTextApi,
} from '@atproto/api' } from '@atproto/api'
import {msg, plural, Trans} from '@lingui/macro' import {msg, Plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
@@ -210,10 +210,9 @@ export function Likes({count}: {count: number}) {
const t = useTheme() const t = useTheme()
return ( return (
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}> <Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
{plural(count || 0, { <Trans>
one: 'Liked by # user', Liked by <Plural value={count || 0} one="# user" other="# users" />
other: 'Liked by # users', </Trans>
})}
</Text> </Text>
) )
} }
+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() const t = useTheme()
return ( return (
<Text <Text
@@ -93,7 +93,9 @@ export function LikeCount({count}: {count: number}) {
t.atoms.text_contrast_medium, t.atoms.text_contrast_medium,
{fontWeight: '600'}, {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> </Text>
) )
} }
@@ -138,7 +140,7 @@ export function Default({
value={labeler.creator.description} value={labeler.creator.description}
handle={labeler.creator.handle} handle={labeler.creator.handle}
/> />
{labeler.likeCount ? <LikeCount count={labeler.likeCount} /> : null} {labeler.likeCount ? <LikeCount likeCount={labeler.likeCount} /> : null}
</Content> </Content>
</Outer> </Outer>
) )
+2 -2
View File
@@ -48,8 +48,8 @@ export function Outer({
a.gap_sm, a.gap_sm,
gutters, gutters,
platform({ platform({
native: [a.pb_sm, a.pt_xs], native: [a.pb_xs, {minHeight: 48}],
web: [a.py_sm], web: [a.py_xs, {minHeight: 52}],
}), }),
t.atoms.border_contrast_low, t.atoms.border_contrast_low,
gtMobile && [a.mx_auto, {maxWidth: 600}], gtMobile && [a.mx_auto, {maxWidth: 600}],
+17 -11
View File
@@ -1,6 +1,6 @@
import {StyleProp, View, ViewStyle} from 'react-native' import {StyleProp, View, ViewStyle} from 'react-native'
import {AppBskyFeedDefs, ComAtprotoLabelDefs} from '@atproto/api' 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 {useLingui} from '@lingui/react'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
@@ -50,17 +50,23 @@ export function LabelsOnMe({
<ButtonIcon position="left" icon={CircleInfo} /> <ButtonIcon position="left" icon={CircleInfo} />
<ButtonText style={[a.leading_snug]}> <ButtonText style={[a.leading_snug]}>
{type === 'account' ? ( {type === 'account' ? (
<Plural <Trans>
value={labels.length} <Plural
one="# label has been placed on this account" value={labels.length}
other="# labels have been placed on this account" one="# label has"
/> other="# labels have"
/>{' '}
been placed on this account
</Trans>
) : ( ) : (
<Plural <Trans>
value={labels.length} <Plural
one="# label has been placed on this content" value={labels.length}
other="# labels have been placed on this content" one="# label has"
/> other="# labels have"
/>{' '}
been placed on this content
</Trans>
)} )}
</ButtonText> </ButtonText>
</Button> </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'})
}
+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 {add} from '#/logger/logDump'
import {Sentry} from '#/logger/sentry' import {Sentry} from '#/logger/sentry'
import * as env from '#/env' import * as env from '#/env'
import {createBitdriftTransport} from './bitdriftTransport'
import {Metadata} from './types'
import {ConsoleTransportEntry, LogLevel, Transport} from './types'
export enum LogLevel { export {LogLevel}
Debug = 'debug', export type {ConsoleTransportEntry, Transport}
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
}
const enabledLogLevels: { const enabledLogLevels: {
[key in LogLevel]: LogLevel[] [key in LogLevel]: LogLevel[]
@@ -328,6 +266,10 @@ export class Logger {
*/ */
export const logger = new Logger() export const logger = new Logger()
if (!env.IS_TEST) {
logger.addTransport(createBitdriftTransport())
}
if (env.IS_DEV && !env.IS_TEST) { if (env.IS_DEV && !env.IS_TEST) {
logger.addTransport(consoleTransport) 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
}
+24 -5
View File
@@ -1,13 +1,12 @@
import React from 'react' import React from 'react'
import {msg} from '@lingui/macro' import {Plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native' import {useFocusEffect} from '@react-navigation/native'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {makeRecordUri} from '#/lib/strings/url-helpers' import {makeRecordUri} from '#/lib/strings/url-helpers'
import {usePostThreadQuery} from '#/state/queries/post-thread'
import {useSetMinimalShellMode} from '#/state/shell' import {useSetMinimalShellMode} from '#/state/shell'
import {PostLikedBy as PostLikedByComponent} from '#/view/com/post-thread/PostLikedBy' import {PostLikedBy as PostLikedByComponent} from '#/view/com/post-thread/PostLikedBy'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import * as Layout from '#/components/Layout' import * as Layout from '#/components/Layout'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostLikedBy'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostLikedBy'>
@@ -15,7 +14,12 @@ export const PostLikedByScreen = ({route}: Props) => {
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()
const {name, rkey} = route.params const {name, rkey} = route.params
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) 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( useFocusEffect(
React.useCallback(() => { React.useCallback(() => {
@@ -25,7 +29,22 @@ export const PostLikedByScreen = ({route}: Props) => {
return ( return (
<Layout.Screen> <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} /> <PostLikedByComponent uri={uri} />
</Layout.Screen> </Layout.Screen>
) )
+29 -10
View File
@@ -1,15 +1,12 @@
import React from 'react' import React from 'react'
import {msg} from '@lingui/macro' import {Plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native' import {useFocusEffect} from '@react-navigation/native'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {makeRecordUri} from '#/lib/strings/url-helpers' 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 {useSetMinimalShellMode} from '#/state/shell'
import {PostQuotes as PostQuotesComponent} from '#/view/com/post-thread/PostQuotes' 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' import * as Layout from '#/components/Layout'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostQuotes'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostQuotes'>
@@ -17,7 +14,12 @@ export const PostQuotesScreen = ({route}: Props) => {
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()
const {name, rkey} = route.params const {name, rkey} = route.params
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) 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( useFocusEffect(
React.useCallback(() => { React.useCallback(() => {
@@ -27,10 +29,27 @@ export const PostQuotesScreen = ({route}: Props) => {
return ( return (
<Layout.Screen> <Layout.Screen>
<CenteredView sideBorders={true}> <Layout.Header.Outer>
<ViewHeader title={_(msg`Quotes`)} showBorder={!isWeb} /> <Layout.Header.BackButton />
<PostQuotesComponent uri={uri} /> <Layout.Header.Content>
</CenteredView> {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> </Layout.Screen>
) )
} }
+29 -10
View File
@@ -1,15 +1,12 @@
import React from 'react' import React from 'react'
import {msg} from '@lingui/macro' import {Plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native' import {useFocusEffect} from '@react-navigation/native'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {makeRecordUri} from '#/lib/strings/url-helpers' 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 {useSetMinimalShellMode} from '#/state/shell'
import {PostRepostedBy as PostRepostedByComponent} from '#/view/com/post-thread/PostRepostedBy' 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' import * as Layout from '#/components/Layout'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostRepostedBy'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostRepostedBy'>
@@ -17,7 +14,12 @@ export const PostRepostedByScreen = ({route}: Props) => {
const {name, rkey} = route.params const {name, rkey} = route.params
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()
const {_} = useLingui() const {data: post} = usePostThreadQuery(uri)
let quoteCount
if (post?.thread.type === 'post') {
quoteCount = post.thread.post.repostCount
}
useFocusEffect( useFocusEffect(
React.useCallback(() => { React.useCallback(() => {
@@ -27,10 +29,27 @@ export const PostRepostedByScreen = ({route}: Props) => {
return ( return (
<Layout.Screen> <Layout.Screen>
<CenteredView sideBorders={true}> <Layout.Header.Outer>
<ViewHeader title={_(msg`Reposted By`)} showBorder={!isWeb} /> <Layout.Header.BackButton />
<PostRepostedByComponent uri={uri} /> <Layout.Header.Content>
</CenteredView> {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> </Layout.Screen>
) )
} }
+1 -1
View File
@@ -30,7 +30,7 @@ export function ProfileHeaderMetrics({
return ( return (
<View <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"> pointerEvents="box-none">
<InlineLinkText <InlineLinkText
testID="profileHeaderFollowersButton" testID="profileHeaderFollowersButton"
@@ -291,10 +291,12 @@ let ProfileHeaderLabeler = ({
}, },
}} }}
size="tiny" size="tiny"
label={plural(likeCount, { label={_(
one: 'Liked by # user', msg`Liked by ${plural(likeCount, {
other: 'Liked by # users', one: '# user',
})}> other: '# users',
})}`,
)}>
{({hovered, focused, pressed}) => ( {({hovered, focused, pressed}) => (
<Text <Text
style={[ style={[
@@ -304,11 +306,14 @@ let ProfileHeaderLabeler = ({
(hovered || focused || pressed) && (hovered || focused || pressed) &&
t.atoms.text_contrast_high, t.atoms.text_contrast_high,
]}> ]}>
<Plural <Trans>
value={likeCount} Liked by{' '}
one="Liked by # user" <Plural
other="Liked by # users" value={likeCount}
/> one="# user"
other="# users"
/>
</Trans>
</Text> </Text>
)} )}
</Link> </Link>
@@ -244,7 +244,7 @@ let ProfileHeaderStandard = ({
<ProfileHeaderHandle profile={profile} /> <ProfileHeaderHandle profile={profile} />
</View> </View>
{!isPlaceholderProfile && !isBlockedUser && ( {!isPlaceholderProfile && !isBlockedUser && (
<> <View style={a.gap_md}>
<ProfileHeaderMetrics profile={profile} /> <ProfileHeaderMetrics profile={profile} />
{descriptionRT && !moderation.ui('profileView').blur ? ( {descriptionRT && !moderation.ui('profileView').blur ? (
<View pointerEvents="auto"> <View pointerEvents="auto">
@@ -262,14 +262,14 @@ let ProfileHeaderStandard = ({
{!isMe && {!isMe &&
!isBlockedUser && !isBlockedUser &&
shouldShowKnownFollowers(profile.viewer?.knownFollowers) && ( 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 <KnownFollowers
profile={profile} profile={profile}
moderationOpts={moderationOpts} moderationOpts={moderationOpts}
/> />
</View> </View>
)} )}
</> </View>
)} )}
</View> </View>
<Prompt.Basic <Prompt.Basic
+54
View File
@@ -0,0 +1,54 @@
import React from 'react'
import {Plural} from '@lingui/macro'
import {useFocusEffect} from '@react-navigation/native'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {useProfileQuery} from '#/state/queries/profile'
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
import {useSetMinimalShellMode} from '#/state/shell'
import {ProfileFollowers as ProfileFollowersComponent} from '#/view/com/profile/ProfileFollowers'
import * as Layout from '#/components/Layout'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFollowers'>
export const ProfileFollowersScreen = ({route}: Props) => {
const {name} = route.params
const setMinimalShellMode = useSetMinimalShellMode()
const {data: resolvedDid} = useResolveDidQuery(name)
const {data: profile} = useProfileQuery({
did: resolvedDid,
})
useFocusEffect(
React.useCallback(() => {
setMinimalShellMode(false)
}, [setMinimalShellMode]),
)
return (
<Layout.Screen testID="profileFollowersScreen">
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
{profile && (
<>
<Layout.Header.TitleText>
{sanitizeDisplayName(profile.displayName || profile.handle)}
</Layout.Header.TitleText>
<Layout.Header.SubtitleText>
<Plural
value={profile.followersCount ?? 0}
one="# follower"
other="# followers"
/>
</Layout.Header.SubtitleText>
</>
)}
</Layout.Header.Content>
<Layout.Header.Slot />
</Layout.Header.Outer>
<ProfileFollowersComponent name={name} />
</Layout.Screen>
)
}
+54
View File
@@ -0,0 +1,54 @@
import React from 'react'
import {Plural} from '@lingui/macro'
import {useFocusEffect} from '@react-navigation/native'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {useProfileQuery} from '#/state/queries/profile'
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
import {useSetMinimalShellMode} from '#/state/shell'
import {ProfileFollows as ProfileFollowsComponent} from '#/view/com/profile/ProfileFollows'
import * as Layout from '#/components/Layout'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFollows'>
export const ProfileFollowsScreen = ({route}: Props) => {
const {name} = route.params
const setMinimalShellMode = useSetMinimalShellMode()
const {data: resolvedDid} = useResolveDidQuery(name)
const {data: profile} = useProfileQuery({
did: resolvedDid,
})
useFocusEffect(
React.useCallback(() => {
setMinimalShellMode(false)
}, [setMinimalShellMode]),
)
return (
<Layout.Screen testID="profileFollowsScreen">
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
{profile && (
<>
<Layout.Header.TitleText>
{sanitizeDisplayName(profile.displayName || profile.handle)}
</Layout.Header.TitleText>
<Layout.Header.SubtitleText>
<Plural
value={profile.followersCount ?? 0}
one="# following"
other="# following"
/>
</Layout.Header.SubtitleText>
</>
)}
</Layout.Header.Content>
<Layout.Header.Slot />
</Layout.Header.Outer>
<ProfileFollowsComponent name={name} />
</Layout.Screen>
)
}
@@ -458,11 +458,9 @@ function DialogInner({
to={makeCustomFeedLink(info.creatorDid, feedRkey, 'liked-by')} to={makeCustomFeedLink(info.creatorDid, feedRkey, 'liked-by')}
style={[a.underline, t.atoms.text_contrast_medium]} style={[a.underline, t.atoms.text_contrast_medium]}
onPress={() => control.close()}> onPress={() => control.close()}>
<Plural <Trans>
value={likeCount} Liked by <Plural value={likeCount} one="# user" other="# users" />
one="Liked by # user" </Trans>
other="Liked by # users"
/>
</InlineLinkText> </InlineLinkText>
)} )}
</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 {isNative} from '#/platform/detection'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {useSetThemePrefs, useThemePrefs} from '#/state/shell' 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 {atoms as a, native, useAlf, useTheme} from '#/alf'
import * as ToggleButton from '#/components/forms/ToggleButton' import * as ToggleButton from '#/components/forms/ToggleButton'
import {Props as SVGIconProps} from '#/components/icons/common' import {Props as SVGIconProps} from '#/components/icons/common'
@@ -181,15 +181,7 @@ export function AppearanceSettingsScreen({}: Props) {
{isNative && DISCOVER_DEBUG_DIDS[currentAccount?.did ?? ''] && ( {isNative && DISCOVER_DEBUG_DIDS[currentAccount?.did ?? ''] && (
<> <>
<SettingsList.Divider /> <SettingsList.Divider />
<AppIconSettingsListItem />
<SettingsList.LinkItem
to="/settings/app-icon"
label={_(msg`App Icon`)}>
<SettingsList.ItemIcon icon={Logo} />
<SettingsList.ItemText>
<Trans>App Icon</Trans>
</SettingsList.ItemText>
</SettingsList.LinkItem>
</> </>
)} )}
</Animated.View> </Animated.View>
+8 -5
View File
@@ -300,11 +300,14 @@ export function FeedSourceCardLoaded({
{showLikes && feed.type === 'feed' ? ( {showLikes && feed.type === 'feed' ? (
<Text type="sm-medium" style={[pal.text, pal.textLight]}> <Text type="sm-medium" style={[pal.text, pal.textLight]}>
<Plural <Trans>
value={feed.likeCount || 0} Liked by{' '}
one="Liked by # user" <Plural
other="Liked by # users" value={feed.likeCount || 0}
/> one="# user"
other="# users"
/>
</Trans>
</Text> </Text>
) : null} ) : null}
</Pressable> </Pressable>
+4 -3
View File
@@ -421,9 +421,6 @@ export function PostThread({uri}: {uri: string | undefined}) {
</View> </View>
) )
} else if (isThreadPost(item)) { } else if (isThreadPost(item)) {
if (!treeView && item.ctx.hasMoreSelfThread) {
return <PostThreadLoadMore post={item.post} />
}
const prev = isThreadPost(posts[index - 1]) const prev = isThreadPost(posts[index - 1])
? (posts[index - 1] as ThreadPost) ? (posts[index - 1] as ThreadPost)
: undefined : undefined
@@ -436,6 +433,10 @@ export function PostThread({uri}: {uri: string | undefined}) {
const hasUnrevealedParents = const hasUnrevealedParents =
index === 0 && skeleton?.parents && maxParents < skeleton.parents.length index === 0 && skeleton?.parents && maxParents < skeleton.parents.length
if (!treeView && prev && item.ctx.hasMoreSelfThread) {
return <PostThreadLoadMore post={prev.post} />
}
return ( return (
<View <View
ref={item.ctx.isHighlightedPost ? highlightedPostRef : undefined} ref={item.ctx.isHighlightedPost ? highlightedPostRef : undefined}
+6 -19
View File
@@ -1,11 +1,6 @@
import React, {useCallback, useEffect} from 'react' import React, {useCallback, useEffect} from 'react'
import {NativeScrollEvent} from 'react-native' import {NativeScrollEvent} from 'react-native'
import { import {interpolate, useSharedValue, withSpring} from 'react-native-reanimated'
interpolate,
makeMutable,
useSharedValue,
withSpring,
} from 'react-native-reanimated'
import EventEmitter from 'eventemitter3' import EventEmitter from 'eventemitter3'
import {ScrollProvider} from '#/lib/ScrollContext' import {ScrollProvider} from '#/lib/ScrollContext'
@@ -20,18 +15,6 @@ function clamp(num: number, min: number, max: number) {
return Math.min(Math.max(num, min), max) return Math.min(Math.max(num, min), max)
} }
const V0 = makeMutable(
withSpring(0, {
overshootClamping: true,
}),
)
const V1 = makeMutable(
withSpring(1, {
overshootClamping: true,
}),
)
export function MainScrollProvider({children}: {children: React.ReactNode}) { export function MainScrollProvider({children}: {children: React.ReactNode}) {
const {headerHeight} = useShellLayout() const {headerHeight} = useShellLayout()
const {headerMode} = useMinimalShellMode() const {headerMode} = useMinimalShellMode()
@@ -42,7 +25,11 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
const setMode = React.useCallback( const setMode = React.useCallback(
(v: boolean) => { (v: boolean) => {
'worklet' 'worklet'
headerMode.set(v ? V1.get() : V0.get()) headerMode.set(() =>
withSpring(v ? 1 : 0, {
overshootClamping: true,
}),
)
}, },
[headerMode], [headerMode],
) )
@@ -1,92 +0,0 @@
import {describe, expect, it} from '@jest/globals'
import {APP_LANGUAGES} from '#/locale/languages'
import {formatCount} from '../format'
const formatCountRound = (locale: string, num: number) => {
const options: Intl.NumberFormatOptions = {
notation: 'compact',
maximumFractionDigits: 1,
}
return new Intl.NumberFormat(locale, options).format(num)
}
const formatCountTrunc = (locale: string, num: number) => {
const options: Intl.NumberFormatOptions = {
notation: 'compact',
maximumFractionDigits: 1,
// @ts-ignore
roundingMode: 'trunc',
}
return new Intl.NumberFormat(locale, options).format(num)
}
// prettier-ignore
const testNums = [
1,
5,
9,
11,
55,
99,
111,
555,
999,
1111,
5555,
9999,
11111,
55555,
99999,
111111,
555555,
999999,
1111111,
5555555,
9999999,
11111111,
55555555,
99999999,
111111111,
555555555,
999999999,
1111111111,
5555555555,
9999999999,
11111111111,
55555555555,
99999999999,
111111111111,
555555555555,
999999999999,
1111111111111,
5555555555555,
9999999999999,
11111111111111,
55555555555555,
99999999999999,
111111111111111,
555555555555555,
999999999999999,
1111111111111111,
5555555555555555,
]
describe('formatCount', () => {
for (const appLanguage of APP_LANGUAGES) {
const locale = appLanguage.code2
it('truncates for ' + locale, () => {
const mockI8nn = {
locale,
number(num: number) {
return formatCountRound(locale, num)
},
}
for (const num of testNums) {
const formatManual = formatCount(mockI8nn as any, num)
const formatOriginal = formatCountTrunc(locale, num)
expect(formatManual).toEqual(formatOriginal)
}
})
}
})
+3 -43
View File
@@ -1,50 +1,10 @@
import {I18n} from '@lingui/core' import {I18n} from '@lingui/core'
const truncateRounding = (num: number, factors: Array<number>): number => {
for (let i = factors.length - 1; i >= 0; i--) {
let factor = factors[i]
if (num >= 10 ** factor) {
if (factor === 10) {
// CA and ES abruptly jump from "9999,9 M" to "10 mil M"
factor--
}
const precision = 1
const divisor = 10 ** (factor - precision)
return Math.floor(num / divisor) * divisor
}
}
return num
}
const koFactors = [3, 4, 8, 12]
const hiFactors = [3, 5, 7, 9, 11, 13]
const esCaFactors = [3, 6, 10, 12]
const itDeFactors = [6, 9, 12]
const jaZhFactors = [4, 8, 12]
const glFactors = [6, 12]
const restFactors = [3, 6, 9, 12]
export const formatCount = (i18n: I18n, num: number) => { export const formatCount = (i18n: I18n, num: number) => {
const locale = i18n.locale return i18n.number(num, {
let truncatedNum: number
if (locale === 'hi') {
truncatedNum = truncateRounding(num, hiFactors)
} else if (locale === 'ko') {
truncatedNum = truncateRounding(num, koFactors)
} else if (locale === 'es' || locale === 'ca') {
truncatedNum = truncateRounding(num, esCaFactors)
} else if (locale === 'ja' || locale === 'zh-CN' || locale === 'zh-TW') {
truncatedNum = truncateRounding(num, jaZhFactors)
} else if (locale === 'it' || locale === 'de') {
truncatedNum = truncateRounding(num, itDeFactors)
} else if (locale === 'gl') {
truncatedNum = truncateRounding(num, glFactors)
} else {
truncatedNum = truncateRounding(num, restFactors)
}
return i18n.number(truncatedNum, {
notation: 'compact', notation: 'compact',
maximumFractionDigits: 1, maximumFractionDigits: 1,
// Ideally we'd use roundingMode: 'trunc' but it isn't supported on RN. // @ts-expect-error - roundingMode not in the types
roundingMode: 'trunc',
}) })
} }
+18 -12
View File
@@ -258,10 +258,12 @@ let PostCtrls = ({
} }
}} }}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={plural(post.replyCount || 0, { accessibilityLabel={_(
one: 'Reply (# reply)', msg`Reply (${plural(post.replyCount || 0, {
other: 'Reply (# replies)', one: '# reply',
})} other: '# replies',
})})`,
)}
accessibilityHint="" accessibilityHint=""
hitSlop={POST_CTRL_HITSLOP}> hitSlop={POST_CTRL_HITSLOP}>
<Bubble <Bubble
@@ -298,14 +300,18 @@ let PostCtrls = ({
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={ accessibilityLabel={
post.viewer?.like post.viewer?.like
? plural(post.likeCount || 0, { ? _(
one: 'Unlike (# like)', msg`Unlike (${plural(post.likeCount || 0, {
other: 'Unlike (# likes)', one: '# like',
}) other: '# likes',
: plural(post.likeCount || 0, { })})`,
one: 'Like (# like)', )
other: 'Like (# likes)', : _(
}) msg`Like (${plural(post.likeCount || 0, {
one: '# like',
other: '# likes',
})})`,
)
} }
accessibilityHint="" accessibilityHint=""
hitSlop={POST_CTRL_HITSLOP}> hitSlop={POST_CTRL_HITSLOP}>
+14 -4
View File
@@ -62,11 +62,21 @@ let RepostButton = ({
{padding: 5}, {padding: 5},
]} ]}
hoverStyle={t.atoms.bg_contrast_25} hoverStyle={t.atoms.bg_contrast_25}
label={`${ label={
isReposted isReposted
? _(msg`Undo repost`) ? _(
: _(msg({message: 'Repost', context: 'action'})) msg`Undo repost (${plural(repostCount || 0, {
} (${plural(repostCount || 0, {one: '# repost', other: '# reposts'})})`} one: '# repost',
other: '# reposts',
})})`,
)
: _(
msg`Repost (${plural(repostCount || 0, {
one: '# repost',
other: '# reposts',
})})`,
)
}
shape="round" shape="round"
variant="ghost" variant="ghost"
color="secondary" color="secondary"
-34
View File
@@ -1,34 +0,0 @@
import React from 'react'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {isWeb} from '#/platform/detection'
import {useSetMinimalShellMode} from '#/state/shell'
import {ProfileFollowers as ProfileFollowersComponent} from '#/view/com/profile/ProfileFollowers'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {CenteredView} from '#/view/com/util/Views'
import * as Layout from '#/components/Layout'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFollowers'>
export const ProfileFollowersScreen = ({route}: Props) => {
const {name} = route.params
const setMinimalShellMode = useSetMinimalShellMode()
const {_} = useLingui()
useFocusEffect(
React.useCallback(() => {
setMinimalShellMode(false)
}, [setMinimalShellMode]),
)
return (
<Layout.Screen testID="profileFollowersScreen">
<CenteredView sideBorders={true}>
<ViewHeader title={_(msg`Followers`)} showBorder={!isWeb} />
<ProfileFollowersComponent name={name} />
</CenteredView>
</Layout.Screen>
)
}
-34
View File
@@ -1,34 +0,0 @@
import React from 'react'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {isWeb} from '#/platform/detection'
import {useSetMinimalShellMode} from '#/state/shell'
import {ProfileFollows as ProfileFollowsComponent} from '#/view/com/profile/ProfileFollows'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {CenteredView} from '#/view/com/util/Views'
import * as Layout from '#/components/Layout'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFollows'>
export const ProfileFollowsScreen = ({route}: Props) => {
const {name} = route.params
const setMinimalShellMode = useSetMinimalShellMode()
const {_} = useLingui()
useFocusEffect(
React.useCallback(() => {
setMinimalShellMode(false)
}, [setMinimalShellMode]),
)
return (
<Layout.Screen testID="profileFollowsScreen">
<CenteredView sideBorders={true}>
<ViewHeader title={_(msg`Following`)} showBorder={!isWeb} />
<ProfileFollowsComponent name={name} />
</CenteredView>
</Layout.Screen>
)
}
+70 -48
View File
@@ -3377,6 +3377,11 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@bitdrift/react-native@0.4.0":
version "0.4.0"
resolved "https://registry.yarnpkg.com/@bitdrift/react-native/-/react-native-0.4.0.tgz#e6484343ef04824aa924df2a757bd9620b2106c1"
integrity sha512-KuYzWEkoGwjjP0ZurjHwV+zfRZjQXxbXa3zhijWv0iqzMI/7kbrBd9lm+wNQo8OrkqFVDlebCb8AGPc0jMZw7A==
"@braintree/sanitize-url@^6.0.2": "@braintree/sanitize-url@^6.0.2":
version "6.0.4" version "6.0.4"
resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz#923ca57e173c6b232bbbb07347b1be982f03e783" resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz#923ca57e173c6b232bbbb07347b1be982f03e783"
@@ -4250,62 +4255,74 @@
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.1.tgz#16308cea045f0fc777b6ff20a9f25474dd8293d2" resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.1.tgz#16308cea045f0fc777b6ff20a9f25474dd8293d2"
integrity sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q== integrity sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==
"@formatjs/ecma402-abstract@2.0.0": "@formatjs/ecma402-abstract@2.3.1":
version "2.0.0" version "2.3.1"
resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-2.0.0.tgz#39197ab90b1c78b7342b129a56a7acdb8f512e17" resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.1.tgz#cdeb3ffe1aeea9c4284b85b7e37e8e8615314c39"
integrity sha512-rRqXOqdFmk7RYvj4khklyqzcfQl9vEL/usogncBHRZfZBDOwMGuSRNFl02fu5KGHXdbinju+YXyuR+Nk8xlr/g== integrity sha512-Ip9uV+/MpLXWRk03U/GzeJMuPeOXpJBSB5V1tjA6kJhvqssye5J5LoYLc7Z5IAHb7nR62sRoguzrFiVCP/hnzw==
dependencies: dependencies:
"@formatjs/intl-localematcher" "0.5.4" "@formatjs/fast-memoize" "2.2.5"
tslib "^2.4.0" "@formatjs/intl-localematcher" "0.5.9"
decimal.js "10"
tslib "2"
"@formatjs/intl-enumerator@1.4.7": "@formatjs/fast-memoize@2.2.5":
version "1.4.7" version "2.2.5"
resolved "https://registry.yarnpkg.com/@formatjs/intl-enumerator/-/intl-enumerator-1.4.7.tgz#6ab697f3f8f18cf0cc6a6b028cb9c40db6001f3d" resolved "https://registry.yarnpkg.com/@formatjs/fast-memoize/-/fast-memoize-2.2.5.tgz#54a4a1793d773b72c372d3dcab3595149aee7880"
integrity sha512-03RHnFqfpB4H/jwCwlzC+wkTDk2Fi24JmVIY2PVGvTUpikN2bSr9+8oTXfOC+y7B7VxjCArUnqWXVoctkmy85w== integrity sha512-6PoewUMrrcqxSoBXAOJDiW1m+AmkrAj0RiXnOMD59GRaswjXhm3MDhgepXPBgonc09oSirAJTsAggzAGQf6A6g==
dependencies: dependencies:
tslib "^2.4.0" tslib "2"
"@formatjs/intl-getcanonicallocales@2.3.0": "@formatjs/intl-enumerator@1.8.7":
version "2.3.0" version "1.8.7"
resolved "https://registry.yarnpkg.com/@formatjs/intl-getcanonicallocales/-/intl-getcanonicallocales-2.3.0.tgz#b6c6fa1c664e30a61f27fa6399a76159d82a5842" resolved "https://registry.yarnpkg.com/@formatjs/intl-enumerator/-/intl-enumerator-1.8.7.tgz#3f004753333f80cc468ae34046bd8416772a0412"
integrity sha512-BOXbLwqQ7nKua/l7tKqDLRN84WupDXFDhGJQMFvsMVA2dKuOdRaWTxWpL3cJ7qPkoNw11Jf+Xpj4OSPBBvW0eQ== integrity sha512-qd7UlWUivKRJ073btssUqMSqzWW9yN3Ki6EqfCZ6uvIv19mONelE5q3GMmdPWBEjgqZikBzBE2qPTqfrgJ4TCA==
dependencies: dependencies:
tslib "^2.4.0" "@formatjs/ecma402-abstract" "2.3.1"
tslib "2"
"@formatjs/intl-locale@^4.0.0": "@formatjs/intl-getcanonicallocales@2.5.4":
version "4.0.0" version "2.5.4"
resolved "https://registry.yarnpkg.com/@formatjs/intl-locale/-/intl-locale-4.0.0.tgz#c111a33078413eba2011e82140466261eb1d67cd" resolved "https://registry.yarnpkg.com/@formatjs/intl-getcanonicallocales/-/intl-getcanonicallocales-2.5.4.tgz#9b843e1891dea83405c51eb3d00c42ef9cb6cab9"
integrity sha512-+4dbMEGsp1bvB3JB3UHH6YTjMnFTifnfdaHp4ROrCCu50NedA69RBsDCG3eivcZkbj57X9ehGhMWjLxlP+gyVw== integrity sha512-vSDOsAcc3U+Kl/0b3de8wCQkb3W30H8LUuslyz67wTAHOPSQhPimZyquhwxXpJR+K5yy9CkzTgk5YE5kFT+PFg==
dependencies: dependencies:
"@formatjs/ecma402-abstract" "2.0.0" tslib "2"
"@formatjs/intl-enumerator" "1.4.7"
"@formatjs/intl-getcanonicallocales" "2.3.0"
tslib "^2.4.0"
"@formatjs/intl-localematcher@0.5.4": "@formatjs/intl-locale@^4.2.8":
version "0.5.4" version "4.2.8"
resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.5.4.tgz#caa71f2e40d93e37d58be35cfffe57865f2b366f" resolved "https://registry.yarnpkg.com/@formatjs/intl-locale/-/intl-locale-4.2.8.tgz#571d44e92b6eb43b7410b37f25e280ec384a32cf"
integrity sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g== integrity sha512-6RY/npeA0kyoZ8QW0JRAT+VBAFBT6+4ZVeGkKCNIDjbLX2LPuU73emGR35Mbwcc6pquVFrxyo6mXxKNzib0kEA==
dependencies: dependencies:
tslib "^2.4.0" "@formatjs/ecma402-abstract" "2.3.1"
"@formatjs/intl-enumerator" "1.8.7"
"@formatjs/intl-getcanonicallocales" "2.5.4"
tslib "2"
"@formatjs/intl-numberformat@^8.10.3": "@formatjs/intl-localematcher@0.5.9":
version "8.10.3" version "0.5.9"
resolved "https://registry.yarnpkg.com/@formatjs/intl-numberformat/-/intl-numberformat-8.10.3.tgz#abc97cc6a7b7f1b20da9f07a976b5589c1192ab8" resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.5.9.tgz#43c6ee22be85b83340bcb09bdfed53657a2720db"
integrity sha512-lH3liLMeIjZ19Zxt8RRPnBcpPweS1YNSXRURDiFfvFmRlDZUOd8+GlcVyECcPZPkIoSH/p4lfGrnaUzepxJ92g== integrity sha512-8zkGu/sv5euxbjfZ/xmklqLyDGQSxsLqg8XOq88JW3cmJtzhCP8EtSJXlaKZnVO4beEaoiT9wj4eIoCQ9smwxA==
dependencies: dependencies:
"@formatjs/ecma402-abstract" "2.0.0" tslib "2"
"@formatjs/intl-localematcher" "0.5.4"
tslib "^2.4.0"
"@formatjs/intl-pluralrules@^5.2.14": "@formatjs/intl-numberformat@^8.15.1":
version "5.2.14" version "8.15.1"
resolved "https://registry.yarnpkg.com/@formatjs/intl-pluralrules/-/intl-pluralrules-5.2.14.tgz#7477bd2aa9bfde9e543d839707eff5460eb08026" resolved "https://registry.yarnpkg.com/@formatjs/intl-numberformat/-/intl-numberformat-8.15.1.tgz#b2a5b00889ed31dbef9d4e5aeee1dea3d040b068"
integrity sha512-l6Ev7aOGXJSh5EPDEqzsbyufdCCKXZk993QXRQebLsB0TXRhIyF4alqjdMEatLwIigK/Mka8kiVIOLeFP5Cj9Q== integrity sha512-NIouSY50xpH/SMJrRbX1Q3hMsGyQmT5MQrta/bOYhpZda1bztOlEYZAKLytk8VGs10wkGz875602mCMhtg4/LA==
dependencies: dependencies:
"@formatjs/ecma402-abstract" "2.0.0" "@formatjs/ecma402-abstract" "2.3.1"
"@formatjs/intl-localematcher" "0.5.4" "@formatjs/intl-localematcher" "0.5.9"
tslib "^2.4.0" decimal.js "10"
tslib "2"
"@formatjs/intl-pluralrules@^5.4.1":
version "5.4.1"
resolved "https://registry.yarnpkg.com/@formatjs/intl-pluralrules/-/intl-pluralrules-5.4.1.tgz#1c03cd2da449e1871bb7c54ea36fec1de68b7e7e"
integrity sha512-kKK4ixTsfKAzyJIVRiJGuw4zd18nEHXiKloYBO9VmLpxrwJTgLQHv2+1hcbxQcwbbo2uc8moUFQuyvxeGEFOfw==
dependencies:
"@formatjs/ecma402-abstract" "2.3.1"
"@formatjs/intl-localematcher" "0.5.9"
decimal.js "10"
tslib "2"
"@fortawesome/fontawesome-common-types@6.4.2": "@fortawesome/fontawesome-common-types@6.4.2":
version "6.4.2" version "6.4.2"
@@ -9372,7 +9389,7 @@ decamelize@^1.2.0:
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==
decimal.js@^10.4.2: decimal.js@10, decimal.js@^10.4.2:
version "10.4.3" version "10.4.3"
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23"
integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA== integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==
@@ -16035,10 +16052,10 @@ react-native-qrcode-styled@^0.3.3:
qrcode "^1.5.4" qrcode "^1.5.4"
react-fast-compare "^3.2.2" react-fast-compare "^3.2.2"
react-native-reanimated@^3.16.3: react-native-reanimated@3.17.0-nightly-20241211-17e89ca24:
version "3.16.3" version "3.17.0-nightly-20241211-17e89ca24"
resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-3.16.3.tgz#3b559dca49e9e40abcf5de834dc27fc05f856b66" resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-3.17.0-nightly-20241211-17e89ca24.tgz#af0c36e278646eb2f79e28ad0047cfd80d0e29f5"
integrity sha512-OWlA6e1oHhytTpc7WiSZ7Tmb8OYwLKYZz29Sz6d6WAg60Hm5GuAiKIWUG7Ako7FLcYhFkA0pEQ2xPMEYUo9vlw== integrity sha512-5p7jr0DrnID1puOzMel3VZVRw5Hl/UdMUvPCI1sEG9IA2mUaWrgeoojS2wVwW1U0Pj6HXjPNEimDSXZneZKNuQ==
dependencies: dependencies:
"@babel/plugin-transform-arrow-functions" "^7.0.0-0" "@babel/plugin-transform-arrow-functions" "^7.0.0-0"
"@babel/plugin-transform-class-properties" "^7.0.0-0" "@babel/plugin-transform-class-properties" "^7.0.0-0"
@@ -18105,6 +18122,11 @@ ts-node@^10.9.1:
v8-compile-cache-lib "^3.0.1" v8-compile-cache-lib "^3.0.1"
yn "3.1.1" yn "3.1.1"
tslib@2:
version "2.8.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
tslib@^1.11.1, tslib@^1.8.1, tslib@^1.9.0: tslib@^1.11.1, tslib@^1.8.1, tslib@^1.9.0:
version "1.14.1" version "1.14.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"