diff --git a/app.config.js b/app.config.js
--- a/app.config.js
+++ b/app.config.js
@@ -221,7 +221,7 @@
compileSdkVersion: 35,
targetSdkVersion: 35,
buildToolsVersion: '35.0.0',
- newArchEnabled: true,
+ newArchEnabled: false,
},
},
],
diff --git a/package.json b/package.json
--- a/package.json
+++ b/package.json
@@ -61,8 +61,7 @@
"intl:push": "crowdin push translations --verbose -b main",
"nuke": "rm -rf ./node_modules && rm -rf ./ios && rm -rf ./android",
"update-extensions": "bash scripts/updateExtensions.sh",
- "export-ios": "npx expo export --platform ios --dump-sourcemap && yarn upload-native-sourcemaps",
- "export-android": "npx expo export --platform android --dump-sourcemap && yarn upload-native-sourcemaps",
+ "export": "npx expo export --dump-sourcemap && yarn upload-native-sourcemaps",
"upload-native-sourcemaps": "npx sentry-expo-upload-sourcemaps dist",
"make-deploy-bundle": "bash scripts/bundleUpdate.sh",
"generate-webpack-stats-file": "EXPO_PUBLIC_GENERATE_STATS=1 yarn build-web",
@@ -266,7 +265,7 @@
"lint-staged": "^13.2.3",
"lockfile-lint": "^4.14.0",
"metro-react-native-babel-preset": "^0.77.0",
- "prettier": "^2.8.3",
+ "prettier": "^3.6.0",
"react-native-dotenv": "^3.4.11",
"react-refresh": "^0.14.0",
"svgo": "^3.3.2",
diff --git a/svgo.config.mjs b/svgo.config.mjs
--- a/svgo.config.mjs
+++ b/svgo.config.mjs
@@ -1,64 +1,66 @@
const preset = [
- "removeDoctype",
- "removeXMLProcInst",
- "removeComments",
- "removeMetadata",
- "removeEditorsNSData",
- "cleanupAttrs",
- "mergeStyles",
- "inlineStyles",
- "minifyStyles",
- "cleanupIds",
- "removeUselessDefs",
- "cleanupNumericValues",
- "convertColors",
- "removeUnknownsAndDefaults",
- "removeNonInheritableGroupAttrs",
- "removeUselessStrokeAndFill",
- "removeDimensions",
- "cleanupEnableBackground",
- "removeHiddenElems",
- "removeEmptyText",
- "convertShapeToPath",
- "convertEllipseToCircle",
- "moveElemsAttrsToGroup",
- "moveGroupAttrsToElems",
- "collapseGroups",
- "convertPathData",
- "convertTransform",
- "removeEmptyAttrs",
- "removeEmptyContainers",
- "removeUnusedNS",
- "mergePaths",
- "sortAttrs",
- "sortDefsChildren",
- "removeTitle",
- "removeDesc",
+ 'removeDoctype',
+ 'removeXMLProcInst',
+ 'removeComments',
+ 'removeMetadata',
+ 'removeEditorsNSData',
+ 'cleanupAttrs',
+ 'mergeStyles',
+ 'inlineStyles',
+ 'minifyStyles',
+ 'cleanupIds',
+ 'removeUselessDefs',
+ 'cleanupNumericValues',
+ 'convertColors',
+ 'removeUnknownsAndDefaults',
+ 'removeNonInheritableGroupAttrs',
+ 'removeUselessStrokeAndFill',
+ 'removeDimensions',
+ 'cleanupEnableBackground',
+ 'removeHiddenElems',
+ 'removeEmptyText',
+ 'convertShapeToPath',
+ 'convertEllipseToCircle',
+ 'moveElemsAttrsToGroup',
+ 'moveGroupAttrsToElems',
+ 'collapseGroups',
+ 'convertPathData',
+ 'convertTransform',
+ 'removeEmptyAttrs',
+ 'removeEmptyContainers',
+ 'removeUnusedNS',
+ 'mergePaths',
+ 'sortAttrs',
+ 'sortDefsChildren',
+ 'removeTitle',
+ 'removeDesc',
]
export default {
- plugins: [...preset.map(name => ({
- name,
- params: {
- floatPrecision: 3,
- transformPrecision: 5,
- // minimise diff in ouput from svgomg
- // maybe remove in future? will produce smaller output
- convertToZ: false,
- removeUseless: false,
- }
- })),
- {
- name: 'addTrailingWhitespace',
- fn() {
- return {
- root: {
- exit (root) {
- root.children.push({ type: 'text', value: '\n' })
- return root
- }
+ plugins: [
+ ...preset.map(name => ({
+ name,
+ params: {
+ floatPrecision: 3,
+ transformPrecision: 5,
+ // minimise diff in ouput from svgomg
+ // maybe remove in future? will produce smaller output
+ convertToZ: false,
+ removeUseless: false,
+ },
+ })),
+ {
+ name: 'addTrailingWhitespace',
+ fn() {
+ return {
+ root: {
+ exit(root) {
+ root.children.push({type: 'text', value: '\n'})
+ return root
+ },
+ },
}
- }
- }
- }]
-};
+ },
+ },
+ ],
+}
diff --git a/yarn.lock b/yarn.lock
--- a/yarn.lock
+++ b/yarn.lock
@@ -16220,10 +16220,10 @@
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
-prettier@^2.8.3:
- version "2.8.8"
- resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da"
- integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==
+prettier@^3.6.0:
+ version "3.6.0"
+ resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.6.0.tgz#18ec98d62cb0757a5d4eab40253ff3e6d0fc8dea"
+ integrity sha512-ujSB9uXHJKzM/2GBuE0hBOUgC77CN3Bnpqa+g80bkv3T3A93wL/xlzDATHhnhkzifz/UE2SNOvmbTz5hSkDlHw==
pretty-bytes@^5.6.0:
version "5.6.0"
diff --git a/bskyembed/tailwind.config.cjs b/bskyembed/tailwind.config.cjs
--- a/bskyembed/tailwind.config.cjs
+++ b/bskyembed/tailwind.config.cjs
@@ -1,9 +1,7 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
- darkMode: ['variant', [
- '&:is(.dark *):not(:is(.dark .light *))',
- ]],
+ darkMode: ['variant', ['&:is(.dark *):not(:is(.dark .light *))']],
theme: {
extend: {
colors: {
diff --git a/bskyembed/tsconfig.snippet.json b/bskyembed/tsconfig.snippet.json
--- a/bskyembed/tsconfig.snippet.json
+++ b/bskyembed/tsconfig.snippet.json
@@ -1,4 +1,3 @@
-
{
"compilerOptions": {
"target": "ES5",
diff --git a/scripts/bundleUpdate.js b/scripts/bundleUpdate.js
--- a/scripts/bundleUpdate.js
+++ b/scripts/bundleUpdate.js
@@ -3,25 +3,17 @@
const fsp = fs.promises
const path = require('path')
-const IOS_DIST_DIR = './ios-dist'
-const ANDROID_DIST_DIR = './android-dist'
-
+const DIST_DIR = './dist'
const BUNDLES_DIR = '/_expo/static/js'
-
-const IOS_BUNDLE_DIR = path.join(IOS_DIST_DIR, BUNDLES_DIR, '/ios')
-const ANDROID_BUNDLE_DIR = path.join(ANDROID_DIST_DIR, BUNDLES_DIR, '/android')
-
-const IOS_METADATA_PATH = path.join(IOS_DIST_DIR, '/metadata.json')
-const ANDROID_METADATA_PATH = path.join(ANDROID_DIST_DIR, '/metadata.json')
-
+const IOS_BUNDLE_DIR = path.join(DIST_DIR, BUNDLES_DIR, '/ios')
+const ANDROID_BUNDLE_DIR = path.join(DIST_DIR, BUNDLES_DIR, '/android')
+const METADATA_PATH = path.join(DIST_DIR, '/metadata.json')
const DEST_DIR = './bundleTempDir'
// Weird, don't feel like figuring out _why_ it wants this
-const IOS_METADATA = require(`../${IOS_METADATA_PATH}`)
-const ANDROID_METADATA = require(`../${ANDROID_METADATA_PATH}`)
-
-const IOS_METADATA_ASSETS = IOS_METADATA.fileMetadata.ios.assets
-const ANDROID_METADATA_ASSETS = ANDROID_METADATA.fileMetadata.android.assets
+const METADATA = require(`../${METADATA_PATH}`)
+const IOS_METADATA_ASSETS = METADATA.fileMetadata.ios.assets
+const ANDROID_METADATA_ASSETS = METADATA.fileMetadata.android.assets
const getMd5 = async path => {
return new Promise(res => {
@@ -68,7 +60,7 @@
console.log('Getting ios asset md5s and moving them...')
for (const asset of IOS_METADATA_ASSETS) {
- const currPath = path.join(IOS_DIST_DIR, asset.path)
+ const currPath = path.join(DIST_DIR, asset.path)
const md5 = await getMd5(currPath)
const withExtPath = `assets/${md5}.${asset.ext}`
iosAssets.push(withExtPath)
@@ -77,7 +69,7 @@
console.log('Getting android asset md5s and moving them...')
for (const asset of ANDROID_METADATA_ASSETS) {
- const currPath = path.join(ANDROID_DIST_DIR, asset.path)
+ const currPath = path.join(DIST_DIR, asset.path)
const md5 = await getMd5(currPath)
const withExtPath = `assets/${md5}.${asset.ext}`
androidAssets.push(withExtPath)
diff --git a/scripts/post-web-build.js b/scripts/post-web-build.js
--- a/scripts/post-web-build.js
+++ b/scripts/post-web-build.js
@@ -9,10 +9,9 @@
'scripts.html',
)
-const {entrypoints} = require(path.join(
- projectRoot,
- 'web-build/asset-manifest.json',
-))
+const {entrypoints} = require(
+ path.join(projectRoot, 'web-build/asset-manifest.json'),
+)
console.log(`Found ${entrypoints.length} entrypoints`)
console.log(`Writing ${templateFile}`)
diff --git a/src/App.native.tsx b/src/App.native.tsx
--- a/src/App.native.tsx
+++ b/src/App.native.tsx
@@ -33,6 +33,7 @@
ensureGeolocationResolved,
Provider as GeolocationProvider,
} from '#/state/geolocation'
+import {GlobalGestureEventsProvider} from '#/state/global-gesture-events'
import {Provider as HomeBadgeProvider} from '#/state/home-badge'
import {Provider as InvitesStateProvider} from '#/state/invites'
import {Provider as LightboxStateProvider} from '#/state/lightbox'
@@ -154,11 +155,13 @@
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/src/style.css b/src/style.css
--- a/src/style.css
+++ b/src/style.css
@@ -328,8 +328,44 @@
/* #/components/Select/index.web.tsx */
.radix-select-content {
- box-shadow: 0px 6px 24px -10px rgba(22, 23, 24, 0.25),
+ box-shadow:
+ 0px 6px 24px -10px rgba(22, 23, 24, 0.25),
0px 6px 12px -12px rgba(22, 23, 24, 0.15);
min-width: var(--radix-select-trigger-width);
max-height: var(--radix-select-content-available-height);
+}
+
+/*
+ * #/components/Tooltip/index.web.tsx
+ */
+.radix-popover-content {
+ animation-duration: 300ms;
+ animation-timing-function: cubic-bezier(0.17, 0.73, 0.14, 1);
+ will-change: transform, opacity;
+}
+.radix-popover-content[data-state='open'][data-side='top'] {
+ animation-name: radixPopoverSlideUpAndFade;
+}
+.radix-popover-content[data-state='open'][data-side='bottom'] {
+ animation-name: radixPopoverSlideDownAndFade;
+}
+@keyframes radixPopoverSlideUpAndFade {
+ from {
+ opacity: 0;
+ transform: translateY(2px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+@keyframes radixPopoverSlideDownAndFade {
+ from {
+ opacity: 0;
+ transform: translateY(-2px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
}
diff --git a/.github/workflows/build-submit-android.yml b/.github/workflows/build-submit-android.yml
--- a/.github/workflows/build-submit-android.yml
+++ b/.github/workflows/build-submit-android.yml
@@ -52,10 +52,6 @@
distribution: 'temurin'
java-version: '17'
- - name: "Use upgraded MMKV for Fabric"
- run: |
- sed -i 's/"react-native-mmkv": "\^2\.12\.2"/"react-native-mmkv": "^3.3.0"/' package.json
-
- name: βοΈ Install dependencies
run: yarn install
diff --git a/.github/workflows/bundle-deploy-eas-update.yml b/.github/workflows/bundle-deploy-eas-update.yml
--- a/.github/workflows/bundle-deploy-eas-update.yml
+++ b/.github/workflows/bundle-deploy-eas-update.yml
@@ -5,7 +5,6 @@
push:
branches:
- main
- - hailey/eas-fab
workflow_dispatch:
inputs:
channel:
@@ -119,14 +118,7 @@
- name: ποΈ Create Bundle
if: ${{ !steps.fingerprint.outputs.includes-changes }}
- run: |
- SENTRY_DIST=${{ steps.sentry.outputs.SENTRY_DIST }} SENTRY_RELEASE=${{ steps.sentry.outputs.SENTRY_RELEASE }} SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_DSN=${{ secrets.SENTRY_DSN }} EXPO_PUBLIC_ENV="${{ inputs.channel || 'testflight' }}" yarn export-ios
- mv ./dist ./ios-dist
- sed -i 's/"react-native-mmkv": "\^2\.12\.2"/"react-native-mmkv": "^3.3.0"/' package.json
- yarn install
- SENTRY_DIST=${{ steps.sentry.outputs.SENTRY_DIST }} SENTRY_RELEASE=${{ steps.sentry.outputs.SENTRY_RELEASE }} SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_DSN=${{ secrets.SENTRY_DSN }} EXPO_PUBLIC_ENV="${{ inputs.channel || 'testflight' }}" yarn export-android
- mv ./dist ./android-dist
-
+ run: SENTRY_DIST=${{ steps.sentry.outputs.SENTRY_DIST }} SENTRY_RELEASE=${{ steps.sentry.outputs.SENTRY_RELEASE }} SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_DSN=${{ secrets.SENTRY_DSN }} EXPO_PUBLIC_ENV="${{ inputs.channel || 'testflight' }}" yarn export
- name: π¦ Package Bundle and π Deploy
if: ${{ !steps.fingerprint.outputs.includes-changes }}
@@ -282,10 +274,6 @@
with:
distribution: 'temurin'
java-version: '17'
-
- - name: "Use upgraded MMKV for Fabric"
- run: |
- sed -i 's/"react-native-mmkv": "\^2\.12\.2"/"react-native-mmkv": "^3.3.0"/' package.json
- name: βοΈ Install dependencies
run: yarn install
diff --git a/bskylink/src/index.ts b/bskylink/src/index.ts
--- a/bskylink/src/index.ts
+++ b/bskylink/src/index.ts
@@ -1,11 +1,11 @@
import events from 'node:events'
-import http from 'node:http'
+import type http from 'node:http'
import cors from 'cors'
import express from 'express'
-import {createHttpTerminator, HttpTerminator} from 'http-terminator'
+import {createHttpTerminator, type HttpTerminator} from 'http-terminator'
-import {Config} from './config.js'
+import {type Config} from './config.js'
import {AppContext} from './context.js'
import {default as routes, errorHandler} from './routes/index.js'
@@ -17,7 +17,10 @@
public server?: http.Server
private terminator?: HttpTerminator
- constructor(public app: express.Application, public ctx: AppContext) {}
+ constructor(
+ public app: express.Application,
+ public ctx: AppContext,
+ ) {}
static async create(cfg: Config): Promise {
let app = express()
diff --git a/bskyogcard/src/index.ts b/bskyogcard/src/index.ts
--- a/bskyogcard/src/index.ts
+++ b/bskyogcard/src/index.ts
@@ -1,10 +1,10 @@
import events from 'node:events'
-import http from 'node:http'
+import type http from 'node:http'
import express from 'express'
-import {createHttpTerminator, HttpTerminator} from 'http-terminator'
+import {createHttpTerminator, type HttpTerminator} from 'http-terminator'
-import {Config} from './config.js'
+import {type Config} from './config.js'
import {AppContext} from './context.js'
import {default as routes, errorHandler} from './routes/index.js'
@@ -15,7 +15,10 @@
public server?: http.Server
private terminator?: HttpTerminator
- constructor(public app: express.Application, public ctx: AppContext) {}
+ constructor(
+ public app: express.Application,
+ public ctx: AppContext,
+ ) {}
static async create(cfg: Config): Promise {
let app = express()
diff --git a/src/alf/atoms.ts b/src/alf/atoms.ts
--- a/src/alf/atoms.ts
+++ b/src/alf/atoms.ts
@@ -983,6 +983,10 @@
transition_none: web({
transitionProperty: 'none',
}),
+ transition_timing_default: web({
+ transitionTimingFunction: 'cubic-bezier(0.17, 0.73, 0.14, 1)',
+ transitionDuration: '100ms',
+ }),
transition_all: web({
transitionProperty: 'all',
transitionTimingFunction: 'cubic-bezier(0.17, 0.73, 0.14, 1)',
diff --git a/src/components/Link.tsx b/src/components/Link.tsx
--- a/src/components/Link.tsx
+++ b/src/components/Link.tsx
@@ -100,10 +100,10 @@
return typeof to === 'string'
? convertBskyAppUrlIfNeeded(sanitizeUrl(to))
: to.screen
- ? router.matchName(to.screen)?.build(to.params)
- : to.href
- ? convertBskyAppUrlIfNeeded(sanitizeUrl(to.href))
- : undefined
+ ? router.matchName(to.screen)?.build(to.params)
+ : to.href
+ ? convertBskyAppUrlIfNeeded(sanitizeUrl(to.href))
+ : undefined
}, [to])
if (!href) {
diff --git a/src/components/WhoCanReply.tsx b/src/components/WhoCanReply.tsx
--- a/src/components/WhoCanReply.tsx
+++ b/src/components/WhoCanReply.tsx
@@ -1,9 +1,15 @@
import React from 'react'
-import {Keyboard, Platform, StyleProp, View, ViewStyle} from 'react-native'
import {
- AppBskyFeedDefs,
+ Keyboard,
+ Platform,
+ type StyleProp,
+ View,
+ type ViewStyle,
+} from 'react-native'
+import {
+ type AppBskyFeedDefs,
AppBskyFeedPost,
- AppBskyGraphDefs,
+ type AppBskyGraphDefs,
AtUri,
} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
@@ -13,7 +19,7 @@
import {makeListLink, makeProfileLink} from '#/lib/routes/links'
import {isNative} from '#/platform/detection'
import {
- ThreadgateAllowUISetting,
+ type ThreadgateAllowUISetting,
threadgateViewToAllowUISetting,
} from '#/state/queries/threadgate'
import {atoms as a, useTheme} from '#/alf'
@@ -70,8 +76,8 @@
const description = anyoneCanReply
? _(msg`Everybody can reply`)
: noOneCanReply
- ? _(msg`Replies disabled`)
- : _(msg`Some people can reply`)
+ ? _(msg`Replies disabled`)
+ : _(msg`Some people can reply`)
const onPressOpen = () => {
if (isNative && Keyboard.isVisible()) {
diff --git a/src/lib/moderation.ts b/src/lib/moderation.ts
--- a/src/lib/moderation.ts
+++ b/src/lib/moderation.ts
@@ -1,18 +1,18 @@
import React from 'react'
import {
- AppBskyLabelerDefs,
+ type AppBskyLabelerDefs,
BskyAgent,
- ComAtprotoLabelDefs,
- InterpretedLabelValueDefinition,
+ type ComAtprotoLabelDefs,
+ type InterpretedLabelValueDefinition,
LABELS,
- ModerationCause,
- ModerationOpts,
- ModerationUI,
+ type ModerationCause,
+ type ModerationOpts,
+ type ModerationUI,
} from '@atproto/api'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
-import {AppModerationCause} from '#/components/Pills'
+import {type AppModerationCause} from '#/components/Pills'
export const ADULT_CONTENT_LABELS = ['sexual', 'nudity', 'porn']
export const OTHER_SELF_LABELS = ['graphic-media']
@@ -29,8 +29,8 @@
cause.source.type === 'labeler'
? cause.source.did
: cause.source.type === 'list'
- ? cause.source.list.uri
- : 'user'
+ ? cause.source.list.uri
+ : 'user'
if (cause.type === 'label') {
return `label:${cause.label.val}:${source}`
}
diff --git a/src/platform/polyfills.ts b/src/platform/polyfills.ts
--- a/src/platform/polyfills.ts
+++ b/src/platform/polyfills.ts
@@ -41,12 +41,12 @@
r1 === 64
? String.fromCharCode((bitmap >> 16) & 255)
: r2 === 64
- ? String.fromCharCode((bitmap >> 16) & 255, (bitmap >> 8) & 255)
- : String.fromCharCode(
- (bitmap >> 16) & 255,
- (bitmap >> 8) & 255,
- bitmap & 255,
- )
+ ? String.fromCharCode((bitmap >> 16) & 255, (bitmap >> 8) & 255)
+ : String.fromCharCode(
+ (bitmap >> 16) & 255,
+ (bitmap >> 8) & 255,
+ bitmap & 255,
+ )
}
return result
}
diff --git a/src/storage/index.ts b/src/storage/index.ts
--- a/src/storage/index.ts
+++ b/src/storage/index.ts
@@ -1,7 +1,7 @@
import {useCallback, useEffect, useState} from 'react'
import {MMKV} from 'react-native-mmkv'
-import {Account, Device} from '#/storage/schema'
+import {type Account, type Device} from '#/storage/schema'
export * from '#/storage/schema'
@@ -83,18 +83,10 @@
}
}
-type StorageSchema> = T extends Storage<
- any,
- infer U
->
- ? U
- : never
-type StorageScopes> = T extends Storage<
- infer S,
- any
->
- ? S
- : never
+type StorageSchema> =
+ T extends Storage ? U : never
+type StorageScopes> =
+ T extends Storage ? S : never
/**
* Hook to use a storage instance. Acts like a useState hook, but persists the
diff --git a/bskylink/src/db/index.ts b/bskylink/src/db/index.ts
--- a/bskylink/src/db/index.ts
+++ b/bskylink/src/db/index.ts
@@ -1,27 +1,30 @@
import assert from 'assert'
import {
Kysely,
- KyselyPlugin,
+ type KyselyPlugin,
Migrator,
- PluginTransformQueryArgs,
- PluginTransformResultArgs,
+ type PluginTransformQueryArgs,
+ type PluginTransformResultArgs,
PostgresDialect,
- QueryResult,
- RootOperationNode,
- UnknownRow,
+ type QueryResult,
+ type RootOperationNode,
+ type UnknownRow,
} from 'kysely'
import {default as Pg} from 'pg'
import {dbLogger as log} from '../logger.js'
import {default as migrations} from './migrations/index.js'
import {DbMigrationProvider} from './migrations/provider.js'
-import {DbSchema} from './schema.js'
+import {type DbSchema} from './schema.js'
export class Database {
migrator: Migrator
destroyed = false
- constructor(public db: Kysely, public cfg: PgConfig) {
+ constructor(
+ public db: Kysely,
+ public cfg: PgConfig,
+ ) {
this.migrator = new Migrator({
db,
migrationTableSchema: cfg.schema,
diff --git a/modules/expo-emoji-picker/src/EmojiPicker.android.tsx b/modules/expo-emoji-picker/src/EmojiPicker.android.tsx
--- a/modules/expo-emoji-picker/src/EmojiPicker.android.tsx
+++ b/modules/expo-emoji-picker/src/EmojiPicker.android.tsx
@@ -12,7 +12,7 @@
flex: 1,
width: '100%',
backgroundColor: scheme === 'dark' ? '#000' : '#fff',
- } as const),
+ }) as const,
[scheme],
)
diff --git a/src/components/ContextMenu/index.tsx b/src/components/ContextMenu/index.tsx
--- a/src/components/ContextMenu/index.tsx
+++ b/src/components/ContextMenu/index.tsx
@@ -190,7 +190,7 @@
if (item) playHaptic('Light')
setHoveredMenuItem(item)
},
- } satisfies ContextType),
+ }) satisfies ContextType,
[
measurement,
setMeasurement,
@@ -710,8 +710,8 @@
const xOffset = position
? position.x
: align === 'left'
- ? measurement.x
- : measurement.x + measurement.width - layout.width
+ ? measurement.x
+ : measurement.x + measurement.width - layout.width
registerHoverable(
id,
diff --git a/src/components/Select/index.web.tsx b/src/components/Select/index.web.tsx
--- a/src/components/Select/index.web.tsx
+++ b/src/components/Select/index.web.tsx
@@ -111,8 +111,8 @@
borderColor: focused
? t.palette.primary_500
: hovered
- ? t.palette.contrast_100
- : t.palette.contrast_25,
+ ? t.palette.contrast_100
+ : t.palette.contrast_25,
},
])}>
{children}
diff --git a/src/components/Tooltip/const.ts b/src/components/Tooltip/const.ts
new file mode 100644
--- /dev/null
+++ b/src/components/Tooltip/const.ts
@@ -0,0 +1,6 @@
+import {atoms as a} from '#/alf'
+
+export const BUBBLE_MAX_WIDTH = 240
+export const ARROW_SIZE = 12
+export const ARROW_HALF_SIZE = ARROW_SIZE / 2
+export const MIN_EDGE_SPACE = a.px_lg.paddingLeft
diff --git a/src/components/Tooltip/index.tsx b/src/components/Tooltip/index.tsx
new file mode 100644
--- /dev/null
+++ b/src/components/Tooltip/index.tsx
@@ -0,0 +1,411 @@
+import {
+ Children,
+ createContext,
+ useCallback,
+ useContext,
+ useMemo,
+ useRef,
+ useState,
+} from 'react'
+import {useWindowDimensions, View} from 'react-native'
+import Animated, {Easing, ZoomIn} from 'react-native-reanimated'
+import {useSafeAreaInsets} from 'react-native-safe-area-context'
+
+import {atoms as a, select, useTheme} from '#/alf'
+import {useOnGesture} from '#/components/hooks/useOnGesture'
+import {Portal} from '#/components/Portal'
+import {
+ ARROW_HALF_SIZE,
+ ARROW_SIZE,
+ BUBBLE_MAX_WIDTH,
+ MIN_EDGE_SPACE,
+} from '#/components/Tooltip/const'
+import {Text} from '#/components/Typography'
+
+/**
+ * These are native specific values, not shared with web
+ */
+const ARROW_VISUAL_OFFSET = ARROW_SIZE / 1.25 // vibes-based, slightly off the target
+const BUBBLE_SHADOW_OFFSET = ARROW_SIZE / 3 // vibes-based, provide more shadow beneath tip
+
+type TooltipContextType = {
+ position: 'top' | 'bottom'
+ ready: boolean
+ onVisibleChange: (visible: boolean) => void
+}
+
+type TargetContextType = {
+ targetMeasurements:
+ | {
+ x: number
+ y: number
+ width: number
+ height: number
+ }
+ | undefined
+ targetRef: React.RefObject
+}
+
+const TooltipContext = createContext({
+ position: 'bottom',
+ ready: false,
+ onVisibleChange: () => {},
+})
+
+const TargetContext = createContext({
+ targetMeasurements: undefined,
+ targetRef: {current: null},
+})
+
+export function Outer({
+ children,
+ position = 'bottom',
+ visible: requestVisible,
+ onVisibleChange,
+}: {
+ children: React.ReactNode
+ position?: 'top' | 'bottom'
+ visible: boolean
+ onVisibleChange: (visible: boolean) => void
+}) {
+ /**
+ * Whether we have measured the target and are ready to show the tooltip.
+ */
+ const [ready, setReady] = useState(false)
+ /**
+ * Lagging state to track the externally-controlled visibility of the
+ * tooltip.
+ */
+ const [prevRequestVisible, setPrevRequestVisible] = useState<
+ boolean | undefined
+ >()
+ /**
+ * Needs to reference the element this Tooltip is attached to.
+ */
+ const targetRef = useRef(null)
+ const [targetMeasurements, setTargetMeasurements] = useState<
+ | {
+ x: number
+ y: number
+ width: number
+ height: number
+ }
+ | undefined
+ >(undefined)
+
+ if (requestVisible && !prevRequestVisible) {
+ setPrevRequestVisible(true)
+
+ if (targetRef.current) {
+ /*
+ * Once opened, measure the dimensions and position of the target
+ */
+ targetRef.current.measure((_x, _y, width, height, pageX, pageY) => {
+ if (pageX !== undefined && pageY !== undefined && width && height) {
+ setTargetMeasurements({x: pageX, y: pageY, width, height})
+ setReady(true)
+ }
+ })
+ }
+ } else if (!requestVisible && prevRequestVisible) {
+ setPrevRequestVisible(false)
+ setTargetMeasurements(undefined)
+ setReady(false)
+ }
+
+ const ctx = useMemo(
+ () => ({position, ready, onVisibleChange}),
+ [position, ready, onVisibleChange],
+ )
+ const targetCtx = useMemo(
+ () => ({targetMeasurements, targetRef}),
+ [targetMeasurements, targetRef],
+ )
+
+ return (
+
+
+ {children}
+
+
+ )
+}
+
+export function Target({children}: {children: React.ReactNode}) {
+ const {targetRef} = useContext(TargetContext)
+
+ return (
+
+ {children}
+
+ )
+}
+
+export function Content({
+ children,
+ label,
+}: {
+ children: React.ReactNode
+ label: string
+}) {
+ const {position, ready, onVisibleChange} = useContext(TooltipContext)
+ const {targetMeasurements} = useContext(TargetContext)
+ const requestClose = useCallback(() => {
+ onVisibleChange(false)
+ }, [onVisibleChange])
+
+ if (!ready || !targetMeasurements) return null
+
+ return (
+
+
+ {children}
+
+
+ )
+}
+
+function Bubble({
+ children,
+ label,
+ position,
+ requestClose,
+ targetMeasurements,
+}: {
+ children: React.ReactNode
+ label: string
+ position: TooltipContextType['position']
+ requestClose: () => void
+ targetMeasurements: Exclude<
+ TargetContextType['targetMeasurements'],
+ undefined
+ >
+}) {
+ const t = useTheme()
+ const insets = useSafeAreaInsets()
+ const dimensions = useWindowDimensions()
+ const [bubbleMeasurements, setBubbleMeasurements] = useState<
+ | {
+ width: number
+ height: number
+ }
+ | undefined
+ >(undefined)
+ const coords = useMemo(() => {
+ if (!bubbleMeasurements)
+ return {
+ top: 0,
+ bottom: 0,
+ left: 0,
+ right: 0,
+ tipTop: 0,
+ tipLeft: 0,
+ }
+
+ const {width: ww, height: wh} = dimensions
+ const maxTop = insets.top
+ const maxBottom = wh - insets.bottom
+ const {width: cw, height: ch} = bubbleMeasurements
+ const minLeft = MIN_EDGE_SPACE
+ const maxLeft = ww - minLeft
+
+ let computedPosition: 'top' | 'bottom' = position
+ let top = targetMeasurements.y + targetMeasurements.height
+ let left = Math.max(
+ minLeft,
+ targetMeasurements.x + targetMeasurements.width / 2 - cw / 2,
+ )
+ const tipTranslate = ARROW_HALF_SIZE * -1
+ let tipTop = tipTranslate
+
+ if (left + cw > maxLeft) {
+ left -= left + cw - maxLeft
+ }
+
+ let tipLeft =
+ targetMeasurements.x -
+ left +
+ targetMeasurements.width / 2 -
+ ARROW_HALF_SIZE
+
+ let bottom = top + ch
+
+ function positionTop() {
+ top = top - ch - targetMeasurements.height
+ bottom = top + ch
+ tipTop = tipTop + ch
+ computedPosition = 'top'
+ }
+
+ function positionBottom() {
+ top = targetMeasurements.y + targetMeasurements.height
+ bottom = top + ch
+ tipTop = tipTranslate
+ computedPosition = 'bottom'
+ }
+
+ if (position === 'top') {
+ positionTop()
+ if (top < maxTop) {
+ positionBottom()
+ }
+ } else {
+ if (bottom > maxBottom) {
+ positionTop()
+ }
+ }
+
+ if (computedPosition === 'bottom') {
+ top += ARROW_VISUAL_OFFSET
+ bottom += ARROW_VISUAL_OFFSET
+ } else {
+ top -= ARROW_VISUAL_OFFSET
+ bottom -= ARROW_VISUAL_OFFSET
+ }
+
+ return {
+ computedPosition,
+ top,
+ bottom,
+ left,
+ right: left + cw,
+ tipTop,
+ tipLeft,
+ }
+ }, [position, targetMeasurements, bubbleMeasurements, insets, dimensions])
+
+ const requestCloseWrapped = useCallback(() => {
+ setBubbleMeasurements(undefined)
+ requestClose()
+ }, [requestClose])
+
+ useOnGesture(
+ useCallback(
+ e => {
+ const {x, y} = e
+ const isInside =
+ x > coords.left &&
+ x < coords.right &&
+ y > coords.top &&
+ y < coords.bottom
+
+ if (!isInside) {
+ requestCloseWrapped()
+ }
+ },
+ [coords, requestCloseWrapped],
+ ),
+ )
+
+ return (
+
+
+
+ {
+ setBubbleMeasurements({
+ width: e.nativeEvent.layout.width,
+ height: e.nativeEvent.layout.height,
+ })
+ }}>
+ {children}
+
+
+
+ )
+}
+
+function oppposite(position: 'top' | 'bottom') {
+ switch (position) {
+ case 'top':
+ return 'center bottom'
+ case 'bottom':
+ return 'center top'
+ default:
+ return 'center'
+ }
+}
+
+export function TextBubble({children}: {children: React.ReactNode}) {
+ const c = Children.toArray(children)
+ return (
+
+
+ {c.map((child, i) => (
+
+ {child}
+
+ ))}
+
+
+ )
+}
diff --git a/src/components/Tooltip/index.web.tsx b/src/components/Tooltip/index.web.tsx
new file mode 100644
--- /dev/null
+++ b/src/components/Tooltip/index.web.tsx
@@ -0,0 +1,112 @@
+import {Children, createContext, useContext, useMemo} from 'react'
+import {View} from 'react-native'
+import {Popover} from 'radix-ui'
+
+import {atoms as a, flatten, select, useTheme} from '#/alf'
+import {transparentifyColor} from '#/alf/util/colorGeneration'
+import {
+ ARROW_SIZE,
+ BUBBLE_MAX_WIDTH,
+ MIN_EDGE_SPACE,
+} from '#/components/Tooltip/const'
+import {Text} from '#/components/Typography'
+
+type TooltipContextType = {
+ position: 'top' | 'bottom'
+}
+
+const TooltipContext = createContext({
+ position: 'bottom',
+})
+
+export function Outer({
+ children,
+ position = 'bottom',
+ visible,
+ onVisibleChange,
+}: {
+ children: React.ReactNode
+ position?: 'top' | 'bottom'
+ visible: boolean
+ onVisibleChange: (visible: boolean) => void
+}) {
+ const ctx = useMemo(() => ({position}), [position])
+ return (
+
+ {children}
+
+ )
+}
+
+export function Target({children}: {children: React.ReactNode}) {
+ return (
+
+ {children}
+
+ )
+}
+
+export function Content({
+ children,
+ label,
+}: {
+ children: React.ReactNode
+ label: string
+}) {
+ const t = useTheme()
+ const {position} = useContext(TooltipContext)
+ return (
+
+
+
+
+ {children}
+
+
+
+ )
+}
+
+export function TextBubble({children}: {children: React.ReactNode}) {
+ const c = Children.toArray(children)
+ return (
+
+
+ {c.map((child, i) => (
+
+ {child}
+
+ ))}
+
+
+ )
+}
diff --git a/src/components/dialogs/SearchablePeopleList.tsx b/src/components/dialogs/SearchablePeopleList.tsx
--- a/src/components/dialogs/SearchablePeopleList.tsx
+++ b/src/components/dialogs/SearchablePeopleList.tsx
@@ -390,8 +390,8 @@
!enabled
? {opacity: 0.5}
: pressed || focused || hovered
- ? t.atoms.bg_contrast_25
- : t.atoms.bg,
+ ? t.atoms.bg_contrast_25
+ : t.atoms.bg,
]}>
{children}
diff --git a/src/components/icons/VerifiedCheck.tsx b/src/components/icons/VerifiedCheck.tsx
--- a/src/components/icons/VerifiedCheck.tsx
+++ b/src/components/icons/VerifiedCheck.tsx
@@ -3,28 +3,27 @@
import {type Props, useCommonSVGProps} from '#/components/icons/common'
-export const VerifiedCheck = React.forwardRef