diff options
48 files changed, 667 insertions, 581 deletions
diff --git a/.github/workflows/build-submit-android.yml b/.github/workflows/build-submit-android.yml new file mode 100644 index 000000000..051e95151 --- /dev/null +++ b/.github/workflows/build-submit-android.yml @@ -0,0 +1,61 @@ +--- +name: Build and Submit Android + +on: + workflow_dispatch: + inputs: + profile: + type: choice + description: Build profile to use + options: + - production + +jobs: + build: + name: Build and Submit Android + runs-on: ubuntu-latest + steps: + - name: Check for EXPO_TOKEN + run: > + if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then + echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions" + exit 1 + fi + + - name: ā¬ļø Checkout + uses: actions/checkout@v4 + + - name: š§ Setup Node + uses: actions/setup-node@v3 + with: + node-version: 18.x + cache: yarn + + - name: šØ Setup EAS + uses: expo/expo-github-action@v8 + with: + expo-version: latest + eas-version: latest + token: ${{ secrets.EXPO_TOKEN }} + + - name: āļø Setup EAS local builds + run: yarn global add eas-cli-local-build-plugin + + - uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: āļø Install dependencies + run: yarn install + + - name: āļø Write environment variables + run: | + echo "${{ secrets.ENV_TOKEN }}" > .env + echo "${{ secrets.GOOGLE_SERVICES_TOKEN }}" > google-services.json + + - name: šļø EAS Build + run: yarn use-build-number eas build -p android --profile production --local --output build.aab --non-interactive + + - name: š Deploy + run: eas submit -p android --non-interactive --path build.aab diff --git a/.github/workflows/build-submit-ios.yml b/.github/workflows/build-submit-ios.yml new file mode 100644 index 000000000..0fd691bb9 --- /dev/null +++ b/.github/workflows/build-submit-ios.yml @@ -0,0 +1,72 @@ +--- +name: Build and Submit iOS + +on: + schedule: + - cron: '0 5 * * *' + workflow_dispatch: + inputs: + profile: + type: choice + description: Build profile to use + options: + - production + +jobs: + build: + name: Build and Submit iOS + runs-on: macos-14 + steps: + - name: Check for EXPO_TOKEN + run: > + if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then + echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions" + exit 1 + fi + + - name: ā¬ļø Checkout + uses: actions/checkout@v4 + + - name: š§ Setup Node + uses: actions/setup-node@v3 + with: + node-version: 18.x + cache: yarn + + - name: šØ Setup EAS + uses: expo/expo-github-action@v8 + with: + expo-version: latest + eas-version: latest + token: ${{ secrets.EXPO_TOKEN }} + + - name: āļø Setup EAS local builds + run: yarn global add eas-cli-local-build-plugin + + - name: āļø Install dependencies + run: yarn install + + - name: āļø Setup Cocoapods + uses: maxim-lobanov/setup-cocoapods@v1 + with: + version: 1.14.3 + + - name: š¾ Cache Pods + uses: actions/cache@v3 + id: pods-cache + with: + path: ./ios/Pods + # We'll use the yarn.lock for our hash since we don't yet have a Podfile.lock. Pod versions will not + # change unless the yarn version changes as well. + key: ${{ runner.os }}-pods-${{ hashFiles('yarn.lock') }} + + - name: āļø Write environment variables + run: | + echo "${{ secrets.ENV_TOKEN }}" > .env + echo "${{ secrets.GOOGLE_SERVICES_TOKEN }}" > google-services.json + + - name: šļø EAS Build + run: yarn use-build-number eas build -p ios --profile production --local --output build.ipa --non-interactive + + - name: š Deploy + run: eas submit -p ios --non-interactive --path build.ipa diff --git a/.github/workflows/deploy-nightly-testflight.yml b/.github/workflows/deploy-nightly-testflight.yml deleted file mode 100644 index e3875899e..000000000 --- a/.github/workflows/deploy-nightly-testflight.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: Deploy Nightly Testflight Release - -on: - schedule: - - cron: '0 5 * * *' - -jobs: - build: - name: Deploy Nightly Testflight Release - runs-on: ubuntu-latest - permissions: - contents: write - - steps: - - name: Check for EXPO_TOKEN - run: | - if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then - echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions" - exit 1 - fi - - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Node - uses: actions/setup-node@v3 - with: - node-version: 18.x - cache: yarn - - - name: Setup EAS - uses: expo/expo-github-action@v8 - with: - eas-version: latest - token: ${{ secrets.EXPO_TOKEN }} - - - name: Install dependencies - run: yarn install - - - name: Bump build number - run: yarn bump:ios - - - name: EAS build and submit - run: eas build -p ios --profile production --auto-submit --non-interactive - - - name: Commit - uses: stefanzweifel/git-auto-commit-action@v5 - with: - commit_message: Nightly iOS Build Bump - branch: main - commit_user_name: github-actions[bot] - commit_user_email: github-actions[bot]@users.noreply.github.com diff --git a/app.config.js b/app.config.js index 9814065f4..e710420b0 100644 --- a/app.config.js +++ b/app.config.js @@ -11,36 +11,23 @@ const DARK_SPLASH_CONFIG = { resizeMode: 'cover', } -module.exports = function () { +module.exports = function (config) { /** * App version number. Should be incremented as part of a release cycle. */ const VERSION = pkg.version /** - * iOS build number. Must be incremented for each TestFlight version. - * WARNING: Always leave this variable on line 24! If it is moved, you need to update ./scripts/bumpIosBuildNumber.sh - */ - const IOS_BUILD_NUMBER = '7' - - /** - * Android build number. Must be incremented for each release. - * WARNING: Always leave this variable on line 30! If it is moved, you need to update ./scripts/bumpAndroidBuildNumber.sh - */ - const ANDROID_VERSION_CODE = 62 - - /** * Uses built-in Expo env vars * * @see https://docs.expo.dev/build-reference/variables/#built-in-environment-variables */ const PLATFORM = process.env.EAS_BUILD_PLATFORM - /** - * Additional granularity for the `dist` field - */ const DIST_BUILD_NUMBER = - PLATFORM === 'android' ? ANDROID_VERSION_CODE : IOS_BUILD_NUMBER + PLATFORM === 'android' + ? process.env.BSKY_ANDROID_VERSION_CODE + : process.env.BSKY_IOS_BUILD_NUMBER return { expo: { @@ -57,7 +44,6 @@ module.exports = function () { userInterfaceStyle: 'automatic', splash: SPLASH_CONFIG, ios: { - buildNumber: IOS_BUILD_NUMBER, supportsTablet: false, bundleIdentifier: 'xyz.blueskyweb.app', config: { @@ -85,7 +71,6 @@ module.exports = function () { backgroundColor: '#ffffff', }, android: { - versionCode: ANDROID_VERSION_CODE, icon: './assets/icon.png', adaptiveIcon: { foregroundImage: './assets/icon-android-foreground.png', diff --git a/bskyweb/templates/base.html b/bskyweb/templates/base.html index 99b6f1dc6..e29e4032b 100644 --- a/bskyweb/templates/base.html +++ b/bskyweb/templates/base.html @@ -2,7 +2,6 @@ <html> <head> <meta charset="UTF-8"> - <meta name="theme-color"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, viewport-fit=cover"> <meta name="referrer" content="origin-when-cross-origin"> <!-- @@ -212,7 +211,7 @@ <link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png"> <link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png"> <link rel="mask-icon" href="/static/safari-pinned-tab.svg" color="#1185fe"> - <meta name="theme-color" content="#ffffff"> + <meta name="theme-color"> <meta name="application-name" content="Bluesky"> <meta name="generator" content="bskyweb"> <meta property="og:site_name" content="Bluesky Social" /> diff --git a/eas.json b/eas.json index 75254d293..2b4c7cb61 100644 --- a/eas.json +++ b/eas.json @@ -1,7 +1,8 @@ { "cli": { "version": ">= 3.8.1", - "promptToConfigurePushNotifications": false + "promptToConfigurePushNotifications": false, + "appVersionSource": "remote" }, "build": { "base": { @@ -28,7 +29,21 @@ "production": { "extends": "base", "ios": { - "resourceClass": "large" + "resourceClass": "large", + "autoIncrement": true + }, + "android": { + "autoIncrement": true + }, + "channel": "production" + }, + "github": { + "extends": "base", + "ios": { + "autoIncrement": true + }, + "android": { + "autoIncrement": true }, "channel": "production" } diff --git a/modules/react-native-ui-text-view/ios/RNUITextViewManager.m b/modules/react-native-ui-text-view/ios/RNUITextViewManager.m index 9a6f0285c..32dfb3b28 100644 --- a/modules/react-native-ui-text-view/ios/RNUITextViewManager.m +++ b/modules/react-native-ui-text-view/ios/RNUITextViewManager.m @@ -4,6 +4,7 @@ RCT_REMAP_SHADOW_PROPERTY(numberOfLines, numberOfLines, NSInteger) RCT_REMAP_SHADOW_PROPERTY(allowsFontScaling, allowsFontScaling, BOOL) +RCT_EXPORT_VIEW_PROPERTY(numberOfLines, NSInteger) RCT_EXPORT_VIEW_PROPERTY(onTextLayout, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(ellipsizeMode, NSString) RCT_EXPORT_VIEW_PROPERTY(selectable, BOOL) diff --git a/modules/react-native-ui-text-view/ios/RNUITextViewShadow.swift b/modules/react-native-ui-text-view/ios/RNUITextViewShadow.swift index 4f3eda43c..5a462f6b6 100644 --- a/modules/react-native-ui-text-view/ios/RNUITextViewShadow.swift +++ b/modules/react-native-ui-text-view/ios/RNUITextViewShadow.swift @@ -40,19 +40,19 @@ class RNUITextViewShadow: RCTShadowView { self.setAttributedText() } - // Tell yoga not to use flexbox + // Returning true here will tell Yoga to not use flexbox and instead use our custom measure func. override func isYogaLeafNode() -> Bool { return true } - // We only need to insert text children + // We should only insert children that are UITextView shadows override func insertReactSubview(_ subview: RCTShadowView!, at atIndex: Int) { if subview.isKind(of: RNUITextViewChildShadow.self) { super.insertReactSubview(subview, at: atIndex) } } - // Whenever the subvies update, set the text + // Every time the subviews change, we need to reformat and render the text. override func didUpdateReactSubviews() { self.setAttributedText() } @@ -64,7 +64,7 @@ class RNUITextViewShadow: RCTShadowView { return } - // Update the text + // Since we are inside the shadow view here, we have to find the real view and update the text. self.bridge.uiManager.addUIBlock { uiManager, viewRegistry in guard let textView = viewRegistry?[self.reactTag] as? RNUITextView else { return @@ -100,18 +100,25 @@ class RNUITextViewShadow: RCTShadowView { // Create the attributed string with the generic attributes let string = NSMutableAttributedString(string: child.text, attributes: attributes) - // Set the paragraph style attributes if necessary + // Set the paragraph style attributes if necessary. We can check this by seeing if the provided + // line height is not 0.0. let paragraphStyle = NSMutableParagraphStyle() if child.lineHeight != 0.0 { - paragraphStyle.minimumLineHeight = child.lineHeight - paragraphStyle.maximumLineHeight = child.lineHeight + // Whenever we change the line height for the text, we are also removing the DynamicType + // adjustment for line height. We need to get the multiplier and apply that to the + // line height. + let scaleMultiplier = scaledFontSize / child.fontSize + paragraphStyle.minimumLineHeight = child.lineHeight * scaleMultiplier + paragraphStyle.maximumLineHeight = child.lineHeight * scaleMultiplier + string.addAttribute( NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: NSMakeRange(0, string.length) ) - // Store that height + // To calcualte the size of the text without creating a new UILabel or UITextView, we have + // to store this line height for later. self.lineHeight = child.lineHeight } else { self.lineHeight = font.lineHeight @@ -124,24 +131,22 @@ class RNUITextViewShadow: RCTShadowView { self.dirtyLayout() } - // Create a YGSize based on the max width + // To create the needed size we need to: + // 1. Get the max size that we can use for the view + // 2. Calculate the height of the text based on that max size + // 3. Determine how many lines the text is, and limit that number if it exceeds the max + // 4. Set the frame size and return the YGSize. YGSize requires Float values while CGSize needs CGFloat func getNeededSize(maxWidth: Float) -> YGSize { - // Create the max size and figure out the size of the entire text let maxSize = CGSize(width: CGFloat(maxWidth), height: CGFloat(MAXFLOAT)) let textSize = self.attributedText.boundingRect(with: maxSize, options: .usesLineFragmentOrigin, context: nil) - // Figure out how many total lines there are - let totalLines = Int(ceil(textSize.height / self.lineHeight)) - - // Default to the text size - var neededSize: CGSize = textSize.size + var totalLines = Int(ceil(textSize.height / self.lineHeight)) - // If the total lines > max number, return size with the max if self.numberOfLines != 0, totalLines > self.numberOfLines { - neededSize = CGSize(width: CGFloat(maxWidth), height: CGFloat(CGFloat(self.numberOfLines) * self.lineHeight)) + totalLines = self.numberOfLines } - self.frameSize = neededSize - return YGSize(width: Float(neededSize.width), height: Float(neededSize.height)) + self.frameSize = CGSize(width: CGFloat(maxWidth), height: CGFloat(CGFloat(totalLines) * self.lineHeight)) + return YGSize(width: Float(self.frameSize.width), height: Float(self.frameSize.height)) } } diff --git a/package.json b/package.json index 4a3a2a7dc..5c31f10f0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bsky.app", - "version": "1.68.0", + "version": "1.69.0", "private": true, "engines": { "node": ">=18" @@ -12,8 +12,12 @@ "android": "expo run:android", "ios": "expo run:ios", "web": "expo start --web", + "use-build-number": "./scripts/useBuildNumberEnv.sh", "build-web": "expo export:web && node ./scripts/post-web-build.js && cp -v ./web-build/static/js/*.* ./bskyweb/static/js/", - "build-all": "yarn intl:build && eas build --platform all", + "build-all": "yarn intl:build && yarn use-build-number eas build --platform all", + "build-ios": "yarn use-build-number eas build -p ios", + "build-android": "yarn use-build-number eas build -p android", + "build": "yarn use-build-number eas build", "start": "expo start --dev-client", "start:prod": "expo start --dev-client --no-dev --minify", "clean-cache": "rm -rf node_modules/.cache/babel-loader/*", @@ -36,10 +40,7 @@ "intl:check": "yarn intl:extract && git diff-index -G'(^[^\\*# /])|(^#\\w)|(^\\s+[^\\*#/])' HEAD || (echo '\nā ļø i18n detected un-extracted translations\n' && exit 1)", "intl:extract": "lingui extract", "intl:compile": "lingui compile", - "nuke": "rm -rf ./node_modules && rm -rf ./ios && rm -rf ./android", - "bump": "./scripts/bumpIosBuildNumber.sh && ./scripts/bumpAndroidBuildNumber.sh", - "bump:ios": "./scripts/bumpIosBuildNumber.sh", - "bump:android": "./scripts/bumpAndroidBuildNumber.sh" + "nuke": "rm -rf ./node_modules && rm -rf ./ios && rm -rf ./android" }, "dependencies": { "@atproto/api": "^0.9.5", diff --git a/scripts/bumpAndroidBuildNumber.sh b/scripts/bumpAndroidBuildNumber.sh deleted file mode 100755 index 105f1296d..000000000 --- a/scripts/bumpAndroidBuildNumber.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/sh -# The number here should always be the line number the iOS build variable is on -line=$(sed "30q;d" ./app.config.js) -currentBuildNumber=$(echo "$line" | grep -oE '[0-9]+([.][0-9]+)?') -newBuildNumber=$((currentBuildNumber+1)) -newBuildVariable="const ANDROID_VERSION_CODE = '$newBuildNumber'" -sed -i.bak "30s/.*/ $newBuildVariable/" ./app.config.js -rm -rf ./app.config.js.bak - -echo "Android build number bumped to $newBuildNumber" diff --git a/scripts/bumpIosBuildNumber.sh b/scripts/bumpIosBuildNumber.sh deleted file mode 100755 index b78d2e69d..000000000 --- a/scripts/bumpIosBuildNumber.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/sh -# The number here should always be the line number the iOS build variable is on -line=$(sed "24q;d" ./app.config.js) -currentBuildNumber=$(echo "$line" | grep -oE '[0-9]+([.][0-9]+)?') -newBuildNumber=$((currentBuildNumber+1)) -newBuildVariable="const IOS_BUILD_NUMBER = '$newBuildNumber'" -sed -i.bak "24s/.*/ $newBuildVariable/" ./app.config.js -rm -rf ./app.config.js.bak - -echo "iOS build number bumped to $newBuildNumber" diff --git a/scripts/useBuildNumberEnv.sh b/scripts/useBuildNumberEnv.sh new file mode 100755 index 000000000..fe273d394 --- /dev/null +++ b/scripts/useBuildNumberEnv.sh @@ -0,0 +1,11 @@ +#!/bin/bash +outputIos=$(eas build:version:get -p ios) +outputAndroid=$(eas build:version:get -p android) +currentIosVersion=${outputIos#*buildNumber - } +currentAndroidVersion=${outputAndroid#*versionCode - } + +BSKY_IOS_BUILD_NUMBER=$((currentIosVersion+1)) +BSKY_ANDROID_VERSION_CODE=$((currentAndroidVersion+1)) + +bash -c "BSKY_IOS_BUILD_NUMBER=$BSKY_IOS_BUILD_NUMBER BSKY_ANDROID_VERSION_CODE=$BSKY_ANDROID_VERSION_CODE $*" + diff --git a/src/alf/atoms.ts b/src/alf/atoms.ts index f75e8ffe0..18f492d6e 100644 --- a/src/alf/atoms.ts +++ b/src/alf/atoms.ts @@ -176,43 +176,59 @@ export const atoms = { }, text_2xs: { fontSize: tokens.fontSize._2xs, + letterSpacing: 0.25, }, text_xs: { fontSize: tokens.fontSize.xs, + letterSpacing: 0.25, }, text_sm: { fontSize: tokens.fontSize.sm, + letterSpacing: 0.25, }, text_md: { fontSize: tokens.fontSize.md, + letterSpacing: 0.25, }, text_lg: { fontSize: tokens.fontSize.lg, + letterSpacing: 0.25, }, text_xl: { fontSize: tokens.fontSize.xl, + letterSpacing: 0.25, }, text_2xl: { fontSize: tokens.fontSize._2xl, + letterSpacing: 0.25, }, text_3xl: { fontSize: tokens.fontSize._3xl, + letterSpacing: 0.25, }, text_4xl: { fontSize: tokens.fontSize._4xl, + letterSpacing: 0.25, }, text_5xl: { fontSize: tokens.fontSize._5xl, + letterSpacing: 0.25, }, leading_tight: { lineHeight: 1.15, }, leading_snug: { - lineHeight: 1.25, + lineHeight: 1.3, }, leading_normal: { lineHeight: 1.5, }, + tracking_normal: { + letterSpacing: 0, + }, + tracking_wide: { + letterSpacing: 0.25, + }, font_normal: { fontWeight: tokens.fontWeight.normal, }, diff --git a/src/components/Dialog/context.ts b/src/components/Dialog/context.ts index b28b9f5a2..f0c7c983a 100644 --- a/src/components/Dialog/context.ts +++ b/src/components/Dialog/context.ts @@ -1,7 +1,11 @@ import React from 'react' import {useDialogStateContext} from '#/state/dialogs' -import {DialogContextProps, DialogControlProps} from '#/components/Dialog/types' +import { + DialogContextProps, + DialogControlProps, + DialogOuterProps, +} from '#/components/Dialog/types' export const Context = React.createContext<DialogContextProps>({ close: () => {}, @@ -11,7 +15,7 @@ export function useDialogContext() { return React.useContext(Context) } -export function useDialogControl() { +export function useDialogControl(): DialogOuterProps['control'] { const id = React.useId() const control = React.useRef<DialogControlProps>({ open: () => {}, @@ -30,6 +34,6 @@ export function useDialogControl() { return { ref: control, open: () => control.current.open(), - close: () => control.current.close(), + close: cb => control.current.close(cb), } } diff --git a/src/components/Dialog/index.tsx b/src/components/Dialog/index.tsx index 9132e68de..27f43afd3 100644 --- a/src/components/Dialog/index.tsx +++ b/src/components/Dialog/index.tsx @@ -8,7 +8,7 @@ import BottomSheet, { } from '@gorhom/bottom-sheet' import {useSafeAreaInsets} from 'react-native-safe-area-context' -import {useTheme, atoms as a} from '#/alf' +import {useTheme, atoms as a, flatten} from '#/alf' import {Portal} from '#/components/Portal' import {createInput} from '#/components/forms/TextField' @@ -35,12 +35,30 @@ export function Outer({ const sheetOptions = nativeOptions?.sheet || {} const hasSnapPoints = !!sheetOptions.snapPoints const insets = useSafeAreaInsets() + const closeCallback = React.useRef<() => void>() - const open = React.useCallback<DialogControlProps['open']>((i = 0) => { - sheet.current?.snapToIndex(i) - }, []) + /* + * Used to manage open/closed, but index is otherwise handled internally by `BottomSheet` + */ + const [openIndex, setOpenIndex] = React.useState(-1) + + /* + * `openIndex` is the index of the snap point to open the bottom sheet to. If >0, the bottom sheet is open. + */ + const isOpen = openIndex > -1 + + const open = React.useCallback<DialogControlProps['open']>( + ({index} = {}) => { + // can be set to any index of `snapPoints`, but `0` is the first i.e. "open" + setOpenIndex(index || 0) + }, + [setOpenIndex], + ) - const close = React.useCallback(() => { + const close = React.useCallback<DialogControlProps['close']>(cb => { + if (cb) { + closeCallback.current = cb + } sheet.current?.close() }, []) @@ -56,78 +74,85 @@ export function Outer({ const onChange = React.useCallback( (index: number) => { if (index === -1) { + closeCallback.current?.() + closeCallback.current = undefined onClose?.() + setOpenIndex(-1) } }, - [onClose], + [onClose, setOpenIndex], ) const context = React.useMemo(() => ({close}), [close]) return ( - <Portal> - <BottomSheet - enableDynamicSizing={!hasSnapPoints} - enablePanDownToClose - keyboardBehavior="interactive" - android_keyboardInputMode="adjustResize" - keyboardBlurBehavior="restore" - topInset={insets.top} - {...sheetOptions} - ref={sheet} - index={-1} - backgroundStyle={{backgroundColor: 'transparent'}} - backdropComponent={props => ( - <BottomSheetBackdrop - opacity={0.4} - appearsOnIndex={0} - disappearsOnIndex={-1} - {...props} - /> - )} - handleIndicatorStyle={{backgroundColor: t.palette.primary_500}} - handleStyle={{display: 'none'}} - onChange={onChange}> - <Context.Provider value={context}> - <View - style={[ - a.absolute, - a.inset_0, - t.atoms.bg, - { - borderTopLeftRadius: 40, - borderTopRightRadius: 40, - height: Dimensions.get('window').height * 2, - }, - ]} - /> - {children} - </Context.Provider> - </BottomSheet> - </Portal> + isOpen && ( + <Portal> + <BottomSheet + enableDynamicSizing={!hasSnapPoints} + enablePanDownToClose + keyboardBehavior="interactive" + android_keyboardInputMode="adjustResize" + keyboardBlurBehavior="restore" + topInset={insets.top} + {...sheetOptions} + snapPoints={sheetOptions.snapPoints || ['100%']} + ref={sheet} + index={openIndex} + backgroundStyle={{backgroundColor: 'transparent'}} + backdropComponent={props => ( + <BottomSheetBackdrop + opacity={0.4} + appearsOnIndex={0} + disappearsOnIndex={-1} + {...props} + style={[flatten(props.style), t.atoms.bg_contrast_300]} + /> + )} + handleIndicatorStyle={{backgroundColor: t.palette.primary_500}} + handleStyle={{display: 'none'}} + onChange={onChange}> + <Context.Provider value={context}> + <View + style={[ + a.absolute, + a.inset_0, + t.atoms.bg, + { + borderTopLeftRadius: 40, + borderTopRightRadius: 40, + height: Dimensions.get('window').height * 2, + }, + ]} + /> + {children} + </Context.Provider> + </BottomSheet> + </Portal> + ) ) } -// TODO a11y props here, or is that handled by the sheet? -export function Inner(props: DialogInnerProps) { +export function Inner({children, style}: DialogInnerProps) { const insets = useSafeAreaInsets() return ( <BottomSheetView style={[ - a.p_lg, + a.p_xl, { paddingTop: 40, borderTopLeftRadius: 40, borderTopRightRadius: 40, paddingBottom: insets.bottom + a.pb_5xl.paddingBottom, }, + flatten(style), ]}> - {props.children} + {children} </BottomSheetView> ) } -export function ScrollableInner(props: DialogInnerProps) { +export function ScrollableInner({children, style}: DialogInnerProps) { const insets = useSafeAreaInsets() return ( <BottomSheetScrollView @@ -136,13 +161,15 @@ export function ScrollableInner(props: DialogInnerProps) { style={[ a.flex_1, // main diff is this a.p_xl, + a.h_full, { paddingTop: 40, borderTopLeftRadius: 40, borderTopRightRadius: 40, }, + flatten(style), ]}> - {props.children} + {children} <View style={{height: insets.bottom + a.pt_5xl.paddingTop}} /> </BottomSheetScrollView> ) diff --git a/src/components/Dialog/index.web.tsx b/src/components/Dialog/index.web.tsx index 305c00e97..79441fb5e 100644 --- a/src/components/Dialog/index.web.tsx +++ b/src/components/Dialog/index.web.tsx @@ -5,11 +5,13 @@ import Animated, {FadeInDown, FadeIn} from 'react-native-reanimated' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {useTheme, atoms as a, useBreakpoints, web} from '#/alf' +import {useTheme, atoms as a, useBreakpoints, web, flatten} from '#/alf' import {Portal} from '#/components/Portal' import {DialogOuterProps, DialogInnerProps} from '#/components/Dialog/types' import {Context} from '#/components/Dialog/context' +import {Button, ButtonIcon} from '#/components/Button' +import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' export {useDialogControl, useDialogContext} from '#/components/Dialog/context' export * from '#/components/Dialog/types' @@ -18,9 +20,9 @@ export {Input} from '#/components/forms/TextField' const stopPropagation = (e: any) => e.stopPropagation() export function Outer({ + children, control, onClose, - children, }: React.PropsWithChildren<DialogOuterProps>) { const {_} = useLingui() const t = useTheme() @@ -147,7 +149,7 @@ export function Inner({ a.rounded_md, a.w_full, a.border, - gtMobile ? a.p_xl : a.p_lg, + gtMobile ? a.p_2xl : a.p_xl, t.atoms.bg, { maxWidth: 600, @@ -156,7 +158,7 @@ export function Inner({ shadowOpacity: t.name === 'light' ? 0.1 : 0.4, shadowRadius: 30, }, - ...(Array.isArray(style) ? style : [style || {}]), + flatten(style), ]}> {children} </Animated.View> @@ -170,25 +172,28 @@ export function Handle() { return null } -/** - * TODO(eric) unused rn - */ -// export function Close() { -// const {_} = useLingui() -// const t = useTheme() -// const {close} = useDialogContext() -// return ( -// <View -// style={[ -// a.absolute, -// a.z_10, -// { -// top: a.pt_lg.paddingTop, -// right: a.pr_lg.paddingRight, -// }, -// ]}> -// <Button onPress={close} label={_(msg`Close active dialog`)}> -// </Button> -// </View> -// ) -// } +export function Close() { + const {_} = useLingui() + const {close} = React.useContext(Context) + return ( + <View + style={[ + a.absolute, + a.z_10, + { + top: a.pt_md.paddingTop, + right: a.pr_md.paddingRight, + }, + ]}> + <Button + size="small" + variant="ghost" + color="primary" + shape="round" + onPress={close} + label={_(msg`Close active dialog`)}> + <ButtonIcon icon={X} size="md" /> + </Button> + </View> + ) +} diff --git a/src/components/Dialog/types.ts b/src/components/Dialog/types.ts index d36784183..75ba825ac 100644 --- a/src/components/Dialog/types.ts +++ b/src/components/Dialog/types.ts @@ -1,24 +1,34 @@ import React from 'react' -import type {ViewStyle, AccessibilityProps} from 'react-native' +import type {AccessibilityProps} from 'react-native' import {BottomSheetProps} from '@gorhom/bottom-sheet' +import {ViewStyleProp} from '#/alf' + type A11yProps = Required<AccessibilityProps> export type DialogContextProps = { close: () => void } +export type DialogControlOpenOptions = { + /** + * NATIVE ONLY + * + * Optional index of the snap point to open the bottom sheet to. Defaults to + * 0, which is the first snap point (i.e. "open"). + */ + index?: number +} + export type DialogControlProps = { - open: (index?: number) => void - close: () => void + open: (options?: DialogControlOpenOptions) => void + close: (callback?: () => void) => void } export type DialogOuterProps = { control: { ref: React.RefObject<DialogControlProps> - open: (index?: number) => void - close: () => void - } + } & DialogControlProps onClose?: () => void nativeOptions?: { sheet?: Omit<BottomSheetProps, 'children'> @@ -26,10 +36,7 @@ export type DialogOuterProps = { webOptions?: {} } -type DialogInnerPropsBase<T> = React.PropsWithChildren<{ - style?: ViewStyle -}> & - T +type DialogInnerPropsBase<T> = React.PropsWithChildren<ViewStyleProp> & T export type DialogInnerProps = | DialogInnerPropsBase<{ label?: undefined diff --git a/src/components/Link.tsx b/src/components/Link.tsx index afd30b5ee..593b0863a 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -1,9 +1,5 @@ import React from 'react' -import { - GestureResponderEvent, - Linking, - TouchableWithoutFeedback, -} from 'react-native' +import {GestureResponderEvent, Linking} from 'react-native' import { useLinkProps, useNavigation, @@ -23,7 +19,7 @@ import { } from '#/lib/strings/url-helpers' import {useModalControls} from '#/state/modals' import {router} from '#/routes' -import {Text} from '#/components/Typography' +import {Text, TextProps} from '#/components/Typography' /** * Only available within a `Link`, since that inherits from `Button`. @@ -55,11 +51,12 @@ type BaseLinkProps = Pick< warnOnMismatchingTextChild?: boolean /** - * Callback for when the link is pressed. + * Callback for when the link is pressed. Prevent default and return `false` + * to exit early and prevent navigation. * * DO NOT use this for navigation, that's what the `to` prop is for. */ - onPress?: (e: GestureResponderEvent) => void + onPress?: (e: GestureResponderEvent) => void | false /** * Web-only attribute. Sets `download` attr on web. @@ -86,7 +83,9 @@ export function useLink({ const onPress = React.useCallback( (e: GestureResponderEvent) => { - outerOnPress?.(e) + const exitEarlyIfFalse = outerOnPress?.(e) + + if (exitEarlyIfFalse === false) return const requiresWarning = Boolean( warnOnMismatchingTextChild && @@ -217,7 +216,7 @@ export function Link({ } export type InlineLinkProps = React.PropsWithChildren< - BaseLinkProps & TextStyleProp + BaseLinkProps & TextStyleProp & Pick<TextProps, 'selectable'> > export function InlineLink({ @@ -228,6 +227,7 @@ export function InlineLink({ style, onPress: outerOnPress, download, + selectable, ...rest }: InlineLinkProps) { const t = useTheme() @@ -253,43 +253,41 @@ export function InlineLink({ const flattenedStyle = flatten(style) return ( - <TouchableWithoutFeedback - accessibilityRole="button" + <Text + selectable={selectable} + label={href} + {...rest} + style={[ + {color: t.palette.primary_500}, + (hovered || focused || pressed) && { + outline: 0, + textDecorationLine: 'underline', + textDecorationColor: flattenedStyle.color ?? t.palette.primary_500, + }, + flattenedStyle, + ]} + role="link" onPress={download ? undefined : onPress} onPressIn={onPressIn} onPressOut={onPressOut} onFocus={onFocus} - onBlur={onBlur}> - <Text - label={href} - {...rest} - style={[ - {color: t.palette.primary_500}, - (hovered || focused || pressed) && { - outline: 0, - textDecorationLine: 'underline', - textDecorationColor: flattenedStyle.color ?? t.palette.primary_500, - }, - flattenedStyle, - ]} - role="link" - onMouseEnter={onHoverIn} - onMouseLeave={onHoverOut} - accessibilityRole="link" - href={href} - {...web({ - hrefAttrs: { - target: download ? undefined : isExternal ? 'blank' : undefined, - rel: isExternal ? 'noopener noreferrer' : undefined, - download, - }, - dataSet: { - // default to no underline, apply this ourselves - noUnderline: '1', - }, - })}> - {children} - </Text> - </TouchableWithoutFeedback> + onBlur={onBlur} + onMouseEnter={onHoverIn} + onMouseLeave={onHoverOut} + accessibilityRole="link" + href={href} + {...web({ + hrefAttrs: { + target: download ? undefined : isExternal ? 'blank' : undefined, + rel: isExternal ? 'noopener noreferrer' : undefined, + download, + }, + dataSet: { + // default to no underline, apply this ourselves + noUnderline: '1', + }, + })}> + {children} + </Text> ) } diff --git a/src/components/Prompt.tsx b/src/components/Prompt.tsx index 2c79d27cf..411679102 100644 --- a/src/components/Prompt.tsx +++ b/src/components/Prompt.tsx @@ -41,7 +41,7 @@ export function Outer({ <Dialog.Inner accessibilityLabelledBy={titleId} accessibilityDescribedBy={descriptionId} - style={{width: 'auto', maxWidth: 400}}> + style={[{width: 'auto', maxWidth: 400}]}> {children} </Dialog.Inner> </Context.Provider> diff --git a/src/components/RichText.tsx b/src/components/RichText.tsx index 068ee99e0..c72fcabdd 100644 --- a/src/components/RichText.tsx +++ b/src/components/RichText.tsx @@ -1,9 +1,9 @@ import React from 'react' import {RichText as RichTextAPI, AppBskyRichtextFacet} from '@atproto/api' -import {atoms as a, TextStyleProp} from '#/alf' +import {atoms as a, TextStyleProp, flatten} from '#/alf' import {InlineLink} from '#/components/Link' -import {Text} from '#/components/Typography' +import {Text, TextProps} from '#/components/Typography' import {toShortUrl} from 'lib/strings/url-helpers' import {getAgent} from '#/state/session' @@ -16,18 +16,20 @@ export function RichText({ numberOfLines, disableLinks, resolveFacets = false, -}: TextStyleProp & { - value: RichTextAPI | string - testID?: string - numberOfLines?: number - disableLinks?: boolean - resolveFacets?: boolean -}) { + selectable, +}: TextStyleProp & + Pick<TextProps, 'selectable'> & { + value: RichTextAPI | string + testID?: string + numberOfLines?: number + disableLinks?: boolean + resolveFacets?: boolean + }) { const detected = React.useRef(false) const [richText, setRichText] = React.useState<RichTextAPI>(() => value instanceof RichTextAPI ? value : new RichTextAPI({text: value}), ) - const styles = [a.leading_normal, style] + const styles = [a.leading_snug, flatten(style)] React.useEffect(() => { if (!resolveFacets) return @@ -50,6 +52,7 @@ export function RichText({ if (text.length <= 5 && /^\p{Extended_Pictographic}+$/u.test(text)) { return ( <Text + selectable={selectable} testID={testID} style={[ { @@ -65,6 +68,7 @@ export function RichText({ } return ( <Text + selectable={selectable} testID={testID} style={styles} numberOfLines={numberOfLines} @@ -88,6 +92,7 @@ export function RichText({ ) { els.push( <InlineLink + selectable={selectable} key={key} to={`/profile/${mention.did}`} style={[...styles, {pointerEvents: 'auto'}]} @@ -102,6 +107,7 @@ export function RichText({ } else { els.push( <InlineLink + selectable={selectable} key={key} to={link.uri} style={[...styles, {pointerEvents: 'auto'}]} @@ -120,6 +126,7 @@ export function RichText({ return ( <Text + selectable={selectable} testID={testID} style={styles} numberOfLines={numberOfLines} diff --git a/src/components/Typography.tsx b/src/components/Typography.tsx index b34f51018..c9ab7a8a1 100644 --- a/src/components/Typography.tsx +++ b/src/components/Typography.tsx @@ -1,7 +1,16 @@ import React from 'react' -import {Text as RNText, TextStyle, TextProps} from 'react-native' +import {Text as RNText, TextStyle, TextProps as RNTextProps} from 'react-native' +import {UITextView} from 'react-native-ui-text-view' import {useTheme, atoms, web, flatten} from '#/alf' +import {isIOS} from '#/platform/detection' + +export type TextProps = RNTextProps & { + /** + * Lets the user select text, to use the native copy and paste functionality. + */ + selectable?: boolean +} /** * Util to calculate lineHeight from a text size atom and a leading atom @@ -44,27 +53,24 @@ function normalizeTextStyles(styles: TextStyle[]) { /** * Our main text component. Use this most of the time. */ -export function Text({style, ...rest}: TextProps) { +export function Text({style, selectable, ...rest}: TextProps) { const t = useTheme() const s = normalizeTextStyles([atoms.text_sm, t.atoms.text, flatten(style)]) - return <RNText style={s} {...rest} /> + return selectable && isIOS ? ( + <UITextView style={s} {...rest} /> + ) : ( + <RNText selectable={selectable} style={s} {...rest} /> + ) } export function createHeadingElement({level}: {level: number}) { return function HeadingElement({style, ...rest}: TextProps) { - const t = useTheme() const attr = web({ role: 'heading', 'aria-level': level, }) || {} - return ( - <RNText - {...attr} - {...rest} - style={normalizeTextStyles([t.atoms.text, flatten(style)])} - /> - ) + return <Text {...attr} {...rest} style={style} /> } } @@ -78,21 +84,15 @@ export const H4 = createHeadingElement({level: 4}) export const H5 = createHeadingElement({level: 5}) export const H6 = createHeadingElement({level: 6}) export function P({style, ...rest}: TextProps) { - const t = useTheme() const attr = web({ role: 'paragraph', }) || {} return ( - <RNText + <Text {...attr} {...rest} - style={normalizeTextStyles([ - atoms.text_md, - atoms.leading_normal, - t.atoms.text, - flatten(style), - ])} + style={[atoms.text_md, atoms.leading_normal, flatten(style)]} /> ) } diff --git a/src/components/icons/Times.tsx b/src/components/icons/Times.tsx new file mode 100644 index 000000000..678ac3fcb --- /dev/null +++ b/src/components/icons/Times.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const TimesLarge_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M4.293 4.293a1 1 0 0 1 1.414 0L12 10.586l6.293-6.293a1 1 0 1 1 1.414 1.414L13.414 12l6.293 6.293a1 1 0 0 1-1.414 1.414L12 13.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L10.586 12 4.293 5.707a1 1 0 0 1 0-1.414Z', +}) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index aec8338d0..c8e5273d4 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -3,9 +3,8 @@ import {Insets, Platform} from 'react-native' export const LOCAL_DEV_SERVICE = Platform.OS === 'android' ? 'http://10.0.2.2:2583' : 'http://localhost:2583' export const STAGING_SERVICE = 'https://staging.bsky.dev' -export const PROD_SERVICE = 'https://bsky.social' -export const DEFAULT_SERVICE = PROD_SERVICE - +export const BSKY_SERVICE = 'https://bsky.social' +export const DEFAULT_SERVICE = BSKY_SERVICE const HELP_DESK_LANG = 'en-us' export const HELP_DESK_URL = `https://blueskyweb.zendesk.com/hc/${HELP_DESK_LANG}` @@ -36,92 +35,12 @@ export const MAX_GRAPHEME_LENGTH = 300 // but increasing limit per user feedback export const MAX_ALT_TEXT = 1000 -export function IS_LOCAL_DEV(url: string) { - return url.includes('localhost') -} - -export function IS_STAGING(url: string) { - return url.startsWith('https://staging.bsky.dev') -} - -export function IS_PROD(url: string) { - // NOTE - // until open federation, "production" is defined as the main server - // this definition will not work once federation is enabled! - // -prf - return ( - url.startsWith('https://bsky.social') || - url.startsWith('https://api.bsky.app') || - /bsky\.network\/?$/.test(url) - ) +export function IS_PROD_SERVICE(url?: string) { + return url && url !== STAGING_SERVICE && url !== LOCAL_DEV_SERVICE } -export const PROD_TEAM_HANDLES = [ - 'jay.bsky.social', - 'pfrazee.com', - 'divy.zone', - 'dholms.xyz', - 'why.bsky.world', - 'iamrosewang.bsky.social', -] -export const STAGING_TEAM_HANDLES = [ - 'arcalinea.staging.bsky.dev', - 'paul.staging.bsky.dev', - 'paul2.staging.bsky.dev', -] -export const DEV_TEAM_HANDLES = ['alice.test', 'bob.test', 'carla.test'] - -export function TEAM_HANDLES(serviceUrl: string) { - if (serviceUrl.includes('localhost')) { - return DEV_TEAM_HANDLES - } else if (serviceUrl.includes('staging')) { - return STAGING_TEAM_HANDLES - } else { - return PROD_TEAM_HANDLES - } -} - -export const STAGING_DEFAULT_FEED = (rkey: string) => - `at://did:plc:wqzurwm3kmaig6e6hnc2gqwo/app.bsky.feed.generator/${rkey}` export const PROD_DEFAULT_FEED = (rkey: string) => `at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/${rkey}` -export async function DEFAULT_FEEDS( - serviceUrl: string, - resolveHandle: (name: string) => Promise<string>, -) { - // TODO: remove this when the test suite no longer relies on it - if (IS_LOCAL_DEV(serviceUrl)) { - // local dev - const aliceDid = await resolveHandle('alice.test') - return { - pinned: [ - `at://${aliceDid}/app.bsky.feed.generator/alice-favs`, - `at://${aliceDid}/app.bsky.feed.generator/alice-favs2`, - ], - saved: [ - `at://${aliceDid}/app.bsky.feed.generator/alice-favs`, - `at://${aliceDid}/app.bsky.feed.generator/alice-favs2`, - ], - } - } else if (IS_STAGING(serviceUrl)) { - // staging - return { - pinned: [STAGING_DEFAULT_FEED('whats-hot')], - saved: [ - STAGING_DEFAULT_FEED('bsky-team'), - STAGING_DEFAULT_FEED('with-friends'), - STAGING_DEFAULT_FEED('whats-hot'), - STAGING_DEFAULT_FEED('hot-classic'), - ], - } - } else { - // production - return { - pinned: [PROD_DEFAULT_FEED('whats-hot')], - saved: [PROD_DEFAULT_FEED('whats-hot')], - } - } -} export const POST_IMG_MAX = { width: 2000, @@ -135,13 +54,11 @@ export const STAGING_LINK_META_PROXY = export const PROD_LINK_META_PROXY = 'https://cardyb.bsky.app/v1/extract?url=' export function LINK_META_PROXY(serviceUrl: string) { - if (IS_LOCAL_DEV(serviceUrl)) { - return STAGING_LINK_META_PROXY - } else if (IS_STAGING(serviceUrl)) { - return STAGING_LINK_META_PROXY - } else { + if (IS_PROD_SERVICE(serviceUrl)) { return PROD_LINK_META_PROXY } + + return STAGING_LINK_META_PROXY } export const STATUS_PAGE_URL = 'https://status.bsky.app/' diff --git a/src/lib/strings/embed-player.ts b/src/lib/strings/embed-player.ts index 21a575b91..1cf3b1293 100644 --- a/src/lib/strings/embed-player.ts +++ b/src/lib/strings/embed-player.ts @@ -343,45 +343,45 @@ export function parseEmbedPlayerFromUrl( } } -export function getPlayerHeight({ +export function getPlayerAspect({ type, - width, hasThumb, + width, }: { type: EmbedPlayerParams['type'] - width: number hasThumb: boolean -}) { - if (!hasThumb) return (width / 16) * 9 + width: number +}): {aspectRatio?: number; height?: number} { + if (!hasThumb) return {aspectRatio: 16 / 9} switch (type) { case 'youtube_video': case 'twitch_video': case 'vimeo_video': - return (width / 16) * 9 + return {aspectRatio: 16 / 9} case 'youtube_short': if (SCREEN_HEIGHT < 600) { - return ((width / 9) * 16) / 1.75 + return {aspectRatio: (9 / 16) * 1.75} } else { - return ((width / 9) * 16) / 1.5 + return {aspectRatio: (9 / 16) * 1.5} } case 'spotify_album': case 'apple_music_album': case 'apple_music_playlist': case 'spotify_playlist': case 'soundcloud_set': - return 380 + return {height: 380} case 'spotify_song': if (width <= 300) { - return 155 + return {height: 155} } - return 232 + return {height: 232} case 'soundcloud_track': - return 165 + return {height: 165} case 'apple_music_song': - return 150 + return {height: 150} default: - return width + return {aspectRatio: 16 / 9} } } diff --git a/src/lib/strings/url-helpers.ts b/src/lib/strings/url-helpers.ts index 8a71718c8..ef341154d 100644 --- a/src/lib/strings/url-helpers.ts +++ b/src/lib/strings/url-helpers.ts @@ -1,5 +1,5 @@ import {AtUri} from '@atproto/api' -import {PROD_SERVICE} from 'lib/constants' +import {BSKY_SERVICE} from 'lib/constants' import TLDs from 'tlds' import psl from 'psl' @@ -28,7 +28,7 @@ export function makeRecordUri( export function toNiceDomain(url: string): string { try { const urlp = new URL(url) - if (`https://${urlp.host}` === PROD_SERVICE) { + if (`https://${urlp.host}` === BSKY_SERVICE) { return 'Bluesky Social' } return urlp.host ? urlp.host : url diff --git a/src/screens/Onboarding/StepTopicalFeeds.tsx b/src/screens/Onboarding/StepTopicalFeeds.tsx index 4a2210853..636565e34 100644 --- a/src/screens/Onboarding/StepTopicalFeeds.tsx +++ b/src/screens/Onboarding/StepTopicalFeeds.tsx @@ -3,7 +3,6 @@ import {View} from 'react-native' import {useLingui} from '@lingui/react' import {msg, Trans} from '@lingui/macro' -import {IS_PROD} from '#/env' import {atoms as a} from '#/alf' import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron' import {ListMagnifyingGlass_Stroke2_Corner0_Rounded as ListMagnifyingGlass} from '#/components/icons/ListMagnifyingGlass' @@ -22,21 +21,28 @@ import { import {FeedCard} from '#/screens/Onboarding/StepAlgoFeeds/FeedCard' import {aggregateInterestItems} from '#/screens/Onboarding/util' import {IconCircle} from '#/components/IconCircle' +import {IS_PROD_SERVICE} from 'lib/constants' +import {useSession} from 'state/session' export function StepTopicalFeeds() { const {_} = useLingui() const {track} = useAnalytics() + const {currentAccount} = useSession() const {state, dispatch, interestsDisplayNames} = React.useContext(Context) const [selectedFeedUris, setSelectedFeedUris] = React.useState<string[]>([]) const [saving, setSaving] = React.useState(false) const suggestedFeedUris = React.useMemo(() => { - if (!IS_PROD) return [] + if (!IS_PROD_SERVICE(currentAccount?.service)) return [] return aggregateInterestItems( state.interestsStepResults.selectedInterests, state.interestsStepResults.apiResponse.suggestedFeedUris, state.interestsStepResults.apiResponse.suggestedFeedUris.default, ).slice(0, 10) - }, [state.interestsStepResults]) + }, [ + currentAccount?.service, + state.interestsStepResults.apiResponse.suggestedFeedUris, + state.interestsStepResults.selectedInterests, + ]) const interestsText = React.useMemo(() => { const i = state.interestsStepResults.selectedInterests.map( diff --git a/src/state/queries/notifications/feed.ts b/src/state/queries/notifications/feed.ts index b91db9237..405d054d4 100644 --- a/src/state/queries/notifications/feed.ts +++ b/src/state/queries/notifications/feed.ts @@ -133,23 +133,6 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) { return query } -/** - * This helper is used by the post-thread placeholder function to - * find a post in the query-data cache - */ -export function findPostInQueryData( - queryClient: QueryClient, - uri: string, -): AppBskyFeedDefs.PostView | undefined { - const generator = findAllPostsInQueryData(queryClient, uri) - const result = generator.next() - if (result.done) { - return undefined - } else { - return result.value - } -} - export function* findAllPostsInQueryData( queryClient: QueryClient, uri: string, diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts index 320009089..40399395a 100644 --- a/src/state/queries/post-feed.ts +++ b/src/state/queries/post-feed.ts @@ -365,23 +365,6 @@ function createApi( } } -/** - * This helper is used by the post-thread placeholder function to - * find a post in the query-data cache - */ -export function findPostInQueryData( - queryClient: QueryClient, - uri: string, -): AppBskyFeedDefs.PostView | undefined { - const generator = findAllPostsInQueryData(queryClient, uri) - const result = generator.next() - if (result.done) { - return undefined - } else { - return result.value - } -} - export function* findAllPostsInQueryData( queryClient: QueryClient, uri: string, diff --git a/src/state/queries/post-thread.ts b/src/state/queries/post-thread.ts index ba4243163..26d40599c 100644 --- a/src/state/queries/post-thread.ts +++ b/src/state/queries/post-thread.ts @@ -8,8 +8,8 @@ import {useQuery, useQueryClient, QueryClient} from '@tanstack/react-query' import {getAgent} from '#/state/session' import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types' -import {findPostInQueryData as findPostInFeedQueryData} from './post-feed' -import {findPostInQueryData as findPostInNotifsQueryData} from './notifications/feed' +import {findAllPostsInQueryData as findAllPostsInFeedQueryData} from './post-feed' +import {findAllPostsInQueryData as findAllPostsInNotifsQueryData} from './notifications/feed' import {precacheThreadPostProfiles} from './profile' import {getEmbeddedPost} from './util' @@ -82,21 +82,9 @@ export function usePostThreadQuery(uri: string | undefined) { return undefined } { - const item = findPostInQueryData(queryClient, uri) - if (item) { - return threadNodeToPlaceholderThread(item) - } - } - { - const item = findPostInFeedQueryData(queryClient, uri) - if (item) { - return postViewToPlaceholderThread(item) - } - } - { - const item = findPostInNotifsQueryData(queryClient, uri) - if (item) { - return postViewToPlaceholderThread(item) + const post = findPostInQueryData(queryClient, uri) + if (post) { + return post } } return undefined @@ -171,11 +159,18 @@ function responseToThreadNodes( AppBskyFeedPost.isRecord(node.post.record) && AppBskyFeedPost.validateRecord(node.post.record).success ) { + const post = node.post + // These should normally be present. They're missing only for + // posts that were *just* created. Ideally, the backend would + // know to return zeros. Fill them in manually to compensate. + post.replyCount ??= 0 + post.likeCount ??= 0 + post.repostCount ??= 0 return { type: 'post', _reactKey: node.post.uri, uri: node.post.uri, - post: node.post, + post: post, record: node.post.record, parent: node.parent && direction !== 'down' @@ -213,14 +208,24 @@ function responseToThreadNodes( function findPostInQueryData( queryClient: QueryClient, uri: string, -): ThreadNode | undefined { - const generator = findAllPostsInQueryData(queryClient, uri) - const result = generator.next() - if (result.done) { - return undefined - } else { - return result.value +): ThreadNode | void { + let partial + for (let item of findAllPostsInQueryData(queryClient, uri)) { + if (item.type === 'post') { + // Currently, the backend doesn't send full post info in some cases + // (for example, for quoted posts). We use missing `likeCount` + // as a way to detect that. In the future, we should fix this on + // the backend, which will let us always stop on the first result. + const hasAllInfo = item.post.likeCount != null + if (hasAllInfo) { + return item + } else { + partial = item + // Keep searching, we might still find a full post in the cache. + } + } } + return partial } export function* findAllPostsInQueryData( @@ -236,7 +241,10 @@ export function* findAllPostsInQueryData( } for (const item of traverseThread(queryData)) { if (item.uri === uri) { - yield item + const placeholder = threadNodeToPlaceholderThread(item) + if (placeholder) { + yield placeholder + } } const quotedPost = item.type === 'post' ? getEmbeddedPost(item.post.embed) : undefined @@ -245,6 +253,12 @@ export function* findAllPostsInQueryData( } } } + for (let post of findAllPostsInFeedQueryData(queryClient, uri)) { + yield postViewToPlaceholderThread(post) + } + for (let post of findAllPostsInNotifsQueryData(queryClient, uri)) { + yield postViewToPlaceholderThread(post) + } } function* traverseThread(node: ThreadNode): Generator<ThreadNode, void> { diff --git a/src/state/queries/util.ts b/src/state/queries/util.ts index f3a87ae5d..54752b332 100644 --- a/src/state/queries/util.ts +++ b/src/state/queries/util.ts @@ -53,5 +53,6 @@ export function embedViewRecordToPostView( record: v.value, indexedAt: v.indexedAt, labels: v.labels, + embed: v.embeds?.[0], } } diff --git a/src/view/com/auth/create/state.ts b/src/view/com/auth/create/state.ts index 276eaf924..68cfaceec 100644 --- a/src/view/com/auth/create/state.ts +++ b/src/view/com/auth/create/state.ts @@ -12,7 +12,7 @@ import {createFullHandle} from '#/lib/strings/handles' import {cleanError} from '#/lib/strings/errors' import {useOnboardingDispatch} from '#/state/shell/onboarding' import {useSessionApi} from '#/state/session' -import {DEFAULT_SERVICE, IS_PROD} from '#/lib/constants' +import {DEFAULT_SERVICE, IS_PROD_SERVICE} from '#/lib/constants' import { DEFAULT_PROD_FEEDS, usePreferencesSetBirthDateMutation, @@ -147,7 +147,7 @@ export function useSubmitCreateAccount( : undefined, }) setBirthDate({birthDate: uiState.birthDate}) - if (IS_PROD(uiState.serviceUrl)) { + if (IS_PROD_SERVICE(uiState.serviceUrl)) { setSavedFeeds(DEFAULT_PROD_FEEDS) } } catch (e: any) { diff --git a/src/view/com/auth/server-input/index.tsx b/src/view/com/auth/server-input/index.tsx index 2277eb867..b26ac1dcb 100644 --- a/src/view/com/auth/server-input/index.tsx +++ b/src/view/com/auth/server-input/index.tsx @@ -2,7 +2,7 @@ import React from 'react' import {View} from 'react-native' import {useLingui} from '@lingui/react' import {Trans, msg} from '@lingui/macro' -import {PROD_SERVICE} from 'lib/constants' +import {BSKY_SERVICE} from 'lib/constants' import * as persisted from '#/state/persisted' import {atoms as a, useBreakpoints, useTheme} from '#/alf' @@ -26,7 +26,7 @@ export function ServerInputDialog({ const [pdsAddressHistory, setPdsAddressHistory] = React.useState<string[]>( persisted.get('pdsAddressHistory') || [], ) - const [fixedOption, setFixedOption] = React.useState([PROD_SERVICE]) + const [fixedOption, setFixedOption] = React.useState([BSKY_SERVICE]) const [customAddress, setCustomAddress] = React.useState('') const onClose = React.useCallback(() => { @@ -86,7 +86,7 @@ export function ServerInputDialog({ label="Preferences" values={fixedOption} onChange={setFixedOption}> - <ToggleButton.Button name={PROD_SERVICE} label={_(msg`Bluesky`)}> + <ToggleButton.Button name={BSKY_SERVICE} label={_(msg`Bluesky`)}> {_(msg`Bluesky`)} </ToggleButton.Button> <ToggleButton.Button diff --git a/src/view/com/feeds/FeedSourceCard.tsx b/src/view/com/feeds/FeedSourceCard.tsx index 0de88b248..9bd7238df 100644 --- a/src/view/com/feeds/FeedSourceCard.tsx +++ b/src/view/com/feeds/FeedSourceCard.tsx @@ -2,7 +2,7 @@ import React from 'react' import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {Text} from '../util/text/Text' -import {RichText} from '../util/text/RichText' +import {RichText} from '#/components/RichText' import {usePalette} from 'lib/hooks/usePalette' import {s} from 'lib/styles' import {UserAvatar} from '../util/UserAvatar' @@ -25,6 +25,7 @@ import { } from '#/state/queries/preferences' import {useFeedSourceInfoQuery, FeedSourceInfo} from '#/state/queries/feed' import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' +import {useTheme} from '#/alf' export function FeedSourceCard({ feedUri, @@ -82,6 +83,7 @@ export function FeedSourceCardLoaded({ pinOnSave?: boolean showMinimalPlaceholder?: boolean }) { + const t = useTheme() const pal = usePalette('default') const {_} = useLingui() const navigation = useNavigation<NavigationProp>() @@ -266,8 +268,8 @@ export function FeedSourceCardLoaded({ {showDescription && feed.description ? ( <RichText - style={[pal.textLight, styles.description]} - richText={feed.description} + style={[t.atoms.text_contrast_high, styles.description]} + value={feed.description} numberOfLines={3} /> ) : null} diff --git a/src/view/com/lists/ListCard.tsx b/src/view/com/lists/ListCard.tsx index 5750faec1..19842eb54 100644 --- a/src/view/com/lists/ListCard.tsx +++ b/src/view/com/lists/ListCard.tsx @@ -3,7 +3,7 @@ import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' import {AtUri, AppBskyGraphDefs, RichText} from '@atproto/api' import {Link} from '../util/Link' import {Text} from '../util/text/Text' -import {RichText as RichTextCom} from '../util/text/RichText' +import {RichText as RichTextCom} from '#/components/RichText' import {UserAvatar} from '../util/UserAvatar' import {s} from 'lib/styles' import {usePalette} from 'lib/hooks/usePalette' @@ -12,6 +12,7 @@ import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeHandle} from 'lib/strings/handles' import {makeProfileLink} from 'lib/routes/links' import {Trans} from '@lingui/macro' +import {atoms as a} from '#/alf' export const ListCard = ({ testID, @@ -119,9 +120,9 @@ export const ListCard = ({ {descriptionRichText ? ( <View style={styles.details}> <RichTextCom - style={[pal.text, s.flex1]} + style={[a.flex_1]} numberOfLines={20} - richText={descriptionRichText} + value={descriptionRichText} /> </View> ) : undefined} diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index c66947d44..015859dd6 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -11,7 +11,7 @@ import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {PostThreadFollowBtn} from 'view/com/post-thread/PostThreadFollowBtn' import {Link, TextLink} from '../util/Link' -import {RichText} from '../util/text/RichText' +import {RichText} from '#/components/RichText' import {Text} from '../util/text/Text' import {PreviewableUserAvatar} from '../util/UserAvatar' import {s} from 'lib/styles' @@ -44,6 +44,7 @@ import {ThreadPost} from '#/state/queries/post-thread' import {useSession} from 'state/session' import {WhoCanReply} from '../threadgate/WhoCanReply' import {LoadingPlaceholder} from '../util/LoadingPlaceholder' +import {atoms as a} from '#/alf' export function PostThreadItem({ post, @@ -326,10 +327,8 @@ let PostThreadItemLoaded = ({ styles.postTextLargeContainer, ]}> <RichText - type="post-text-lg" - richText={richText} - lineHeight={1.3} - style={s.flex1} + value={richText} + style={[a.flex_1, a.text_xl]} selectable /> </View> @@ -522,10 +521,8 @@ let PostThreadItemLoaded = ({ {richText?.text ? ( <View style={styles.postTextContainer}> <RichText - type="post-text" - richText={richText} - style={[pal.text, s.flex1]} - lineHeight={1.3} + value={richText} + style={[a.flex_1, a.text_md]} numberOfLines={limitLines ? MAX_POST_LINES : undefined} /> </View> diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx index 2f1c0d37b..aec916adb 100644 --- a/src/view/com/post/Post.tsx +++ b/src/view/com/post/Post.tsx @@ -17,7 +17,7 @@ import {PostCtrls} from '../util/post-ctrls/PostCtrls' import {ContentHider} from '../util/moderation/ContentHider' import {PostAlerts} from '../util/moderation/PostAlerts' import {Text} from '../util/text/Text' -import {RichText} from '../util/text/RichText' +import {RichText} from '#/components/RichText' import {PreviewableUserAvatar} from '../util/UserAvatar' import {s, colors} from 'lib/styles' import {usePalette} from 'lib/hooks/usePalette' @@ -29,6 +29,7 @@ import {useComposerControls} from '#/state/shell/composer' import {Shadow, usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow' import {Trans, msg} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {atoms as a} from '#/alf' export function Post({ post, @@ -184,11 +185,9 @@ function PostInner({ <View style={styles.postTextContainer}> <RichText testID="postText" - type="post-text" - richText={richText} - lineHeight={1.3} + value={richText} numberOfLines={limitLines ? MAX_POST_LINES : undefined} - style={s.flex1} + style={[a.flex_1, a.text_md]} /> </View> ) : undefined} diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx index 8d0f2bef2..6f64de181 100644 --- a/src/view/com/posts/FeedItem.tsx +++ b/src/view/com/posts/FeedItem.tsx @@ -20,7 +20,7 @@ import {PostCtrls} from '../util/post-ctrls/PostCtrls' import {PostEmbeds} from '../util/post-embeds' import {ContentHider} from '../util/moderation/ContentHider' import {PostAlerts} from '../util/moderation/PostAlerts' -import {RichText} from '../util/text/RichText' +import {RichText} from '#/components/RichText' import {PreviewableUserAvatar} from '../util/UserAvatar' import {s} from 'lib/styles' import {usePalette} from 'lib/hooks/usePalette' @@ -36,6 +36,7 @@ import {FeedNameText} from '../util/FeedInfoText' import {useSession} from '#/state/session' import {Trans, msg} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {atoms as a} from '#/alf' export function FeedItem({ post, @@ -347,11 +348,9 @@ let PostContent = ({ <View style={styles.postTextContainer}> <RichText testID="postText" - type="post-text" - richText={richText} - lineHeight={1.3} + value={richText} numberOfLines={limitLines ? MAX_POST_LINES : undefined} - style={s.flex1} + style={[a.flex_1, a.text_md]} /> </View> ) : undefined} diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx index 8fd50fad6..3e479d7b5 100644 --- a/src/view/com/profile/ProfileHeader.tsx +++ b/src/view/com/profile/ProfileHeader.tsx @@ -23,7 +23,7 @@ import * as Toast from '../util/Toast' import {LoadingPlaceholder} from '../util/LoadingPlaceholder' import {Text} from '../util/text/Text' import {ThemedText} from '../util/text/ThemedText' -import {RichText} from '../util/text/RichText' +import {RichText} from '#/components/RichText' import {UserAvatar} from '../util/UserAvatar' import {UserBanner} from '../util/UserBanner' import {ProfileHeaderAlerts} from '../util/moderation/ProfileHeaderAlerts' @@ -56,6 +56,7 @@ import {Shadow} from '#/state/cache/types' import {useRequireAuth} from '#/state/session' import {LabelInfo} from '../util/moderation/LabelInfo' import {useProfileShadow} from 'state/cache/profile-shadow' +import {atoms as a} from '#/alf' let ProfileHeaderLoading = (_props: {}): React.ReactNode => { const pal = usePalette('default') @@ -608,12 +609,12 @@ let ProfileHeader = ({ </Text> </View> {descriptionRT && !moderation.profile.blur ? ( - <View pointerEvents="auto"> + <View pointerEvents="auto" style={[styles.description]}> <RichText testID="profileHeaderDescription" - style={[styles.description, pal.text]} + style={[a.text_md]} numberOfLines={15} - richText={descriptionRT} + value={descriptionRT} /> </View> ) : undefined} diff --git a/src/view/com/util/EventStopper.tsx b/src/view/com/util/EventStopper.tsx index 1e672e945..e743e89bb 100644 --- a/src/view/com/util/EventStopper.tsx +++ b/src/view/com/util/EventStopper.tsx @@ -1,11 +1,14 @@ import React from 'react' -import {View} from 'react-native' +import {View, ViewStyle} from 'react-native' /** * This utility function captures events and stops * them from propagating upwards. */ -export function EventStopper({children}: React.PropsWithChildren<{}>) { +export function EventStopper({ + children, + style, +}: React.PropsWithChildren<{style?: ViewStyle | ViewStyle[]}>) { const stop = (e: any) => { e.stopPropagation() } @@ -15,7 +18,8 @@ export function EventStopper({children}: React.PropsWithChildren<{}>) { onTouchEnd={stop} // @ts-ignore web only -prf onClick={stop} - onKeyDown={stop}> + onKeyDown={stop} + style={style}> {children} </View> ) diff --git a/src/view/com/util/forms/PostDropdownBtn.tsx b/src/view/com/util/forms/PostDropdownBtn.tsx index e56c88d2c..1dfb687df 100644 --- a/src/view/com/util/forms/PostDropdownBtn.tsx +++ b/src/view/com/util/forms/PostDropdownBtn.tsx @@ -2,6 +2,7 @@ import React, {memo} from 'react' import {StyleProp, View, ViewStyle} from 'react-native' import Clipboard from '@react-native-clipboard/clipboard' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {useNavigation} from '@react-navigation/native' import { AppBskyActorDefs, AppBskyFeedPost, @@ -19,6 +20,8 @@ import * as Toast from '../Toast' import {EventStopper} from '../EventStopper' import {useModalControls} from '#/state/modals' import {makeProfileLink} from '#/lib/routes/links' +import {CommonNavigatorParams} from '#/lib/routes/types' +import {getCurrentRoute} from 'lib/routes/helpers' import {getTranslatorLink} from '#/locale/helpers' import {usePostDeleteMutation} from '#/state/queries/post' import {useMutedThreads, useToggleThreadMute} from '#/state/muted-threads' @@ -63,6 +66,7 @@ let PostDropdownBtn = ({ const hiddenPosts = useHiddenPosts() const {hidePost} = useHiddenPostsApi() const openLink = useOpenLink() + const navigation = useNavigation() const rootUri = record.reply?.root?.uri || postUri const isThreadMuted = mutedThreads.includes(rootUri) @@ -82,13 +86,38 @@ let PostDropdownBtn = ({ postDeleteMutation.mutateAsync({uri: postUri}).then( () => { Toast.show(_(msg`Post deleted`)) + + const route = getCurrentRoute(navigation.getState()) + if (route.name === 'PostThread') { + const params = route.params as CommonNavigatorParams['PostThread'] + if ( + currentAccount && + isAuthor && + (params.name === currentAccount.handle || + params.name === currentAccount.did) + ) { + const currentHref = makeProfileLink(postAuthor, 'post', params.rkey) + if (currentHref === href && navigation.canGoBack()) { + navigation.goBack() + } + } + } }, e => { logger.error('Failed to delete post', {message: e}) Toast.show(_(msg`Failed to delete post, please try again`)) }, ) - }, [postUri, postDeleteMutation, _]) + }, [ + navigation, + postUri, + postDeleteMutation, + postAuthor, + currentAccount, + isAuthor, + href, + _, + ]) const onToggleThreadMute = React.useCallback(() => { try { diff --git a/src/view/com/util/post-embeds/ExternalPlayerEmbed.tsx b/src/view/com/util/post-embeds/ExternalPlayerEmbed.tsx index d556e7669..cf2db5b33 100644 --- a/src/view/com/util/post-embeds/ExternalPlayerEmbed.tsx +++ b/src/view/com/util/post-embeds/ExternalPlayerEmbed.tsx @@ -21,7 +21,7 @@ import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useNavigation} from '@react-navigation/native' import {AppBskyEmbedExternal} from '@atproto/api' -import {EmbedPlayerParams, getPlayerHeight} from 'lib/strings/embed-player' +import {EmbedPlayerParams, getPlayerAspect} from 'lib/strings/embed-player' import {EventStopper} from '../EventStopper' import {isNative} from 'platform/detection' import {NavigationProp} from 'lib/routes/types' @@ -67,14 +67,12 @@ function PlaceholderOverlay({ // This renders the webview/youtube player as a separate layer function Player({ - height, params, onLoad, isPlayerActive, }: { isPlayerActive: boolean params: EmbedPlayerParams - height: number onLoad: () => void }) { // ensures we only load what's requested @@ -91,25 +89,21 @@ function Player({ if (!isPlayerActive) return null return ( - <View style={[styles.layer, styles.playerLayer]}> - <EventStopper> - <View style={{height, width: '100%'}}> - <WebView - javaScriptEnabled={true} - onShouldStartLoadWithRequest={onShouldStartLoadWithRequest} - mediaPlaybackRequiresUserAction={false} - allowsInlineMediaPlayback - bounces={false} - allowsFullscreenVideo - nestedScrollEnabled - source={{uri: params.playerUri}} - onLoad={onLoad} - setSupportMultipleWindows={false} // Prevent any redirects from opening a new window (ads) - style={[styles.webview, styles.topRadius]} - /> - </View> - </EventStopper> - </View> + <EventStopper style={[styles.layer, styles.playerLayer]}> + <WebView + javaScriptEnabled={true} + onShouldStartLoadWithRequest={onShouldStartLoadWithRequest} + mediaPlaybackRequiresUserAction={false} + allowsInlineMediaPlayback + bounces={false} + allowsFullscreenVideo + nestedScrollEnabled + source={{uri: params.playerUri}} + onLoad={onLoad} + style={styles.webview} + setSupportMultipleWindows={false} // Prevent any redirects from opening a new window (ads) + /> + </EventStopper> ) } @@ -129,13 +123,16 @@ export function ExternalPlayer({ const [isPlayerActive, setPlayerActive] = React.useState(false) const [isLoading, setIsLoading] = React.useState(true) - const [dim, setDim] = React.useState({ - width: 0, - height: 0, - }) - const viewRef = useAnimatedRef() + const aspect = React.useMemo(() => { + return getPlayerAspect({ + type: params.type, + width: windowDims.width, + hasThumb: !!link.thumb, + }) + }, [params.type, windowDims.width, link.thumb]) + const viewRef = useAnimatedRef() const frameCallback = useFrameCallback(() => { const measurement = measure(viewRef) if (!measurement) return @@ -180,17 +177,6 @@ export function ExternalPlayer({ } }, [navigation, isPlayerActive, frameCallback]) - // calculate height for the player and the screen size - const height = React.useMemo( - () => - getPlayerHeight({ - type: params.type, - width: dim.width, - hasThumb: !!link.thumb, - }), - [params.type, dim.width, link.thumb], - ) - const onLoad = React.useCallback(() => { setIsLoading(false) }, []) @@ -216,32 +202,11 @@ export function ExternalPlayer({ [externalEmbedsPrefs, openModal, params.source], ) - // measure the layout to set sizing - const onLayout = React.useCallback( - (event: {nativeEvent: {layout: {width: any; height: any}}}) => { - setDim({ - width: event.nativeEvent.layout.width, - height: event.nativeEvent.layout.height, - }) - }, - [], - ) - return ( - <Animated.View - ref={viewRef} - style={{height}} - collapsable={false} - onLayout={onLayout}> + <Animated.View ref={viewRef} collapsable={false} style={[aspect]}> {link.thumb && (!isPlayerActive || isLoading) && ( <Image - style={[ - { - width: dim.width, - height, - }, - styles.topRadius, - ]} + style={[{flex: 1}, styles.topRadius]} source={{uri: link.thumb}} accessibilityIgnoresInvertColors /> @@ -251,12 +216,7 @@ export function ExternalPlayer({ isPlayerActive={isPlayerActive} onPress={onPlayPress} /> - <Player - isPlayerActive={isPlayerActive} - params={params} - height={height} - onLoad={onLoad} - /> + <Player isPlayerActive={isPlayerActive} params={params} onLoad={onLoad} /> </Animated.View> ) } diff --git a/src/view/com/util/post-embeds/QuoteEmbed.tsx b/src/view/com/util/post-embeds/QuoteEmbed.tsx index d9d84feb4..c128a6f00 100644 --- a/src/view/com/util/post-embeds/QuoteEmbed.tsx +++ b/src/view/com/util/post-embeds/QuoteEmbed.tsx @@ -20,7 +20,8 @@ import {PostAlerts} from '../moderation/PostAlerts' import {makeProfileLink} from 'lib/routes/links' import {InfoCircleIcon} from 'lib/icons' import {Trans} from '@lingui/macro' -import {RichText} from 'view/com/util/text/RichText' +import {RichText} from '#/components/RichText' +import {atoms as a} from '#/alf' export function MaybeQuoteEmbed({ embed, @@ -127,11 +128,10 @@ export function QuoteEmbed({ ) : null} {richText ? ( <RichText - richText={richText} - type="post-text" - style={pal.text} + value={richText} + style={[a.text_md]} numberOfLines={20} - noLinks + disableLinks /> ) : null} {embed && <PostEmbeds embed={embed} moderation={{}} />} diff --git a/src/view/com/util/text/RichText.tsx b/src/view/com/util/text/RichText.tsx index e910127fe..b6d461224 100644 --- a/src/view/com/util/text/RichText.tsx +++ b/src/view/com/util/text/RichText.tsx @@ -10,6 +10,9 @@ import {usePalette} from 'lib/hooks/usePalette' const WORD_WRAP = {wordWrap: 1} +/** + * @deprecated use `#/components/RichText` + */ export function RichText({ testID, type = 'md', diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx index 4901308ba..2c346e892 100644 --- a/src/view/screens/ProfileFeed.tsx +++ b/src/view/screens/ProfileFeed.tsx @@ -17,7 +17,7 @@ import {TextLink} from 'view/com/util/Link' import {ListRef} from 'view/com/util/List' import {Button} from 'view/com/util/forms/Button' import {Text} from 'view/com/util/text/Text' -import {RichText} from 'view/com/util/text/RichText' +import {RichText} from '#/components/RichText' import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn' import {FAB} from 'view/com/util/fab/FAB' import {EmptyState} from 'view/com/util/EmptyState' @@ -59,6 +59,7 @@ import {useComposerControls} from '#/state/shell/composer' import {truncateAndInvalidate} from '#/state/queries/util' import {isNative} from '#/platform/detection' import {listenSoftReset} from '#/state/events' +import {atoms as a} from '#/alf' const SECTION_TITLES = ['Posts', 'About'] @@ -575,9 +576,8 @@ function AboutSection({ {feedInfo.description ? ( <RichText testID="listDescription" - type="lg" - style={pal.text} - richText={feedInfo.description} + style={[a.text_md]} + value={feedInfo.description} /> ) : ( <Text type="lg" style={[{fontStyle: 'italic'}, pal.textLight]}> diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx index 796464883..d86b569e2 100644 --- a/src/view/screens/ProfileList.tsx +++ b/src/view/screens/ProfileList.tsx @@ -14,7 +14,7 @@ import {NativeDropdown, DropdownItem} from 'view/com/util/forms/NativeDropdown' import {CenteredView} from 'view/com/util/Views' import {EmptyState} from 'view/com/util/EmptyState' import {LoadingScreen} from 'view/com/util/LoadingScreen' -import {RichText} from 'view/com/util/text/RichText' +import {RichText} from '#/components/RichText' import {Button} from 'view/com/util/forms/Button' import {TextLink} from 'view/com/util/Link' import {ListRef} from 'view/com/util/List' @@ -60,6 +60,7 @@ import { import {logger} from '#/logger' import {useAnalytics} from '#/lib/analytics/analytics' import {listenSoftReset} from '#/state/events' +import {atoms as a} from '#/alf' const SECTION_TITLES_CURATE = ['Posts', 'About'] const SECTION_TITLES_MOD = ['About'] @@ -742,9 +743,8 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>( {descriptionRT ? ( <RichText testID="listDescription" - type="lg" - style={pal.text} - richText={descriptionRT} + style={[a.text_md]} + value={descriptionRT} /> ) : ( <Text diff --git a/src/view/screens/Settings/ExportCarDialog.tsx b/src/view/screens/Settings/ExportCarDialog.tsx index 8c028d4d3..ba8fad2df 100644 --- a/src/view/screens/Settings/ExportCarDialog.tsx +++ b/src/view/screens/Settings/ExportCarDialog.tsx @@ -76,7 +76,7 @@ export function ExportCarDialog({ This feature is in beta. You can read more about repository exports in{' '} <InlineLink - to="https://atproto.com/blog/repo-export" + to="https://docs.bsky.app/blog/repo-export" style={[a.text_sm]}> this blogpost </InlineLink> diff --git a/src/view/screens/Storybook/Dialogs.tsx b/src/view/screens/Storybook/Dialogs.tsx index db568c6bd..09be124db 100644 --- a/src/view/screens/Storybook/Dialogs.tsx +++ b/src/view/screens/Storybook/Dialogs.tsx @@ -9,7 +9,8 @@ import * as Prompt from '#/components/Prompt' import {useDialogStateControlContext} from '#/state/dialogs' export function Dialogs() { - const control = Dialog.useDialogControl() + const scrollable = Dialog.useDialogControl() + const basic = Dialog.useDialogControl() const prompt = Prompt.usePromptControl() const {closeAllDialogs} = useDialogStateControlContext() @@ -20,8 +21,31 @@ export function Dialogs() { color="secondary" size="small" onPress={() => { - control.open() + scrollable.open() prompt.open() + basic.open() + }} + label="Open basic dialog"> + Open all dialogs + </Button> + + <Button + variant="outline" + color="secondary" + size="small" + onPress={() => { + scrollable.open() + }} + label="Open basic dialog"> + Open scrollable dialog + </Button> + + <Button + variant="outline" + color="secondary" + size="small" + onPress={() => { + basic.open() }} label="Open basic dialog"> Open basic dialog @@ -48,9 +72,18 @@ export function Dialogs() { </Prompt.Actions> </Prompt.Outer> + <Dialog.Outer control={basic}> + <Dialog.Handle /> + + <Dialog.Inner label="test"> + <H3 nativeID="dialog-title">Dialog</H3> + <P nativeID="dialog-description">A basic dialog</P> + </Dialog.Inner> + </Dialog.Outer> + <Dialog.Outer - control={control} - nativeOptions={{sheet: {snapPoints: ['90%']}}}> + control={scrollable} + nativeOptions={{sheet: {snapPoints: ['100%']}}}> <Dialog.Handle /> <Dialog.ScrollableInner @@ -77,9 +110,13 @@ export function Dialogs() { variant="outline" color="primary" size="small" - onPress={() => control.close()} + onPress={() => + scrollable.close(() => { + console.log('CLOSED') + }) + } label="Open basic dialog"> - Close basic dialog + Close dialog </Button> </View> </View> diff --git a/src/view/screens/Storybook/Typography.tsx b/src/view/screens/Storybook/Typography.tsx index 5d3a96f4d..8ee4270b2 100644 --- a/src/view/screens/Storybook/Typography.tsx +++ b/src/view/screens/Storybook/Typography.tsx @@ -8,7 +8,9 @@ import {RichText} from '#/components/RichText' export function Typography() { return ( <View style={[a.gap_md]}> - <Text style={[a.text_5xl]}>atoms.text_5xl</Text> + <Text selectable style={[a.text_5xl]}> + atoms.text_5xl + </Text> <Text style={[a.text_4xl]}>atoms.text_4xl</Text> <Text style={[a.text_3xl]}>atoms.text_3xl</Text> <Text style={[a.text_2xl]}>atoms.text_2xl</Text> @@ -24,6 +26,7 @@ export function Typography() { value={`This is rich text. It can have mentions like @bsky.app or links like https://bsky.social`} /> <RichText + selectable resolveFacets value={`This is rich text. It can have mentions like @bsky.app or links like https://bsky.social`} style={[a.text_xl]} |