Merge branch 'bluesky-social:main' into patch-3
commit
c2d87b8075
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
|
@ -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',
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
19
eas.json
19
eas.json
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
13
package.json
13
package.json
|
@ -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",
|
||||||
|
|
|
@ -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"
|
|
|
@ -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"
|
|
|
@ -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 $*"
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
@ -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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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),
|
|
||||||
])}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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',
|
||||||
|
})
|
|
@ -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/'
|
||||||
|
|
|
@ -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}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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={{}} />}
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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]}>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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]}
|
||||||
|
|
Loading…
Reference in New Issue