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
BITDRIFT_API_KEY=
SENTRY_AUTH_TOKEN=
EXPO_PUBLIC_ENV=development
EXPO_PUBLIC_LOG_LEVEL=debug
+1
View File
@@ -222,6 +222,7 @@ module.exports = function (config) {
},
],
'react-native-compressor',
'@bitdrift/react-native',
'./plugins/starterPackAppClipExtension/withStarterPackAppClip.js',
'./plugins/withAndroidManifestPlugin.js',
'./plugins/withAndroidManifestFCMIconPlugin.js',
+3 -1
View File
@@ -20,6 +20,7 @@ window.addEventListener('message', event => {
return
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const id = (event.data as {id: string}).id
if (!id) {
return
@@ -33,6 +34,7 @@ window.addEventListener('message', event => {
return
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const height = (event.data as {height: number}).height
if (height) {
embed.style.height = `${height}px`
@@ -47,7 +49,7 @@ window.addEventListener('message', event => {
* @returns
*/
function scan(node = document) {
const embeds = node.querySelectorAll('[data-bluesky-uri]')
const embeds = node.querySelectorAll<HTMLIFrameElement>('[data-bluesky-uri]')
for (let i = 0; i < embeds.length; i++) {
const id = String(Math.random()).slice(2)
+17
View File
@@ -0,0 +1,17 @@
export function applyTheme(theme: 'light' | 'dark') {
document.documentElement.classList.remove('light', 'dark')
document.documentElement.classList.add(theme)
}
export function initColorMode() {
applyTheme(
window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light',
)
window
.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', mql => {
applyTheme(mql.matches ? 'dark' : 'light')
})
}
+1 -1
View File
@@ -37,7 +37,7 @@ export function Container({
return (
<div
ref={ref}
className="w-full bg-white hover:bg-neutral-50 relative transition-colors max-w-[600px] min-w-[300px] flex border rounded-xl"
className="w-full bg-white text-black hover:bg-neutral-50 dark:bg-dimmedBg dark:hover:bg-dimmedBgLighten relative transition-colors max-w-[600px] min-w-[300px] flex border dark:border-slate-600 dark:text-slate-200 rounded-xl"
onClick={() => {
if (ref.current && href) {
// forwardRef requires preact/compat - let's keep it simple
+20 -14
View File
@@ -78,9 +78,9 @@ export function Embed({
return (
<Link
href={`/profile/${record.author.did}/post/${getRkey(record)}`}
className="transition-colors hover:bg-neutral-100 border rounded-lg p-2 gap-1.5 w-full flex flex-col">
className="transition-colors hover:bg-neutral-100 dark:hover:bg-slate-700 border dark:border-slate-600 rounded-lg p-2 gap-1.5 w-full flex flex-col">
<div className="flex gap-1.5 items-center">
<div className="w-4 h-4 overflow-hidden rounded-full bg-neutral-300 shrink-0">
<div className="w-4 h-4 overflow-hidden rounded-full bg-neutral-300 dark:bg-slate-700 shrink-0">
<img
src={record.author.avatar}
style={isAuthorLabeled ? {filter: 'blur(1.5px)'} : undefined}
@@ -88,7 +88,7 @@ export function Embed({
</div>
<p className="line-clamp-1 text-sm">
<span className="font-bold">{record.author.displayName}</span>
<span className="text-textLight ml-1">
<span className="text-textLight dark:text-textDimmed ml-1">
@{record.author.handle}
</span>
</p>
@@ -209,7 +209,7 @@ function Info({children}: {children: ComponentChildren}) {
return (
<div className="w-full rounded-lg border py-2 px-2.5 flex-row flex gap-2 bg-neutral-50">
<img src={infoIcon} className="w-4 h-4 shrink-0 mt-0.5" />
<p className="text-sm text-textLight">{children}</p>
<p className="text-sm text-textLight dark:text-textDimmed">{children}</p>
</div>
)
}
@@ -308,7 +308,7 @@ function ExternalEmbed({
return (
<Link
href={content.external.uri}
className="w-full rounded-lg overflow-hidden border flex flex-col items-stretch"
className="w-full rounded-lg overflow-hidden border dark:border-slate-600 flex flex-col items-stretch"
disableTracking>
{content.external.thumb && (
<img
@@ -317,11 +317,11 @@ function ExternalEmbed({
/>
)}
<div className="py-3 px-4">
<p className="text-sm text-textLight line-clamp-1">
<p className="text-sm text-textLight dark:text-textDimmed line-clamp-1">
{toNiceDomain(content.external.uri)}
</p>
<p className="font-semibold line-clamp-3">{content.external.title}</p>
<p className="text-sm text-textLight line-clamp-2 mt-0.5">
<p className="text-sm text-textLight dark:text-textDimmed line-clamp-2 mt-0.5">
{content.external.description}
</p>
</div>
@@ -345,23 +345,29 @@ function GenericWithImageEmbed({
return (
<Link
href={href}
className="w-full rounded-lg border py-2 px-3 flex flex-col gap-2">
className="w-full rounded-lg border dark:border-slate-600 py-2 px-3 flex flex-col gap-2">
<div className="flex gap-2.5 items-center">
{image ? (
<img
src={image}
alt={title}
className="w-8 h-8 rounded-md bg-neutral-300 shrink-0"
className="w-8 h-8 rounded-md bg-neutral-300 dark:bg-slate-700 shrink-0"
/>
) : (
<div className="w-8 h-8 rounded-md bg-brand shrink-0" />
)}
<div className="flex-1">
<p className="font-bold text-sm">{title}</p>
<p className="text-textLight text-sm">{subtitle}</p>
<p className="text-textLight dark:text-textDimmed text-sm">
{subtitle}
</p>
</div>
</div>
{description && <p className="text-textLight text-sm">{description}</p>}
{description && (
<p className="text-textLight dark:text-textDimmed text-sm">
{description}
</p>
)}
</Link>
)
}
@@ -406,7 +412,7 @@ function StarterPackEmbed({
return (
<Link
href={starterPackHref}
className="w-full rounded-lg overflow-hidden border flex flex-col items-stretch">
className="w-full rounded-lg overflow-hidden border dark:border-slate-600 flex flex-col items-stretch">
<img src={imageUri} className="aspect-[1.91/1] object-cover" />
<div className="py-3 px-4">
<div className="flex space-x-2 items-center">
@@ -415,7 +421,7 @@ function StarterPackEmbed({
<p className="font-semibold leading-[21px]">
{content.record.name}
</p>
<p className="text-sm text-textLight line-clamp-2 leading-[18px]">
<p className="text-sm text-textLight dark:text-textDimmed line-clamp-2 leading-[18px]">
Starter pack by{' '}
{content.creator.displayName || `@${content.creator.handle}`}
</p>
@@ -425,7 +431,7 @@ function StarterPackEmbed({
<p className="text-sm mt-1">{content.record.description}</p>
)}
{!!content.joinedAllTimeCount && content.joinedAllTimeCount > 50 && (
<p className="text-sm font-semibold text-textLight mt-1">
<p className="text-sm font-semibold text-textLight dark:text-textDimmed mt-1">
{content.joinedAllTimeCount} users have joined!
</p>
)}
+10 -8
View File
@@ -38,7 +38,7 @@ export function Post({thread}: Props) {
<div className="flex-1 flex-col flex gap-2" lang={record?.langs?.[0]}>
<div className="flex gap-2.5 items-center cursor-pointer">
<Link href={`/profile/${post.author.did}`} className="rounded-full">
<div className="w-10 h-10 overflow-hidden rounded-full bg-neutral-300 shrink-0">
<div className="w-10 h-10 overflow-hidden rounded-full bg-neutral-300 dark:bg-slate-700 shrink-0">
<img
src={post.author.avatar}
style={isAuthorLabeled ? {filter: 'blur(2.5px)'} : undefined}
@@ -53,7 +53,7 @@ export function Post({thread}: Props) {
</Link>
<Link
href={`/profile/${post.author.did}`}
className="text-[15px] text-textLight hover:underline line-clamp-1">
className="text-[15px] text-textLight dark:text-textDimmed hover:underline line-clamp-1">
<p>@{post.author.handle}</p>
</Link>
</div>
@@ -69,15 +69,15 @@ export function Post({thread}: Props) {
<Link href={href}>
<time
datetime={new Date(post.indexedAt).toISOString()}
className="text-textLight mt-1 text-sm hover:underline">
className="text-textLight dark:text-textDimmed mt-1 text-sm hover:underline">
{niceDate(post.indexedAt)}
</time>
</Link>
<div className="border-t w-full pt-2.5 flex items-center gap-5 text-sm cursor-pointer">
<div className="border-t dark:border-slate-600 w-full pt-2.5 flex items-center gap-5 text-sm cursor-pointer">
{!!post.likeCount && (
<div className="flex items-center gap-2 cursor-pointer">
<img src={likeIcon} className="w-5 h-5" />
<p className="font-bold text-neutral-500 mb-px">
<p className="font-bold text-neutral-500 dark:text-neutral-300 mb-px">
{prettyNumber(post.likeCount)}
</p>
</div>
@@ -85,17 +85,19 @@ export function Post({thread}: Props) {
{!!post.repostCount && (
<div className="flex items-center gap-2 cursor-pointer">
<img src={repostIcon} className="w-5 h-5" />
<p className="font-bold text-neutral-500 mb-px">
<p className="font-bold text-neutral-500 dark:text-neutral-300 mb-px">
{prettyNumber(post.repostCount)}
</p>
</div>
)}
<div className="flex items-center gap-2 cursor-pointer">
<img src={replyIcon} className="w-5 h-5" />
<p className="font-bold text-neutral-500 mb-px">Reply</p>
<p className="font-bold text-neutral-500 dark:text-neutral-300 mb-px">
Reply
</p>
</div>
<div className="flex-1" />
<p className="cursor-pointer text-brand font-bold hover:underline hidden min-[450px]:inline">
<p className="cursor-pointer text-brand dark:text-brandLighten font-bold hover:underline hidden min-[450px]:inline">
{post.replyCount
? `Read ${prettyNumber(post.replyCount)} ${
post.replyCount > 1 ? 'replies' : 'reply'
+4
View File
@@ -5,3 +5,7 @@
.break-word {
word-break: break-word;
}
:root {
color-scheme: light dark;
}
+18 -13
View File
@@ -6,6 +6,7 @@ import {useEffect, useMemo, useRef, useState} from 'preact/hooks'
import arrowBottom from '../../assets/arrowBottom_stroke2_corner0_rounded.svg'
import logo from '../../assets/logo.svg'
import {initColorMode} from '../color-mode'
import {Container} from '../components/container'
import {Link} from '../components/link'
import {Post} from '../components/post'
@@ -21,6 +22,8 @@ export const EMBED_SCRIPT = `${EMBED_SERVICE}/static/embed.js`
const root = document.getElementById('app')
if (!root) throw new Error('No root element')
initColorMode()
const agent = new BskyAgent({
service: 'https://public.api.bsky.app',
})
@@ -108,7 +111,7 @@ function LandingPage() {
}, [uri])
return (
<main className="w-full min-h-screen flex flex-col items-center gap-8 py-14 px-4 md:pt-32">
<main className="w-full min-h-screen flex flex-col items-center gap-8 py-14 px-4 md:pt-32 dark:bg-dimmedBgDarken dark:text-slate-200">
<Link
href="https://bsky.social/about"
className="transition-transform hover:scale-110">
@@ -121,20 +124,22 @@ function LandingPage() {
type="text"
value={uri}
onInput={e => setUri(e.currentTarget.value)}
className="border rounded-lg py-3 w-full max-w-[600px] px-4"
className="border rounded-lg py-3 w-full max-w-[600px] px-4 dark:bg-dimmedBg dark:border-slate-500"
placeholder={DEFAULT_POST}
/>
<img src={arrowBottom} className="w-6" />
<img src={arrowBottom} className="w-6 dark:invert" />
{loading ? (
<Skeleton />
<div className="w-full max-w-[600px]">
<Skeleton />
</div>
) : (
<div className="w-full max-w-[600px] gap-8 flex flex-col">
{!error && thread && uri && <Snippet thread={thread} />}
{!error && thread && <Post thread={thread} key={thread.post.uri} />}
{error && (
<div className="w-full border border-red-500 bg-red-50 px-4 py-3 rounded-lg">
<div className="w-full border border-red-500 bg-red-500/10 px-4 py-3 rounded-lg">
<p className="text-red-500 text-center">{error}</p>
</div>
)}
@@ -149,15 +154,15 @@ function Skeleton() {
<Container>
<div className="flex-1 flex-col flex gap-2 pb-8">
<div className="flex gap-2.5 items-center">
<div className="w-10 h-10 overflow-hidden rounded-full bg-neutral-100 shrink-0 animate-pulse" />
<div className="w-10 h-10 overflow-hidden rounded-full bg-neutral-100 dark:bg-slate-700 shrink-0 animate-pulse" />
<div className="flex-1">
<div className="bg-neutral-100 animate-pulse w-64 h-4 rounded" />
<div className="bg-neutral-100 animate-pulse w-32 h-3 mt-1 rounded" />
<div className="bg-neutral-100 dark:bg-slate-700 animate-pulse w-64 h-4 rounded" />
<div className="bg-neutral-100 dark:bg-slate-700 animate-pulse w-32 h-3 mt-1 rounded" />
</div>
</div>
<div className="w-full h-4 mt-2 bg-neutral-100 rounded animate-pulse" />
<div className="w-5/6 h-4 bg-neutral-100 rounded animate-pulse" />
<div className="w-3/4 h-4 bg-neutral-100 rounded animate-pulse" />
<div className="w-full h-4 mt-2 bg-neutral-100 dark:bg-slate-700 rounded animate-pulse" />
<div className="w-5/6 h-4 bg-neutral-100 dark:bg-slate-700 rounded animate-pulse" />
<div className="w-3/4 h-4 bg-neutral-100 dark:bg-slate-700 rounded animate-pulse" />
</div>
</Container>
)
@@ -220,7 +225,7 @@ function Snippet({thread}: {thread: AppBskyFeedDefs.ThreadViewPost}) {
ref={ref}
type="text"
value={snippet}
className="border rounded-lg py-3 w-full px-4"
className="border rounded-lg py-3 w-full px-4 dark:bg-dimmedBg dark:border-slate-500"
readOnly
autoFocus
onFocus={() => {
@@ -228,7 +233,7 @@ function Snippet({thread}: {thread: AppBskyFeedDefs.ThreadViewPost}) {
}}
/>
<button
className="rounded-lg bg-brand text-white color-white py-3 px-4 whitespace-nowrap min-w-28"
className="rounded-lg bg-brand text-white py-3 px-4 whitespace-nowrap min-w-28"
onClick={() => {
ref.current?.focus()
ref.current?.select()
+6 -3
View File
@@ -4,6 +4,7 @@ import {AppBskyFeedDefs, AtpAgent} from '@atproto/api'
import {h, render} from 'preact'
import logo from '../../assets/logo.svg'
import {initColorMode} from '../color-mode'
import {Container} from '../components/container'
import {Link} from '../components/link'
import {Post} from '../components/post'
@@ -21,6 +22,8 @@ if (!uri) {
throw new Error('No uri in path')
}
initColorMode()
agent
.getPostThread({
uri,
@@ -55,13 +58,13 @@ function PwiOptOut({thread}: {thread: AppBskyFeedDefs.ThreadViewPost}) {
<img src={logo} className="h-6" />
</Link>
<div className="w-full py-12 gap-4 flex flex-col items-center">
<p className="max-w-80 text-center w-full text-textLight">
<p className="max-w-80 text-center w-full text-textLight dark:text-textDimmed">
The author of this post has requested their posts not be displayed on
external sites.
</p>
<Link
href={href}
className="max-w-80 rounded-lg bg-brand text-white color-white text-center py-1 px-4 w-full mx-auto">
className="max-w-80 rounded-lg bg-brand text-white text-center py-1 px-4 w-full mx-auto">
View on Bluesky
</Link>
</div>
@@ -77,7 +80,7 @@ function ErrorMessage() {
className="transition-transform hover:scale-110 absolute top-4 right-4">
<img src={logo} className="h-6" />
</Link>
<p className="my-16 text-center w-full text-textLight">
<p className="my-16 text-center w-full text-textLight dark:text-textDimmed">
Post not found, it may have been deleted.
</p>
</Container>
+8
View File
@@ -1,11 +1,19 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
darkMode: ['variant', [
'&:is(.dark *):not(:is(.dark .light *))',
]],
theme: {
extend: {
colors: {
brand: 'rgb(10,122,255)',
brandLighten: 'rgb(32,139,254)',
textLight: 'rgb(66,87,108)',
textDimmed: 'rgb(174,187,201)',
dimmedBgLighten: 'rgb(30,41,54)',
dimmedBg: 'rgb(22,30,39)',
dimmedBgDarken: 'rgb(18,25,32)',
},
},
},
+1 -2
View File
@@ -1,4 +1,3 @@
{
"compilerOptions": {
"target": "ES5",
@@ -20,5 +19,5 @@
"jsxFragmentFactory": "Fragment",
"downlevelIteration": true
},
"include": ["src", "vite.config.ts"]
"include": ["src", "snippet", "vite.config.ts"]
}
+6
View File
@@ -87,6 +87,12 @@ However, if you're a part of the Bluesky team and want to enable Sentry, fill in
If you change `SENTRY_AUTH_TOKEN`, you need to do `yarn prebuild` before running `yarn ios` or `yarn android` again.
### Adding bitdrift
Adding bitdirft is NOT required. You can keep `BITDRIFT_API_KEY=` in `.env` which will avoid initializing bitdrift during startup.
However, if you're a part of the Bluesky team and want to enable bitdrift, fill in `BITDRIFT_API_KEY` in your `.env` to enable bitdrift.
### Adding and Updating Locales
- `yarn intl:build` -> you will also need to run this anytime `./src/locale/{locale}/messages.po` change
+5 -4
View File
@@ -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",
+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 '#/lib/sentry' // must be near top
import '#/lib/bitdrift' // must be near top
import '#/view/icons'
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 {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'
+4 -5
View File
@@ -7,7 +7,7 @@ import {
AtUri,
RichText as RichTextApi,
} from '@atproto/api'
import {msg, plural, Trans} from '@lingui/macro'
import {msg, Plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
@@ -210,10 +210,9 @@ export function Likes({count}: {count: number}) {
const t = useTheme()
return (
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
{plural(count || 0, {
one: 'Liked by # user',
other: 'Liked by # users',
})}
<Trans>
Liked by <Plural value={count || 0} one="# user" other="# users" />
</Trans>
</Text>
)
}
+5 -3
View File
@@ -83,7 +83,7 @@ export function RegionalNotice() {
)
}
export function LikeCount({count}: {count: number}) {
export function LikeCount({likeCount}: {likeCount: number}) {
const t = useTheme()
return (
<Text
@@ -93,7 +93,9 @@ export function LikeCount({count}: {count: number}) {
t.atoms.text_contrast_medium,
{fontWeight: '600'},
]}>
<Plural value={count} one="Liked by # user" other="Liked by # users" />
<Trans>
Liked by <Plural value={likeCount} one="# user" other="# users" />
</Trans>
</Text>
)
}
@@ -138,7 +140,7 @@ export function Default({
value={labeler.creator.description}
handle={labeler.creator.handle}
/>
{labeler.likeCount ? <LikeCount count={labeler.likeCount} /> : null}
{labeler.likeCount ? <LikeCount likeCount={labeler.likeCount} /> : null}
</Content>
</Outer>
)
+2 -2
View File
@@ -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}],
+17 -11
View File
@@ -1,6 +1,6 @@
import {StyleProp, View, ViewStyle} from 'react-native'
import {AppBskyFeedDefs, ComAtprotoLabelDefs} from '@atproto/api'
import {msg, Plural} from '@lingui/macro'
import {msg, Plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useSession} from '#/state/session'
@@ -50,17 +50,23 @@ export function LabelsOnMe({
<ButtonIcon position="left" icon={CircleInfo} />
<ButtonText style={[a.leading_snug]}>
{type === 'account' ? (
<Plural
value={labels.length}
one="# label has been placed on this account"
other="# labels have been placed on this account"
/>
<Trans>
<Plural
value={labels.length}
one="# label has"
other="# labels have"
/>{' '}
been placed on this account
</Trans>
) : (
<Plural
value={labels.length}
one="# label has been placed on this content"
other="# labels have been placed on this content"
/>
<Trans>
<Plural
value={labels.length}
one="# label has"
other="# labels have"
/>{' '}
been placed on this content
</Trans>
)}
</ButtonText>
</Button>
+7
View File
@@ -0,0 +1,7 @@
import {init} from '@bitdrift/react-native'
const BITDRIFT_API_KEY = process.env.BITDRIFT_API_KEY
if (BITDRIFT_API_KEY) {
init(BITDRIFT_API_KEY, {url: 'https://api-bsky.bitdrift.io'})
}
+23
View File
@@ -0,0 +1,23 @@
import {
debug as bdDebug,
error as bdError,
info as bdInfo,
warn as bdWarn,
} from '@bitdrift/react-native'
import {LogLevel, Transport} from './types'
export function createBitdriftTransport(): Transport {
const logFunctions = {
[LogLevel.Debug]: bdDebug,
[LogLevel.Info]: bdInfo,
[LogLevel.Log]: bdInfo,
[LogLevel.Warn]: bdWarn,
[LogLevel.Error]: bdError,
} as const
return (level, message) => {
const log = logFunctions[level]
log(message.toString())
}
}
+7
View File
@@ -0,0 +1,7 @@
import {Transport} from './index'
export function createBitdriftTransport(): Transport {
return (_level, _message) => {
// noop
}
}
+9 -67
View File
@@ -6,74 +6,12 @@ import {DebugContext} from '#/logger/debugContext'
import {add} from '#/logger/logDump'
import {Sentry} from '#/logger/sentry'
import * as env from '#/env'
import {createBitdriftTransport} from './bitdriftTransport'
import {Metadata} from './types'
import {ConsoleTransportEntry, LogLevel, Transport} from './types'
export enum LogLevel {
Debug = 'debug',
Info = 'info',
Log = 'log',
Warn = 'warn',
Error = 'error',
}
type Transport = (
level: LogLevel,
message: string | Error,
metadata: Metadata,
timestamp: number,
) => void
/**
* A union of some of Sentry's breadcrumb properties as well as Sentry's
* `captureException` parameter, `CaptureContext`.
*/
type Metadata = {
/**
* Applied as Sentry breadcrumb types. Defaults to `default`.
*
* @see https://develop.sentry.dev/sdk/event-payloads/breadcrumbs/#breadcrumb-types
*/
type?:
| 'default'
| 'debug'
| 'error'
| 'navigation'
| 'http'
| 'info'
| 'query'
| 'transaction'
| 'ui'
| 'user'
/**
* Passed through to `Sentry.captureException`
*
* @see https://github.com/getsentry/sentry-javascript/blob/903addf9a1a1534a6cb2ba3143654b918a86f6dd/packages/types/src/misc.ts#L65
*/
tags?: {
[key: string]:
| number
| string
| boolean
| bigint
| symbol
| null
| undefined
}
/**
* Any additional data, passed through to Sentry as `extra` param on
* exceptions, or the `data` param on breadcrumbs.
*/
[key: string]: unknown
} & Parameters<typeof Sentry.captureException>[1]
export type ConsoleTransportEntry = {
id: string
timestamp: number
level: LogLevel
message: string | Error
metadata: Metadata
}
export {LogLevel}
export type {ConsoleTransportEntry, Transport}
const enabledLogLevels: {
[key in LogLevel]: LogLevel[]
@@ -328,6 +266,10 @@ export class Logger {
*/
export const logger = new Logger()
if (!env.IS_TEST) {
logger.addTransport(createBitdriftTransport())
}
if (env.IS_DEV && !env.IS_TEST) {
logger.addTransport(consoleTransport)
+69
View File
@@ -0,0 +1,69 @@
import type {Sentry} from '#/logger/sentry'
export enum LogLevel {
Debug = 'debug',
Info = 'info',
Log = 'log',
Warn = 'warn',
Error = 'error',
}
export type Transport = (
level: LogLevel,
message: string | Error,
metadata: Metadata,
timestamp: number,
) => void
/**
* A union of some of Sentry's breadcrumb properties as well as Sentry's
* `captureException` parameter, `CaptureContext`.
*/
export type Metadata = {
/**
* Applied as Sentry breadcrumb types. Defaults to `default`.
*
* @see https://develop.sentry.dev/sdk/event-payloads/breadcrumbs/#breadcrumb-types
*/
type?:
| 'default'
| 'debug'
| 'error'
| 'navigation'
| 'http'
| 'info'
| 'query'
| 'transaction'
| 'ui'
| 'user'
/**
* Passed through to `Sentry.captureException`
*
* @see https://github.com/getsentry/sentry-javascript/blob/903addf9a1a1534a6cb2ba3143654b918a86f6dd/packages/types/src/misc.ts#L65
*/
tags?: {
[key: string]:
| number
| string
| boolean
| bigint
| symbol
| null
| undefined
}
/**
* Any additional data, passed through to Sentry as `extra` param on
* exceptions, or the `data` param on breadcrumbs.
*/
[key: string]: unknown
} & Parameters<typeof Sentry.captureException>[1]
export type ConsoleTransportEntry = {
id: string
timestamp: number
level: LogLevel
message: string | Error
metadata: Metadata
}
+24 -5
View File
@@ -1,13 +1,12 @@
import React from 'react'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {Plural, Trans} from '@lingui/macro'
import {useFocusEffect} from '@react-navigation/native'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {makeRecordUri} from '#/lib/strings/url-helpers'
import {usePostThreadQuery} from '#/state/queries/post-thread'
import {useSetMinimalShellMode} from '#/state/shell'
import {PostLikedBy as PostLikedByComponent} from '#/view/com/post-thread/PostLikedBy'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import * as Layout from '#/components/Layout'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostLikedBy'>
@@ -15,7 +14,12 @@ export const PostLikedByScreen = ({route}: Props) => {
const setMinimalShellMode = useSetMinimalShellMode()
const {name, rkey} = route.params
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
const {_} = useLingui()
const {data: post} = usePostThreadQuery(uri)
let likeCount
if (post?.thread.type === 'post') {
likeCount = post.thread.post.likeCount
}
useFocusEffect(
React.useCallback(() => {
@@ -25,7 +29,22 @@ export const PostLikedByScreen = ({route}: Props) => {
return (
<Layout.Screen>
<ViewHeader title={_(msg`Liked By`)} />
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
{post && (
<>
<Layout.Header.TitleText>
<Trans>Liked By</Trans>
</Layout.Header.TitleText>
<Layout.Header.SubtitleText>
<Plural value={likeCount ?? 0} one="# like" other="# likes" />
</Layout.Header.SubtitleText>
</>
)}
</Layout.Header.Content>
<Layout.Header.Slot />
</Layout.Header.Outer>
<PostLikedByComponent uri={uri} />
</Layout.Screen>
)
+29 -10
View File
@@ -1,15 +1,12 @@
import React from 'react'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {Plural, Trans} from '@lingui/macro'
import {useFocusEffect} from '@react-navigation/native'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {makeRecordUri} from '#/lib/strings/url-helpers'
import {isWeb} from '#/platform/detection'
import {usePostThreadQuery} from '#/state/queries/post-thread'
import {useSetMinimalShellMode} from '#/state/shell'
import {PostQuotes as PostQuotesComponent} from '#/view/com/post-thread/PostQuotes'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {CenteredView} from '#/view/com/util/Views'
import * as Layout from '#/components/Layout'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostQuotes'>
@@ -17,7 +14,12 @@ export const PostQuotesScreen = ({route}: Props) => {
const setMinimalShellMode = useSetMinimalShellMode()
const {name, rkey} = route.params
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
const {_} = useLingui()
const {data: post} = usePostThreadQuery(uri)
let quoteCount
if (post?.thread.type === 'post') {
quoteCount = post.thread.post.quoteCount
}
useFocusEffect(
React.useCallback(() => {
@@ -27,10 +29,27 @@ export const PostQuotesScreen = ({route}: Props) => {
return (
<Layout.Screen>
<CenteredView sideBorders={true}>
<ViewHeader title={_(msg`Quotes`)} showBorder={!isWeb} />
<PostQuotesComponent uri={uri} />
</CenteredView>
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
{post && (
<>
<Layout.Header.TitleText>
<Trans>Quotes</Trans>
</Layout.Header.TitleText>
<Layout.Header.SubtitleText>
<Plural
value={quoteCount ?? 0}
one="# quote"
other="# quotes"
/>
</Layout.Header.SubtitleText>
</>
)}
</Layout.Header.Content>
<Layout.Header.Slot />
</Layout.Header.Outer>
<PostQuotesComponent uri={uri} />
</Layout.Screen>
)
}
+29 -10
View File
@@ -1,15 +1,12 @@
import React from 'react'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {Plural, Trans} from '@lingui/macro'
import {useFocusEffect} from '@react-navigation/native'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {makeRecordUri} from '#/lib/strings/url-helpers'
import {isWeb} from '#/platform/detection'
import {usePostThreadQuery} from '#/state/queries/post-thread'
import {useSetMinimalShellMode} from '#/state/shell'
import {PostRepostedBy as PostRepostedByComponent} from '#/view/com/post-thread/PostRepostedBy'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {CenteredView} from '#/view/com/util/Views'
import * as Layout from '#/components/Layout'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostRepostedBy'>
@@ -17,7 +14,12 @@ export const PostRepostedByScreen = ({route}: Props) => {
const {name, rkey} = route.params
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
const setMinimalShellMode = useSetMinimalShellMode()
const {_} = useLingui()
const {data: post} = usePostThreadQuery(uri)
let quoteCount
if (post?.thread.type === 'post') {
quoteCount = post.thread.post.repostCount
}
useFocusEffect(
React.useCallback(() => {
@@ -27,10 +29,27 @@ export const PostRepostedByScreen = ({route}: Props) => {
return (
<Layout.Screen>
<CenteredView sideBorders={true}>
<ViewHeader title={_(msg`Reposted By`)} showBorder={!isWeb} />
<PostRepostedByComponent uri={uri} />
</CenteredView>
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
{post && (
<>
<Layout.Header.TitleText>
<Trans>Reposted By</Trans>
</Layout.Header.TitleText>
<Layout.Header.SubtitleText>
<Plural
value={quoteCount ?? 0}
one="# repost"
other="# reposts"
/>
</Layout.Header.SubtitleText>
</>
)}
</Layout.Header.Content>
<Layout.Header.Slot />
</Layout.Header.Outer>
<PostRepostedByComponent uri={uri} />
</Layout.Screen>
)
}
+1 -1
View File
@@ -30,7 +30,7 @@ export function ProfileHeaderMetrics({
return (
<View
style={[a.flex_row, a.gap_sm, a.align_center, a.pb_md]}
style={[a.flex_row, a.gap_sm, a.align_center]}
pointerEvents="box-none">
<InlineLinkText
testID="profileHeaderFollowersButton"
@@ -291,10 +291,12 @@ let ProfileHeaderLabeler = ({
},
}}
size="tiny"
label={plural(likeCount, {
one: 'Liked by # user',
other: 'Liked by # users',
})}>
label={_(
msg`Liked by ${plural(likeCount, {
one: '# user',
other: '# users',
})}`,
)}>
{({hovered, focused, pressed}) => (
<Text
style={[
@@ -304,11 +306,14 @@ let ProfileHeaderLabeler = ({
(hovered || focused || pressed) &&
t.atoms.text_contrast_high,
]}>
<Plural
value={likeCount}
one="Liked by # user"
other="Liked by # users"
/>
<Trans>
Liked by{' '}
<Plural
value={likeCount}
one="# user"
other="# users"
/>
</Trans>
</Text>
)}
</Link>
@@ -244,7 +244,7 @@ let ProfileHeaderStandard = ({
<ProfileHeaderHandle profile={profile} />
</View>
{!isPlaceholderProfile && !isBlockedUser && (
<>
<View style={a.gap_md}>
<ProfileHeaderMetrics profile={profile} />
{descriptionRT && !moderation.ui('profileView').blur ? (
<View pointerEvents="auto">
@@ -262,14 +262,14 @@ let ProfileHeaderStandard = ({
{!isMe &&
!isBlockedUser &&
shouldShowKnownFollowers(profile.viewer?.knownFollowers) && (
<View style={[a.flex_row, a.align_center, a.gap_sm, a.pt_md]}>
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
<KnownFollowers
profile={profile}
moderationOpts={moderationOpts}
/>
</View>
)}
</>
</View>
)}
</View>
<Prompt.Basic
+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')}
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>
-260
View File
@@ -1,260 +0,0 @@
import React from 'react'
import {Alert, View} from 'react-native'
import {Image} from 'expo-image'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import * as AppIcon from '@mozzius/expo-dynamic-app-icon'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {PressableScale} from '#/lib/custom-animations/PressableScale'
import {CommonNavigatorParams} from '#/lib/routes/types'
import {isAndroid} from '#/platform/detection'
import {atoms as a, platform} from '#/alf'
import * as Layout from '#/components/Layout'
import {Text} from '#/components/Typography'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppIconSettings'>
export function AppIconSettingsScreen({}: Props) {
const {_} = useLingui()
const sets = useAppIconSets()
return (
<Layout.Screen>
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
<Layout.Header.TitleText>
<Trans>App Icon</Trans>
</Layout.Header.TitleText>
</Layout.Header.Content>
<Layout.Header.Slot />
</Layout.Header.Outer>
<Layout.Content
contentContainerStyle={[a.py_2xl, a.px_xl, {paddingBottom: 100}]}>
<Text style={[a.text_lg, a.font_heavy]}>Defaults</Text>
<View style={[a.flex_row, a.flex_wrap]}>
{sets.defaults.map(icon => (
<View
style={[{width: '50%'}, a.py_lg, a.px_xs, a.align_center]}
key={icon.id}>
<PressableScale
accessibilityLabel={icon.name}
accessibilityHint={_(msg`Tap to change app icon`)}
targetScale={0.95}
onPress={() => AppIcon.setAppIcon(icon.id)}>
<Image
source={platform({
ios: icon.iosImage(),
android: icon.androidImage(),
})}
style={[
{width: 100, height: 100},
platform({
ios: {borderRadius: 20},
android: a.rounded_full,
}),
a.curve_continuous,
]}
accessibilityIgnoresInvertColors
/>
</PressableScale>
<Text style={[a.text_center, a.font_bold, a.text_md, a.mt_md]}>
{icon.name}
</Text>
</View>
))}
</View>
<Text style={[a.text_lg, a.font_heavy]}>Bluesky+</Text>
<View style={[a.flex_row, a.flex_wrap]}>
{sets.core.map(icon => (
<View
style={[{width: '50%'}, a.py_lg, a.px_xs, a.align_center]}
key={icon.id}>
<PressableScale
accessibilityLabel={icon.name}
accessibilityHint={_(msg`Tap to change app icon`)}
targetScale={0.95}
onPress={() => {
if (isAndroid) {
Alert.alert(
_(msg`Change app icon to "${icon.name}"`),
_(msg`The app will be restarted`),
[
{
text: _(msg`Cancel`),
style: 'cancel',
},
{
text: _(msg`OK`),
onPress: () => {
AppIcon.setAppIcon(icon.id)
},
style: 'default',
},
],
)
} else {
AppIcon.setAppIcon(icon.id)
}
}}>
<Image
source={platform({
ios: icon.iosImage(),
android: icon.androidImage(),
})}
style={[
{width: 100, height: 100},
platform({
ios: {borderRadius: 20},
android: a.rounded_full,
}),
a.curve_continuous,
a.shadow_lg,
]}
accessibilityIgnoresInvertColors
/>
</PressableScale>
<Text
style={[a.text_center, a.font_bold, a.text_md, a.mt_md]}
// for Classic™
emoji>
{icon.name}
</Text>
</View>
))}
</View>
</Layout.Content>
</Layout.Screen>
)
}
function useAppIconSets() {
const {_} = useLingui()
return React.useMemo(() => {
const defaults = [
{
id: 'default_light',
name: _('Light'),
iosImage: () => {
return require(`../../../assets/app-icons/ios_icon_default_light.png`)
},
androidImage: () => {
return require(`../../../assets/app-icons/android_icon_default_light.png`)
},
},
{
id: 'default_dark',
name: _('Dark'),
iosImage: () => {
return require(`../../../assets/app-icons/ios_icon_default_dark.png`)
},
androidImage: () => {
return require(`../../../assets/app-icons/android_icon_default_dark.png`)
},
},
]
/**
* Bluesky+
*/
const core = [
{
id: 'core_aurora',
name: _('Aurora'),
iosImage: () => {
return require(`../../../assets/app-icons/ios_icon_core_aurora.png`)
},
androidImage: () => {
return require(`../../../assets/app-icons/android_icon_core_aurora.png`)
},
},
// {
// id: 'core_bonfire',
// name: _('Bonfire'),
// iosImage: () => {
// return require(`../../../assets/app-icons/ios_icon_core_bonfire.png`)
// },
// androidImage: () => {
// return require(`../../../assets/app-icons/android_icon_core_bonfire.png`)
// },
// },
{
id: 'core_sunrise',
name: _('Sunrise'),
iosImage: () => {
return require(`../../../assets/app-icons/ios_icon_core_sunrise.png`)
},
androidImage: () => {
return require(`../../../assets/app-icons/android_icon_core_sunrise.png`)
},
},
{
id: 'core_sunset',
name: _('Sunset'),
iosImage: () => {
return require(`../../../assets/app-icons/ios_icon_core_sunset.png`)
},
androidImage: () => {
return require(`../../../assets/app-icons/android_icon_core_sunset.png`)
},
},
{
id: 'core_midnight',
name: _('Midnight'),
iosImage: () => {
return require(`../../../assets/app-icons/ios_icon_core_midnight.png`)
},
androidImage: () => {
return require(`../../../assets/app-icons/android_icon_core_midnight.png`)
},
},
{
id: 'core_flat_blue',
name: _('Flat Blue'),
iosImage: () => {
return require(`../../../assets/app-icons/ios_icon_core_flat_blue.png`)
},
androidImage: () => {
return require(`../../../assets/app-icons/android_icon_core_flat_blue.png`)
},
},
{
id: 'core_flat_white',
name: _('Flat White'),
iosImage: () => {
return require(`../../../assets/app-icons/ios_icon_core_flat_white.png`)
},
androidImage: () => {
return require(`../../../assets/app-icons/android_icon_core_flat_white.png`)
},
},
{
id: 'core_flat_black',
name: _('Flat Black'),
iosImage: () => {
return require(`../../../assets/app-icons/ios_icon_core_flat_black.png`)
},
androidImage: () => {
return require(`../../../assets/app-icons/android_icon_core_flat_black.png`)
},
},
{
id: 'core_classic',
name: _('Bluesky Classic™'),
iosImage: () => {
return require(`../../../assets/app-icons/ios_icon_core_classic.png`)
},
androidImage: () => {
return require(`../../../assets/app-icons/android_icon_core_classic.png`)
},
},
]
return {
defaults,
core,
}
}, [_])
}
@@ -0,0 +1,33 @@
import {Image} from 'expo-image'
import {AppIconSet} from '#/screens/Settings/AppIconSettings/types'
import {atoms as a, platform, useTheme} from '#/alf'
export function AppIconImage({
icon,
size = 50,
}: {
icon: AppIconSet
size: number
}) {
const t = useTheme()
return (
<Image
source={platform({
ios: icon.iosImage(),
android: icon.androidImage(),
})}
style={[
{width: size, height: size},
platform({
ios: {borderRadius: size / 5},
android: a.rounded_full,
}),
a.curve_continuous,
t.atoms.border_contrast_medium,
a.border,
]}
accessibilityIgnoresInvertColors
/>
)
}
@@ -0,0 +1,29 @@
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {AppIconImage} from '#/screens/Settings/AppIconSettings/AppIconImage'
import {useCurrentAppIcon} from '#/screens/Settings/AppIconSettings/useCurrentAppIcon'
import * as SettingsList from '#/screens/Settings/components/SettingsList'
import {atoms as a} from '#/alf'
import {Shapes_Stroke2_Corner0_Rounded as Shapes} from '#/components/icons/Shapes'
export function SettingsListItem() {
const {_} = useLingui()
const icon = useCurrentAppIcon()
return (
<SettingsList.LinkItem
to="/settings/app-icon"
label={_(msg`App Icon`)}
contentContainerStyle={[a.align_start]}>
<SettingsList.ItemIcon icon={Shapes} />
<View style={[a.flex_1]}>
<SettingsList.ItemText style={[a.pt_xs, a.pb_md]}>
<Trans>App Icon</Trans>
</SettingsList.ItemText>
<AppIconImage icon={icon} size={60} />
</View>
</SettingsList.LinkItem>
)
}
@@ -0,0 +1 @@
export function SettingsListItem() {}
@@ -0,0 +1,244 @@
import {useState} from 'react'
import {Alert, View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import * as DynamicAppIcon from '@mozzius/expo-dynamic-app-icon'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {DISCOVER_DEBUG_DIDS} from '#/lib/constants'
import {PressableScale} from '#/lib/custom-animations/PressableScale'
import {CommonNavigatorParams} from '#/lib/routes/types'
import {isAndroid} from '#/platform/detection'
import {useSession} from '#/state/session'
import {AppIconImage} from '#/screens/Settings/AppIconSettings/AppIconImage'
import {AppIconSet} from '#/screens/Settings/AppIconSettings/types'
import {useAppIconSets} from '#/screens/Settings/AppIconSettings/useAppIconSets'
import {atoms as a, useTheme} from '#/alf'
import * as Toggle from '#/components/forms/Toggle'
import * as Layout from '#/components/Layout'
import {Text} from '#/components/Typography'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppIconSettings'>
export function AppIconSettingsScreen({}: Props) {
const t = useTheme()
const {_} = useLingui()
const sets = useAppIconSets()
const {currentAccount} = useSession()
const [currentAppIcon, setCurrentAppIcon] = useState(() =>
getAppIconName(DynamicAppIcon.getAppIcon()),
)
const onSetAppIcon = (icon: string) => {
if (isAndroid) {
const next =
sets.defaults.find(i => i.id === icon) ??
sets.core.find(i => i.id === icon)
Alert.alert(
next
? _(msg`Change app icon to "${next.name}"`)
: _(msg`Change app icon`),
// to determine - can we stop this happening? -sfn
_(msg`The app will be restarted`),
[
{
text: _(msg`Cancel`),
style: 'cancel',
},
{
text: _(msg`OK`),
onPress: () => {
setCurrentAppIcon(setAppIcon(icon))
},
style: 'default',
},
],
)
} else {
setCurrentAppIcon(setAppIcon(icon))
}
}
return (
<Layout.Screen>
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
<Layout.Header.TitleText>
<Trans>App Icon</Trans>
</Layout.Header.TitleText>
</Layout.Header.Content>
<Layout.Header.Slot />
</Layout.Header.Outer>
<Layout.Content contentContainerStyle={[a.p_lg]}>
<Group
label={_(msg`Default icons`)}
value={currentAppIcon}
onChange={onSetAppIcon}>
{sets.defaults.map((icon, i) => (
<Row
key={icon.id}
icon={icon}
isEnd={i === sets.defaults.length - 1}>
<AppIcon icon={icon} key={icon.id} size={40} />
<RowText>{icon.name}</RowText>
</Row>
))}
</Group>
{DISCOVER_DEBUG_DIDS[currentAccount?.did ?? ''] && (
<>
<Text
style={[
a.text_md,
a.mt_xl,
a.mb_sm,
a.font_bold,
t.atoms.text_contrast_medium,
]}>
<Trans>Bluesky+</Trans>
</Text>
<Group
label={_(msg`Bluesky+ icons`)}
value={currentAppIcon}
onChange={onSetAppIcon}>
{sets.core.map((icon, i) => (
<Row
key={icon.id}
icon={icon}
isEnd={i === sets.core.length - 1}>
<AppIcon icon={icon} key={icon.id} size={40} />
<RowText>{icon.name}</RowText>
</Row>
))}
</Group>
</>
)}
</Layout.Content>
</Layout.Screen>
)
}
function setAppIcon(icon: string) {
if (icon === 'default_light') {
return getAppIconName(DynamicAppIcon.setAppIcon(null))
} else {
return getAppIconName(DynamicAppIcon.setAppIcon(icon))
}
}
function getAppIconName(icon: string | false) {
if (!icon || icon === 'DEFAULT') {
return 'default_light'
} else {
return icon
}
}
function Group({
children,
label,
value,
onChange,
}: {
children: React.ReactNode
label: string
value: string
onChange: (value: string) => void
}) {
return (
<Toggle.Group
type="radio"
label={label}
values={[value]}
maxSelections={1}
onChange={vals => {
if (vals[0]) onChange(vals[0])
}}>
<View style={[a.flex_1, a.rounded_md, a.overflow_hidden]}>
{children}
</View>
</Toggle.Group>
)
}
function Row({
icon,
children,
isEnd,
}: {
icon: AppIconSet
children: React.ReactNode
isEnd: boolean
}) {
const t = useTheme()
const {_} = useLingui()
return (
<Toggle.Item label={_(msg`Set app icon to ${icon.name}`)} name={icon.id}>
{({hovered, pressed}) => (
<View
style={[
a.flex_1,
a.p_md,
a.flex_row,
a.gap_md,
a.align_center,
t.atoms.bg_contrast_25,
(hovered || pressed) && t.atoms.bg_contrast_50,
t.atoms.border_contrast_high,
!isEnd && a.border_b,
]}>
{children}
<Toggle.Radio />
</View>
)}
</Toggle.Item>
)
}
function RowText({children}: {children: React.ReactNode}) {
const t = useTheme()
return (
<Text
style={[a.text_md, a.font_bold, a.flex_1, t.atoms.text_contrast_medium]}
emoji>
{children}
</Text>
)
}
function AppIcon({icon, size = 50}: {icon: AppIconSet; size: number}) {
const {_} = useLingui()
return (
<PressableScale
accessibilityLabel={icon.name}
accessibilityHint={_(msg`Tap to change app icon`)}
targetScale={0.95}
onPress={() => {
if (isAndroid) {
Alert.alert(
_(msg`Change app icon to "${icon.name}"`),
_(msg`The app will be restarted`),
[
{
text: _(msg`Cancel`),
style: 'cancel',
},
{
text: _(msg`OK`),
onPress: () => {
DynamicAppIcon.setAppIcon(icon.id)
},
style: 'default',
},
],
)
} else {
DynamicAppIcon.setAppIcon(icon.id)
}
}}>
<AppIconImage icon={icon} size={size} />
</PressableScale>
)
}
@@ -0,0 +1,8 @@
import {ImageSourcePropType} from 'react-native'
export type AppIconSet = {
id: string
name: string
iosImage: () => ImageSourcePropType
androidImage: () => ImageSourcePropType
}
@@ -0,0 +1,134 @@
import {useMemo} from 'react'
import {useLingui} from '@lingui/react'
import {AppIconSet} from '#/screens/Settings/AppIconSettings/types'
export function useAppIconSets() {
const {_} = useLingui()
return useMemo(() => {
const defaults = [
{
id: 'default_light',
name: _('Light'),
iosImage: () => {
return require(`../../../../assets/app-icons/ios_icon_default_light.png`)
},
androidImage: () => {
return require(`../../../../assets/app-icons/android_icon_default_light.png`)
},
},
{
id: 'default_dark',
name: _('Dark'),
iosImage: () => {
return require(`../../../../assets/app-icons/ios_icon_default_dark.png`)
},
androidImage: () => {
return require(`../../../../assets/app-icons/android_icon_default_dark.png`)
},
},
] satisfies AppIconSet[]
/**
* Bluesky+
*/
const core = [
{
id: 'core_aurora',
name: _('Aurora'),
iosImage: () => {
return require(`../../../../assets/app-icons/ios_icon_core_aurora.png`)
},
androidImage: () => {
return require(`../../../../assets/app-icons/android_icon_core_aurora.png`)
},
},
// {
// id: 'core_bonfire',
// name: _('Bonfire'),
// iosImage: () => {
// return require(`../../../../assets/app-icons/ios_icon_core_bonfire.png`)
// },
// androidImage: () => {
// return require(`../../../../assets/app-icons/android_icon_core_bonfire.png`)
// },
// },
{
id: 'core_sunrise',
name: _('Sunrise'),
iosImage: () => {
return require(`../../../../assets/app-icons/ios_icon_core_sunrise.png`)
},
androidImage: () => {
return require(`../../../../assets/app-icons/android_icon_core_sunrise.png`)
},
},
{
id: 'core_sunset',
name: _('Sunset'),
iosImage: () => {
return require(`../../../../assets/app-icons/ios_icon_core_sunset.png`)
},
androidImage: () => {
return require(`../../../../assets/app-icons/android_icon_core_sunset.png`)
},
},
{
id: 'core_midnight',
name: _('Midnight'),
iosImage: () => {
return require(`../../../../assets/app-icons/ios_icon_core_midnight.png`)
},
androidImage: () => {
return require(`../../../../assets/app-icons/android_icon_core_midnight.png`)
},
},
{
id: 'core_flat_blue',
name: _('Flat Blue'),
iosImage: () => {
return require(`../../../../assets/app-icons/ios_icon_core_flat_blue.png`)
},
androidImage: () => {
return require(`../../../../assets/app-icons/android_icon_core_flat_blue.png`)
},
},
{
id: 'core_flat_white',
name: _('Flat White'),
iosImage: () => {
return require(`../../../../assets/app-icons/ios_icon_core_flat_white.png`)
},
androidImage: () => {
return require(`../../../../assets/app-icons/android_icon_core_flat_white.png`)
},
},
{
id: 'core_flat_black',
name: _('Flat Black'),
iosImage: () => {
return require(`../../../../assets/app-icons/ios_icon_core_flat_black.png`)
},
androidImage: () => {
return require(`../../../../assets/app-icons/android_icon_core_flat_black.png`)
},
},
{
id: 'core_classic',
name: _('Bluesky Classic™'),
iosImage: () => {
return require(`../../../../assets/app-icons/ios_icon_core_classic.png`)
},
androidImage: () => {
return require(`../../../../assets/app-icons/android_icon_core_classic.png`)
},
},
] satisfies AppIconSet[]
return {
defaults,
core,
}
}, [_])
}
@@ -0,0 +1,27 @@
import {useCallback, useMemo, useState} from 'react'
import * as DynamicAppIcon from '@mozzius/expo-dynamic-app-icon'
import {useFocusEffect} from '@react-navigation/native'
import {useAppIconSets} from '#/screens/Settings/AppIconSettings/useAppIconSets'
export function useCurrentAppIcon() {
const appIconSets = useAppIconSets()
const [currentAppIcon, setCurrentAppIcon] = useState(() =>
DynamicAppIcon.getAppIcon(),
)
// refresh current icon when screen is focused
useFocusEffect(
useCallback(() => {
setCurrentAppIcon(DynamicAppIcon.getAppIcon())
}, []),
)
return useMemo(() => {
return (
appIconSets.defaults.find(i => i.id === currentAppIcon) ??
appIconSets.core.find(i => i.id === currentAppIcon) ??
appIconSets.defaults[0]
)
}, [appIconSets, currentAppIcon])
}
+2 -10
View File
@@ -13,7 +13,7 @@ import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {isNative} from '#/platform/detection'
import {useSession} from '#/state/session'
import {useSetThemePrefs, useThemePrefs} from '#/state/shell'
import {Logo} from '#/view/icons/Logo'
import {SettingsListItem as AppIconSettingsListItem} from '#/screens/Settings/AppIconSettings/SettingsListItem'
import {atoms as a, native, useAlf, useTheme} from '#/alf'
import * as ToggleButton from '#/components/forms/ToggleButton'
import {Props as SVGIconProps} from '#/components/icons/common'
@@ -181,15 +181,7 @@ export function AppearanceSettingsScreen({}: Props) {
{isNative && DISCOVER_DEBUG_DIDS[currentAccount?.did ?? ''] && (
<>
<SettingsList.Divider />
<SettingsList.LinkItem
to="/settings/app-icon"
label={_(msg`App Icon`)}>
<SettingsList.ItemIcon icon={Logo} />
<SettingsList.ItemText>
<Trans>App Icon</Trans>
</SettingsList.ItemText>
</SettingsList.LinkItem>
<AppIconSettingsListItem />
</>
)}
</Animated.View>
+8 -5
View File
@@ -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>
+4 -3
View File
@@ -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}
+6 -19
View File
@@ -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)
}
})
}
})
+3 -43
View File
@@ -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',
})
}
+18 -12
View File
@@ -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}>
+14 -4
View File
@@ -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"
-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"
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"