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',
}
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
*
@ -36,11 +24,10 @@ module.exports = function () {
*/
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',

View File

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

View File

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

View File

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

View File

@ -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))
var 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 {
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))
}
}

View File

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

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: {
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,
},

View File

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

View File

@ -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)
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()
}, [])
@ -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>
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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)]}
/>
)
}

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 =
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_PROD_SERVICE(url?: string) {
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) =>
`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/'

View File

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

View File

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

View File

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

View File

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

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(
queryClient: QueryClient,
uri: string,

View File

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

View File

@ -53,5 +53,6 @@ export function embedViewRecordToPostView(
record: v.value,
indexedAt: v.indexedAt,
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 {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) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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={{}} />}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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