Merge branch 'bluesky-social:main' into patch-3

zio/stable
Minseo Lee 2024-02-22 09:37:09 +09:00 committed by GitHub
commit c2d87b8075
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
48 changed files with 667 additions and 581 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -11,24 +11,12 @@ const DARK_SPLASH_CONFIG = {
resizeMode: 'cover', resizeMode: 'cover',
} }
module.exports = function () { module.exports = function (config) {
/** /**
* App version number. Should be incremented as part of a release cycle. * App version number. Should be incremented as part of a release cycle.
*/ */
const VERSION = pkg.version 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 * Uses built-in Expo env vars
* *
@ -36,11 +24,10 @@ module.exports = function () {
*/ */
const PLATFORM = process.env.EAS_BUILD_PLATFORM const PLATFORM = process.env.EAS_BUILD_PLATFORM
/**
* Additional granularity for the `dist` field
*/
const DIST_BUILD_NUMBER = 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 { return {
expo: { expo: {
@ -57,7 +44,6 @@ module.exports = function () {
userInterfaceStyle: 'automatic', userInterfaceStyle: 'automatic',
splash: SPLASH_CONFIG, splash: SPLASH_CONFIG,
ios: { ios: {
buildNumber: IOS_BUILD_NUMBER,
supportsTablet: false, supportsTablet: false,
bundleIdentifier: 'xyz.blueskyweb.app', bundleIdentifier: 'xyz.blueskyweb.app',
config: { config: {
@ -85,7 +71,6 @@ module.exports = function () {
backgroundColor: '#ffffff', backgroundColor: '#ffffff',
}, },
android: { android: {
versionCode: ANDROID_VERSION_CODE,
icon: './assets/icon.png', icon: './assets/icon.png',
adaptiveIcon: { adaptiveIcon: {
foregroundImage: './assets/icon-android-foreground.png', foregroundImage: './assets/icon-android-foreground.png',

View File

@ -2,7 +2,6 @@
<html> <html>
<head> <head>
<meta charset="UTF-8"> <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="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, viewport-fit=cover">
<meta name="referrer" content="origin-when-cross-origin"> <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="32x32" href="/static/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.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"> <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="application-name" content="Bluesky">
<meta name="generator" content="bskyweb"> <meta name="generator" content="bskyweb">
<meta property="og:site_name" content="Bluesky Social" /> <meta property="og:site_name" content="Bluesky Social" />

View File

@ -1,7 +1,8 @@
{ {
"cli": { "cli": {
"version": ">= 3.8.1", "version": ">= 3.8.1",
"promptToConfigurePushNotifications": false "promptToConfigurePushNotifications": false,
"appVersionSource": "remote"
}, },
"build": { "build": {
"base": { "base": {
@ -28,7 +29,21 @@
"production": { "production": {
"extends": "base", "extends": "base",
"ios": { "ios": {
"resourceClass": "large" "resourceClass": "large",
"autoIncrement": true
},
"android": {
"autoIncrement": true
},
"channel": "production"
},
"github": {
"extends": "base",
"ios": {
"autoIncrement": true
},
"android": {
"autoIncrement": true
}, },
"channel": "production" "channel": "production"
} }

View File

@ -4,6 +4,7 @@
RCT_REMAP_SHADOW_PROPERTY(numberOfLines, numberOfLines, NSInteger) RCT_REMAP_SHADOW_PROPERTY(numberOfLines, numberOfLines, NSInteger)
RCT_REMAP_SHADOW_PROPERTY(allowsFontScaling, allowsFontScaling, BOOL) RCT_REMAP_SHADOW_PROPERTY(allowsFontScaling, allowsFontScaling, BOOL)
RCT_EXPORT_VIEW_PROPERTY(numberOfLines, NSInteger)
RCT_EXPORT_VIEW_PROPERTY(onTextLayout, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onTextLayout, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(ellipsizeMode, NSString) RCT_EXPORT_VIEW_PROPERTY(ellipsizeMode, NSString)
RCT_EXPORT_VIEW_PROPERTY(selectable, BOOL) RCT_EXPORT_VIEW_PROPERTY(selectable, BOOL)

View File

@ -40,19 +40,19 @@ class RNUITextViewShadow: RCTShadowView {
self.setAttributedText() 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 { override func isYogaLeafNode() -> Bool {
return true 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) { override func insertReactSubview(_ subview: RCTShadowView!, at atIndex: Int) {
if subview.isKind(of: RNUITextViewChildShadow.self) { if subview.isKind(of: RNUITextViewChildShadow.self) {
super.insertReactSubview(subview, at: atIndex) 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() { override func didUpdateReactSubviews() {
self.setAttributedText() self.setAttributedText()
} }
@ -64,7 +64,7 @@ class RNUITextViewShadow: RCTShadowView {
return 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 self.bridge.uiManager.addUIBlock { uiManager, viewRegistry in
guard let textView = viewRegistry?[self.reactTag] as? RNUITextView else { guard let textView = viewRegistry?[self.reactTag] as? RNUITextView else {
return return
@ -100,18 +100,25 @@ class RNUITextViewShadow: RCTShadowView {
// Create the attributed string with the generic attributes // Create the attributed string with the generic attributes
let string = NSMutableAttributedString(string: child.text, attributes: 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() let paragraphStyle = NSMutableParagraphStyle()
if child.lineHeight != 0.0 { if child.lineHeight != 0.0 {
paragraphStyle.minimumLineHeight = child.lineHeight // Whenever we change the line height for the text, we are also removing the DynamicType
paragraphStyle.maximumLineHeight = child.lineHeight // 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( string.addAttribute(
NSAttributedString.Key.paragraphStyle, NSAttributedString.Key.paragraphStyle,
value: paragraphStyle, value: paragraphStyle,
range: NSMakeRange(0, string.length) 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 self.lineHeight = child.lineHeight
} else { } else {
self.lineHeight = font.lineHeight self.lineHeight = font.lineHeight
@ -124,24 +131,22 @@ class RNUITextViewShadow: RCTShadowView {
self.dirtyLayout() 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 { 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 maxSize = CGSize(width: CGFloat(maxWidth), height: CGFloat(MAXFLOAT))
let textSize = self.attributedText.boundingRect(with: maxSize, options: .usesLineFragmentOrigin, context: nil) let textSize = self.attributedText.boundingRect(with: maxSize, options: .usesLineFragmentOrigin, context: nil)
// Figure out how many total lines there are var totalLines = Int(ceil(textSize.height / self.lineHeight))
let totalLines = Int(ceil(textSize.height / self.lineHeight))
// Default to the text size
var neededSize: CGSize = textSize.size
// If the total lines > max number, return size with the max
if self.numberOfLines != 0, totalLines > self.numberOfLines { 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 self.frameSize = CGSize(width: CGFloat(maxWidth), height: CGFloat(CGFloat(totalLines) * self.lineHeight))
return YGSize(width: Float(neededSize.width), height: Float(neededSize.height)) return YGSize(width: Float(self.frameSize.width), height: Float(self.frameSize.height))
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "bsky.app", "name": "bsky.app",
"version": "1.68.0", "version": "1.69.0",
"private": true, "private": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
@ -12,8 +12,12 @@
"android": "expo run:android", "android": "expo run:android",
"ios": "expo run:ios", "ios": "expo run:ios",
"web": "expo start --web", "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-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": "expo start --dev-client",
"start:prod": "expo start --dev-client --no-dev --minify", "start:prod": "expo start --dev-client --no-dev --minify",
"clean-cache": "rm -rf node_modules/.cache/babel-loader/*", "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: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:extract": "lingui extract",
"intl:compile": "lingui compile", "intl:compile": "lingui compile",
"nuke": "rm -rf ./node_modules && rm -rf ./ios && rm -rf ./android", "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"
}, },
"dependencies": { "dependencies": {
"@atproto/api": "^0.9.5", "@atproto/api": "^0.9.5",

View File

@ -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"

View File

@ -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"

View File

@ -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 $*"

View File

@ -176,43 +176,59 @@ export const atoms = {
}, },
text_2xs: { text_2xs: {
fontSize: tokens.fontSize._2xs, fontSize: tokens.fontSize._2xs,
letterSpacing: 0.25,
}, },
text_xs: { text_xs: {
fontSize: tokens.fontSize.xs, fontSize: tokens.fontSize.xs,
letterSpacing: 0.25,
}, },
text_sm: { text_sm: {
fontSize: tokens.fontSize.sm, fontSize: tokens.fontSize.sm,
letterSpacing: 0.25,
}, },
text_md: { text_md: {
fontSize: tokens.fontSize.md, fontSize: tokens.fontSize.md,
letterSpacing: 0.25,
}, },
text_lg: { text_lg: {
fontSize: tokens.fontSize.lg, fontSize: tokens.fontSize.lg,
letterSpacing: 0.25,
}, },
text_xl: { text_xl: {
fontSize: tokens.fontSize.xl, fontSize: tokens.fontSize.xl,
letterSpacing: 0.25,
}, },
text_2xl: { text_2xl: {
fontSize: tokens.fontSize._2xl, fontSize: tokens.fontSize._2xl,
letterSpacing: 0.25,
}, },
text_3xl: { text_3xl: {
fontSize: tokens.fontSize._3xl, fontSize: tokens.fontSize._3xl,
letterSpacing: 0.25,
}, },
text_4xl: { text_4xl: {
fontSize: tokens.fontSize._4xl, fontSize: tokens.fontSize._4xl,
letterSpacing: 0.25,
}, },
text_5xl: { text_5xl: {
fontSize: tokens.fontSize._5xl, fontSize: tokens.fontSize._5xl,
letterSpacing: 0.25,
}, },
leading_tight: { leading_tight: {
lineHeight: 1.15, lineHeight: 1.15,
}, },
leading_snug: { leading_snug: {
lineHeight: 1.25, lineHeight: 1.3,
}, },
leading_normal: { leading_normal: {
lineHeight: 1.5, lineHeight: 1.5,
}, },
tracking_normal: {
letterSpacing: 0,
},
tracking_wide: {
letterSpacing: 0.25,
},
font_normal: { font_normal: {
fontWeight: tokens.fontWeight.normal, fontWeight: tokens.fontWeight.normal,
}, },

View File

@ -1,7 +1,11 @@
import React from 'react' import React from 'react'
import {useDialogStateContext} from '#/state/dialogs' 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>({ export const Context = React.createContext<DialogContextProps>({
close: () => {}, close: () => {},
@ -11,7 +15,7 @@ export function useDialogContext() {
return React.useContext(Context) return React.useContext(Context)
} }
export function useDialogControl() { export function useDialogControl(): DialogOuterProps['control'] {
const id = React.useId() const id = React.useId()
const control = React.useRef<DialogControlProps>({ const control = React.useRef<DialogControlProps>({
open: () => {}, open: () => {},
@ -30,6 +34,6 @@ export function useDialogControl() {
return { return {
ref: control, ref: control,
open: () => control.current.open(), open: () => control.current.open(),
close: () => control.current.close(), close: cb => control.current.close(cb),
} }
} }

View File

@ -8,7 +8,7 @@ import BottomSheet, {
} from '@gorhom/bottom-sheet' } from '@gorhom/bottom-sheet'
import {useSafeAreaInsets} from 'react-native-safe-area-context' 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 {Portal} from '#/components/Portal'
import {createInput} from '#/components/forms/TextField' import {createInput} from '#/components/forms/TextField'
@ -35,12 +35,30 @@ export function Outer({
const sheetOptions = nativeOptions?.sheet || {} const sheetOptions = nativeOptions?.sheet || {}
const hasSnapPoints = !!sheetOptions.snapPoints const hasSnapPoints = !!sheetOptions.snapPoints
const insets = useSafeAreaInsets() 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)
const close = React.useCallback(() => { /*
* `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<DialogControlProps['close']>(cb => {
if (cb) {
closeCallback.current = cb
}
sheet.current?.close() sheet.current?.close()
}, []) }, [])
@ -56,78 +74,85 @@ export function Outer({
const onChange = React.useCallback( const onChange = React.useCallback(
(index: number) => { (index: number) => {
if (index === -1) { if (index === -1) {
closeCallback.current?.()
closeCallback.current = undefined
onClose?.() onClose?.()
setOpenIndex(-1)
} }
}, },
[onClose], [onClose, setOpenIndex],
) )
const context = React.useMemo(() => ({close}), [close]) const context = React.useMemo(() => ({close}), [close])
return ( return (
<Portal> isOpen && (
<BottomSheet <Portal>
enableDynamicSizing={!hasSnapPoints} <BottomSheet
enablePanDownToClose enableDynamicSizing={!hasSnapPoints}
keyboardBehavior="interactive" enablePanDownToClose
android_keyboardInputMode="adjustResize" keyboardBehavior="interactive"
keyboardBlurBehavior="restore" android_keyboardInputMode="adjustResize"
topInset={insets.top} keyboardBlurBehavior="restore"
{...sheetOptions} topInset={insets.top}
ref={sheet} {...sheetOptions}
index={-1} snapPoints={sheetOptions.snapPoints || ['100%']}
backgroundStyle={{backgroundColor: 'transparent'}} ref={sheet}
backdropComponent={props => ( index={openIndex}
<BottomSheetBackdrop backgroundStyle={{backgroundColor: 'transparent'}}
opacity={0.4} backdropComponent={props => (
appearsOnIndex={0} <BottomSheetBackdrop
disappearsOnIndex={-1} opacity={0.4}
{...props} appearsOnIndex={0}
/> disappearsOnIndex={-1}
)} {...props}
handleIndicatorStyle={{backgroundColor: t.palette.primary_500}} style={[flatten(props.style), t.atoms.bg_contrast_300]}
handleStyle={{display: 'none'}} />
onChange={onChange}> )}
<Context.Provider value={context}> handleIndicatorStyle={{backgroundColor: t.palette.primary_500}}
<View handleStyle={{display: 'none'}}
style={[ onChange={onChange}>
a.absolute, <Context.Provider value={context}>
a.inset_0, <View
t.atoms.bg, style={[
{ a.absolute,
borderTopLeftRadius: 40, a.inset_0,
borderTopRightRadius: 40, t.atoms.bg,
height: Dimensions.get('window').height * 2, {
}, borderTopLeftRadius: 40,
]} borderTopRightRadius: 40,
/> height: Dimensions.get('window').height * 2,
{children} },
</Context.Provider> ]}
</BottomSheet> />
</Portal> {children}
</Context.Provider>
</BottomSheet>
</Portal>
)
) )
} }
// TODO a11y props here, or is that handled by the sheet? export function Inner({children, style}: DialogInnerProps) {
export function Inner(props: DialogInnerProps) {
const insets = useSafeAreaInsets() const insets = useSafeAreaInsets()
return ( return (
<BottomSheetView <BottomSheetView
style={[ style={[
a.p_lg, a.p_xl,
{ {
paddingTop: 40, paddingTop: 40,
borderTopLeftRadius: 40, borderTopLeftRadius: 40,
borderTopRightRadius: 40, borderTopRightRadius: 40,
paddingBottom: insets.bottom + a.pb_5xl.paddingBottom, paddingBottom: insets.bottom + a.pb_5xl.paddingBottom,
}, },
flatten(style),
]}> ]}>
{props.children} {children}
</BottomSheetView> </BottomSheetView>
) )
} }
export function ScrollableInner(props: DialogInnerProps) { export function ScrollableInner({children, style}: DialogInnerProps) {
const insets = useSafeAreaInsets() const insets = useSafeAreaInsets()
return ( return (
<BottomSheetScrollView <BottomSheetScrollView
@ -136,13 +161,15 @@ export function ScrollableInner(props: DialogInnerProps) {
style={[ style={[
a.flex_1, // main diff is this a.flex_1, // main diff is this
a.p_xl, a.p_xl,
a.h_full,
{ {
paddingTop: 40, paddingTop: 40,
borderTopLeftRadius: 40, borderTopLeftRadius: 40,
borderTopRightRadius: 40, borderTopRightRadius: 40,
}, },
flatten(style),
]}> ]}>
{props.children} {children}
<View style={{height: insets.bottom + a.pt_5xl.paddingTop}} /> <View style={{height: insets.bottom + a.pt_5xl.paddingTop}} />
</BottomSheetScrollView> </BottomSheetScrollView>
) )

View File

@ -5,11 +5,13 @@ import Animated, {FadeInDown, FadeIn} from 'react-native-reanimated'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' 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 {Portal} from '#/components/Portal'
import {DialogOuterProps, DialogInnerProps} from '#/components/Dialog/types' import {DialogOuterProps, DialogInnerProps} from '#/components/Dialog/types'
import {Context} from '#/components/Dialog/context' 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 {useDialogControl, useDialogContext} from '#/components/Dialog/context'
export * from '#/components/Dialog/types' export * from '#/components/Dialog/types'
@ -18,9 +20,9 @@ export {Input} from '#/components/forms/TextField'
const stopPropagation = (e: any) => e.stopPropagation() const stopPropagation = (e: any) => e.stopPropagation()
export function Outer({ export function Outer({
children,
control, control,
onClose, onClose,
children,
}: React.PropsWithChildren<DialogOuterProps>) { }: React.PropsWithChildren<DialogOuterProps>) {
const {_} = useLingui() const {_} = useLingui()
const t = useTheme() const t = useTheme()
@ -147,7 +149,7 @@ export function Inner({
a.rounded_md, a.rounded_md,
a.w_full, a.w_full,
a.border, a.border,
gtMobile ? a.p_xl : a.p_lg, gtMobile ? a.p_2xl : a.p_xl,
t.atoms.bg, t.atoms.bg,
{ {
maxWidth: 600, maxWidth: 600,
@ -156,7 +158,7 @@ export function Inner({
shadowOpacity: t.name === 'light' ? 0.1 : 0.4, shadowOpacity: t.name === 'light' ? 0.1 : 0.4,
shadowRadius: 30, shadowRadius: 30,
}, },
...(Array.isArray(style) ? style : [style || {}]), flatten(style),
]}> ]}>
{children} {children}
</Animated.View> </Animated.View>
@ -170,25 +172,28 @@ export function Handle() {
return null return null
} }
/** export function Close() {
* TODO(eric) unused rn const {_} = useLingui()
*/ const {close} = React.useContext(Context)
// export function Close() { return (
// const {_} = useLingui() <View
// const t = useTheme() style={[
// const {close} = useDialogContext() a.absolute,
// return ( a.z_10,
// <View {
// style={[ top: a.pt_md.paddingTop,
// a.absolute, right: a.pr_md.paddingRight,
// a.z_10, },
// { ]}>
// top: a.pt_lg.paddingTop, <Button
// right: a.pr_lg.paddingRight, size="small"
// }, variant="ghost"
// ]}> color="primary"
// <Button onPress={close} label={_(msg`Close active dialog`)}> shape="round"
// </Button> onPress={close}
// </View> label={_(msg`Close active dialog`)}>
// ) <ButtonIcon icon={X} size="md" />
// } </Button>
</View>
)
}

View File

@ -1,24 +1,34 @@
import React from 'react' import React from 'react'
import type {ViewStyle, AccessibilityProps} from 'react-native' import type {AccessibilityProps} from 'react-native'
import {BottomSheetProps} from '@gorhom/bottom-sheet' import {BottomSheetProps} from '@gorhom/bottom-sheet'
import {ViewStyleProp} from '#/alf'
type A11yProps = Required<AccessibilityProps> type A11yProps = Required<AccessibilityProps>
export type DialogContextProps = { export type DialogContextProps = {
close: () => void 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 = { export type DialogControlProps = {
open: (index?: number) => void open: (options?: DialogControlOpenOptions) => void
close: () => void close: (callback?: () => void) => void
} }
export type DialogOuterProps = { export type DialogOuterProps = {
control: { control: {
ref: React.RefObject<DialogControlProps> ref: React.RefObject<DialogControlProps>
open: (index?: number) => void } & DialogControlProps
close: () => void
}
onClose?: () => void onClose?: () => void
nativeOptions?: { nativeOptions?: {
sheet?: Omit<BottomSheetProps, 'children'> sheet?: Omit<BottomSheetProps, 'children'>
@ -26,10 +36,7 @@ export type DialogOuterProps = {
webOptions?: {} webOptions?: {}
} }
type DialogInnerPropsBase<T> = React.PropsWithChildren<{ type DialogInnerPropsBase<T> = React.PropsWithChildren<ViewStyleProp> & T
style?: ViewStyle
}> &
T
export type DialogInnerProps = export type DialogInnerProps =
| DialogInnerPropsBase<{ | DialogInnerPropsBase<{
label?: undefined label?: undefined

View File

@ -1,9 +1,5 @@
import React from 'react' import React from 'react'
import { import {GestureResponderEvent, Linking} from 'react-native'
GestureResponderEvent,
Linking,
TouchableWithoutFeedback,
} from 'react-native'
import { import {
useLinkProps, useLinkProps,
useNavigation, useNavigation,
@ -23,7 +19,7 @@ import {
} from '#/lib/strings/url-helpers' } from '#/lib/strings/url-helpers'
import {useModalControls} from '#/state/modals' import {useModalControls} from '#/state/modals'
import {router} from '#/routes' 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`. * Only available within a `Link`, since that inherits from `Button`.
@ -55,11 +51,12 @@ type BaseLinkProps = Pick<
warnOnMismatchingTextChild?: boolean 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. * 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. * Web-only attribute. Sets `download` attr on web.
@ -86,7 +83,9 @@ export function useLink({
const onPress = React.useCallback( const onPress = React.useCallback(
(e: GestureResponderEvent) => { (e: GestureResponderEvent) => {
outerOnPress?.(e) const exitEarlyIfFalse = outerOnPress?.(e)
if (exitEarlyIfFalse === false) return
const requiresWarning = Boolean( const requiresWarning = Boolean(
warnOnMismatchingTextChild && warnOnMismatchingTextChild &&
@ -217,7 +216,7 @@ export function Link({
} }
export type InlineLinkProps = React.PropsWithChildren< export type InlineLinkProps = React.PropsWithChildren<
BaseLinkProps & TextStyleProp BaseLinkProps & TextStyleProp & Pick<TextProps, 'selectable'>
> >
export function InlineLink({ export function InlineLink({
@ -228,6 +227,7 @@ export function InlineLink({
style, style,
onPress: outerOnPress, onPress: outerOnPress,
download, download,
selectable,
...rest ...rest
}: InlineLinkProps) { }: InlineLinkProps) {
const t = useTheme() const t = useTheme()
@ -253,43 +253,41 @@ export function InlineLink({
const flattenedStyle = flatten(style) const flattenedStyle = flatten(style)
return ( return (
<TouchableWithoutFeedback <Text
accessibilityRole="button" 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} onPress={download ? undefined : onPress}
onPressIn={onPressIn} onPressIn={onPressIn}
onPressOut={onPressOut} onPressOut={onPressOut}
onFocus={onFocus} onFocus={onFocus}
onBlur={onBlur}> onBlur={onBlur}
<Text onMouseEnter={onHoverIn}
label={href} onMouseLeave={onHoverOut}
{...rest} accessibilityRole="link"
style={[ href={href}
{color: t.palette.primary_500}, {...web({
(hovered || focused || pressed) && { hrefAttrs: {
outline: 0, target: download ? undefined : isExternal ? 'blank' : undefined,
textDecorationLine: 'underline', rel: isExternal ? 'noopener noreferrer' : undefined,
textDecorationColor: flattenedStyle.color ?? t.palette.primary_500, download,
}, },
flattenedStyle, dataSet: {
]} // default to no underline, apply this ourselves
role="link" noUnderline: '1',
onMouseEnter={onHoverIn} },
onMouseLeave={onHoverOut} })}>
accessibilityRole="link" {children}
href={href} </Text>
{...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>
) )
} }

View File

@ -41,7 +41,7 @@ export function Outer({
<Dialog.Inner <Dialog.Inner
accessibilityLabelledBy={titleId} accessibilityLabelledBy={titleId}
accessibilityDescribedBy={descriptionId} accessibilityDescribedBy={descriptionId}
style={{width: 'auto', maxWidth: 400}}> style={[{width: 'auto', maxWidth: 400}]}>
{children} {children}
</Dialog.Inner> </Dialog.Inner>
</Context.Provider> </Context.Provider>

View File

@ -1,9 +1,9 @@
import React from 'react' import React from 'react'
import {RichText as RichTextAPI, AppBskyRichtextFacet} from '@atproto/api' 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 {InlineLink} from '#/components/Link'
import {Text} from '#/components/Typography' import {Text, TextProps} from '#/components/Typography'
import {toShortUrl} from 'lib/strings/url-helpers' import {toShortUrl} from 'lib/strings/url-helpers'
import {getAgent} from '#/state/session' import {getAgent} from '#/state/session'
@ -16,18 +16,20 @@ export function RichText({
numberOfLines, numberOfLines,
disableLinks, disableLinks,
resolveFacets = false, resolveFacets = false,
}: TextStyleProp & { selectable,
value: RichTextAPI | string }: TextStyleProp &
testID?: string Pick<TextProps, 'selectable'> & {
numberOfLines?: number value: RichTextAPI | string
disableLinks?: boolean testID?: string
resolveFacets?: boolean numberOfLines?: number
}) { disableLinks?: boolean
resolveFacets?: boolean
}) {
const detected = React.useRef(false) const detected = React.useRef(false)
const [richText, setRichText] = React.useState<RichTextAPI>(() => const [richText, setRichText] = React.useState<RichTextAPI>(() =>
value instanceof RichTextAPI ? value : new RichTextAPI({text: value}), value instanceof RichTextAPI ? value : new RichTextAPI({text: value}),
) )
const styles = [a.leading_normal, style] const styles = [a.leading_snug, flatten(style)]
React.useEffect(() => { React.useEffect(() => {
if (!resolveFacets) return if (!resolveFacets) return
@ -50,6 +52,7 @@ export function RichText({
if (text.length <= 5 && /^\p{Extended_Pictographic}+$/u.test(text)) { if (text.length <= 5 && /^\p{Extended_Pictographic}+$/u.test(text)) {
return ( return (
<Text <Text
selectable={selectable}
testID={testID} testID={testID}
style={[ style={[
{ {
@ -65,6 +68,7 @@ export function RichText({
} }
return ( return (
<Text <Text
selectable={selectable}
testID={testID} testID={testID}
style={styles} style={styles}
numberOfLines={numberOfLines} numberOfLines={numberOfLines}
@ -88,6 +92,7 @@ export function RichText({
) { ) {
els.push( els.push(
<InlineLink <InlineLink
selectable={selectable}
key={key} key={key}
to={`/profile/${mention.did}`} to={`/profile/${mention.did}`}
style={[...styles, {pointerEvents: 'auto'}]} style={[...styles, {pointerEvents: 'auto'}]}
@ -102,6 +107,7 @@ export function RichText({
} else { } else {
els.push( els.push(
<InlineLink <InlineLink
selectable={selectable}
key={key} key={key}
to={link.uri} to={link.uri}
style={[...styles, {pointerEvents: 'auto'}]} style={[...styles, {pointerEvents: 'auto'}]}
@ -120,6 +126,7 @@ export function RichText({
return ( return (
<Text <Text
selectable={selectable}
testID={testID} testID={testID}
style={styles} style={styles}
numberOfLines={numberOfLines} numberOfLines={numberOfLines}

View File

@ -1,7 +1,16 @@
import React from 'react' 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 {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 * 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. * 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 t = useTheme()
const s = normalizeTextStyles([atoms.text_sm, t.atoms.text, flatten(style)]) 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}) { export function createHeadingElement({level}: {level: number}) {
return function HeadingElement({style, ...rest}: TextProps) { return function HeadingElement({style, ...rest}: TextProps) {
const t = useTheme()
const attr = const attr =
web({ web({
role: 'heading', role: 'heading',
'aria-level': level, 'aria-level': level,
}) || {} }) || {}
return ( return <Text {...attr} {...rest} style={style} />
<RNText
{...attr}
{...rest}
style={normalizeTextStyles([t.atoms.text, flatten(style)])}
/>
)
} }
} }
@ -78,21 +84,15 @@ export const H4 = createHeadingElement({level: 4})
export const H5 = createHeadingElement({level: 5}) export const H5 = createHeadingElement({level: 5})
export const H6 = createHeadingElement({level: 6}) export const H6 = createHeadingElement({level: 6})
export function P({style, ...rest}: TextProps) { export function P({style, ...rest}: TextProps) {
const t = useTheme()
const attr = const attr =
web({ web({
role: 'paragraph', role: 'paragraph',
}) || {} }) || {}
return ( return (
<RNText <Text
{...attr} {...attr}
{...rest} {...rest}
style={normalizeTextStyles([ style={[atoms.text_md, atoms.leading_normal, flatten(style)]}
atoms.text_md,
atoms.leading_normal,
t.atoms.text,
flatten(style),
])}
/> />
) )
} }

View File

@ -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',
})

View File

@ -3,9 +3,8 @@ import {Insets, Platform} from 'react-native'
export const LOCAL_DEV_SERVICE = export const LOCAL_DEV_SERVICE =
Platform.OS === 'android' ? 'http://10.0.2.2:2583' : 'http://localhost:2583' Platform.OS === 'android' ? 'http://10.0.2.2:2583' : 'http://localhost:2583'
export const STAGING_SERVICE = 'https://staging.bsky.dev' export const STAGING_SERVICE = 'https://staging.bsky.dev'
export const PROD_SERVICE = 'https://bsky.social' export const BSKY_SERVICE = 'https://bsky.social'
export const DEFAULT_SERVICE = PROD_SERVICE export const DEFAULT_SERVICE = BSKY_SERVICE
const HELP_DESK_LANG = 'en-us' const HELP_DESK_LANG = 'en-us'
export const HELP_DESK_URL = `https://blueskyweb.zendesk.com/hc/${HELP_DESK_LANG}` 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 // but increasing limit per user feedback
export const MAX_ALT_TEXT = 1000 export const MAX_ALT_TEXT = 1000
export function IS_LOCAL_DEV(url: string) { export function IS_PROD_SERVICE(url?: string) {
return url.includes('localhost') return url && url !== STAGING_SERVICE && url !== LOCAL_DEV_SERVICE
} }
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 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) => export const PROD_DEFAULT_FEED = (rkey: string) =>
`at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/${rkey}` `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 = { export const POST_IMG_MAX = {
width: 2000, 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 const PROD_LINK_META_PROXY = 'https://cardyb.bsky.app/v1/extract?url='
export function LINK_META_PROXY(serviceUrl: string) { export function LINK_META_PROXY(serviceUrl: string) {
if (IS_LOCAL_DEV(serviceUrl)) { if (IS_PROD_SERVICE(serviceUrl)) {
return STAGING_LINK_META_PROXY
} else if (IS_STAGING(serviceUrl)) {
return STAGING_LINK_META_PROXY
} else {
return PROD_LINK_META_PROXY return PROD_LINK_META_PROXY
} }
return STAGING_LINK_META_PROXY
} }
export const STATUS_PAGE_URL = 'https://status.bsky.app/' export const STATUS_PAGE_URL = 'https://status.bsky.app/'

View File

@ -343,45 +343,45 @@ export function parseEmbedPlayerFromUrl(
} }
} }
export function getPlayerHeight({ export function getPlayerAspect({
type, type,
width,
hasThumb, hasThumb,
width,
}: { }: {
type: EmbedPlayerParams['type'] type: EmbedPlayerParams['type']
width: number
hasThumb: boolean hasThumb: boolean
}) { width: number
if (!hasThumb) return (width / 16) * 9 }): {aspectRatio?: number; height?: number} {
if (!hasThumb) return {aspectRatio: 16 / 9}
switch (type) { switch (type) {
case 'youtube_video': case 'youtube_video':
case 'twitch_video': case 'twitch_video':
case 'vimeo_video': case 'vimeo_video':
return (width / 16) * 9 return {aspectRatio: 16 / 9}
case 'youtube_short': case 'youtube_short':
if (SCREEN_HEIGHT < 600) { if (SCREEN_HEIGHT < 600) {
return ((width / 9) * 16) / 1.75 return {aspectRatio: (9 / 16) * 1.75}
} else { } else {
return ((width / 9) * 16) / 1.5 return {aspectRatio: (9 / 16) * 1.5}
} }
case 'spotify_album': case 'spotify_album':
case 'apple_music_album': case 'apple_music_album':
case 'apple_music_playlist': case 'apple_music_playlist':
case 'spotify_playlist': case 'spotify_playlist':
case 'soundcloud_set': case 'soundcloud_set':
return 380 return {height: 380}
case 'spotify_song': case 'spotify_song':
if (width <= 300) { if (width <= 300) {
return 155 return {height: 155}
} }
return 232 return {height: 232}
case 'soundcloud_track': case 'soundcloud_track':
return 165 return {height: 165}
case 'apple_music_song': case 'apple_music_song':
return 150 return {height: 150}
default: default:
return width return {aspectRatio: 16 / 9}
} }
} }

View File

@ -1,5 +1,5 @@
import {AtUri} from '@atproto/api' import {AtUri} from '@atproto/api'
import {PROD_SERVICE} from 'lib/constants' import {BSKY_SERVICE} from 'lib/constants'
import TLDs from 'tlds' import TLDs from 'tlds'
import psl from 'psl' import psl from 'psl'
@ -28,7 +28,7 @@ export function makeRecordUri(
export function toNiceDomain(url: string): string { export function toNiceDomain(url: string): string {
try { try {
const urlp = new URL(url) const urlp = new URL(url)
if (`https://${urlp.host}` === PROD_SERVICE) { if (`https://${urlp.host}` === BSKY_SERVICE) {
return 'Bluesky Social' return 'Bluesky Social'
} }
return urlp.host ? urlp.host : url return urlp.host ? urlp.host : url

View File

@ -3,7 +3,6 @@ import {View} from 'react-native'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {IS_PROD} from '#/env'
import {atoms as a} from '#/alf' import {atoms as a} from '#/alf'
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron' import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
import {ListMagnifyingGlass_Stroke2_Corner0_Rounded as ListMagnifyingGlass} from '#/components/icons/ListMagnifyingGlass' import {ListMagnifyingGlass_Stroke2_Corner0_Rounded as ListMagnifyingGlass} from '#/components/icons/ListMagnifyingGlass'
@ -22,21 +21,28 @@ import {
import {FeedCard} from '#/screens/Onboarding/StepAlgoFeeds/FeedCard' import {FeedCard} from '#/screens/Onboarding/StepAlgoFeeds/FeedCard'
import {aggregateInterestItems} from '#/screens/Onboarding/util' import {aggregateInterestItems} from '#/screens/Onboarding/util'
import {IconCircle} from '#/components/IconCircle' import {IconCircle} from '#/components/IconCircle'
import {IS_PROD_SERVICE} from 'lib/constants'
import {useSession} from 'state/session'
export function StepTopicalFeeds() { export function StepTopicalFeeds() {
const {_} = useLingui() const {_} = useLingui()
const {track} = useAnalytics() const {track} = useAnalytics()
const {currentAccount} = useSession()
const {state, dispatch, interestsDisplayNames} = React.useContext(Context) const {state, dispatch, interestsDisplayNames} = React.useContext(Context)
const [selectedFeedUris, setSelectedFeedUris] = React.useState<string[]>([]) const [selectedFeedUris, setSelectedFeedUris] = React.useState<string[]>([])
const [saving, setSaving] = React.useState(false) const [saving, setSaving] = React.useState(false)
const suggestedFeedUris = React.useMemo(() => { const suggestedFeedUris = React.useMemo(() => {
if (!IS_PROD) return [] if (!IS_PROD_SERVICE(currentAccount?.service)) return []
return aggregateInterestItems( return aggregateInterestItems(
state.interestsStepResults.selectedInterests, state.interestsStepResults.selectedInterests,
state.interestsStepResults.apiResponse.suggestedFeedUris, state.interestsStepResults.apiResponse.suggestedFeedUris,
state.interestsStepResults.apiResponse.suggestedFeedUris.default, state.interestsStepResults.apiResponse.suggestedFeedUris.default,
).slice(0, 10) ).slice(0, 10)
}, [state.interestsStepResults]) }, [
currentAccount?.service,
state.interestsStepResults.apiResponse.suggestedFeedUris,
state.interestsStepResults.selectedInterests,
])
const interestsText = React.useMemo(() => { const interestsText = React.useMemo(() => {
const i = state.interestsStepResults.selectedInterests.map( const i = state.interestsStepResults.selectedInterests.map(

View File

@ -133,23 +133,6 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
return query 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( export function* findAllPostsInQueryData(
queryClient: QueryClient, queryClient: QueryClient,
uri: string, uri: string,

View File

@ -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( export function* findAllPostsInQueryData(
queryClient: QueryClient, queryClient: QueryClient,
uri: string, uri: string,

View File

@ -8,8 +8,8 @@ import {useQuery, useQueryClient, QueryClient} from '@tanstack/react-query'
import {getAgent} from '#/state/session' import {getAgent} from '#/state/session'
import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types' import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
import {findPostInQueryData as findPostInFeedQueryData} from './post-feed' import {findAllPostsInQueryData as findAllPostsInFeedQueryData} from './post-feed'
import {findPostInQueryData as findPostInNotifsQueryData} from './notifications/feed' import {findAllPostsInQueryData as findAllPostsInNotifsQueryData} from './notifications/feed'
import {precacheThreadPostProfiles} from './profile' import {precacheThreadPostProfiles} from './profile'
import {getEmbeddedPost} from './util' import {getEmbeddedPost} from './util'
@ -82,21 +82,9 @@ export function usePostThreadQuery(uri: string | undefined) {
return undefined return undefined
} }
{ {
const item = findPostInQueryData(queryClient, uri) const post = findPostInQueryData(queryClient, uri)
if (item) { if (post) {
return threadNodeToPlaceholderThread(item) return post
}
}
{
const item = findPostInFeedQueryData(queryClient, uri)
if (item) {
return postViewToPlaceholderThread(item)
}
}
{
const item = findPostInNotifsQueryData(queryClient, uri)
if (item) {
return postViewToPlaceholderThread(item)
} }
} }
return undefined return undefined
@ -171,11 +159,18 @@ function responseToThreadNodes(
AppBskyFeedPost.isRecord(node.post.record) && AppBskyFeedPost.isRecord(node.post.record) &&
AppBskyFeedPost.validateRecord(node.post.record).success 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 { return {
type: 'post', type: 'post',
_reactKey: node.post.uri, _reactKey: node.post.uri,
uri: node.post.uri, uri: node.post.uri,
post: node.post, post: post,
record: node.post.record, record: node.post.record,
parent: parent:
node.parent && direction !== 'down' node.parent && direction !== 'down'
@ -213,14 +208,24 @@ function responseToThreadNodes(
function findPostInQueryData( function findPostInQueryData(
queryClient: QueryClient, queryClient: QueryClient,
uri: string, uri: string,
): ThreadNode | undefined { ): ThreadNode | void {
const generator = findAllPostsInQueryData(queryClient, uri) let partial
const result = generator.next() for (let item of findAllPostsInQueryData(queryClient, uri)) {
if (result.done) { if (item.type === 'post') {
return undefined // Currently, the backend doesn't send full post info in some cases
} else { // (for example, for quoted posts). We use missing `likeCount`
return result.value // 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( export function* findAllPostsInQueryData(
@ -236,7 +241,10 @@ export function* findAllPostsInQueryData(
} }
for (const item of traverseThread(queryData)) { for (const item of traverseThread(queryData)) {
if (item.uri === uri) { if (item.uri === uri) {
yield item const placeholder = threadNodeToPlaceholderThread(item)
if (placeholder) {
yield placeholder
}
} }
const quotedPost = const quotedPost =
item.type === 'post' ? getEmbeddedPost(item.post.embed) : undefined 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> { function* traverseThread(node: ThreadNode): Generator<ThreadNode, void> {

View File

@ -53,5 +53,6 @@ export function embedViewRecordToPostView(
record: v.value, record: v.value,
indexedAt: v.indexedAt, indexedAt: v.indexedAt,
labels: v.labels, labels: v.labels,
embed: v.embeds?.[0],
} }
} }

View File

@ -12,7 +12,7 @@ import {createFullHandle} from '#/lib/strings/handles'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
import {useOnboardingDispatch} from '#/state/shell/onboarding' import {useOnboardingDispatch} from '#/state/shell/onboarding'
import {useSessionApi} from '#/state/session' import {useSessionApi} from '#/state/session'
import {DEFAULT_SERVICE, IS_PROD} from '#/lib/constants' import {DEFAULT_SERVICE, IS_PROD_SERVICE} from '#/lib/constants'
import { import {
DEFAULT_PROD_FEEDS, DEFAULT_PROD_FEEDS,
usePreferencesSetBirthDateMutation, usePreferencesSetBirthDateMutation,
@ -147,7 +147,7 @@ export function useSubmitCreateAccount(
: undefined, : undefined,
}) })
setBirthDate({birthDate: uiState.birthDate}) setBirthDate({birthDate: uiState.birthDate})
if (IS_PROD(uiState.serviceUrl)) { if (IS_PROD_SERVICE(uiState.serviceUrl)) {
setSavedFeeds(DEFAULT_PROD_FEEDS) setSavedFeeds(DEFAULT_PROD_FEEDS)
} }
} catch (e: any) { } catch (e: any) {

View File

@ -2,7 +2,7 @@ import React from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {Trans, msg} from '@lingui/macro' 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 * as persisted from '#/state/persisted'
import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {atoms as a, useBreakpoints, useTheme} from '#/alf'
@ -26,7 +26,7 @@ export function ServerInputDialog({
const [pdsAddressHistory, setPdsAddressHistory] = React.useState<string[]>( const [pdsAddressHistory, setPdsAddressHistory] = React.useState<string[]>(
persisted.get('pdsAddressHistory') || [], persisted.get('pdsAddressHistory') || [],
) )
const [fixedOption, setFixedOption] = React.useState([PROD_SERVICE]) const [fixedOption, setFixedOption] = React.useState([BSKY_SERVICE])
const [customAddress, setCustomAddress] = React.useState('') const [customAddress, setCustomAddress] = React.useState('')
const onClose = React.useCallback(() => { const onClose = React.useCallback(() => {
@ -86,7 +86,7 @@ export function ServerInputDialog({
label="Preferences" label="Preferences"
values={fixedOption} values={fixedOption}
onChange={setFixedOption}> onChange={setFixedOption}>
<ToggleButton.Button name={PROD_SERVICE} label={_(msg`Bluesky`)}> <ToggleButton.Button name={BSKY_SERVICE} label={_(msg`Bluesky`)}>
{_(msg`Bluesky`)} {_(msg`Bluesky`)}
</ToggleButton.Button> </ToggleButton.Button>
<ToggleButton.Button <ToggleButton.Button

View File

@ -2,7 +2,7 @@ import React from 'react'
import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native' import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import {RichText} from '../util/text/RichText' import {RichText} from '#/components/RichText'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {UserAvatar} from '../util/UserAvatar' import {UserAvatar} from '../util/UserAvatar'
@ -25,6 +25,7 @@ import {
} from '#/state/queries/preferences' } from '#/state/queries/preferences'
import {useFeedSourceInfoQuery, FeedSourceInfo} from '#/state/queries/feed' import {useFeedSourceInfoQuery, FeedSourceInfo} from '#/state/queries/feed'
import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {useTheme} from '#/alf'
export function FeedSourceCard({ export function FeedSourceCard({
feedUri, feedUri,
@ -82,6 +83,7 @@ export function FeedSourceCardLoaded({
pinOnSave?: boolean pinOnSave?: boolean
showMinimalPlaceholder?: boolean showMinimalPlaceholder?: boolean
}) { }) {
const t = useTheme()
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui() const {_} = useLingui()
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
@ -266,8 +268,8 @@ export function FeedSourceCardLoaded({
{showDescription && feed.description ? ( {showDescription && feed.description ? (
<RichText <RichText
style={[pal.textLight, styles.description]} style={[t.atoms.text_contrast_high, styles.description]}
richText={feed.description} value={feed.description}
numberOfLines={3} numberOfLines={3}
/> />
) : null} ) : null}

View File

@ -3,7 +3,7 @@ import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import {AtUri, AppBskyGraphDefs, RichText} from '@atproto/api' import {AtUri, AppBskyGraphDefs, RichText} from '@atproto/api'
import {Link} from '../util/Link' import {Link} from '../util/Link'
import {Text} from '../util/text/Text' 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 {UserAvatar} from '../util/UserAvatar'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
@ -12,6 +12,7 @@ import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles' import {sanitizeHandle} from 'lib/strings/handles'
import {makeProfileLink} from 'lib/routes/links' import {makeProfileLink} from 'lib/routes/links'
import {Trans} from '@lingui/macro' import {Trans} from '@lingui/macro'
import {atoms as a} from '#/alf'
export const ListCard = ({ export const ListCard = ({
testID, testID,
@ -119,9 +120,9 @@ export const ListCard = ({
{descriptionRichText ? ( {descriptionRichText ? (
<View style={styles.details}> <View style={styles.details}>
<RichTextCom <RichTextCom
style={[pal.text, s.flex1]} style={[a.flex_1]}
numberOfLines={20} numberOfLines={20}
richText={descriptionRichText} value={descriptionRichText}
/> />
</View> </View>
) : undefined} ) : undefined}

View File

@ -11,7 +11,7 @@ import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {PostThreadFollowBtn} from 'view/com/post-thread/PostThreadFollowBtn' import {PostThreadFollowBtn} from 'view/com/post-thread/PostThreadFollowBtn'
import {Link, TextLink} from '../util/Link' import {Link, TextLink} from '../util/Link'
import {RichText} from '../util/text/RichText' import {RichText} from '#/components/RichText'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import {PreviewableUserAvatar} from '../util/UserAvatar' import {PreviewableUserAvatar} from '../util/UserAvatar'
import {s} from 'lib/styles' import {s} from 'lib/styles'
@ -44,6 +44,7 @@ import {ThreadPost} from '#/state/queries/post-thread'
import {useSession} from 'state/session' import {useSession} from 'state/session'
import {WhoCanReply} from '../threadgate/WhoCanReply' import {WhoCanReply} from '../threadgate/WhoCanReply'
import {LoadingPlaceholder} from '../util/LoadingPlaceholder' import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
import {atoms as a} from '#/alf'
export function PostThreadItem({ export function PostThreadItem({
post, post,
@ -326,10 +327,8 @@ let PostThreadItemLoaded = ({
styles.postTextLargeContainer, styles.postTextLargeContainer,
]}> ]}>
<RichText <RichText
type="post-text-lg" value={richText}
richText={richText} style={[a.flex_1, a.text_xl]}
lineHeight={1.3}
style={s.flex1}
selectable selectable
/> />
</View> </View>
@ -522,10 +521,8 @@ let PostThreadItemLoaded = ({
{richText?.text ? ( {richText?.text ? (
<View style={styles.postTextContainer}> <View style={styles.postTextContainer}>
<RichText <RichText
type="post-text" value={richText}
richText={richText} style={[a.flex_1, a.text_md]}
style={[pal.text, s.flex1]}
lineHeight={1.3}
numberOfLines={limitLines ? MAX_POST_LINES : undefined} numberOfLines={limitLines ? MAX_POST_LINES : undefined}
/> />
</View> </View>

View File

@ -17,7 +17,7 @@ import {PostCtrls} from '../util/post-ctrls/PostCtrls'
import {ContentHider} from '../util/moderation/ContentHider' import {ContentHider} from '../util/moderation/ContentHider'
import {PostAlerts} from '../util/moderation/PostAlerts' import {PostAlerts} from '../util/moderation/PostAlerts'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import {RichText} from '../util/text/RichText' import {RichText} from '#/components/RichText'
import {PreviewableUserAvatar} from '../util/UserAvatar' import {PreviewableUserAvatar} from '../util/UserAvatar'
import {s, colors} from 'lib/styles' import {s, colors} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette' 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 {Shadow, usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {atoms as a} from '#/alf'
export function Post({ export function Post({
post, post,
@ -184,11 +185,9 @@ function PostInner({
<View style={styles.postTextContainer}> <View style={styles.postTextContainer}>
<RichText <RichText
testID="postText" testID="postText"
type="post-text" value={richText}
richText={richText}
lineHeight={1.3}
numberOfLines={limitLines ? MAX_POST_LINES : undefined} numberOfLines={limitLines ? MAX_POST_LINES : undefined}
style={s.flex1} style={[a.flex_1, a.text_md]}
/> />
</View> </View>
) : undefined} ) : undefined}

View File

@ -20,7 +20,7 @@ import {PostCtrls} from '../util/post-ctrls/PostCtrls'
import {PostEmbeds} from '../util/post-embeds' import {PostEmbeds} from '../util/post-embeds'
import {ContentHider} from '../util/moderation/ContentHider' import {ContentHider} from '../util/moderation/ContentHider'
import {PostAlerts} from '../util/moderation/PostAlerts' import {PostAlerts} from '../util/moderation/PostAlerts'
import {RichText} from '../util/text/RichText' import {RichText} from '#/components/RichText'
import {PreviewableUserAvatar} from '../util/UserAvatar' import {PreviewableUserAvatar} from '../util/UserAvatar'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
@ -36,6 +36,7 @@ import {FeedNameText} from '../util/FeedInfoText'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {atoms as a} from '#/alf'
export function FeedItem({ export function FeedItem({
post, post,
@ -347,11 +348,9 @@ let PostContent = ({
<View style={styles.postTextContainer}> <View style={styles.postTextContainer}>
<RichText <RichText
testID="postText" testID="postText"
type="post-text" value={richText}
richText={richText}
lineHeight={1.3}
numberOfLines={limitLines ? MAX_POST_LINES : undefined} numberOfLines={limitLines ? MAX_POST_LINES : undefined}
style={s.flex1} style={[a.flex_1, a.text_md]}
/> />
</View> </View>
) : undefined} ) : undefined}

View File

@ -23,7 +23,7 @@ import * as Toast from '../util/Toast'
import {LoadingPlaceholder} from '../util/LoadingPlaceholder' import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import {ThemedText} from '../util/text/ThemedText' import {ThemedText} from '../util/text/ThemedText'
import {RichText} from '../util/text/RichText' import {RichText} from '#/components/RichText'
import {UserAvatar} from '../util/UserAvatar' import {UserAvatar} from '../util/UserAvatar'
import {UserBanner} from '../util/UserBanner' import {UserBanner} from '../util/UserBanner'
import {ProfileHeaderAlerts} from '../util/moderation/ProfileHeaderAlerts' import {ProfileHeaderAlerts} from '../util/moderation/ProfileHeaderAlerts'
@ -56,6 +56,7 @@ import {Shadow} from '#/state/cache/types'
import {useRequireAuth} from '#/state/session' import {useRequireAuth} from '#/state/session'
import {LabelInfo} from '../util/moderation/LabelInfo' import {LabelInfo} from '../util/moderation/LabelInfo'
import {useProfileShadow} from 'state/cache/profile-shadow' import {useProfileShadow} from 'state/cache/profile-shadow'
import {atoms as a} from '#/alf'
let ProfileHeaderLoading = (_props: {}): React.ReactNode => { let ProfileHeaderLoading = (_props: {}): React.ReactNode => {
const pal = usePalette('default') const pal = usePalette('default')
@ -608,12 +609,12 @@ let ProfileHeader = ({
</Text> </Text>
</View> </View>
{descriptionRT && !moderation.profile.blur ? ( {descriptionRT && !moderation.profile.blur ? (
<View pointerEvents="auto"> <View pointerEvents="auto" style={[styles.description]}>
<RichText <RichText
testID="profileHeaderDescription" testID="profileHeaderDescription"
style={[styles.description, pal.text]} style={[a.text_md]}
numberOfLines={15} numberOfLines={15}
richText={descriptionRT} value={descriptionRT}
/> />
</View> </View>
) : undefined} ) : undefined}

View File

@ -1,11 +1,14 @@
import React from 'react' import React from 'react'
import {View} from 'react-native' import {View, ViewStyle} from 'react-native'
/** /**
* This utility function captures events and stops * This utility function captures events and stops
* them from propagating upwards. * them from propagating upwards.
*/ */
export function EventStopper({children}: React.PropsWithChildren<{}>) { export function EventStopper({
children,
style,
}: React.PropsWithChildren<{style?: ViewStyle | ViewStyle[]}>) {
const stop = (e: any) => { const stop = (e: any) => {
e.stopPropagation() e.stopPropagation()
} }
@ -15,7 +18,8 @@ export function EventStopper({children}: React.PropsWithChildren<{}>) {
onTouchEnd={stop} onTouchEnd={stop}
// @ts-ignore web only -prf // @ts-ignore web only -prf
onClick={stop} onClick={stop}
onKeyDown={stop}> onKeyDown={stop}
style={style}>
{children} {children}
</View> </View>
) )

View File

@ -2,6 +2,7 @@ import React, {memo} from 'react'
import {StyleProp, View, ViewStyle} from 'react-native' import {StyleProp, View, ViewStyle} from 'react-native'
import Clipboard from '@react-native-clipboard/clipboard' import Clipboard from '@react-native-clipboard/clipboard'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {useNavigation} from '@react-navigation/native'
import { import {
AppBskyActorDefs, AppBskyActorDefs,
AppBskyFeedPost, AppBskyFeedPost,
@ -19,6 +20,8 @@ import * as Toast from '../Toast'
import {EventStopper} from '../EventStopper' import {EventStopper} from '../EventStopper'
import {useModalControls} from '#/state/modals' import {useModalControls} from '#/state/modals'
import {makeProfileLink} from '#/lib/routes/links' import {makeProfileLink} from '#/lib/routes/links'
import {CommonNavigatorParams} from '#/lib/routes/types'
import {getCurrentRoute} from 'lib/routes/helpers'
import {getTranslatorLink} from '#/locale/helpers' import {getTranslatorLink} from '#/locale/helpers'
import {usePostDeleteMutation} from '#/state/queries/post' import {usePostDeleteMutation} from '#/state/queries/post'
import {useMutedThreads, useToggleThreadMute} from '#/state/muted-threads' import {useMutedThreads, useToggleThreadMute} from '#/state/muted-threads'
@ -63,6 +66,7 @@ let PostDropdownBtn = ({
const hiddenPosts = useHiddenPosts() const hiddenPosts = useHiddenPosts()
const {hidePost} = useHiddenPostsApi() const {hidePost} = useHiddenPostsApi()
const openLink = useOpenLink() const openLink = useOpenLink()
const navigation = useNavigation()
const rootUri = record.reply?.root?.uri || postUri const rootUri = record.reply?.root?.uri || postUri
const isThreadMuted = mutedThreads.includes(rootUri) const isThreadMuted = mutedThreads.includes(rootUri)
@ -82,13 +86,38 @@ let PostDropdownBtn = ({
postDeleteMutation.mutateAsync({uri: postUri}).then( postDeleteMutation.mutateAsync({uri: postUri}).then(
() => { () => {
Toast.show(_(msg`Post deleted`)) 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 => { e => {
logger.error('Failed to delete post', {message: e}) logger.error('Failed to delete post', {message: e})
Toast.show(_(msg`Failed to delete post, please try again`)) Toast.show(_(msg`Failed to delete post, please try again`))
}, },
) )
}, [postUri, postDeleteMutation, _]) }, [
navigation,
postUri,
postDeleteMutation,
postAuthor,
currentAccount,
isAuthor,
href,
_,
])
const onToggleThreadMute = React.useCallback(() => { const onToggleThreadMute = React.useCallback(() => {
try { try {

View File

@ -21,7 +21,7 @@ import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native' import {useNavigation} from '@react-navigation/native'
import {AppBskyEmbedExternal} from '@atproto/api' 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 {EventStopper} from '../EventStopper'
import {isNative} from 'platform/detection' import {isNative} from 'platform/detection'
import {NavigationProp} from 'lib/routes/types' import {NavigationProp} from 'lib/routes/types'
@ -67,14 +67,12 @@ function PlaceholderOverlay({
// This renders the webview/youtube player as a separate layer // This renders the webview/youtube player as a separate layer
function Player({ function Player({
height,
params, params,
onLoad, onLoad,
isPlayerActive, isPlayerActive,
}: { }: {
isPlayerActive: boolean isPlayerActive: boolean
params: EmbedPlayerParams params: EmbedPlayerParams
height: number
onLoad: () => void onLoad: () => void
}) { }) {
// ensures we only load what's requested // ensures we only load what's requested
@ -91,25 +89,21 @@ function Player({
if (!isPlayerActive) return null if (!isPlayerActive) return null
return ( return (
<View style={[styles.layer, styles.playerLayer]}> <EventStopper style={[styles.layer, styles.playerLayer]}>
<EventStopper> <WebView
<View style={{height, width: '100%'}}> javaScriptEnabled={true}
<WebView onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
javaScriptEnabled={true} mediaPlaybackRequiresUserAction={false}
onShouldStartLoadWithRequest={onShouldStartLoadWithRequest} allowsInlineMediaPlayback
mediaPlaybackRequiresUserAction={false} bounces={false}
allowsInlineMediaPlayback allowsFullscreenVideo
bounces={false} nestedScrollEnabled
allowsFullscreenVideo source={{uri: params.playerUri}}
nestedScrollEnabled onLoad={onLoad}
source={{uri: params.playerUri}} style={styles.webview}
onLoad={onLoad} setSupportMultipleWindows={false} // Prevent any redirects from opening a new window (ads)
setSupportMultipleWindows={false} // Prevent any redirects from opening a new window (ads) />
style={[styles.webview, styles.topRadius]} </EventStopper>
/>
</View>
</EventStopper>
</View>
) )
} }
@ -129,13 +123,16 @@ export function ExternalPlayer({
const [isPlayerActive, setPlayerActive] = React.useState(false) const [isPlayerActive, setPlayerActive] = React.useState(false)
const [isLoading, setIsLoading] = React.useState(true) const [isLoading, setIsLoading] = React.useState(true)
const [dim, setDim] = React.useState({
width: 0, const aspect = React.useMemo(() => {
height: 0, return getPlayerAspect({
}) type: params.type,
width: windowDims.width,
hasThumb: !!link.thumb,
})
}, [params.type, windowDims.width, link.thumb])
const viewRef = useAnimatedRef() const viewRef = useAnimatedRef()
const frameCallback = useFrameCallback(() => { const frameCallback = useFrameCallback(() => {
const measurement = measure(viewRef) const measurement = measure(viewRef)
if (!measurement) return if (!measurement) return
@ -180,17 +177,6 @@ export function ExternalPlayer({
} }
}, [navigation, isPlayerActive, frameCallback]) }, [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(() => { const onLoad = React.useCallback(() => {
setIsLoading(false) setIsLoading(false)
}, []) }, [])
@ -216,32 +202,11 @@ export function ExternalPlayer({
[externalEmbedsPrefs, openModal, params.source], [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 ( return (
<Animated.View <Animated.View ref={viewRef} collapsable={false} style={[aspect]}>
ref={viewRef}
style={{height}}
collapsable={false}
onLayout={onLayout}>
{link.thumb && (!isPlayerActive || isLoading) && ( {link.thumb && (!isPlayerActive || isLoading) && (
<Image <Image
style={[ style={[{flex: 1}, styles.topRadius]}
{
width: dim.width,
height,
},
styles.topRadius,
]}
source={{uri: link.thumb}} source={{uri: link.thumb}}
accessibilityIgnoresInvertColors accessibilityIgnoresInvertColors
/> />
@ -251,12 +216,7 @@ export function ExternalPlayer({
isPlayerActive={isPlayerActive} isPlayerActive={isPlayerActive}
onPress={onPlayPress} onPress={onPlayPress}
/> />
<Player <Player isPlayerActive={isPlayerActive} params={params} onLoad={onLoad} />
isPlayerActive={isPlayerActive}
params={params}
height={height}
onLoad={onLoad}
/>
</Animated.View> </Animated.View>
) )
} }

View File

@ -20,7 +20,8 @@ import {PostAlerts} from '../moderation/PostAlerts'
import {makeProfileLink} from 'lib/routes/links' import {makeProfileLink} from 'lib/routes/links'
import {InfoCircleIcon} from 'lib/icons' import {InfoCircleIcon} from 'lib/icons'
import {Trans} from '@lingui/macro' 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({ export function MaybeQuoteEmbed({
embed, embed,
@ -127,11 +128,10 @@ export function QuoteEmbed({
) : null} ) : null}
{richText ? ( {richText ? (
<RichText <RichText
richText={richText} value={richText}
type="post-text" style={[a.text_md]}
style={pal.text}
numberOfLines={20} numberOfLines={20}
noLinks disableLinks
/> />
) : null} ) : null}
{embed && <PostEmbeds embed={embed} moderation={{}} />} {embed && <PostEmbeds embed={embed} moderation={{}} />}

View File

@ -10,6 +10,9 @@ import {usePalette} from 'lib/hooks/usePalette'
const WORD_WRAP = {wordWrap: 1} const WORD_WRAP = {wordWrap: 1}
/**
* @deprecated use `#/components/RichText`
*/
export function RichText({ export function RichText({
testID, testID,
type = 'md', type = 'md',

View File

@ -17,7 +17,7 @@ import {TextLink} from 'view/com/util/Link'
import {ListRef} from 'view/com/util/List' import {ListRef} from 'view/com/util/List'
import {Button} from 'view/com/util/forms/Button' import {Button} from 'view/com/util/forms/Button'
import {Text} from 'view/com/util/text/Text' 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 {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn'
import {FAB} from 'view/com/util/fab/FAB' import {FAB} from 'view/com/util/fab/FAB'
import {EmptyState} from 'view/com/util/EmptyState' import {EmptyState} from 'view/com/util/EmptyState'
@ -59,6 +59,7 @@ import {useComposerControls} from '#/state/shell/composer'
import {truncateAndInvalidate} from '#/state/queries/util' import {truncateAndInvalidate} from '#/state/queries/util'
import {isNative} from '#/platform/detection' import {isNative} from '#/platform/detection'
import {listenSoftReset} from '#/state/events' import {listenSoftReset} from '#/state/events'
import {atoms as a} from '#/alf'
const SECTION_TITLES = ['Posts', 'About'] const SECTION_TITLES = ['Posts', 'About']
@ -575,9 +576,8 @@ function AboutSection({
{feedInfo.description ? ( {feedInfo.description ? (
<RichText <RichText
testID="listDescription" testID="listDescription"
type="lg" style={[a.text_md]}
style={pal.text} value={feedInfo.description}
richText={feedInfo.description}
/> />
) : ( ) : (
<Text type="lg" style={[{fontStyle: 'italic'}, pal.textLight]}> <Text type="lg" style={[{fontStyle: 'italic'}, pal.textLight]}>

View File

@ -14,7 +14,7 @@ import {NativeDropdown, DropdownItem} from 'view/com/util/forms/NativeDropdown'
import {CenteredView} from 'view/com/util/Views' import {CenteredView} from 'view/com/util/Views'
import {EmptyState} from 'view/com/util/EmptyState' import {EmptyState} from 'view/com/util/EmptyState'
import {LoadingScreen} from 'view/com/util/LoadingScreen' 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 {Button} from 'view/com/util/forms/Button'
import {TextLink} from 'view/com/util/Link' import {TextLink} from 'view/com/util/Link'
import {ListRef} from 'view/com/util/List' import {ListRef} from 'view/com/util/List'
@ -60,6 +60,7 @@ import {
import {logger} from '#/logger' import {logger} from '#/logger'
import {useAnalytics} from '#/lib/analytics/analytics' import {useAnalytics} from '#/lib/analytics/analytics'
import {listenSoftReset} from '#/state/events' import {listenSoftReset} from '#/state/events'
import {atoms as a} from '#/alf'
const SECTION_TITLES_CURATE = ['Posts', 'About'] const SECTION_TITLES_CURATE = ['Posts', 'About']
const SECTION_TITLES_MOD = ['About'] const SECTION_TITLES_MOD = ['About']
@ -742,9 +743,8 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
{descriptionRT ? ( {descriptionRT ? (
<RichText <RichText
testID="listDescription" testID="listDescription"
type="lg" style={[a.text_md]}
style={pal.text} value={descriptionRT}
richText={descriptionRT}
/> />
) : ( ) : (
<Text <Text

View File

@ -76,7 +76,7 @@ export function ExportCarDialog({
This feature is in beta. You can read more about repository This feature is in beta. You can read more about repository
exports in{' '} exports in{' '}
<InlineLink <InlineLink
to="https://atproto.com/blog/repo-export" to="https://docs.bsky.app/blog/repo-export"
style={[a.text_sm]}> style={[a.text_sm]}>
this blogpost this blogpost
</InlineLink> </InlineLink>

View File

@ -9,7 +9,8 @@ import * as Prompt from '#/components/Prompt'
import {useDialogStateControlContext} from '#/state/dialogs' import {useDialogStateControlContext} from '#/state/dialogs'
export function Dialogs() { export function Dialogs() {
const control = Dialog.useDialogControl() const scrollable = Dialog.useDialogControl()
const basic = Dialog.useDialogControl()
const prompt = Prompt.usePromptControl() const prompt = Prompt.usePromptControl()
const {closeAllDialogs} = useDialogStateControlContext() const {closeAllDialogs} = useDialogStateControlContext()
@ -20,8 +21,31 @@ export function Dialogs() {
color="secondary" color="secondary"
size="small" size="small"
onPress={() => { onPress={() => {
control.open() scrollable.open()
prompt.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"> label="Open basic dialog">
Open basic dialog Open basic dialog
@ -48,9 +72,18 @@ export function Dialogs() {
</Prompt.Actions> </Prompt.Actions>
</Prompt.Outer> </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 <Dialog.Outer
control={control} control={scrollable}
nativeOptions={{sheet: {snapPoints: ['90%']}}}> nativeOptions={{sheet: {snapPoints: ['100%']}}}>
<Dialog.Handle /> <Dialog.Handle />
<Dialog.ScrollableInner <Dialog.ScrollableInner
@ -77,9 +110,13 @@ export function Dialogs() {
variant="outline" variant="outline"
color="primary" color="primary"
size="small" size="small"
onPress={() => control.close()} onPress={() =>
scrollable.close(() => {
console.log('CLOSED')
})
}
label="Open basic dialog"> label="Open basic dialog">
Close basic dialog Close dialog
</Button> </Button>
</View> </View>
</View> </View>

View File

@ -8,7 +8,9 @@ import {RichText} from '#/components/RichText'
export function Typography() { export function Typography() {
return ( return (
<View style={[a.gap_md]}> <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_4xl]}>atoms.text_4xl</Text>
<Text style={[a.text_3xl]}>atoms.text_3xl</Text> <Text style={[a.text_3xl]}>atoms.text_3xl</Text>
<Text style={[a.text_2xl]}>atoms.text_2xl</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`} value={`This is rich text. It can have mentions like @bsky.app or links like https://bsky.social`}
/> />
<RichText <RichText
selectable
resolveFacets resolveFacets
value={`This is rich text. It can have mentions like @bsky.app or links like https://bsky.social`} value={`This is rich text. It can have mentions like @bsky.app or links like https://bsky.social`}
style={[a.text_xl]} style={[a.text_xl]}