Merge branch 'main' into new-profile-feed-header
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -222,6 +222,7 @@ module.exports = function (config) {
|
||||
},
|
||||
],
|
||||
'react-native-compressor',
|
||||
'@bitdrift/react-native',
|
||||
'./plugins/starterPackAppClipExtension/withStarterPackAppClip.js',
|
||||
'./plugins/withAndroidManifestPlugin.js',
|
||||
'./plugins/withAndroidManifestFCMIconPlugin.js',
|
||||
|
||||
@@ -20,6 +20,7 @@ window.addEventListener('message', event => {
|
||||
return
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const id = (event.data as {id: string}).id
|
||||
if (!id) {
|
||||
return
|
||||
@@ -33,6 +34,7 @@ window.addEventListener('message', event => {
|
||||
return
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const height = (event.data as {height: number}).height
|
||||
if (height) {
|
||||
embed.style.height = `${height}px`
|
||||
@@ -47,7 +49,7 @@ window.addEventListener('message', event => {
|
||||
* @returns
|
||||
*/
|
||||
function scan(node = document) {
|
||||
const embeds = node.querySelectorAll('[data-bluesky-uri]')
|
||||
const embeds = node.querySelectorAll<HTMLIFrameElement>('[data-bluesky-uri]')
|
||||
|
||||
for (let i = 0; i < embeds.length; i++) {
|
||||
const id = String(Math.random()).slice(2)
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
export function applyTheme(theme: 'light' | 'dark') {
|
||||
document.documentElement.classList.remove('light', 'dark')
|
||||
document.documentElement.classList.add(theme)
|
||||
}
|
||||
|
||||
export function initColorMode() {
|
||||
applyTheme(
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
? 'dark'
|
||||
: 'light',
|
||||
)
|
||||
window
|
||||
.matchMedia('(prefers-color-scheme: dark)')
|
||||
.addEventListener('change', mql => {
|
||||
applyTheme(mql.matches ? 'dark' : 'light')
|
||||
})
|
||||
}
|
||||
@@ -37,7 +37,7 @@ export function Container({
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className="w-full bg-white hover:bg-neutral-50 relative transition-colors max-w-[600px] min-w-[300px] flex border rounded-xl"
|
||||
className="w-full bg-white text-black hover:bg-neutral-50 dark:bg-dimmedBg dark:hover:bg-dimmedBgLighten relative transition-colors max-w-[600px] min-w-[300px] flex border dark:border-slate-600 dark:text-slate-200 rounded-xl"
|
||||
onClick={() => {
|
||||
if (ref.current && href) {
|
||||
// forwardRef requires preact/compat - let's keep it simple
|
||||
|
||||
@@ -78,9 +78,9 @@ export function Embed({
|
||||
return (
|
||||
<Link
|
||||
href={`/profile/${record.author.did}/post/${getRkey(record)}`}
|
||||
className="transition-colors hover:bg-neutral-100 border rounded-lg p-2 gap-1.5 w-full flex flex-col">
|
||||
className="transition-colors hover:bg-neutral-100 dark:hover:bg-slate-700 border dark:border-slate-600 rounded-lg p-2 gap-1.5 w-full flex flex-col">
|
||||
<div className="flex gap-1.5 items-center">
|
||||
<div className="w-4 h-4 overflow-hidden rounded-full bg-neutral-300 shrink-0">
|
||||
<div className="w-4 h-4 overflow-hidden rounded-full bg-neutral-300 dark:bg-slate-700 shrink-0">
|
||||
<img
|
||||
src={record.author.avatar}
|
||||
style={isAuthorLabeled ? {filter: 'blur(1.5px)'} : undefined}
|
||||
@@ -88,7 +88,7 @@ export function Embed({
|
||||
</div>
|
||||
<p className="line-clamp-1 text-sm">
|
||||
<span className="font-bold">{record.author.displayName}</span>
|
||||
<span className="text-textLight ml-1">
|
||||
<span className="text-textLight dark:text-textDimmed ml-1">
|
||||
@{record.author.handle}
|
||||
</span>
|
||||
</p>
|
||||
@@ -209,7 +209,7 @@ function Info({children}: {children: ComponentChildren}) {
|
||||
return (
|
||||
<div className="w-full rounded-lg border py-2 px-2.5 flex-row flex gap-2 bg-neutral-50">
|
||||
<img src={infoIcon} className="w-4 h-4 shrink-0 mt-0.5" />
|
||||
<p className="text-sm text-textLight">{children}</p>
|
||||
<p className="text-sm text-textLight dark:text-textDimmed">{children}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -308,7 +308,7 @@ function ExternalEmbed({
|
||||
return (
|
||||
<Link
|
||||
href={content.external.uri}
|
||||
className="w-full rounded-lg overflow-hidden border flex flex-col items-stretch"
|
||||
className="w-full rounded-lg overflow-hidden border dark:border-slate-600 flex flex-col items-stretch"
|
||||
disableTracking>
|
||||
{content.external.thumb && (
|
||||
<img
|
||||
@@ -317,11 +317,11 @@ function ExternalEmbed({
|
||||
/>
|
||||
)}
|
||||
<div className="py-3 px-4">
|
||||
<p className="text-sm text-textLight line-clamp-1">
|
||||
<p className="text-sm text-textLight dark:text-textDimmed line-clamp-1">
|
||||
{toNiceDomain(content.external.uri)}
|
||||
</p>
|
||||
<p className="font-semibold line-clamp-3">{content.external.title}</p>
|
||||
<p className="text-sm text-textLight line-clamp-2 mt-0.5">
|
||||
<p className="text-sm text-textLight dark:text-textDimmed line-clamp-2 mt-0.5">
|
||||
{content.external.description}
|
||||
</p>
|
||||
</div>
|
||||
@@ -345,23 +345,29 @@ function GenericWithImageEmbed({
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
className="w-full rounded-lg border py-2 px-3 flex flex-col gap-2">
|
||||
className="w-full rounded-lg border dark:border-slate-600 py-2 px-3 flex flex-col gap-2">
|
||||
<div className="flex gap-2.5 items-center">
|
||||
{image ? (
|
||||
<img
|
||||
src={image}
|
||||
alt={title}
|
||||
className="w-8 h-8 rounded-md bg-neutral-300 shrink-0"
|
||||
className="w-8 h-8 rounded-md bg-neutral-300 dark:bg-slate-700 shrink-0"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-8 h-8 rounded-md bg-brand shrink-0" />
|
||||
)}
|
||||
<div className="flex-1">
|
||||
<p className="font-bold text-sm">{title}</p>
|
||||
<p className="text-textLight text-sm">{subtitle}</p>
|
||||
<p className="text-textLight dark:text-textDimmed text-sm">
|
||||
{subtitle}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{description && <p className="text-textLight text-sm">{description}</p>}
|
||||
{description && (
|
||||
<p className="text-textLight dark:text-textDimmed text-sm">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
@@ -406,7 +412,7 @@ function StarterPackEmbed({
|
||||
return (
|
||||
<Link
|
||||
href={starterPackHref}
|
||||
className="w-full rounded-lg overflow-hidden border flex flex-col items-stretch">
|
||||
className="w-full rounded-lg overflow-hidden border dark:border-slate-600 flex flex-col items-stretch">
|
||||
<img src={imageUri} className="aspect-[1.91/1] object-cover" />
|
||||
<div className="py-3 px-4">
|
||||
<div className="flex space-x-2 items-center">
|
||||
@@ -415,7 +421,7 @@ function StarterPackEmbed({
|
||||
<p className="font-semibold leading-[21px]">
|
||||
{content.record.name}
|
||||
</p>
|
||||
<p className="text-sm text-textLight line-clamp-2 leading-[18px]">
|
||||
<p className="text-sm text-textLight dark:text-textDimmed line-clamp-2 leading-[18px]">
|
||||
Starter pack by{' '}
|
||||
{content.creator.displayName || `@${content.creator.handle}`}
|
||||
</p>
|
||||
@@ -425,7 +431,7 @@ function StarterPackEmbed({
|
||||
<p className="text-sm mt-1">{content.record.description}</p>
|
||||
)}
|
||||
{!!content.joinedAllTimeCount && content.joinedAllTimeCount > 50 && (
|
||||
<p className="text-sm font-semibold text-textLight mt-1">
|
||||
<p className="text-sm font-semibold text-textLight dark:text-textDimmed mt-1">
|
||||
{content.joinedAllTimeCount} users have joined!
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -38,7 +38,7 @@ export function Post({thread}: Props) {
|
||||
<div className="flex-1 flex-col flex gap-2" lang={record?.langs?.[0]}>
|
||||
<div className="flex gap-2.5 items-center cursor-pointer">
|
||||
<Link href={`/profile/${post.author.did}`} className="rounded-full">
|
||||
<div className="w-10 h-10 overflow-hidden rounded-full bg-neutral-300 shrink-0">
|
||||
<div className="w-10 h-10 overflow-hidden rounded-full bg-neutral-300 dark:bg-slate-700 shrink-0">
|
||||
<img
|
||||
src={post.author.avatar}
|
||||
style={isAuthorLabeled ? {filter: 'blur(2.5px)'} : undefined}
|
||||
@@ -53,7 +53,7 @@ export function Post({thread}: Props) {
|
||||
</Link>
|
||||
<Link
|
||||
href={`/profile/${post.author.did}`}
|
||||
className="text-[15px] text-textLight hover:underline line-clamp-1">
|
||||
className="text-[15px] text-textLight dark:text-textDimmed hover:underline line-clamp-1">
|
||||
<p>@{post.author.handle}</p>
|
||||
</Link>
|
||||
</div>
|
||||
@@ -69,15 +69,15 @@ export function Post({thread}: Props) {
|
||||
<Link href={href}>
|
||||
<time
|
||||
datetime={new Date(post.indexedAt).toISOString()}
|
||||
className="text-textLight mt-1 text-sm hover:underline">
|
||||
className="text-textLight dark:text-textDimmed mt-1 text-sm hover:underline">
|
||||
{niceDate(post.indexedAt)}
|
||||
</time>
|
||||
</Link>
|
||||
<div className="border-t w-full pt-2.5 flex items-center gap-5 text-sm cursor-pointer">
|
||||
<div className="border-t dark:border-slate-600 w-full pt-2.5 flex items-center gap-5 text-sm cursor-pointer">
|
||||
{!!post.likeCount && (
|
||||
<div className="flex items-center gap-2 cursor-pointer">
|
||||
<img src={likeIcon} className="w-5 h-5" />
|
||||
<p className="font-bold text-neutral-500 mb-px">
|
||||
<p className="font-bold text-neutral-500 dark:text-neutral-300 mb-px">
|
||||
{prettyNumber(post.likeCount)}
|
||||
</p>
|
||||
</div>
|
||||
@@ -85,17 +85,19 @@ export function Post({thread}: Props) {
|
||||
{!!post.repostCount && (
|
||||
<div className="flex items-center gap-2 cursor-pointer">
|
||||
<img src={repostIcon} className="w-5 h-5" />
|
||||
<p className="font-bold text-neutral-500 mb-px">
|
||||
<p className="font-bold text-neutral-500 dark:text-neutral-300 mb-px">
|
||||
{prettyNumber(post.repostCount)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2 cursor-pointer">
|
||||
<img src={replyIcon} className="w-5 h-5" />
|
||||
<p className="font-bold text-neutral-500 mb-px">Reply</p>
|
||||
<p className="font-bold text-neutral-500 dark:text-neutral-300 mb-px">
|
||||
Reply
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex-1" />
|
||||
<p className="cursor-pointer text-brand font-bold hover:underline hidden min-[450px]:inline">
|
||||
<p className="cursor-pointer text-brand dark:text-brandLighten font-bold hover:underline hidden min-[450px]:inline">
|
||||
{post.replyCount
|
||||
? `Read ${prettyNumber(post.replyCount)} ${
|
||||
post.replyCount > 1 ? 'replies' : 'reply'
|
||||
|
||||
@@ -5,3 +5,7 @@
|
||||
.break-word {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {useEffect, useMemo, useRef, useState} from 'preact/hooks'
|
||||
|
||||
import arrowBottom from '../../assets/arrowBottom_stroke2_corner0_rounded.svg'
|
||||
import logo from '../../assets/logo.svg'
|
||||
import {initColorMode} from '../color-mode'
|
||||
import {Container} from '../components/container'
|
||||
import {Link} from '../components/link'
|
||||
import {Post} from '../components/post'
|
||||
@@ -21,6 +22,8 @@ export const EMBED_SCRIPT = `${EMBED_SERVICE}/static/embed.js`
|
||||
const root = document.getElementById('app')
|
||||
if (!root) throw new Error('No root element')
|
||||
|
||||
initColorMode()
|
||||
|
||||
const agent = new BskyAgent({
|
||||
service: 'https://public.api.bsky.app',
|
||||
})
|
||||
@@ -108,7 +111,7 @@ function LandingPage() {
|
||||
}, [uri])
|
||||
|
||||
return (
|
||||
<main className="w-full min-h-screen flex flex-col items-center gap-8 py-14 px-4 md:pt-32">
|
||||
<main className="w-full min-h-screen flex flex-col items-center gap-8 py-14 px-4 md:pt-32 dark:bg-dimmedBgDarken dark:text-slate-200">
|
||||
<Link
|
||||
href="https://bsky.social/about"
|
||||
className="transition-transform hover:scale-110">
|
||||
@@ -121,20 +124,22 @@ function LandingPage() {
|
||||
type="text"
|
||||
value={uri}
|
||||
onInput={e => setUri(e.currentTarget.value)}
|
||||
className="border rounded-lg py-3 w-full max-w-[600px] px-4"
|
||||
className="border rounded-lg py-3 w-full max-w-[600px] px-4 dark:bg-dimmedBg dark:border-slate-500"
|
||||
placeholder={DEFAULT_POST}
|
||||
/>
|
||||
|
||||
<img src={arrowBottom} className="w-6" />
|
||||
<img src={arrowBottom} className="w-6 dark:invert" />
|
||||
|
||||
{loading ? (
|
||||
<Skeleton />
|
||||
<div className="w-full max-w-[600px]">
|
||||
<Skeleton />
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full max-w-[600px] gap-8 flex flex-col">
|
||||
{!error && thread && uri && <Snippet thread={thread} />}
|
||||
{!error && thread && <Post thread={thread} key={thread.post.uri} />}
|
||||
{error && (
|
||||
<div className="w-full border border-red-500 bg-red-50 px-4 py-3 rounded-lg">
|
||||
<div className="w-full border border-red-500 bg-red-500/10 px-4 py-3 rounded-lg">
|
||||
<p className="text-red-500 text-center">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -149,15 +154,15 @@ function Skeleton() {
|
||||
<Container>
|
||||
<div className="flex-1 flex-col flex gap-2 pb-8">
|
||||
<div className="flex gap-2.5 items-center">
|
||||
<div className="w-10 h-10 overflow-hidden rounded-full bg-neutral-100 shrink-0 animate-pulse" />
|
||||
<div className="w-10 h-10 overflow-hidden rounded-full bg-neutral-100 dark:bg-slate-700 shrink-0 animate-pulse" />
|
||||
<div className="flex-1">
|
||||
<div className="bg-neutral-100 animate-pulse w-64 h-4 rounded" />
|
||||
<div className="bg-neutral-100 animate-pulse w-32 h-3 mt-1 rounded" />
|
||||
<div className="bg-neutral-100 dark:bg-slate-700 animate-pulse w-64 h-4 rounded" />
|
||||
<div className="bg-neutral-100 dark:bg-slate-700 animate-pulse w-32 h-3 mt-1 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full h-4 mt-2 bg-neutral-100 rounded animate-pulse" />
|
||||
<div className="w-5/6 h-4 bg-neutral-100 rounded animate-pulse" />
|
||||
<div className="w-3/4 h-4 bg-neutral-100 rounded animate-pulse" />
|
||||
<div className="w-full h-4 mt-2 bg-neutral-100 dark:bg-slate-700 rounded animate-pulse" />
|
||||
<div className="w-5/6 h-4 bg-neutral-100 dark:bg-slate-700 rounded animate-pulse" />
|
||||
<div className="w-3/4 h-4 bg-neutral-100 dark:bg-slate-700 rounded animate-pulse" />
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
@@ -220,7 +225,7 @@ function Snippet({thread}: {thread: AppBskyFeedDefs.ThreadViewPost}) {
|
||||
ref={ref}
|
||||
type="text"
|
||||
value={snippet}
|
||||
className="border rounded-lg py-3 w-full px-4"
|
||||
className="border rounded-lg py-3 w-full px-4 dark:bg-dimmedBg dark:border-slate-500"
|
||||
readOnly
|
||||
autoFocus
|
||||
onFocus={() => {
|
||||
@@ -228,7 +233,7 @@ function Snippet({thread}: {thread: AppBskyFeedDefs.ThreadViewPost}) {
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
className="rounded-lg bg-brand text-white color-white py-3 px-4 whitespace-nowrap min-w-28"
|
||||
className="rounded-lg bg-brand text-white py-3 px-4 whitespace-nowrap min-w-28"
|
||||
onClick={() => {
|
||||
ref.current?.focus()
|
||||
ref.current?.select()
|
||||
|
||||
@@ -4,6 +4,7 @@ import {AppBskyFeedDefs, AtpAgent} from '@atproto/api'
|
||||
import {h, render} from 'preact'
|
||||
|
||||
import logo from '../../assets/logo.svg'
|
||||
import {initColorMode} from '../color-mode'
|
||||
import {Container} from '../components/container'
|
||||
import {Link} from '../components/link'
|
||||
import {Post} from '../components/post'
|
||||
@@ -21,6 +22,8 @@ if (!uri) {
|
||||
throw new Error('No uri in path')
|
||||
}
|
||||
|
||||
initColorMode()
|
||||
|
||||
agent
|
||||
.getPostThread({
|
||||
uri,
|
||||
@@ -55,13 +58,13 @@ function PwiOptOut({thread}: {thread: AppBskyFeedDefs.ThreadViewPost}) {
|
||||
<img src={logo} className="h-6" />
|
||||
</Link>
|
||||
<div className="w-full py-12 gap-4 flex flex-col items-center">
|
||||
<p className="max-w-80 text-center w-full text-textLight">
|
||||
<p className="max-w-80 text-center w-full text-textLight dark:text-textDimmed">
|
||||
The author of this post has requested their posts not be displayed on
|
||||
external sites.
|
||||
</p>
|
||||
<Link
|
||||
href={href}
|
||||
className="max-w-80 rounded-lg bg-brand text-white color-white text-center py-1 px-4 w-full mx-auto">
|
||||
className="max-w-80 rounded-lg bg-brand text-white text-center py-1 px-4 w-full mx-auto">
|
||||
View on Bluesky
|
||||
</Link>
|
||||
</div>
|
||||
@@ -77,7 +80,7 @@ function ErrorMessage() {
|
||||
className="transition-transform hover:scale-110 absolute top-4 right-4">
|
||||
<img src={logo} className="h-6" />
|
||||
</Link>
|
||||
<p className="my-16 text-center w-full text-textLight">
|
||||
<p className="my-16 text-center w-full text-textLight dark:text-textDimmed">
|
||||
Post not found, it may have been deleted.
|
||||
</p>
|
||||
</Container>
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||
darkMode: ['variant', [
|
||||
'&:is(.dark *):not(:is(.dark .light *))',
|
||||
]],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
brand: 'rgb(10,122,255)',
|
||||
brandLighten: 'rgb(32,139,254)',
|
||||
textLight: 'rgb(66,87,108)',
|
||||
textDimmed: 'rgb(174,187,201)',
|
||||
dimmedBgLighten: 'rgb(30,41,54)',
|
||||
dimmedBg: 'rgb(22,30,39)',
|
||||
dimmedBgDarken: 'rgb(18,25,32)',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES5",
|
||||
@@ -20,5 +19,5 @@
|
||||
"jsxFragmentFactory": "Fragment",
|
||||
"downlevelIteration": true
|
||||
},
|
||||
"include": ["src", "vite.config.ts"]
|
||||
"include": ["src", "snippet", "vite.config.ts"]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
+5
-4
@@ -55,6 +55,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "^0.13.18",
|
||||
"@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",
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
## expo-modules-core Patch
|
||||
|
||||
This patch fixes an issue where bitdrift's API stream gets blocked by the Expo interceptor used to power the devtools
|
||||
@@ -0,0 +1,15 @@
|
||||
diff --git a/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt b/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt
|
||||
index 47c4d15..afe138d 100644
|
||||
--- a/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt
|
||||
+++ b/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt
|
||||
@@ -125,6 +125,10 @@ internal fun peekResponseBody(
|
||||
}
|
||||
|
||||
internal fun shouldParseBody(response: Response): Boolean {
|
||||
+ if (response.request.url.encodedPath == "/bitdrift_public.protobuf.client.v1.ApiService/Mux") {
|
||||
+ return false
|
||||
+ }
|
||||
+
|
||||
// Check for Content-Type
|
||||
val skipContentTypes = listOf(
|
||||
"text/event-stream", // Server Sent Events
|
||||
@@ -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'
|
||||
|
||||
+2
-2
@@ -56,8 +56,6 @@ import {PostThreadScreen} from '#/view/screens/PostThread'
|
||||
import {PrivacyPolicyScreen} from '#/view/screens/PrivacyPolicy'
|
||||
import {ProfileScreen} from '#/view/screens/Profile'
|
||||
import {ProfileFeedLikedByScreen} from '#/view/screens/ProfileFeedLikedBy'
|
||||
import {ProfileFollowersScreen} from '#/view/screens/ProfileFollowers'
|
||||
import {ProfileFollowsScreen} from '#/view/screens/ProfileFollows'
|
||||
import {ProfileListScreen} from '#/view/screens/ProfileList'
|
||||
import {SavedFeeds} from '#/view/screens/SavedFeeds'
|
||||
import {SearchScreen} from '#/view/screens/Search'
|
||||
@@ -77,6 +75,8 @@ import {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'
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -48,8 +48,8 @@ export function Outer({
|
||||
a.gap_sm,
|
||||
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}],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {StyleProp, View, ViewStyle} from 'react-native'
|
||||
import {AppBskyFeedDefs, ComAtprotoLabelDefs} from '@atproto/api'
|
||||
import {msg, Plural} from '@lingui/macro'
|
||||
import {msg, Plural, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useSession} from '#/state/session'
|
||||
@@ -50,17 +50,23 @@ export function LabelsOnMe({
|
||||
<ButtonIcon position="left" icon={CircleInfo} />
|
||||
<ButtonText style={[a.leading_snug]}>
|
||||
{type === 'account' ? (
|
||||
<Plural
|
||||
value={labels.length}
|
||||
one="# label has been placed on this account"
|
||||
other="# labels have been placed on this account"
|
||||
/>
|
||||
<Trans>
|
||||
<Plural
|
||||
value={labels.length}
|
||||
one="# label has"
|
||||
other="# labels have"
|
||||
/>{' '}
|
||||
been placed on this account
|
||||
</Trans>
|
||||
) : (
|
||||
<Plural
|
||||
value={labels.length}
|
||||
one="# label has been placed on this content"
|
||||
other="# labels have been placed on this content"
|
||||
/>
|
||||
<Trans>
|
||||
<Plural
|
||||
value={labels.length}
|
||||
one="# label has"
|
||||
other="# labels have"
|
||||
/>{' '}
|
||||
been placed on this content
|
||||
</Trans>
|
||||
)}
|
||||
</ButtonText>
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import {init} from '@bitdrift/react-native'
|
||||
|
||||
const BITDRIFT_API_KEY = process.env.BITDRIFT_API_KEY
|
||||
|
||||
if (BITDRIFT_API_KEY) {
|
||||
init(BITDRIFT_API_KEY, {url: 'https://api-bsky.bitdrift.io'})
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import {
|
||||
debug as bdDebug,
|
||||
error as bdError,
|
||||
info as bdInfo,
|
||||
warn as bdWarn,
|
||||
} from '@bitdrift/react-native'
|
||||
|
||||
import {LogLevel, Transport} from './types'
|
||||
|
||||
export function createBitdriftTransport(): Transport {
|
||||
const logFunctions = {
|
||||
[LogLevel.Debug]: bdDebug,
|
||||
[LogLevel.Info]: bdInfo,
|
||||
[LogLevel.Log]: bdInfo,
|
||||
[LogLevel.Warn]: bdWarn,
|
||||
[LogLevel.Error]: bdError,
|
||||
} as const
|
||||
|
||||
return (level, message) => {
|
||||
const log = logFunctions[level]
|
||||
log(message.toString())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import {Transport} from './index'
|
||||
|
||||
export function createBitdriftTransport(): Transport {
|
||||
return (_level, _message) => {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
+9
-67
@@ -6,74 +6,12 @@ import {DebugContext} from '#/logger/debugContext'
|
||||
import {add} from '#/logger/logDump'
|
||||
import {Sentry} from '#/logger/sentry'
|
||||
import * as env from '#/env'
|
||||
import {createBitdriftTransport} from './bitdriftTransport'
|
||||
import {Metadata} from './types'
|
||||
import {ConsoleTransportEntry, LogLevel, Transport} from './types'
|
||||
|
||||
export enum LogLevel {
|
||||
Debug = 'debug',
|
||||
Info = 'info',
|
||||
Log = 'log',
|
||||
Warn = 'warn',
|
||||
Error = 'error',
|
||||
}
|
||||
|
||||
type Transport = (
|
||||
level: LogLevel,
|
||||
message: string | Error,
|
||||
metadata: Metadata,
|
||||
timestamp: number,
|
||||
) => void
|
||||
|
||||
/**
|
||||
* A union of some of Sentry's breadcrumb properties as well as Sentry's
|
||||
* `captureException` parameter, `CaptureContext`.
|
||||
*/
|
||||
type Metadata = {
|
||||
/**
|
||||
* Applied as Sentry breadcrumb types. Defaults to `default`.
|
||||
*
|
||||
* @see https://develop.sentry.dev/sdk/event-payloads/breadcrumbs/#breadcrumb-types
|
||||
*/
|
||||
type?:
|
||||
| 'default'
|
||||
| 'debug'
|
||||
| 'error'
|
||||
| 'navigation'
|
||||
| 'http'
|
||||
| 'info'
|
||||
| 'query'
|
||||
| 'transaction'
|
||||
| 'ui'
|
||||
| 'user'
|
||||
|
||||
/**
|
||||
* Passed through to `Sentry.captureException`
|
||||
*
|
||||
* @see https://github.com/getsentry/sentry-javascript/blob/903addf9a1a1534a6cb2ba3143654b918a86f6dd/packages/types/src/misc.ts#L65
|
||||
*/
|
||||
tags?: {
|
||||
[key: string]:
|
||||
| number
|
||||
| string
|
||||
| boolean
|
||||
| bigint
|
||||
| symbol
|
||||
| null
|
||||
| undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Any additional data, passed through to Sentry as `extra` param on
|
||||
* exceptions, or the `data` param on breadcrumbs.
|
||||
*/
|
||||
[key: string]: unknown
|
||||
} & Parameters<typeof Sentry.captureException>[1]
|
||||
|
||||
export type ConsoleTransportEntry = {
|
||||
id: string
|
||||
timestamp: number
|
||||
level: LogLevel
|
||||
message: string | Error
|
||||
metadata: Metadata
|
||||
}
|
||||
export {LogLevel}
|
||||
export type {ConsoleTransportEntry, Transport}
|
||||
|
||||
const enabledLogLevels: {
|
||||
[key in LogLevel]: LogLevel[]
|
||||
@@ -328,6 +266,10 @@ export class Logger {
|
||||
*/
|
||||
export const logger = new Logger()
|
||||
|
||||
if (!env.IS_TEST) {
|
||||
logger.addTransport(createBitdriftTransport())
|
||||
}
|
||||
|
||||
if (env.IS_DEV && !env.IS_TEST) {
|
||||
logger.addTransport(consoleTransport)
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import type {Sentry} from '#/logger/sentry'
|
||||
|
||||
export enum LogLevel {
|
||||
Debug = 'debug',
|
||||
Info = 'info',
|
||||
Log = 'log',
|
||||
Warn = 'warn',
|
||||
Error = 'error',
|
||||
}
|
||||
|
||||
export type Transport = (
|
||||
level: LogLevel,
|
||||
message: string | Error,
|
||||
metadata: Metadata,
|
||||
timestamp: number,
|
||||
) => void
|
||||
|
||||
/**
|
||||
* A union of some of Sentry's breadcrumb properties as well as Sentry's
|
||||
* `captureException` parameter, `CaptureContext`.
|
||||
*/
|
||||
export type Metadata = {
|
||||
/**
|
||||
* Applied as Sentry breadcrumb types. Defaults to `default`.
|
||||
*
|
||||
* @see https://develop.sentry.dev/sdk/event-payloads/breadcrumbs/#breadcrumb-types
|
||||
*/
|
||||
type?:
|
||||
| 'default'
|
||||
| 'debug'
|
||||
| 'error'
|
||||
| 'navigation'
|
||||
| 'http'
|
||||
| 'info'
|
||||
| 'query'
|
||||
| 'transaction'
|
||||
| 'ui'
|
||||
| 'user'
|
||||
|
||||
/**
|
||||
* Passed through to `Sentry.captureException`
|
||||
*
|
||||
* @see https://github.com/getsentry/sentry-javascript/blob/903addf9a1a1534a6cb2ba3143654b918a86f6dd/packages/types/src/misc.ts#L65
|
||||
*/
|
||||
tags?: {
|
||||
[key: string]:
|
||||
| number
|
||||
| string
|
||||
| boolean
|
||||
| bigint
|
||||
| symbol
|
||||
| null
|
||||
| undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Any additional data, passed through to Sentry as `extra` param on
|
||||
* exceptions, or the `data` param on breadcrumbs.
|
||||
*/
|
||||
[key: string]: unknown
|
||||
} & Parameters<typeof Sentry.captureException>[1]
|
||||
|
||||
export type ConsoleTransportEntry = {
|
||||
id: string
|
||||
timestamp: number
|
||||
level: LogLevel
|
||||
message: string | Error
|
||||
metadata: Metadata
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
import React from 'react'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Plural, Trans} from '@lingui/macro'
|
||||
import {useFocusEffect} from '@react-navigation/native'
|
||||
|
||||
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
|
||||
import {makeRecordUri} from '#/lib/strings/url-helpers'
|
||||
import {usePostThreadQuery} from '#/state/queries/post-thread'
|
||||
import {useSetMinimalShellMode} from '#/state/shell'
|
||||
import {PostLikedBy as PostLikedByComponent} from '#/view/com/post-thread/PostLikedBy'
|
||||
import {ViewHeader} from '#/view/com/util/ViewHeader'
|
||||
import * as Layout from '#/components/Layout'
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostLikedBy'>
|
||||
@@ -15,7 +14,12 @@ export const PostLikedByScreen = ({route}: Props) => {
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const {name, rkey} = route.params
|
||||
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
|
||||
const {_} = useLingui()
|
||||
const {data: post} = usePostThreadQuery(uri)
|
||||
|
||||
let likeCount
|
||||
if (post?.thread.type === 'post') {
|
||||
likeCount = post.thread.post.likeCount
|
||||
}
|
||||
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
@@ -25,7 +29,22 @@ export const PostLikedByScreen = ({route}: Props) => {
|
||||
|
||||
return (
|
||||
<Layout.Screen>
|
||||
<ViewHeader title={_(msg`Liked By`)} />
|
||||
<Layout.Header.Outer>
|
||||
<Layout.Header.BackButton />
|
||||
<Layout.Header.Content>
|
||||
{post && (
|
||||
<>
|
||||
<Layout.Header.TitleText>
|
||||
<Trans>Liked By</Trans>
|
||||
</Layout.Header.TitleText>
|
||||
<Layout.Header.SubtitleText>
|
||||
<Plural value={likeCount ?? 0} one="# like" other="# likes" />
|
||||
</Layout.Header.SubtitleText>
|
||||
</>
|
||||
)}
|
||||
</Layout.Header.Content>
|
||||
<Layout.Header.Slot />
|
||||
</Layout.Header.Outer>
|
||||
<PostLikedByComponent uri={uri} />
|
||||
</Layout.Screen>
|
||||
)
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import React from 'react'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Plural, Trans} from '@lingui/macro'
|
||||
import {useFocusEffect} from '@react-navigation/native'
|
||||
|
||||
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
|
||||
import {makeRecordUri} from '#/lib/strings/url-helpers'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {usePostThreadQuery} from '#/state/queries/post-thread'
|
||||
import {useSetMinimalShellMode} from '#/state/shell'
|
||||
import {PostQuotes as PostQuotesComponent} from '#/view/com/post-thread/PostQuotes'
|
||||
import {ViewHeader} from '#/view/com/util/ViewHeader'
|
||||
import {CenteredView} from '#/view/com/util/Views'
|
||||
import * as Layout from '#/components/Layout'
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostQuotes'>
|
||||
@@ -17,7 +14,12 @@ export const PostQuotesScreen = ({route}: Props) => {
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const {name, rkey} = route.params
|
||||
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
|
||||
const {_} = useLingui()
|
||||
const {data: post} = usePostThreadQuery(uri)
|
||||
|
||||
let quoteCount
|
||||
if (post?.thread.type === 'post') {
|
||||
quoteCount = post.thread.post.quoteCount
|
||||
}
|
||||
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
@@ -27,10 +29,27 @@ export const PostQuotesScreen = ({route}: Props) => {
|
||||
|
||||
return (
|
||||
<Layout.Screen>
|
||||
<CenteredView sideBorders={true}>
|
||||
<ViewHeader title={_(msg`Quotes`)} showBorder={!isWeb} />
|
||||
<PostQuotesComponent uri={uri} />
|
||||
</CenteredView>
|
||||
<Layout.Header.Outer>
|
||||
<Layout.Header.BackButton />
|
||||
<Layout.Header.Content>
|
||||
{post && (
|
||||
<>
|
||||
<Layout.Header.TitleText>
|
||||
<Trans>Quotes</Trans>
|
||||
</Layout.Header.TitleText>
|
||||
<Layout.Header.SubtitleText>
|
||||
<Plural
|
||||
value={quoteCount ?? 0}
|
||||
one="# quote"
|
||||
other="# quotes"
|
||||
/>
|
||||
</Layout.Header.SubtitleText>
|
||||
</>
|
||||
)}
|
||||
</Layout.Header.Content>
|
||||
<Layout.Header.Slot />
|
||||
</Layout.Header.Outer>
|
||||
<PostQuotesComponent uri={uri} />
|
||||
</Layout.Screen>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import React from 'react'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Plural, Trans} from '@lingui/macro'
|
||||
import {useFocusEffect} from '@react-navigation/native'
|
||||
|
||||
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
|
||||
import {makeRecordUri} from '#/lib/strings/url-helpers'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {usePostThreadQuery} from '#/state/queries/post-thread'
|
||||
import {useSetMinimalShellMode} from '#/state/shell'
|
||||
import {PostRepostedBy as PostRepostedByComponent} from '#/view/com/post-thread/PostRepostedBy'
|
||||
import {ViewHeader} from '#/view/com/util/ViewHeader'
|
||||
import {CenteredView} from '#/view/com/util/Views'
|
||||
import * as Layout from '#/components/Layout'
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostRepostedBy'>
|
||||
@@ -17,7 +14,12 @@ export const PostRepostedByScreen = ({route}: Props) => {
|
||||
const {name, rkey} = route.params
|
||||
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const {_} = useLingui()
|
||||
const {data: post} = usePostThreadQuery(uri)
|
||||
|
||||
let quoteCount
|
||||
if (post?.thread.type === 'post') {
|
||||
quoteCount = post.thread.post.repostCount
|
||||
}
|
||||
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
@@ -27,10 +29,27 @@ export const PostRepostedByScreen = ({route}: Props) => {
|
||||
|
||||
return (
|
||||
<Layout.Screen>
|
||||
<CenteredView sideBorders={true}>
|
||||
<ViewHeader title={_(msg`Reposted By`)} showBorder={!isWeb} />
|
||||
<PostRepostedByComponent uri={uri} />
|
||||
</CenteredView>
|
||||
<Layout.Header.Outer>
|
||||
<Layout.Header.BackButton />
|
||||
<Layout.Header.Content>
|
||||
{post && (
|
||||
<>
|
||||
<Layout.Header.TitleText>
|
||||
<Trans>Reposted By</Trans>
|
||||
</Layout.Header.TitleText>
|
||||
<Layout.Header.SubtitleText>
|
||||
<Plural
|
||||
value={quoteCount ?? 0}
|
||||
one="# repost"
|
||||
other="# reposts"
|
||||
/>
|
||||
</Layout.Header.SubtitleText>
|
||||
</>
|
||||
)}
|
||||
</Layout.Header.Content>
|
||||
<Layout.Header.Slot />
|
||||
</Layout.Header.Outer>
|
||||
<PostRepostedByComponent uri={uri} />
|
||||
</Layout.Screen>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import React from 'react'
|
||||
import {Plural} from '@lingui/macro'
|
||||
import {useFocusEffect} from '@react-navigation/native'
|
||||
|
||||
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
|
||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||
import {useProfileQuery} from '#/state/queries/profile'
|
||||
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
|
||||
import {useSetMinimalShellMode} from '#/state/shell'
|
||||
import {ProfileFollowers as ProfileFollowersComponent} from '#/view/com/profile/ProfileFollowers'
|
||||
import * as Layout from '#/components/Layout'
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFollowers'>
|
||||
export const ProfileFollowersScreen = ({route}: Props) => {
|
||||
const {name} = route.params
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
|
||||
const {data: resolvedDid} = useResolveDidQuery(name)
|
||||
const {data: profile} = useProfileQuery({
|
||||
did: resolvedDid,
|
||||
})
|
||||
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
setMinimalShellMode(false)
|
||||
}, [setMinimalShellMode]),
|
||||
)
|
||||
|
||||
return (
|
||||
<Layout.Screen testID="profileFollowersScreen">
|
||||
<Layout.Header.Outer>
|
||||
<Layout.Header.BackButton />
|
||||
<Layout.Header.Content>
|
||||
{profile && (
|
||||
<>
|
||||
<Layout.Header.TitleText>
|
||||
{sanitizeDisplayName(profile.displayName || profile.handle)}
|
||||
</Layout.Header.TitleText>
|
||||
<Layout.Header.SubtitleText>
|
||||
<Plural
|
||||
value={profile.followersCount ?? 0}
|
||||
one="# follower"
|
||||
other="# followers"
|
||||
/>
|
||||
</Layout.Header.SubtitleText>
|
||||
</>
|
||||
)}
|
||||
</Layout.Header.Content>
|
||||
<Layout.Header.Slot />
|
||||
</Layout.Header.Outer>
|
||||
<ProfileFollowersComponent name={name} />
|
||||
</Layout.Screen>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import React from 'react'
|
||||
import {Plural} from '@lingui/macro'
|
||||
import {useFocusEffect} from '@react-navigation/native'
|
||||
|
||||
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
|
||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||
import {useProfileQuery} from '#/state/queries/profile'
|
||||
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
|
||||
import {useSetMinimalShellMode} from '#/state/shell'
|
||||
import {ProfileFollows as ProfileFollowsComponent} from '#/view/com/profile/ProfileFollows'
|
||||
import * as Layout from '#/components/Layout'
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFollows'>
|
||||
export const ProfileFollowsScreen = ({route}: Props) => {
|
||||
const {name} = route.params
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
|
||||
const {data: resolvedDid} = useResolveDidQuery(name)
|
||||
const {data: profile} = useProfileQuery({
|
||||
did: resolvedDid,
|
||||
})
|
||||
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
setMinimalShellMode(false)
|
||||
}, [setMinimalShellMode]),
|
||||
)
|
||||
|
||||
return (
|
||||
<Layout.Screen testID="profileFollowsScreen">
|
||||
<Layout.Header.Outer>
|
||||
<Layout.Header.BackButton />
|
||||
<Layout.Header.Content>
|
||||
{profile && (
|
||||
<>
|
||||
<Layout.Header.TitleText>
|
||||
{sanitizeDisplayName(profile.displayName || profile.handle)}
|
||||
</Layout.Header.TitleText>
|
||||
<Layout.Header.SubtitleText>
|
||||
<Plural
|
||||
value={profile.followersCount ?? 0}
|
||||
one="# following"
|
||||
other="# following"
|
||||
/>
|
||||
</Layout.Header.SubtitleText>
|
||||
</>
|
||||
)}
|
||||
</Layout.Header.Content>
|
||||
<Layout.Header.Slot />
|
||||
</Layout.Header.Outer>
|
||||
<ProfileFollowsComponent name={name} />
|
||||
</Layout.Screen>
|
||||
)
|
||||
}
|
||||
@@ -458,11 +458,9 @@ function DialogInner({
|
||||
to={makeCustomFeedLink(info.creatorDid, feedRkey, 'liked-by')}
|
||||
style={[a.underline, t.atoms.text_contrast_medium]}
|
||||
onPress={() => control.close()}>
|
||||
<Plural
|
||||
value={likeCount}
|
||||
one="Liked by # user"
|
||||
other="Liked by # users"
|
||||
/>
|
||||
<Trans>
|
||||
Liked by <Plural value={likeCount} one="# user" other="# users" />
|
||||
</Trans>
|
||||
</InlineLinkText>
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -1,260 +0,0 @@
|
||||
import React from 'react'
|
||||
import {Alert, View} from 'react-native'
|
||||
import {Image} from 'expo-image'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import * as AppIcon from '@mozzius/expo-dynamic-app-icon'
|
||||
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||
|
||||
import {PressableScale} from '#/lib/custom-animations/PressableScale'
|
||||
import {CommonNavigatorParams} from '#/lib/routes/types'
|
||||
import {isAndroid} from '#/platform/detection'
|
||||
import {atoms as a, platform} from '#/alf'
|
||||
import * as Layout from '#/components/Layout'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppIconSettings'>
|
||||
export function AppIconSettingsScreen({}: Props) {
|
||||
const {_} = useLingui()
|
||||
const sets = useAppIconSets()
|
||||
|
||||
return (
|
||||
<Layout.Screen>
|
||||
<Layout.Header.Outer>
|
||||
<Layout.Header.BackButton />
|
||||
<Layout.Header.Content>
|
||||
<Layout.Header.TitleText>
|
||||
<Trans>App Icon</Trans>
|
||||
</Layout.Header.TitleText>
|
||||
</Layout.Header.Content>
|
||||
<Layout.Header.Slot />
|
||||
</Layout.Header.Outer>
|
||||
<Layout.Content
|
||||
contentContainerStyle={[a.py_2xl, a.px_xl, {paddingBottom: 100}]}>
|
||||
<Text style={[a.text_lg, a.font_heavy]}>Defaults</Text>
|
||||
<View style={[a.flex_row, a.flex_wrap]}>
|
||||
{sets.defaults.map(icon => (
|
||||
<View
|
||||
style={[{width: '50%'}, a.py_lg, a.px_xs, a.align_center]}
|
||||
key={icon.id}>
|
||||
<PressableScale
|
||||
accessibilityLabel={icon.name}
|
||||
accessibilityHint={_(msg`Tap to change app icon`)}
|
||||
targetScale={0.95}
|
||||
onPress={() => AppIcon.setAppIcon(icon.id)}>
|
||||
<Image
|
||||
source={platform({
|
||||
ios: icon.iosImage(),
|
||||
android: icon.androidImage(),
|
||||
})}
|
||||
style={[
|
||||
{width: 100, height: 100},
|
||||
platform({
|
||||
ios: {borderRadius: 20},
|
||||
android: a.rounded_full,
|
||||
}),
|
||||
a.curve_continuous,
|
||||
]}
|
||||
accessibilityIgnoresInvertColors
|
||||
/>
|
||||
</PressableScale>
|
||||
<Text style={[a.text_center, a.font_bold, a.text_md, a.mt_md]}>
|
||||
{icon.name}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<Text style={[a.text_lg, a.font_heavy]}>Bluesky+</Text>
|
||||
<View style={[a.flex_row, a.flex_wrap]}>
|
||||
{sets.core.map(icon => (
|
||||
<View
|
||||
style={[{width: '50%'}, a.py_lg, a.px_xs, a.align_center]}
|
||||
key={icon.id}>
|
||||
<PressableScale
|
||||
accessibilityLabel={icon.name}
|
||||
accessibilityHint={_(msg`Tap to change app icon`)}
|
||||
targetScale={0.95}
|
||||
onPress={() => {
|
||||
if (isAndroid) {
|
||||
Alert.alert(
|
||||
_(msg`Change app icon to "${icon.name}"`),
|
||||
_(msg`The app will be restarted`),
|
||||
[
|
||||
{
|
||||
text: _(msg`Cancel`),
|
||||
style: 'cancel',
|
||||
},
|
||||
{
|
||||
text: _(msg`OK`),
|
||||
onPress: () => {
|
||||
AppIcon.setAppIcon(icon.id)
|
||||
},
|
||||
style: 'default',
|
||||
},
|
||||
],
|
||||
)
|
||||
} else {
|
||||
AppIcon.setAppIcon(icon.id)
|
||||
}
|
||||
}}>
|
||||
<Image
|
||||
source={platform({
|
||||
ios: icon.iosImage(),
|
||||
android: icon.androidImage(),
|
||||
})}
|
||||
style={[
|
||||
{width: 100, height: 100},
|
||||
platform({
|
||||
ios: {borderRadius: 20},
|
||||
android: a.rounded_full,
|
||||
}),
|
||||
a.curve_continuous,
|
||||
a.shadow_lg,
|
||||
]}
|
||||
accessibilityIgnoresInvertColors
|
||||
/>
|
||||
</PressableScale>
|
||||
<Text
|
||||
style={[a.text_center, a.font_bold, a.text_md, a.mt_md]}
|
||||
// for Classic™
|
||||
emoji>
|
||||
{icon.name}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</Layout.Content>
|
||||
</Layout.Screen>
|
||||
)
|
||||
}
|
||||
|
||||
function useAppIconSets() {
|
||||
const {_} = useLingui()
|
||||
|
||||
return React.useMemo(() => {
|
||||
const defaults = [
|
||||
{
|
||||
id: 'default_light',
|
||||
name: _('Light'),
|
||||
iosImage: () => {
|
||||
return require(`../../../assets/app-icons/ios_icon_default_light.png`)
|
||||
},
|
||||
androidImage: () => {
|
||||
return require(`../../../assets/app-icons/android_icon_default_light.png`)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'default_dark',
|
||||
name: _('Dark'),
|
||||
iosImage: () => {
|
||||
return require(`../../../assets/app-icons/ios_icon_default_dark.png`)
|
||||
},
|
||||
androidImage: () => {
|
||||
return require(`../../../assets/app-icons/android_icon_default_dark.png`)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* Bluesky+
|
||||
*/
|
||||
const core = [
|
||||
{
|
||||
id: 'core_aurora',
|
||||
name: _('Aurora'),
|
||||
iosImage: () => {
|
||||
return require(`../../../assets/app-icons/ios_icon_core_aurora.png`)
|
||||
},
|
||||
androidImage: () => {
|
||||
return require(`../../../assets/app-icons/android_icon_core_aurora.png`)
|
||||
},
|
||||
},
|
||||
// {
|
||||
// id: 'core_bonfire',
|
||||
// name: _('Bonfire'),
|
||||
// iosImage: () => {
|
||||
// return require(`../../../assets/app-icons/ios_icon_core_bonfire.png`)
|
||||
// },
|
||||
// androidImage: () => {
|
||||
// return require(`../../../assets/app-icons/android_icon_core_bonfire.png`)
|
||||
// },
|
||||
// },
|
||||
{
|
||||
id: 'core_sunrise',
|
||||
name: _('Sunrise'),
|
||||
iosImage: () => {
|
||||
return require(`../../../assets/app-icons/ios_icon_core_sunrise.png`)
|
||||
},
|
||||
androidImage: () => {
|
||||
return require(`../../../assets/app-icons/android_icon_core_sunrise.png`)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'core_sunset',
|
||||
name: _('Sunset'),
|
||||
iosImage: () => {
|
||||
return require(`../../../assets/app-icons/ios_icon_core_sunset.png`)
|
||||
},
|
||||
androidImage: () => {
|
||||
return require(`../../../assets/app-icons/android_icon_core_sunset.png`)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'core_midnight',
|
||||
name: _('Midnight'),
|
||||
iosImage: () => {
|
||||
return require(`../../../assets/app-icons/ios_icon_core_midnight.png`)
|
||||
},
|
||||
androidImage: () => {
|
||||
return require(`../../../assets/app-icons/android_icon_core_midnight.png`)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'core_flat_blue',
|
||||
name: _('Flat Blue'),
|
||||
iosImage: () => {
|
||||
return require(`../../../assets/app-icons/ios_icon_core_flat_blue.png`)
|
||||
},
|
||||
androidImage: () => {
|
||||
return require(`../../../assets/app-icons/android_icon_core_flat_blue.png`)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'core_flat_white',
|
||||
name: _('Flat White'),
|
||||
iosImage: () => {
|
||||
return require(`../../../assets/app-icons/ios_icon_core_flat_white.png`)
|
||||
},
|
||||
androidImage: () => {
|
||||
return require(`../../../assets/app-icons/android_icon_core_flat_white.png`)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'core_flat_black',
|
||||
name: _('Flat Black'),
|
||||
iosImage: () => {
|
||||
return require(`../../../assets/app-icons/ios_icon_core_flat_black.png`)
|
||||
},
|
||||
androidImage: () => {
|
||||
return require(`../../../assets/app-icons/android_icon_core_flat_black.png`)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'core_classic',
|
||||
name: _('Bluesky Classic™'),
|
||||
iosImage: () => {
|
||||
return require(`../../../assets/app-icons/ios_icon_core_classic.png`)
|
||||
},
|
||||
androidImage: () => {
|
||||
return require(`../../../assets/app-icons/android_icon_core_classic.png`)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
return {
|
||||
defaults,
|
||||
core,
|
||||
}
|
||||
}, [_])
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import {Image} from 'expo-image'
|
||||
|
||||
import {AppIconSet} from '#/screens/Settings/AppIconSettings/types'
|
||||
import {atoms as a, platform, useTheme} from '#/alf'
|
||||
|
||||
export function AppIconImage({
|
||||
icon,
|
||||
size = 50,
|
||||
}: {
|
||||
icon: AppIconSet
|
||||
size: number
|
||||
}) {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<Image
|
||||
source={platform({
|
||||
ios: icon.iosImage(),
|
||||
android: icon.androidImage(),
|
||||
})}
|
||||
style={[
|
||||
{width: size, height: size},
|
||||
platform({
|
||||
ios: {borderRadius: size / 5},
|
||||
android: a.rounded_full,
|
||||
}),
|
||||
a.curve_continuous,
|
||||
t.atoms.border_contrast_medium,
|
||||
a.border,
|
||||
]}
|
||||
accessibilityIgnoresInvertColors
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import {View} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {AppIconImage} from '#/screens/Settings/AppIconSettings/AppIconImage'
|
||||
import {useCurrentAppIcon} from '#/screens/Settings/AppIconSettings/useCurrentAppIcon'
|
||||
import * as SettingsList from '#/screens/Settings/components/SettingsList'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {Shapes_Stroke2_Corner0_Rounded as Shapes} from '#/components/icons/Shapes'
|
||||
|
||||
export function SettingsListItem() {
|
||||
const {_} = useLingui()
|
||||
const icon = useCurrentAppIcon()
|
||||
|
||||
return (
|
||||
<SettingsList.LinkItem
|
||||
to="/settings/app-icon"
|
||||
label={_(msg`App Icon`)}
|
||||
contentContainerStyle={[a.align_start]}>
|
||||
<SettingsList.ItemIcon icon={Shapes} />
|
||||
<View style={[a.flex_1]}>
|
||||
<SettingsList.ItemText style={[a.pt_xs, a.pb_md]}>
|
||||
<Trans>App Icon</Trans>
|
||||
</SettingsList.ItemText>
|
||||
<AppIconImage icon={icon} size={60} />
|
||||
</View>
|
||||
</SettingsList.LinkItem>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export function SettingsListItem() {}
|
||||
@@ -0,0 +1,244 @@
|
||||
import {useState} from 'react'
|
||||
import {Alert, View} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import * as DynamicAppIcon from '@mozzius/expo-dynamic-app-icon'
|
||||
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||
|
||||
import {DISCOVER_DEBUG_DIDS} from '#/lib/constants'
|
||||
import {PressableScale} from '#/lib/custom-animations/PressableScale'
|
||||
import {CommonNavigatorParams} from '#/lib/routes/types'
|
||||
import {isAndroid} from '#/platform/detection'
|
||||
import {useSession} from '#/state/session'
|
||||
import {AppIconImage} from '#/screens/Settings/AppIconSettings/AppIconImage'
|
||||
import {AppIconSet} from '#/screens/Settings/AppIconSettings/types'
|
||||
import {useAppIconSets} from '#/screens/Settings/AppIconSettings/useAppIconSets'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import * as Toggle from '#/components/forms/Toggle'
|
||||
import * as Layout from '#/components/Layout'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppIconSettings'>
|
||||
export function AppIconSettingsScreen({}: Props) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const sets = useAppIconSets()
|
||||
const {currentAccount} = useSession()
|
||||
const [currentAppIcon, setCurrentAppIcon] = useState(() =>
|
||||
getAppIconName(DynamicAppIcon.getAppIcon()),
|
||||
)
|
||||
|
||||
const onSetAppIcon = (icon: string) => {
|
||||
if (isAndroid) {
|
||||
const next =
|
||||
sets.defaults.find(i => i.id === icon) ??
|
||||
sets.core.find(i => i.id === icon)
|
||||
Alert.alert(
|
||||
next
|
||||
? _(msg`Change app icon to "${next.name}"`)
|
||||
: _(msg`Change app icon`),
|
||||
// to determine - can we stop this happening? -sfn
|
||||
_(msg`The app will be restarted`),
|
||||
[
|
||||
{
|
||||
text: _(msg`Cancel`),
|
||||
style: 'cancel',
|
||||
},
|
||||
{
|
||||
text: _(msg`OK`),
|
||||
onPress: () => {
|
||||
setCurrentAppIcon(setAppIcon(icon))
|
||||
},
|
||||
style: 'default',
|
||||
},
|
||||
],
|
||||
)
|
||||
} else {
|
||||
setCurrentAppIcon(setAppIcon(icon))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout.Screen>
|
||||
<Layout.Header.Outer>
|
||||
<Layout.Header.BackButton />
|
||||
<Layout.Header.Content>
|
||||
<Layout.Header.TitleText>
|
||||
<Trans>App Icon</Trans>
|
||||
</Layout.Header.TitleText>
|
||||
</Layout.Header.Content>
|
||||
<Layout.Header.Slot />
|
||||
</Layout.Header.Outer>
|
||||
|
||||
<Layout.Content contentContainerStyle={[a.p_lg]}>
|
||||
<Group
|
||||
label={_(msg`Default icons`)}
|
||||
value={currentAppIcon}
|
||||
onChange={onSetAppIcon}>
|
||||
{sets.defaults.map((icon, i) => (
|
||||
<Row
|
||||
key={icon.id}
|
||||
icon={icon}
|
||||
isEnd={i === sets.defaults.length - 1}>
|
||||
<AppIcon icon={icon} key={icon.id} size={40} />
|
||||
<RowText>{icon.name}</RowText>
|
||||
</Row>
|
||||
))}
|
||||
</Group>
|
||||
|
||||
{DISCOVER_DEBUG_DIDS[currentAccount?.did ?? ''] && (
|
||||
<>
|
||||
<Text
|
||||
style={[
|
||||
a.text_md,
|
||||
a.mt_xl,
|
||||
a.mb_sm,
|
||||
a.font_bold,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}>
|
||||
<Trans>Bluesky+</Trans>
|
||||
</Text>
|
||||
<Group
|
||||
label={_(msg`Bluesky+ icons`)}
|
||||
value={currentAppIcon}
|
||||
onChange={onSetAppIcon}>
|
||||
{sets.core.map((icon, i) => (
|
||||
<Row
|
||||
key={icon.id}
|
||||
icon={icon}
|
||||
isEnd={i === sets.core.length - 1}>
|
||||
<AppIcon icon={icon} key={icon.id} size={40} />
|
||||
<RowText>{icon.name}</RowText>
|
||||
</Row>
|
||||
))}
|
||||
</Group>
|
||||
</>
|
||||
)}
|
||||
</Layout.Content>
|
||||
</Layout.Screen>
|
||||
)
|
||||
}
|
||||
|
||||
function setAppIcon(icon: string) {
|
||||
if (icon === 'default_light') {
|
||||
return getAppIconName(DynamicAppIcon.setAppIcon(null))
|
||||
} else {
|
||||
return getAppIconName(DynamicAppIcon.setAppIcon(icon))
|
||||
}
|
||||
}
|
||||
|
||||
function getAppIconName(icon: string | false) {
|
||||
if (!icon || icon === 'DEFAULT') {
|
||||
return 'default_light'
|
||||
} else {
|
||||
return icon
|
||||
}
|
||||
}
|
||||
|
||||
function Group({
|
||||
children,
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
label: string
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
}) {
|
||||
return (
|
||||
<Toggle.Group
|
||||
type="radio"
|
||||
label={label}
|
||||
values={[value]}
|
||||
maxSelections={1}
|
||||
onChange={vals => {
|
||||
if (vals[0]) onChange(vals[0])
|
||||
}}>
|
||||
<View style={[a.flex_1, a.rounded_md, a.overflow_hidden]}>
|
||||
{children}
|
||||
</View>
|
||||
</Toggle.Group>
|
||||
)
|
||||
}
|
||||
|
||||
function Row({
|
||||
icon,
|
||||
children,
|
||||
isEnd,
|
||||
}: {
|
||||
icon: AppIconSet
|
||||
children: React.ReactNode
|
||||
isEnd: boolean
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
|
||||
return (
|
||||
<Toggle.Item label={_(msg`Set app icon to ${icon.name}`)} name={icon.id}>
|
||||
{({hovered, pressed}) => (
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.p_md,
|
||||
a.flex_row,
|
||||
a.gap_md,
|
||||
a.align_center,
|
||||
t.atoms.bg_contrast_25,
|
||||
(hovered || pressed) && t.atoms.bg_contrast_50,
|
||||
t.atoms.border_contrast_high,
|
||||
!isEnd && a.border_b,
|
||||
]}>
|
||||
{children}
|
||||
<Toggle.Radio />
|
||||
</View>
|
||||
)}
|
||||
</Toggle.Item>
|
||||
)
|
||||
}
|
||||
|
||||
function RowText({children}: {children: React.ReactNode}) {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<Text
|
||||
style={[a.text_md, a.font_bold, a.flex_1, t.atoms.text_contrast_medium]}
|
||||
emoji>
|
||||
{children}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
function AppIcon({icon, size = 50}: {icon: AppIconSet; size: number}) {
|
||||
const {_} = useLingui()
|
||||
return (
|
||||
<PressableScale
|
||||
accessibilityLabel={icon.name}
|
||||
accessibilityHint={_(msg`Tap to change app icon`)}
|
||||
targetScale={0.95}
|
||||
onPress={() => {
|
||||
if (isAndroid) {
|
||||
Alert.alert(
|
||||
_(msg`Change app icon to "${icon.name}"`),
|
||||
_(msg`The app will be restarted`),
|
||||
[
|
||||
{
|
||||
text: _(msg`Cancel`),
|
||||
style: 'cancel',
|
||||
},
|
||||
{
|
||||
text: _(msg`OK`),
|
||||
onPress: () => {
|
||||
DynamicAppIcon.setAppIcon(icon.id)
|
||||
},
|
||||
style: 'default',
|
||||
},
|
||||
],
|
||||
)
|
||||
} else {
|
||||
DynamicAppIcon.setAppIcon(icon.id)
|
||||
}
|
||||
}}>
|
||||
<AppIconImage icon={icon} size={size} />
|
||||
</PressableScale>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import {ImageSourcePropType} from 'react-native'
|
||||
|
||||
export type AppIconSet = {
|
||||
id: string
|
||||
name: string
|
||||
iosImage: () => ImageSourcePropType
|
||||
androidImage: () => ImageSourcePropType
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import {useMemo} from 'react'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {AppIconSet} from '#/screens/Settings/AppIconSettings/types'
|
||||
|
||||
export function useAppIconSets() {
|
||||
const {_} = useLingui()
|
||||
|
||||
return useMemo(() => {
|
||||
const defaults = [
|
||||
{
|
||||
id: 'default_light',
|
||||
name: _('Light'),
|
||||
iosImage: () => {
|
||||
return require(`../../../../assets/app-icons/ios_icon_default_light.png`)
|
||||
},
|
||||
androidImage: () => {
|
||||
return require(`../../../../assets/app-icons/android_icon_default_light.png`)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'default_dark',
|
||||
name: _('Dark'),
|
||||
iosImage: () => {
|
||||
return require(`../../../../assets/app-icons/ios_icon_default_dark.png`)
|
||||
},
|
||||
androidImage: () => {
|
||||
return require(`../../../../assets/app-icons/android_icon_default_dark.png`)
|
||||
},
|
||||
},
|
||||
] satisfies AppIconSet[]
|
||||
|
||||
/**
|
||||
* Bluesky+
|
||||
*/
|
||||
const core = [
|
||||
{
|
||||
id: 'core_aurora',
|
||||
name: _('Aurora'),
|
||||
iosImage: () => {
|
||||
return require(`../../../../assets/app-icons/ios_icon_core_aurora.png`)
|
||||
},
|
||||
androidImage: () => {
|
||||
return require(`../../../../assets/app-icons/android_icon_core_aurora.png`)
|
||||
},
|
||||
},
|
||||
// {
|
||||
// id: 'core_bonfire',
|
||||
// name: _('Bonfire'),
|
||||
// iosImage: () => {
|
||||
// return require(`../../../../assets/app-icons/ios_icon_core_bonfire.png`)
|
||||
// },
|
||||
// androidImage: () => {
|
||||
// return require(`../../../../assets/app-icons/android_icon_core_bonfire.png`)
|
||||
// },
|
||||
// },
|
||||
{
|
||||
id: 'core_sunrise',
|
||||
name: _('Sunrise'),
|
||||
iosImage: () => {
|
||||
return require(`../../../../assets/app-icons/ios_icon_core_sunrise.png`)
|
||||
},
|
||||
androidImage: () => {
|
||||
return require(`../../../../assets/app-icons/android_icon_core_sunrise.png`)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'core_sunset',
|
||||
name: _('Sunset'),
|
||||
iosImage: () => {
|
||||
return require(`../../../../assets/app-icons/ios_icon_core_sunset.png`)
|
||||
},
|
||||
androidImage: () => {
|
||||
return require(`../../../../assets/app-icons/android_icon_core_sunset.png`)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'core_midnight',
|
||||
name: _('Midnight'),
|
||||
iosImage: () => {
|
||||
return require(`../../../../assets/app-icons/ios_icon_core_midnight.png`)
|
||||
},
|
||||
androidImage: () => {
|
||||
return require(`../../../../assets/app-icons/android_icon_core_midnight.png`)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'core_flat_blue',
|
||||
name: _('Flat Blue'),
|
||||
iosImage: () => {
|
||||
return require(`../../../../assets/app-icons/ios_icon_core_flat_blue.png`)
|
||||
},
|
||||
androidImage: () => {
|
||||
return require(`../../../../assets/app-icons/android_icon_core_flat_blue.png`)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'core_flat_white',
|
||||
name: _('Flat White'),
|
||||
iosImage: () => {
|
||||
return require(`../../../../assets/app-icons/ios_icon_core_flat_white.png`)
|
||||
},
|
||||
androidImage: () => {
|
||||
return require(`../../../../assets/app-icons/android_icon_core_flat_white.png`)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'core_flat_black',
|
||||
name: _('Flat Black'),
|
||||
iosImage: () => {
|
||||
return require(`../../../../assets/app-icons/ios_icon_core_flat_black.png`)
|
||||
},
|
||||
androidImage: () => {
|
||||
return require(`../../../../assets/app-icons/android_icon_core_flat_black.png`)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'core_classic',
|
||||
name: _('Bluesky Classic™'),
|
||||
iosImage: () => {
|
||||
return require(`../../../../assets/app-icons/ios_icon_core_classic.png`)
|
||||
},
|
||||
androidImage: () => {
|
||||
return require(`../../../../assets/app-icons/android_icon_core_classic.png`)
|
||||
},
|
||||
},
|
||||
] satisfies AppIconSet[]
|
||||
|
||||
return {
|
||||
defaults,
|
||||
core,
|
||||
}
|
||||
}, [_])
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import {useCallback, useMemo, useState} from 'react'
|
||||
import * as DynamicAppIcon from '@mozzius/expo-dynamic-app-icon'
|
||||
import {useFocusEffect} from '@react-navigation/native'
|
||||
|
||||
import {useAppIconSets} from '#/screens/Settings/AppIconSettings/useAppIconSets'
|
||||
|
||||
export function useCurrentAppIcon() {
|
||||
const appIconSets = useAppIconSets()
|
||||
const [currentAppIcon, setCurrentAppIcon] = useState(() =>
|
||||
DynamicAppIcon.getAppIcon(),
|
||||
)
|
||||
|
||||
// refresh current icon when screen is focused
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
setCurrentAppIcon(DynamicAppIcon.getAppIcon())
|
||||
}, []),
|
||||
)
|
||||
|
||||
return useMemo(() => {
|
||||
return (
|
||||
appIconSets.defaults.find(i => i.id === currentAppIcon) ??
|
||||
appIconSets.core.find(i => i.id === currentAppIcon) ??
|
||||
appIconSets.defaults[0]
|
||||
)
|
||||
}, [appIconSets, currentAppIcon])
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {useSession} from '#/state/session'
|
||||
import {useSetThemePrefs, useThemePrefs} from '#/state/shell'
|
||||
import {Logo} from '#/view/icons/Logo'
|
||||
import {SettingsListItem as AppIconSettingsListItem} from '#/screens/Settings/AppIconSettings/SettingsListItem'
|
||||
import {atoms as a, native, useAlf, useTheme} from '#/alf'
|
||||
import * as ToggleButton from '#/components/forms/ToggleButton'
|
||||
import {Props as SVGIconProps} from '#/components/icons/common'
|
||||
@@ -181,15 +181,7 @@ export function AppearanceSettingsScreen({}: Props) {
|
||||
{isNative && DISCOVER_DEBUG_DIDS[currentAccount?.did ?? ''] && (
|
||||
<>
|
||||
<SettingsList.Divider />
|
||||
|
||||
<SettingsList.LinkItem
|
||||
to="/settings/app-icon"
|
||||
label={_(msg`App Icon`)}>
|
||||
<SettingsList.ItemIcon icon={Logo} />
|
||||
<SettingsList.ItemText>
|
||||
<Trans>App Icon</Trans>
|
||||
</SettingsList.ItemText>
|
||||
</SettingsList.LinkItem>
|
||||
<AppIconSettingsListItem />
|
||||
</>
|
||||
)}
|
||||
</Animated.View>
|
||||
|
||||
@@ -300,11 +300,14 @@ export function FeedSourceCardLoaded({
|
||||
|
||||
{showLikes && feed.type === 'feed' ? (
|
||||
<Text type="sm-medium" style={[pal.text, pal.textLight]}>
|
||||
<Plural
|
||||
value={feed.likeCount || 0}
|
||||
one="Liked by # user"
|
||||
other="Liked by # users"
|
||||
/>
|
||||
<Trans>
|
||||
Liked by{' '}
|
||||
<Plural
|
||||
value={feed.likeCount || 0}
|
||||
one="# user"
|
||||
other="# users"
|
||||
/>
|
||||
</Trans>
|
||||
</Text>
|
||||
) : null}
|
||||
</Pressable>
|
||||
|
||||
@@ -421,9 +421,6 @@ export function PostThread({uri}: {uri: string | undefined}) {
|
||||
</View>
|
||||
)
|
||||
} else if (isThreadPost(item)) {
|
||||
if (!treeView && item.ctx.hasMoreSelfThread) {
|
||||
return <PostThreadLoadMore post={item.post} />
|
||||
}
|
||||
const prev = isThreadPost(posts[index - 1])
|
||||
? (posts[index - 1] as ThreadPost)
|
||||
: undefined
|
||||
@@ -436,6 +433,10 @@ export function PostThread({uri}: {uri: string | undefined}) {
|
||||
const hasUnrevealedParents =
|
||||
index === 0 && skeleton?.parents && maxParents < skeleton.parents.length
|
||||
|
||||
if (!treeView && prev && item.ctx.hasMoreSelfThread) {
|
||||
return <PostThreadLoadMore post={prev.post} />
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
ref={item.ctx.isHighlightedPost ? highlightedPostRef : undefined}
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import React, {useCallback, useEffect} from 'react'
|
||||
import {NativeScrollEvent} from 'react-native'
|
||||
import {
|
||||
interpolate,
|
||||
makeMutable,
|
||||
useSharedValue,
|
||||
withSpring,
|
||||
} from 'react-native-reanimated'
|
||||
import {interpolate, useSharedValue, withSpring} from 'react-native-reanimated'
|
||||
import EventEmitter from 'eventemitter3'
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
const V0 = makeMutable(
|
||||
withSpring(0, {
|
||||
overshootClamping: true,
|
||||
}),
|
||||
)
|
||||
|
||||
const V1 = makeMutable(
|
||||
withSpring(1, {
|
||||
overshootClamping: true,
|
||||
}),
|
||||
)
|
||||
|
||||
export function MainScrollProvider({children}: {children: React.ReactNode}) {
|
||||
const {headerHeight} = useShellLayout()
|
||||
const {headerMode} = useMinimalShellMode()
|
||||
@@ -42,7 +25,11 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
|
||||
const setMode = React.useCallback(
|
||||
(v: boolean) => {
|
||||
'worklet'
|
||||
headerMode.set(v ? V1.get() : V0.get())
|
||||
headerMode.set(() =>
|
||||
withSpring(v ? 1 : 0, {
|
||||
overshootClamping: true,
|
||||
}),
|
||||
)
|
||||
},
|
||||
[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)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -1,50 +1,10 @@
|
||||
import {I18n} from '@lingui/core'
|
||||
|
||||
const truncateRounding = (num: number, factors: Array<number>): number => {
|
||||
for (let i = factors.length - 1; i >= 0; i--) {
|
||||
let factor = factors[i]
|
||||
if (num >= 10 ** factor) {
|
||||
if (factor === 10) {
|
||||
// CA and ES abruptly jump from "9999,9 M" to "10 mil M"
|
||||
factor--
|
||||
}
|
||||
const precision = 1
|
||||
const divisor = 10 ** (factor - precision)
|
||||
return Math.floor(num / divisor) * divisor
|
||||
}
|
||||
}
|
||||
return num
|
||||
}
|
||||
|
||||
const koFactors = [3, 4, 8, 12]
|
||||
const hiFactors = [3, 5, 7, 9, 11, 13]
|
||||
const esCaFactors = [3, 6, 10, 12]
|
||||
const itDeFactors = [6, 9, 12]
|
||||
const jaZhFactors = [4, 8, 12]
|
||||
const glFactors = [6, 12]
|
||||
const restFactors = [3, 6, 9, 12]
|
||||
|
||||
export const formatCount = (i18n: I18n, num: number) => {
|
||||
const locale = i18n.locale
|
||||
let truncatedNum: number
|
||||
if (locale === 'hi') {
|
||||
truncatedNum = truncateRounding(num, hiFactors)
|
||||
} else if (locale === 'ko') {
|
||||
truncatedNum = truncateRounding(num, koFactors)
|
||||
} else if (locale === 'es' || locale === 'ca') {
|
||||
truncatedNum = truncateRounding(num, esCaFactors)
|
||||
} else if (locale === 'ja' || locale === 'zh-CN' || locale === 'zh-TW') {
|
||||
truncatedNum = truncateRounding(num, jaZhFactors)
|
||||
} else if (locale === 'it' || locale === 'de') {
|
||||
truncatedNum = truncateRounding(num, itDeFactors)
|
||||
} else if (locale === 'gl') {
|
||||
truncatedNum = truncateRounding(num, glFactors)
|
||||
} else {
|
||||
truncatedNum = truncateRounding(num, restFactors)
|
||||
}
|
||||
return i18n.number(truncatedNum, {
|
||||
return i18n.number(num, {
|
||||
notation: 'compact',
|
||||
maximumFractionDigits: 1,
|
||||
// Ideally we'd use roundingMode: 'trunc' but it isn't supported on RN.
|
||||
// @ts-expect-error - roundingMode not in the types
|
||||
roundingMode: 'trunc',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -258,10 +258,12 @@ let PostCtrls = ({
|
||||
}
|
||||
}}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={plural(post.replyCount || 0, {
|
||||
one: 'Reply (# reply)',
|
||||
other: 'Reply (# replies)',
|
||||
})}
|
||||
accessibilityLabel={_(
|
||||
msg`Reply (${plural(post.replyCount || 0, {
|
||||
one: '# reply',
|
||||
other: '# replies',
|
||||
})})`,
|
||||
)}
|
||||
accessibilityHint=""
|
||||
hitSlop={POST_CTRL_HITSLOP}>
|
||||
<Bubble
|
||||
@@ -298,14 +300,18 @@ let PostCtrls = ({
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={
|
||||
post.viewer?.like
|
||||
? plural(post.likeCount || 0, {
|
||||
one: 'Unlike (# like)',
|
||||
other: 'Unlike (# likes)',
|
||||
})
|
||||
: plural(post.likeCount || 0, {
|
||||
one: 'Like (# like)',
|
||||
other: 'Like (# likes)',
|
||||
})
|
||||
? _(
|
||||
msg`Unlike (${plural(post.likeCount || 0, {
|
||||
one: '# like',
|
||||
other: '# likes',
|
||||
})})`,
|
||||
)
|
||||
: _(
|
||||
msg`Like (${plural(post.likeCount || 0, {
|
||||
one: '# like',
|
||||
other: '# likes',
|
||||
})})`,
|
||||
)
|
||||
}
|
||||
accessibilityHint=""
|
||||
hitSlop={POST_CTRL_HITSLOP}>
|
||||
|
||||
@@ -62,11 +62,21 @@ let RepostButton = ({
|
||||
{padding: 5},
|
||||
]}
|
||||
hoverStyle={t.atoms.bg_contrast_25}
|
||||
label={`${
|
||||
label={
|
||||
isReposted
|
||||
? _(msg`Undo repost`)
|
||||
: _(msg({message: 'Repost', context: 'action'}))
|
||||
} (${plural(repostCount || 0, {one: '# repost', other: '# reposts'})})`}
|
||||
? _(
|
||||
msg`Undo repost (${plural(repostCount || 0, {
|
||||
one: '# repost',
|
||||
other: '# reposts',
|
||||
})})`,
|
||||
)
|
||||
: _(
|
||||
msg`Repost (${plural(repostCount || 0, {
|
||||
one: '# repost',
|
||||
other: '# reposts',
|
||||
})})`,
|
||||
)
|
||||
}
|
||||
shape="round"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -3377,6 +3377,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
||||
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":
|
||||
version "6.0.4"
|
||||
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"
|
||||
integrity sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==
|
||||
|
||||
"@formatjs/ecma402-abstract@2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-2.0.0.tgz#39197ab90b1c78b7342b129a56a7acdb8f512e17"
|
||||
integrity sha512-rRqXOqdFmk7RYvj4khklyqzcfQl9vEL/usogncBHRZfZBDOwMGuSRNFl02fu5KGHXdbinju+YXyuR+Nk8xlr/g==
|
||||
"@formatjs/ecma402-abstract@2.3.1":
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.1.tgz#cdeb3ffe1aeea9c4284b85b7e37e8e8615314c39"
|
||||
integrity sha512-Ip9uV+/MpLXWRk03U/GzeJMuPeOXpJBSB5V1tjA6kJhvqssye5J5LoYLc7Z5IAHb7nR62sRoguzrFiVCP/hnzw==
|
||||
dependencies:
|
||||
"@formatjs/intl-localematcher" "0.5.4"
|
||||
tslib "^2.4.0"
|
||||
"@formatjs/fast-memoize" "2.2.5"
|
||||
"@formatjs/intl-localematcher" "0.5.9"
|
||||
decimal.js "10"
|
||||
tslib "2"
|
||||
|
||||
"@formatjs/intl-enumerator@1.4.7":
|
||||
version "1.4.7"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/intl-enumerator/-/intl-enumerator-1.4.7.tgz#6ab697f3f8f18cf0cc6a6b028cb9c40db6001f3d"
|
||||
integrity sha512-03RHnFqfpB4H/jwCwlzC+wkTDk2Fi24JmVIY2PVGvTUpikN2bSr9+8oTXfOC+y7B7VxjCArUnqWXVoctkmy85w==
|
||||
"@formatjs/fast-memoize@2.2.5":
|
||||
version "2.2.5"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/fast-memoize/-/fast-memoize-2.2.5.tgz#54a4a1793d773b72c372d3dcab3595149aee7880"
|
||||
integrity sha512-6PoewUMrrcqxSoBXAOJDiW1m+AmkrAj0RiXnOMD59GRaswjXhm3MDhgepXPBgonc09oSirAJTsAggzAGQf6A6g==
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
tslib "2"
|
||||
|
||||
"@formatjs/intl-getcanonicallocales@2.3.0":
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/intl-getcanonicallocales/-/intl-getcanonicallocales-2.3.0.tgz#b6c6fa1c664e30a61f27fa6399a76159d82a5842"
|
||||
integrity sha512-BOXbLwqQ7nKua/l7tKqDLRN84WupDXFDhGJQMFvsMVA2dKuOdRaWTxWpL3cJ7qPkoNw11Jf+Xpj4OSPBBvW0eQ==
|
||||
"@formatjs/intl-enumerator@1.8.7":
|
||||
version "1.8.7"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/intl-enumerator/-/intl-enumerator-1.8.7.tgz#3f004753333f80cc468ae34046bd8416772a0412"
|
||||
integrity sha512-qd7UlWUivKRJ073btssUqMSqzWW9yN3Ki6EqfCZ6uvIv19mONelE5q3GMmdPWBEjgqZikBzBE2qPTqfrgJ4TCA==
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
"@formatjs/ecma402-abstract" "2.3.1"
|
||||
tslib "2"
|
||||
|
||||
"@formatjs/intl-locale@^4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/intl-locale/-/intl-locale-4.0.0.tgz#c111a33078413eba2011e82140466261eb1d67cd"
|
||||
integrity sha512-+4dbMEGsp1bvB3JB3UHH6YTjMnFTifnfdaHp4ROrCCu50NedA69RBsDCG3eivcZkbj57X9ehGhMWjLxlP+gyVw==
|
||||
"@formatjs/intl-getcanonicallocales@2.5.4":
|
||||
version "2.5.4"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/intl-getcanonicallocales/-/intl-getcanonicallocales-2.5.4.tgz#9b843e1891dea83405c51eb3d00c42ef9cb6cab9"
|
||||
integrity sha512-vSDOsAcc3U+Kl/0b3de8wCQkb3W30H8LUuslyz67wTAHOPSQhPimZyquhwxXpJR+K5yy9CkzTgk5YE5kFT+PFg==
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract" "2.0.0"
|
||||
"@formatjs/intl-enumerator" "1.4.7"
|
||||
"@formatjs/intl-getcanonicallocales" "2.3.0"
|
||||
tslib "^2.4.0"
|
||||
tslib "2"
|
||||
|
||||
"@formatjs/intl-localematcher@0.5.4":
|
||||
version "0.5.4"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.5.4.tgz#caa71f2e40d93e37d58be35cfffe57865f2b366f"
|
||||
integrity sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==
|
||||
"@formatjs/intl-locale@^4.2.8":
|
||||
version "4.2.8"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/intl-locale/-/intl-locale-4.2.8.tgz#571d44e92b6eb43b7410b37f25e280ec384a32cf"
|
||||
integrity sha512-6RY/npeA0kyoZ8QW0JRAT+VBAFBT6+4ZVeGkKCNIDjbLX2LPuU73emGR35Mbwcc6pquVFrxyo6mXxKNzib0kEA==
|
||||
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":
|
||||
version "8.10.3"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/intl-numberformat/-/intl-numberformat-8.10.3.tgz#abc97cc6a7b7f1b20da9f07a976b5589c1192ab8"
|
||||
integrity sha512-lH3liLMeIjZ19Zxt8RRPnBcpPweS1YNSXRURDiFfvFmRlDZUOd8+GlcVyECcPZPkIoSH/p4lfGrnaUzepxJ92g==
|
||||
"@formatjs/intl-localematcher@0.5.9":
|
||||
version "0.5.9"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.5.9.tgz#43c6ee22be85b83340bcb09bdfed53657a2720db"
|
||||
integrity sha512-8zkGu/sv5euxbjfZ/xmklqLyDGQSxsLqg8XOq88JW3cmJtzhCP8EtSJXlaKZnVO4beEaoiT9wj4eIoCQ9smwxA==
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract" "2.0.0"
|
||||
"@formatjs/intl-localematcher" "0.5.4"
|
||||
tslib "^2.4.0"
|
||||
tslib "2"
|
||||
|
||||
"@formatjs/intl-pluralrules@^5.2.14":
|
||||
version "5.2.14"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/intl-pluralrules/-/intl-pluralrules-5.2.14.tgz#7477bd2aa9bfde9e543d839707eff5460eb08026"
|
||||
integrity sha512-l6Ev7aOGXJSh5EPDEqzsbyufdCCKXZk993QXRQebLsB0TXRhIyF4alqjdMEatLwIigK/Mka8kiVIOLeFP5Cj9Q==
|
||||
"@formatjs/intl-numberformat@^8.15.1":
|
||||
version "8.15.1"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/intl-numberformat/-/intl-numberformat-8.15.1.tgz#b2a5b00889ed31dbef9d4e5aeee1dea3d040b068"
|
||||
integrity sha512-NIouSY50xpH/SMJrRbX1Q3hMsGyQmT5MQrta/bOYhpZda1bztOlEYZAKLytk8VGs10wkGz875602mCMhtg4/LA==
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract" "2.0.0"
|
||||
"@formatjs/intl-localematcher" "0.5.4"
|
||||
tslib "^2.4.0"
|
||||
"@formatjs/ecma402-abstract" "2.3.1"
|
||||
"@formatjs/intl-localematcher" "0.5.9"
|
||||
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":
|
||||
version "6.4.2"
|
||||
@@ -9372,7 +9389,7 @@ decamelize@^1.2.0:
|
||||
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
|
||||
integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==
|
||||
|
||||
decimal.js@^10.4.2:
|
||||
decimal.js@10, decimal.js@^10.4.2:
|
||||
version "10.4.3"
|
||||
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23"
|
||||
integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==
|
||||
@@ -16035,10 +16052,10 @@ react-native-qrcode-styled@^0.3.3:
|
||||
qrcode "^1.5.4"
|
||||
react-fast-compare "^3.2.2"
|
||||
|
||||
react-native-reanimated@^3.16.3:
|
||||
version "3.16.3"
|
||||
resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-3.16.3.tgz#3b559dca49e9e40abcf5de834dc27fc05f856b66"
|
||||
integrity sha512-OWlA6e1oHhytTpc7WiSZ7Tmb8OYwLKYZz29Sz6d6WAg60Hm5GuAiKIWUG7Ako7FLcYhFkA0pEQ2xPMEYUo9vlw==
|
||||
react-native-reanimated@3.17.0-nightly-20241211-17e89ca24:
|
||||
version "3.17.0-nightly-20241211-17e89ca24"
|
||||
resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-3.17.0-nightly-20241211-17e89ca24.tgz#af0c36e278646eb2f79e28ad0047cfd80d0e29f5"
|
||||
integrity sha512-5p7jr0DrnID1puOzMel3VZVRw5Hl/UdMUvPCI1sEG9IA2mUaWrgeoojS2wVwW1U0Pj6HXjPNEimDSXZneZKNuQ==
|
||||
dependencies:
|
||||
"@babel/plugin-transform-arrow-functions" "^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"
|
||||
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:
|
||||
version "1.14.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
||||
|
||||
Reference in New Issue
Block a user