Bluesky app fork with some witchin' additions 💫 witchsky.app
bluesky fork client

Merge branch 'main' of https://github.com/bluesky-social/social-app

+1124 -448
-4
.github/workflows/build-submit-android.yml
··· 52 distribution: 'temurin' 53 java-version: '17' 54 55 - - name: "Use upgraded MMKV for Fabric" 56 - run: | 57 - sed -i 's/"react-native-mmkv": "\^2\.12\.2"/"react-native-mmkv": "^3.3.0"/' package.json 58 - 59 - name: ⚙️ Install dependencies 60 run: yarn install 61
··· 52 distribution: 'temurin' 53 java-version: '17' 54 55 - name: ⚙️ Install dependencies 56 run: yarn install 57
+1 -13
.github/workflows/bundle-deploy-eas-update.yml
··· 5 push: 6 branches: 7 - main 8 - - hailey/eas-fab 9 workflow_dispatch: 10 inputs: 11 channel: ··· 119 120 - name: 🏗️ Create Bundle 121 if: ${{ !steps.fingerprint.outputs.includes-changes }} 122 - run: | 123 - 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 124 - mv ./dist ./ios-dist 125 - sed -i 's/"react-native-mmkv": "\^2\.12\.2"/"react-native-mmkv": "^3.3.0"/' package.json 126 - yarn install 127 - 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 128 - mv ./dist ./android-dist 129 - 130 131 - name: 📦 Package Bundle and 🚀 Deploy 132 if: ${{ !steps.fingerprint.outputs.includes-changes }} ··· 282 with: 283 distribution: 'temurin' 284 java-version: '17' 285 - 286 - - name: "Use upgraded MMKV for Fabric" 287 - run: | 288 - sed -i 's/"react-native-mmkv": "\^2\.12\.2"/"react-native-mmkv": "^3.3.0"/' package.json 289 290 - name: ⚙️ Install dependencies 291 run: yarn install
··· 5 push: 6 branches: 7 - main 8 workflow_dispatch: 9 inputs: 10 channel: ··· 118 119 - name: 🏗️ Create Bundle 120 if: ${{ !steps.fingerprint.outputs.includes-changes }} 121 + 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 122 123 - name: 📦 Package Bundle and 🚀 Deploy 124 if: ${{ !steps.fingerprint.outputs.includes-changes }} ··· 274 with: 275 distribution: 'temurin' 276 java-version: '17' 277 278 - name: ⚙️ Install dependencies 279 run: yarn install
+1 -1
app.config.js
··· 221 compileSdkVersion: 35, 222 targetSdkVersion: 35, 223 buildToolsVersion: '35.0.0', 224 - newArchEnabled: true, 225 }, 226 }, 227 ],
··· 221 compileSdkVersion: 35, 222 targetSdkVersion: 35, 223 buildToolsVersion: '35.0.0', 224 + newArchEnabled: false, 225 }, 226 }, 227 ],
+1 -3
bskyembed/tailwind.config.cjs
··· 1 /** @type {import('tailwindcss').Config} */ 2 module.exports = { 3 content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], 4 - darkMode: ['variant', [ 5 - '&:is(.dark *):not(:is(.dark .light *))', 6 - ]], 7 theme: { 8 extend: { 9 colors: {
··· 1 /** @type {import('tailwindcss').Config} */ 2 module.exports = { 3 content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], 4 + darkMode: ['variant', ['&:is(.dark *):not(:is(.dark .light *))']], 5 theme: { 6 extend: { 7 colors: {
-1
bskyembed/tsconfig.snippet.json
··· 1 - 2 { 3 "compilerOptions": { 4 "target": "ES5",
··· 1 { 2 "compilerOptions": { 3 "target": "ES5",
+11 -8
bskylink/src/db/index.ts
··· 1 import assert from 'assert' 2 import { 3 Kysely, 4 - KyselyPlugin, 5 Migrator, 6 - PluginTransformQueryArgs, 7 - PluginTransformResultArgs, 8 PostgresDialect, 9 - QueryResult, 10 - RootOperationNode, 11 - UnknownRow, 12 } from 'kysely' 13 import {default as Pg} from 'pg' 14 15 import {dbLogger as log} from '../logger.js' 16 import {default as migrations} from './migrations/index.js' 17 import {DbMigrationProvider} from './migrations/provider.js' 18 - import {DbSchema} from './schema.js' 19 20 export class Database { 21 migrator: Migrator 22 destroyed = false 23 24 - constructor(public db: Kysely<DbSchema>, public cfg: PgConfig) { 25 this.migrator = new Migrator({ 26 db, 27 migrationTableSchema: cfg.schema,
··· 1 import assert from 'assert' 2 import { 3 Kysely, 4 + type KyselyPlugin, 5 Migrator, 6 + type PluginTransformQueryArgs, 7 + type PluginTransformResultArgs, 8 PostgresDialect, 9 + type QueryResult, 10 + type RootOperationNode, 11 + type UnknownRow, 12 } from 'kysely' 13 import {default as Pg} from 'pg' 14 15 import {dbLogger as log} from '../logger.js' 16 import {default as migrations} from './migrations/index.js' 17 import {DbMigrationProvider} from './migrations/provider.js' 18 + import {type DbSchema} from './schema.js' 19 20 export class Database { 21 migrator: Migrator 22 destroyed = false 23 24 + constructor( 25 + public db: Kysely<DbSchema>, 26 + public cfg: PgConfig, 27 + ) { 28 this.migrator = new Migrator({ 29 db, 30 migrationTableSchema: cfg.schema,
+7 -4
bskylink/src/index.ts
··· 1 import events from 'node:events' 2 - import http from 'node:http' 3 4 import cors from 'cors' 5 import express from 'express' 6 - import {createHttpTerminator, HttpTerminator} from 'http-terminator' 7 8 - import {Config} from './config.js' 9 import {AppContext} from './context.js' 10 import {default as routes, errorHandler} from './routes/index.js' 11 ··· 17 public server?: http.Server 18 private terminator?: HttpTerminator 19 20 - constructor(public app: express.Application, public ctx: AppContext) {} 21 22 static async create(cfg: Config): Promise<LinkService> { 23 let app = express()
··· 1 import events from 'node:events' 2 + import type http from 'node:http' 3 4 import cors from 'cors' 5 import express from 'express' 6 + import {createHttpTerminator, type HttpTerminator} from 'http-terminator' 7 8 + import {type Config} from './config.js' 9 import {AppContext} from './context.js' 10 import {default as routes, errorHandler} from './routes/index.js' 11 ··· 17 public server?: http.Server 18 private terminator?: HttpTerminator 19 20 + constructor( 21 + public app: express.Application, 22 + public ctx: AppContext, 23 + ) {} 24 25 static async create(cfg: Config): Promise<LinkService> { 26 let app = express()
+7 -4
bskyogcard/src/index.ts
··· 1 import events from 'node:events' 2 - import http from 'node:http' 3 4 import express from 'express' 5 - import {createHttpTerminator, HttpTerminator} from 'http-terminator' 6 7 - import {Config} from './config.js' 8 import {AppContext} from './context.js' 9 import {default as routes, errorHandler} from './routes/index.js' 10 ··· 15 public server?: http.Server 16 private terminator?: HttpTerminator 17 18 - constructor(public app: express.Application, public ctx: AppContext) {} 19 20 static async create(cfg: Config): Promise<CardService> { 21 let app = express()
··· 1 import events from 'node:events' 2 + import type http from 'node:http' 3 4 import express from 'express' 5 + import {createHttpTerminator, type HttpTerminator} from 'http-terminator' 6 7 + import {type Config} from './config.js' 8 import {AppContext} from './context.js' 9 import {default as routes, errorHandler} from './routes/index.js' 10 ··· 15 public server?: http.Server 16 private terminator?: HttpTerminator 17 18 + constructor( 19 + public app: express.Application, 20 + public ctx: AppContext, 21 + ) {} 22 23 static async create(cfg: Config): Promise<CardService> { 24 let app = express()
+1 -1
modules/expo-emoji-picker/src/EmojiPicker.android.tsx
··· 12 flex: 1, 13 width: '100%', 14 backgroundColor: scheme === 'dark' ? '#000' : '#fff', 15 - } as const), 16 [scheme], 17 ) 18
··· 12 flex: 1, 13 width: '100%', 14 backgroundColor: scheme === 'dark' ? '#000' : '#fff', 15 + }) as const, 16 [scheme], 17 ) 18
+2 -3
package.json
··· 61 "intl:push": "crowdin push translations --verbose -b main", 62 "nuke": "rm -rf ./node_modules && rm -rf ./ios && rm -rf ./android", 63 "update-extensions": "bash scripts/updateExtensions.sh", 64 - "export-ios": "npx expo export --platform ios --dump-sourcemap && yarn upload-native-sourcemaps", 65 - "export-android": "npx expo export --platform android --dump-sourcemap && yarn upload-native-sourcemaps", 66 "upload-native-sourcemaps": "npx sentry-expo-upload-sourcemaps dist", 67 "make-deploy-bundle": "bash scripts/bundleUpdate.sh", 68 "generate-webpack-stats-file": "EXPO_PUBLIC_GENERATE_STATS=1 yarn build-web", ··· 266 "lint-staged": "^13.2.3", 267 "lockfile-lint": "^4.14.0", 268 "metro-react-native-babel-preset": "^0.77.0", 269 - "prettier": "^2.8.3", 270 "react-native-dotenv": "^3.4.11", 271 "react-refresh": "^0.14.0", 272 "svgo": "^3.3.2",
··· 61 "intl:push": "crowdin push translations --verbose -b main", 62 "nuke": "rm -rf ./node_modules && rm -rf ./ios && rm -rf ./android", 63 "update-extensions": "bash scripts/updateExtensions.sh", 64 + "export": "npx expo export --dump-sourcemap && yarn upload-native-sourcemaps", 65 "upload-native-sourcemaps": "npx sentry-expo-upload-sourcemaps dist", 66 "make-deploy-bundle": "bash scripts/bundleUpdate.sh", 67 "generate-webpack-stats-file": "EXPO_PUBLIC_GENERATE_STATS=1 yarn build-web", ··· 265 "lint-staged": "^13.2.3", 266 "lockfile-lint": "^4.14.0", 267 "metro-react-native-babel-preset": "^0.77.0", 268 + "prettier": "^3.6.0", 269 "react-native-dotenv": "^3.4.11", 270 "react-refresh": "^0.14.0", 271 "svgo": "^3.3.2",
+9 -17
scripts/bundleUpdate.js
··· 3 const fsp = fs.promises 4 const path = require('path') 5 6 - const IOS_DIST_DIR = './ios-dist' 7 - const ANDROID_DIST_DIR = './android-dist' 8 - 9 const BUNDLES_DIR = '/_expo/static/js' 10 - 11 - const IOS_BUNDLE_DIR = path.join(IOS_DIST_DIR, BUNDLES_DIR, '/ios') 12 - const ANDROID_BUNDLE_DIR = path.join(ANDROID_DIST_DIR, BUNDLES_DIR, '/android') 13 - 14 - const IOS_METADATA_PATH = path.join(IOS_DIST_DIR, '/metadata.json') 15 - const ANDROID_METADATA_PATH = path.join(ANDROID_DIST_DIR, '/metadata.json') 16 - 17 const DEST_DIR = './bundleTempDir' 18 19 // Weird, don't feel like figuring out _why_ it wants this 20 - const IOS_METADATA = require(`../${IOS_METADATA_PATH}`) 21 - const ANDROID_METADATA = require(`../${ANDROID_METADATA_PATH}`) 22 - 23 - const IOS_METADATA_ASSETS = IOS_METADATA.fileMetadata.ios.assets 24 - const ANDROID_METADATA_ASSETS = ANDROID_METADATA.fileMetadata.android.assets 25 26 const getMd5 = async path => { 27 return new Promise(res => { ··· 68 69 console.log('Getting ios asset md5s and moving them...') 70 for (const asset of IOS_METADATA_ASSETS) { 71 - const currPath = path.join(IOS_DIST_DIR, asset.path) 72 const md5 = await getMd5(currPath) 73 const withExtPath = `assets/${md5}.${asset.ext}` 74 iosAssets.push(withExtPath) ··· 77 78 console.log('Getting android asset md5s and moving them...') 79 for (const asset of ANDROID_METADATA_ASSETS) { 80 - const currPath = path.join(ANDROID_DIST_DIR, asset.path) 81 const md5 = await getMd5(currPath) 82 const withExtPath = `assets/${md5}.${asset.ext}` 83 androidAssets.push(withExtPath)
··· 3 const fsp = fs.promises 4 const path = require('path') 5 6 + const DIST_DIR = './dist' 7 const BUNDLES_DIR = '/_expo/static/js' 8 + const IOS_BUNDLE_DIR = path.join(DIST_DIR, BUNDLES_DIR, '/ios') 9 + const ANDROID_BUNDLE_DIR = path.join(DIST_DIR, BUNDLES_DIR, '/android') 10 + const METADATA_PATH = path.join(DIST_DIR, '/metadata.json') 11 const DEST_DIR = './bundleTempDir' 12 13 // Weird, don't feel like figuring out _why_ it wants this 14 + const METADATA = require(`../${METADATA_PATH}`) 15 + const IOS_METADATA_ASSETS = METADATA.fileMetadata.ios.assets 16 + const ANDROID_METADATA_ASSETS = METADATA.fileMetadata.android.assets 17 18 const getMd5 = async path => { 19 return new Promise(res => { ··· 60 61 console.log('Getting ios asset md5s and moving them...') 62 for (const asset of IOS_METADATA_ASSETS) { 63 + const currPath = path.join(DIST_DIR, asset.path) 64 const md5 = await getMd5(currPath) 65 const withExtPath = `assets/${md5}.${asset.ext}` 66 iosAssets.push(withExtPath) ··· 69 70 console.log('Getting android asset md5s and moving them...') 71 for (const asset of ANDROID_METADATA_ASSETS) { 72 + const currPath = path.join(DIST_DIR, asset.path) 73 const md5 = await getMd5(currPath) 74 const withExtPath = `assets/${md5}.${asset.ext}` 75 androidAssets.push(withExtPath)
+3 -4
scripts/post-web-build.js
··· 9 'scripts.html', 10 ) 11 12 - const {entrypoints} = require(path.join( 13 - projectRoot, 14 - 'web-build/asset-manifest.json', 15 - )) 16 17 console.log(`Found ${entrypoints.length} entrypoints`) 18 console.log(`Writing ${templateFile}`)
··· 9 'scripts.html', 10 ) 11 12 + const {entrypoints} = require( 13 + path.join(projectRoot, 'web-build/asset-manifest.json'), 14 + ) 15 16 console.log(`Found ${entrypoints.length} entrypoints`) 17 console.log(`Writing ${templateFile}`)
+8 -5
src/App.native.tsx
··· 33 ensureGeolocationResolved, 34 Provider as GeolocationProvider, 35 } from '#/state/geolocation' 36 import {Provider as HomeBadgeProvider} from '#/state/home-badge' 37 import {Provider as InvitesStateProvider} from '#/state/invites' 38 import {Provider as LightboxStateProvider} from '#/state/lightbox' ··· 154 <HideBottomBarBorderProvider> 155 <GestureHandlerRootView 156 style={s.h100pct}> 157 - <IntentDialogProvider> 158 - <TestCtrls /> 159 - <Shell /> 160 - <NuxDialogs /> 161 - </IntentDialogProvider> 162 </GestureHandlerRootView> 163 </HideBottomBarBorderProvider> 164 </ServiceAccountManager>
··· 33 ensureGeolocationResolved, 34 Provider as GeolocationProvider, 35 } from '#/state/geolocation' 36 + import {GlobalGestureEventsProvider} from '#/state/global-gesture-events' 37 import {Provider as HomeBadgeProvider} from '#/state/home-badge' 38 import {Provider as InvitesStateProvider} from '#/state/invites' 39 import {Provider as LightboxStateProvider} from '#/state/lightbox' ··· 155 <HideBottomBarBorderProvider> 156 <GestureHandlerRootView 157 style={s.h100pct}> 158 + <GlobalGestureEventsProvider> 159 + <IntentDialogProvider> 160 + <TestCtrls /> 161 + <Shell /> 162 + <NuxDialogs /> 163 + </IntentDialogProvider> 164 + </GlobalGestureEventsProvider> 165 </GestureHandlerRootView> 166 </HideBottomBarBorderProvider> 167 </ServiceAccountManager>
+4
src/alf/atoms.ts
··· 983 transition_none: web({ 984 transitionProperty: 'none', 985 }), 986 transition_all: web({ 987 transitionProperty: 'all', 988 transitionTimingFunction: 'cubic-bezier(0.17, 0.73, 0.14, 1)',
··· 983 transition_none: web({ 984 transitionProperty: 'none', 985 }), 986 + transition_timing_default: web({ 987 + transitionTimingFunction: 'cubic-bezier(0.17, 0.73, 0.14, 1)', 988 + transitionDuration: '100ms', 989 + }), 990 transition_all: web({ 991 transitionProperty: 'all', 992 transitionTimingFunction: 'cubic-bezier(0.17, 0.73, 0.14, 1)',
+3 -3
src/components/ContextMenu/index.tsx
··· 190 if (item) playHaptic('Light') 191 setHoveredMenuItem(item) 192 }, 193 - } satisfies ContextType), 194 [ 195 measurement, 196 setMeasurement, ··· 710 const xOffset = position 711 ? position.x 712 : align === 'left' 713 - ? measurement.x 714 - : measurement.x + measurement.width - layout.width 715 716 registerHoverable( 717 id,
··· 190 if (item) playHaptic('Light') 191 setHoveredMenuItem(item) 192 }, 193 + }) satisfies ContextType, 194 [ 195 measurement, 196 setMeasurement, ··· 710 const xOffset = position 711 ? position.x 712 : align === 'left' 713 + ? measurement.x 714 + : measurement.x + measurement.width - layout.width 715 716 registerHoverable( 717 id,
+4 -4
src/components/Link.tsx
··· 100 return typeof to === 'string' 101 ? convertBskyAppUrlIfNeeded(sanitizeUrl(to)) 102 : to.screen 103 - ? router.matchName(to.screen)?.build(to.params) 104 - : to.href 105 - ? convertBskyAppUrlIfNeeded(sanitizeUrl(to.href)) 106 - : undefined 107 }, [to]) 108 109 if (!href) {
··· 100 return typeof to === 'string' 101 ? convertBskyAppUrlIfNeeded(sanitizeUrl(to)) 102 : to.screen 103 + ? router.matchName(to.screen)?.build(to.params) 104 + : to.href 105 + ? convertBskyAppUrlIfNeeded(sanitizeUrl(to.href)) 106 + : undefined 107 }, [to]) 108 109 if (!href) {
+3 -3
src/components/Post/Embed/ImageEmbed.tsx
··· 82 rest.viewContext === PostEmbedViewContext.ThreadHighlighted 83 ? 'none' 84 : rest.viewContext === 85 - PostEmbedViewContext.FeedEmbedRecordWithMedia 86 - ? 'square' 87 - : 'constrained' 88 } 89 image={image} 90 onPress={(containerRef, dims) => onPress(0, [containerRef], [dims])}
··· 82 rest.viewContext === PostEmbedViewContext.ThreadHighlighted 83 ? 'none' 84 : rest.viewContext === 85 + PostEmbedViewContext.FeedEmbedRecordWithMedia 86 + ? 'square' 87 + : 'constrained' 88 } 89 image={image} 90 onPress={(containerRef, dims) => onPress(0, [containerRef], [dims])}
+2 -2
src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx
··· 317 !focused 318 ? msg`Unmute video` 319 : playing 320 - ? msg`Pause video` 321 - : msg`Play video`, 322 )} 323 accessibilityHint="" 324 style={[
··· 317 !focused 318 ? msg`Unmute video` 319 : playing 320 + ? msg`Pause video` 321 + : msg`Play video`, 322 )} 323 accessibilityHint="" 324 style={[
+2 -2
src/components/PostControls/PostMenu/PostMenuItems.tsx
··· 698 isDetachPending 699 ? Loader 700 : quoteEmbed.isDetached 701 - ? Eye 702 - : EyeSlash 703 } 704 position="right" 705 />
··· 698 isDetachPending 699 ? Loader 700 : quoteEmbed.isDetached 701 + ? Eye 702 + : EyeSlash 703 } 704 position="right" 705 />
+2 -2
src/components/Select/index.web.tsx
··· 111 borderColor: focused 112 ? t.palette.primary_500 113 : hovered 114 - ? t.palette.contrast_100 115 - : t.palette.contrast_25, 116 }, 117 ])}> 118 {children}
··· 111 borderColor: focused 112 ? t.palette.primary_500 113 : hovered 114 + ? t.palette.contrast_100 115 + : t.palette.contrast_25, 116 }, 117 ])}> 118 {children}
+6
src/components/Tooltip/const.ts
···
··· 1 + import {atoms as a} from '#/alf' 2 + 3 + export const BUBBLE_MAX_WIDTH = 240 4 + export const ARROW_SIZE = 12 5 + export const ARROW_HALF_SIZE = ARROW_SIZE / 2 6 + export const MIN_EDGE_SPACE = a.px_lg.paddingLeft
+411
src/components/Tooltip/index.tsx
···
··· 1 + import { 2 + Children, 3 + createContext, 4 + useCallback, 5 + useContext, 6 + useMemo, 7 + useRef, 8 + useState, 9 + } from 'react' 10 + import {useWindowDimensions, View} from 'react-native' 11 + import Animated, {Easing, ZoomIn} from 'react-native-reanimated' 12 + import {useSafeAreaInsets} from 'react-native-safe-area-context' 13 + 14 + import {atoms as a, select, useTheme} from '#/alf' 15 + import {useOnGesture} from '#/components/hooks/useOnGesture' 16 + import {Portal} from '#/components/Portal' 17 + import { 18 + ARROW_HALF_SIZE, 19 + ARROW_SIZE, 20 + BUBBLE_MAX_WIDTH, 21 + MIN_EDGE_SPACE, 22 + } from '#/components/Tooltip/const' 23 + import {Text} from '#/components/Typography' 24 + 25 + /** 26 + * These are native specific values, not shared with web 27 + */ 28 + const ARROW_VISUAL_OFFSET = ARROW_SIZE / 1.25 // vibes-based, slightly off the target 29 + const BUBBLE_SHADOW_OFFSET = ARROW_SIZE / 3 // vibes-based, provide more shadow beneath tip 30 + 31 + type TooltipContextType = { 32 + position: 'top' | 'bottom' 33 + ready: boolean 34 + onVisibleChange: (visible: boolean) => void 35 + } 36 + 37 + type TargetContextType = { 38 + targetMeasurements: 39 + | { 40 + x: number 41 + y: number 42 + width: number 43 + height: number 44 + } 45 + | undefined 46 + targetRef: React.RefObject<View> 47 + } 48 + 49 + const TooltipContext = createContext<TooltipContextType>({ 50 + position: 'bottom', 51 + ready: false, 52 + onVisibleChange: () => {}, 53 + }) 54 + 55 + const TargetContext = createContext<TargetContextType>({ 56 + targetMeasurements: undefined, 57 + targetRef: {current: null}, 58 + }) 59 + 60 + export function Outer({ 61 + children, 62 + position = 'bottom', 63 + visible: requestVisible, 64 + onVisibleChange, 65 + }: { 66 + children: React.ReactNode 67 + position?: 'top' | 'bottom' 68 + visible: boolean 69 + onVisibleChange: (visible: boolean) => void 70 + }) { 71 + /** 72 + * Whether we have measured the target and are ready to show the tooltip. 73 + */ 74 + const [ready, setReady] = useState(false) 75 + /** 76 + * Lagging state to track the externally-controlled visibility of the 77 + * tooltip. 78 + */ 79 + const [prevRequestVisible, setPrevRequestVisible] = useState< 80 + boolean | undefined 81 + >() 82 + /** 83 + * Needs to reference the element this Tooltip is attached to. 84 + */ 85 + const targetRef = useRef<View>(null) 86 + const [targetMeasurements, setTargetMeasurements] = useState< 87 + | { 88 + x: number 89 + y: number 90 + width: number 91 + height: number 92 + } 93 + | undefined 94 + >(undefined) 95 + 96 + if (requestVisible && !prevRequestVisible) { 97 + setPrevRequestVisible(true) 98 + 99 + if (targetRef.current) { 100 + /* 101 + * Once opened, measure the dimensions and position of the target 102 + */ 103 + targetRef.current.measure((_x, _y, width, height, pageX, pageY) => { 104 + if (pageX !== undefined && pageY !== undefined && width && height) { 105 + setTargetMeasurements({x: pageX, y: pageY, width, height}) 106 + setReady(true) 107 + } 108 + }) 109 + } 110 + } else if (!requestVisible && prevRequestVisible) { 111 + setPrevRequestVisible(false) 112 + setTargetMeasurements(undefined) 113 + setReady(false) 114 + } 115 + 116 + const ctx = useMemo( 117 + () => ({position, ready, onVisibleChange}), 118 + [position, ready, onVisibleChange], 119 + ) 120 + const targetCtx = useMemo( 121 + () => ({targetMeasurements, targetRef}), 122 + [targetMeasurements, targetRef], 123 + ) 124 + 125 + return ( 126 + <TooltipContext.Provider value={ctx}> 127 + <TargetContext.Provider value={targetCtx}> 128 + {children} 129 + </TargetContext.Provider> 130 + </TooltipContext.Provider> 131 + ) 132 + } 133 + 134 + export function Target({children}: {children: React.ReactNode}) { 135 + const {targetRef} = useContext(TargetContext) 136 + 137 + return ( 138 + <View collapsable={false} ref={targetRef}> 139 + {children} 140 + </View> 141 + ) 142 + } 143 + 144 + export function Content({ 145 + children, 146 + label, 147 + }: { 148 + children: React.ReactNode 149 + label: string 150 + }) { 151 + const {position, ready, onVisibleChange} = useContext(TooltipContext) 152 + const {targetMeasurements} = useContext(TargetContext) 153 + const requestClose = useCallback(() => { 154 + onVisibleChange(false) 155 + }, [onVisibleChange]) 156 + 157 + if (!ready || !targetMeasurements) return null 158 + 159 + return ( 160 + <Portal> 161 + <Bubble 162 + label={label} 163 + position={position} 164 + /* 165 + * Gotta pass these in here. Inside the Bubble, we're Potal-ed outside 166 + * the context providers. 167 + */ 168 + targetMeasurements={targetMeasurements} 169 + requestClose={requestClose}> 170 + {children} 171 + </Bubble> 172 + </Portal> 173 + ) 174 + } 175 + 176 + function Bubble({ 177 + children, 178 + label, 179 + position, 180 + requestClose, 181 + targetMeasurements, 182 + }: { 183 + children: React.ReactNode 184 + label: string 185 + position: TooltipContextType['position'] 186 + requestClose: () => void 187 + targetMeasurements: Exclude< 188 + TargetContextType['targetMeasurements'], 189 + undefined 190 + > 191 + }) { 192 + const t = useTheme() 193 + const insets = useSafeAreaInsets() 194 + const dimensions = useWindowDimensions() 195 + const [bubbleMeasurements, setBubbleMeasurements] = useState< 196 + | { 197 + width: number 198 + height: number 199 + } 200 + | undefined 201 + >(undefined) 202 + const coords = useMemo(() => { 203 + if (!bubbleMeasurements) 204 + return { 205 + top: 0, 206 + bottom: 0, 207 + left: 0, 208 + right: 0, 209 + tipTop: 0, 210 + tipLeft: 0, 211 + } 212 + 213 + const {width: ww, height: wh} = dimensions 214 + const maxTop = insets.top 215 + const maxBottom = wh - insets.bottom 216 + const {width: cw, height: ch} = bubbleMeasurements 217 + const minLeft = MIN_EDGE_SPACE 218 + const maxLeft = ww - minLeft 219 + 220 + let computedPosition: 'top' | 'bottom' = position 221 + let top = targetMeasurements.y + targetMeasurements.height 222 + let left = Math.max( 223 + minLeft, 224 + targetMeasurements.x + targetMeasurements.width / 2 - cw / 2, 225 + ) 226 + const tipTranslate = ARROW_HALF_SIZE * -1 227 + let tipTop = tipTranslate 228 + 229 + if (left + cw > maxLeft) { 230 + left -= left + cw - maxLeft 231 + } 232 + 233 + let tipLeft = 234 + targetMeasurements.x - 235 + left + 236 + targetMeasurements.width / 2 - 237 + ARROW_HALF_SIZE 238 + 239 + let bottom = top + ch 240 + 241 + function positionTop() { 242 + top = top - ch - targetMeasurements.height 243 + bottom = top + ch 244 + tipTop = tipTop + ch 245 + computedPosition = 'top' 246 + } 247 + 248 + function positionBottom() { 249 + top = targetMeasurements.y + targetMeasurements.height 250 + bottom = top + ch 251 + tipTop = tipTranslate 252 + computedPosition = 'bottom' 253 + } 254 + 255 + if (position === 'top') { 256 + positionTop() 257 + if (top < maxTop) { 258 + positionBottom() 259 + } 260 + } else { 261 + if (bottom > maxBottom) { 262 + positionTop() 263 + } 264 + } 265 + 266 + if (computedPosition === 'bottom') { 267 + top += ARROW_VISUAL_OFFSET 268 + bottom += ARROW_VISUAL_OFFSET 269 + } else { 270 + top -= ARROW_VISUAL_OFFSET 271 + bottom -= ARROW_VISUAL_OFFSET 272 + } 273 + 274 + return { 275 + computedPosition, 276 + top, 277 + bottom, 278 + left, 279 + right: left + cw, 280 + tipTop, 281 + tipLeft, 282 + } 283 + }, [position, targetMeasurements, bubbleMeasurements, insets, dimensions]) 284 + 285 + const requestCloseWrapped = useCallback(() => { 286 + setBubbleMeasurements(undefined) 287 + requestClose() 288 + }, [requestClose]) 289 + 290 + useOnGesture( 291 + useCallback( 292 + e => { 293 + const {x, y} = e 294 + const isInside = 295 + x > coords.left && 296 + x < coords.right && 297 + y > coords.top && 298 + y < coords.bottom 299 + 300 + if (!isInside) { 301 + requestCloseWrapped() 302 + } 303 + }, 304 + [coords, requestCloseWrapped], 305 + ), 306 + ) 307 + 308 + return ( 309 + <View 310 + accessible 311 + role="alert" 312 + accessibilityHint="" 313 + accessibilityLabel={label} 314 + // android 315 + importantForAccessibility="yes" 316 + // ios 317 + accessibilityViewIsModal 318 + style={[ 319 + a.absolute, 320 + a.align_start, 321 + { 322 + width: BUBBLE_MAX_WIDTH, 323 + opacity: bubbleMeasurements ? 1 : 0, 324 + top: coords.top, 325 + left: coords.left, 326 + }, 327 + ]}> 328 + <Animated.View 329 + entering={ZoomIn.easing(Easing.out(Easing.exp))} 330 + style={{transformOrigin: oppposite(position)}}> 331 + <View 332 + style={[ 333 + a.absolute, 334 + a.top_0, 335 + a.z_10, 336 + t.atoms.bg, 337 + select(t.name, { 338 + light: t.atoms.bg, 339 + dark: t.atoms.bg_contrast_100, 340 + dim: t.atoms.bg_contrast_100, 341 + }), 342 + { 343 + borderTopLeftRadius: a.rounded_2xs.borderRadius, 344 + borderBottomRightRadius: a.rounded_2xs.borderRadius, 345 + width: ARROW_SIZE, 346 + height: ARROW_SIZE, 347 + transform: [{rotate: '45deg'}], 348 + top: coords.tipTop, 349 + left: coords.tipLeft, 350 + }, 351 + ]} 352 + /> 353 + <View 354 + style={[ 355 + a.px_md, 356 + a.py_sm, 357 + a.rounded_sm, 358 + select(t.name, { 359 + light: t.atoms.bg, 360 + dark: t.atoms.bg_contrast_100, 361 + dim: t.atoms.bg_contrast_100, 362 + }), 363 + t.atoms.shadow_md, 364 + { 365 + shadowOpacity: 0.2, 366 + shadowOffset: { 367 + width: 0, 368 + height: 369 + BUBBLE_SHADOW_OFFSET * 370 + (coords.computedPosition === 'bottom' ? -1 : 1), 371 + }, 372 + }, 373 + ]} 374 + onLayout={e => { 375 + setBubbleMeasurements({ 376 + width: e.nativeEvent.layout.width, 377 + height: e.nativeEvent.layout.height, 378 + }) 379 + }}> 380 + {children} 381 + </View> 382 + </Animated.View> 383 + </View> 384 + ) 385 + } 386 + 387 + function oppposite(position: 'top' | 'bottom') { 388 + switch (position) { 389 + case 'top': 390 + return 'center bottom' 391 + case 'bottom': 392 + return 'center top' 393 + default: 394 + return 'center' 395 + } 396 + } 397 + 398 + export function TextBubble({children}: {children: React.ReactNode}) { 399 + const c = Children.toArray(children) 400 + return ( 401 + <Content label={c.join(' ')}> 402 + <View style={[a.gap_xs]}> 403 + {c.map((child, i) => ( 404 + <Text key={i} style={[a.text_sm, a.leading_snug]}> 405 + {child} 406 + </Text> 407 + ))} 408 + </View> 409 + </Content> 410 + ) 411 + }
+112
src/components/Tooltip/index.web.tsx
···
··· 1 + import {Children, createContext, useContext, useMemo} from 'react' 2 + import {View} from 'react-native' 3 + import {Popover} from 'radix-ui' 4 + 5 + import {atoms as a, flatten, select, useTheme} from '#/alf' 6 + import {transparentifyColor} from '#/alf/util/colorGeneration' 7 + import { 8 + ARROW_SIZE, 9 + BUBBLE_MAX_WIDTH, 10 + MIN_EDGE_SPACE, 11 + } from '#/components/Tooltip/const' 12 + import {Text} from '#/components/Typography' 13 + 14 + type TooltipContextType = { 15 + position: 'top' | 'bottom' 16 + } 17 + 18 + const TooltipContext = createContext<TooltipContextType>({ 19 + position: 'bottom', 20 + }) 21 + 22 + export function Outer({ 23 + children, 24 + position = 'bottom', 25 + visible, 26 + onVisibleChange, 27 + }: { 28 + children: React.ReactNode 29 + position?: 'top' | 'bottom' 30 + visible: boolean 31 + onVisibleChange: (visible: boolean) => void 32 + }) { 33 + const ctx = useMemo(() => ({position}), [position]) 34 + return ( 35 + <Popover.Root open={visible} onOpenChange={onVisibleChange}> 36 + <TooltipContext.Provider value={ctx}>{children}</TooltipContext.Provider> 37 + </Popover.Root> 38 + ) 39 + } 40 + 41 + export function Target({children}: {children: React.ReactNode}) { 42 + return ( 43 + <Popover.Trigger asChild> 44 + <View collapsable={false}>{children}</View> 45 + </Popover.Trigger> 46 + ) 47 + } 48 + 49 + export function Content({ 50 + children, 51 + label, 52 + }: { 53 + children: React.ReactNode 54 + label: string 55 + }) { 56 + const t = useTheme() 57 + const {position} = useContext(TooltipContext) 58 + return ( 59 + <Popover.Portal> 60 + <Popover.Content 61 + className="radix-popover-content" 62 + aria-label={label} 63 + side={position} 64 + sideOffset={4} 65 + collisionPadding={MIN_EDGE_SPACE} 66 + style={flatten([ 67 + a.rounded_sm, 68 + select(t.name, { 69 + light: t.atoms.bg, 70 + dark: t.atoms.bg_contrast_100, 71 + dim: t.atoms.bg_contrast_100, 72 + }), 73 + { 74 + minWidth: 'max-content', 75 + boxShadow: select(t.name, { 76 + light: `0 0 24px ${transparentifyColor(t.palette.black, 0.2)}`, 77 + dark: `0 0 24px ${transparentifyColor(t.palette.black, 0.2)}`, 78 + dim: `0 0 24px ${transparentifyColor(t.palette.black, 0.2)}`, 79 + }), 80 + }, 81 + ])}> 82 + <Popover.Arrow 83 + width={ARROW_SIZE} 84 + height={ARROW_SIZE / 2} 85 + fill={select(t.name, { 86 + light: t.atoms.bg.backgroundColor, 87 + dark: t.atoms.bg_contrast_100.backgroundColor, 88 + dim: t.atoms.bg_contrast_100.backgroundColor, 89 + })} 90 + /> 91 + <View style={[a.px_md, a.py_sm, {maxWidth: BUBBLE_MAX_WIDTH}]}> 92 + {children} 93 + </View> 94 + </Popover.Content> 95 + </Popover.Portal> 96 + ) 97 + } 98 + 99 + export function TextBubble({children}: {children: React.ReactNode}) { 100 + const c = Children.toArray(children) 101 + return ( 102 + <Content label={c.join(' ')}> 103 + <View style={[a.gap_xs]}> 104 + {c.map((child, i) => ( 105 + <Text key={i} style={[a.text_sm, a.leading_snug]}> 106 + {child} 107 + </Text> 108 + ))} 109 + </View> 110 + </Content> 111 + ) 112 + }
+12 -6
src/components/WhoCanReply.tsx
··· 1 import React from 'react' 2 - import {Keyboard, Platform, StyleProp, View, ViewStyle} from 'react-native' 3 import { 4 - AppBskyFeedDefs, 5 AppBskyFeedPost, 6 - AppBskyGraphDefs, 7 AtUri, 8 } from '@atproto/api' 9 import {msg, Trans} from '@lingui/macro' ··· 13 import {makeListLink, makeProfileLink} from '#/lib/routes/links' 14 import {isNative} from '#/platform/detection' 15 import { 16 - ThreadgateAllowUISetting, 17 threadgateViewToAllowUISetting, 18 } from '#/state/queries/threadgate' 19 import {atoms as a, useTheme} from '#/alf' ··· 70 const description = anyoneCanReply 71 ? _(msg`Everybody can reply`) 72 : noOneCanReply 73 - ? _(msg`Replies disabled`) 74 - : _(msg`Some people can reply`) 75 76 const onPressOpen = () => { 77 if (isNative && Keyboard.isVisible()) {
··· 1 import React from 'react' 2 import { 3 + Keyboard, 4 + Platform, 5 + type StyleProp, 6 + View, 7 + type ViewStyle, 8 + } from 'react-native' 9 + import { 10 + type AppBskyFeedDefs, 11 AppBskyFeedPost, 12 + type AppBskyGraphDefs, 13 AtUri, 14 } from '@atproto/api' 15 import {msg, Trans} from '@lingui/macro' ··· 19 import {makeListLink, makeProfileLink} from '#/lib/routes/links' 20 import {isNative} from '#/platform/detection' 21 import { 22 + type ThreadgateAllowUISetting, 23 threadgateViewToAllowUISetting, 24 } from '#/state/queries/threadgate' 25 import {atoms as a, useTheme} from '#/alf' ··· 76 const description = anyoneCanReply 77 ? _(msg`Everybody can reply`) 78 : noOneCanReply 79 + ? _(msg`Replies disabled`) 80 + : _(msg`Some people can reply`) 81 82 const onPressOpen = () => { 83 if (isNative && Keyboard.isVisible()) {
+2 -2
src/components/dialogs/EmailDialog/screens/Manage2FA/Disable.tsx
··· 185 state.emailStatus === 'pending' 186 ? Loader 187 : state.emailStatus === 'success' 188 - ? Check 189 - : Envelope 190 } 191 /> 192 </Button>
··· 185 state.emailStatus === 'pending' 186 ? Loader 187 : state.emailStatus === 'success' 188 + ? Check 189 + : Envelope 190 } 191 /> 192 </Button>
+2 -2
src/components/dialogs/EmailDialog/screens/Manage2FA/Enable.tsx
··· 116 state.status === 'pending' 117 ? Loader 118 : state.status === 'success' 119 - ? Check 120 - : ShieldIcon 121 } 122 /> 123 </Button>
··· 116 state.status === 'pending' 117 ? Loader 118 : state.status === 'success' 119 + ? Check 120 + : ShieldIcon 121 } 122 /> 123 </Button>
+2 -2
src/components/dialogs/SearchablePeopleList.tsx
··· 390 !enabled 391 ? {opacity: 0.5} 392 : pressed || focused || hovered 393 - ? t.atoms.bg_contrast_25 394 - : t.atoms.bg, 395 ]}> 396 <ProfileCard.Header> 397 <ProfileCard.Avatar
··· 390 !enabled 391 ? {opacity: 0.5} 392 : pressed || focused || hovered 393 + ? t.atoms.bg_contrast_25 394 + : t.atoms.bg, 395 ]}> 396 <ProfileCard.Header> 397 <ProfileCard.Avatar
+2 -2
src/components/dms/EmojiReactionPicker.tsx
··· 98 : t.palette.primary_500, 99 } 100 : alreadyReacted 101 - ? {backgroundColor: t.palette.primary_200} 102 - : bgColor, 103 {height: 40, width: 40}, 104 a.justify_center, 105 a.align_center,
··· 98 : t.palette.primary_500, 99 } 100 : alreadyReacted 101 + ? {backgroundColor: t.palette.primary_200} 102 + : bgColor, 103 {height: 40, width: 40}, 104 a.justify_center, 105 a.align_center,
+1 -2
src/components/dms/MessageContextMenu.tsx
··· 128 label={_(msg`Message options`)} 129 contentLabel={_( 130 msg`Message from @${ 131 - sender?.handle ?? // should always be defined 132 - 'unknown' 133 }: ${message.text}`, 134 )}> 135 {children}
··· 128 label={_(msg`Message options`)} 129 contentLabel={_( 130 msg`Message from @${ 131 + sender?.handle ?? 'unknown' // should always be defined 132 }: ${message.text}`, 133 )}> 134 {children}
+24
src/components/hooks/useOnGesture/index.ts
···
··· 1 + import {useEffect} from 'react' 2 + 3 + import { 4 + type GlobalGestureEvents, 5 + useGlobalGestureEvents, 6 + } from '#/state/global-gesture-events' 7 + 8 + /** 9 + * Listen for global gesture events. Callback should be wrapped with 10 + * `useCallback` or otherwise memoized to avoid unnecessary re-renders. 11 + */ 12 + export function useOnGesture( 13 + onGestureCallback: (e: GlobalGestureEvents['begin']) => void, 14 + ) { 15 + const ctx = useGlobalGestureEvents() 16 + useEffect(() => { 17 + ctx.register() 18 + ctx.events.on('begin', onGestureCallback) 19 + return () => { 20 + ctx.unregister() 21 + ctx.events.off('begin', onGestureCallback) 22 + } 23 + }, [ctx, onGestureCallback]) 24 + }
+1
src/components/hooks/useOnGesture/index.web.ts
···
··· 1 + export function useOnGesture() {}
+23 -24
src/components/icons/VerifiedCheck.tsx
··· 3 4 import {type Props, useCommonSVGProps} from '#/components/icons/common' 5 6 - export const VerifiedCheck = React.forwardRef<Svg, Props>(function LogoImpl( 7 - props, 8 - ref, 9 - ) { 10 - const {fill, size, style, ...rest} = useCommonSVGProps(props) 11 12 - return ( 13 - <Svg 14 - fill="none" 15 - {...rest} 16 - ref={ref} 17 - viewBox="0 0 24 24" 18 - width={size} 19 - height={size} 20 - style={[style]}> 21 - <Circle cx="12" cy="12" r="11.5" fill={fill} /> 22 - <Path 23 - fill="#fff" 24 - fillRule="evenodd" 25 - clipRule="evenodd" 26 - d="M17.659 8.175a1.361 1.361 0 0 1 0 1.925l-6.224 6.223a1.361 1.361 0 0 1-1.925 0L6.4 13.212a1.361 1.361 0 0 1 1.925-1.925l2.149 2.148 5.26-5.26a1.361 1.361 0 0 1 1.925 0Z" 27 - /> 28 - </Svg> 29 - ) 30 - })
··· 3 4 import {type Props, useCommonSVGProps} from '#/components/icons/common' 5 6 + export const VerifiedCheck = React.forwardRef<Svg, Props>( 7 + function LogoImpl(props, ref) { 8 + const {fill, size, style, ...rest} = useCommonSVGProps(props) 9 10 + return ( 11 + <Svg 12 + fill="none" 13 + {...rest} 14 + ref={ref} 15 + viewBox="0 0 24 24" 16 + width={size} 17 + height={size} 18 + style={[style]}> 19 + <Circle cx="12" cy="12" r="11.5" fill={fill} /> 20 + <Path 21 + fill="#fff" 22 + fillRule="evenodd" 23 + clipRule="evenodd" 24 + d="M17.659 8.175a1.361 1.361 0 0 1 0 1.925l-6.224 6.223a1.361 1.361 0 0 1-1.925 0L6.4 13.212a1.361 1.361 0 0 1 1.925-1.925l2.149 2.148 5.26-5.26a1.361 1.361 0 0 1 1.925 0Z" 25 + /> 26 + </Svg> 27 + ) 28 + }, 29 + )
+28 -29
src/components/icons/VerifierCheck.tsx
··· 3 4 import {type Props, useCommonSVGProps} from '#/components/icons/common' 5 6 - export const VerifierCheck = React.forwardRef<Svg, Props>(function LogoImpl( 7 - props, 8 - ref, 9 - ) { 10 - const {fill, size, style, ...rest} = useCommonSVGProps(props) 11 12 - return ( 13 - <Svg 14 - fill="none" 15 - {...rest} 16 - ref={ref} 17 - viewBox="0 0 24 24" 18 - width={size} 19 - height={size} 20 - style={[style]}> 21 - <Path 22 - fill={fill} 23 - fillRule="evenodd" 24 - clipRule="evenodd" 25 - d="M8.792 1.615a4.154 4.154 0 0 1 6.416 0 4.154 4.154 0 0 0 3.146 1.515 4.154 4.154 0 0 1 4 5.017 4.154 4.154 0 0 0 .777 3.404 4.154 4.154 0 0 1-1.427 6.255 4.153 4.153 0 0 0-2.177 2.73 4.154 4.154 0 0 1-5.781 2.784 4.154 4.154 0 0 0-3.492 0 4.154 4.154 0 0 1-5.78-2.784 4.154 4.154 0 0 0-2.178-2.73A4.154 4.154 0 0 1 .87 11.551a4.154 4.154 0 0 0 .776-3.404A4.154 4.154 0 0 1 5.646 3.13a4.154 4.154 0 0 0 3.146-1.515Z" 26 - /> 27 - <Path 28 - fill="#fff" 29 - fillRule="evenodd" 30 - clipRule="evenodd" 31 - d="M17.861 8.26a1.438 1.438 0 0 1 0 2.033l-6.571 6.571a1.437 1.437 0 0 1-2.033 0L5.97 13.58a1.438 1.438 0 0 1 2.033-2.033l2.27 2.269 5.554-5.555a1.437 1.437 0 0 1 2.033 0Z" 32 - /> 33 - </Svg> 34 - ) 35 - })
··· 3 4 import {type Props, useCommonSVGProps} from '#/components/icons/common' 5 6 + export const VerifierCheck = React.forwardRef<Svg, Props>( 7 + function LogoImpl(props, ref) { 8 + const {fill, size, style, ...rest} = useCommonSVGProps(props) 9 10 + return ( 11 + <Svg 12 + fill="none" 13 + {...rest} 14 + ref={ref} 15 + viewBox="0 0 24 24" 16 + width={size} 17 + height={size} 18 + style={[style]}> 19 + <Path 20 + fill={fill} 21 + fillRule="evenodd" 22 + clipRule="evenodd" 23 + d="M8.792 1.615a4.154 4.154 0 0 1 6.416 0 4.154 4.154 0 0 0 3.146 1.515 4.154 4.154 0 0 1 4 5.017 4.154 4.154 0 0 0 .777 3.404 4.154 4.154 0 0 1-1.427 6.255 4.153 4.153 0 0 0-2.177 2.73 4.154 4.154 0 0 1-5.781 2.784 4.154 4.154 0 0 0-3.492 0 4.154 4.154 0 0 1-5.78-2.784 4.154 4.154 0 0 0-2.178-2.73A4.154 4.154 0 0 1 .87 11.551a4.154 4.154 0 0 0 .776-3.404A4.154 4.154 0 0 1 5.646 3.13a4.154 4.154 0 0 0 3.146-1.515Z" 24 + /> 25 + <Path 26 + fill="#fff" 27 + fillRule="evenodd" 28 + clipRule="evenodd" 29 + d="M17.861 8.26a1.438 1.438 0 0 1 0 2.033l-6.571 6.571a1.437 1.437 0 0 1-2.033 0L5.97 13.58a1.438 1.438 0 0 1 2.033-2.033l2.27 2.269 5.554-5.555a1.437 1.437 0 0 1 2.033 0Z" 30 + /> 31 + </Svg> 32 + ) 33 + }, 34 + )
+4 -4
src/components/moderation/ContentHider.tsx
··· 1 import React from 'react' 2 - import {StyleProp, View, ViewStyle} from 'react-native' 3 - import {ModerationUI} from '@atproto/api' 4 import {msg, Trans} from '@lingui/macro' 5 import {useLingui} from '@lingui/react' 6 ··· 148 modui.noOverride 149 ? _(msg`Learn more about the moderation applied to this content`) 150 : override 151 - ? _(msg`Hides the content`) 152 - : _(msg`Shows the content`) 153 }> 154 {state => ( 155 <View
··· 1 import React from 'react' 2 + import {type StyleProp, View, type ViewStyle} from 'react-native' 3 + import {type ModerationUI} from '@atproto/api' 4 import {msg, Trans} from '@lingui/macro' 5 import {useLingui} from '@lingui/react' 6 ··· 148 modui.noOverride 149 ? _(msg`Learn more about the moderation applied to this content`) 150 : override 151 + ? _(msg`Hides the content`) 152 + : _(msg`Shows the content`) 153 }> 154 {state => ( 155 <View
+6 -6
src/components/moderation/ReportDialog/index.tsx
··· 512 backgroundColor: active 513 ? t.palette.primary_500 514 : completed 515 - ? t.palette.primary_100 516 - : t.atoms.bg_contrast_25.backgroundColor, 517 borderColor: active 518 ? t.palette.primary_500 519 : completed 520 - ? t.palette.primary_400 521 - : t.atoms.border_contrast_low.borderColor, 522 }, 523 ]}> 524 {completed ? ( ··· 533 color: active 534 ? 'white' 535 : completed 536 - ? t.palette.primary_700 537 - : t.atoms.text_contrast_medium.color, 538 fontVariant: ['tabular-nums'], 539 width: 24, 540 height: 24,
··· 512 backgroundColor: active 513 ? t.palette.primary_500 514 : completed 515 + ? t.palette.primary_100 516 + : t.atoms.bg_contrast_25.backgroundColor, 517 borderColor: active 518 ? t.palette.primary_500 519 : completed 520 + ? t.palette.primary_400 521 + : t.atoms.border_contrast_low.borderColor, 522 }, 523 ]}> 524 {completed ? ( ··· 533 color: active 534 ? 'white' 535 : completed 536 + ? t.palette.primary_700 537 + : t.atoms.text_contrast_medium.color, 538 fontVariant: ['tabular-nums'], 539 width: 24, 540 height: 24,
+2 -2
src/components/verification/VerificationCheckButton.tsx
··· 130 verifiedByHidden 131 ? t.atoms.bg_contrast_100.backgroundColor 132 : state.profile.isVerified 133 - ? t.palette.primary_500 134 - : t.atoms.bg_contrast_100.backgroundColor 135 } 136 verifier={state.profile.role === 'verifier'} 137 />
··· 130 verifiedByHidden 131 ? t.atoms.bg_contrast_100.backgroundColor 132 : state.profile.isVerified 133 + ? t.palette.primary_500 134 + : t.atoms.bg_contrast_100.backgroundColor 135 } 136 verifier={state.profile.role === 'verifier'} 137 />
+7 -7
src/components/verification/VerificationsDialog.tsx
··· 64 ? _(msg`You are verified`) 65 : _(msg`Your verifications`) 66 : state.profile.isVerified 67 - ? _(msg`${userName} is verified`) 68 - : _( 69 - msg({ 70 - message: `${userName}'s verifications`, 71 - comment: `Possessive, meaning "the verifications of {userName}"`, 72 - }), 73 - ) 74 75 return ( 76 <Dialog.ScrollableInner
··· 64 ? _(msg`You are verified`) 65 : _(msg`Your verifications`) 66 : state.profile.isVerified 67 + ? _(msg`${userName} is verified`) 68 + : _( 69 + msg({ 70 + message: `${userName}'s verifications`, 71 + comment: `Possessive, meaning "the verifications of {userName}"`, 72 + }), 73 + ) 74 75 return ( 76 <Dialog.ScrollableInner
+9 -9
src/lib/moderation.ts
··· 1 import React from 'react' 2 import { 3 - AppBskyLabelerDefs, 4 BskyAgent, 5 - ComAtprotoLabelDefs, 6 - InterpretedLabelValueDefinition, 7 LABELS, 8 - ModerationCause, 9 - ModerationOpts, 10 - ModerationUI, 11 } from '@atproto/api' 12 13 import {sanitizeDisplayName} from '#/lib/strings/display-names' 14 import {sanitizeHandle} from '#/lib/strings/handles' 15 - import {AppModerationCause} from '#/components/Pills' 16 17 export const ADULT_CONTENT_LABELS = ['sexual', 'nudity', 'porn'] 18 export const OTHER_SELF_LABELS = ['graphic-media'] ··· 29 cause.source.type === 'labeler' 30 ? cause.source.did 31 : cause.source.type === 'list' 32 - ? cause.source.list.uri 33 - : 'user' 34 if (cause.type === 'label') { 35 return `label:${cause.label.val}:${source}` 36 }
··· 1 import React from 'react' 2 import { 3 + type AppBskyLabelerDefs, 4 BskyAgent, 5 + type ComAtprotoLabelDefs, 6 + type InterpretedLabelValueDefinition, 7 LABELS, 8 + type ModerationCause, 9 + type ModerationOpts, 10 + type ModerationUI, 11 } from '@atproto/api' 12 13 import {sanitizeDisplayName} from '#/lib/strings/display-names' 14 import {sanitizeHandle} from '#/lib/strings/handles' 15 + import {type AppModerationCause} from '#/components/Pills' 16 17 export const ADULT_CONTENT_LABELS = ['sexual', 'nudity', 'porn'] 18 export const OTHER_SELF_LABELS = ['graphic-media'] ··· 29 cause.source.type === 'labeler' 30 ? cause.source.did 31 : cause.source.type === 'list' 32 + ? cause.source.list.uri 33 + : 'user' 34 if (cause.type === 'label') { 35 return `label:${cause.label.val}:${source}` 36 }
+6 -6
src/lib/moderation/useModerationCauseDescription.ts
··· 1 import React from 'react' 2 import { 3 BSKY_LABELER_DID, 4 - ModerationCause, 5 - ModerationCauseSource, 6 } from '@atproto/api' 7 import {msg} from '@lingui/macro' 8 import {useLingui} from '@lingui/react' ··· 12 import {useSession} from '#/state/session' 13 import {CircleBanSign_Stroke2_Corner0_Rounded as CircleBanSign} from '#/components/icons/CircleBanSign' 14 import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' 15 - import {Props as SVGIconProps} from '#/components/icons/common' 16 import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash' 17 import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning' 18 - import {AppModerationCause} from '#/components/Pills' 19 import {useGlobalLabelStrings} from './useGlobalLabelStrings' 20 import {getDefinition, getLabelStrings} from './useLabelInfo' 21 ··· 153 def.identifier === '!no-unauthenticated' 154 ? EyeSlash 155 : def.severity === 'alert' 156 - ? Warning 157 - : CircleInfo, 158 name: strings.name, 159 description: strings.description, 160 source,
··· 1 import React from 'react' 2 import { 3 BSKY_LABELER_DID, 4 + type ModerationCause, 5 + type ModerationCauseSource, 6 } from '@atproto/api' 7 import {msg} from '@lingui/macro' 8 import {useLingui} from '@lingui/react' ··· 12 import {useSession} from '#/state/session' 13 import {CircleBanSign_Stroke2_Corner0_Rounded as CircleBanSign} from '#/components/icons/CircleBanSign' 14 import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' 15 + import {type Props as SVGIconProps} from '#/components/icons/common' 16 import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash' 17 import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning' 18 + import {type AppModerationCause} from '#/components/Pills' 19 import {useGlobalLabelStrings} from './useGlobalLabelStrings' 20 import {getDefinition, getLabelStrings} from './useLabelInfo' 21 ··· 153 def.identifier === '!no-unauthenticated' 154 ? EyeSlash 155 : def.severity === 'alert' 156 + ? Warning 157 + : CircleInfo, 158 name: strings.name, 159 description: strings.description, 160 source,
+55 -55
src/locale/locales/en/messages.po
··· 456 msgid "<0>{0}</0> is included in your starter pack" 457 msgstr "" 458 459 - #: src/components/WhoCanReply.tsx:296 460 msgid "<0>{0}</0> members" 461 msgstr "" 462 ··· 866 msgid "An error occurred while compressing the video." 867 msgstr "" 868 869 - #: src/state/queries/explore-feed-previews.tsx:184 870 msgid "An error occurred while fetching the feed." 871 msgstr "" 872 ··· 939 msgid "an unknown labeler" 940 msgstr "" 941 942 - #: src/components/WhoCanReply.tsx:317 943 msgid "and" 944 msgstr "" 945 ··· 1066 msgid "Are you sure you want to delete the app password \"{0}\"?" 1067 msgstr "" 1068 1069 - #: src/components/dms/MessageContextMenu.tsx:189 1070 msgid "Are you sure you want to delete this message? The message will be deleted for you, but not for the other participant." 1071 msgstr "" 1072 ··· 1128 msgid "At least 3 characters" 1129 msgstr "" 1130 1131 - #: src/screens/Settings/AppIconSettings/useAppIconSets.ts:40 1132 msgctxt "Name of app icon variant" 1133 msgid "Aurora" 1134 msgstr "" ··· 1302 msgid "Bluesky cannot confirm the authenticity of the claimed date." 1303 msgstr "" 1304 1305 - #: src/screens/Settings/AppIconSettings/useAppIconSets.ts:129 1306 msgctxt "Name of app icon variant" 1307 msgid "Bluesky Classic™" 1308 msgstr "" ··· 2096 msgid "Copy link to starter pack" 2097 msgstr "" 2098 2099 - #: src/components/dms/MessageContextMenu.tsx:150 2100 - #: src/components/dms/MessageContextMenu.tsx:153 2101 msgid "Copy message text" 2102 msgstr "" 2103 ··· 2145 msgid "Could not process your video" 2146 msgstr "" 2147 2148 - #: src/state/queries/notifications/settings.ts:50 2149 msgid "Could not update notification settings" 2150 msgstr "" 2151 ··· 2247 msgid "Dark" 2248 msgstr "" 2249 2250 - #: src/screens/Settings/AppIconSettings/useAppIconSets.ts:24 2251 msgctxt "Name of app icon variant" 2252 msgid "Dark" 2253 msgstr "" ··· 2286 msgid "Default icons" 2287 msgstr "" 2288 2289 - #: src/components/dms/MessageContextMenu.tsx:191 2290 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:701 2291 #: src/screens/Messages/components/ChatStatusInfo.tsx:55 2292 #: src/screens/Settings/AppPasswords.tsx:212 ··· 2334 msgid "Delete Conversation" 2335 msgstr "" 2336 2337 - #: src/components/dms/MessageContextMenu.tsx:164 2338 msgid "Delete for me" 2339 msgstr "" 2340 ··· 2342 msgid "Delete list" 2343 msgstr "" 2344 2345 - #: src/components/dms/MessageContextMenu.tsx:187 2346 msgid "Delete message" 2347 msgstr "" 2348 2349 - #: src/components/dms/MessageContextMenu.tsx:162 2350 msgid "Delete message for me" 2351 msgstr "" 2352 ··· 2433 msgid "Developer options" 2434 msgstr "" 2435 2436 - #: src/components/WhoCanReply.tsx:179 2437 msgid "Dialog: adjust who can interact with this post" 2438 msgstr "" 2439 ··· 2759 msgid "Edit User List" 2760 msgstr "" 2761 2762 - #: src/components/WhoCanReply.tsx:91 2763 msgid "Edit who can reply" 2764 msgstr "" 2765 ··· 2984 msgid "Everybody" 2985 msgstr "" 2986 2987 - #: src/components/WhoCanReply.tsx:71 2988 msgid "Everybody can reply" 2989 msgstr "" 2990 2991 - #: src/components/WhoCanReply.tsx:222 2992 msgid "Everybody can reply to this post." 2993 msgstr "" 2994 ··· 3409 msgid "Fitness" 3410 msgstr "" 3411 3412 - #: src/screens/Settings/AppIconSettings/useAppIconSets.ts:117 3413 msgctxt "Name of app icon variant" 3414 msgid "Flat Black" 3415 msgstr "" 3416 3417 - #: src/screens/Settings/AppIconSettings/useAppIconSets.ts:93 3418 msgctxt "Name of app icon variant" 3419 msgid "Flat Blue" 3420 msgstr "" 3421 3422 - #: src/screens/Settings/AppIconSettings/useAppIconSets.ts:105 3423 msgctxt "Name of app icon variant" 3424 msgid "Flat White" 3425 msgstr "" ··· 3920 msgid "Hides the content" 3921 msgstr "" 3922 3923 - #: src/view/com/posts/PostFeedErrorMessage.tsx:117 3924 msgid "Hmm, some kind of issue occurred when contacting the feed server. Please let the feed owner know about this issue." 3925 msgstr "" 3926 3927 - #: src/view/com/posts/PostFeedErrorMessage.tsx:105 3928 msgid "Hmm, the feed server appears to be misconfigured. Please let the feed owner know about this issue." 3929 msgstr "" 3930 3931 - #: src/view/com/posts/PostFeedErrorMessage.tsx:111 3932 msgid "Hmm, the feed server appears to be offline. Please let the feed owner know about this issue." 3933 msgstr "" 3934 3935 - #: src/view/com/posts/PostFeedErrorMessage.tsx:108 3936 msgid "Hmm, the feed server gave a bad response. Please let the feed owner know about this issue." 3937 msgstr "" 3938 3939 - #: src/view/com/posts/PostFeedErrorMessage.tsx:102 3940 msgid "Hmm, we're having trouble finding this feed. It may have been deleted." 3941 msgstr "" 3942 ··· 4680 msgid "Mention notifications" 4681 msgstr "" 4682 4683 - #: src/components/WhoCanReply.tsx:263 4684 msgid "mentioned users" 4685 msgstr "" 4686 ··· 4716 msgid "Message from @{0}: {1}" 4717 msgstr "" 4718 4719 - #: src/view/com/posts/PostFeedErrorMessage.tsx:201 4720 msgid "Message from server: {0}" 4721 msgstr "" 4722 ··· 4737 msgid "Messages" 4738 msgstr "" 4739 4740 - #: src/screens/Settings/AppIconSettings/useAppIconSets.ts:81 4741 msgctxt "Name of app icon variant" 4742 msgid "Midnight" 4743 msgstr "" ··· 5193 msgid "No one" 5194 msgstr "" 5195 5196 - #: src/components/WhoCanReply.tsx:246 5197 msgid "No one but the author can quote this post." 5198 msgstr "" 5199 ··· 5419 msgid "Only .jpg and .png files are supported" 5420 msgstr "" 5421 5422 - #: src/components/WhoCanReply.tsx:226 5423 msgid "Only {0} can reply." 5424 msgstr "" 5425 ··· 6057 msgid "Posts can be muted based on their text, their tags, or both. We recommend avoiding common words that appear in many posts, since it can result in no posts being shown." 6058 msgstr "" 6059 6060 - #: src/view/com/posts/PostFeedErrorMessage.tsx:68 6061 msgid "Posts hidden" 6062 msgstr "" 6063 ··· 6361 #: src/screens/Settings/Settings.tsx:556 6362 #: src/view/com/feeds/FeedSourceCard.tsx:322 6363 #: src/view/com/modals/UserAddRemoveLists.tsx:235 6364 - #: src/view/com/posts/PostFeedErrorMessage.tsx:213 6365 msgid "Remove" 6366 msgstr "" 6367 ··· 6398 6399 #: src/view/com/posts/FeedShutdownMsg.tsx:116 6400 #: src/view/com/posts/FeedShutdownMsg.tsx:120 6401 - #: src/view/com/posts/PostFeedErrorMessage.tsx:169 6402 msgid "Remove feed" 6403 msgstr "" 6404 6405 - #: src/view/com/posts/PostFeedErrorMessage.tsx:210 6406 msgid "Remove feed?" 6407 msgstr "" 6408 ··· 6455 msgid "Remove subtitle file" 6456 msgstr "" 6457 6458 - #: src/view/com/posts/PostFeedErrorMessage.tsx:211 6459 msgid "Remove this feed from your saved feeds" 6460 msgstr "" 6461 ··· 6517 msgid "Replies" 6518 msgstr "" 6519 6520 - #: src/components/WhoCanReply.tsx:73 6521 msgid "Replies disabled" 6522 msgstr "" 6523 6524 - #: src/components/WhoCanReply.tsx:224 6525 msgid "Replies to this post are disabled." 6526 msgstr "" 6527 ··· 6593 msgid "Reply was successfully hidden" 6594 msgstr "" 6595 6596 - #: src/components/dms/MessageContextMenu.tsx:172 6597 #: src/components/dms/MessagesListBlockedFooter.tsx:85 6598 #: src/components/dms/MessagesListBlockedFooter.tsx:92 6599 msgid "Report" ··· 6627 msgid "Report list" 6628 msgstr "" 6629 6630 - #: src/components/dms/MessageContextMenu.tsx:170 6631 msgid "Report message" 6632 msgstr "" 6633 ··· 7646 msgid "Some other feeds you might like" 7647 msgstr "" 7648 7649 - #: src/components/WhoCanReply.tsx:74 7650 msgid "Some people can reply" 7651 msgstr "" 7652 ··· 7871 msgid "Suggestive" 7872 msgstr "" 7873 7874 - #: src/screens/Settings/AppIconSettings/useAppIconSets.ts:60 7875 msgctxt "Name of app icon variant" 7876 msgid "Sunrise" 7877 msgstr "" 7878 7879 - #: src/screens/Settings/AppIconSettings/useAppIconSets.ts:70 7880 msgctxt "Name of app icon variant" 7881 msgid "Sunset" 7882 msgstr "" ··· 8165 msgstr "" 8166 8167 #: src/screens/Search/Explore.tsx:990 8168 - #: src/view/com/posts/PostFeed.tsx:657 8169 msgid "There was an issue fetching posts. Tap here to try again." 8170 msgstr "" 8171 ··· 8186 msgid "There was an issue fetching your service info" 8187 msgstr "" 8188 8189 - #: src/view/com/posts/PostFeedErrorMessage.tsx:145 8190 msgid "There was an issue removing this feed. Please check your internet connection and try again." 8191 msgstr "" 8192 ··· 8295 msgid "This content is not available because one of the users involved has blocked the other." 8296 msgstr "" 8297 8298 - #: src/view/com/posts/PostFeedErrorMessage.tsx:114 8299 msgid "This content is not viewable without a Bluesky account." 8300 msgstr "" 8301 ··· 8319 msgid "This feature is not available while using an App Password. Please sign in with your main password." 8320 msgstr "" 8321 8322 - #: src/view/com/posts/PostFeedErrorMessage.tsx:120 8323 msgid "This feed is currently receiving high traffic and is temporarily unavailable. Please try again later." 8324 msgstr "" 8325 ··· 8387 msgid "This post claims to have been created on <0>{0}</0>, but was first seen by Bluesky on <1>{1}</1>." 8388 msgstr "" 8389 8390 - #: src/components/WhoCanReply.tsx:217 8391 msgid "This post has an unknown type of threadgate on it. Your app may be out of date." 8392 msgstr "" 8393 ··· 8558 msgid "Topic" 8559 msgstr "" 8560 8561 - #: src/components/dms/MessageContextMenu.tsx:143 8562 - #: src/components/dms/MessageContextMenu.tsx:145 8563 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:444 8564 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:446 8565 #: src/screens/PostThread/components/ThreadItemAnchor.tsx:567 ··· 8984 msgid "Username or email address" 8985 msgstr "" 8986 8987 - #: src/components/WhoCanReply.tsx:280 8988 msgid "users followed by <0>@{0}</0>" 8989 msgstr "" 8990 8991 - #: src/components/WhoCanReply.tsx:267 8992 msgid "users following <0>@{0}</0>" 8993 msgstr "" 8994 ··· 9188 #: src/components/ProfileHoverCard/index.web.tsx:464 9189 #: src/components/ProfileHoverCard/index.web.tsx:484 9190 #: src/components/ProfileHoverCard/index.web.tsx:511 9191 - #: src/view/com/posts/PostFeedErrorMessage.tsx:175 9192 #: src/view/com/util/PostMeta.tsx:91 9193 #: src/view/com/util/PostMeta.tsx:126 9194 msgid "View profile" ··· 9416 msgid "Which languages would you like to see in your algorithmic feeds?" 9417 msgstr "" 9418 9419 - #: src/components/WhoCanReply.tsx:183 9420 msgid "Who can interact with this post?" 9421 msgstr "" 9422 9423 - #: src/components/WhoCanReply.tsx:91 9424 msgid "Who can reply" 9425 msgstr "" 9426
··· 456 msgid "<0>{0}</0> is included in your starter pack" 457 msgstr "" 458 459 + #: src/components/WhoCanReply.tsx:302 460 msgid "<0>{0}</0> members" 461 msgstr "" 462 ··· 866 msgid "An error occurred while compressing the video." 867 msgstr "" 868 869 + #: src/state/queries/explore-feed-previews.tsx:173 870 msgid "An error occurred while fetching the feed." 871 msgstr "" 872 ··· 939 msgid "an unknown labeler" 940 msgstr "" 941 942 + #: src/components/WhoCanReply.tsx:323 943 msgid "and" 944 msgstr "" 945 ··· 1066 msgid "Are you sure you want to delete the app password \"{0}\"?" 1067 msgstr "" 1068 1069 + #: src/components/dms/MessageContextMenu.tsx:188 1070 msgid "Are you sure you want to delete this message? The message will be deleted for you, but not for the other participant." 1071 msgstr "" 1072 ··· 1128 msgid "At least 3 characters" 1129 msgstr "" 1130 1131 + #: src/screens/Settings/AppIconSettings/useAppIconSets.ts:48 1132 msgctxt "Name of app icon variant" 1133 msgid "Aurora" 1134 msgstr "" ··· 1302 msgid "Bluesky cannot confirm the authenticity of the claimed date." 1303 msgstr "" 1304 1305 + #: src/screens/Settings/AppIconSettings/useAppIconSets.ts:165 1306 msgctxt "Name of app icon variant" 1307 msgid "Bluesky Classic™" 1308 msgstr "" ··· 2096 msgid "Copy link to starter pack" 2097 msgstr "" 2098 2099 + #: src/components/dms/MessageContextMenu.tsx:149 2100 + #: src/components/dms/MessageContextMenu.tsx:152 2101 msgid "Copy message text" 2102 msgstr "" 2103 ··· 2145 msgid "Could not process your video" 2146 msgstr "" 2147 2148 + #: src/state/queries/notifications/settings.ts:49 2149 msgid "Could not update notification settings" 2150 msgstr "" 2151 ··· 2247 msgid "Dark" 2248 msgstr "" 2249 2250 + #: src/screens/Settings/AppIconSettings/useAppIconSets.ts:28 2251 msgctxt "Name of app icon variant" 2252 msgid "Dark" 2253 msgstr "" ··· 2286 msgid "Default icons" 2287 msgstr "" 2288 2289 + #: src/components/dms/MessageContextMenu.tsx:190 2290 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:701 2291 #: src/screens/Messages/components/ChatStatusInfo.tsx:55 2292 #: src/screens/Settings/AppPasswords.tsx:212 ··· 2334 msgid "Delete Conversation" 2335 msgstr "" 2336 2337 + #: src/components/dms/MessageContextMenu.tsx:163 2338 msgid "Delete for me" 2339 msgstr "" 2340 ··· 2342 msgid "Delete list" 2343 msgstr "" 2344 2345 + #: src/components/dms/MessageContextMenu.tsx:186 2346 msgid "Delete message" 2347 msgstr "" 2348 2349 + #: src/components/dms/MessageContextMenu.tsx:161 2350 msgid "Delete message for me" 2351 msgstr "" 2352 ··· 2433 msgid "Developer options" 2434 msgstr "" 2435 2436 + #: src/components/WhoCanReply.tsx:185 2437 msgid "Dialog: adjust who can interact with this post" 2438 msgstr "" 2439 ··· 2759 msgid "Edit User List" 2760 msgstr "" 2761 2762 + #: src/components/WhoCanReply.tsx:97 2763 msgid "Edit who can reply" 2764 msgstr "" 2765 ··· 2984 msgid "Everybody" 2985 msgstr "" 2986 2987 + #: src/components/WhoCanReply.tsx:77 2988 msgid "Everybody can reply" 2989 msgstr "" 2990 2991 + #: src/components/WhoCanReply.tsx:228 2992 msgid "Everybody can reply to this post." 2993 msgstr "" 2994 ··· 3409 msgid "Fitness" 3410 msgstr "" 3411 3412 + #: src/screens/Settings/AppIconSettings/useAppIconSets.ts:149 3413 msgctxt "Name of app icon variant" 3414 msgid "Flat Black" 3415 msgstr "" 3416 3417 + #: src/screens/Settings/AppIconSettings/useAppIconSets.ts:117 3418 msgctxt "Name of app icon variant" 3419 msgid "Flat Blue" 3420 msgstr "" 3421 3422 + #: src/screens/Settings/AppIconSettings/useAppIconSets.ts:133 3423 msgctxt "Name of app icon variant" 3424 msgid "Flat White" 3425 msgstr "" ··· 3920 msgid "Hides the content" 3921 msgstr "" 3922 3923 + #: src/view/com/posts/PostFeedErrorMessage.tsx:121 3924 msgid "Hmm, some kind of issue occurred when contacting the feed server. Please let the feed owner know about this issue." 3925 msgstr "" 3926 3927 + #: src/view/com/posts/PostFeedErrorMessage.tsx:109 3928 msgid "Hmm, the feed server appears to be misconfigured. Please let the feed owner know about this issue." 3929 msgstr "" 3930 3931 + #: src/view/com/posts/PostFeedErrorMessage.tsx:115 3932 msgid "Hmm, the feed server appears to be offline. Please let the feed owner know about this issue." 3933 msgstr "" 3934 3935 + #: src/view/com/posts/PostFeedErrorMessage.tsx:112 3936 msgid "Hmm, the feed server gave a bad response. Please let the feed owner know about this issue." 3937 msgstr "" 3938 3939 + #: src/view/com/posts/PostFeedErrorMessage.tsx:106 3940 msgid "Hmm, we're having trouble finding this feed. It may have been deleted." 3941 msgstr "" 3942 ··· 4680 msgid "Mention notifications" 4681 msgstr "" 4682 4683 + #: src/components/WhoCanReply.tsx:269 4684 msgid "mentioned users" 4685 msgstr "" 4686 ··· 4716 msgid "Message from @{0}: {1}" 4717 msgstr "" 4718 4719 + #: src/view/com/posts/PostFeedErrorMessage.tsx:205 4720 msgid "Message from server: {0}" 4721 msgstr "" 4722 ··· 4737 msgid "Messages" 4738 msgstr "" 4739 4740 + #: src/screens/Settings/AppIconSettings/useAppIconSets.ts:101 4741 msgctxt "Name of app icon variant" 4742 msgid "Midnight" 4743 msgstr "" ··· 5193 msgid "No one" 5194 msgstr "" 5195 5196 + #: src/components/WhoCanReply.tsx:252 5197 msgid "No one but the author can quote this post." 5198 msgstr "" 5199 ··· 5419 msgid "Only .jpg and .png files are supported" 5420 msgstr "" 5421 5422 + #: src/components/WhoCanReply.tsx:232 5423 msgid "Only {0} can reply." 5424 msgstr "" 5425 ··· 6057 msgid "Posts can be muted based on their text, their tags, or both. We recommend avoiding common words that appear in many posts, since it can result in no posts being shown." 6058 msgstr "" 6059 6060 + #: src/view/com/posts/PostFeedErrorMessage.tsx:72 6061 msgid "Posts hidden" 6062 msgstr "" 6063 ··· 6361 #: src/screens/Settings/Settings.tsx:556 6362 #: src/view/com/feeds/FeedSourceCard.tsx:322 6363 #: src/view/com/modals/UserAddRemoveLists.tsx:235 6364 + #: src/view/com/posts/PostFeedErrorMessage.tsx:217 6365 msgid "Remove" 6366 msgstr "" 6367 ··· 6398 6399 #: src/view/com/posts/FeedShutdownMsg.tsx:116 6400 #: src/view/com/posts/FeedShutdownMsg.tsx:120 6401 + #: src/view/com/posts/PostFeedErrorMessage.tsx:173 6402 msgid "Remove feed" 6403 msgstr "" 6404 6405 + #: src/view/com/posts/PostFeedErrorMessage.tsx:214 6406 msgid "Remove feed?" 6407 msgstr "" 6408 ··· 6455 msgid "Remove subtitle file" 6456 msgstr "" 6457 6458 + #: src/view/com/posts/PostFeedErrorMessage.tsx:215 6459 msgid "Remove this feed from your saved feeds" 6460 msgstr "" 6461 ··· 6517 msgid "Replies" 6518 msgstr "" 6519 6520 + #: src/components/WhoCanReply.tsx:79 6521 msgid "Replies disabled" 6522 msgstr "" 6523 6524 + #: src/components/WhoCanReply.tsx:230 6525 msgid "Replies to this post are disabled." 6526 msgstr "" 6527 ··· 6593 msgid "Reply was successfully hidden" 6594 msgstr "" 6595 6596 + #: src/components/dms/MessageContextMenu.tsx:171 6597 #: src/components/dms/MessagesListBlockedFooter.tsx:85 6598 #: src/components/dms/MessagesListBlockedFooter.tsx:92 6599 msgid "Report" ··· 6627 msgid "Report list" 6628 msgstr "" 6629 6630 + #: src/components/dms/MessageContextMenu.tsx:169 6631 msgid "Report message" 6632 msgstr "" 6633 ··· 7646 msgid "Some other feeds you might like" 7647 msgstr "" 7648 7649 + #: src/components/WhoCanReply.tsx:80 7650 msgid "Some people can reply" 7651 msgstr "" 7652 ··· 7871 msgid "Suggestive" 7872 msgstr "" 7873 7874 + #: src/screens/Settings/AppIconSettings/useAppIconSets.ts:72 7875 msgctxt "Name of app icon variant" 7876 msgid "Sunrise" 7877 msgstr "" 7878 7879 + #: src/screens/Settings/AppIconSettings/useAppIconSets.ts:86 7880 msgctxt "Name of app icon variant" 7881 msgid "Sunset" 7882 msgstr "" ··· 8165 msgstr "" 8166 8167 #: src/screens/Search/Explore.tsx:990 8168 + #: src/view/com/posts/PostFeed.tsx:656 8169 msgid "There was an issue fetching posts. Tap here to try again." 8170 msgstr "" 8171 ··· 8186 msgid "There was an issue fetching your service info" 8187 msgstr "" 8188 8189 + #: src/view/com/posts/PostFeedErrorMessage.tsx:149 8190 msgid "There was an issue removing this feed. Please check your internet connection and try again." 8191 msgstr "" 8192 ··· 8295 msgid "This content is not available because one of the users involved has blocked the other." 8296 msgstr "" 8297 8298 + #: src/view/com/posts/PostFeedErrorMessage.tsx:118 8299 msgid "This content is not viewable without a Bluesky account." 8300 msgstr "" 8301 ··· 8319 msgid "This feature is not available while using an App Password. Please sign in with your main password." 8320 msgstr "" 8321 8322 + #: src/view/com/posts/PostFeedErrorMessage.tsx:124 8323 msgid "This feed is currently receiving high traffic and is temporarily unavailable. Please try again later." 8324 msgstr "" 8325 ··· 8387 msgid "This post claims to have been created on <0>{0}</0>, but was first seen by Bluesky on <1>{1}</1>." 8388 msgstr "" 8389 8390 + #: src/components/WhoCanReply.tsx:223 8391 msgid "This post has an unknown type of threadgate on it. Your app may be out of date." 8392 msgstr "" 8393 ··· 8558 msgid "Topic" 8559 msgstr "" 8560 8561 + #: src/components/dms/MessageContextMenu.tsx:142 8562 + #: src/components/dms/MessageContextMenu.tsx:144 8563 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:444 8564 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:446 8565 #: src/screens/PostThread/components/ThreadItemAnchor.tsx:567 ··· 8984 msgid "Username or email address" 8985 msgstr "" 8986 8987 + #: src/components/WhoCanReply.tsx:286 8988 msgid "users followed by <0>@{0}</0>" 8989 msgstr "" 8990 8991 + #: src/components/WhoCanReply.tsx:273 8992 msgid "users following <0>@{0}</0>" 8993 msgstr "" 8994 ··· 9188 #: src/components/ProfileHoverCard/index.web.tsx:464 9189 #: src/components/ProfileHoverCard/index.web.tsx:484 9190 #: src/components/ProfileHoverCard/index.web.tsx:511 9191 + #: src/view/com/posts/PostFeedErrorMessage.tsx:179 9192 #: src/view/com/util/PostMeta.tsx:91 9193 #: src/view/com/util/PostMeta.tsx:126 9194 msgid "View profile" ··· 9416 msgid "Which languages would you like to see in your algorithmic feeds?" 9417 msgstr "" 9418 9419 + #: src/components/WhoCanReply.tsx:189 9420 msgid "Who can interact with this post?" 9421 msgstr "" 9422 9423 + #: src/components/WhoCanReply.tsx:97 9424 msgid "Who can reply" 9425 msgstr "" 9426
+6 -6
src/platform/polyfills.ts
··· 41 r1 === 64 42 ? String.fromCharCode((bitmap >> 16) & 255) 43 : r2 === 64 44 - ? String.fromCharCode((bitmap >> 16) & 255, (bitmap >> 8) & 255) 45 - : String.fromCharCode( 46 - (bitmap >> 16) & 255, 47 - (bitmap >> 8) & 255, 48 - bitmap & 255, 49 - ) 50 } 51 return result 52 }
··· 41 r1 === 64 42 ? String.fromCharCode((bitmap >> 16) & 255) 43 : r2 === 64 44 + ? String.fromCharCode((bitmap >> 16) & 255, (bitmap >> 8) & 255) 45 + : String.fromCharCode( 46 + (bitmap >> 16) & 255, 47 + (bitmap >> 8) & 255, 48 + bitmap & 255, 49 + ) 50 } 51 return result 52 }
+2 -2
src/screens/Login/index.tsx
··· 49 requestedAccount 50 ? Forms.Login 51 : accounts.length 52 - ? Forms.ChooseAccount 53 - : Forms.Login, 54 ) 55 56 const {
··· 49 requestedAccount 50 ? Forms.Login 51 : accounts.length 52 + ? Forms.ChooseAccount 53 + : Forms.Login, 54 ) 55 56 const {
+1 -1
src/screens/Messages/ChatList.tsx
··· 155 profiles: inboxPreviewConvos.slice(0, 3), 156 }, 157 ...conversations.map( 158 - convo => ({type: 'CONVERSATION', conversation: convo} as const), 159 ), 160 ] satisfies ListItem[] 161 }
··· 155 profiles: inboxPreviewConvos.slice(0, 3), 156 }, 157 ...conversations.map( 158 + convo => ({type: 'CONVERSATION', conversation: convo}) as const, 159 ), 160 ] satisfies ListItem[] 161 }
+2 -2
src/screens/Onboarding/StepFinished.tsx
··· 176 avatarResult: profileStepResults.isCreatedAvatar 177 ? 'created' 178 : profileStepResults.image 179 - ? 'uploaded' 180 - : 'default', 181 }) 182 })(), 183 requestNotificationsPermission('AfterOnboarding'),
··· 176 avatarResult: profileStepResults.isCreatedAvatar 177 ? 'created' 178 : profileStepResults.image 179 + ? 'uploaded' 180 + : 'default', 181 }) 182 })(), 183 requestNotificationsPermission('AfterOnboarding'),
+2 -2
src/screens/Profile/Header/ProfileHeaderLabeler.tsx
··· 214 ? t.palette.contrast_50 215 : t.palette.contrast_25 216 : state.hovered || state.pressed 217 - ? tokens.color.temp_purple_dark 218 - : tokens.color.temp_purple, 219 }, 220 ]}> 221 <Text
··· 214 ? t.palette.contrast_50 215 : t.palette.contrast_25 216 : state.hovered || state.pressed 217 + ? tokens.color.temp_purple_dark 218 + : tokens.color.temp_purple, 219 }, 220 ]}> 221 <Text
+2 -2
src/screens/Search/Explore.tsx
··· 342 ]) 343 344 const topBorder = useMemo( 345 - () => ({type: 'topBorder', key: 'top-border'} as const), 346 [], 347 ) 348 const trendingTopicsModule = useMemo( 349 - () => ({type: 'trendingTopics', key: 'trending-topics'} as const), 350 [], 351 ) 352 const suggestedFollowsModule = useMemo(() => {
··· 342 ]) 343 344 const topBorder = useMemo( 345 + () => ({type: 'topBorder', key: 'top-border'}) as const, 346 [], 347 ) 348 const trendingTopicsModule = useMemo( 349 + () => ({type: 'trendingTopics', key: 'trending-topics'}) as const, 350 [], 351 ) 352 const suggestedFollowsModule = useMemo(() => {
+12 -4
src/screens/Settings/AppIconSettings/useAppIconSets.ts
··· 13 id: 'default_light', 14 name: _(msg({context: 'Name of app icon variant', message: 'Light'})), 15 iosImage: () => { 16 - return require(`../../../../assets/app-icons/ios_icon_default_light.png`) 17 }, 18 androidImage: () => { 19 - return require(`../../../../assets/app-icons/android_icon_default_light.png`) 20 }, 21 }, 22 { 23 id: 'default_dark', 24 name: _(msg({context: 'Name of app icon variant', message: 'Dark'})), 25 iosImage: () => { 26 - return require(`../../../../assets/app-icons/ios_icon_default_dark.png`) 27 }, 28 androidImage: () => { 29 - return require(`../../../../assets/app-icons/android_icon_default_dark.png`) 30 }, 31 }, 32 ] satisfies AppIconSet[]
··· 13 id: 'default_light', 14 name: _(msg({context: 'Name of app icon variant', message: 'Light'})), 15 iosImage: () => { 16 + return require( 17 + `../../../../assets/app-icons/ios_icon_default_light.png`, 18 + ) 19 }, 20 androidImage: () => { 21 + return require( 22 + `../../../../assets/app-icons/android_icon_default_light.png`, 23 + ) 24 }, 25 }, 26 { 27 id: 'default_dark', 28 name: _(msg({context: 'Name of app icon variant', message: 'Dark'})), 29 iosImage: () => { 30 + return require( 31 + `../../../../assets/app-icons/ios_icon_default_dark.png`, 32 + ) 33 }, 34 androidImage: () => { 35 + return require( 36 + `../../../../assets/app-icons/android_icon_default_dark.png`, 37 + ) 38 }, 39 }, 40 ] satisfies AppIconSet[]
+2 -2
src/screens/Settings/components/ChangeHandleDialog.tsx
··· 525 isVerified 526 ? _(msg`Update to ${domain}`) 527 : dnsPanel 528 - ? _(msg`Verify DNS Record`) 529 - : _(msg`Verify Text File`) 530 } 531 variant="solid" 532 size="large"
··· 525 isVerified 526 ? _(msg`Update to ${domain}`) 527 : dnsPanel 528 + ? _(msg`Verify DNS Record`) 529 + : _(msg`Verify Text File`) 530 } 531 variant="solid" 532 size="large"
+2 -2
src/state/cache/profile-shadow.ts
··· 146 'status' in shadow 147 ? shadow.status 148 : 'status' in profile 149 - ? profile.status 150 - : undefined, 151 }) 152 } 153
··· 146 'status' in shadow 147 ? shadow.status 148 : 'status' in profile 149 + ? profile.status 150 + : undefined, 151 }) 152 } 153
+83
src/state/global-gesture-events/index.tsx
···
··· 1 + import {createContext, useContext, useMemo, useRef, useState} from 'react' 2 + import {View} from 'react-native' 3 + import { 4 + Gesture, 5 + GestureDetector, 6 + type GestureStateChangeEvent, 7 + type GestureUpdateEvent, 8 + type PanGestureHandlerEventPayload, 9 + } from 'react-native-gesture-handler' 10 + import EventEmitter from 'eventemitter3' 11 + 12 + export type GlobalGestureEvents = { 13 + begin: GestureStateChangeEvent<PanGestureHandlerEventPayload> 14 + update: GestureUpdateEvent<PanGestureHandlerEventPayload> 15 + end: GestureStateChangeEvent<PanGestureHandlerEventPayload> 16 + finalize: GestureStateChangeEvent<PanGestureHandlerEventPayload> 17 + } 18 + 19 + const Context = createContext<{ 20 + events: EventEmitter<GlobalGestureEvents> 21 + register: () => void 22 + unregister: () => void 23 + }>({ 24 + events: new EventEmitter<GlobalGestureEvents>(), 25 + register: () => {}, 26 + unregister: () => {}, 27 + }) 28 + 29 + export function GlobalGestureEventsProvider({ 30 + children, 31 + }: { 32 + children: React.ReactNode 33 + }) { 34 + const refCount = useRef(0) 35 + const events = useMemo(() => new EventEmitter<GlobalGestureEvents>(), []) 36 + const [enabled, setEnabled] = useState(false) 37 + const ctx = useMemo( 38 + () => ({ 39 + events, 40 + register() { 41 + refCount.current += 1 42 + if (refCount.current === 1) { 43 + setEnabled(true) 44 + } 45 + }, 46 + unregister() { 47 + refCount.current -= 1 48 + if (refCount.current === 0) { 49 + setEnabled(false) 50 + } 51 + }, 52 + }), 53 + [events, setEnabled], 54 + ) 55 + const gesture = Gesture.Pan() 56 + .runOnJS(true) 57 + .enabled(enabled) 58 + .simultaneousWithExternalGesture() 59 + .onBegin(e => { 60 + events.emit('begin', e) 61 + }) 62 + .onUpdate(e => { 63 + events.emit('update', e) 64 + }) 65 + .onEnd(e => { 66 + events.emit('end', e) 67 + }) 68 + .onFinalize(e => { 69 + events.emit('finalize', e) 70 + }) 71 + 72 + return ( 73 + <Context.Provider value={ctx}> 74 + <GestureDetector gesture={gesture}> 75 + <View collapsable={false}>{children}</View> 76 + </GestureDetector> 77 + </Context.Provider> 78 + ) 79 + } 80 + 81 + export function useGlobalGestureEvents() { 82 + return useContext(Context) 83 + }
+9
src/state/global-gesture-events/index.web.tsx
···
··· 1 + export function GlobalGestureEventsProvider(_props: { 2 + children: React.ReactNode 3 + }) { 4 + throw new Error('GlobalGestureEventsProvider is not supported on web.') 5 + } 6 + 7 + export function useGlobalGestureEvents() { 8 + throw new Error('useGlobalGestureEvents is not supported on web.') 9 + }
+11 -22
src/state/queries/explore-feed-previews.tsx
··· 36 const LIMIT = 8 // sliced to 6, overfetch to account for moderation 37 const PINNED_POST_URIS: Record<string, boolean> = { 38 // 📰 News 39 - 'at://did:plc:kkf4naxqmweop7dv4l2iqqf5/app.bsky.feed.post/3lgh27w2ngc2b': 40 - true, 41 // Gardening 42 - 'at://did:plc:5rw2on4i56btlcajojaxwcat/app.bsky.feed.post/3kjorckgcwc27': 43 - true, 44 // Web Development Trending 45 - 'at://did:plc:m2sjv3wncvsasdapla35hzwj/app.bsky.feed.post/3lfaw445axs22': 46 - true, 47 // Anime & Manga EN 48 - 'at://did:plc:tazrmeme4dzahimsykusrwrk/app.bsky.feed.post/3knxx2gmkns2y': 49 - true, 50 // 📽️ Film 51 - 'at://did:plc:2hwwem55ce6djnk6bn62cstr/app.bsky.feed.post/3llhpzhbq7c2g': 52 - true, 53 // PopSky 54 - 'at://did:plc:lfdf4srj43iwdng7jn35tjsp/app.bsky.feed.post/3lbblgly65c2g': 55 - true, 56 // Science 57 - 'at://did:plc:hu2obebw3nhfj667522dahfg/app.bsky.feed.post/3kl33otd6ob2s': 58 - true, 59 // Birds! 🦉 60 - 'at://did:plc:ffkgesg3jsv2j7aagkzrtcvt/app.bsky.feed.post/3lbg4r57yk22d': 61 - true, 62 // Astronomy 63 - 'at://did:plc:xy2zorw2ys47poflotxthlzg/app.bsky.feed.post/3kyzye4lujs2w': 64 - true, 65 // What's Cooking 🍽️ 66 - 'at://did:plc:geoqe3qls5mwezckxxsewys2/app.bsky.feed.post/3lfqhgvxbqc2q': 67 - true, 68 // BookSky 💙📚 #booksky 69 - 'at://did:plc:geoqe3qls5mwezckxxsewys2/app.bsky.feed.post/3kgrm2rw5ww2e': 70 - true, 71 } 72 73 export type FeedPreviewItem =
··· 36 const LIMIT = 8 // sliced to 6, overfetch to account for moderation 37 const PINNED_POST_URIS: Record<string, boolean> = { 38 // 📰 News 39 + 'at://did:plc:kkf4naxqmweop7dv4l2iqqf5/app.bsky.feed.post/3lgh27w2ngc2b': true, 40 // Gardening 41 + 'at://did:plc:5rw2on4i56btlcajojaxwcat/app.bsky.feed.post/3kjorckgcwc27': true, 42 // Web Development Trending 43 + 'at://did:plc:m2sjv3wncvsasdapla35hzwj/app.bsky.feed.post/3lfaw445axs22': true, 44 // Anime & Manga EN 45 + 'at://did:plc:tazrmeme4dzahimsykusrwrk/app.bsky.feed.post/3knxx2gmkns2y': true, 46 // 📽️ Film 47 + 'at://did:plc:2hwwem55ce6djnk6bn62cstr/app.bsky.feed.post/3llhpzhbq7c2g': true, 48 // PopSky 49 + 'at://did:plc:lfdf4srj43iwdng7jn35tjsp/app.bsky.feed.post/3lbblgly65c2g': true, 50 // Science 51 + 'at://did:plc:hu2obebw3nhfj667522dahfg/app.bsky.feed.post/3kl33otd6ob2s': true, 52 // Birds! 🦉 53 + 'at://did:plc:ffkgesg3jsv2j7aagkzrtcvt/app.bsky.feed.post/3lbg4r57yk22d': true, 54 // Astronomy 55 + 'at://did:plc:xy2zorw2ys47poflotxthlzg/app.bsky.feed.post/3kyzye4lujs2w': true, 56 // What's Cooking 🍽️ 57 + 'at://did:plc:geoqe3qls5mwezckxxsewys2/app.bsky.feed.post/3lfqhgvxbqc2q': true, 58 // BookSky 💙📚 #booksky 59 + 'at://did:plc:geoqe3qls5mwezckxxsewys2/app.bsky.feed.post/3kgrm2rw5ww2e': true, 60 } 61 62 export type FeedPreviewItem =
+2 -3
src/state/queries/notifications/settings.ts
··· 36 mutationFn: async ( 37 update: Partial<AppBskyNotificationDefs.Preferences>, 38 ) => { 39 - const response = await agent.app.bsky.notification.putPreferencesV2( 40 - update, 41 - ) 42 return response.data.preferences 43 }, 44 onMutate: update => {
··· 36 mutationFn: async ( 37 update: Partial<AppBskyNotificationDefs.Preferences>, 38 ) => { 39 + const response = 40 + await agent.app.bsky.notification.putPreferencesV2(update) 41 return response.data.preferences 42 }, 43 onMutate: update => {
+5 -5
src/state/queries/notifications/unread.tsx
··· 15 import {useModerationOpts} from '../../preferences/moderation-opts' 16 import {truncateAndInvalidate} from '../util' 17 import {RQKEY as RQKEY_NOTIFS} from './feed' 18 - import {CachedFeedPage, FeedPage} from './types' 19 import {fetchPage} from './util' 20 21 const UPDATE_INTERVAL = 30 * 1e3 // 30sec ··· 96 data.event === '30+' 97 ? 30 98 : data.event === '' 99 - ? 0 100 - : parseInt(data.event, 10) || 1, 101 } 102 setNumUnread(data.event) 103 } ··· 167 unreadCount >= 30 168 ? '30+' 169 : unreadCount === 0 170 - ? '' 171 - : String(unreadCount) 172 173 // track last sync 174 const now = new Date()
··· 15 import {useModerationOpts} from '../../preferences/moderation-opts' 16 import {truncateAndInvalidate} from '../util' 17 import {RQKEY as RQKEY_NOTIFS} from './feed' 18 + import {type CachedFeedPage, type FeedPage} from './types' 19 import {fetchPage} from './util' 20 21 const UPDATE_INTERVAL = 30 * 1e3 // 30sec ··· 96 data.event === '30+' 97 ? 30 98 : data.event === '' 99 + ? 0 100 + : parseInt(data.event, 10) || 1, 101 } 102 setNumUnread(data.event) 103 } ··· 167 unreadCount >= 30 168 ? '30+' 169 : unreadCount === 0 170 + ? '' 171 + : String(unreadCount) 172 173 // track last sync 174 const now = new Date()
+2 -2
src/state/queries/usePostThread/index.ts
··· 52 return view === 'linear' 53 ? LINEAR_VIEW_BELOW 54 : isWeb && gtPhone 55 - ? TREE_VIEW_BELOW_DESKTOP 56 - : TREE_VIEW_BELOW 57 }, [view, gtPhone]) 58 59 const postThreadQueryKey = createPostThreadQueryKey({
··· 52 return view === 'linear' 53 ? LINEAR_VIEW_BELOW 54 : isWeb && gtPhone 55 + ? TREE_VIEW_BELOW_DESKTOP 56 + : TREE_VIEW_BELOW 57 }, [view, gtPhone]) 58 59 const postThreadQueryKey = createPostThreadQueryKey({
+1 -1
src/state/queries/usePostThread/traversal.ts
··· 444 const anchorPost = items.at(0) 445 const hasAnchorFromCache = anchorPost && anchorPost.type === 'threadPost' 446 const skeletonReplies = hasAnchorFromCache 447 - ? anchorPost.value.post.replyCount ?? 4 448 : 4 449 450 if (!items.length) {
··· 444 const anchorPost = items.at(0) 445 const hasAnchorFromCache = anchorPost && anchorPost.type === 'threadPost' 446 const skeletonReplies = hasAnchorFromCache 447 + ? (anchorPost.value.post.replyCount ?? 4) 448 : 4 449 450 if (!items.length) {
+5 -13
src/storage/index.ts
··· 1 import {useCallback, useEffect, useState} from 'react' 2 import {MMKV} from 'react-native-mmkv' 3 4 - import {Account, Device} from '#/storage/schema' 5 6 export * from '#/storage/schema' 7 ··· 83 } 84 } 85 86 - type StorageSchema<T extends Storage<any, any>> = T extends Storage< 87 - any, 88 - infer U 89 - > 90 - ? U 91 - : never 92 - type StorageScopes<T extends Storage<any, any>> = T extends Storage< 93 - infer S, 94 - any 95 - > 96 - ? S 97 - : never 98 99 /** 100 * Hook to use a storage instance. Acts like a useState hook, but persists the
··· 1 import {useCallback, useEffect, useState} from 'react' 2 import {MMKV} from 'react-native-mmkv' 3 4 + import {type Account, type Device} from '#/storage/schema' 5 6 export * from '#/storage/schema' 7 ··· 83 } 84 } 85 86 + type StorageSchema<T extends Storage<any, any>> = 87 + T extends Storage<any, infer U> ? U : never 88 + type StorageScopes<T extends Storage<any, any>> = 89 + T extends Storage<infer S, any> ? S : never 90 91 /** 92 * Hook to use a storage instance. Acts like a useState hook, but persists the
+37 -1
src/style.css
··· 328 329 /* #/components/Select/index.web.tsx */ 330 .radix-select-content { 331 - box-shadow: 0px 6px 24px -10px rgba(22, 23, 24, 0.25), 332 0px 6px 12px -12px rgba(22, 23, 24, 0.15); 333 min-width: var(--radix-select-trigger-width); 334 max-height: var(--radix-select-content-available-height); 335 }
··· 328 329 /* #/components/Select/index.web.tsx */ 330 .radix-select-content { 331 + box-shadow: 332 + 0px 6px 24px -10px rgba(22, 23, 24, 0.25), 333 0px 6px 12px -12px rgba(22, 23, 24, 0.15); 334 min-width: var(--radix-select-trigger-width); 335 max-height: var(--radix-select-content-available-height); 336 } 337 + 338 + /* 339 + * #/components/Tooltip/index.web.tsx 340 + */ 341 + .radix-popover-content { 342 + animation-duration: 300ms; 343 + animation-timing-function: cubic-bezier(0.17, 0.73, 0.14, 1); 344 + will-change: transform, opacity; 345 + } 346 + .radix-popover-content[data-state='open'][data-side='top'] { 347 + animation-name: radixPopoverSlideUpAndFade; 348 + } 349 + .radix-popover-content[data-state='open'][data-side='bottom'] { 350 + animation-name: radixPopoverSlideDownAndFade; 351 + } 352 + @keyframes radixPopoverSlideUpAndFade { 353 + from { 354 + opacity: 0; 355 + transform: translateY(2px); 356 + } 357 + to { 358 + opacity: 1; 359 + transform: translateY(0); 360 + } 361 + } 362 + @keyframes radixPopoverSlideDownAndFade { 363 + from { 364 + opacity: 0; 365 + transform: translateY(-2px); 366 + } 367 + to { 368 + opacity: 1; 369 + transform: translateY(0); 370 + } 371 + }
+16 -16
src/view/com/composer/Composer.tsx
··· 518 thread.posts.length > 1 519 ? _(msg`Your posts have been published`) 520 : replyTo 521 - ? _(msg`Your reply has been published`) 522 - : _(msg`Your post has been published`), 523 ) 524 }, [ 525 _, ··· 1000 }), 1001 ) 1002 : isThread 1003 - ? _( 1004 - msg({ 1005 - message: 'Publish posts', 1006 - comment: 1007 - 'Accessibility label for button to publish multiple posts in a thread', 1008 - }), 1009 - ) 1010 - : _( 1011 - msg({ 1012 - message: 'Publish post', 1013 - comment: 1014 - 'Accessibility label for button to publish a single post', 1015 - }), 1016 - ) 1017 } 1018 variant="solid" 1019 color="primary"
··· 518 thread.posts.length > 1 519 ? _(msg`Your posts have been published`) 520 : replyTo 521 + ? _(msg`Your reply has been published`) 522 + : _(msg`Your post has been published`), 523 ) 524 }, [ 525 _, ··· 1000 }), 1001 ) 1002 : isThread 1003 + ? _( 1004 + msg({ 1005 + message: 'Publish posts', 1006 + comment: 1007 + 'Accessibility label for button to publish multiple posts in a thread', 1008 + }), 1009 + ) 1010 + : _( 1011 + msg({ 1012 + message: 'Publish post', 1013 + comment: 1014 + 'Accessibility label for button to publish a single post', 1015 + }), 1016 + ) 1017 } 1018 variant="solid" 1019 color="primary"
+10 -10
src/view/com/composer/photos/Gallery.tsx
··· 1 import React from 'react' 2 import { 3 - ImageStyle, 4 Keyboard, 5 - LayoutChangeEvent, 6 StyleSheet, 7 TouchableOpacity, 8 View, 9 - ViewStyle, 10 } from 'react-native' 11 import {Image} from 'expo-image' 12 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' ··· 14 import {useLingui} from '@lingui/react' 15 16 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 17 - import {Dimensions} from '#/lib/media/types' 18 import {colors, s} from '#/lib/styles' 19 import {isNative} from '#/platform/detection' 20 - import {ComposerImage, cropImage} from '#/state/gallery' 21 import {Text} from '#/view/com/util/text/Text' 22 import {useTheme} from '#/alf' 23 import * as Dialog from '#/components/Dialog' 24 - import {PostAction} from '../state/composer' 25 import {EditImageDialog} from './EditImageDialog' 26 import {ImageAltTextDialog} from './ImageAltTextDialog' 27 ··· 74 altTextControlStyle: isOverflow 75 ? {left: 4, bottom: 4} 76 : !isMobile && images.length < 3 77 - ? {left: 8, top: 8} 78 - : {left: 4, top: 4}, 79 imageControlsStyle: { 80 display: 'flex' as const, 81 flexDirection: 'row' as const, ··· 83 ...(isOverflow 84 ? {top: 4, right: 4, gap: 4} 85 : !isMobile && images.length < 3 86 - ? {top: 8, right: 8, gap: 8} 87 - : {top: 4, right: 4, gap: 4}), 88 zIndex: 1, 89 }, 90 imageStyle: {
··· 1 import React from 'react' 2 import { 3 + type ImageStyle, 4 Keyboard, 5 + type LayoutChangeEvent, 6 StyleSheet, 7 TouchableOpacity, 8 View, 9 + type ViewStyle, 10 } from 'react-native' 11 import {Image} from 'expo-image' 12 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' ··· 14 import {useLingui} from '@lingui/react' 15 16 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 17 + import {type Dimensions} from '#/lib/media/types' 18 import {colors, s} from '#/lib/styles' 19 import {isNative} from '#/platform/detection' 20 + import {type ComposerImage, cropImage} from '#/state/gallery' 21 import {Text} from '#/view/com/util/text/Text' 22 import {useTheme} from '#/alf' 23 import * as Dialog from '#/components/Dialog' 24 + import {type PostAction} from '../state/composer' 25 import {EditImageDialog} from './EditImageDialog' 26 import {ImageAltTextDialog} from './ImageAltTextDialog' 27 ··· 74 altTextControlStyle: isOverflow 75 ? {left: 4, bottom: 4} 76 : !isMobile && images.length < 3 77 + ? {left: 8, top: 8} 78 + : {left: 4, top: 4}, 79 imageControlsStyle: { 80 display: 'flex' as const, 81 flexDirection: 'row' as const, ··· 83 ...(isOverflow 84 ? {top: 4, right: 4, gap: 4} 85 : !isMobile && images.length < 3 86 + ? {top: 8, right: 8, gap: 8} 87 + : {top: 4, right: 4, gap: 4}), 88 zIndex: 1, 89 }, 90 imageStyle: {
+21 -16
src/view/com/composer/state/composer.ts
··· 1 - import {ImagePickerAsset} from 'expo-image-picker' 2 import { 3 - AppBskyFeedPostgate, 4 AppBskyRichtextFacet, 5 - BskyPreferences, 6 RichText, 7 } from '@atproto/api' 8 import {nanoid} from 'nanoid/non-secure' 9 10 - import {SelfLabel} from '#/lib/moderation' 11 import {insertMentionAt} from '#/lib/strings/mention-manip' 12 import {shortenLinks} from '#/lib/strings/rich-text-manip' 13 import { ··· 15 postUriToRelativePath, 16 toBskyAppUrl, 17 } from '#/lib/strings/url-helpers' 18 - import {ComposerImage, createInitialImages} from '#/state/gallery' 19 import {createPostgateRecord} from '#/state/queries/postgate/util' 20 - import {Gif} from '#/state/queries/tenor' 21 import {threadgateRecordToAllowUISetting} from '#/state/queries/threadgate' 22 - import {ThreadgateAllowUISetting} from '#/state/queries/threadgate' 23 - import {ComposerOpts} from '#/state/shell/composer' 24 import { 25 - LinkFacetMatch, 26 suggestLinkCardUri, 27 } from '#/view/com/composer/text-input/text-input-util' 28 - import {createVideoState, VideoAction, videoReducer, VideoState} from './video' 29 30 type ImagesMedia = { 31 type: 'images' ··· 514 text: initText 515 ? initText 516 : initMention 517 - ? insertMentionAt( 518 - `@${initMention}`, 519 - initMention.length + 1, 520 - `${initMention}`, 521 - ) 522 - : '', 523 }) 524 525 let link: Link | undefined
··· 1 + import {type ImagePickerAsset} from 'expo-image-picker' 2 import { 3 + type AppBskyFeedPostgate, 4 AppBskyRichtextFacet, 5 + type BskyPreferences, 6 RichText, 7 } from '@atproto/api' 8 import {nanoid} from 'nanoid/non-secure' 9 10 + import {type SelfLabel} from '#/lib/moderation' 11 import {insertMentionAt} from '#/lib/strings/mention-manip' 12 import {shortenLinks} from '#/lib/strings/rich-text-manip' 13 import { ··· 15 postUriToRelativePath, 16 toBskyAppUrl, 17 } from '#/lib/strings/url-helpers' 18 + import {type ComposerImage, createInitialImages} from '#/state/gallery' 19 import {createPostgateRecord} from '#/state/queries/postgate/util' 20 + import {type Gif} from '#/state/queries/tenor' 21 import {threadgateRecordToAllowUISetting} from '#/state/queries/threadgate' 22 + import {type ThreadgateAllowUISetting} from '#/state/queries/threadgate' 23 + import {type ComposerOpts} from '#/state/shell/composer' 24 import { 25 + type LinkFacetMatch, 26 suggestLinkCardUri, 27 } from '#/view/com/composer/text-input/text-input-util' 28 + import { 29 + createVideoState, 30 + type VideoAction, 31 + videoReducer, 32 + type VideoState, 33 + } from './video' 34 35 type ImagesMedia = { 36 type: 'images' ··· 519 text: initText 520 ? initText 521 : initMention 522 + ? insertMentionAt( 523 + `@${initMention}`, 524 + initMention.length + 1, 525 + `${initMention}`, 526 + ) 527 + : '', 528 }) 529 530 let link: Link | undefined
+2 -2
src/view/com/composer/text-input/web/Autocomplete.tsx
··· 209 itemIndex === 0 210 ? styles.firstMention 211 : itemIndex === totalItems - 1 212 - ? styles.lastMention 213 - : undefined, 214 ]} 215 onPress={onPress} 216 accessibilityRole="button">
··· 209 itemIndex === 0 210 ? styles.firstMention 211 : itemIndex === totalItems - 1 212 + ? styles.lastMention 213 + : undefined, 214 ]} 215 onPress={onPress} 216 accessibilityRole="button">
+6 -6
src/view/com/lightbox/Lightbox.web.tsx
··· 1 import React, {useCallback, useEffect, useState} from 'react' 2 import { 3 Image, 4 - ImageStyle, 5 Pressable, 6 StyleSheet, 7 TouchableOpacity, 8 TouchableWithoutFeedback, 9 View, 10 - ViewStyle, 11 } from 'react-native' 12 import { 13 FontAwesomeIcon, 14 - FontAwesomeIconStyle, 15 } from '@fortawesome/react-native-fontawesome' 16 import {msg} from '@lingui/macro' 17 import {useLingui} from '@lingui/react' ··· 21 import {colors, s} from '#/lib/styles' 22 import {useLightbox, useLightboxControls} from '#/state/lightbox' 23 import {Text} from '../util/text/Text' 24 - import {ImageSource} from './ImageViewing/@types' 25 import ImageDefaultHeader from './ImageViewing/components/ImageDefaultHeader' 26 27 export function Lightbox() { ··· 121 img.type === 'circle-avi' 122 ? '50%' 123 : img.type === 'rect-avi' 124 - ? '10%' 125 - : 0, 126 } as ImageStyle 127 } 128 alt={img.alt}
··· 1 import React, {useCallback, useEffect, useState} from 'react' 2 import { 3 Image, 4 + type ImageStyle, 5 Pressable, 6 StyleSheet, 7 TouchableOpacity, 8 TouchableWithoutFeedback, 9 View, 10 + type ViewStyle, 11 } from 'react-native' 12 import { 13 FontAwesomeIcon, 14 + type FontAwesomeIconStyle, 15 } from '@fortawesome/react-native-fontawesome' 16 import {msg} from '@lingui/macro' 17 import {useLingui} from '@lingui/react' ··· 21 import {colors, s} from '#/lib/styles' 22 import {useLightbox, useLightboxControls} from '#/state/lightbox' 23 import {Text} from '../util/text/Text' 24 + import {type ImageSource} from './ImageViewing/@types' 25 import ImageDefaultHeader from './ImageViewing/components/ImageDefaultHeader' 26 27 export function Lightbox() { ··· 121 img.type === 'circle-avi' 122 ? '50%' 123 : img.type === 'rect-avi' 124 + ? '10%' 125 + : 0, 126 } as ImageStyle 127 } 128 alt={img.alt}
+2 -2
src/view/com/post-thread/PostThreadItem.tsx
··· 630 showChildReplyLine && !isThreadedChild 631 ? 0 632 : isThreadedChildAdjacentBot 633 - ? 4 634 - : 8, 635 }, 636 ]}> 637 {/* If we are in threaded mode, the avatar is rendered in PostMeta */}
··· 630 showChildReplyLine && !isThreadedChild 631 ? 0 632 : isThreadedChildAdjacentBot 633 + ? 4 634 + : 8, 635 }, 636 ]}> 637 {/* If we are in threaded mode, the avatar is rendered in PostMeta */}
+8 -4
src/view/com/posts/PostFeedErrorMessage.tsx
··· 1 import React from 'react' 2 import {View} from 'react-native' 3 - import {AppBskyActorDefs, AppBskyFeedGetAuthorFeed, AtUri} from '@atproto/api' 4 import {msg as msgLingui, Trans} from '@lingui/macro' 5 import {useLingui} from '@lingui/react' 6 import {useNavigation} from '@react-navigation/native' 7 8 import {usePalette} from '#/lib/hooks/usePalette' 9 - import {NavigationProp} from '#/lib/routes/types' 10 import {cleanError} from '#/lib/strings/errors' 11 import {logger} from '#/logger' 12 - import {FeedDescriptor} from '#/state/queries/post-feed' 13 import {useRemoveFeedMutation} from '#/state/queries/preferences' 14 import * as Prompt from '#/components/Prompt' 15 import {EmptyState} from '../util/EmptyState' ··· 119 [KnownError.FeedTooManyRequests]: _l( 120 msgLingui`This feed is currently receiving high traffic and is temporarily unavailable. Please try again later.`, 121 ), 122 - }[knownError]), 123 [_l, knownError], 124 ) 125 const [_, uri] = feedDesc.split('|')
··· 1 import React from 'react' 2 import {View} from 'react-native' 3 + import { 4 + type AppBskyActorDefs, 5 + AppBskyFeedGetAuthorFeed, 6 + AtUri, 7 + } from '@atproto/api' 8 import {msg as msgLingui, Trans} from '@lingui/macro' 9 import {useLingui} from '@lingui/react' 10 import {useNavigation} from '@react-navigation/native' 11 12 import {usePalette} from '#/lib/hooks/usePalette' 13 + import {type NavigationProp} from '#/lib/routes/types' 14 import {cleanError} from '#/lib/strings/errors' 15 import {logger} from '#/logger' 16 + import {type FeedDescriptor} from '#/state/queries/post-feed' 17 import {useRemoveFeedMutation} from '#/state/queries/preferences' 18 import * as Prompt from '#/components/Prompt' 19 import {EmptyState} from '../util/EmptyState' ··· 123 [KnownError.FeedTooManyRequests]: _l( 124 msgLingui`This feed is currently receiving high traffic and is temporarily unavailable. Please try again later.`, 125 ), 126 + })[knownError], 127 [_l, knownError], 128 ) 129 const [_, uri] = feedDesc.split('|')
+6 -6
src/view/com/profile/ProfileMenu.tsx
··· 495 msg`The account will be able to interact with you after unblocking.`, 496 ) 497 : profile.associated?.labeler 498 - ? _( 499 - msg`Blocking will not prevent labels from being applied on your account, but it will stop this account from replying in your threads or interacting with you.`, 500 - ) 501 - : _( 502 - msg`Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`, 503 - ) 504 } 505 onConfirm={blockAccount} 506 confirmButtonCta={
··· 495 msg`The account will be able to interact with you after unblocking.`, 496 ) 497 : profile.associated?.labeler 498 + ? _( 499 + msg`Blocking will not prevent labels from being applied on your account, but it will stop this account from replying in your threads or interacting with you.`, 500 + ) 501 + : _( 502 + msg`Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`, 503 + ) 504 } 505 onConfirm={blockAccount} 506 confirmButtonCta={
+8 -3
src/view/com/util/List.web.tsx
··· 1 import React, {isValidElement, memo, startTransition, useRef} from 'react' 2 - import {FlatListProps, StyleSheet, View, ViewProps} from 'react-native' 3 - import {ReanimatedScrollEvent} from 'react-native-reanimated/lib/typescript/hook/commonTypes' 4 5 import {batchedUpdates} from '#/lib/batchedUpdates' 6 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' ··· 205 behavior: animated ? 'smooth' : 'instant', 206 }) 207 }, 208 - } as any), // TODO: Better types. 209 [getScrollableNode], 210 ) 211
··· 1 import React, {isValidElement, memo, startTransition, useRef} from 'react' 2 + import { 3 + type FlatListProps, 4 + StyleSheet, 5 + View, 6 + type ViewProps, 7 + } from 'react-native' 8 + import {type ReanimatedScrollEvent} from 'react-native-reanimated/lib/typescript/hook/commonTypes' 9 10 import {batchedUpdates} from '#/lib/batchedUpdates' 11 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' ··· 210 behavior: animated ? 'smooth' : 'instant', 211 }) 212 }, 213 + }) as any, // TODO: Better types. 214 [getScrollableNode], 215 ) 216
+8 -8
src/view/screens/DebugMod.tsx
··· 113 }), 114 ] 115 : scenario[0] === 'label' && target[0] === 'profile' 116 - ? [ 117 - mock.label({ 118 - src: isSelfLabel ? did : undefined, 119 - val: label[0], 120 - uri: `at://${did}/app.bsky.actor.profile/self`, 121 - }), 122 - ] 123 - : undefined, 124 viewer: mock.actorViewerState({ 125 following: isFollowing 126 ? `at://${currentAccount?.did || ''}/app.bsky.graph.follow/1234`
··· 113 }), 114 ] 115 : scenario[0] === 'label' && target[0] === 'profile' 116 + ? [ 117 + mock.label({ 118 + src: isSelfLabel ? did : undefined, 119 + val: label[0], 120 + uri: `at://${did}/app.bsky.actor.profile/self`, 121 + }), 122 + ] 123 + : undefined, 124 viewer: mock.actorViewerState({ 125 following: isFollowing 126 ? `at://${currentAccount?.did || ''}/app.bsky.graph.follow/1234`
+61 -59
svgo.config.mjs
··· 1 const preset = [ 2 - "removeDoctype", 3 - "removeXMLProcInst", 4 - "removeComments", 5 - "removeMetadata", 6 - "removeEditorsNSData", 7 - "cleanupAttrs", 8 - "mergeStyles", 9 - "inlineStyles", 10 - "minifyStyles", 11 - "cleanupIds", 12 - "removeUselessDefs", 13 - "cleanupNumericValues", 14 - "convertColors", 15 - "removeUnknownsAndDefaults", 16 - "removeNonInheritableGroupAttrs", 17 - "removeUselessStrokeAndFill", 18 - "removeDimensions", 19 - "cleanupEnableBackground", 20 - "removeHiddenElems", 21 - "removeEmptyText", 22 - "convertShapeToPath", 23 - "convertEllipseToCircle", 24 - "moveElemsAttrsToGroup", 25 - "moveGroupAttrsToElems", 26 - "collapseGroups", 27 - "convertPathData", 28 - "convertTransform", 29 - "removeEmptyAttrs", 30 - "removeEmptyContainers", 31 - "removeUnusedNS", 32 - "mergePaths", 33 - "sortAttrs", 34 - "sortDefsChildren", 35 - "removeTitle", 36 - "removeDesc", 37 ] 38 39 export default { 40 - plugins: [...preset.map(name => ({ 41 - name, 42 - params: { 43 - floatPrecision: 3, 44 - transformPrecision: 5, 45 - // minimise diff in ouput from svgomg 46 - // maybe remove in future? will produce smaller output 47 - convertToZ: false, 48 - removeUseless: false, 49 - } 50 - })), 51 - { 52 - name: 'addTrailingWhitespace', 53 - fn() { 54 - return { 55 - root: { 56 - exit (root) { 57 - root.children.push({ type: 'text', value: '\n' }) 58 - return root 59 - } 60 } 61 - } 62 - } 63 - }] 64 - };
··· 1 const preset = [ 2 + 'removeDoctype', 3 + 'removeXMLProcInst', 4 + 'removeComments', 5 + 'removeMetadata', 6 + 'removeEditorsNSData', 7 + 'cleanupAttrs', 8 + 'mergeStyles', 9 + 'inlineStyles', 10 + 'minifyStyles', 11 + 'cleanupIds', 12 + 'removeUselessDefs', 13 + 'cleanupNumericValues', 14 + 'convertColors', 15 + 'removeUnknownsAndDefaults', 16 + 'removeNonInheritableGroupAttrs', 17 + 'removeUselessStrokeAndFill', 18 + 'removeDimensions', 19 + 'cleanupEnableBackground', 20 + 'removeHiddenElems', 21 + 'removeEmptyText', 22 + 'convertShapeToPath', 23 + 'convertEllipseToCircle', 24 + 'moveElemsAttrsToGroup', 25 + 'moveGroupAttrsToElems', 26 + 'collapseGroups', 27 + 'convertPathData', 28 + 'convertTransform', 29 + 'removeEmptyAttrs', 30 + 'removeEmptyContainers', 31 + 'removeUnusedNS', 32 + 'mergePaths', 33 + 'sortAttrs', 34 + 'sortDefsChildren', 35 + 'removeTitle', 36 + 'removeDesc', 37 ] 38 39 export default { 40 + plugins: [ 41 + ...preset.map(name => ({ 42 + name, 43 + params: { 44 + floatPrecision: 3, 45 + transformPrecision: 5, 46 + // minimise diff in ouput from svgomg 47 + // maybe remove in future? will produce smaller output 48 + convertToZ: false, 49 + removeUseless: false, 50 + }, 51 + })), 52 + { 53 + name: 'addTrailingWhitespace', 54 + fn() { 55 + return { 56 + root: { 57 + exit(root) { 58 + root.children.push({type: 'text', value: '\n'}) 59 + return root 60 + }, 61 + }, 62 } 63 + }, 64 + }, 65 + ], 66 + }
+4 -4
yarn.lock
··· 16220 resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" 16221 integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== 16222 16223 - prettier@^2.8.3: 16224 - version "2.8.8" 16225 - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" 16226 - integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== 16227 16228 pretty-bytes@^5.6.0: 16229 version "5.6.0"
··· 16220 resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" 16221 integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== 16222 16223 + prettier@^3.6.0: 16224 + version "3.6.0" 16225 + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.6.0.tgz#18ec98d62cb0757a5d4eab40253ff3e6d0fc8dea" 16226 + integrity sha512-ujSB9uXHJKzM/2GBuE0hBOUgC77CN3Bnpqa+g80bkv3T3A93wL/xlzDATHhnhkzifz/UE2SNOvmbTz5hSkDlHw== 16227 16228 pretty-bytes@^5.6.0: 16229 version "5.6.0"