Compare commits

..

1 Commits

Author SHA1 Message Date
Eric Bailey 5f6ba8fc02 Update ruby version to latest homebrew stable 2024-12-07 14:22:20 -06:00
272 changed files with 3040 additions and 7437 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
# Copy this to `.env` and `.env.test` files
BITDRIFT_API_KEY=
SENTRY_AUTH_TOKEN=
EXPO_PUBLIC_ENV=development
EXPO_PUBLIC_LOG_LEVEL=debug
EXPO_PUBLIC_LOG_DEBUG=
EXPO_PUBLIC_BUNDLE_IDENTIFIER=
+1 -1
View File
@@ -54,4 +54,4 @@ jobs:
run: yarn intl:build
- name: Run tests
run: |
NODE_ENV=test yarn test --forceExit
NODE_ENV=test EXPO_PUBLIC_ENV=test yarn test --forceExit
+1 -1
View File
@@ -1 +1 @@
2.7.6
3.3.6
+1 -1
View File
@@ -20,7 +20,7 @@ build-web-embed: ## Compile web embed bundle, copy to bskyweb/embedr* directorie
.PHONY: test
test: ## Run all tests
NODE_ENV=test yarn test
NODE_ENV=test EXPO_PUBLIC_ENV=test yarn test
.PHONY: lint
lint: ## Run style checks and verify syntax
+4 -4
View File
@@ -26,7 +26,7 @@ appId: xyz.blueskyweb.app
- tapOn: "Save"
- assertNotVisible:
id: "createOrEditListModal"
- tapOn: "People"
- tapOn: "About"
- assertVisible: "Good Ppl"
- assertVisible: "They good"
@@ -93,10 +93,10 @@ appId: xyz.blueskyweb.app
- tapOn: "Save"
- assertNotVisible:
id: "createOrEditListModal"
- tapOn: "People"
- tapOn: "About"
- assertVisible: "Good Ppl"
- assertVisible: "They good"
- tapOn: "People"
- tapOn: "About"
- tapOn:
label: "Adds users on curatelists from the list"
@@ -145,7 +145,7 @@ appId: xyz.blueskyweb.app
id: "e2eGotoLists"
- tapOn: "Good Ppl"
- tapOn: "People"
- tapOn: "About"
- assertVisible:
label: "Removes users on curatelists from the list"
id: "user-bob.test"
+16 -8
View File
@@ -11,8 +11,10 @@ appId: xyz.blueskyweb.app
# Pin alice's feed
- extendedWaitUntil:
visible: "Open drawer menu"
- tapOn: "Open drawer menu"
visible:
id: "viewHeaderDrawerBtn"
- tapOn:
id: "viewHeaderDrawerBtn"
- tapOn:
id: "profileCardButton"
- tapOn:
@@ -30,7 +32,8 @@ appId: xyz.blueskyweb.app
text: "alice-favs"
# Set alice-favs first
- tapOn: "Open drawer menu"
- tapOn:
id: "viewHeaderDrawerBtn"
- tapOn:
id: "menuItemButton-Feeds"
- tapOn:
@@ -41,7 +44,8 @@ appId: xyz.blueskyweb.app
- tapOn:
label: "Save button"
id: "saveChangesBtn"
- tapOn: "Go back"
- tapOn:
id: "viewHeaderDrawerBtn"
- assertVisible:
id: "homeScreenFeedTabs-selector-0"
text: "alice-favs"
@@ -50,7 +54,8 @@ appId: xyz.blueskyweb.app
text: "Following"
# Set following first
- tapOn: "Open drawer menu"
- tapOn:
id: "viewHeaderDrawerBtn"
- tapOn:
id: "menuItemButton-Feeds"
- tapOn:
@@ -61,7 +66,8 @@ appId: xyz.blueskyweb.app
- tapOn:
label: "Save button"
id: "saveChangesBtn"
- tapOn: "Go back"
- tapOn:
id: "viewHeaderDrawerBtn"
- assertVisible:
id: "homeScreenFeedTabs-selector-0"
text: "Following"
@@ -70,7 +76,8 @@ appId: xyz.blueskyweb.app
text: "alice-favs"
# Remove following
- tapOn: "Open drawer menu"
- tapOn:
id: "viewHeaderDrawerBtn"
- tapOn:
id: "menuItemButton-Feeds"
- tapOn:
@@ -81,7 +88,8 @@ appId: xyz.blueskyweb.app
- tapOn:
label: "Save button"
id: "saveChangesBtn"
- tapOn: "Go back"
- tapOn:
id: "viewHeaderDrawerBtn"
- assertVisible:
id: "homeScreenFeedTabs-selector-0"
text: "alice-favs"
+1 -3
View File
@@ -27,9 +27,7 @@ appId: xyz.blueskyweb.app
- tapOn:
id: "bottomBarHomeBtn"
- tapOn:
id: "bottomBarHomeBtn"
- tapOn:
id: "bottomBarHomeBtn"
id: "viewHeaderDrawerBtn"
- assertNotVisible: "Feeds ✨"
- tapOn:
+1 -1
View File
@@ -14,7 +14,7 @@ appId: xyz.blueskyweb.app
id: "e2eGotoModeration"
- tapOn:
id: "moderationlistsBtn"
- tapOn: "New list"
- tapOn: "New"
- tapOn:
id: "editNameInput"
- inputText: "Muted Users"
-2
View File
@@ -6,8 +6,6 @@ appId: xyz.blueskyweb.app
- waitForAnimationToEnd
- tapOn: "http://localhost:8081"
- waitForAnimationToEnd
- extendedWaitUntil:
visible: "Continue"
- swipe:
from: "Bluesky"
direction: DOWN
+3 -4
View File
@@ -15,9 +15,9 @@ module.exports = function (config) {
*/
const PLATFORM = process.env.EAS_BUILD_PLATFORM
const IS_DEV = process.env.EXPO_PUBLIC_ENV === 'development'
const IS_TESTFLIGHT = process.env.EXPO_PUBLIC_ENV === 'testflight'
const IS_PRODUCTION = process.env.EXPO_PUBLIC_ENV === 'production'
const IS_DEV = !IS_TESTFLIGHT || !IS_PRODUCTION
const ASSOCIATED_DOMAINS = [
'applinks:bsky.app',
@@ -222,7 +222,6 @@ module.exports = function (config) {
},
],
'react-native-compressor',
'@bitdrift/react-native',
'./plugins/starterPackAppClipExtension/withStarterPackAppClip.js',
'./plugins/withAndroidManifestPlugin.js',
'./plugins/withAndroidManifestFCMIconPlugin.js',
@@ -235,8 +234,8 @@ module.exports = function (config) {
'expo-font',
{
fonts: [
'./assets/fonts/inter/InterVariable.woff2',
'./assets/fonts/inter/InterVariable-Italic.woff2',
'./assets/fonts/inter/InterVariable.ttf',
'./assets/fonts/inter/InterVariable-Italic.ttf',
// Android only
'./assets/fonts/inter/Inter-Regular.otf',
'./assets/fonts/inter/Inter-Italic.otf',
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M7.5 2a1 1 0 0 0-1 1v3.997a6.25 6.25 0 0 1-1.83 4.42l-.377.376A1 1 0 0 0 4 12.5V15a1 1 0 0 0 1 1h6v5a1 1 0 1 0 2 0v-5h6a1 1 0 0 0 1-1v-2.5a1 1 0 0 0-.293-.707l-.376-.377a6.25 6.25 0 0 1-1.831-4.42V3.001a1 1 0 0 0-1-1h-9Z"/></svg>

Before

Width:  |  Height:  |  Size: 323 B

+1 -3
View File
@@ -20,7 +20,6 @@ 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
@@ -34,7 +33,6 @@ 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`
@@ -49,7 +47,7 @@ window.addEventListener('message', event => {
* @returns
*/
function scan(node = document) {
const embeds = node.querySelectorAll<HTMLIFrameElement>('[data-bluesky-uri]')
const embeds = node.querySelectorAll('[data-bluesky-uri]')
for (let i = 0; i < embeds.length; i++) {
const id = String(Math.random()).slice(2)
-17
View File
@@ -1,17 +0,0 @@
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 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"
className="w-full bg-white hover:bg-neutral-50 relative transition-colors max-w-[600px] min-w-[300px] flex border rounded-xl"
onClick={() => {
if (ref.current && href) {
// forwardRef requires preact/compat - let's keep it simple
+14 -20
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 dark:hover:bg-slate-700 border dark:border-slate-600 rounded-lg p-2 gap-1.5 w-full flex flex-col">
className="transition-colors hover:bg-neutral-100 border 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 dark:bg-slate-700 shrink-0">
<div className="w-4 h-4 overflow-hidden rounded-full bg-neutral-300 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 dark:text-textDimmed ml-1">
<span className="text-textLight 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 dark:text-textDimmed">{children}</p>
<p className="text-sm text-textLight">{children}</p>
</div>
)
}
@@ -308,7 +308,7 @@ function ExternalEmbed({
return (
<Link
href={content.external.uri}
className="w-full rounded-lg overflow-hidden border dark:border-slate-600 flex flex-col items-stretch"
className="w-full rounded-lg overflow-hidden border 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 dark:text-textDimmed line-clamp-1">
<p className="text-sm text-textLight 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 dark:text-textDimmed line-clamp-2 mt-0.5">
<p className="text-sm text-textLight line-clamp-2 mt-0.5">
{content.external.description}
</p>
</div>
@@ -345,29 +345,23 @@ function GenericWithImageEmbed({
return (
<Link
href={href}
className="w-full rounded-lg border dark:border-slate-600 py-2 px-3 flex flex-col gap-2">
className="w-full rounded-lg border 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 dark:bg-slate-700 shrink-0"
className="w-8 h-8 rounded-md bg-neutral-300 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 dark:text-textDimmed text-sm">
{subtitle}
</p>
<p className="text-textLight text-sm">{subtitle}</p>
</div>
</div>
{description && (
<p className="text-textLight dark:text-textDimmed text-sm">
{description}
</p>
)}
{description && <p className="text-textLight text-sm">{description}</p>}
</Link>
)
}
@@ -412,7 +406,7 @@ function StarterPackEmbed({
return (
<Link
href={starterPackHref}
className="w-full rounded-lg overflow-hidden border dark:border-slate-600 flex flex-col items-stretch">
className="w-full rounded-lg overflow-hidden border 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">
@@ -421,7 +415,7 @@ function StarterPackEmbed({
<p className="font-semibold leading-[21px]">
{content.record.name}
</p>
<p className="text-sm text-textLight dark:text-textDimmed line-clamp-2 leading-[18px]">
<p className="text-sm text-textLight line-clamp-2 leading-[18px]">
Starter pack by{' '}
{content.creator.displayName || `@${content.creator.handle}`}
</p>
@@ -431,7 +425,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 dark:text-textDimmed mt-1">
<p className="text-sm font-semibold text-textLight mt-1">
{content.joinedAllTimeCount} users have joined!
</p>
)}
+8 -10
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 dark:bg-slate-700 shrink-0">
<div className="w-10 h-10 overflow-hidden rounded-full bg-neutral-300 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 dark:text-textDimmed hover:underline line-clamp-1">
className="text-[15px] text-textLight 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 dark:text-textDimmed mt-1 text-sm hover:underline">
className="text-textLight mt-1 text-sm hover:underline">
{niceDate(post.indexedAt)}
</time>
</Link>
<div className="border-t dark:border-slate-600 w-full pt-2.5 flex items-center gap-5 text-sm cursor-pointer">
<div className="border-t 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 dark:text-neutral-300 mb-px">
<p className="font-bold text-neutral-500 mb-px">
{prettyNumber(post.likeCount)}
</p>
</div>
@@ -85,19 +85,17 @@ 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 dark:text-neutral-300 mb-px">
<p className="font-bold text-neutral-500 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 dark:text-neutral-300 mb-px">
Reply
</p>
<p className="font-bold text-neutral-500 mb-px">Reply</p>
</div>
<div className="flex-1" />
<p className="cursor-pointer text-brand dark:text-brandLighten font-bold hover:underline hidden min-[450px]:inline">
<p className="cursor-pointer text-brand font-bold hover:underline hidden min-[450px]:inline">
{post.replyCount
? `Read ${prettyNumber(post.replyCount)} ${
post.replyCount > 1 ? 'replies' : 'reply'
-4
View File
@@ -5,7 +5,3 @@
.break-word {
word-break: break-word;
}
:root {
color-scheme: light dark;
}
+13 -18
View File
@@ -6,7 +6,6 @@ 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'
@@ -22,8 +21,6 @@ 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',
})
@@ -111,7 +108,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 dark:bg-dimmedBgDarken dark:text-slate-200">
<main className="w-full min-h-screen flex flex-col items-center gap-8 py-14 px-4 md:pt-32">
<Link
href="https://bsky.social/about"
className="transition-transform hover:scale-110">
@@ -124,22 +121,20 @@ 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 dark:bg-dimmedBg dark:border-slate-500"
className="border rounded-lg py-3 w-full max-w-[600px] px-4"
placeholder={DEFAULT_POST}
/>
<img src={arrowBottom} className="w-6 dark:invert" />
<img src={arrowBottom} className="w-6" />
{loading ? (
<div className="w-full max-w-[600px]">
<Skeleton />
</div>
<Skeleton />
) : (
<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-500/10 px-4 py-3 rounded-lg">
<div className="w-full border border-red-500 bg-red-50 px-4 py-3 rounded-lg">
<p className="text-red-500 text-center">{error}</p>
</div>
)}
@@ -154,15 +149,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 dark:bg-slate-700 shrink-0 animate-pulse" />
<div className="w-10 h-10 overflow-hidden rounded-full bg-neutral-100 shrink-0 animate-pulse" />
<div className="flex-1">
<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 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>
</div>
<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 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>
</Container>
)
@@ -225,7 +220,7 @@ function Snippet({thread}: {thread: AppBskyFeedDefs.ThreadViewPost}) {
ref={ref}
type="text"
value={snippet}
className="border rounded-lg py-3 w-full px-4 dark:bg-dimmedBg dark:border-slate-500"
className="border rounded-lg py-3 w-full px-4"
readOnly
autoFocus
onFocus={() => {
@@ -233,7 +228,7 @@ function Snippet({thread}: {thread: AppBskyFeedDefs.ThreadViewPost}) {
}}
/>
<button
className="rounded-lg bg-brand text-white py-3 px-4 whitespace-nowrap min-w-28"
className="rounded-lg bg-brand text-white color-white py-3 px-4 whitespace-nowrap min-w-28"
onClick={() => {
ref.current?.focus()
ref.current?.select()
+3 -6
View File
@@ -4,7 +4,6 @@ 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'
@@ -22,8 +21,6 @@ if (!uri) {
throw new Error('No uri in path')
}
initColorMode()
agent
.getPostThread({
uri,
@@ -58,13 +55,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 dark:text-textDimmed">
<p className="max-w-80 text-center w-full text-textLight">
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 text-center py-1 px-4 w-full mx-auto">
className="max-w-80 rounded-lg bg-brand text-white color-white text-center py-1 px-4 w-full mx-auto">
View on Bluesky
</Link>
</div>
@@ -80,7 +77,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 dark:text-textDimmed">
<p className="my-16 text-center w-full text-textLight">
Post not found, it may have been deleted.
</p>
</Container>
-8
View File
@@ -1,19 +1,11 @@
/** @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)',
},
},
},
+2 -1
View File
@@ -1,3 +1,4 @@
{
"compilerOptions": {
"target": "ES5",
@@ -19,5 +20,5 @@
"jsxFragmentFactory": "Fragment",
"downlevelIteration": true
},
"include": ["src", "snippet", "vite.config.ts"]
"include": ["src", "vite.config.ts"]
}
-1
View File
@@ -235,7 +235,6 @@ func serve(cctx *cli.Context) error {
// generic routes
e.GET("/hashtag/:tag", server.WebGeneric)
e.GET("/topic/:topic", server.WebGeneric)
e.GET("/search", server.WebGeneric)
e.GET("/feeds", server.WebGeneric)
e.GET("/notifications", server.WebGeneric)
+4 -3
View File
@@ -13,7 +13,8 @@
<!-- Hello Humans! API docs at https://atproto.com -->
<link rel="preload" as="font" type="font/ttf" href="{{ staticCDNHost }}/static/media/InterVariable.c504db5c06caaf7cdfba.woff2">
<link rel="preload" as="font" type="font/ttf" href="{{ staticCDNHost }}/static/media/InterVariable.c9f788f6e7ebaec75d7c.ttf">
<link rel="preload" as="font" type="font/ttf" href="{{ staticCDNHost }}/static/media/InterVariable-Italic.55d6a3f35e9b605ba6f4.ttf">
<style>
/**
@@ -25,14 +26,14 @@
*/
@font-face {
font-family: 'InterVariable';
src: url("{{ staticCDNHost }}/static/media/InterVariable.c504db5c06caaf7cdfba.woff2") format('woff2');
src: url("{{ staticCDNHost }}/static/media/InterVariable.c9f788f6e7ebaec75d7c.ttf") format('truetype');
font-weight: 300 1000;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'InterVariableItalic';
src: url("{{ staticCDNHost }}/static/media/InterVariable-Italic.01dcbad1bac635f9c9cd.woff2") format('woff2');
src: url("{{ staticCDNHost }}/static/media/InterVariable-Italic.55d6a3f35e9b605ba6f4.ttf") format('truetype');
font-weight: 300 1000;
font-style: italic;
font-display: swap;
+2 -2
View File
@@ -12,14 +12,14 @@
{# don't include the bundle on non-404 error pages #}
{% block head_bundle %}
{% if statusCode == 404 %}
{{ block.Super }}
{{ super() }}
{% else %}
{% endif %}
{% endblock %}
{%- block body_all %}
{% if statusCode == 404 %}
{{ block.Super }}
{{ super() }}
{% else %}
<h1>{{ statusCode }}: Server Error</h1>
<p>Sorry about that! Our <a href="https://bluesky.statuspage.io/">Status Page</a> might have more context.
-6
View File
@@ -87,12 +87,6 @@ However, if you're a part of the Bluesky team and want to enable Sentry, fill in
If you change `SENTRY_AUTH_TOKEN`, you need to do `yarn prebuild` before running `yarn ios` or `yarn android` again.
### Adding bitdrift
Adding bitdirft is NOT required. You can keep `BITDRIFT_API_KEY=` in `.env` which will avoid initializing bitdrift during startup.
However, if you're a part of the Bluesky team and want to enable bitdrift, fill in `BITDRIFT_API_KEY` in your `.env` to enable bitdrift.
### Adding and Updating Locales
- `yarn intl:build` -> you will also need to run this anytime `./src/locale/{locale}/messages.po` change
+2 -1
View File
@@ -5,8 +5,9 @@ import {LogBox} from 'react-native'
import {registerRootComponent} from 'expo'
import App from '#/App'
import {IS_TEST} from '#/env'
if (process.env.NODE_ENV === 'test') {
if (IS_TEST) {
LogBox.ignoreAllLogs() // suppress all logs in tests
} else {
LogBox.ignoreLogs(['Require cycle:']) // suppress require-cycle warnings, it's fine
-16
View File
@@ -105,19 +105,3 @@ jest.mock('expo-modules-core', () => ({
return () => null
}),
}))
jest.mock('expo-localization', () => ({
getLocales: () => [],
}))
jest.mock('statsig-react-native-expo', () => ({
Statsig: {
initialize() {},
initializeCalled() {
return false
},
},
}))
jest.mock('../src/lib/bitdrift', () => ({}))
jest.mock('../src/lib/statsig/statsig', () => ({}))
-2
View File
@@ -14,8 +14,6 @@ if (process.env.BSKY_PROFILE) {
cfg.cacheVersion += ':PROFILE'
}
cfg.resolver.assetExts = [...cfg.resolver.assetExts, 'woff2']
cfg.resolver.resolveRequest = (context, moduleName, platform) => {
// HACK: manually resolve a few packages that use `exports` in `package.json`.
// A proper solution is to enable `unstable_enablePackageExports` but this needs careful testing.
+15 -16
View File
@@ -54,8 +54,7 @@
"icons:optimize": "svgo -f ./assets/icons"
},
"dependencies": {
"@atproto/api": "^0.13.21",
"@bitdrift/react-native": "0.4.0",
"@atproto/api": "^0.13.18",
"@braintree/sanitize-url": "^6.0.2",
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
"@emoji-mart/react": "^1.1.1",
@@ -63,10 +62,9 @@
"@expo/webpack-config": "^19.0.0",
"@floating-ui/dom": "^1.6.3",
"@floating-ui/react-dom": "^2.0.8",
"@formatjs/intl-datetimeformat": "^6.17.1",
"@formatjs/intl-locale": "^4.2.8",
"@formatjs/intl-numberformat": "^8.15.1",
"@formatjs/intl-pluralrules": "^5.4.1",
"@formatjs/intl-locale": "^4.0.0",
"@formatjs/intl-numberformat": "^8.10.3",
"@formatjs/intl-pluralrules": "^5.2.14",
"@fortawesome/fontawesome-svg-core": "^6.1.1",
"@fortawesome/free-regular-svg-icons": "^6.1.1",
"@fortawesome/free-solid-svg-icons": "^6.1.1",
@@ -76,7 +74,7 @@
"@lingui/react": "^4.14.1",
"@mattermost/react-native-paste-input": "^0.7.1",
"@miblanchard/react-native-slider": "^2.3.1",
"@mozzius/expo-dynamic-app-icon": "^1.5.0",
"@mozzius/expo-dynamic-app-icon": "^1.4.1",
"@radix-ui/react-dismissable-layer": "^1.1.1",
"@radix-ui/react-dropdown-menu": "2.0.1",
"@radix-ui/react-focus-guards": "^1.1.1",
@@ -120,7 +118,7 @@
"emoji-mart": "^5.5.2",
"emoji-regex": "^10.4.0",
"eventemitter3": "^5.0.1",
"expo": "~52.0.19",
"expo": "~52.0.17",
"expo-application": "^6.0.1",
"expo-blur": "^14.0.1",
"expo-build-properties": "^0.13.1",
@@ -128,7 +126,7 @@
"expo-clipboard": "^7.0.0",
"expo-dev-client": "^5.0.4",
"expo-device": "~7.0.1",
"expo-file-system": "^18.0.6",
"expo-file-system": "^18.0.4",
"expo-font": "~13.0.1",
"expo-haptics": "^14.0.0",
"expo-image": "~2.0.3",
@@ -139,13 +137,13 @@
"expo-localization": "~16.0.0",
"expo-media-library": "~17.0.3",
"expo-navigation-bar": "~4.0.4",
"expo-notifications": "~0.29.11",
"expo-notifications": "~0.29.10",
"expo-sharing": "^13.0.0",
"expo-splash-screen": "~0.29.18",
"expo-splash-screen": "~0.29.16",
"expo-status-bar": "~2.0.0",
"expo-system-ui": "^4.0.4",
"expo-task-manager": "~12.0.3",
"expo-updates": "~0.26.10",
"expo-updates": "~0.26.9",
"expo-web-browser": "~14.0.1",
"fast-text-encoding": "^1.0.6",
"history": "^5.3.0",
@@ -183,7 +181,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.17.0-nightly-20241211-17e89ca24",
"react-native-reanimated": "^3.16.3",
"react-native-root-siblings": "^4.1.1",
"react-native-safe-area-context": "4.14.0",
"react-native-screens": "~4.3.0",
@@ -207,12 +205,13 @@
"zod": "^3.20.2"
},
"devDependencies": {
"@atproto/dev-env": "^0.3.67",
"@atproto/dev-env": "^0.3.64",
"@babel/core": "^7.26.0",
"@babel/preset-env": "^7.26.0",
"@babel/runtime": "^7.26.0",
"@did-plc/server": "^0.0.1",
"@expo/config-plugins": "9.0.10",
"@expo/prebuild-config": "8.0.22",
"@lingui/cli": "^4.14.1",
"@lingui/macro": "^4.14.1",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
@@ -267,8 +266,8 @@
"@react-native/normalize-colors": "0.76.1",
"@radix-ui/react-focus-scope": "1.1.0",
"@types/react": "^18",
"**/expo-constants": "17.0.3",
"**/expo-device": "7.0.1",
"**/expo-constants": "16.0.1",
"**/expo-device": "6.0.2",
"**/zod": "3.23.8"
},
"jest": {
-101
View File
@@ -1,101 +0,0 @@
diff --git a/node_modules/expo-image-manipulator/src/ImageManipulator.web.ts b/node_modules/expo-image-manipulator/src/ImageManipulator.web.ts
new file mode 100644
index 0000000..babbb3b
--- /dev/null
+++ b/node_modules/expo-image-manipulator/src/ImageManipulator.web.ts
@@ -0,0 +1,60 @@
+import { useReleasingSharedObject } from 'expo-modules-core';
+import { SharedRef } from 'expo-modules-core/types';
+
+import { Action, ImageResult, SaveFormat, SaveOptions } from './ImageManipulator.types';
+import { ImageManipulatorContext } from './ImageManipulatorContext';
+import ExpoImageManipulator from './NativeImageManipulatorModule';
+import { validateArguments } from './validators';
+
+// @needsAudit
+/**
+ * Manipulate the image provided via `uri`. Available modifications are rotating, flipping (mirroring),
+ * resizing and cropping. Each invocation results in a new file. With one invocation you can provide
+ * a set of actions to perform over the image. Overwriting the source file would not have an effect
+ * in displaying the result as images are cached.
+ * @param uri URI of the file to manipulate. Should be on the local file system or a base64 data URI.
+ * @param actions An array of objects representing manipulation options. Each object should have
+ * __only one__ of the keys that corresponds to specific transformation.
+ * @param saveOptions A map defining how modified image should be saved.
+ * @return Promise which fulfils with [`ImageResult`](#imageresult) object.
+ * @deprecated It has been replaced by the new, contextual and object-oriented API.
+ * Use [`ImageManipulator.manipulate`](#manipulateuri) or [`useImageManipulator`](#useimagemanipulatoruri) instead.
+ */
+export async function manipulateAsync(
+ uri: string,
+ actions: Action[] = [],
+ saveOptions: SaveOptions = {}
+): Promise<ImageResult> {
+ validateArguments(uri, actions, saveOptions);
+
+ const { format = SaveFormat.JPEG, ...rest } = saveOptions;
+ const context = ExpoImageManipulator.manipulate(uri);
+
+ for (const action of actions) {
+ if ('resize' in action) {
+ context.resize(action.resize);
+ } else if ('rotate' in action) {
+ context.rotate(action.rotate);
+ } else if ('flip' in action) {
+ context.flip(action.flip);
+ } else if ('crop' in action) {
+ context.crop(action.crop);
+ } else if ('extent' in action && context.extent) {
+ context.extent(action.extent);
+ }
+ }
+ const image = await context.renderAsync(saveOptions.compress);
+ const result = await image.saveAsync({ format, ...rest });
+
+ // These shared objects will not be used anymore, so free up some memory.
+ context.release();
+ image.release();
+
+ return result;
+}
+
+export function useImageManipulator(source: string | SharedRef<'image'>): ImageManipulatorContext {
+ return useReleasingSharedObject(() => ExpoImageManipulator.manipulate(source), [source]);
+}
+
+export { ExpoImageManipulator as ImageManipulator };
diff --git a/node_modules/expo-image-manipulator/src/ImageManipulatorContext.ts b/node_modules/expo-image-manipulator/src/ImageManipulatorContext.ts
index 120d8d3..f8aa49c 100644
--- a/node_modules/expo-image-manipulator/src/ImageManipulatorContext.ts
+++ b/node_modules/expo-image-manipulator/src/ImageManipulatorContext.ts
@@ -52,7 +52,7 @@ export declare class ImageManipulatorContext extends SharedObject {
/**
* Awaits for all manipulation tasks to finish and resolves with a reference to the resulted native image.
*/
- renderAsync(): Promise<ImageRef>;
+ renderAsync(compress?: number): Promise<ImageRef>;
}
export default ExpoImageManipulator.Context as typeof ImageManipulatorContext;
diff --git a/node_modules/expo-image-manipulator/src/web/ImageManipulatorContext.web.ts b/node_modules/expo-image-manipulator/src/web/ImageManipulatorContext.web.ts
index 428848c..363a57a 100644
--- a/node_modules/expo-image-manipulator/src/web/ImageManipulatorContext.web.ts
+++ b/node_modules/expo-image-manipulator/src/web/ImageManipulatorContext.web.ts
@@ -41,7 +41,7 @@ export default class ImageManipulatorContext extends SharedObject {
return this;
}
- async renderAsync(): Promise<ImageManipulatorImageRef> {
+ async renderAsync(compress?: number): Promise<ImageManipulatorImageRef> {
const canvas = await this.currentTask;
return new Promise((resolve) => {
@@ -49,7 +49,7 @@ export default class ImageManipulatorContext extends SharedObject {
const url = blob ? URL.createObjectURL(blob) : canvas.toDataURL();
resolve(new ImageManipulatorImageRef(url, canvas.width, canvas.height));
- });
+ }, typeof compress === 'number' ? 'image/jpeg' : undefined, compress);
});
}
-3
View File
@@ -1,3 +0,0 @@
## 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
@@ -1,15 +0,0 @@
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 -36
View File
@@ -222,7 +222,7 @@ index 40aaf9c..1c60164 100644
{
[self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
diff --git a/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m b/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m
index e9ce48c..84a6fca 100644
index e9ce48c..ccd9ad6 100644
--- a/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m
+++ b/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m
@@ -159,26 +159,8 @@ - (BOOL)touchesShouldCancelInContentView:(__unused UIView *)view
@@ -329,38 +329,3 @@ index e9ce48c..84a6fca 100644
}
}
@@ -1055,6 +1082,22 @@ -(type)getter \
RCT_SET_AND_PRESERVE_OFFSET(setShowsVerticalScrollIndicator, showsVerticalScrollIndicator, BOOL)
RCT_SET_AND_PRESERVE_OFFSET(setZoomScale, zoomScale, CGFloat);
+- (void)setScrollIndicatorInsets:(UIEdgeInsets)value
+{
+ [_scrollView setScrollIndicatorInsets:value];
+}
+
+- (UIEdgeInsets)scrollIndicatorInsets
+{
+ UIEdgeInsets verticalScrollIndicatorInsets = [_scrollView verticalScrollIndicatorInsets];
+ UIEdgeInsets horizontalScrollIndicatorInsets = [_scrollView horizontalScrollIndicatorInsets];
+ return UIEdgeInsetsMake(
+ verticalScrollIndicatorInsets.top,
+ horizontalScrollIndicatorInsets.left,
+ verticalScrollIndicatorInsets.bottom,
+ horizontalScrollIndicatorInsets.right);
+}
+
- (void)setAutomaticallyAdjustsScrollIndicatorInsets:(BOOL)automaticallyAdjusts API_AVAILABLE(ios(13.0))
{
// `automaticallyAdjustsScrollIndicatorInsets` is available since iOS 13.
diff --git a/node_modules/react-native/React/Views/ScrollView/RCTScrollViewManager.m b/node_modules/react-native/React/Views/ScrollView/RCTScrollViewManager.m
index cd1e7eb..c1d0172 100644
--- a/node_modules/react-native/React/Views/ScrollView/RCTScrollViewManager.m
+++ b/node_modules/react-native/React/Views/ScrollView/RCTScrollViewManager.m
@@ -83,6 +83,7 @@ - (UIView *)view
RCT_EXPORT_VIEW_PROPERTY(scrollEventThrottle, NSTimeInterval)
RCT_EXPORT_VIEW_PROPERTY(zoomScale, CGFloat)
RCT_EXPORT_VIEW_PROPERTY(contentInset, UIEdgeInsets)
+RCT_EXPORT_VIEW_PROPERTY(scrollIndicatorInsets, UIEdgeInsets)
RCT_EXPORT_VIEW_PROPERTY(verticalScrollIndicatorInsets, UIEdgeInsets)
RCT_EXPORT_VIEW_PROPERTY(scrollToOverflowEnabled, BOOL)
RCT_EXPORT_VIEW_PROPERTY(snapToInterval, int)
@@ -1,876 +0,0 @@
diff --git a/node_modules/react-native-drawer-layout/lib/commonjs/index.js b/node_modules/react-native-drawer-layout/lib/commonjs/index.js
index 3dce76e..6c4b3e5 100644
--- a/node_modules/react-native-drawer-layout/lib/commonjs/index.js
+++ b/node_modules/react-native-drawer-layout/lib/commonjs/index.js
@@ -9,6 +9,12 @@ Object.defineProperty(exports, "Drawer", {
return _Drawer.Drawer;
}
});
+Object.defineProperty(exports, "DrawerGestureContext", {
+ enumerable: true,
+ get: function () {
+ return _DrawerGestureContext.DrawerGestureContext;
+ }
+});
Object.defineProperty(exports, "DrawerProgressContext", {
enumerable: true,
get: function () {
@@ -21,6 +27,7 @@ Object.defineProperty(exports, "useDrawerProgress", {
return _useDrawerProgress.useDrawerProgress;
}
});
+var _DrawerGestureContext = require("./utils/DrawerGestureContext.js");
var _DrawerProgressContext = require("./utils/DrawerProgressContext.js");
var _useDrawerProgress = require("./utils/useDrawerProgress.js");
var _Drawer = require("./views/Drawer");
diff --git a/node_modules/react-native-drawer-layout/lib/commonjs/utils/DrawerGestureContext.js b/node_modules/react-native-drawer-layout/lib/commonjs/utils/DrawerGestureContext.js
new file mode 100644
index 0000000..de6d793
--- /dev/null
+++ b/node_modules/react-native-drawer-layout/lib/commonjs/utils/DrawerGestureContext.js
@@ -0,0 +1,11 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.DrawerGestureContext = void 0;
+var React = _interopRequireWildcard(require("react"));
+function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
+function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
+const DrawerGestureContext = exports.DrawerGestureContext = /*#__PURE__*/React.createContext(undefined);
+//# sourceMappingURL=DrawerGestureContext.js.map
\ No newline at end of file
diff --git a/node_modules/react-native-drawer-layout/lib/commonjs/views/Drawer.native.js b/node_modules/react-native-drawer-layout/lib/commonjs/views/Drawer.native.js
index fd65ab8..86a9a6a 100644
--- a/node_modules/react-native-drawer-layout/lib/commonjs/views/Drawer.native.js
+++ b/node_modules/react-native-drawer-layout/lib/commonjs/views/Drawer.native.js
@@ -8,6 +8,7 @@ var React = _interopRequireWildcard(require("react"));
var _reactNative = require("react-native");
var _reactNativeReanimated = _interopRequireWildcard(require("react-native-reanimated"));
var _useLatestCallback = _interopRequireDefault(require("use-latest-callback"));
+var _DrawerGestureContext = require("../utils/DrawerGestureContext.js");
var _DrawerProgressContext = require("../utils/DrawerProgressContext.js");
var _getDrawerWidth = require("../utils/getDrawerWidth.js");
var _GestureHandler = require("./GestureHandler");
@@ -79,38 +80,38 @@ function Drawer({
return () => hideStatusBar(false);
}, [isOpen, hideStatusBarOnOpen, statusBarAnimation, hideStatusBar]);
const interactionHandleRef = React.useRef(null);
- const startInteraction = () => {
+ const startInteraction = React.useCallback(() => {
interactionHandleRef.current = _reactNative.InteractionManager.createInteractionHandle();
- };
- const endInteraction = () => {
+ }, []);
+ const endInteraction = React.useCallback(() => {
if (interactionHandleRef.current != null) {
_reactNative.InteractionManager.clearInteractionHandle(interactionHandleRef.current);
interactionHandleRef.current = null;
}
- };
- const hideKeyboard = () => {
+ }, []);
+ const hideKeyboard = React.useCallback(() => {
if (keyboardDismissMode === 'on-drag') {
_reactNative.Keyboard.dismiss();
}
- };
- const onGestureBegin = () => {
+ }, [keyboardDismissMode]);
+ const onGestureBegin = React.useCallback(() => {
onGestureStart?.();
startInteraction();
hideKeyboard();
hideStatusBar(true);
- };
- const onGestureFinish = () => {
+ }, [onGestureStart, startInteraction, hideKeyboard, hideStatusBar]);
+ const onGestureFinish = React.useCallback(() => {
onGestureEnd?.();
endInteraction();
- };
- const onGestureAbort = () => {
+ }, [onGestureEnd, endInteraction]);
+ const onGestureAbort = React.useCallback(() => {
onGestureCancel?.();
endInteraction();
- };
+ }, [onGestureCancel, endInteraction]);
// FIXME: Currently hitSlop is broken when on Android when drawer is on right
// https://github.com/software-mansion/react-native-gesture-handler/issues/569
- const hitSlop = isRight ?
+ const hitSlop = React.useMemo(() => isRight ?
// Extend hitSlop to the side of the screen when drawer is closed
// This lets the user drag the drawer from the side of the screen
{
@@ -119,7 +120,7 @@ function Drawer({
} : {
left: 0,
width: isOpen ? undefined : swipeEdgeWidth
- };
+ }, [isRight, isOpen, swipeEdgeWidth]);
const touchStartX = (0, _reactNativeReanimated.useSharedValue)(0);
const touchX = (0, _reactNativeReanimated.useSharedValue)(0);
const translationX = (0, _reactNativeReanimated.useSharedValue)(getDrawerTranslationX(open));
@@ -158,40 +159,43 @@ function Drawer({
}, [getDrawerTranslationX, handleAnimationEnd, handleAnimationStart, onClose, onOpen, touchStartX, touchX, translationX]);
React.useEffect(() => toggleDrawer(open), [open, toggleDrawer]);
const startX = (0, _reactNativeReanimated.useSharedValue)(0);
- let pan = _GestureHandler.Gesture?.Pan().onBegin(event => {
- 'worklet';
+ const pan = React.useMemo(() => {
+ let panGesture = _GestureHandler.Gesture?.Pan().onBegin(event => {
+ 'worklet';
- startX.value = translationX.value;
- gestureState.value = event.state;
- touchStartX.value = event.x;
- }).onStart(() => {
- 'worklet';
+ startX.value = translationX.value;
+ gestureState.value = event.state;
+ touchStartX.value = event.x;
+ }).onStart(() => {
+ 'worklet';
- (0, _reactNativeReanimated.runOnJS)(onGestureBegin)();
- }).onChange(event => {
- 'worklet';
+ (0, _reactNativeReanimated.runOnJS)(onGestureBegin)();
+ }).onChange(event => {
+ 'worklet';
- touchX.value = event.x;
- translationX.value = startX.value + event.translationX;
- gestureState.value = event.state;
- }).onEnd((event, success) => {
- 'worklet';
+ touchX.value = event.x;
+ translationX.value = startX.value + event.translationX;
+ gestureState.value = event.state;
+ }).onEnd((event, success) => {
+ 'worklet';
- gestureState.value = event.state;
- if (!success) {
- (0, _reactNativeReanimated.runOnJS)(onGestureAbort)();
+ gestureState.value = event.state;
+ if (!success) {
+ (0, _reactNativeReanimated.runOnJS)(onGestureAbort)();
+ }
+ const nextOpen = Math.abs(event.translationX) > SWIPE_MIN_OFFSET && Math.abs(event.translationX) > swipeMinVelocity || Math.abs(event.translationX) > swipeMinDistance ? drawerPosition === 'left' ?
+ // If swiped to right, open the drawer, otherwise close it
+ (event.velocityX === 0 ? event.translationX : event.velocityX) > 0 :
+ // If swiped to left, open the drawer, otherwise close it
+ (event.velocityX === 0 ? event.translationX : event.velocityX) < 0 : open;
+ toggleDrawer(nextOpen, event.velocityX);
+ (0, _reactNativeReanimated.runOnJS)(onGestureFinish)();
+ }).activeOffsetX([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET]).failOffsetY([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET]).hitSlop(hitSlop).enabled(drawerType !== 'permanent' && swipeEnabled);
+ if (panGesture && configureGestureHandler) {
+ panGesture = configureGestureHandler(panGesture);
}
- const nextOpen = Math.abs(event.translationX) > SWIPE_MIN_OFFSET && Math.abs(event.translationX) > swipeMinVelocity || Math.abs(event.translationX) > swipeMinDistance ? drawerPosition === 'left' ?
- // If swiped to right, open the drawer, otherwise close it
- (event.velocityX === 0 ? event.translationX : event.velocityX) > 0 :
- // If swiped to left, open the drawer, otherwise close it
- (event.velocityX === 0 ? event.translationX : event.velocityX) < 0 : open;
- toggleDrawer(nextOpen, event.velocityX);
- (0, _reactNativeReanimated.runOnJS)(onGestureFinish)();
- }).activeOffsetX([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET]).failOffsetY([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET]).hitSlop(hitSlop).enabled(drawerType !== 'permanent' && swipeEnabled);
- if (pan && configureGestureHandler) {
- pan = configureGestureHandler(pan);
- }
+ return panGesture;
+ }, [configureGestureHandler, drawerPosition, drawerType, gestureState, hitSlop, onGestureBegin, onGestureAbort, onGestureFinish, open, startX, swipeEnabled, swipeMinDistance, swipeMinVelocity, toggleDrawer, touchStartX, touchX, translationX]);
const translateX = (0, _reactNativeReanimated.useDerivedValue)(() => {
// Comment stolen from react-native-gesture-handler/DrawerLayout
//
@@ -254,35 +258,38 @@ function Drawer({
style: [styles.container, style],
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_DrawerProgressContext.DrawerProgressContext.Provider, {
value: progress,
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_GestureHandler.GestureDetector, {
- gesture: pan,
- children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNativeReanimated.default.View, {
- style: [styles.main, {
- flexDirection: drawerType === 'permanent' ? isRight && direction === 'ltr' || !isRight && direction === 'rtl' ? 'row' : 'row-reverse' : 'row'
- }],
- children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNativeReanimated.default.View, {
- style: [styles.content, contentAnimatedStyle],
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
- accessibilityElementsHidden: isOpen && drawerType !== 'permanent',
- importantForAccessibility: isOpen && drawerType !== 'permanent' ? 'no-hide-descendants' : 'auto',
- style: styles.content,
- children: children
- }), drawerType !== 'permanent' ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_Overlay.Overlay, {
- open: open,
- progress: progress,
- onPress: () => toggleDrawer(false),
- style: overlayStyle,
- accessibilityLabel: overlayAccessibilityLabel
- }) : null]
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, {
- removeClippedSubviews: _reactNative.Platform.OS !== 'ios',
- style: [styles.drawer, {
- width: drawerWidth,
- position: drawerType === 'permanent' ? 'relative' : 'absolute',
- zIndex: drawerType === 'back' ? -1 : 0
- }, drawerAnimatedStyle, drawerStyle],
- children: renderDrawerContent()
- })]
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_DrawerGestureContext.DrawerGestureContext.Provider, {
+ value: pan,
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_GestureHandler.GestureDetector, {
+ gesture: pan,
+ children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNativeReanimated.default.View, {
+ style: [styles.main, {
+ flexDirection: drawerType === 'permanent' ? isRight && direction === 'ltr' || !isRight && direction === 'rtl' ? 'row' : 'row-reverse' : 'row'
+ }],
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNativeReanimated.default.View, {
+ style: [styles.content, contentAnimatedStyle],
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
+ accessibilityElementsHidden: isOpen && drawerType !== 'permanent',
+ importantForAccessibility: isOpen && drawerType !== 'permanent' ? 'no-hide-descendants' : 'auto',
+ style: styles.content,
+ children: children
+ }), drawerType !== 'permanent' ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_Overlay.Overlay, {
+ open: open,
+ progress: progress,
+ onPress: () => toggleDrawer(false),
+ style: overlayStyle,
+ accessibilityLabel: overlayAccessibilityLabel
+ }) : null]
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, {
+ removeClippedSubviews: _reactNative.Platform.OS !== 'ios',
+ style: [styles.drawer, {
+ width: drawerWidth,
+ position: drawerType === 'permanent' ? 'relative' : 'absolute',
+ zIndex: drawerType === 'back' ? -1 : 0
+ }, drawerAnimatedStyle, drawerStyle],
+ children: renderDrawerContent()
+ })]
+ })
})
})
})
diff --git a/node_modules/react-native-drawer-layout/lib/module/index.js b/node_modules/react-native-drawer-layout/lib/module/index.js
index a600e51..f08275c 100644
--- a/node_modules/react-native-drawer-layout/lib/module/index.js
+++ b/node_modules/react-native-drawer-layout/lib/module/index.js
@@ -1,5 +1,6 @@
"use strict";
+export { DrawerGestureContext } from "./utils/DrawerGestureContext.js";
export { DrawerProgressContext } from "./utils/DrawerProgressContext.js";
export { useDrawerProgress } from "./utils/useDrawerProgress.js";
export { Drawer } from './views/Drawer';
diff --git a/node_modules/react-native-drawer-layout/lib/module/utils/DrawerGestureContext.js b/node_modules/react-native-drawer-layout/lib/module/utils/DrawerGestureContext.js
new file mode 100644
index 0000000..1adaa9c
--- /dev/null
+++ b/node_modules/react-native-drawer-layout/lib/module/utils/DrawerGestureContext.js
@@ -0,0 +1,5 @@
+"use strict";
+
+import * as React from 'react';
+export const DrawerGestureContext = /*#__PURE__*/React.createContext(undefined);
+//# sourceMappingURL=DrawerGestureContext.js.map
\ No newline at end of file
diff --git a/node_modules/react-native-drawer-layout/lib/module/views/Drawer.native.js b/node_modules/react-native-drawer-layout/lib/module/views/Drawer.native.js
index 6d07126..981d9f8 100644
--- a/node_modules/react-native-drawer-layout/lib/module/views/Drawer.native.js
+++ b/node_modules/react-native-drawer-layout/lib/module/views/Drawer.native.js
@@ -4,6 +4,7 @@ import * as React from 'react';
import { I18nManager, InteractionManager, Keyboard, Platform, StatusBar, StyleSheet, useWindowDimensions, View } from 'react-native';
import Animated, { interpolate, ReduceMotion, runOnJS, useAnimatedStyle, useDerivedValue, useSharedValue, withSpring } from 'react-native-reanimated';
import useLatestCallback from 'use-latest-callback';
+import { DrawerGestureContext } from "../utils/DrawerGestureContext.js";
import { DrawerProgressContext } from "../utils/DrawerProgressContext.js";
import { getDrawerWidth } from "../utils/getDrawerWidth.js";
import { Gesture, GestureDetector, GestureHandlerRootView, GestureState } from './GestureHandler';
@@ -72,38 +73,38 @@ export function Drawer({
return () => hideStatusBar(false);
}, [isOpen, hideStatusBarOnOpen, statusBarAnimation, hideStatusBar]);
const interactionHandleRef = React.useRef(null);
- const startInteraction = () => {
+ const startInteraction = React.useCallback(() => {
interactionHandleRef.current = InteractionManager.createInteractionHandle();
- };
- const endInteraction = () => {
+ }, []);
+ const endInteraction = React.useCallback(() => {
if (interactionHandleRef.current != null) {
InteractionManager.clearInteractionHandle(interactionHandleRef.current);
interactionHandleRef.current = null;
}
- };
- const hideKeyboard = () => {
+ }, []);
+ const hideKeyboard = React.useCallback(() => {
if (keyboardDismissMode === 'on-drag') {
Keyboard.dismiss();
}
- };
- const onGestureBegin = () => {
+ }, [keyboardDismissMode]);
+ const onGestureBegin = React.useCallback(() => {
onGestureStart?.();
startInteraction();
hideKeyboard();
hideStatusBar(true);
- };
- const onGestureFinish = () => {
+ }, [onGestureStart, startInteraction, hideKeyboard, hideStatusBar]);
+ const onGestureFinish = React.useCallback(() => {
onGestureEnd?.();
endInteraction();
- };
- const onGestureAbort = () => {
+ }, [onGestureEnd, endInteraction]);
+ const onGestureAbort = React.useCallback(() => {
onGestureCancel?.();
endInteraction();
- };
+ }, [onGestureCancel, endInteraction]);
// FIXME: Currently hitSlop is broken when on Android when drawer is on right
// https://github.com/software-mansion/react-native-gesture-handler/issues/569
- const hitSlop = isRight ?
+ const hitSlop = React.useMemo(() => isRight ?
// Extend hitSlop to the side of the screen when drawer is closed
// This lets the user drag the drawer from the side of the screen
{
@@ -112,7 +113,7 @@ export function Drawer({
} : {
left: 0,
width: isOpen ? undefined : swipeEdgeWidth
- };
+ }, [isRight, isOpen, swipeEdgeWidth]);
const touchStartX = useSharedValue(0);
const touchX = useSharedValue(0);
const translationX = useSharedValue(getDrawerTranslationX(open));
@@ -151,40 +152,43 @@ export function Drawer({
}, [getDrawerTranslationX, handleAnimationEnd, handleAnimationStart, onClose, onOpen, touchStartX, touchX, translationX]);
React.useEffect(() => toggleDrawer(open), [open, toggleDrawer]);
const startX = useSharedValue(0);
- let pan = Gesture?.Pan().onBegin(event => {
- 'worklet';
+ const pan = React.useMemo(() => {
+ let panGesture = Gesture?.Pan().onBegin(event => {
+ 'worklet';
- startX.value = translationX.value;
- gestureState.value = event.state;
- touchStartX.value = event.x;
- }).onStart(() => {
- 'worklet';
+ startX.value = translationX.value;
+ gestureState.value = event.state;
+ touchStartX.value = event.x;
+ }).onStart(() => {
+ 'worklet';
- runOnJS(onGestureBegin)();
- }).onChange(event => {
- 'worklet';
+ runOnJS(onGestureBegin)();
+ }).onChange(event => {
+ 'worklet';
- touchX.value = event.x;
- translationX.value = startX.value + event.translationX;
- gestureState.value = event.state;
- }).onEnd((event, success) => {
- 'worklet';
+ touchX.value = event.x;
+ translationX.value = startX.value + event.translationX;
+ gestureState.value = event.state;
+ }).onEnd((event, success) => {
+ 'worklet';
- gestureState.value = event.state;
- if (!success) {
- runOnJS(onGestureAbort)();
+ gestureState.value = event.state;
+ if (!success) {
+ runOnJS(onGestureAbort)();
+ }
+ const nextOpen = Math.abs(event.translationX) > SWIPE_MIN_OFFSET && Math.abs(event.translationX) > swipeMinVelocity || Math.abs(event.translationX) > swipeMinDistance ? drawerPosition === 'left' ?
+ // If swiped to right, open the drawer, otherwise close it
+ (event.velocityX === 0 ? event.translationX : event.velocityX) > 0 :
+ // If swiped to left, open the drawer, otherwise close it
+ (event.velocityX === 0 ? event.translationX : event.velocityX) < 0 : open;
+ toggleDrawer(nextOpen, event.velocityX);
+ runOnJS(onGestureFinish)();
+ }).activeOffsetX([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET]).failOffsetY([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET]).hitSlop(hitSlop).enabled(drawerType !== 'permanent' && swipeEnabled);
+ if (panGesture && configureGestureHandler) {
+ panGesture = configureGestureHandler(panGesture);
}
- const nextOpen = Math.abs(event.translationX) > SWIPE_MIN_OFFSET && Math.abs(event.translationX) > swipeMinVelocity || Math.abs(event.translationX) > swipeMinDistance ? drawerPosition === 'left' ?
- // If swiped to right, open the drawer, otherwise close it
- (event.velocityX === 0 ? event.translationX : event.velocityX) > 0 :
- // If swiped to left, open the drawer, otherwise close it
- (event.velocityX === 0 ? event.translationX : event.velocityX) < 0 : open;
- toggleDrawer(nextOpen, event.velocityX);
- runOnJS(onGestureFinish)();
- }).activeOffsetX([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET]).failOffsetY([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET]).hitSlop(hitSlop).enabled(drawerType !== 'permanent' && swipeEnabled);
- if (pan && configureGestureHandler) {
- pan = configureGestureHandler(pan);
- }
+ return panGesture;
+ }, [configureGestureHandler, drawerPosition, drawerType, gestureState, hitSlop, onGestureBegin, onGestureAbort, onGestureFinish, open, startX, swipeEnabled, swipeMinDistance, swipeMinVelocity, toggleDrawer, touchStartX, touchX, translationX]);
const translateX = useDerivedValue(() => {
// Comment stolen from react-native-gesture-handler/DrawerLayout
//
@@ -247,35 +251,38 @@ export function Drawer({
style: [styles.container, style],
children: /*#__PURE__*/_jsx(DrawerProgressContext.Provider, {
value: progress,
- children: /*#__PURE__*/_jsx(GestureDetector, {
- gesture: pan,
- children: /*#__PURE__*/_jsxs(Animated.View, {
- style: [styles.main, {
- flexDirection: drawerType === 'permanent' ? isRight && direction === 'ltr' || !isRight && direction === 'rtl' ? 'row' : 'row-reverse' : 'row'
- }],
- children: [/*#__PURE__*/_jsxs(Animated.View, {
- style: [styles.content, contentAnimatedStyle],
- children: [/*#__PURE__*/_jsx(View, {
- accessibilityElementsHidden: isOpen && drawerType !== 'permanent',
- importantForAccessibility: isOpen && drawerType !== 'permanent' ? 'no-hide-descendants' : 'auto',
- style: styles.content,
- children: children
- }), drawerType !== 'permanent' ? /*#__PURE__*/_jsx(Overlay, {
- open: open,
- progress: progress,
- onPress: () => toggleDrawer(false),
- style: overlayStyle,
- accessibilityLabel: overlayAccessibilityLabel
- }) : null]
- }), /*#__PURE__*/_jsx(Animated.View, {
- removeClippedSubviews: Platform.OS !== 'ios',
- style: [styles.drawer, {
- width: drawerWidth,
- position: drawerType === 'permanent' ? 'relative' : 'absolute',
- zIndex: drawerType === 'back' ? -1 : 0
- }, drawerAnimatedStyle, drawerStyle],
- children: renderDrawerContent()
- })]
+ children: /*#__PURE__*/_jsx(DrawerGestureContext.Provider, {
+ value: pan,
+ children: /*#__PURE__*/_jsx(GestureDetector, {
+ gesture: pan,
+ children: /*#__PURE__*/_jsxs(Animated.View, {
+ style: [styles.main, {
+ flexDirection: drawerType === 'permanent' ? isRight && direction === 'ltr' || !isRight && direction === 'rtl' ? 'row' : 'row-reverse' : 'row'
+ }],
+ children: [/*#__PURE__*/_jsxs(Animated.View, {
+ style: [styles.content, contentAnimatedStyle],
+ children: [/*#__PURE__*/_jsx(View, {
+ accessibilityElementsHidden: isOpen && drawerType !== 'permanent',
+ importantForAccessibility: isOpen && drawerType !== 'permanent' ? 'no-hide-descendants' : 'auto',
+ style: styles.content,
+ children: children
+ }), drawerType !== 'permanent' ? /*#__PURE__*/_jsx(Overlay, {
+ open: open,
+ progress: progress,
+ onPress: () => toggleDrawer(false),
+ style: overlayStyle,
+ accessibilityLabel: overlayAccessibilityLabel
+ }) : null]
+ }), /*#__PURE__*/_jsx(Animated.View, {
+ removeClippedSubviews: Platform.OS !== 'ios',
+ style: [styles.drawer, {
+ width: drawerWidth,
+ position: drawerType === 'permanent' ? 'relative' : 'absolute',
+ zIndex: drawerType === 'back' ? -1 : 0
+ }, drawerAnimatedStyle, drawerStyle],
+ children: renderDrawerContent()
+ })]
+ })
})
})
})
diff --git a/node_modules/react-native-drawer-layout/lib/typescript/commonjs/src/index.d.ts b/node_modules/react-native-drawer-layout/lib/typescript/commonjs/src/index.d.ts
index 7e978f0..a8bce18 100644
--- a/node_modules/react-native-drawer-layout/lib/typescript/commonjs/src/index.d.ts
+++ b/node_modules/react-native-drawer-layout/lib/typescript/commonjs/src/index.d.ts
@@ -1,3 +1,4 @@
+export { DrawerGestureContext } from './utils/DrawerGestureContext';
export { DrawerProgressContext } from './utils/DrawerProgressContext';
export { useDrawerProgress } from './utils/useDrawerProgress';
export { Drawer } from './views/Drawer';
diff --git a/node_modules/react-native-drawer-layout/lib/typescript/commonjs/src/utils/DrawerGestureContext.d.ts b/node_modules/react-native-drawer-layout/lib/typescript/commonjs/src/utils/DrawerGestureContext.d.ts
new file mode 100644
index 0000000..33ffbeb
--- /dev/null
+++ b/node_modules/react-native-drawer-layout/lib/typescript/commonjs/src/utils/DrawerGestureContext.d.ts
@@ -0,0 +1,3 @@
+import * as React from 'react';
+export declare const DrawerGestureContext: React.Context<import("react-native-gesture-handler/lib/typescript/handlers/gestures/panGesture").PanGesture | undefined>;
+//# sourceMappingURL=DrawerGestureContext.d.ts.map
\ No newline at end of file
diff --git a/node_modules/react-native-drawer-layout/lib/typescript/module/src/index.d.ts b/node_modules/react-native-drawer-layout/lib/typescript/module/src/index.d.ts
index 7e978f0..a8bce18 100644
--- a/node_modules/react-native-drawer-layout/lib/typescript/module/src/index.d.ts
+++ b/node_modules/react-native-drawer-layout/lib/typescript/module/src/index.d.ts
@@ -1,3 +1,4 @@
+export { DrawerGestureContext } from './utils/DrawerGestureContext';
export { DrawerProgressContext } from './utils/DrawerProgressContext';
export { useDrawerProgress } from './utils/useDrawerProgress';
export { Drawer } from './views/Drawer';
diff --git a/node_modules/react-native-drawer-layout/lib/typescript/module/src/utils/DrawerGestureContext.d.ts b/node_modules/react-native-drawer-layout/lib/typescript/module/src/utils/DrawerGestureContext.d.ts
new file mode 100644
index 0000000..33ffbeb
--- /dev/null
+++ b/node_modules/react-native-drawer-layout/lib/typescript/module/src/utils/DrawerGestureContext.d.ts
@@ -0,0 +1,3 @@
+import * as React from 'react';
+export declare const DrawerGestureContext: React.Context<import("react-native-gesture-handler/lib/typescript/handlers/gestures/panGesture").PanGesture | undefined>;
+//# sourceMappingURL=DrawerGestureContext.d.ts.map
\ No newline at end of file
diff --git a/node_modules/react-native-drawer-layout/src/index.tsx b/node_modules/react-native-drawer-layout/src/index.tsx
index 0aa6f53..61a37cf 100644
--- a/node_modules/react-native-drawer-layout/src/index.tsx
+++ b/node_modules/react-native-drawer-layout/src/index.tsx
@@ -1,3 +1,4 @@
+export { DrawerGestureContext } from './utils/DrawerGestureContext';
export { DrawerProgressContext } from './utils/DrawerProgressContext';
export { useDrawerProgress } from './utils/useDrawerProgress';
export { Drawer } from './views/Drawer';
diff --git a/node_modules/react-native-drawer-layout/src/utils/DrawerGestureContext.tsx b/node_modules/react-native-drawer-layout/src/utils/DrawerGestureContext.tsx
new file mode 100644
index 0000000..3aac957
--- /dev/null
+++ b/node_modules/react-native-drawer-layout/src/utils/DrawerGestureContext.tsx
@@ -0,0 +1,6 @@
+import * as React from 'react';
+import type { PanGesture } from 'react-native-gesture-handler';
+
+export const DrawerGestureContext = React.createContext<PanGesture | undefined>(
+ undefined
+);
diff --git a/node_modules/react-native-drawer-layout/src/views/Drawer.native.tsx b/node_modules/react-native-drawer-layout/src/views/Drawer.native.tsx
index 9c40f41..ee5d075 100644
--- a/node_modules/react-native-drawer-layout/src/views/Drawer.native.tsx
+++ b/node_modules/react-native-drawer-layout/src/views/Drawer.native.tsx
@@ -21,6 +21,7 @@ import Animated, {
import useLatestCallback from 'use-latest-callback';
import type { DrawerProps } from '../types';
+import { DrawerGestureContext } from '../utils/DrawerGestureContext';
import { DrawerProgressContext } from '../utils/DrawerProgressContext';
import { getDrawerWidth } from '../utils/getDrawerWidth';
import {
@@ -110,47 +111,51 @@ export function Drawer({
const interactionHandleRef = React.useRef<number | null>(null);
- const startInteraction = () => {
+ const startInteraction = React.useCallback(() => {
interactionHandleRef.current = InteractionManager.createInteractionHandle();
- };
+ }, []);
- const endInteraction = () => {
+ const endInteraction = React.useCallback(() => {
if (interactionHandleRef.current != null) {
InteractionManager.clearInteractionHandle(interactionHandleRef.current);
interactionHandleRef.current = null;
}
- };
+ }, []);
- const hideKeyboard = () => {
+ const hideKeyboard = React.useCallback(() => {
if (keyboardDismissMode === 'on-drag') {
Keyboard.dismiss();
}
- };
+ }, [keyboardDismissMode]);
- const onGestureBegin = () => {
+ const onGestureBegin = React.useCallback(() => {
onGestureStart?.();
startInteraction();
hideKeyboard();
hideStatusBar(true);
- };
+ }, [onGestureStart, startInteraction, hideKeyboard, hideStatusBar]);
- const onGestureFinish = () => {
+ const onGestureFinish = React.useCallback(() => {
onGestureEnd?.();
endInteraction();
- };
+ }, [onGestureEnd, endInteraction]);
- const onGestureAbort = () => {
+ const onGestureAbort = React.useCallback(() => {
onGestureCancel?.();
endInteraction();
- };
+ }, [onGestureCancel, endInteraction]);
// FIXME: Currently hitSlop is broken when on Android when drawer is on right
// https://github.com/software-mansion/react-native-gesture-handler/issues/569
- const hitSlop = isRight
- ? // Extend hitSlop to the side of the screen when drawer is closed
- // This lets the user drag the drawer from the side of the screen
- { right: 0, width: isOpen ? undefined : swipeEdgeWidth }
- : { left: 0, width: isOpen ? undefined : swipeEdgeWidth };
+ const hitSlop = React.useMemo(
+ () =>
+ isRight
+ ? // Extend hitSlop to the side of the screen when drawer is closed
+ // This lets the user drag the drawer from the side of the screen
+ { right: 0, width: isOpen ? undefined : swipeEdgeWidth }
+ : { left: 0, width: isOpen ? undefined : swipeEdgeWidth },
+ [isRight, isOpen, swipeEdgeWidth]
+ );
const touchStartX = useSharedValue(0);
const touchX = useSharedValue(0);
@@ -217,53 +222,76 @@ export function Drawer({
const startX = useSharedValue(0);
- let pan = Gesture?.Pan()
- .onBegin((event) => {
- 'worklet';
- startX.value = translationX.value;
- gestureState.value = event.state;
- touchStartX.value = event.x;
- })
- .onStart(() => {
- 'worklet';
- runOnJS(onGestureBegin)();
- })
- .onChange((event) => {
- 'worklet';
- touchX.value = event.x;
- translationX.value = startX.value + event.translationX;
- gestureState.value = event.state;
- })
- .onEnd((event, success) => {
- 'worklet';
- gestureState.value = event.state;
-
- if (!success) {
- runOnJS(onGestureAbort)();
- }
-
- const nextOpen =
- (Math.abs(event.translationX) > SWIPE_MIN_OFFSET &&
- Math.abs(event.translationX) > swipeMinVelocity) ||
- Math.abs(event.translationX) > swipeMinDistance
- ? drawerPosition === 'left'
- ? // If swiped to right, open the drawer, otherwise close it
- (event.velocityX === 0 ? event.translationX : event.velocityX) > 0
- : // If swiped to left, open the drawer, otherwise close it
- (event.velocityX === 0 ? event.translationX : event.velocityX) < 0
- : open;
-
- toggleDrawer(nextOpen, event.velocityX);
- runOnJS(onGestureFinish)();
- })
- .activeOffsetX([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET])
- .failOffsetY([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET])
- .hitSlop(hitSlop)
- .enabled(drawerType !== 'permanent' && swipeEnabled);
-
- if (pan && configureGestureHandler) {
- pan = configureGestureHandler(pan);
- }
+ const pan = React.useMemo(() => {
+ let panGesture = Gesture?.Pan()
+ .onBegin((event) => {
+ 'worklet';
+ startX.value = translationX.value;
+ gestureState.value = event.state;
+ touchStartX.value = event.x;
+ })
+ .onStart(() => {
+ 'worklet';
+ runOnJS(onGestureBegin)();
+ })
+ .onChange((event) => {
+ 'worklet';
+ touchX.value = event.x;
+ translationX.value = startX.value + event.translationX;
+ gestureState.value = event.state;
+ })
+ .onEnd((event, success) => {
+ 'worklet';
+ gestureState.value = event.state;
+
+ if (!success) {
+ runOnJS(onGestureAbort)();
+ }
+
+ const nextOpen =
+ (Math.abs(event.translationX) > SWIPE_MIN_OFFSET &&
+ Math.abs(event.translationX) > swipeMinVelocity) ||
+ Math.abs(event.translationX) > swipeMinDistance
+ ? drawerPosition === 'left'
+ ? // If swiped to right, open the drawer, otherwise close it
+ (event.velocityX === 0 ? event.translationX : event.velocityX) >
+ 0
+ : // If swiped to left, open the drawer, otherwise close it
+ (event.velocityX === 0 ? event.translationX : event.velocityX) <
+ 0
+ : open;
+
+ toggleDrawer(nextOpen, event.velocityX);
+ runOnJS(onGestureFinish)();
+ })
+ .activeOffsetX([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET])
+ .failOffsetY([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET])
+ .hitSlop(hitSlop)
+ .enabled(drawerType !== 'permanent' && swipeEnabled);
+
+ if (panGesture && configureGestureHandler) {
+ panGesture = configureGestureHandler(panGesture);
+ }
+ return panGesture;
+ }, [
+ configureGestureHandler,
+ drawerPosition,
+ drawerType,
+ gestureState,
+ hitSlop,
+ onGestureBegin,
+ onGestureAbort,
+ onGestureFinish,
+ open,
+ startX,
+ swipeEnabled,
+ swipeMinDistance,
+ swipeMinVelocity,
+ toggleDrawer,
+ touchStartX,
+ touchX,
+ translationX,
+ ]);
const translateX = useDerivedValue(() => {
// Comment stolen from react-native-gesture-handler/DrawerLayout
@@ -376,64 +404,66 @@ export function Drawer({
return (
<GestureHandlerRootView style={[styles.container, style]}>
<DrawerProgressContext.Provider value={progress}>
- <GestureDetector gesture={pan}>
- {/* Immediate child of gesture handler needs to be an Animated.View */}
- <Animated.View
- style={[
- styles.main,
- {
- flexDirection:
- drawerType === 'permanent'
- ? (isRight && direction === 'ltr') ||
- (!isRight && direction === 'rtl')
- ? 'row'
- : 'row-reverse'
- : 'row',
- },
- ]}
- >
- <Animated.View style={[styles.content, contentAnimatedStyle]}>
- <View
- accessibilityElementsHidden={
- isOpen && drawerType !== 'permanent'
- }
- importantForAccessibility={
- isOpen && drawerType !== 'permanent'
- ? 'no-hide-descendants'
- : 'auto'
- }
- style={styles.content}
- >
- {children}
- </View>
- {drawerType !== 'permanent' ? (
- <Overlay
- open={open}
- progress={progress}
- onPress={() => toggleDrawer(false)}
- style={overlayStyle}
- accessibilityLabel={overlayAccessibilityLabel}
- />
- ) : null}
- </Animated.View>
+ <DrawerGestureContext.Provider value={pan}>
+ <GestureDetector gesture={pan}>
+ {/* Immediate child of gesture handler needs to be an Animated.View */}
<Animated.View
- removeClippedSubviews={Platform.OS !== 'ios'}
style={[
- styles.drawer,
+ styles.main,
{
- width: drawerWidth,
- position:
- drawerType === 'permanent' ? 'relative' : 'absolute',
- zIndex: drawerType === 'back' ? -1 : 0,
+ flexDirection:
+ drawerType === 'permanent'
+ ? (isRight && direction === 'ltr') ||
+ (!isRight && direction === 'rtl')
+ ? 'row'
+ : 'row-reverse'
+ : 'row',
},
- drawerAnimatedStyle,
- drawerStyle,
]}
>
- {renderDrawerContent()}
+ <Animated.View style={[styles.content, contentAnimatedStyle]}>
+ <View
+ accessibilityElementsHidden={
+ isOpen && drawerType !== 'permanent'
+ }
+ importantForAccessibility={
+ isOpen && drawerType !== 'permanent'
+ ? 'no-hide-descendants'
+ : 'auto'
+ }
+ style={styles.content}
+ >
+ {children}
+ </View>
+ {drawerType !== 'permanent' ? (
+ <Overlay
+ open={open}
+ progress={progress}
+ onPress={() => toggleDrawer(false)}
+ style={overlayStyle}
+ accessibilityLabel={overlayAccessibilityLabel}
+ />
+ ) : null}
+ </Animated.View>
+ <Animated.View
+ removeClippedSubviews={Platform.OS !== 'ios'}
+ style={[
+ styles.drawer,
+ {
+ width: drawerWidth,
+ position:
+ drawerType === 'permanent' ? 'relative' : 'absolute',
+ zIndex: drawerType === 'back' ? -1 : 0,
+ },
+ drawerAnimatedStyle,
+ drawerStyle,
+ ]}
+ >
+ {renderDrawerContent()}
+ </Animated.View>
</Animated.View>
- </Animated.View>
- </GestureDetector>
+ </GestureDetector>
+ </DrawerGestureContext.Provider>
</DrawerProgressContext.Provider>
</GestureHandlerRootView>
);
+21 -26
View File
@@ -1,6 +1,5 @@
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'
@@ -17,7 +16,11 @@ import {useLingui} from '@lingui/react'
import {KeyboardControllerProvider} from '#/lib/hooks/useEnableKeyboardController'
import {QueryProvider} from '#/lib/react-query'
import {Provider as StatsigProvider, tryFetchGates} from '#/lib/statsig/statsig'
import {
initialize,
Provider as StatsigProvider,
tryFetchGates,
} from '#/lib/statsig/statsig'
import {s} from '#/lib/styles'
import {ThemeProvider} from '#/lib/ThemeContext'
import I18nProvider from '#/locale/i18nProvider'
@@ -32,7 +35,6 @@ import {
ensureGeolocationResolved,
Provider as GeolocationProvider,
} from '#/state/geolocation'
import {Provider as HomeBadgeProvider} from '#/state/home-badge'
import {Provider as InvitesStateProvider} from '#/state/invites'
import {Provider as LightboxStateProvider} from '#/state/lightbox'
import {MessagesProvider} from '#/state/messages'
@@ -51,13 +53,11 @@ import {
import {readLastActiveAccount} from '#/state/session/util'
import {Provider as ShellStateProvider} from '#/state/shell'
import {Provider as ComposerProvider} from '#/state/shell/composer'
import {Provider as LightStatusBarProvider} from '#/state/shell/light-status-bar'
import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out'
import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide'
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
import {Provider as TrendingConfigProvider} from '#/state/trending-config'
import {TestCtrls} from '#/view/com/testing/TestCtrls'
import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext'
import * as Toast from '#/view/com/util/Toast'
@@ -99,6 +99,7 @@ function InnerApp() {
if (account) {
await resumeSession(account)
} else {
await initialize()
await tryFetchGates(undefined, 'prefer-fresh-gates')
}
} catch (e) {
@@ -139,24 +140,20 @@ function InnerApp() {
<LoggedOutViewProvider>
<SelectedFeedProvider>
<HiddenRepliesProvider>
<HomeBadgeProvider>
<UnreadNotifsProvider>
<BackgroundNotificationPreferencesProvider>
<MutedThreadsProvider>
<ProgressGuideProvider>
<TrendingConfigProvider>
<GestureHandlerRootView
style={s.h100pct}>
<TestCtrls />
<Shell />
<NuxDialogs />
</GestureHandlerRootView>
</TrendingConfigProvider>
</ProgressGuideProvider>
</MutedThreadsProvider>
</BackgroundNotificationPreferencesProvider>
</UnreadNotifsProvider>
</HomeBadgeProvider>
<UnreadNotifsProvider>
<BackgroundNotificationPreferencesProvider>
<MutedThreadsProvider>
<ProgressGuideProvider>
<GestureHandlerRootView
style={s.h100pct}>
<TestCtrls />
<Shell />
<NuxDialogs />
</GestureHandlerRootView>
</ProgressGuideProvider>
</MutedThreadsProvider>
</BackgroundNotificationPreferencesProvider>
</UnreadNotifsProvider>
</HiddenRepliesProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
@@ -211,9 +208,7 @@ function App() {
<SafeAreaProvider
initialMetrics={initialWindowMetrics}>
<IntentDialogProvider>
<LightStatusBarProvider>
<InnerApp />
</LightStatusBarProvider>
<InnerApp />
</IntentDialogProvider>
</SafeAreaProvider>
</StarterPackProvider>
+13 -22
View File
@@ -22,7 +22,6 @@ import {
ensureGeolocationResolved,
Provider as GeolocationProvider,
} from '#/state/geolocation'
import {Provider as HomeBadgeProvider} from '#/state/home-badge'
import {Provider as InvitesStateProvider} from '#/state/invites'
import {Provider as LightboxStateProvider} from '#/state/lightbox'
import {MessagesProvider} from '#/state/messages'
@@ -41,13 +40,11 @@ import {
import {readLastActiveAccount} from '#/state/session/util'
import {Provider as ShellStateProvider} from '#/state/shell'
import {Provider as ComposerProvider} from '#/state/shell/composer'
import {Provider as LightStatusBarProvider} from '#/state/shell/light-status-bar'
import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out'
import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide'
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
import {Provider as TrendingConfigProvider} from '#/state/trending-config'
import {Provider as ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoWebContext'
import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext'
import * as Toast from '#/view/com/util/Toast'
@@ -122,22 +119,18 @@ function InnerApp() {
<LoggedOutViewProvider>
<SelectedFeedProvider>
<HiddenRepliesProvider>
<HomeBadgeProvider>
<UnreadNotifsProvider>
<BackgroundNotificationPreferencesProvider>
<MutedThreadsProvider>
<SafeAreaProvider>
<ProgressGuideProvider>
<TrendingConfigProvider>
<Shell />
<NuxDialogs />
</TrendingConfigProvider>
</ProgressGuideProvider>
</SafeAreaProvider>
</MutedThreadsProvider>
</BackgroundNotificationPreferencesProvider>
</UnreadNotifsProvider>
</HomeBadgeProvider>
<UnreadNotifsProvider>
<BackgroundNotificationPreferencesProvider>
<MutedThreadsProvider>
<SafeAreaProvider>
<ProgressGuideProvider>
<Shell />
<NuxDialogs />
</ProgressGuideProvider>
</SafeAreaProvider>
</MutedThreadsProvider>
</BackgroundNotificationPreferencesProvider>
</UnreadNotifsProvider>
</HiddenRepliesProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
@@ -188,9 +181,7 @@ function App() {
<PortalProvider>
<StarterPackProvider>
<IntentDialogProvider>
<LightStatusBarProvider>
<InnerApp />
</LightStatusBarProvider>
<InnerApp />
</IntentDialogProvider>
</StarterPackProvider>
</PortalProvider>
+3 -9
View File
@@ -55,7 +55,10 @@ import {NotificationsScreen} from '#/view/screens/Notifications'
import {PostThreadScreen} from '#/view/screens/PostThread'
import {PrivacyPolicyScreen} from '#/view/screens/PrivacyPolicy'
import {ProfileScreen} from '#/view/screens/Profile'
import {ProfileFeedScreen} from '#/view/screens/ProfileFeed'
import {ProfileFeedLikedByScreen} from '#/view/screens/ProfileFeedLikedBy'
import {ProfileFollowersScreen} from '#/view/screens/ProfileFollowers'
import {ProfileFollowsScreen} from '#/view/screens/ProfileFollows'
import {ProfileListScreen} from '#/view/screens/ProfileList'
import {SavedFeeds} from '#/view/screens/SavedFeeds'
import {SearchScreen} from '#/view/screens/Search'
@@ -74,9 +77,6 @@ import {PostLikedByScreen} from '#/screens/Post/PostLikedBy'
import {PostQuotesScreen} from '#/screens/Post/PostQuotes'
import {PostRepostedByScreen} from '#/screens/Post/PostRepostedBy'
import {ProfileKnownFollowersScreen} from '#/screens/Profile/KnownFollowers'
import {ProfileFeedScreen} from '#/screens/Profile/ProfileFeed'
import {ProfileFollowersScreen} from '#/screens/Profile/ProfileFollowers'
import {ProfileFollowsScreen} from '#/screens/Profile/ProfileFollows'
import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy'
import {AppearanceSettingsScreen} from '#/screens/Settings/AppearanceSettings'
import {AppIconSettingsScreen} from '#/screens/Settings/AppIconSettings'
@@ -100,7 +100,6 @@ import {LanguageSettingsScreen} from './screens/Settings/LanguageSettings'
import {PrivacyAndSecuritySettingsScreen} from './screens/Settings/PrivacyAndSecuritySettings'
import {SettingsScreen} from './screens/Settings/Settings'
import {ThreadPreferencesScreen} from './screens/Settings/ThreadPreferences'
import TopicScreen from './screens/Topic'
const navigationRef = createNavigationContainerRef<AllNavigatorParams>()
@@ -377,11 +376,6 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
getComponent={() => HashtagScreen}
options={{title: title(msg`Hashtag`)}}
/>
<Stack.Screen
name="Topic"
getComponent={() => TopicScreen}
options={{title: title(msg`Topic`)}}
/>
<Stack.Screen
name="MessagesConversation"
getComponent={() => MessagesConversationScreen}
-12
View File
@@ -302,18 +302,6 @@ export const atoms = {
border_0: {
borderWidth: 0,
},
border_t_0: {
borderTopWidth: 0,
},
border_b_0: {
borderBottomWidth: 0,
},
border_l_0: {
borderLeftWidth: 0,
},
border_r_0: {
borderRightWidth: 0,
},
border: {
borderWidth: StyleSheet.hairlineWidth,
},
-28
View File
@@ -1,28 +0,0 @@
import {useMemo} from 'react'
import {useMediaQuery} from 'react-responsive'
export type Breakpoint = 'gtPhone' | 'gtMobile' | 'gtTablet'
export function useBreakpoints(): Record<Breakpoint, boolean> & {
activeBreakpoint: Breakpoint | undefined
} {
const gtPhone = useMediaQuery({minWidth: 500})
const gtMobile = useMediaQuery({minWidth: 800})
const gtTablet = useMediaQuery({minWidth: 1300})
return useMemo(() => {
let active: Breakpoint | undefined
if (gtTablet) {
active = 'gtTablet'
} else if (gtMobile) {
active = 'gtMobile'
} else if (gtPhone) {
active = 'gtPhone'
}
return {
activeBreakpoint: active,
gtPhone,
gtMobile,
gtTablet,
}
}, [gtPhone, gtMobile, gtTablet])
}
+13 -2
View File
@@ -1,4 +1,5 @@
import React from 'react'
import {useMediaQuery} from 'react-responsive'
import {
computeFontScaleMultiplier,
@@ -13,14 +14,13 @@ import {BLUE_HUE, GREEN_HUE, RED_HUE} from '#/alf/util/colorGeneration'
import {Device} from '#/storage'
export {atoms} from '#/alf/atoms'
export * from '#/alf/breakpoints'
export * from '#/alf/fonts'
export * as tokens from '#/alf/tokens'
export * from '#/alf/types'
export * from '#/alf/util/flatten'
export * from '#/alf/util/platform'
export * from '#/alf/util/themeSelector'
export * from '#/alf/util/useGutters'
export * from '#/alf/util/useGutterStyles'
export type Alf = {
themeName: ThemeName
@@ -142,3 +142,14 @@ export function useTheme(theme?: ThemeName) {
return theme ? alf.themes[theme] : alf.theme
}, [theme, alf])
}
export function useBreakpoints() {
const gtPhone = useMediaQuery({minWidth: 500})
const gtMobile = useMediaQuery({minWidth: 800})
const gtTablet = useMediaQuery({minWidth: 1300})
return {
gtPhone,
gtMobile,
gtTablet,
}
}
-4
View File
@@ -60,7 +60,6 @@ export function createThemes({
dim: Theme
} {
const color = {
like: '#ec4899',
trueBlack: '#000000',
gray_0: `hsl(${hues.primary}, 20%, ${defaultScale[14]}%)`,
@@ -125,7 +124,6 @@ export function createThemes({
const lightPalette = {
white: color.gray_0,
black: color.gray_1000,
like: color.like,
contrast_25: color.gray_25,
contrast_50: color.gray_50,
@@ -187,7 +185,6 @@ export function createThemes({
const darkPalette: Palette = {
white: color.gray_25,
black: color.trueBlack,
like: color.like,
contrast_25: color.gray_975,
contrast_50: color.gray_950,
@@ -249,7 +246,6 @@ export function createThemes({
const dimPalette: Palette = {
...darkPalette,
black: `hsl(${hues.primary}, 28%, ${dimScale[0]}%)`,
like: color.like,
contrast_25: `hsl(${hues.primary}, 28%, ${dimScale[1]}%)`,
contrast_50: `hsl(${hues.primary}, 28%, ${dimScale[2]}%)`,
-1
View File
@@ -12,7 +12,6 @@ export type ThemeName = 'light' | 'dim' | 'dark'
export type Palette = {
white: string
black: string
like: string
contrast_25: string
contrast_50: string
+3 -1
View File
@@ -110,7 +110,9 @@ export function renderChildrenWithEmoji(
return child.split(EMOJI).map((stringPart, index) => [
stringPart,
emojis[index] ? (
<UITextView {...props} style={[props?.style, {fontFamily: 'System'}]}>
<UITextView
{...props}
style={[props?.style, {color: 'black', fontFamily: 'System'}]}>
{emojis[index]}
</UITextView>
) : null,
+2 -2
View File
@@ -9,7 +9,7 @@ import {useFonts} from 'expo-font'
*/
export function DO_NOT_USE() {
return useFonts({
InterVariable: require('../../../assets/fonts/inter/InterVariable.woff2'),
'InterVariable-Italic': require('../../../assets/fonts/inter/InterVariable-Italic.woff2'),
InterVariable: require('../../../assets/fonts/inter/InterVariable.ttf'),
'InterVariable-Italic': require('../../../assets/fonts/inter/InterVariable-Italic.ttf'),
})
}
+21
View File
@@ -0,0 +1,21 @@
import React from 'react'
import {atoms as a, useBreakpoints, ViewStyleProp} from '#/alf'
export function useGutterStyles({
top,
bottom,
}: {
top?: boolean
bottom?: boolean
} = {}) {
const {gtMobile} = useBreakpoints()
return React.useMemo<ViewStyleProp['style']>(() => {
return [
a.px_lg,
top && a.pt_md,
bottom && a.pb_md,
gtMobile && [a.px_xl, top && a.pt_lg, bottom && a.pb_lg],
]
}, [gtMobile, top, bottom])
}
-66
View File
@@ -1,66 +0,0 @@
import React from 'react'
import {Breakpoint, useBreakpoints} from '#/alf/breakpoints'
import * as tokens from '#/alf/tokens'
type Gutter = 'compact' | 'base' | 'wide' | 0
const gutters: Record<
Exclude<Gutter, 0>,
Record<Breakpoint | 'default', number>
> = {
compact: {
default: tokens.space.sm,
gtPhone: tokens.space.sm,
gtMobile: tokens.space.md,
gtTablet: tokens.space.md,
},
base: {
default: tokens.space.lg,
gtPhone: tokens.space.lg,
gtMobile: tokens.space.xl,
gtTablet: tokens.space.xl,
},
wide: {
default: tokens.space.xl,
gtPhone: tokens.space.xl,
gtMobile: tokens.space._3xl,
gtTablet: tokens.space._3xl,
},
}
type Gutters = {
paddingTop: number
paddingRight: number
paddingBottom: number
paddingLeft: number
}
export function useGutters([all]: [Gutter]): Gutters
export function useGutters([vertical, horizontal]: [Gutter, Gutter]): Gutters
export function useGutters([top, right, bottom, left]: [
Gutter,
Gutter,
Gutter,
Gutter,
]): Gutters
export function useGutters([top, right, bottom, left]: Gutter[]) {
const {activeBreakpoint} = useBreakpoints()
if (right === undefined) {
right = bottom = left = top
} else if (bottom === undefined) {
bottom = top
left = right
}
return React.useMemo(() => {
return {
paddingTop: top === 0 ? 0 : gutters[top][activeBreakpoint || 'default'],
paddingRight:
right === 0 ? 0 : gutters[right][activeBreakpoint || 'default'],
paddingBottom:
bottom === 0 ? 0 : gutters[bottom][activeBreakpoint || 'default'],
paddingLeft:
left === 0 ? 0 : gutters[left][activeBreakpoint || 'default'],
}
}, [activeBreakpoint, top, right, bottom, left])
}
+5 -4
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,9 +210,10 @@ export function Likes({count}: {count: number}) {
const t = useTheme()
return (
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
<Trans>
Liked by <Plural value={count || 0} one="# user" other="# users" />
</Trans>
{plural(count || 0, {
one: 'Liked by # user',
other: 'Liked by # users',
})}
</Text>
)
}
+20 -4
View File
@@ -6,6 +6,7 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {NavigationProp} from '#/lib/routes/types'
import {logEvent} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
@@ -15,6 +16,7 @@ import {FeedDescriptor} from '#/state/queries/post-feed'
import {useProfilesQuery} from '#/state/queries/profile'
import {useSuggestedFollowsByActorQuery} from '#/state/queries/suggested-follows'
import {useSession} from '#/state/session'
import {useProgressGuide} from '#/state/shell/progress-guide'
import * as userActionHistory from '#/state/userActionHistory'
import {SeenPost} from '#/state/userActionHistory'
import {atoms as a, useBreakpoints, useTheme, ViewStyleProp, web} from '#/alf'
@@ -280,8 +282,8 @@ export function ProfileGrid({
profile={profile}
moderationOpts={moderationOpts}
logContext="FeedInterstitial"
color="secondary_inverted"
shape="round"
colorInverted
/>
</ProfileCard.Header>
<ProfileCard.Description profile={profile} numberOfLines={2} />
@@ -504,9 +506,23 @@ export function SuggestedFeeds() {
export function ProgressGuide() {
const t = useTheme()
return (
<View style={[t.atoms.border_contrast_low, a.px_lg, a.py_lg, a.pb_lg]}>
const {isDesktop} = useWebMediaQueries()
const guide = useProgressGuide('like-10-and-follow-7')
if (isDesktop) {
return null
}
return guide ? (
<View
style={[
a.border_t,
t.atoms.border_contrast_low,
a.px_lg,
a.py_lg,
a.pb_lg,
]}>
<ProgressGuideList />
</View>
)
) : null
}
+3 -5
View File
@@ -1,13 +1,11 @@
import {LinearGradient} from 'expo-linear-gradient'
import {atoms as a, tokens, ViewStyleProp} from '#/alf'
import {atoms as a, tokens} from '#/alf'
export function GradientFill({
gradient,
style,
}: ViewStyleProp & {
}: {
gradient:
| typeof tokens.gradients.primary
| typeof tokens.gradients.sky
| typeof tokens.gradients.midnight
| typeof tokens.gradients.sunrise
@@ -28,7 +26,7 @@ export function GradientFill({
}
start={{x: 0, y: 0}}
end={{x: 1, y: 1}}
style={[a.absolute, a.inset_0, style]}
style={[a.absolute, a.inset_0]}
/>
)
}
+3 -5
View File
@@ -83,7 +83,7 @@ export function RegionalNotice() {
)
}
export function LikeCount({likeCount}: {likeCount: number}) {
export function LikeCount({count}: {count: number}) {
const t = useTheme()
return (
<Text
@@ -93,9 +93,7 @@ export function LikeCount({likeCount}: {likeCount: number}) {
t.atoms.text_contrast_medium,
{fontWeight: '600'},
]}>
<Trans>
Liked by <Plural value={likeCount} one="# user" other="# users" />
</Trans>
<Plural value={count} one="Liked by # user" other="Liked by # users" />
</Text>
)
}
@@ -140,7 +138,7 @@ export function Default({
value={labeler.creator.description}
handle={labeler.creator.handle}
/>
{labeler.likeCount ? <LikeCount likeCount={labeler.likeCount} /> : null}
{labeler.likeCount ? <LikeCount count={labeler.likeCount} /> : null}
</Content>
</Outer>
)
+6 -7
View File
@@ -13,7 +13,7 @@ import {
platform,
TextStyleProp,
useBreakpoints,
useGutters,
useGutterStyles,
useTheme,
} from '#/alf'
import {Button, ButtonIcon, ButtonProps} from '#/components/Button'
@@ -34,7 +34,7 @@ export function Outer({
noBottomBorder?: boolean
}) {
const t = useTheme()
const gutters = useGutters([0, 'base'])
const gutter = useGutterStyles()
const {gtMobile} = useBreakpoints()
const {isWithinOffsetView} = useContext(ScrollbarOffsetContext)
@@ -46,10 +46,10 @@ export function Outer({
a.flex_row,
a.align_center,
a.gap_sm,
gutters,
gutter,
platform({
native: [a.pb_xs, {minHeight: 48}],
web: [a.py_xs, {minHeight: 52}],
native: [a.pb_sm, a.pt_xs],
web: [a.py_sm],
}),
t.atoms.border_contrast_low,
gtMobile && [a.mx_auto, {maxWidth: 600}],
@@ -175,8 +175,7 @@ export function TitleText({
gtMobile && a.text_xl,
style,
]}
numberOfLines={2}
emoji>
numberOfLines={2}>
{children}
</Text>
)
+1 -3
View File
@@ -21,7 +21,6 @@ export * as Header from '#/components/Layout/Header'
export type ScreenProps = React.ComponentProps<typeof View> & {
style?: StyleProp<ViewStyle>
noInsetTop?: boolean
}
/**
@@ -29,7 +28,6 @@ export type ScreenProps = React.ComponentProps<typeof View> & {
*/
export const Screen = React.memo(function Screen({
style,
noInsetTop,
...props
}: ScreenProps) {
const {top} = useSafeAreaInsets()
@@ -37,7 +35,7 @@ export const Screen = React.memo(function Screen({
<>
{isWeb && <WebCenterBorders />}
<View
style={[a.util_screen_outer, {paddingTop: noInsetTop ? 0 : top}, style]}
style={[a.util_screen_outer, {paddingTop: top}, style]}
{...props}
/>
</>
+5 -4
View File
@@ -237,9 +237,7 @@ export function Link({
}
export type InlineLinkProps = React.PropsWithChildren<
BaseLinkProps &
TextStyleProp &
Pick<TextProps, 'selectable' | 'numberOfLines'>
BaseLinkProps & TextStyleProp & Pick<TextProps, 'selectable'>
> &
Pick<ButtonProps, 'label'> & {
disableUnderline?: boolean
@@ -275,6 +273,7 @@ export function InlineLinkText({
onIn: onHoverIn,
onOut: onHoverOut,
} = useInteractionState()
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
const flattenedStyle = flatten(style) || {}
return (
@@ -285,7 +284,7 @@ export function InlineLinkText({
{...rest}
style={[
{color: t.palette.primary_500},
hovered &&
(hovered || focused) &&
!disableUnderline && {
...web({
outline: 0,
@@ -299,6 +298,8 @@ export function InlineLinkText({
role="link"
onPress={download ? undefined : onPress}
onLongPress={onLongPress}
onFocus={onFocus}
onBlur={onBlur}
onMouseEnter={onHoverIn}
onMouseLeave={onHoverOut}
accessibilityRole="link"
-20
View File
@@ -10,23 +10,3 @@ export const Context = React.createContext<ContextType>({
export const ItemContext = React.createContext<ItemContextType>({
disabled: false,
})
export function useMenuContext() {
const context = React.useContext(Context)
if (!context) {
throw new Error('useMenuContext must be used within a Context.Provider')
}
return context
}
export function useMenuItemContext() {
const context = React.useContext(ItemContext)
if (!context) {
throw new Error('useMenuItemContext must be used within a Context.Provider')
}
return context
}
+18 -18
View File
@@ -9,12 +9,7 @@ import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {
Context,
ItemContext,
useMenuContext,
useMenuItemContext,
} from '#/components/Menu/context'
import {Context, ItemContext} from '#/components/Menu/context'
import {
ContextType,
GroupProps,
@@ -30,6 +25,10 @@ export {
useDialogControl as useMenuControl,
} from '#/components/Dialog'
export function useMemoControlContext() {
return React.useContext(Context)
}
export function Root({
children,
control,
@@ -48,7 +47,7 @@ export function Root({
}
export function Trigger({children, label, role = 'button'}: TriggerProps) {
const context = useMenuContext()
const {control} = React.useContext(Context)
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
const {
state: pressed,
@@ -58,14 +57,14 @@ export function Trigger({children, label, role = 'button'}: TriggerProps) {
return children({
isNative: true,
control: context.control,
control,
state: {
hovered: false,
focused,
pressed,
},
props: {
onPress: context.control.open,
onPress: control.open,
onFocus,
onBlur,
onPressIn,
@@ -83,7 +82,7 @@ export function Outer({
showCancel?: boolean
style?: StyleProp<ViewStyle>
}>) {
const context = useMenuContext()
const context = React.useContext(Context)
const {_} = useLingui()
return (
@@ -106,7 +105,7 @@ export function Outer({
export function Item({children, label, style, onPress, ...rest}: ItemProps) {
const t = useTheme()
const context = useMenuContext()
const {control} = React.useContext(Context)
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
const {
state: pressed,
@@ -122,9 +121,10 @@ export function Item({children, label, style, onPress, ...rest}: ItemProps) {
onFocus={onFocus}
onBlur={onBlur}
onPress={async e => {
context.control.close(() => {
onPress?.(e)
})
await onPress(e)
if (!e.defaultPrevented) {
control?.close()
}
}}
onPressIn={e => {
onPressIn()
@@ -156,7 +156,7 @@ export function Item({children, label, style, onPress, ...rest}: ItemProps) {
export function ItemText({children, style}: ItemTextProps) {
const t = useTheme()
const {disabled} = useMenuItemContext()
const {disabled} = React.useContext(ItemContext)
return (
<Text
numberOfLines={1}
@@ -177,7 +177,7 @@ export function ItemText({children, style}: ItemTextProps) {
export function ItemIcon({icon: Comp}: ItemIconProps) {
const t = useTheme()
const {disabled} = useMenuItemContext()
const {disabled} = React.useContext(ItemContext)
return (
<Comp
size="lg"
@@ -223,7 +223,7 @@ export function Group({children, style}: GroupProps) {
function Cancel() {
const {_} = useLingui()
const context = useMenuContext()
const {control} = React.useContext(Context)
return (
<Button
@@ -231,7 +231,7 @@ function Cancel() {
size="small"
variant="ghost"
color="secondary"
onPress={() => context.control.close()}>
onPress={() => control.close()}>
<ButtonText>
<Trans>Cancel</Trans>
</ButtonText>
+9 -10
View File
@@ -7,12 +7,7 @@ import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import {atoms as a, flatten, useTheme, web} from '#/alf'
import * as Dialog from '#/components/Dialog'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {
Context,
ItemContext,
useMenuContext,
useMenuItemContext,
} from '#/components/Menu/context'
import {Context, ItemContext} from '#/components/Menu/context'
import {
ContextType,
GroupProps,
@@ -45,6 +40,10 @@ export function useMenuControl(): Dialog.DialogControlProps {
)
}
export function useMemoControlContext() {
return React.useContext(Context)
}
export function Root({
children,
control,
@@ -111,7 +110,7 @@ const RadixTriggerPassThrough = React.forwardRef(
RadixTriggerPassThrough.displayName = 'RadixTriggerPassThrough'
export function Trigger({children, label, role = 'button'}: TriggerProps) {
const {control} = useMenuContext()
const {control} = React.useContext(Context)
const {
state: hovered,
onIn: onMouseEnter,
@@ -204,7 +203,7 @@ export function Outer({
export function Item({children, label, onPress, ...rest}: ItemProps) {
const t = useTheme()
const {control} = useMenuContext()
const {control} = React.useContext(Context)
const {
state: hovered,
onIn: onMouseEnter,
@@ -263,7 +262,7 @@ export function Item({children, label, onPress, ...rest}: ItemProps) {
export function ItemText({children, style}: ItemTextProps) {
const t = useTheme()
const {disabled} = useMenuItemContext()
const {disabled} = React.useContext(ItemContext)
return (
<Text
style={[
@@ -280,7 +279,7 @@ export function ItemText({children, style}: ItemTextProps) {
export function ItemIcon({icon: Comp, position = 'left'}: ItemIconProps) {
const t = useTheme()
const {disabled} = useMenuItemContext()
const {disabled} = React.useContext(ItemContext)
return (
<View
style={[
+1 -6
View File
@@ -285,7 +285,6 @@ export type FollowButtonProps = {
moderationOpts: ModerationOpts
logContext: LogEvents['profile:follow']['logContext'] &
LogEvents['profile:unfollow']['logContext']
colorInverted?: boolean
} & Partial<ButtonProps>
export function FollowButton(props: FollowButtonProps) {
@@ -298,8 +297,6 @@ export function FollowButtonInner({
profile: profileUnshadowed,
moderationOpts,
logContext,
onPress: onPressProp,
colorInverted,
...rest
}: FollowButtonProps) {
const {_} = useLingui()
@@ -324,7 +321,6 @@ export function FollowButtonInner({
)}`,
),
)
onPressProp?.(e)
} catch (err: any) {
if (err?.name !== 'AbortError') {
Toast.show(_(msg`An issue occurred, please try again.`), 'xmark')
@@ -345,7 +341,6 @@ export function FollowButtonInner({
)}`,
),
)
onPressProp?.(e)
} catch (err: any) {
if (err?.name !== 'AbortError') {
Toast.show(_(msg`An issue occurred, please try again.`), 'xmark')
@@ -392,7 +387,7 @@ export function FollowButtonInner({
label={followLabel}
size="small"
variant="solid"
color={colorInverted ? 'secondary_inverted' : 'primary'}
color="primary"
{...rest}
onPress={onPressFollow}>
<ButtonIcon icon={Plus} position={isRound ? undefined : 'left'} />
@@ -1,829 +0,0 @@
import {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'
import {ScrollView, TextInput, useWindowDimensions, View} from 'react-native'
import Animated, {
LayoutAnimationConfig,
LinearTransition,
ZoomInEasyDown,
} from 'react-native-reanimated'
import {AppBskyActorDefs, ModerationOpts} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {cleanError} from '#/lib/strings/errors'
import {logger} from '#/logger'
import {isWeb} from '#/platform/detection'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useActorSearchPaginated} from '#/state/queries/actor-search'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {useSuggestedFollowsByActorQuery} from '#/state/queries/suggested-follows'
import {useSession} from '#/state/session'
import {Follow10ProgressGuide} from '#/state/shell/progress-guide'
import {ListMethods} from '#/view/com/util/List'
import {
popularInterests,
useInterestsDisplayNames,
} from '#/screens/Onboarding/state'
import {
atoms as a,
native,
tokens,
useBreakpoints,
useTheme,
ViewStyleProp,
web,
} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as SearchIcon} from '#/components/icons/MagnifyingGlass2'
import {PersonGroup_Stroke2_Corner2_Rounded as PersonGroupIcon} from '#/components/icons/Person'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import * as ProfileCard from '#/components/ProfileCard'
import {Text} from '#/components/Typography'
import {ListFooter} from '../Lists'
import {ProgressGuideTask} from './Task'
type Item =
| {
type: 'profile'
key: string
profile: AppBskyActorDefs.ProfileView
isSuggestion: boolean
}
| {
type: 'empty'
key: string
message: string
}
| {
type: 'placeholder'
key: string
}
| {
type: 'error'
key: string
}
export function FollowDialog({guide}: {guide: Follow10ProgressGuide}) {
const {_} = useLingui()
const control = Dialog.useDialogControl()
const {gtMobile} = useBreakpoints()
const {height: minHeight} = useWindowDimensions()
return (
<>
<Button
label={_(msg`Find people to follow`)}
onPress={control.open}
size={gtMobile ? 'small' : 'large'}
color="primary"
variant="solid">
<ButtonIcon icon={PersonGroupIcon} />
<ButtonText>
<Trans>Find people to follow</Trans>
</ButtonText>
</Button>
<Dialog.Outer control={control} nativeOptions={{minHeight}}>
<Dialog.Handle />
<DialogInner guide={guide} />
</Dialog.Outer>
</>
)
}
// Fine to keep this top-level.
let lastSelectedInterest = ''
let lastSearchText = ''
function DialogInner({guide}: {guide: Follow10ProgressGuide}) {
const {_} = useLingui()
const interestsDisplayNames = useInterestsDisplayNames()
const {data: preferences} = usePreferencesQuery()
const personalizedInterests = preferences?.interests?.tags
const interests = Object.keys(interestsDisplayNames)
.sort(boostInterests(popularInterests))
.sort(boostInterests(personalizedInterests))
const [selectedInterest, setSelectedInterest] = useState(
() =>
lastSelectedInterest ||
(personalizedInterests && interests.includes(personalizedInterests[0])
? personalizedInterests[0]
: interests[0]),
)
const [searchText, setSearchText] = useState(lastSearchText)
const moderationOpts = useModerationOpts()
const listRef = useRef<ListMethods>(null)
const inputRef = useRef<TextInput>(null)
const [headerHeight, setHeaderHeight] = useState(0)
const {currentAccount} = useSession()
const [suggestedAccounts, setSuggestedAccounts] = useState<
Map<string, AppBskyActorDefs.ProfileView[]>
>(() => new Map())
useEffect(() => {
lastSearchText = searchText
lastSelectedInterest = selectedInterest
}, [searchText, selectedInterest])
const query = searchText || selectedInterest
const {
data: searchResults,
isFetching,
error,
isError,
hasNextPage,
isFetchingNextPage,
fetchNextPage,
} = useActorSearchPaginated({
query,
})
const hasSearchText = !!searchText
const items = useMemo(() => {
const results = searchResults?.pages.flatMap(r => r.actors)
let _items: Item[] = []
const seen = new Set<string>()
if (isError) {
_items.push({
type: 'empty',
key: 'empty',
message: _(msg`We're having network issues, try again`),
})
} else if (results) {
// First pass: search results
for (const profile of results) {
if (profile.did === currentAccount?.did) continue
if (profile.viewer?.following) continue
// my sincere apologies to Jake Gold - your bio is too keyword-filled and
// your page-rank too high, so you're at the top of half the categories -sfn
if (
!hasSearchText &&
profile.did === 'did:plc:tpg43qhh4lw4ksiffs4nbda3' &&
// constrain to 'tech'
selectedInterest !== 'tech'
) {
continue
}
seen.add(profile.did)
_items.push({
type: 'profile',
// Don't share identity across tabs or typing attempts
key: query + ':' + profile.did,
profile,
isSuggestion: false,
})
}
// Second pass: suggestions
_items = _items.flatMap(item => {
if (item.type !== 'profile') {
return item
}
const suggestions = suggestedAccounts.get(item.profile.did)
if (!suggestions) {
return item
}
const itemWithSuggestions = [item]
for (const suggested of suggestions) {
if (seen.has(suggested.did)) {
// Skip search results from previous step or already seen suggestions
continue
}
seen.add(suggested.did)
itemWithSuggestions.push({
type: 'profile',
key: suggested.did,
profile: suggested,
isSuggestion: true,
})
if (itemWithSuggestions.length === 1 + 3) {
break
}
}
return itemWithSuggestions
})
} else {
const placeholders: Item[] = Array(10)
.fill(0)
.map((__, i) => ({
type: 'placeholder',
key: i + '',
}))
_items.push(...placeholders)
}
return _items
}, [
_,
searchResults,
isError,
currentAccount?.did,
hasSearchText,
selectedInterest,
suggestedAccounts,
query,
])
if (searchText && !isFetching && !items.length && !isError) {
items.push({type: 'empty', key: 'empty', message: _(msg`No results`)})
}
const renderItems = useCallback(
({item, index}: {item: Item; index: number}) => {
switch (item.type) {
case 'profile': {
return (
<FollowProfileCard
profile={item.profile}
isSuggestion={item.isSuggestion}
moderationOpts={moderationOpts!}
setSuggestedAccounts={setSuggestedAccounts}
noBorder={index === 0}
/>
)
}
case 'placeholder': {
return <ProfileCardSkeleton key={item.key} />
}
case 'empty': {
return <Empty key={item.key} message={item.message} />
}
default:
return null
}
},
[moderationOpts],
)
const onSelectTab = useCallback(
(interest: string) => {
setSelectedInterest(interest)
inputRef.current?.clear()
setSearchText('')
listRef.current?.scrollToOffset({
offset: 0,
animated: false,
})
},
[setSelectedInterest, setSearchText],
)
const listHeader = (
<Header
guide={guide}
inputRef={inputRef}
listRef={listRef}
searchText={searchText}
onSelectTab={onSelectTab}
setHeaderHeight={setHeaderHeight}
setSearchText={setSearchText}
interests={interests}
selectedInterest={selectedInterest}
interestsDisplayNames={interestsDisplayNames}
/>
)
const onEndReached = useCallback(async () => {
if (isFetchingNextPage || !hasNextPage || isError) return
try {
await fetchNextPage()
} catch (err) {
logger.error('Failed to load more people to follow', {message: err})
}
}, [isFetchingNextPage, hasNextPage, isError, fetchNextPage])
return (
<Dialog.InnerFlatList
ref={listRef}
data={items}
renderItem={renderItems}
ListHeaderComponent={listHeader}
stickyHeaderIndices={[0]}
keyExtractor={(item: Item) => item.key}
style={[
a.px_0,
web([a.py_0, {height: '100vh', maxHeight: 600}]),
native({height: '100%'}),
]}
webInnerContentContainerStyle={a.py_0}
webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]}
keyboardDismissMode="on-drag"
scrollIndicatorInsets={{top: headerHeight}}
initialNumToRender={8}
maxToRenderPerBatch={8}
onEndReached={onEndReached}
itemLayoutAnimation={LinearTransition}
ListFooterComponent={
<ListFooter
isFetchingNextPage={isFetchingNextPage}
error={cleanError(error)}
onRetry={fetchNextPage}
/>
}
/>
)
}
let Header = ({
guide,
inputRef,
listRef,
searchText,
onSelectTab,
setHeaderHeight,
setSearchText,
interests,
selectedInterest,
interestsDisplayNames,
}: {
guide: Follow10ProgressGuide
inputRef: React.RefObject<TextInput>
listRef: React.RefObject<ListMethods>
onSelectTab: (v: string) => void
searchText: string
setHeaderHeight: (v: number) => void
setSearchText: (v: string) => void
interests: string[]
selectedInterest: string
interestsDisplayNames: Record<string, string>
}): React.ReactNode => {
const t = useTheme()
const control = Dialog.useDialogContext()
return (
<View
onLayout={evt => setHeaderHeight(evt.nativeEvent.layout.height)}
style={[
a.relative,
web(a.pt_lg),
native(a.pt_4xl),
a.pb_xs,
a.border_b,
t.atoms.border_contrast_low,
t.atoms.bg,
]}>
<HeaderTop guide={guide} />
<View style={[web(a.pt_xs), a.pb_xs]}>
<SearchInput
inputRef={inputRef}
defaultValue={searchText}
onChangeText={text => {
setSearchText(text)
listRef.current?.scrollToOffset({offset: 0, animated: false})
}}
onEscape={control.close}
/>
<Tabs
onSelectTab={onSelectTab}
interests={interests}
selectedInterest={selectedInterest}
hasSearchText={!!searchText}
interestsDisplayNames={interestsDisplayNames}
/>
</View>
</View>
)
}
Header = memo(Header)
function HeaderTop({guide}: {guide: Follow10ProgressGuide}) {
const {_} = useLingui()
const t = useTheme()
const control = Dialog.useDialogContext()
return (
<View
style={[
a.px_lg,
a.relative,
a.flex_row,
a.justify_between,
a.align_center,
]}>
<Text
style={[
a.z_10,
a.text_lg,
a.font_heavy,
a.leading_tight,
t.atoms.text_contrast_high,
]}>
<Trans>Find people to follow</Trans>
</Text>
<View style={isWeb && {paddingRight: 36}}>
<ProgressGuideTask
current={guide.numFollows + 1}
total={10 + 1}
title={`${guide.numFollows} / 10`}
tabularNumsTitle
/>
</View>
{isWeb ? (
<Button
label={_(msg`Close`)}
size="small"
shape="round"
variant={isWeb ? 'ghost' : 'solid'}
color="secondary"
style={[
a.absolute,
a.z_20,
web({right: -4}),
native({right: 0}),
native({height: 32, width: 32, borderRadius: 16}),
]}
onPress={() => control.close()}>
<ButtonIcon icon={X} size="md" />
</Button>
) : null}
</View>
)
}
let Tabs = ({
onSelectTab,
interests,
selectedInterest,
hasSearchText,
interestsDisplayNames,
}: {
onSelectTab: (tab: string) => void
interests: string[]
selectedInterest: string
hasSearchText: boolean
interestsDisplayNames: Record<string, string>
}): React.ReactNode => {
const listRef = useRef<ScrollView>(null)
const [scrollX, setScrollX] = useState(0)
const [totalWidth, setTotalWidth] = useState(0)
const pendingTabOffsets = useRef<{x: number; width: number}[]>([])
const [tabOffsets, setTabOffsets] = useState<{x: number; width: number}[]>([])
const onInitialLayout = useNonReactiveCallback(() => {
const index = interests.indexOf(selectedInterest)
scrollIntoViewIfNeeded(index)
})
useEffect(() => {
if (tabOffsets) {
onInitialLayout()
}
}, [tabOffsets, onInitialLayout])
function scrollIntoViewIfNeeded(index: number) {
const btnLayout = tabOffsets[index]
if (!btnLayout) return
const viewportLeftEdge = scrollX
const viewportRightEdge = scrollX + totalWidth
const shouldScrollToLeftEdge = viewportLeftEdge > btnLayout.x
const shouldScrollToRightEdge =
viewportRightEdge < btnLayout.x + btnLayout.width
if (shouldScrollToLeftEdge) {
listRef.current?.scrollTo({
x: btnLayout.x - tokens.space.lg,
animated: true,
})
} else if (shouldScrollToRightEdge) {
listRef.current?.scrollTo({
x: btnLayout.x - totalWidth + btnLayout.width + tokens.space.lg,
animated: true,
})
}
}
function handleSelectTab(index: number) {
const tab = interests[index]
onSelectTab(tab)
scrollIntoViewIfNeeded(index)
}
function handleTabLayout(index: number, x: number, width: number) {
if (!tabOffsets.length) {
pendingTabOffsets.current[index] = {x, width}
if (pendingTabOffsets.current.length === interests.length) {
setTabOffsets(pendingTabOffsets.current)
}
}
}
return (
<ScrollView
ref={listRef}
horizontal
contentContainerStyle={[a.gap_sm, a.px_lg]}
showsHorizontalScrollIndicator={false}
decelerationRate="fast"
snapToOffsets={
tabOffsets.length === interests.length
? tabOffsets.map(o => o.x - tokens.space.xl)
: undefined
}
onLayout={evt => setTotalWidth(evt.nativeEvent.layout.width)}
scrollEventThrottle={200} // big throttle
onScroll={evt => setScrollX(evt.nativeEvent.contentOffset.x)}>
{interests.map((interest, i) => {
const active = interest === selectedInterest && !hasSearchText
return (
<Tab
key={interest}
onSelectTab={handleSelectTab}
active={active}
index={i}
interest={interest}
interestsDisplayName={interestsDisplayNames[interest]}
onLayout={handleTabLayout}
/>
)
})}
</ScrollView>
)
}
Tabs = memo(Tabs)
let Tab = ({
onSelectTab,
interest,
active,
index,
interestsDisplayName,
onLayout,
}: {
onSelectTab: (index: number) => void
interest: string
active: boolean
index: number
interestsDisplayName: string
onLayout: (index: number, x: number, width: number) => void
}): React.ReactNode => {
const {_} = useLingui()
const activeText = active ? _(msg` (active)`) : ''
return (
<View
key={interest}
onLayout={e =>
onLayout(index, e.nativeEvent.layout.x, e.nativeEvent.layout.width)
}>
<Button
label={_(msg`Search for "${interestsDisplayName}"${activeText}`)}
variant={active ? 'solid' : 'outline'}
color={active ? 'primary' : 'secondary'}
size="small"
onPress={() => onSelectTab(index)}>
<ButtonIcon icon={SearchIcon} />
<ButtonText>{interestsDisplayName}</ButtonText>
</Button>
</View>
)
}
Tab = memo(Tab)
let FollowProfileCard = ({
profile,
moderationOpts,
isSuggestion,
setSuggestedAccounts,
noBorder,
}: {
profile: AppBskyActorDefs.ProfileView
moderationOpts: ModerationOpts
isSuggestion: boolean
setSuggestedAccounts: (
updater: (
v: Map<string, AppBskyActorDefs.ProfileView[]>,
) => Map<string, AppBskyActorDefs.ProfileView[]>,
) => void
noBorder?: boolean
}): React.ReactNode => {
const [hasFollowed, setHasFollowed] = useState(false)
const followupSuggestion = useSuggestedFollowsByActorQuery({
did: profile.did,
enabled: hasFollowed,
})
const candidates = followupSuggestion.data?.suggestions
useEffect(() => {
// TODO: Move out of effect.
if (hasFollowed && candidates && candidates.length > 0) {
setSuggestedAccounts(suggestions => {
const newSuggestions = new Map(suggestions)
newSuggestions.set(profile.did, candidates)
return newSuggestions
})
}
}, [hasFollowed, profile.did, candidates, setSuggestedAccounts])
return (
<LayoutAnimationConfig skipEntering={!isSuggestion}>
<Animated.View entering={native(ZoomInEasyDown)}>
<FollowProfileCardInner
profile={profile}
moderationOpts={moderationOpts}
onFollow={() => setHasFollowed(true)}
noBorder={noBorder}
/>
</Animated.View>
</LayoutAnimationConfig>
)
}
FollowProfileCard = memo(FollowProfileCard)
function FollowProfileCardInner({
profile,
moderationOpts,
onFollow,
noBorder,
}: {
profile: AppBskyActorDefs.ProfileView
moderationOpts: ModerationOpts
onFollow?: () => void
noBorder?: boolean
}) {
const control = Dialog.useDialogContext()
const t = useTheme()
return (
<ProfileCard.Link
profile={profile}
style={[a.flex_1]}
onPress={() => control.close()}>
{({hovered, pressed}) => (
<CardOuter
style={[
a.flex_1,
noBorder && a.border_t_0,
(hovered || pressed) && t.atoms.border_contrast_high,
]}>
<ProfileCard.Outer>
<ProfileCard.Header>
<ProfileCard.Avatar
profile={profile}
moderationOpts={moderationOpts}
/>
<ProfileCard.NameAndHandle
profile={profile}
moderationOpts={moderationOpts}
/>
<ProfileCard.FollowButton
profile={profile}
moderationOpts={moderationOpts}
logContext="PostOnboardingFindFollows"
shape="round"
onPress={onFollow}
colorInverted
/>
</ProfileCard.Header>
<ProfileCard.Description profile={profile} numberOfLines={2} />
</ProfileCard.Outer>
</CardOuter>
)}
</ProfileCard.Link>
)
}
function CardOuter({
children,
style,
}: {children: React.ReactNode | React.ReactNode[]} & ViewStyleProp) {
const t = useTheme()
return (
<View
style={[
a.w_full,
a.py_md,
a.px_lg,
a.border_t,
t.atoms.border_contrast_low,
style,
]}>
{children}
</View>
)
}
function SearchInput({
onChangeText,
onEscape,
inputRef,
defaultValue,
}: {
onChangeText: (text: string) => void
onEscape: () => void
inputRef: React.RefObject<TextInput>
defaultValue: string
}) {
const t = useTheme()
const {_} = useLingui()
const {
state: hovered,
onIn: onMouseEnter,
onOut: onMouseLeave,
} = useInteractionState()
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
const interacted = hovered || focused
return (
<View
{...web({
onMouseEnter,
onMouseLeave,
})}
style={[a.flex_row, a.align_center, a.gap_sm, a.px_lg, a.py_xs]}>
<SearchIcon
size="md"
fill={interacted ? t.palette.primary_500 : t.palette.contrast_300}
/>
<TextInput
ref={inputRef}
placeholder={_(msg`Search by name or interest`)}
defaultValue={defaultValue}
onChangeText={onChangeText}
onFocus={onFocus}
onBlur={onBlur}
style={[a.flex_1, a.py_md, a.text_md, t.atoms.text]}
placeholderTextColor={t.palette.contrast_500}
keyboardAppearance={t.name === 'light' ? 'light' : 'dark'}
returnKeyType="search"
clearButtonMode="while-editing"
maxLength={50}
onKeyPress={({nativeEvent}) => {
if (nativeEvent.key === 'Escape') {
onEscape()
}
}}
autoCorrect={false}
autoComplete="off"
autoCapitalize="none"
accessibilityLabel={_(msg`Search profiles`)}
accessibilityHint={_(msg`Search profiles`)}
/>
</View>
)
}
function ProfileCardSkeleton() {
const t = useTheme()
return (
<View
style={[
a.flex_1,
a.py_md,
a.px_lg,
a.gap_md,
a.align_center,
a.flex_row,
]}>
<View
style={[
a.rounded_full,
{width: 42, height: 42},
t.atoms.bg_contrast_25,
]}
/>
<View style={[a.flex_1, a.gap_sm]}>
<View
style={[
a.rounded_xs,
{width: 80, height: 14},
t.atoms.bg_contrast_25,
]}
/>
<View
style={[
a.rounded_xs,
{width: 120, height: 10},
t.atoms.bg_contrast_25,
]}
/>
</View>
</View>
)
}
function Empty({message}: {message: string}) {
const t = useTheme()
return (
<View style={[a.p_lg, a.py_xl, a.align_center, a.gap_md]}>
<Text style={[a.text_sm, a.italic, t.atoms.text_contrast_high]}>
{message}
</Text>
<Text style={[a.text_xs, t.atoms.text_contrast_low]}>(°°) </Text>
</View>
)
}
function boostInterests(boosts?: string[]) {
return (_a: string, _b: string) => {
const indexA = boosts?.indexOf(_a) ?? -1
const indexB = boosts?.indexOf(_b) ?? -1
const rankA = indexA === -1 ? Infinity : indexA
const rankB = indexB === -1 ? Infinity : indexB
return rankA - rankB
}
}
+13 -31
View File
@@ -10,15 +10,12 @@ import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
import {TimesLarge_Stroke2_Corner0_Rounded as Times} from '#/components/icons/Times'
import {Text} from '#/components/Typography'
import {FollowDialog} from './FollowDialog'
import {ProgressGuideTask} from './Task'
export function ProgressGuideList({style}: {style?: StyleProp<ViewStyle>}) {
const t = useTheme()
const {_} = useLingui()
const followProgressGuide = useProgressGuide('follow-10')
const followAndLikeProgressGuide = useProgressGuide('like-10-and-follow-7')
const guide = followProgressGuide || followAndLikeProgressGuide
const guide = useProgressGuide('like-10-and-follow-7')
const {endProgressGuide} = useProgressGuideControls()
if (guide) {
@@ -44,33 +41,18 @@ export function ProgressGuideList({style}: {style?: StyleProp<ViewStyle>}) {
<ButtonIcon icon={Times} size="sm" />
</Button>
</View>
{guide.guide === 'follow-10' && (
<>
<ProgressGuideTask
current={guide.numFollows + 1}
total={10 + 1}
title={_(msg`Follow 10 accounts`)}
subtitle={_(msg`Bluesky is better with friends!`)}
/>
<FollowDialog guide={guide} />
</>
)}
{guide.guide === 'like-10-and-follow-7' && (
<>
<ProgressGuideTask
current={guide.numLikes + 1}
total={10 + 1}
title={_(msg`Like 10 posts`)}
subtitle={_(msg`Teach our algorithm what you like`)}
/>
<ProgressGuideTask
current={guide.numFollows + 1}
total={7 + 1}
title={_(msg`Follow 7 accounts`)}
subtitle={_(msg`Bluesky is better with friends!`)}
/>
</>
)}
<ProgressGuideTask
current={guide.numLikes + 1}
total={10 + 1}
title={_(msg`Like 10 posts`)}
subtitle={_(msg`Teach our algorithm what you like`)}
/>
<ProgressGuideTask
current={guide.numFollows + 1}
total={7 + 1}
title={_(msg`Follow 7 accounts`)}
subtitle={_(msg`Bluesky is better with friends!`)}
/>
</View>
)
}
+2 -12
View File
@@ -10,13 +10,11 @@ export function ProgressGuideTask({
total,
title,
subtitle,
tabularNumsTitle,
}: {
current: number
total: number
title: string
subtitle?: string
tabularNumsTitle?: boolean
}) {
const t = useTheme()
@@ -35,16 +33,8 @@ export function ProgressGuideTask({
/>
)}
<View style={[a.flex_col, a.gap_2xs, subtitle && {marginTop: -2}]}>
<Text
style={[
a.text_sm,
a.font_bold,
a.leading_tight,
tabularNumsTitle && {fontVariant: ['tabular-nums']},
]}>
{title}
</Text>
<View style={[a.flex_col, a.gap_2xs, {marginTop: -2}]}>
<Text style={[a.text_sm, a.font_bold, a.leading_tight]}>{title}</Text>
{subtitle && (
<Text
style={[a.text_sm, t.atoms.text_contrast_medium, a.leading_tight]}>
@@ -5,7 +5,7 @@ import {useLingui} from '@lingui/react'
import {isNative} from '#/platform/detection'
import {FeedDescriptor} from '#/state/queries/post-feed'
import {PostFeed} from '#/view/com/posts/PostFeed'
import {Feed} from '#/view/com/posts/Feed'
import {EmptyState} from '#/view/com/util/EmptyState'
import {ListRef} from '#/view/com/util/List'
import {SectionRef} from '#/screens/Profile/Sections/types'
@@ -38,7 +38,7 @@ export const PostsList = React.forwardRef<SectionRef, ProfilesListProps>(
return (
<View>
<PostFeed
<Feed
feed={feed}
pollInterval={60e3}
scrollElRef={scrollElRef}
-223
View File
@@ -1,223 +0,0 @@
import React from 'react'
import {View} from 'react-native'
import {AtUri} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
// import {makeProfileLink} from '#/lib/routes/links'
// import {feedUriToHref} from '#/lib/strings/url-helpers'
// import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Hashtag'
// import {CloseQuote_Filled_Stroke2_Corner0_Rounded as Quote} from '#/components/icons/Quote'
// import {UserAvatar} from '#/view/com/util/UserAvatar'
import type {TrendingTopic} from '#/state/queries/trending/useTrendingTopics'
import {atoms as a, useTheme, ViewStyleProp} from '#/alf'
import {Link as InternalLink, LinkProps} from '#/components/Link'
import {Text} from '#/components/Typography'
export function TrendingTopic({
topic: raw,
size,
style,
}: {topic: TrendingTopic; size?: 'large' | 'small'} & ViewStyleProp) {
const t = useTheme()
const topic = useTopic(raw)
const isSmall = size === 'small'
// const hasAvi = topic.type === 'feed' || topic.type === 'profile'
// const aviSize = isSmall ? 16 : 20
// const iconSize = isSmall ? 16 : 20
return (
<View
style={[
a.flex_row,
a.align_center,
a.rounded_full,
a.border,
t.atoms.border_contrast_medium,
t.atoms.bg,
isSmall
? [
{
paddingVertical: 5,
paddingHorizontal: 10,
},
]
: [a.py_sm, a.px_md],
style,
/*
{
padding: 6,
gap: hasAvi ? 4 : 2,
},
a.pr_md,
*/
]}>
{/*
<View
style={[
a.align_center,
a.justify_center,
a.rounded_full,
a.overflow_hidden,
{
width: aviSize,
height: aviSize,
},
]}>
{topic.type === 'tag' ? (
<Hashtag width={iconSize} />
) : topic.type === 'topic' ? (
<Quote width={iconSize - 2} />
) : topic.type === 'feed' ? (
<UserAvatar
type="user"
size={aviSize}
avatar=""
/>
) : (
<UserAvatar
type="user"
size={aviSize}
avatar=""
/>
)}
</View>
*/}
<Text
style={[
a.font_bold,
a.leading_tight,
isSmall ? [a.text_sm] : [a.text_md, {paddingBottom: 1}],
]}
numberOfLines={1}>
{topic.displayName}
</Text>
</View>
)
}
export function TrendingTopicSkeleton({
size = 'large',
index = 0,
}: {
size?: 'large' | 'small'
index?: number
}) {
const t = useTheme()
const isSmall = size === 'small'
return (
<View
style={[
a.rounded_full,
a.border,
t.atoms.border_contrast_medium,
t.atoms.bg_contrast_25,
isSmall
? {
width: index % 2 === 0 ? 75 : 90,
height: 27,
}
: {
width: index % 2 === 0 ? 90 : 110,
height: 36,
},
]}
/>
)
}
export function TrendingTopicLink({
topic: raw,
children,
...rest
}: {
topic: TrendingTopic
} & Omit<LinkProps, 'to' | 'label'>) {
const topic = useTopic(raw)
return (
<InternalLink label={topic.label} to={topic.url} {...rest}>
{children}
</InternalLink>
)
}
type ParsedTrendingTopic =
| {
type: 'topic' | 'tag' | 'unknown'
label: string
displayName: string
url: string
uri: undefined
}
| {
type: 'profile' | 'feed'
label: string
displayName: string
url: string
uri: AtUri
}
export function useTopic(raw: TrendingTopic): ParsedTrendingTopic {
const {_} = useLingui()
return React.useMemo(() => {
const {topic: displayName, link} = raw
if (link.startsWith('/search')) {
return {
type: 'topic',
label: _(msg`Browse posts about ${displayName}`),
displayName,
uri: undefined,
url: link,
}
} else if (link.startsWith('/hashtag')) {
return {
type: 'tag',
label: _(msg`Browse posts tagged with ${displayName}`),
displayName,
// displayName: displayName.replace(/^#/, ''),
uri: undefined,
url: link,
}
}
/*
if (!link.startsWith('at://')) {
// above logic
} else {
const urip = new AtUri(link)
switch (urip.collection) {
case 'app.bsky.actor.profile': {
return {
type: 'profile',
label: _(msg`View ${displayName}'s profile`),
displayName,
uri: urip,
url: makeProfileLink({did: urip.host, handle: urip.host}),
}
}
case 'app.bsky.feed.generator': {
return {
type: 'feed',
label: _(msg`Browse the ${displayName} feed`),
displayName,
uri: urip,
url: feedUriToHref(link),
}
}
}
}
*/
return {
type: 'unknown',
label: _(msg`Browse topic ${displayName}`),
displayName,
uri: undefined,
url: link,
}
}, [_, raw])
}
+2 -1
View File
@@ -8,6 +8,7 @@ import {
renderChildrenWithEmoji,
TextProps,
} from '#/alf/typography'
import {IS_DEV} from '#/env'
export type {TextProps}
/**
@@ -30,7 +31,7 @@ export function Text({
flags,
})
if (__DEV__) {
if (IS_DEV) {
if (!emoji && childHasEmoji(children)) {
logger.warn(
`Text: emoji detected but emoji not enabled: "${children}"\n\nPlease add <Text emoji />'`,
@@ -311,7 +311,7 @@ export function PostInteractionSettingsForm({
onChange={onChangeQuotesEnabled}
style={[a.justify_between, a.pt_xs]}>
<Text style={[t.atoms.text_contrast_medium]}>
<Trans>Allow quote posts</Trans>
<Trans>Quote posts enabled</Trans>
</Text>
<Toggle.Switch />
</Toggle.Item>
+3 -2
View File
@@ -15,6 +15,7 @@ import {useOnboardingState} from '#/state/shell'
* NUXs
*/
import {isSnoozed, snooze, unsnooze} from '#/components/dialogs/nuxs/snoozing'
import {IS_DEV} from '#/env'
type Context = {
activeNux: Nux | undefined
@@ -92,10 +93,10 @@ function Inner({
setActiveNux(undefined)
}, [activeNux, setActiveNux])
if (__DEV__ && typeof window !== 'undefined') {
if (IS_DEV && typeof window !== 'undefined') {
// @ts-ignore
window.clearNuxDialog = (id: Nux) => {
if (!__DEV__ || !id) return
if (!IS_DEV || !id) return
resetNuxs([id])
unsnooze()
}
@@ -15,7 +15,7 @@ import {sanitizeHandle} from '#/lib/strings/handles'
import {isWeb} from '#/platform/detection'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
import {useListConvosQuery} from '#/state/queries/messages/list-conversations'
import {useListConvosQuery} from '#/state/queries/messages/list-converations'
import {useProfileFollowsQuery} from '#/state/queries/profile-follows'
import {useSession} from '#/state/session'
import {ListMethods} from '#/view/com/util/List'
@@ -63,7 +63,6 @@ export function SearchablePeopleList({
const {_} = useLingui()
const moderationOpts = useModerationOpts()
const control = Dialog.useDialogContext()
const [headerHeight, setHeaderHeight] = useState(0)
const listRef = useRef<ListMethods>(null)
const {currentAccount} = useSession()
const inputRef = useRef<TextInput>(null)
@@ -238,7 +237,6 @@ export function SearchablePeopleList({
const listHeader = useMemo(() => {
return (
<View
onLayout={evt => setHeaderHeight(evt.nativeEvent.layout.height)}
style={[
a.relative,
web(a.pt_lg),
@@ -317,7 +315,6 @@ export function SearchablePeopleList({
]}
webInnerContentContainerStyle={a.py_0}
webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]}
scrollIndicatorInsets={{top: headerHeight}}
keyboardDismissMode="on-drag"
/>
)
+30 -53
View File
@@ -11,15 +11,7 @@ import {
import {HITSLOP_20} from '#/lib/constants'
import {mergeRefs} from '#/lib/merge-refs'
import {
android,
applyFonts,
atoms as a,
TextStyleProp,
useAlf,
useTheme,
web,
} from '#/alf'
import {android, atoms as a, TextStyleProp, useTheme, web} from '#/alf'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {Props as SVGIconProps} from '#/components/icons/common'
import {Text} from '#/components/Typography'
@@ -156,7 +148,6 @@ export function createInput(Component: typeof TextInput) {
...rest
}: InputProps) {
const t = useTheme()
const {fonts} = useAlf()
const ctx = React.useContext(Context)
const withinRoot = Boolean(ctx.inputRef)
@@ -180,48 +171,6 @@ export function createInput(Component: typeof TextInput) {
const refs = mergeRefs([ctx.inputRef, inputRef!].filter(Boolean))
const flattened = StyleSheet.flatten([
a.relative,
a.z_20,
a.flex_1,
a.text_md,
t.atoms.text,
a.px_xs,
{
// paddingVertical doesn't work w/multiline - esb
paddingTop: 12,
paddingBottom: 13,
lineHeight: a.text_md.fontSize * 1.1875,
textAlignVertical: rest.multiline ? 'top' : undefined,
minHeight: rest.multiline ? 80 : undefined,
minWidth: 0,
},
// fix for autofill styles covering border
web({
paddingTop: 10,
paddingBottom: 11,
marginTop: 2,
marginBottom: 2,
}),
android({
paddingTop: 8,
paddingBottom: 8,
}),
style,
])
applyFonts(flattened, fonts.family)
// should always be defined on `typography`
// @ts-ignore
if (flattened.fontSize) {
// @ts-ignore
flattened.fontSize = Math.round(
// @ts-ignore
flattened.fontSize * fonts.scaleMultiplier,
)
}
return (
<>
<Component
@@ -243,7 +192,35 @@ export function createInput(Component: typeof TextInput) {
placeholderTextColor={t.palette.contrast_500}
keyboardAppearance={t.name === 'light' ? 'light' : 'dark'}
hitSlop={HITSLOP_20}
style={flattened}
style={[
a.relative,
a.z_20,
a.flex_1,
a.text_md,
t.atoms.text,
a.px_xs,
{
// paddingVertical doesn't work w/multiline - esb
paddingTop: 12,
paddingBottom: 13,
lineHeight: a.text_md.fontSize * 1.1875,
textAlignVertical: rest.multiline ? 'top' : undefined,
minHeight: rest.multiline ? 80 : undefined,
minWidth: 0,
},
// fix for autofill styles covering border
web({
paddingTop: 10,
paddingBottom: 11,
marginTop: 2,
marginBottom: 2,
}),
android({
paddingTop: 8,
paddingBottom: 8,
}),
style,
]}
/>
<View
+2 -2
View File
@@ -8,8 +8,8 @@ export function useHeaderOffset() {
if (isDesktop || isTablet) {
return 0
}
const navBarHeight = 52
const tabBarPad = 10 + 10 + 3 // padding + border
const navBarHeight = 42
const tabBarPad = 10 + 10 + 6 // padding + arbitrary
const normalLineHeight = 20 // matches tab bar
const tabBarText = normalLineHeight * fontScale
return navBarHeight + tabBarPad + tabBarText
-4
View File
@@ -3,7 +3,3 @@ import {createSinglePathSVG} from './TEMPLATE'
export const Pin_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M6.5 3a1 1 0 0 1 1-1h9a1 1 0 0 1 1 1v3.997a6.25 6.25 0 0 0 1.83 4.42l.377.376A1 1 0 0 1 20 12.5V15a1 1 0 0 1-1 1h-6v5a1 1 0 1 1-2 0v-5H5a1 1 0 0 1-1-1v-2.5a1 1 0 0 1 .293-.707l.376-.377A6.25 6.25 0 0 0 6.5 6.996V3.001Zm2 1v2.997a8.25 8.25 0 0 1-2.416 5.834L6 12.914V14h12v-1.086l-.084-.083A8.25 8.25 0 0 1 15.5 6.997V4h-7Z',
})
export const Pin_Filled_Corner0_Rounded = createSinglePathSVG({
path: 'M7.5 2a1 1 0 0 0-1 1v3.997a6.25 6.25 0 0 1-1.83 4.42l-.377.376A1 1 0 0 0 4 12.5V15a1 1 0 0 0 1 1h6v5a1 1 0 1 0 2 0v-5h6a1 1 0 0 0 1-1v-2.5a1 1 0 0 0-.293-.707l-.376-.377a6.25 6.25 0 0 1-1.831-4.42V3.001a1 1 0 0 0-1-1h-9Z',
})
-111
View File
@@ -1,111 +0,0 @@
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {
useTrendingSettings,
useTrendingSettingsApi,
} from '#/state/preferences/trending'
import {
DEFAULT_LIMIT as TRENDING_TOPICS_COUNT,
useTrendingTopics,
} from '#/state/queries/trending/useTrendingTopics'
import {useTrendingConfig} from '#/state/trending-config'
import {atoms as a, tokens, useGutters, useTheme} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
import {GradientFill} from '#/components/GradientFill'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending2'
import * as Prompt from '#/components/Prompt'
import {
TrendingTopic,
TrendingTopicLink,
TrendingTopicSkeleton,
} from '#/components/TrendingTopics'
import {Text} from '#/components/Typography'
export function TrendingInterstitial() {
const {enabled} = useTrendingConfig()
const {trendingDisabled} = useTrendingSettings()
return enabled && !trendingDisabled ? <Inner /> : null
}
export function Inner() {
const t = useTheme()
const {_} = useLingui()
const gutters = useGutters(['wide', 'base'])
const trendingPrompt = Prompt.usePromptControl()
const {setTrendingDisabled} = useTrendingSettingsApi()
const {data: trending, error, isLoading} = useTrendingTopics()
const noTopics = !isLoading && !error && !trending?.topics?.length
return error || noTopics ? null : (
<View
style={[
gutters,
a.gap_lg,
a.border_t,
t.atoms.border_contrast_low,
t.atoms.bg_contrast_25,
]}>
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
<View style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
<Graph size="lg" />
<Text style={[a.text_lg, a.font_heavy]}>
<Trans>Trending</Trans>
</Text>
<View style={[a.py_xs, a.px_sm, a.rounded_sm, a.overflow_hidden]}>
<GradientFill gradient={tokens.gradients.primary} />
<Text style={[a.text_sm, a.font_heavy, {color: 'white'}]}>
<Trans>BETA</Trans>
</Text>
</View>
</View>
<Button
label={_(msg`Hide trending topics`)}
size="tiny"
variant="outline"
color="secondary"
shape="round"
onPress={() => trendingPrompt.open()}>
<ButtonIcon icon={X} />
</Button>
</View>
<View style={[a.flex_row, a.flex_wrap, {rowGap: 8, columnGap: 6}]}>
{isLoading ? (
Array(TRENDING_TOPICS_COUNT)
.fill(0)
.map((_n, i) => <TrendingTopicSkeleton key={i} index={i} />)
) : !trending?.topics ? null : (
<>
{trending.topics.map(topic => (
<TrendingTopicLink key={topic.link} topic={topic}>
{({hovered}) => (
<TrendingTopic
topic={topic}
style={[
hovered && [
t.atoms.border_contrast_high,
t.atoms.bg_contrast_25,
],
]}
/>
)}
</TrendingTopicLink>
))}
</>
)}
</View>
<Prompt.Basic
control={trendingPrompt}
title={_(msg`Hide trending topics?`)}
description={_(msg`You can update this later from your settings.`)}
confirmButtonCta={_(msg`Hide`)}
onConfirm={() => setTrendingDisabled(true)}
/>
</View>
)
}
+11 -17
View File
@@ -1,6 +1,6 @@
import {StyleProp, View, ViewStyle} from 'react-native'
import {AppBskyFeedDefs, ComAtprotoLabelDefs} from '@atproto/api'
import {msg, Plural, Trans} from '@lingui/macro'
import {msg, Plural} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useSession} from '#/state/session'
@@ -50,23 +50,17 @@ export function LabelsOnMe({
<ButtonIcon position="left" icon={CircleInfo} />
<ButtonText style={[a.leading_snug]}>
{type === '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 account"
other="# labels have been placed on this account"
/>
) : (
<Trans>
<Plural
value={labels.length}
one="# label has"
other="# labels have"
/>{' '}
been placed on this content
</Trans>
<Plural
value={labels.length}
one="# label has been placed on this content"
other="# labels have been placed on this content"
/>
)}
</ButtonText>
</Button>
+3
View File
@@ -1,3 +1,6 @@
export const IS_TEST = process.env.EXPO_PUBLIC_ENV === 'test'
export const IS_DEV = __DEV__
export const IS_PROD = !IS_DEV
export const LOG_DEBUG = process.env.EXPO_PUBLIC_LOG_DEBUG || ''
export const LOG_LEVEL = (process.env.EXPO_PUBLIC_LOG_LEVEL || 'info') as
| 'debug'
+1 -4
View File
@@ -11,10 +11,7 @@ export async function uploadBlob(
input: string | Blob,
encoding?: string,
): Promise<ComAtprotoRepoUploadBlob.Response> {
if (
typeof input === 'string' &&
(input.startsWith('data:') || input.startsWith('blob:'))
) {
if (typeof input === 'string' && input.startsWith('data:')) {
const blob = await fetch(input).then(r => r.blob())
return agent.uploadBlob(blob, {encoding})
}
+5 -3
View File
@@ -1,7 +1,9 @@
import {nativeApplicationVersion, nativeBuildVersion} from 'expo-application'
export const BUILD_ENV = process.env.EXPO_PUBLIC_ENV
export const IS_DEV = process.env.EXPO_PUBLIC_ENV === 'development'
export const IS_TESTFLIGHT = process.env.EXPO_PUBLIC_ENV === 'testflight'
export const IS_INTERNAL = __DEV__ || IS_TESTFLIGHT
export const IS_INTERNAL = IS_DEV || IS_TESTFLIGHT
// This is the commit hash that the current bundle was made from. The user can see the commit hash in the app's settings
// along with the other version info. Useful for debugging/reporting.
@@ -10,9 +12,9 @@ export const BUNDLE_IDENTIFIER = process.env.EXPO_PUBLIC_BUNDLE_IDENTIFIER ?? ''
// This will always be in the format of YYMMDD, so that it always increases for each build. This should only be used
// for Statsig reporting and shouldn't be used to identify a specific bundle.
export const BUNDLE_DATE =
IS_TESTFLIGHT || __DEV__ ? 0 : Number(process.env.EXPO_PUBLIC_BUNDLE_DATE)
IS_TESTFLIGHT || IS_DEV ? 0 : Number(process.env.EXPO_PUBLIC_BUNDLE_DATE)
export const appVersion = `${nativeApplicationVersion}.${nativeBuildVersion}`
export const bundleInfo = `${BUNDLE_IDENTIFIER} (${
__DEV__ ? 'dev' : IS_TESTFLIGHT ? 'tf' : 'prod'
IS_DEV ? 'dev' : IS_TESTFLIGHT ? 'tf' : 'prod'
})`

Some files were not shown because too many files have changed in this diff Show More