Compare commits
377 Commits
eric/rt
...
fix-session
| Author | SHA1 | Date | |
|---|---|---|---|
| f2443a7193 | |||
| b4bb838d04 | |||
| 3e12132d4f | |||
| 7c675b4a2f | |||
| 8e12918699 | |||
| 6a7f904457 | |||
| 7f178ffb88 | |||
| c5d5261257 | |||
| d410aeef39 | |||
| fc7eb58491 | |||
| cc472565f6 | |||
| 429a5abb9a | |||
| 35c4eac249 | |||
| 4e525cd44f | |||
| 67edca4fbb | |||
| b6993d9d0a | |||
| 6279c5cf31 | |||
| dbb34eb57e | |||
| 2b0db1fa31 | |||
| 84f4c423fd | |||
| 78a836d2b1 | |||
| 7bdaa624b7 | |||
| b8162aa3a7 | |||
| a94f99a8d2 | |||
| 646c318777 | |||
| aa1dc76bde | |||
| 02c270c102 | |||
| 429981708a | |||
| b35183b323 | |||
| 1c25c76645 | |||
| e8adf03cd5 | |||
| 7eaa573b57 | |||
| ef794018f4 | |||
| 1c26471541 | |||
| 45fcf149c4 | |||
| 8b7385ee23 | |||
| 1f8acd92a2 | |||
| 7d8f373d2b | |||
| 2d0a65e835 | |||
| 65b2b451a7 | |||
| 2cbf77b55a | |||
| 6d0ac69a23 | |||
| 3679852e17 | |||
| db79c918b2 | |||
| 444dbe928a | |||
| 14951ab413 | |||
| 85a0085754 | |||
| 274e7cf91f | |||
| 5040146e83 | |||
| f9fbe9d27d | |||
| 44b3a37f65 | |||
| 8c7f813d87 | |||
| f1d55f49fa | |||
| 2f54dcc97e | |||
| 653240bc05 | |||
| 80bdbe6c60 | |||
| 7af338ed21 | |||
| 202adb6d7b | |||
| 5c771050bc | |||
| 9f2f7f221c | |||
| 090b35e52e | |||
| 70ad820d64 | |||
| eeba0387b7 | |||
| 36478a3eea | |||
| c9d821c572 | |||
| cad66269d3 | |||
| 8123299192 | |||
| 80cc1f18a2 | |||
| ee57d74765 | |||
| b8afb935f4 | |||
| 17d921fd9d | |||
| 1c9debed9a | |||
| f3619cf9f9 | |||
| c1c7a55fd5 | |||
| c83fe94f41 | |||
| 3a6e58ba6e | |||
| a289e6bce5 | |||
| e4a1069b43 | |||
| 02f949781a | |||
| 2e60680296 | |||
| 3706719033 | |||
| 2d5080d34d | |||
| 3f14b62b20 | |||
| 60d043ecef | |||
| c0ed0775ed | |||
| ebd60ce38e | |||
| 918e1e6b53 | |||
| a69699a883 | |||
| e1c128c77c | |||
| efc3e3f450 | |||
| 8b760378ff | |||
| cb4974961a | |||
| 7edb6829dc | |||
| 1d5068d584 | |||
| 596e744d41 | |||
| 5b5c3d8b50 | |||
| 5ea406b6bc | |||
| a180788038 | |||
| 6cc61bf8e2 | |||
| 816a48e975 | |||
| 74f1495417 | |||
| a71ee87dfa | |||
| a40ebfa795 | |||
| c1b554a619 | |||
| 0a1fad8332 | |||
| 8c4b9aebe2 | |||
| f0144e7439 | |||
| d21f4d9c7f | |||
| 5566f586b5 | |||
| 6f4556d479 | |||
| db5749124f | |||
| c7e20a4b3d | |||
| 7a02e9dcb5 | |||
| ee08469b4d | |||
| 1aaed1cc0d | |||
| af124516b3 | |||
| 86de3d630a | |||
| e0f93cb9e6 | |||
| 0bd52704da | |||
| 3030825a69 | |||
| fb05cffdd6 | |||
| 185c3d6ee1 | |||
| f6685419d8 | |||
| 8862c1f540 | |||
| 68d4d4142c | |||
| 2f8573b339 | |||
| 944d4c6a82 | |||
| ed91041448 | |||
| 594958c6dc | |||
| aad8c080ed | |||
| 7899a6fd09 | |||
| 62e57c3b08 | |||
| 8ee325e73d | |||
| 0f9f08b1ef | |||
| 8f623c3bdf | |||
| ca561de2d0 | |||
| 1a9f6fcf73 | |||
| 040e9bf63c | |||
| 0b621d4f9c | |||
| e88eaf2d16 | |||
| 7a7c234909 | |||
| c94999e3ce | |||
| cb6674a69e | |||
| db14378cff | |||
| 6ac4b6be5c | |||
| 05f3be04b3 | |||
| dd86d0964d | |||
| 31826633cb | |||
| c8e0fa9c97 | |||
| b37c3adac3 | |||
| 249eb46343 | |||
| 42a6b80399 | |||
| 59d5c8ecc0 | |||
| b951bacdff | |||
| e21b72ee8b | |||
| 94312ac8ed | |||
| b6bf57fca7 | |||
| 2826483fb9 | |||
| 7d80e5db54 | |||
| a2db4d6098 | |||
| 1cf49517b5 | |||
| 8b0e575f64 | |||
| 357b61d0a5 | |||
| 38b217458a | |||
| d5b0a33ae3 | |||
| aa70d0e2ef | |||
| 800eecbfe4 | |||
| 4e6020b88c | |||
| 2d9a5db967 | |||
| b8f36a8bca | |||
| c5872f6644 | |||
| 7a592d8140 | |||
| f61d1e1f94 | |||
| 5b8d116e33 | |||
| 2e0f806441 | |||
| a1fe034c2e | |||
| eb298d2e60 | |||
| 26fc0cf66d | |||
| 26a338f586 | |||
| 5100c6b93b | |||
| 019cc9dd7e | |||
| a610ce875f | |||
| 2a7af0b8c0 | |||
| 317e0cda7a | |||
| e721f84a2c | |||
| 57854e6fc2 | |||
| a84e264dae | |||
| e794e84b48 | |||
| 20b88f43e2 | |||
| 14a99cf3fc | |||
| a4ad364781 | |||
| 19c0ae80eb | |||
| b26e280373 | |||
| f99307033c | |||
| 4d0dc7438b | |||
| 6c9d6f5b05 | |||
| 473ca0deff | |||
| f30f31f123 | |||
| ebd279ed68 | |||
| aa2f94e2fb | |||
| 8cda28e9fd | |||
| d1ffe94b00 | |||
| b70c404d4b | |||
| f2249614be | |||
| b07846f2fa | |||
| 4fc0b566ef | |||
| bde59a6de5 | |||
| 984fe481c3 | |||
| 2962862de2 | |||
| 411061ea32 | |||
| f016cdbca9 | |||
| e950463f71 | |||
| 8bf40b46a0 | |||
| a7195ccbd6 | |||
| 82655f2ee3 | |||
| e11bd4385d | |||
| f8aa5fd005 | |||
| cf8b03801f | |||
| 8900c67df2 | |||
| cecb6e4e69 | |||
| 775a11f557 | |||
| ad135fa705 | |||
| 21bdddcfbe | |||
| 04d4c9779f | |||
| 39d324ab8b | |||
| 1a5afccdb8 | |||
| a35976cdc9 | |||
| 2440975bd2 | |||
| 7fd13cacfe | |||
| 442eb135ee | |||
| c7de733dff | |||
| a697b39c48 | |||
| 60eb6e23d1 | |||
| d2c6edacb6 | |||
| 0c3d55db6f | |||
| 5cb45f9c16 | |||
| d679ae7dbb | |||
| 0dd3f9432b | |||
| 88c66c4bc5 | |||
| e2ae534fde | |||
| ba667af80e | |||
| ffeef8fd49 | |||
| 2a60faf4fe | |||
| c4d30a0b7f | |||
| 00ac76d0fe | |||
| 622d0daad6 | |||
| ed2e6d654e | |||
| 259468b7d5 | |||
| 23347a832d | |||
| 2d14d0e2db | |||
| 05863426c5 | |||
| 603f3c0be9 | |||
| 6bd68e5321 | |||
| d451f82f54 | |||
| ac726497a4 | |||
| 697b262ba4 | |||
| 978bcc1ba9 | |||
| 6717f8f11e | |||
| 2a04546c73 | |||
| c8d02a791a | |||
| 58aaad704a | |||
| c8582924e2 | |||
| 1a34921661 | |||
| 3ec4e034b7 | |||
| 771999761e | |||
| e9ad3f552d | |||
| a8925bac86 | |||
| 0311288dd7 | |||
| 9b11fc720f | |||
| 832582df23 | |||
| 6b9e3764b3 | |||
| 88846ca36d | |||
| 763fd5bc93 | |||
| 0dc1cdfdcf | |||
| 304b6f4f00 | |||
| 67fea6d317 | |||
| dbd42ae0c5 | |||
| d38f168de5 | |||
| 9c5cd391dd | |||
| bdb63fff6d | |||
| 6ae9c5d45c | |||
| de9df50af3 | |||
| 4771caf204 | |||
| 1c5a2232fc | |||
| 35be8ec90e | |||
| e3a4d800b4 | |||
| 047df5939f | |||
| 894f00d687 | |||
| 1ccb3be961 | |||
| 93b5eff4d7 | |||
| 874489b402 | |||
| ba71e4b459 | |||
| acd9532331 | |||
| d2e5f83cd2 | |||
| 43206d9f57 | |||
| 67d0dde5b7 | |||
| f88b165254 | |||
| 7d9890d465 | |||
| 6413b8ba8c | |||
| 9edb82609d | |||
| 0c470d1f61 | |||
| 3a75855677 | |||
| fab6c286f4 | |||
| 09eee05f8b | |||
| df5a8f1542 | |||
| 8a169dc6a1 | |||
| b52a742925 | |||
| da62a77f05 | |||
| 2e132a7a01 | |||
| e28e21fcc1 | |||
| b6bce411e7 | |||
| 58af8e0f99 | |||
| bfe7c2ff38 | |||
| abaf6e6bae | |||
| 014b6222e4 | |||
| 700693b87b | |||
| 283bb44d73 | |||
| 4dd2fd03c7 | |||
| b8d62a8af8 | |||
| ddef1d438a | |||
| 8870c5a183 | |||
| c5d31cd8b0 | |||
| 6d9e1f4dd9 | |||
| ef1a5ca2fd | |||
| 91bc703964 | |||
| 90662d6cc3 | |||
| 6e4ae28a2b | |||
| 1b33b88ae2 | |||
| e72113ec85 | |||
| f255d7a483 | |||
| b73f9710f9 | |||
| b52beefa27 | |||
| 16db93d8f9 | |||
| 4d8b4cf6f7 | |||
| b009a34a7a | |||
| 332c990404 | |||
| 46d95350fb | |||
| 5587fe73cb | |||
| 7313e70ca7 | |||
| cc635ae949 | |||
| 46076c4afb | |||
| da735f4894 | |||
| 78acf4c86e | |||
| 9f028512e6 | |||
| 02bbc5e0d5 | |||
| 6f5058d474 | |||
| 06ede19066 | |||
| 4e9eed5389 | |||
| 95707a1511 | |||
| 53da8d7714 | |||
| 65582457b7 | |||
| a289b616b2 | |||
| 9075c501ec | |||
| 937b66e3b5 | |||
| 9b5f80be7e | |||
| e45cc6f774 | |||
| f71cccd620 | |||
| b6552e9a84 | |||
| 97572fd5a2 | |||
| 667729f7db | |||
| fe20b4d575 | |||
| 4edbcd2b94 | |||
| 15d0f2e90b | |||
| 270476a126 | |||
| 5bd64fbc85 | |||
| f4ce9e22e8 | |||
| fabf6809fc | |||
| e0061056dc | |||
| 97bfa40ce3 | |||
| 9a57155339 | |||
| 8e1575f5ba | |||
| af5b545e25 | |||
| 5998b34471 | |||
| dd56beeebb | |||
| 981fc67138 | |||
| c0771deb9b | |||
| 81820479f3 |
@@ -3,8 +3,8 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- traffic-reduction
|
||||
- respect-optout-for-embeds
|
||||
- 3p-moderators
|
||||
|
||||
env:
|
||||
REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}
|
||||
USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
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@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
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: 🔤 Compile translations
|
||||
run: yarn intl:build
|
||||
|
||||
- name: ✏️ Write environment variables
|
||||
run: |
|
||||
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
|
||||
echo "${{ secrets.ENV_TOKEN }}" > .env
|
||||
echo "$json" > 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,75 @@
|
||||
---
|
||||
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@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
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: 🔤 Compile translations
|
||||
run: yarn intl:build
|
||||
|
||||
- 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
|
||||
@@ -32,12 +32,12 @@ jobs:
|
||||
name: Run tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install node 18
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v3
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
- name: Yarn install
|
||||
uses: Wandalen/wretry.action@master
|
||||
with:
|
||||
|
||||
@@ -23,7 +23,7 @@ COPY . .
|
||||
RUN mkdir --parents $NVM_DIR && \
|
||||
wget \
|
||||
--output-document=/tmp/nvm-install.sh \
|
||||
https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh && \
|
||||
https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh && \
|
||||
bash /tmp/nvm-install.sh
|
||||
|
||||
RUN \. "$NVM_DIR/nvm.sh" && \
|
||||
@@ -31,7 +31,7 @@ RUN \. "$NVM_DIR/nvm.sh" && \
|
||||
nvm use $NODE_VERSION && \
|
||||
npm install --global yarn && \
|
||||
yarn && \
|
||||
yarn intl:compile && \
|
||||
yarn intl:build && \
|
||||
yarn build-web
|
||||
|
||||
# DEBUG
|
||||
|
||||
@@ -65,8 +65,6 @@ If you discover any security issues, please send an email to security@bsky.app.
|
||||
|
||||
Bluesky is an open social network built on the AT Protocol, a flexible technology that will never lock developers out of the ecosystems that they help build. With atproto, third-party can be as seamless as first-party through custom feeds, federated services, clients, and more.
|
||||
|
||||
If you're a developer interested in building on atproto, we'd love to email you a Bluesky invite code. Simply share your GitHub (or similar) profile with us via [this form](https://forms.gle/BF21oxVNZiDjDhXF9).
|
||||
|
||||
## License (MIT)
|
||||
|
||||
See [./LICENSE](./LICENSE) for the full license.
|
||||
|
||||
@@ -11,24 +11,23 @@ const DARK_SPLASH_CONFIG = {
|
||||
resizeMode: 'cover',
|
||||
}
|
||||
|
||||
module.exports = function () {
|
||||
const SPLASH_CONFIG_ANDROID = {
|
||||
backgroundColor: '#0c7cff',
|
||||
image: './assets/splash.png',
|
||||
resizeMode: 'cover',
|
||||
}
|
||||
const DARK_SPLASH_CONFIG_ANDROID = {
|
||||
backgroundColor: '#0f141b',
|
||||
image: './assets/splash-dark.png',
|
||||
resizeMode: 'cover',
|
||||
}
|
||||
|
||||
module.exports = function (config) {
|
||||
/**
|
||||
* App version number. Should be incremented as part of a release cycle.
|
||||
*/
|
||||
const VERSION = pkg.version
|
||||
|
||||
/**
|
||||
* iOS build number. Must be incremented for each TestFlight version.
|
||||
* WARNING: Always leave this variable on line 24! If it is moved, you need to update ./scripts/bumpIosBuildNumber.sh
|
||||
*/
|
||||
const IOS_BUILD_NUMBER = '7'
|
||||
|
||||
/**
|
||||
* Android build number. Must be incremented for each release.
|
||||
* WARNING: Always leave this variable on line 30! If it is moved, you need to update ./scripts/bumpAndroidBuildNumber.sh
|
||||
*/
|
||||
const ANDROID_VERSION_CODE = 62
|
||||
|
||||
/**
|
||||
* Uses built-in Expo env vars
|
||||
*
|
||||
@@ -36,11 +35,12 @@ module.exports = function () {
|
||||
*/
|
||||
const PLATFORM = process.env.EAS_BUILD_PLATFORM
|
||||
|
||||
/**
|
||||
* Additional granularity for the `dist` field
|
||||
*/
|
||||
const DIST_BUILD_NUMBER =
|
||||
PLATFORM === 'android' ? ANDROID_VERSION_CODE : IOS_BUILD_NUMBER
|
||||
PLATFORM === 'android'
|
||||
? process.env.BSKY_ANDROID_VERSION_CODE
|
||||
: process.env.BSKY_IOS_BUILD_NUMBER
|
||||
|
||||
const IS_DEV = process.env.EXPO_PUBLIC_ENV === 'development'
|
||||
|
||||
return {
|
||||
expo: {
|
||||
@@ -57,7 +57,6 @@ module.exports = function () {
|
||||
userInterfaceStyle: 'automatic',
|
||||
splash: SPLASH_CONFIG,
|
||||
ios: {
|
||||
buildNumber: IOS_BUILD_NUMBER,
|
||||
supportsTablet: false,
|
||||
bundleIdentifier: 'xyz.blueskyweb.app',
|
||||
config: {
|
||||
@@ -79,13 +78,15 @@ module.exports = function () {
|
||||
...SPLASH_CONFIG,
|
||||
dark: DARK_SPLASH_CONFIG,
|
||||
},
|
||||
entitlements: {
|
||||
'com.apple.security.application-groups': 'group.app.bsky',
|
||||
},
|
||||
},
|
||||
androidStatusBar: {
|
||||
barStyle: 'dark-content',
|
||||
backgroundColor: '#ffffff',
|
||||
barStyle: 'light-content',
|
||||
backgroundColor: '#00000000',
|
||||
},
|
||||
android: {
|
||||
versionCode: ANDROID_VERSION_CODE,
|
||||
icon: './assets/icon.png',
|
||||
adaptiveIcon: {
|
||||
foregroundImage: './assets/icon-android-foreground.png',
|
||||
@@ -104,13 +105,17 @@ module.exports = function () {
|
||||
scheme: 'https',
|
||||
host: 'bsky.app',
|
||||
},
|
||||
IS_DEV && {
|
||||
scheme: 'http',
|
||||
host: 'localhost:19006',
|
||||
},
|
||||
],
|
||||
category: ['BROWSABLE', 'DEFAULT'],
|
||||
},
|
||||
],
|
||||
splash: {
|
||||
...SPLASH_CONFIG,
|
||||
dark: DARK_SPLASH_CONFIG,
|
||||
...SPLASH_CONFIG_ANDROID,
|
||||
dark: DARK_SPLASH_CONFIG_ANDROID,
|
||||
},
|
||||
},
|
||||
web: {
|
||||
@@ -129,12 +134,14 @@ module.exports = function () {
|
||||
{
|
||||
ios: {
|
||||
deploymentTarget: '13.4',
|
||||
newArchEnabled: false,
|
||||
},
|
||||
android: {
|
||||
compileSdkVersion: 34,
|
||||
targetSdkVersion: 34,
|
||||
buildToolsVersion: '34.0.0',
|
||||
kotlinVersion: '1.8.0',
|
||||
newArchEnabled: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -148,13 +155,33 @@ module.exports = function () {
|
||||
'expo-notifications',
|
||||
{
|
||||
icon: './assets/icon-android-notification.png',
|
||||
color: '#ffffff',
|
||||
color: '#1185fe',
|
||||
},
|
||||
],
|
||||
'./plugins/withAndroidManifestPlugin.js',
|
||||
'./plugins/withAndroidManifestFCMIconPlugin.js',
|
||||
'./plugins/withAndroidStylesWindowBackgroundPlugin.js',
|
||||
'./plugins/shareExtension/withShareExtensions.js',
|
||||
].filter(Boolean),
|
||||
extra: {
|
||||
eas: {
|
||||
build: {
|
||||
experimental: {
|
||||
ios: {
|
||||
appExtensions: [
|
||||
{
|
||||
targetName: 'Share-with-Bluesky',
|
||||
bundleIdentifier: 'xyz.blueskyweb.app.Share-with-Bluesky',
|
||||
entitlements: {
|
||||
'com.apple.security.application-groups': [
|
||||
'group.app.bsky',
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
projectId: '55bd077a-d905-4184-9c7f-94789ba0f302',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M4.213 6.886c-.673-1.35.334-2.889 1.806-2.889H17.98c1.472 0 2.479 1.539 1.806 2.89l-5.982 11.997c-.74 1.484-2.87 1.484-3.61 0L4.213 6.886Z"/></svg>
|
||||
|
After Width: | Height: | Size: 240 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M3 5a1 1 0 0 0 0 2h18a1 1 0 1 0 0-2H3Zm-1 7a1 1 0 0 1 1-1h18a1 1 0 1 1 0 2H3a1 1 0 0 1-1-1Zm0 6a1 1 0 0 1 1-1h18a1 1 0 1 1 0 2H3a1 1 0 0 1-1-1Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 286 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M5.002 17.036V5h14v12.036h-3.986a1 1 0 0 0-.639.23l-2.375 1.968-2.344-1.965a1 1 0 0 0-.643-.233H5.002ZM20.002 3h-16a1 1 0 0 0-1 1v14.036a1 1 0 0 0 1 1h4.65l2.704 2.266a1 1 0 0 0 1.28.004l2.74-2.27h4.626a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1Zm-7.878 3.663c-1.39 0-2.5 1.135-2.5 2.515a1 1 0 0 0 2 0c0-.294.232-.515.5-.515a.507.507 0 0 1 .489.6.174.174 0 0 1-.027.048 1.1 1.1 0 0 1-.267.226c-.508.345-1.128.923-1.286 1.978a1 1 0 1 0 1.978.297.762.762 0 0 1 .14-.359c.063-.086.155-.169.293-.262.436-.297 1.18-.885 1.18-2.013 0-1.38-1.11-2.515-2.5-2.515ZM12 15.75a1.25 1.25 0 1 1 0-2.5 1.25 1.25 0 0 1 0 2.5Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 738 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M8.371 3.89A2 2 0 0 1 10.035 3h3.93a2 2 0 0 1 1.664.89L17.035 6H20a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h2.965L8.37 3.89ZM12 9a3.5 3.5 0 1 0 0 7 3.5 3.5 0 0 0 0-7Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 330 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M8.371 3.89A2 2 0 0 1 10.035 3h3.93a2 2 0 0 1 1.664.89L17.035 6H20a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h2.965L8.37 3.89ZM13.965 5h-3.93L8.63 7.11A2 2 0 0 1 6.965 8H4v11h16V8h-2.965a2 2 0 0 1-1.664-.89L13.965 5ZM12 11a2 2 0 1 0 0 4 2 2 0 0 0 0-4Zm-4 2a4 4 0 1 1 8 0 4 4 0 0 1-8 0Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 447 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M21.474 2.98a2.5 2.5 0 0 1 .545 3.494l-10.222 14a2.5 2.5 0 0 1-3.528.52L2.49 16.617a2.5 2.5 0 0 1 3.018-3.986l3.75 2.84L17.98 3.525a2.5 2.5 0 0 1 3.493-.545Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 300 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M3.293 8.293a1 1 0 0 1 1.414 0L12 15.586l7.293-7.293a1 1 0 1 1 1.414 1.414l-8 8a1 1 0 0 1-1.414 0l-8-8a1 1 0 0 1 0-1.414Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 263 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12 6a1 1 0 0 1 .707.293l8 8a1 1 0 0 1-1.414 1.414L12 8.414l-7.293 7.293a1 1 0 0 1-1.414-1.414l8-8A1 1 0 0 1 12 6Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 256 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12 4a8 8 0 0 0-6.32 12.906L16.906 5.68A7.962 7.962 0 0 0 12 4Zm6.32 3.094L7.094 18.32A8 8 0 0 0 18.32 7.094ZM2 12C2 6.477 6.477 2 12 2a9.972 9.972 0 0 1 7.071 2.929A9.972 9.972 0 0 1 22 12c0 5.523-4.477 10-10 10a9.972 9.972 0 0 1-7.071-2.929A9.972 9.972 0 0 1 2 12Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 409 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M8.17 4A3.001 3.001 0 0 1 11 2h2c1.306 0 2.418.835 2.83 2H17a3 3 0 0 1 3 3v12a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3V7a3 3 0 0 1 3-3h1.17ZM8 6H7a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1h-1v1a1 1 0 0 1-1 1H9a1 1 0 0 1-1-1V6Zm6 0V5a1 1 0 0 0-1-1h-2a1 1 0 0 0-1 1v1h4Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 422 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M2 12a2 2 0 1 1 4 0 2 2 0 0 1-4 0Zm16 0a2 2 0 1 1 4 0 2 2 0 0 1-4 0Zm-6-2a2 2 0 1 0 0 4 2 2 0 0 0 0-4Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 245 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M3 4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v4a1 1 0 0 1-.293.707L15 14.414V20a1 1 0 0 1-.758.97l-4 1A1 1 0 0 1 9 21v-6.586L3.293 8.707A1 1 0 0 1 3 8V4Zm2 1v2.586l5.707 5.707A1 1 0 0 1 11 14v5.72l2-.5V14a1 1 0 0 1 .293-.707L19 7.586V5H5Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 371 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M4 4a2 2 0 0 1 2-2h13.131c1.598 0 2.55 1.78 1.665 3.11L18.202 9l2.594 3.89c.886 1.33-.067 3.11-1.665 3.11H6v5a1 1 0 1 1-2 0V4Zm2 10h13.131l-2.593-3.89a2 2 0 0 1 0-2.22L19.13 4H6v10Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 323 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M8 5a2 2 0 1 0 0 4 2 2 0 0 0 0-4ZM4 7a4 4 0 1 1 8 0 4 4 0 0 1-8 0Zm13-1a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Zm-3.5 1.5a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0Zm5.826 7.376c-.919-.779-2.052-1.03-3.1-.787a1 1 0 0 1-.451-1.949c1.671-.386 3.45.028 4.844 1.211 1.397 1.185 2.348 3.084 2.524 5.579a1 1 0 0 1-.997 1.07H18a1 1 0 1 1 0-2h3.007c-.29-1.47-.935-2.49-1.681-3.124ZM3.126 19h9.747c-.61-3.495-2.867-5-4.873-5-2.006 0-4.263 1.505-4.873 5ZM8 12c3.47 0 6.64 2.857 6.998 7.93A1 1 0 0 1 14 21H2a1 1 0 0 1-.998-1.07C1.36 14.857 4.53 12 8 12Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 674 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M12.489 21.372c8.528-4.78 10.626-10.47 9.022-14.47-.779-1.941-2.414-3.333-4.342-3.763-1.697-.378-3.552.003-5.169 1.287-1.617-1.284-3.472-1.665-5.17-1.287-1.927.43-3.562 1.822-4.34 3.764-1.605 4 .493 9.69 9.021 14.47a1 1 0 0 0 .978 0Z"/></svg>
|
||||
|
After Width: | Height: | Size: 336 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M16.734 5.091c-1.238-.276-2.708.047-4.022 1.38a1 1 0 0 1-1.424 0C9.974 5.137 8.504 4.814 7.266 5.09c-1.263.282-2.379 1.206-2.92 2.556C3.33 10.18 4.252 14.84 12 19.348c7.747-4.508 8.67-9.168 7.654-11.7-.541-1.351-1.657-2.275-2.92-2.557Zm4.777 1.812c1.604 4-.494 9.69-9.022 14.47a1 1 0 0 1-.978 0C2.983 16.592.885 10.902 2.49 6.902c.779-1.942 2.414-3.334 4.342-3.764 1.697-.378 3.552.003 5.169 1.286 1.617-1.283 3.472-1.664 5.17-1.286 1.927.43 3.562 1.822 4.34 3.764Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 608 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M11 5a6 6 0 1 0 0 12 6 6 0 0 0 0-12Zm-8 6a8 8 0 1 1 14.32 4.906l3.387 3.387a1 1 0 0 1-1.414 1.414l-3.387-3.387A8 8 0 0 1 3 11Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 269 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M20.707 3.293a1 1 0 0 1 0 1.414l-16 16a1 1 0 0 1-1.414-1.414l2.616-2.616A1.998 1.998 0 0 1 5 15V9a2 2 0 0 1 2-2h2.697l5.748-3.832A1 1 0 0 1 17 4v1.586l2.293-2.293a1 1 0 0 1 1.414 0ZM15 7.586 7.586 15H7V9h2.697a2 2 0 0 0 1.11-.336L15 5.87v1.717Zm2 3.657-2 2v4.888l-2.933-1.955-1.442 1.442 4.82 3.214A1 1 0 0 0 17 20v-8.757Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 465 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M5 2a1 1 0 0 0-1 1v18a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H5Zm1 18V4h12v16H6Zm3-6a1 1 0 1 0 0 2h2a1 1 0 1 0 0-2H9Zm-1-3a1 1 0 0 1 1-1h6a1 1 0 1 1 0 2H9a1 1 0 0 1-1-1Zm1-5a1 1 0 0 0 0 2h6a1 1 0 1 0 0-2H9Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 356 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none"><path fill="#000" fill-rule="evenodd" d="M10 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM5.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM16 11a1 1 0 0 1 1-1h5a1 1 0 1 1 0 2h-5a1 1 0 0 1-1-1ZM3.678 19h12.644c-.71-2.909-3.092-5-6.322-5s-5.613 2.091-6.322 5Zm-2.174.906C1.917 15.521 5.242 12 10 12c4.758 0 8.083 3.521 8.496 7.906A1 1 0 0 1 17.5 21h-15a1 1 0 0 1-.996-1.094Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 466 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM5.679 19c.709-2.902 3.079-5 6.321-5a6.69 6.69 0 0 1 2.612.51 1 1 0 0 0 .776-1.844A8.687 8.687 0 0 0 12 12c-4.3 0-7.447 2.884-8.304 6.696-.29 1.29.767 2.304 1.902 2.304H11a1 1 0 1 0 0-2H5.679Zm14.835-4.857a1 1 0 0 1 .344 1.371l-3 5a1 1 0 0 1-1.458.286l-2-1.5a1 1 0 0 1 1.2-1.6l1.113.835 2.43-4.05a1 1 0 0 1 1.372-.342Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 547 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM5.679 19c.709-2.902 3.079-5 6.321-5 .302 0 .595.018.878.053a1 1 0 0 0 .243-1.985A9.235 9.235 0 0 0 12 12c-4.3 0-7.447 2.884-8.304 6.696-.29 1.29.767 2.304 1.902 2.304H12a1 1 0 1 0 0-2H5.679Zm9.614-3.707a1 1 0 0 1 1.414 0L18 16.586l1.293-1.293a1 1 0 0 1 1.414 1.414L19.414 18l1.293 1.293a1 1 0 0 1-1.414 1.414L18 19.414l-1.293 1.293a1 1 0 0 1-1.414-1.414L16.586 18l-1.293-1.293a1 1 0 0 1 0-1.414Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 625 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM5.678 19h12.644c-.71-2.909-3.092-5-6.322-5s-5.613 2.091-6.322 5Zm-2.174.906C3.917 15.521 7.242 12 12 12c4.758 0 8.083 3.521 8.496 7.906A1 1 0 0 1 19.5 21h-15a1 1 0 0 1-.996-1.094Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 410 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M10.25 4a.75.75 0 0 0-.75.75V11a1 1 0 1 1-2 0V6.75a.75.75 0 0 0-1.5 0V14a6 6 0 0 0 12 0V9a2 2 0 0 0-2 2v1.5a1 1 0 0 1-.684.949l-.628.21A2.469 2.469 0 0 0 13 16a1 1 0 1 1-2 0 4.469 4.469 0 0 1 3-4.22V11c0-.703.181-1.364.5-1.938V5.75a.75.75 0 0 0-1.5 0V9a1 1 0 1 1-2 0V4.75a.75.75 0 0 0-.75-.75Zm2.316-.733A2.75 2.75 0 0 1 16.5 5.75v1.54c.463-.187.97-.29 1.5-.29h1a1 1 0 0 1 1 1v6a8 8 0 1 1-16 0V6.75a2.75 2.75 0 0 1 3.571-2.625 2.751 2.751 0 0 1 4.995-.858Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 599 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M11.1 2a1 1 0 0 0-.832.445L8.851 4.57 6.6 4.05a1 1 0 0 0-.932.268l-1.35 1.35a1 1 0 0 0-.267.932l.52 2.251-2.126 1.417A1 1 0 0 0 2 11.1v1.8a1 1 0 0 0 .445.832l2.125 1.417-.52 2.251a1 1 0 0 0 .268.932l1.35 1.35a1 1 0 0 0 .932.267l2.251-.52 1.417 2.126A1 1 0 0 0 11.1 22h1.8a1 1 0 0 0 .832-.445l1.417-2.125 2.251.52a1 1 0 0 0 .932-.268l1.35-1.35a1 1 0 0 0 .267-.932l-.52-2.251 2.126-1.417A1 1 0 0 0 22 12.9v-1.8a1 1 0 0 0-.445-.832L19.43 8.851l.52-2.251a1 1 0 0 0-.268-.932l-1.35-1.35a1 1 0 0 0-.932-.267l-2.251.52-1.417-2.126A1 1 0 0 0 12.9 2h-1.8Zm-.968 4.255L11.635 4h.73l1.503 2.255a1 1 0 0 0 1.057.42l2.385-.551.566.566-.55 2.385a1 1 0 0 0 .42 1.057L20 11.635v.73l-2.255 1.503a1 1 0 0 0-.42 1.057l.551 2.385-.566.566-2.385-.55a1 1 0 0 0-1.057.42L12.365 20h-.73l-1.503-2.255a1 1 0 0 0-1.057-.42l-2.385.551-.566-.566.55-2.385a1 1 0 0 0-.42-1.057L4 12.365v-.73l2.255-1.503a1 1 0 0 0 .42-1.057L6.123 6.69l.566-.566 2.385.55a1 1 0 0 0 1.057-.42ZM8 12a4 4 0 1 1 8 0 4 4 0 0 1-8 0Zm4-2a2 2 0 1 0 0 4 2 2 0 0 0 0-4Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M11.675 2.054a1 1 0 0 1 .65 0l8 2.75A1 1 0 0 1 21 5.75v6.162c0 2.807-1.149 4.83-2.813 6.405-1.572 1.488-3.632 2.6-5.555 3.636l-.157.085a1 1 0 0 1-.95 0l-.157-.085c-1.923-1.037-3.983-2.148-5.556-3.636C4.15 16.742 3 14.719 3 11.912V5.75a1 1 0 0 1 .675-.946l8-2.75ZM5 6.464v5.448c0 2.166.851 3.687 2.188 4.952 1.276 1.209 2.964 2.158 4.812 3.157 1.848-1 3.536-1.948 4.813-3.157C18.148 15.6 19 14.078 19 11.912V6.464l-7-2.407-7 2.407Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 572 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12.472 3.118A1 1 0 0 1 13 4v16a1 1 0 0 1-1.555.832L5.697 17H2a1 1 0 0 1-1-1V8a1 1 0 0 1 1-1h3.697l5.748-3.832a1 1 0 0 1 1.027-.05ZM11 5.868 6.555 8.833A1 1 0 0 1 6 9H3v6h3a1 1 0 0 1 .555.168L11 18.131V5.87Zm7.364-1.645a1 1 0 0 1 1.414 0A10.969 10.969 0 0 1 23 12c0 3.037-1.232 5.788-3.222 7.778a1 1 0 1 1-1.414-1.414A8.969 8.969 0 0 0 21 12a8.969 8.969 0 0 0-2.636-6.364 1 1 0 0 1 0-1.414Zm-3.182 3.181a1 1 0 0 1 1.414 0A6.483 6.483 0 0 1 18.5 12a6.483 6.483 0 0 1-1.904 4.597 1 1 0 0 1-1.414-1.415A4.483 4.483 0 0 0 16.5 12a4.483 4.483 0 0 0-1.318-3.182 1 1 0 0 1 0-1.414Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 717 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M14 5a1 1 0 1 1 0-2h6a1 1 0 0 1 1 1v6a1 1 0 1 1-2 0V6.414l-7.293 7.293a1 1 0 0 1-1.414-1.414L17.586 5H14ZM3 6a1 1 0 0 1 1-1h5a1 1 0 0 1 0 2H5v12h12v-4a1 1 0 1 1 2 0v5a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V6Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 342 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M8 8V3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1h-5v5a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1h5Zm1 8a1 1 0 0 1-1-1v-5H4v10h10v-4H9Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 286 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M4 4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H4Zm8 12.5c1.253 0 2.197.609 2.674 1.5H9.326c.477-.891 1.42-1.5 2.674-1.5Zm0-2c2.404 0 4.235 1.475 4.822 3.5H20V6H4v12h3.178c.587-2.025 2.418-3.5 4.822-3.5Zm-1.25-3.75a1.25 1.25 0 1 1 2.5 0 1.25 1.25 0 0 1-2.5 0ZM12 7.5a3.25 3.25 0 1 0 0 6.5 3.25 3.25 0 0 0 0-6.5Zm5.75 2a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 527 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M7.416 5H3a1 1 0 0 0 0 2h1.064l.938 14.067A1 1 0 0 0 6 22h12a1 1 0 0 0 .998-.933L19.936 7H21a1 1 0 1 0 0-2h-4.416a5 5 0 0 0-9.168 0Zm2.348 0h4.472c-.55-.614-1.348-1-2.236-1-.888 0-1.687.386-2.236 1Zm6.087 2H6.07l.867 13h10.128l.867-13h-2.036a1 1 0 0 1-.044 0ZM10 10a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0v-5a1 1 0 0 1 1-1Zm4 0a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0v-5a1 1 0 0 1 1-1Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 508 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12.86 4.494a.995.995 0 0 0-1.72 0L4.14 16.502A.996.996 0 0 0 4.999 18h14.003a.996.996 0 0 0 .86-1.498L12.86 4.494ZM9.413 3.487c1.155-1.983 4.019-1.983 5.174 0l7.002 12.007C22.753 17.491 21.314 20 19.002 20H4.998c-2.312 0-3.751-2.509-2.587-4.506L9.413 3.487ZM12 8.019a1 1 0 0 1 1 1v2.994a1 1 0 1 1-2 0V9.02a1 1 0 0 1 1-1Z" clip-rule="evenodd"/><rect width="2.5" height="2.5" x="10.75" y="13.75" fill="#000" rx="1.25"/></svg>
|
||||
|
After Width: | Height: | Size: 537 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M11.14 4.494a.995.995 0 0 1 1.72 0l7.001 12.008a.996.996 0 0 1-.86 1.498H4.999a.996.996 0 0 1-.86-1.498L11.14 4.494Zm3.447-1.007c-1.155-1.983-4.019-1.983-5.174 0L2.41 15.494C1.247 17.491 2.686 20 4.998 20h14.004c2.312 0 3.751-2.509 2.587-4.506L14.587 3.487ZM13 9.019a1 1 0 1 0-2 0v2.994a1 1 0 1 0 2 0V9.02Zm-1 4.731a1.25 1.25 0 1 0 0 2.5 1.25 1.25 0 0 0 0-2.5Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 503 B |
@@ -6,9 +6,9 @@ To build the SPA bundle (`bundle.web.js`), first get a JavaScript development
|
||||
environment set up. Either follow the top-level README, or something quick
|
||||
like:
|
||||
|
||||
# install nodejs 18 (specifically)
|
||||
nvm install 18
|
||||
nvm use 18
|
||||
# install nodejs
|
||||
nvm install
|
||||
nvm use
|
||||
npm install --global yarn
|
||||
|
||||
# setup tools and deps (in top level of this repo)
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Mailmodo struct {
|
||||
httpClient *http.Client
|
||||
APIKey string
|
||||
BaseURL string
|
||||
ListName string
|
||||
}
|
||||
|
||||
func NewMailmodo(apiKey, listName string) *Mailmodo {
|
||||
return &Mailmodo{
|
||||
APIKey: apiKey,
|
||||
BaseURL: "https://api.mailmodo.com/api/v1",
|
||||
httpClient: &http.Client{},
|
||||
ListName: listName,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Mailmodo) request(ctx context.Context, httpMethod string, apiMethod string, data any) error {
|
||||
endpoint := fmt.Sprintf("%s/%s", m.BaseURL, apiMethod)
|
||||
js, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Mailmodo JSON encoding failed: %w", err)
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, httpMethod, endpoint, bytes.NewBuffer(js))
|
||||
if err != nil {
|
||||
return fmt.Errorf("Mailmodo HTTP creating request %s %s failed: %w", httpMethod, apiMethod, err)
|
||||
}
|
||||
req.Header.Set("mmApiKey", m.APIKey)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
res, err := m.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Mailmodo HTTP making request %s %s failed: %w", httpMethod, apiMethod, err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
status := struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
}{}
|
||||
if err := json.NewDecoder(res.Body).Decode(&status); err != nil {
|
||||
return fmt.Errorf("Mailmodo HTTP parsing response %s %s failed: %w", httpMethod, apiMethod, err)
|
||||
}
|
||||
if !status.Success {
|
||||
return fmt.Errorf("Mailmodo API response %s %s failed: %s", httpMethod, apiMethod, status.Message)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Mailmodo) AddToList(ctx context.Context, email string) error {
|
||||
return m.request(ctx, "POST", "addToList", map[string]any{
|
||||
"listName": m.ListName,
|
||||
"email": email,
|
||||
"data": map[string]any{
|
||||
"email_hashed": fmt.Sprintf("%x", sha256.Sum256([]byte(email))),
|
||||
},
|
||||
"created_at": time.Now().UTC().Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
@@ -40,18 +40,6 @@ func run(args []string) {
|
||||
// retain old PDS env var for easy transition
|
||||
EnvVars: []string{"ATP_APPVIEW_HOST", "ATP_PDS_HOST"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "mailmodo-api-key",
|
||||
Usage: "Mailmodo API key",
|
||||
Required: false,
|
||||
EnvVars: []string{"MAILMODO_API_KEY"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "mailmodo-list-name",
|
||||
Usage: "Mailmodo contact list to add email addresses to",
|
||||
Required: false,
|
||||
EnvVars: []string{"MAILMODO_LIST_NAME"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "http-address",
|
||||
Usage: "Specify the local IP/port to bind to",
|
||||
|
||||
@@ -2,11 +2,9 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
@@ -29,25 +27,19 @@ import (
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
echo *echo.Echo
|
||||
httpd *http.Server
|
||||
mailmodo *Mailmodo
|
||||
xrpcc *xrpc.Client
|
||||
echo *echo.Echo
|
||||
httpd *http.Server
|
||||
xrpcc *xrpc.Client
|
||||
}
|
||||
|
||||
func serve(cctx *cli.Context) error {
|
||||
debug := cctx.Bool("debug")
|
||||
httpAddress := cctx.String("http-address")
|
||||
appviewHost := cctx.String("appview-host")
|
||||
mailmodoAPIKey := cctx.String("mailmodo-api-key")
|
||||
mailmodoListName := cctx.String("mailmodo-list-name")
|
||||
|
||||
// Echo
|
||||
e := echo.New()
|
||||
|
||||
// Mailmodo client.
|
||||
mailmodo := NewMailmodo(mailmodoAPIKey, mailmodoListName)
|
||||
|
||||
// create a new session (no auth)
|
||||
xrpcc := &xrpc.Client{
|
||||
Client: cliutil.NewHttpClient(),
|
||||
@@ -77,9 +69,8 @@ func serve(cctx *cli.Context) error {
|
||||
// server
|
||||
//
|
||||
server := &Server{
|
||||
echo: e,
|
||||
mailmodo: mailmodo,
|
||||
xrpcc: xrpcc,
|
||||
echo: e,
|
||||
xrpcc: xrpcc,
|
||||
}
|
||||
|
||||
// Create the HTTP server.
|
||||
@@ -180,6 +171,7 @@ func serve(cctx *cli.Context) error {
|
||||
e.GET("/", server.WebHome)
|
||||
|
||||
// generic routes
|
||||
e.GET("/hashtag/:tag", server.WebGeneric)
|
||||
e.GET("/search", server.WebGeneric)
|
||||
e.GET("/feeds", server.WebGeneric)
|
||||
e.GET("/notifications", server.WebGeneric)
|
||||
@@ -191,17 +183,19 @@ func serve(cctx *cli.Context) error {
|
||||
e.GET("/settings", server.WebGeneric)
|
||||
e.GET("/settings/language", server.WebGeneric)
|
||||
e.GET("/settings/app-passwords", server.WebGeneric)
|
||||
e.GET("/settings/home-feed", server.WebGeneric)
|
||||
e.GET("/settings/following-feed", server.WebGeneric)
|
||||
e.GET("/settings/saved-feeds", server.WebGeneric)
|
||||
e.GET("/settings/threads", server.WebGeneric)
|
||||
e.GET("/settings/external-embeds", server.WebGeneric)
|
||||
e.GET("/sys/debug", server.WebGeneric)
|
||||
e.GET("/sys/debug-mod", server.WebGeneric)
|
||||
e.GET("/sys/log", server.WebGeneric)
|
||||
e.GET("/support", server.WebGeneric)
|
||||
e.GET("/support/privacy", server.WebGeneric)
|
||||
e.GET("/support/tos", server.WebGeneric)
|
||||
e.GET("/support/community-guidelines", server.WebGeneric)
|
||||
e.GET("/support/copyright", server.WebGeneric)
|
||||
e.GET("/intent/compose", server.WebGeneric)
|
||||
|
||||
// profile endpoints; only first populates info
|
||||
e.GET("/profile/:handleOrDID", server.WebProfile)
|
||||
@@ -210,6 +204,7 @@ func serve(cctx *cli.Context) error {
|
||||
e.GET("/profile/:handleOrDID/lists/:rkey", server.WebGeneric)
|
||||
e.GET("/profile/:handleOrDID/feed/:rkey", server.WebGeneric)
|
||||
e.GET("/profile/:handleOrDID/feed/:rkey/liked-by", server.WebGeneric)
|
||||
e.GET("/profile/:handleOrDID/labeler/liked-by", server.WebGeneric)
|
||||
|
||||
// profile RSS feed (DID not handle)
|
||||
e.GET("/profile/:ident/rss", server.WebProfileRSS)
|
||||
@@ -219,9 +214,6 @@ func serve(cctx *cli.Context) error {
|
||||
e.GET("/profile/:handleOrDID/post/:rkey/liked-by", server.WebGeneric)
|
||||
e.GET("/profile/:handleOrDID/post/:rkey/reposted-by", server.WebGeneric)
|
||||
|
||||
// Mailmodo
|
||||
e.POST("/api/waitlist", server.apiWaitlist)
|
||||
|
||||
// Start the server.
|
||||
log.Infof("starting server address=%s", httpAddress)
|
||||
go func() {
|
||||
@@ -396,36 +388,3 @@ func (srv *Server) WebProfile(c echo.Context) error {
|
||||
data["requestHost"] = req.Host
|
||||
return c.Render(http.StatusOK, "profile.html", data)
|
||||
}
|
||||
|
||||
func (srv *Server) apiWaitlist(c echo.Context) error {
|
||||
type jsonError struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
// Read the API request.
|
||||
type apiRequest struct {
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
bodyReader := http.MaxBytesReader(c.Response(), c.Request().Body, 16*1024)
|
||||
payload, err := ioutil.ReadAll(bodyReader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var req apiRequest
|
||||
if err := json.Unmarshal(payload, &req); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, jsonError{Error: "Invalid API request"})
|
||||
}
|
||||
|
||||
if req.Email == "" {
|
||||
return c.JSON(http.StatusBadRequest, jsonError{Error: "Please enter a valid email address."})
|
||||
}
|
||||
|
||||
if err := srv.mailmodo.AddToList(c.Request().Context(), req.Email); err != nil {
|
||||
log.Errorf("adding email to waitlist failed: %s", err)
|
||||
return c.JSON(http.StatusBadRequest, jsonError{
|
||||
Error: "Storing email in waitlist failed. Please enter a valid email address.",
|
||||
})
|
||||
}
|
||||
return c.JSON(http.StatusOK, map[string]bool{"success": true})
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="theme-color">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, viewport-fit=cover">
|
||||
<meta name="referrer" content="origin-when-cross-origin">
|
||||
<!--
|
||||
@@ -44,6 +43,15 @@
|
||||
height: calc(100% + env(safe-area-inset-top));
|
||||
scrollbar-gutter: stable both-edges;
|
||||
}
|
||||
html, body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
/* Buttons and inputs have a font set by UA, so we'll have to reset that */
|
||||
button, input, textarea {
|
||||
font: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
/* Color theming */
|
||||
/* Default will always be white */
|
||||
@@ -206,13 +214,19 @@
|
||||
[data-tooltip]:hover::before {
|
||||
display:block;
|
||||
}
|
||||
|
||||
/* NativeDropdown component */
|
||||
.radix-dropdown-item:focus,
|
||||
.nativeDropdown-item:focus {
|
||||
outline: none;
|
||||
}
|
||||
</style>
|
||||
{% include "scripts.html" %}
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.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="mask-icon" href="/static/safari-pinned-tab.svg" color="#1185fe">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<meta name="theme-color">
|
||||
<meta name="application-name" content="Bluesky">
|
||||
<meta name="generator" content="bskyweb">
|
||||
<meta property="og:site_name" content="Bluesky Social" />
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"cli": {
|
||||
"version": ">= 3.8.1",
|
||||
"promptToConfigurePushNotifications": false
|
||||
"promptToConfigurePushNotifications": false,
|
||||
"appVersionSource": "remote"
|
||||
},
|
||||
"build": {
|
||||
"base": {
|
||||
@@ -28,7 +29,21 @@
|
||||
"production": {
|
||||
"extends": "base",
|
||||
"ios": {
|
||||
"resourceClass": "large"
|
||||
"resourceClass": "large",
|
||||
"autoIncrement": true
|
||||
},
|
||||
"android": {
|
||||
"autoIncrement": true
|
||||
},
|
||||
"channel": "production"
|
||||
},
|
||||
"github": {
|
||||
"extends": "base",
|
||||
"ios": {
|
||||
"autoIncrement": true
|
||||
},
|
||||
"android": {
|
||||
"autoIncrement": true
|
||||
},
|
||||
"channel": "production"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import '#/platform/markBundleStartTime'
|
||||
|
||||
import '#/platform/polyfills'
|
||||
import {registerRootComponent} from 'expo'
|
||||
import {doPolyfill} from '#/lib/api/api-polyfill'
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).ShareViewController</string>
|
||||
<key>NSExtensionAttributes</key>
|
||||
<dict>
|
||||
<key>NSExtensionActivationRule</key>
|
||||
<dict>
|
||||
<key>NSExtensionActivationSupportsText</key>
|
||||
<true/>
|
||||
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
|
||||
<integer>1</integer>
|
||||
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
|
||||
<integer>10</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.share-services</string>
|
||||
</dict>
|
||||
<key>MainAppScheme</key>
|
||||
<string>bluesky</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Extension</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.app.bsky</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,153 @@
|
||||
import UIKit
|
||||
|
||||
class ShareViewController: UIViewController {
|
||||
// This allows other forks to use this extension while also changing their
|
||||
// scheme.
|
||||
let appScheme = Bundle.main.object(forInfoDictionaryKey: "MainAppScheme") as? String ?? "bluesky"
|
||||
|
||||
//
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
guard let extensionItem = extensionContext?.inputItems.first as? NSExtensionItem,
|
||||
let attachments = extensionItem.attachments,
|
||||
let firstAttachment = extensionItem.attachments?.first
|
||||
else {
|
||||
self.completeRequest()
|
||||
return
|
||||
}
|
||||
|
||||
Task {
|
||||
if firstAttachment.hasItemConformingToTypeIdentifier("public.text") {
|
||||
await self.handleText(item: firstAttachment)
|
||||
} else if firstAttachment.hasItemConformingToTypeIdentifier("public.url") {
|
||||
await self.handleUrl(item: firstAttachment)
|
||||
} else if firstAttachment.hasItemConformingToTypeIdentifier("public.image") {
|
||||
await self.handleImages(items: attachments)
|
||||
} else {
|
||||
self.completeRequest()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func handleText(item: NSItemProvider) async -> Void {
|
||||
do {
|
||||
if let data = try await item.loadItem(forTypeIdentifier: "public.text") as? String {
|
||||
if let encoded = data.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed),
|
||||
let url = URL(string: "\(self.appScheme)://intent/compose?text=\(encoded)")
|
||||
{
|
||||
_ = self.openURL(url)
|
||||
}
|
||||
}
|
||||
self.completeRequest()
|
||||
} catch {
|
||||
self.completeRequest()
|
||||
}
|
||||
}
|
||||
|
||||
private func handleUrl(item: NSItemProvider) async -> Void {
|
||||
do {
|
||||
if let data = try await item.loadItem(forTypeIdentifier: "public.url") as? URL {
|
||||
if let encoded = data.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed),
|
||||
let url = URL(string: "\(self.appScheme)://intent/compose?text=\(encoded)")
|
||||
{
|
||||
_ = self.openURL(url)
|
||||
}
|
||||
}
|
||||
self.completeRequest()
|
||||
} catch {
|
||||
self.completeRequest()
|
||||
}
|
||||
}
|
||||
|
||||
private func handleImages(items: [NSItemProvider]) async -> Void {
|
||||
let firstFourItems: [NSItemProvider]
|
||||
if items.count < 4 {
|
||||
firstFourItems = items
|
||||
} else {
|
||||
firstFourItems = Array(items[0...3])
|
||||
}
|
||||
|
||||
var valid = true
|
||||
var imageUris = ""
|
||||
|
||||
for (index, item) in firstFourItems.enumerated() {
|
||||
var imageUriInfo: String? = nil
|
||||
|
||||
do {
|
||||
if let dataUri = try await item.loadItem(forTypeIdentifier: "public.image") as? URL {
|
||||
// We need to duplicate this image, since we don't have access to the outgoing temp directory
|
||||
// We also will get the image dimensions here, sinze RN makes it difficult to get those dimensions for local files
|
||||
let data = try Data(contentsOf: dataUri)
|
||||
let image = UIImage(data: data)
|
||||
imageUriInfo = self.saveImageWithInfo(image)
|
||||
} else if let image = try await item.loadItem(forTypeIdentifier: "public.image") as? UIImage {
|
||||
imageUriInfo = self.saveImageWithInfo(image)
|
||||
}
|
||||
} catch {
|
||||
valid = false
|
||||
}
|
||||
|
||||
if let imageUriInfo = imageUriInfo {
|
||||
imageUris.append(imageUriInfo)
|
||||
if index < items.count - 1 {
|
||||
imageUris.append(",")
|
||||
}
|
||||
} else {
|
||||
valid = false
|
||||
}
|
||||
}
|
||||
|
||||
if valid,
|
||||
let encoded = imageUris.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed),
|
||||
let url = URL(string: "\(self.appScheme)://intent/compose?imageUris=\(encoded)")
|
||||
{
|
||||
_ = self.openURL(url)
|
||||
}
|
||||
|
||||
self.completeRequest()
|
||||
}
|
||||
|
||||
private func saveImageWithInfo(_ image: UIImage?) -> String? {
|
||||
guard let image = image else {
|
||||
return nil
|
||||
}
|
||||
|
||||
do {
|
||||
// Saving this file to the bundle group's directory lets us access it from
|
||||
// inside of the app. Otherwise, we wouldn't have access even though the
|
||||
// extension does.
|
||||
if let dir = FileManager()
|
||||
.containerURL(
|
||||
forSecurityApplicationGroupIdentifier: "group.app.bsky")
|
||||
{
|
||||
let filePath = "\(dir.absoluteString)\(ProcessInfo.processInfo.globallyUniqueString).jpeg"
|
||||
|
||||
if let newUri = URL(string: filePath),
|
||||
let jpegData = image.jpegData(compressionQuality: 1)
|
||||
{
|
||||
try jpegData.write(to: newUri)
|
||||
return "\(newUri.absoluteString)|\(image.size.width)|\(image.size.height)"
|
||||
}
|
||||
}
|
||||
return nil
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func completeRequest() -> Void {
|
||||
self.extensionContext?.completeRequest(returningItems: nil)
|
||||
}
|
||||
|
||||
@objc func openURL(_ url: URL) -> Bool {
|
||||
var responder: UIResponder? = self
|
||||
while responder != nil {
|
||||
if let application = responder as? UIApplication {
|
||||
return application.perform(#selector(openURL(_:)), with: url) != nil
|
||||
}
|
||||
responder = responder?.next
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
# Expo Receive Android Intents
|
||||
|
||||
This module handles incoming intents on Android. Handled intents are `text/plain` and `image/*` (single or multiple).
|
||||
The module handles saving images to the app's filesystem for access within the app, limiting the selection of images
|
||||
to a max of four, and handling intent types. No JS code is required for this module, and it is no-op on non-android
|
||||
platforms.
|
||||
|
||||
No installation is required. Gradle will automatically add this module on build.
|
||||
@@ -0,0 +1,15 @@
|
||||
# OSX
|
||||
#
|
||||
.DS_Store
|
||||
|
||||
# Android/IntelliJ
|
||||
#
|
||||
build/
|
||||
.idea
|
||||
.gradle
|
||||
local.properties
|
||||
*.iml
|
||||
*.hprof
|
||||
|
||||
# Bundle artifacts
|
||||
*.jsbundle
|
||||
@@ -0,0 +1,92 @@
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'maven-publish'
|
||||
|
||||
group = 'xyz.blueskyweb.app.exporeceiveandroidintents'
|
||||
version = '0.4.1'
|
||||
|
||||
buildscript {
|
||||
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
||||
if (expoModulesCorePlugin.exists()) {
|
||||
apply from: expoModulesCorePlugin
|
||||
applyKotlinExpoModulesCorePlugin()
|
||||
}
|
||||
|
||||
// Simple helper that allows the root project to override versions declared by this library.
|
||||
ext.safeExtGet = { prop, fallback ->
|
||||
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
||||
}
|
||||
|
||||
// Ensures backward compatibility
|
||||
ext.getKotlinVersion = {
|
||||
if (ext.has("kotlinVersion")) {
|
||||
ext.kotlinVersion()
|
||||
} else {
|
||||
ext.safeExtGet("kotlinVersion", "1.8.10")
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${getKotlinVersion()}")
|
||||
}
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
publishing {
|
||||
publications {
|
||||
release(MavenPublication) {
|
||||
from components.release
|
||||
}
|
||||
}
|
||||
repositories {
|
||||
maven {
|
||||
url = mavenLocal().url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion safeExtGet("compileSdkVersion", 33)
|
||||
|
||||
def agpVersion = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION
|
||||
if (agpVersion.tokenize('.')[0].toInteger() < 8) {
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_11
|
||||
targetCompatibility JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_11.majorVersion
|
||||
}
|
||||
}
|
||||
|
||||
namespace "xyz.blueskyweb.app.exporeceiveandroidintents"
|
||||
defaultConfig {
|
||||
minSdkVersion safeExtGet("minSdkVersion", 21)
|
||||
targetSdkVersion safeExtGet("targetSdkVersion", 34)
|
||||
versionCode 1
|
||||
versionName "0.4.1"
|
||||
}
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
publishing {
|
||||
singleVariant("release") {
|
||||
withSourcesJar()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':expo-modules-core')
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}"
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
<manifest>
|
||||
</manifest>
|
||||
@@ -0,0 +1,119 @@
|
||||
package xyz.blueskyweb.app.exporeceiveandroidintents
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.MediaStore
|
||||
import androidx.core.net.toUri
|
||||
import expo.modules.kotlin.modules.Module
|
||||
import expo.modules.kotlin.modules.ModuleDefinition
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.net.URLEncoder
|
||||
|
||||
class ExpoReceiveAndroidIntentsModule : Module() {
|
||||
override fun definition() = ModuleDefinition {
|
||||
Name("ExpoReceiveAndroidIntents")
|
||||
|
||||
OnNewIntent {
|
||||
handleIntent(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleIntent(intent: Intent?) {
|
||||
if(appContext.currentActivity == null || intent == null) return
|
||||
|
||||
if (intent.action == Intent.ACTION_SEND) {
|
||||
if (intent.type == "text/plain") {
|
||||
handleTextIntent(intent)
|
||||
} else if (intent.type.toString().startsWith("image/")) {
|
||||
handleImageIntent(intent)
|
||||
}
|
||||
} else if (intent.action == Intent.ACTION_SEND_MULTIPLE) {
|
||||
if (intent.type.toString().startsWith("image/")) {
|
||||
handleImagesIntent(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleTextIntent(intent: Intent) {
|
||||
intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
|
||||
val encoded = URLEncoder.encode(it, "UTF-8")
|
||||
"bluesky://intent/compose?text=${encoded}".toUri().let { uri ->
|
||||
val newIntent = Intent(Intent.ACTION_VIEW, uri)
|
||||
appContext.currentActivity?.startActivity(newIntent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleImageIntent(intent: Intent) {
|
||||
val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)
|
||||
} else {
|
||||
intent.getParcelableExtra(Intent.EXTRA_STREAM)
|
||||
}
|
||||
if (uri == null) return
|
||||
|
||||
handleImageIntents(listOf(uri))
|
||||
}
|
||||
|
||||
private fun handleImagesIntent(intent: Intent) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM, Uri::class.java)?.let {
|
||||
handleImageIntents(it.filterIsInstance<Uri>().take(4))
|
||||
}
|
||||
} else {
|
||||
intent.getParcelableArrayListExtra<Uri>(Intent.EXTRA_STREAM)?.let {
|
||||
handleImageIntents(it.filterIsInstance<Uri>().take(4))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleImageIntents(uris: List<Uri>) {
|
||||
var allParams = ""
|
||||
|
||||
uris.forEachIndexed { index, uri ->
|
||||
val info = getImageInfo(uri)
|
||||
val params = buildUriData(info)
|
||||
allParams = "${allParams}${params}"
|
||||
|
||||
if (index < uris.count() - 1) {
|
||||
allParams = "${allParams},"
|
||||
}
|
||||
}
|
||||
|
||||
val encoded = URLEncoder.encode(allParams, "UTF-8")
|
||||
|
||||
"bluesky://intent/compose?imageUris=${encoded}".toUri().let {
|
||||
val newIntent = Intent(Intent.ACTION_VIEW, it)
|
||||
appContext.currentActivity?.startActivity(newIntent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getImageInfo(uri: Uri): Map<String, Any> {
|
||||
val bitmap = MediaStore.Images.Media.getBitmap(appContext.currentActivity?.contentResolver, uri)
|
||||
// We have to save this so that we can access it later when uploading the image.
|
||||
// createTempFile will automatically place a unique string between "img" and "temp.jpeg"
|
||||
val file = File.createTempFile("img", "temp.jpeg", appContext.currentActivity?.cacheDir)
|
||||
val out = FileOutputStream(file)
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out)
|
||||
out.flush()
|
||||
out.close()
|
||||
|
||||
return mapOf(
|
||||
"width" to bitmap.width,
|
||||
"height" to bitmap.height,
|
||||
"path" to file.path.toString()
|
||||
)
|
||||
}
|
||||
|
||||
// We will pas the width and height to the app here, since getting measurements
|
||||
// on the RN side is a bit more involved, and we already have them here anyway.
|
||||
private fun buildUriData(info: Map<String, Any>): String {
|
||||
val path = info.getValue("path")
|
||||
val width = info.getValue("width")
|
||||
val height = info.getValue("height")
|
||||
return "file://${path}|${width}|${height}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"platforms": ["android"],
|
||||
"android": {
|
||||
"modules": ["xyz.blueskyweb.app.exporeceiveandroidintents.ExpoReceiveAndroidIntentsModule"]
|
||||
}
|
||||
}
|
||||
@@ -108,14 +108,26 @@ class RNUITextView: UIView {
|
||||
fractionOfDistanceBetweenInsertionPoints: nil
|
||||
)
|
||||
|
||||
var lastUpperBound: String.Index? = nil
|
||||
for child in self.reactSubviews() {
|
||||
if let child = child as? RNUITextViewChild, let childText = child.text {
|
||||
let fullText = self.textView.attributedText.string
|
||||
let range = fullText.range(of: childText)
|
||||
|
||||
|
||||
// We want to skip over the children we have already checked, otherwise we could run into
|
||||
// collisions of similar strings (i.e. links that get shortened to the same hostname but
|
||||
// different paths)
|
||||
let range = fullText.range(of: childText, options: [], range: (lastUpperBound ?? String.Index(utf16Offset: 0, in: fullText) )..<fullText.endIndex)
|
||||
|
||||
if let lowerBound = range?.lowerBound, let upperBound = range?.upperBound {
|
||||
if charIndex >= lowerBound.utf16Offset(in: fullText) && charIndex <= upperBound.utf16Offset(in: fullText) {
|
||||
let lowerOffset = lowerBound.utf16Offset(in: fullText)
|
||||
let upperOffset = upperBound.utf16Offset(in: fullText)
|
||||
|
||||
if charIndex >= lowerOffset,
|
||||
charIndex <= upperOffset
|
||||
{
|
||||
return child
|
||||
} else {
|
||||
lastUpperBound = upperBound
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
RCT_REMAP_SHADOW_PROPERTY(numberOfLines, numberOfLines, NSInteger)
|
||||
RCT_REMAP_SHADOW_PROPERTY(allowsFontScaling, allowsFontScaling, BOOL)
|
||||
|
||||
RCT_EXPORT_VIEW_PROPERTY(numberOfLines, NSInteger)
|
||||
RCT_EXPORT_VIEW_PROPERTY(onTextLayout, RCTDirectEventBlock)
|
||||
RCT_EXPORT_VIEW_PROPERTY(ellipsizeMode, NSString)
|
||||
RCT_EXPORT_VIEW_PROPERTY(selectable, BOOL)
|
||||
|
||||
@@ -40,19 +40,19 @@ class RNUITextViewShadow: RCTShadowView {
|
||||
self.setAttributedText()
|
||||
}
|
||||
|
||||
// Tell yoga not to use flexbox
|
||||
// Returning true here will tell Yoga to not use flexbox and instead use our custom measure func.
|
||||
override func isYogaLeafNode() -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// We only need to insert text children
|
||||
// We should only insert children that are UITextView shadows
|
||||
override func insertReactSubview(_ subview: RCTShadowView!, at atIndex: Int) {
|
||||
if subview.isKind(of: RNUITextViewChildShadow.self) {
|
||||
super.insertReactSubview(subview, at: atIndex)
|
||||
}
|
||||
}
|
||||
|
||||
// Whenever the subvies update, set the text
|
||||
// Every time the subviews change, we need to reformat and render the text.
|
||||
override func didUpdateReactSubviews() {
|
||||
self.setAttributedText()
|
||||
}
|
||||
@@ -64,7 +64,7 @@ class RNUITextViewShadow: RCTShadowView {
|
||||
return
|
||||
}
|
||||
|
||||
// Update the text
|
||||
// Since we are inside the shadow view here, we have to find the real view and update the text.
|
||||
self.bridge.uiManager.addUIBlock { uiManager, viewRegistry in
|
||||
guard let textView = viewRegistry?[self.reactTag] as? RNUITextView else {
|
||||
return
|
||||
@@ -100,18 +100,25 @@ class RNUITextViewShadow: RCTShadowView {
|
||||
// Create the attributed string with the generic attributes
|
||||
let string = NSMutableAttributedString(string: child.text, attributes: attributes)
|
||||
|
||||
// Set the paragraph style attributes if necessary
|
||||
// Set the paragraph style attributes if necessary. We can check this by seeing if the provided
|
||||
// line height is not 0.0.
|
||||
let paragraphStyle = NSMutableParagraphStyle()
|
||||
if child.lineHeight != 0.0 {
|
||||
paragraphStyle.minimumLineHeight = child.lineHeight
|
||||
paragraphStyle.maximumLineHeight = child.lineHeight
|
||||
// Whenever we change the line height for the text, we are also removing the DynamicType
|
||||
// adjustment for line height. We need to get the multiplier and apply that to the
|
||||
// line height.
|
||||
let scaleMultiplier = scaledFontSize / child.fontSize
|
||||
paragraphStyle.minimumLineHeight = child.lineHeight * scaleMultiplier
|
||||
paragraphStyle.maximumLineHeight = child.lineHeight * scaleMultiplier
|
||||
|
||||
string.addAttribute(
|
||||
NSAttributedString.Key.paragraphStyle,
|
||||
value: paragraphStyle,
|
||||
range: NSMakeRange(0, string.length)
|
||||
)
|
||||
|
||||
// Store that height
|
||||
// To calcualte the size of the text without creating a new UILabel or UITextView, we have
|
||||
// to store this line height for later.
|
||||
self.lineHeight = child.lineHeight
|
||||
} else {
|
||||
self.lineHeight = font.lineHeight
|
||||
@@ -124,24 +131,22 @@ class RNUITextViewShadow: RCTShadowView {
|
||||
self.dirtyLayout()
|
||||
}
|
||||
|
||||
// Create a YGSize based on the max width
|
||||
// To create the needed size we need to:
|
||||
// 1. Get the max size that we can use for the view
|
||||
// 2. Calculate the height of the text based on that max size
|
||||
// 3. Determine how many lines the text is, and limit that number if it exceeds the max
|
||||
// 4. Set the frame size and return the YGSize. YGSize requires Float values while CGSize needs CGFloat
|
||||
func getNeededSize(maxWidth: Float) -> YGSize {
|
||||
// Create the max size and figure out the size of the entire text
|
||||
let maxSize = CGSize(width: CGFloat(maxWidth), height: CGFloat(MAXFLOAT))
|
||||
let textSize = self.attributedText.boundingRect(with: maxSize, options: .usesLineFragmentOrigin, context: nil)
|
||||
|
||||
// Figure out how many total lines there are
|
||||
let totalLines = Int(ceil(textSize.height / self.lineHeight))
|
||||
var totalLines = Int(ceil(textSize.height / self.lineHeight))
|
||||
|
||||
// Default to the text size
|
||||
var neededSize: CGSize = textSize.size
|
||||
|
||||
// If the total lines > max number, return size with the max
|
||||
if self.numberOfLines != 0, totalLines > self.numberOfLines {
|
||||
neededSize = CGSize(width: CGFloat(maxWidth), height: CGFloat(CGFloat(self.numberOfLines) * self.lineHeight))
|
||||
totalLines = self.numberOfLines
|
||||
}
|
||||
|
||||
self.frameSize = neededSize
|
||||
return YGSize(width: Float(neededSize.width), height: Float(neededSize.height))
|
||||
self.frameSize = CGSize(width: CGFloat(maxWidth), height: CGFloat(CGFloat(totalLines) * self.lineHeight))
|
||||
return YGSize(width: Float(self.frameSize.width), height: Float(self.frameSize.height))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bsky.app",
|
||||
"version": "1.68.0",
|
||||
"version": "1.73.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
@@ -12,8 +12,12 @@
|
||||
"android": "expo run:android",
|
||||
"ios": "expo run:ios",
|
||||
"web": "expo start --web",
|
||||
"use-build-number": "./scripts/useBuildNumberEnv.sh",
|
||||
"build-web": "expo export:web && node ./scripts/post-web-build.js && cp -v ./web-build/static/js/*.* ./bskyweb/static/js/",
|
||||
"build-all": "yarn intl:build && eas build --platform all",
|
||||
"build-all": "yarn intl:build && yarn use-build-number eas build --platform all",
|
||||
"build-ios": "yarn use-build-number eas build -p ios",
|
||||
"build-android": "yarn use-build-number eas build -p android",
|
||||
"build": "yarn use-build-number eas build",
|
||||
"start": "expo start --dev-client",
|
||||
"start:prod": "expo start --dev-client --no-dev --minify",
|
||||
"clean-cache": "rm -rf node_modules/.cache/babel-loader/*",
|
||||
@@ -32,17 +36,15 @@
|
||||
"perf:test:measure": "NODE_ENV=test flashlight test --bundleId xyz.blueskyweb.app --testCommand 'yarn perf:test' --duration 150000 --resultsFilePath .perf/results.json",
|
||||
"perf:test:results": "NODE_ENV=test flashlight report .perf/results.json",
|
||||
"perf:measure": "NODE_ENV=test flashlight measure",
|
||||
"intl:build": "yarn intl:check && yarn intl:compile",
|
||||
"intl:build": "yarn intl:extract && yarn intl:compile",
|
||||
"intl:check": "yarn intl:extract && git diff-index -G'(^[^\\*# /])|(^#\\w)|(^\\s+[^\\*#/])' HEAD || (echo '\n⚠️ i18n detected un-extracted translations\n' && exit 1)",
|
||||
"intl:extract": "lingui extract",
|
||||
"intl:compile": "lingui compile",
|
||||
"nuke": "rm -rf ./node_modules && rm -rf ./ios && rm -rf ./android",
|
||||
"bump": "./scripts/bumpIosBuildNumber.sh && ./scripts/bumpAndroidBuildNumber.sh",
|
||||
"bump:ios": "./scripts/bumpIosBuildNumber.sh",
|
||||
"bump:android": "./scripts/bumpAndroidBuildNumber.sh"
|
||||
"update-extensions": "scripts/updateExtensions.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "^0.9.5",
|
||||
"@atproto/api": "^0.11.1",
|
||||
"@bam.tech/react-native-image-resizer": "^3.0.4",
|
||||
"@braintree/sanitize-url": "^6.0.2",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
@@ -56,11 +58,11 @@
|
||||
"@lingui/react": "^4.5.0",
|
||||
"@mattermost/react-native-paste-input": "^0.6.4",
|
||||
"@miblanchard/react-native-slider": "^2.3.1",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@react-native-async-storage/async-storage": "1.21.0",
|
||||
"@react-native-camera-roll/camera-roll": "^5.2.2",
|
||||
"@react-native-clipboard/clipboard": "^1.10.0",
|
||||
"@react-native-community/blur": "^4.3.0",
|
||||
"@react-native-community/datetimepicker": "7.6.1",
|
||||
"@react-native-masked-view/masked-view": "0.3.0",
|
||||
"@react-native-menu/menu": "^0.8.0",
|
||||
"@react-native-picker/picker": "2.6.1",
|
||||
@@ -74,7 +76,9 @@
|
||||
"@segment/sovran-react-native": "^0.4.5",
|
||||
"@sentry/react-native": "5.5.0",
|
||||
"@tamagui/focus-scope": "^1.84.1",
|
||||
"@tanstack/query-async-storage-persister": "^5.25.0",
|
||||
"@tanstack/react-query": "^5.8.1",
|
||||
"@tanstack/react-query-persist-client": "^5.25.0",
|
||||
"@tiptap/core": "^2.0.0-beta.220",
|
||||
"@tiptap/extension-document": "^2.0.0-beta.220",
|
||||
"@tiptap/extension-hard-break": "^2.0.3",
|
||||
@@ -108,6 +112,7 @@
|
||||
"expo-image": "~1.10.3",
|
||||
"expo-image-manipulator": "^11.8.0",
|
||||
"expo-image-picker": "~14.7.1",
|
||||
"expo-linking": "^6.2.2",
|
||||
"expo-localization": "~14.8.2",
|
||||
"expo-media-library": "~15.9.1",
|
||||
"expo-notifications": "~0.27.3",
|
||||
@@ -145,8 +150,10 @@
|
||||
"react-avatar-editor": "^13.0.0",
|
||||
"react-circular-progressbar": "^2.1.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-keyed-flatten-children": "^3.0.0",
|
||||
"react-native": "0.73.2",
|
||||
"react-native-appstate-hook": "^1.0.6",
|
||||
"react-native-date-picker": "^4.4.0",
|
||||
"react-native-drawer-layout": "^4.0.0-alpha.3",
|
||||
"react-native-fs": "^2.20.0",
|
||||
"react-native-gesture-handler": "~2.14.0",
|
||||
@@ -174,6 +181,7 @@
|
||||
"react-responsive": "^9.0.2",
|
||||
"rn-fetch-blob": "^0.12.0",
|
||||
"sentry-expo": "~7.0.1",
|
||||
"statsig-react-native-expo": "^4.6.1",
|
||||
"tippy.js": "^6.3.7",
|
||||
"tlds": "^1.234.0",
|
||||
"use-deep-compare": "^1.1.0",
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
diff --git a/node_modules/@react-navigation/native/lib/commonjs/useLinking.js b/node_modules/@react-navigation/native/lib/commonjs/useLinking.js
|
||||
index ef4f368..2b0da35 100644
|
||||
--- a/node_modules/@react-navigation/native/lib/commonjs/useLinking.js
|
||||
+++ b/node_modules/@react-navigation/native/lib/commonjs/useLinking.js
|
||||
@@ -273,8 +273,12 @@ function useLinking(ref, _ref) {
|
||||
});
|
||||
const currentIndex = history.index;
|
||||
try {
|
||||
- if (nextIndex !== -1 && nextIndex < currentIndex) {
|
||||
- // An existing entry for this path exists and it's less than current index, go back to that
|
||||
+ if (
|
||||
+ nextIndex !== -1 &&
|
||||
+ nextIndex < currentIndex &&
|
||||
+ // We should only go back if the entry exists and it's less than current index
|
||||
+ history.get(nextIndex - currentIndex)
|
||||
+ ) { // An existing entry for this path exists and it's less than current index, go back to that
|
||||
await history.go(nextIndex - currentIndex);
|
||||
} else {
|
||||
// We couldn't find an existing entry to go back to, so we'll go back by the delta
|
||||
diff --git a/node_modules/@react-navigation/native/lib/module/useLinking.js b/node_modules/@react-navigation/native/lib/module/useLinking.js
|
||||
index 62a3b43..11a5a28 100644
|
||||
--- a/node_modules/@react-navigation/native/lib/module/useLinking.js
|
||||
+++ b/node_modules/@react-navigation/native/lib/module/useLinking.js
|
||||
@@ -264,8 +264,12 @@ export default function useLinking(ref, _ref) {
|
||||
});
|
||||
const currentIndex = history.index;
|
||||
try {
|
||||
- if (nextIndex !== -1 && nextIndex < currentIndex) {
|
||||
- // An existing entry for this path exists and it's less than current index, go back to that
|
||||
+ if (
|
||||
+ nextIndex !== -1 &&
|
||||
+ nextIndex < currentIndex &&
|
||||
+ // We should only go back if the entry exists and it's less than current index
|
||||
+ history.get(nextIndex - currentIndex)
|
||||
+ ) { // An existing entry for this path exists and it's less than current index, go back to that
|
||||
await history.go(nextIndex - currentIndex);
|
||||
} else {
|
||||
// We couldn't find an existing entry to go back to, so we'll go back by the delta
|
||||
diff --git a/node_modules/@react-navigation/native/src/useLinking.tsx b/node_modules/@react-navigation/native/src/useLinking.tsx
|
||||
index 3db40b7..9ba4ecd 100644
|
||||
--- a/node_modules/@react-navigation/native/src/useLinking.tsx
|
||||
+++ b/node_modules/@react-navigation/native/src/useLinking.tsx
|
||||
@@ -381,7 +381,12 @@ export default function useLinking(
|
||||
const currentIndex = history.index;
|
||||
|
||||
try {
|
||||
- if (nextIndex !== -1 && nextIndex < currentIndex) {
|
||||
+ if (
|
||||
+ nextIndex !== -1 &&
|
||||
+ nextIndex < currentIndex &&
|
||||
+ // We should only go back if the entry exists and it's less than current index
|
||||
+ history.get(nextIndex - currentIndex)
|
||||
+ ) {
|
||||
// An existing entry for this path exists and it's less than current index, go back to that
|
||||
await history.go(nextIndex - currentIndex);
|
||||
} else {
|
||||
@@ -0,0 +1,5 @@
|
||||
# React Navigation history bug patch
|
||||
|
||||
This patches react-navigation to fix the issues in https://github.com/bluesky-social/social-app/issues/710.
|
||||
|
||||
This is based on the PR found at https://github.com/react-navigation/react-navigation/pull/11833
|
||||
@@ -0,0 +1,22 @@
|
||||
# Share extension plugin for Expo
|
||||
|
||||
This plugin handles moving the necessary files into their respective iOS and Android targets and updating the build
|
||||
phases, plists, manifests, etc.
|
||||
|
||||
## Steps
|
||||
|
||||
### ios
|
||||
|
||||
1. Update entitlements
|
||||
2. Set the app group to group.<identifier>
|
||||
3. Add the extension plist
|
||||
4. Add the view controller
|
||||
5. Update the xcode project's build phases
|
||||
|
||||
### android
|
||||
|
||||
1. Update the manifest with the intents the app can receive
|
||||
|
||||
## Credits
|
||||
|
||||
Adapted from https://github.com/andrew-levy/react-native-safari-extension and https://github.com/timedtext/expo-config-plugin-ios-share-extension/blob/master/src/withShareExtensionXcodeTarget.ts
|
||||
@@ -0,0 +1,13 @@
|
||||
const {withEntitlementsPlist} = require('@expo/config-plugins')
|
||||
|
||||
const withAppEntitlements = config => {
|
||||
// eslint-disable-next-line no-shadow
|
||||
return withEntitlementsPlist(config, async config => {
|
||||
config.modResults['com.apple.security.application-groups'] = [
|
||||
`group.app.bsky`,
|
||||
]
|
||||
return config
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {withAppEntitlements}
|
||||
@@ -0,0 +1,31 @@
|
||||
const {withInfoPlist} = require('@expo/config-plugins')
|
||||
const plist = require('@expo/plist')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
|
||||
const withExtensionEntitlements = (config, {extensionName}) => {
|
||||
// eslint-disable-next-line no-shadow
|
||||
return withInfoPlist(config, config => {
|
||||
const extensionEntitlementsPath = path.join(
|
||||
config.modRequest.platformProjectRoot,
|
||||
extensionName,
|
||||
`${extensionName}.entitlements`,
|
||||
)
|
||||
|
||||
const shareExtensionEntitlements = {
|
||||
'com.apple.security.application-groups': [`group.app.bsky`],
|
||||
}
|
||||
|
||||
fs.mkdirSync(path.dirname(extensionEntitlementsPath), {
|
||||
recursive: true,
|
||||
})
|
||||
fs.writeFileSync(
|
||||
extensionEntitlementsPath,
|
||||
plist.default.build(shareExtensionEntitlements),
|
||||
)
|
||||
|
||||
return config
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {withExtensionEntitlements}
|
||||
@@ -0,0 +1,39 @@
|
||||
const {withInfoPlist} = require('@expo/config-plugins')
|
||||
const plist = require('@expo/plist')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
|
||||
const withExtensionInfoPlist = (config, {extensionName}) => {
|
||||
// eslint-disable-next-line no-shadow
|
||||
return withInfoPlist(config, config => {
|
||||
const plistPath = path.join(
|
||||
config.modRequest.projectRoot,
|
||||
'modules',
|
||||
extensionName,
|
||||
'Info.plist',
|
||||
)
|
||||
const targetPath = path.join(
|
||||
config.modRequest.platformProjectRoot,
|
||||
extensionName,
|
||||
'Info.plist',
|
||||
)
|
||||
|
||||
const extPlist = plist.default.parse(fs.readFileSync(plistPath).toString())
|
||||
|
||||
extPlist.MainAppScheme = config.scheme
|
||||
extPlist.CFBundleName = '$(PRODUCT_NAME)'
|
||||
extPlist.CFBundleDisplayName = 'Extension'
|
||||
extPlist.CFBundleIdentifier = '$(PRODUCT_BUNDLE_IDENTIFIER)'
|
||||
extPlist.CFBundleVersion = '$(CURRENT_PROJECT_VERSION)'
|
||||
extPlist.CFBundleExecutable = '$(EXECUTABLE_NAME)'
|
||||
extPlist.CFBundlePackageType = '$(PRODUCT_BUNDLE_PACKAGE_TYPE)'
|
||||
extPlist.CFBundleShortVersionString = '$(MARKETING_VERSION)'
|
||||
|
||||
fs.mkdirSync(path.dirname(targetPath), {recursive: true})
|
||||
fs.writeFileSync(targetPath, plist.default.build(extPlist))
|
||||
|
||||
return config
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {withExtensionInfoPlist}
|
||||
@@ -0,0 +1,31 @@
|
||||
const {withXcodeProject} = require('@expo/config-plugins')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
|
||||
const withExtensionViewController = (
|
||||
config,
|
||||
{controllerName, extensionName},
|
||||
) => {
|
||||
// eslint-disable-next-line no-shadow
|
||||
return withXcodeProject(config, config => {
|
||||
const controllerPath = path.join(
|
||||
config.modRequest.projectRoot,
|
||||
'modules',
|
||||
extensionName,
|
||||
`${controllerName}.swift`,
|
||||
)
|
||||
|
||||
const targetPath = path.join(
|
||||
config.modRequest.platformProjectRoot,
|
||||
extensionName,
|
||||
`${controllerName}.swift`,
|
||||
)
|
||||
|
||||
fs.mkdirSync(path.dirname(targetPath), {recursive: true})
|
||||
fs.copyFileSync(controllerPath, targetPath)
|
||||
|
||||
return config
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {withExtensionViewController}
|
||||
@@ -0,0 +1,89 @@
|
||||
const {withAndroidManifest} = require('@expo/config-plugins')
|
||||
|
||||
const withIntentFilters = config => {
|
||||
// eslint-disable-next-line no-shadow
|
||||
return withAndroidManifest(config, config => {
|
||||
const intents = [
|
||||
{
|
||||
action: [
|
||||
{
|
||||
$: {
|
||||
'android:name': 'android.intent.action.SEND',
|
||||
},
|
||||
},
|
||||
],
|
||||
category: [
|
||||
{
|
||||
$: {
|
||||
'android:name': 'android.intent.category.DEFAULT',
|
||||
},
|
||||
},
|
||||
],
|
||||
data: [
|
||||
{
|
||||
$: {
|
||||
'android:mimeType': 'image/*',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
action: [
|
||||
{
|
||||
$: {
|
||||
'android:name': 'android.intent.action.SEND',
|
||||
},
|
||||
},
|
||||
],
|
||||
category: [
|
||||
{
|
||||
$: {
|
||||
'android:name': 'android.intent.category.DEFAULT',
|
||||
},
|
||||
},
|
||||
],
|
||||
data: [
|
||||
{
|
||||
$: {
|
||||
'android:mimeType': 'text/plain',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
action: [
|
||||
{
|
||||
$: {
|
||||
'android:name': 'android.intent.action.SEND_MULTIPLE',
|
||||
},
|
||||
},
|
||||
],
|
||||
category: [
|
||||
{
|
||||
$: {
|
||||
'android:name': 'android.intent.category.DEFAULT',
|
||||
},
|
||||
},
|
||||
],
|
||||
data: [
|
||||
{
|
||||
$: {
|
||||
'android:mimeType': 'image/*',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const intentFilter =
|
||||
config.modResults.manifest.application?.[0].activity?.[0]['intent-filter']
|
||||
|
||||
if (intentFilter) {
|
||||
intentFilter.push(...intents)
|
||||
}
|
||||
|
||||
return config
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {withIntentFilters}
|
||||
@@ -0,0 +1,47 @@
|
||||
const {withPlugins} = require('@expo/config-plugins')
|
||||
const {withAppEntitlements} = require('./withAppEntitlements')
|
||||
const {withXcodeTarget} = require('./withXcodeTarget')
|
||||
const {withExtensionEntitlements} = require('./withExtensionEntitlements')
|
||||
const {withExtensionInfoPlist} = require('./withExtensionInfoPlist')
|
||||
const {withExtensionViewController} = require('./withExtensionViewController')
|
||||
const {withIntentFilters} = require('./withIntentFilters')
|
||||
|
||||
const SHARE_EXTENSION_NAME = 'Share-with-Bluesky'
|
||||
const SHARE_EXTENSION_CONTROLLER_NAME = 'ShareViewController'
|
||||
|
||||
const withShareExtensions = config => {
|
||||
return withPlugins(config, [
|
||||
// IOS
|
||||
withAppEntitlements,
|
||||
[
|
||||
withExtensionEntitlements,
|
||||
{
|
||||
extensionName: SHARE_EXTENSION_NAME,
|
||||
},
|
||||
],
|
||||
[
|
||||
withExtensionInfoPlist,
|
||||
{
|
||||
extensionName: SHARE_EXTENSION_NAME,
|
||||
},
|
||||
],
|
||||
[
|
||||
withExtensionViewController,
|
||||
{
|
||||
extensionName: SHARE_EXTENSION_NAME,
|
||||
controllerName: SHARE_EXTENSION_CONTROLLER_NAME,
|
||||
},
|
||||
],
|
||||
[
|
||||
withXcodeTarget,
|
||||
{
|
||||
extensionName: SHARE_EXTENSION_NAME,
|
||||
controllerName: SHARE_EXTENSION_CONTROLLER_NAME,
|
||||
},
|
||||
],
|
||||
// Android
|
||||
withIntentFilters,
|
||||
])
|
||||
}
|
||||
|
||||
module.exports = withShareExtensions
|
||||
@@ -0,0 +1,63 @@
|
||||
const {withXcodeProject} = require('@expo/config-plugins')
|
||||
|
||||
const withXcodeTarget = (config, {extensionName, controllerName}) => {
|
||||
// eslint-disable-next-line no-shadow
|
||||
return withXcodeProject(config, config => {
|
||||
const pbxProject = config.modResults
|
||||
|
||||
const target = pbxProject.addTarget(
|
||||
extensionName,
|
||||
'app_extension',
|
||||
extensionName,
|
||||
)
|
||||
pbxProject.addBuildPhase([], 'PBXSourcesBuildPhase', 'Sources', target.uuid)
|
||||
pbxProject.addBuildPhase(
|
||||
[],
|
||||
'PBXResourcesBuildPhase',
|
||||
'Resources',
|
||||
target.uuid,
|
||||
)
|
||||
const pbxGroupKey = pbxProject.pbxCreateGroup(extensionName, extensionName)
|
||||
pbxProject.addFile(`${extensionName}/Info.plist`, pbxGroupKey)
|
||||
pbxProject.addSourceFile(
|
||||
`${extensionName}/${controllerName}.swift`,
|
||||
{target: target.uuid},
|
||||
pbxGroupKey,
|
||||
)
|
||||
|
||||
var configurations = pbxProject.pbxXCBuildConfigurationSection()
|
||||
for (var key in configurations) {
|
||||
if (typeof configurations[key].buildSettings !== 'undefined') {
|
||||
var buildSettingsObj = configurations[key].buildSettings
|
||||
if (
|
||||
typeof buildSettingsObj.PRODUCT_NAME !== 'undefined' &&
|
||||
buildSettingsObj.PRODUCT_NAME === `"${extensionName}"`
|
||||
) {
|
||||
buildSettingsObj.CLANG_ENABLE_MODULES = 'YES'
|
||||
buildSettingsObj.INFOPLIST_FILE = `"${extensionName}/Info.plist"`
|
||||
buildSettingsObj.CODE_SIGN_ENTITLEMENTS = `"${extensionName}/${extensionName}.entitlements"`
|
||||
buildSettingsObj.CODE_SIGN_STYLE = 'Automatic'
|
||||
buildSettingsObj.CURRENT_PROJECT_VERSION = `"${config.ios?.buildNumber}"`
|
||||
buildSettingsObj.GENERATE_INFOPLIST_FILE = 'YES'
|
||||
buildSettingsObj.MARKETING_VERSION = `"${config.version}"`
|
||||
buildSettingsObj.PRODUCT_BUNDLE_IDENTIFIER = `"${config.ios?.bundleIdentifier}.${extensionName}"`
|
||||
buildSettingsObj.SWIFT_EMIT_LOC_STRINGS = 'YES'
|
||||
buildSettingsObj.SWIFT_VERSION = '5.0'
|
||||
buildSettingsObj.TARGETED_DEVICE_FAMILY = `"1,2"`
|
||||
buildSettingsObj.DEVELOPMENT_TEAM = 'B3LX46C5HS'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pbxProject.addTargetAttribute(
|
||||
'DevelopmentTeam',
|
||||
'B3LX46C5HS',
|
||||
extensionName,
|
||||
)
|
||||
pbxProject.addTargetAttribute('DevelopmentTeam', 'B3LX46C5HS')
|
||||
|
||||
return config
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {withXcodeTarget}
|
||||
@@ -0,0 +1,37 @@
|
||||
const {withAndroidManifest} = require('expo/config-plugins')
|
||||
|
||||
module.exports = function withAndroidManifestFCMIconPlugin(appConfig) {
|
||||
return withAndroidManifest(appConfig, function (decoratedAppConfig) {
|
||||
try {
|
||||
function addOrModifyMetaData(metaData, name, resource) {
|
||||
const elem = metaData.find(elem => elem.$['android:name'] === name)
|
||||
if (elem === undefined) {
|
||||
metaData.push({
|
||||
$: {
|
||||
'android:name': name,
|
||||
'android:resource': resource,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
elem.$['android:resource'] = resource
|
||||
}
|
||||
}
|
||||
const androidManifest = decoratedAppConfig.modResults.manifest
|
||||
const metaData = androidManifest.application[0]['meta-data']
|
||||
addOrModifyMetaData(
|
||||
metaData,
|
||||
'com.google.firebase.messaging.default_notification_color',
|
||||
'@color/notification_icon_color',
|
||||
)
|
||||
addOrModifyMetaData(
|
||||
metaData,
|
||||
'com.google.firebase.messaging.default_notification_icon',
|
||||
'@drawable/notification_icon',
|
||||
)
|
||||
return decoratedAppConfig
|
||||
} catch (e) {
|
||||
console.error(`withAndroidManifestFCMIconPlugin failed`, e)
|
||||
}
|
||||
return decoratedAppConfig
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
const {withAndroidStyles, AndroidConfig} = require('@expo/config-plugins')
|
||||
|
||||
module.exports = function withAndroidStylesWindowBackgroundPlugin(appConfig) {
|
||||
return withAndroidStyles(appConfig, function (decoratedAppConfig) {
|
||||
try {
|
||||
decoratedAppConfig.modResults = AndroidConfig.Styles.assignStylesValue(
|
||||
decoratedAppConfig.modResults,
|
||||
{
|
||||
add: true,
|
||||
parent: AndroidConfig.Styles.getAppThemeLightNoActionBarGroup(),
|
||||
name: 'android:windowBackground',
|
||||
value: '@drawable/splashscreen',
|
||||
},
|
||||
)
|
||||
} catch (e) {
|
||||
console.error(`withAndroidStylesWindowBackgroundPlugin failed`, e)
|
||||
}
|
||||
return decoratedAppConfig
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# Tool Scripts
|
||||
|
||||
## updateExtensions.sh
|
||||
|
||||
Updates the extensions in `/modules` with the current iOS/Android project changes.
|
||||
@@ -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,10 @@
|
||||
#!/bin/bash
|
||||
IOS_SHARE_EXTENSION_DIRECTORY="./ios/Share-with-Bluesky"
|
||||
MODULES_DIRECTORY="./modules"
|
||||
|
||||
if [ ! -d $IOS_SHARE_EXTENSION_DIRECTORY ]; then
|
||||
echo "$IOS_SHARE_EXTENSION_DIRECTORY not found inside of your iOS project."
|
||||
exit 1
|
||||
else
|
||||
cp -R $IOS_SHARE_EXTENSION_DIRECTORY $MODULES_DIRECTORY
|
||||
fi
|
||||
@@ -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 $*"
|
||||
|
||||
@@ -5,7 +5,7 @@ import React, {useState, useEffect} from 'react'
|
||||
import {RootSiblingParent} from 'react-native-root-siblings'
|
||||
import * as SplashScreen from 'expo-splash-screen'
|
||||
import {GestureHandlerRootView} from 'react-native-gesture-handler'
|
||||
import {QueryClientProvider} from '@tanstack/react-query'
|
||||
import {PersistQueryClientProvider} from '@tanstack/react-query-persist-client'
|
||||
import {
|
||||
SafeAreaProvider,
|
||||
initialWindowMetrics,
|
||||
@@ -22,7 +22,11 @@ import {s} from 'lib/styles'
|
||||
import {Shell} from 'view/shell'
|
||||
import * as notifications from 'lib/notifications/notifications'
|
||||
import * as Toast from 'view/com/util/Toast'
|
||||
import {queryClient} from 'lib/react-query'
|
||||
import {
|
||||
queryClient,
|
||||
asyncStoragePersister,
|
||||
dehydrateOptions,
|
||||
} from 'lib/react-query'
|
||||
import {TestCtrls} from 'view/com/testing/TestCtrls'
|
||||
import {Provider as ShellStateProvider} from 'state/shell'
|
||||
import {Provider as ModalStateProvider} from 'state/modals'
|
||||
@@ -33,6 +37,7 @@ import {Provider as InvitesStateProvider} from 'state/invites'
|
||||
import {Provider as PrefsStateProvider} from 'state/preferences'
|
||||
import {Provider as LoggedOutViewProvider} from 'state/shell/logged-out'
|
||||
import {Provider as SelectedFeedProvider} from 'state/shell/selected-feed'
|
||||
import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs'
|
||||
import I18nProvider from './locale/i18nProvider'
|
||||
import {
|
||||
Provider as SessionProvider,
|
||||
@@ -43,8 +48,12 @@ import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unre
|
||||
import * as persisted from '#/state/persisted'
|
||||
import {Splash} from '#/Splash'
|
||||
import {Provider as PortalProvider} from '#/components/Portal'
|
||||
import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useIntentHandler} from 'lib/hooks/useIntentHandler'
|
||||
import {StatusBar} from 'expo-status-bar'
|
||||
import {isAndroid} from 'platform/detection'
|
||||
|
||||
SplashScreen.preventAutoHideAsync()
|
||||
|
||||
@@ -53,6 +62,7 @@ function InnerApp() {
|
||||
const {resumeSession} = useSessionApi()
|
||||
const theme = useColorModeTheme()
|
||||
const {_} = useLingui()
|
||||
useIntentHandler()
|
||||
|
||||
// init
|
||||
useEffect(() => {
|
||||
@@ -67,26 +77,31 @@ function InnerApp() {
|
||||
|
||||
return (
|
||||
<SafeAreaProvider initialMetrics={initialWindowMetrics}>
|
||||
{isAndroid && <StatusBar />}
|
||||
<Alf theme={theme}>
|
||||
<Splash isReady={!isInitialLoad}>
|
||||
<React.Fragment
|
||||
// Resets the entire tree below when it changes:
|
||||
key={currentAccount?.did}>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<ThemeProvider theme={theme}>
|
||||
{/* All components should be within this provider */}
|
||||
<RootSiblingParent>
|
||||
<GestureHandlerRootView style={s.h100pct}>
|
||||
<TestCtrls />
|
||||
<Shell />
|
||||
</GestureHandlerRootView>
|
||||
</RootSiblingParent>
|
||||
</ThemeProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
<StatsigProvider>
|
||||
<LabelDefsProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<ThemeProvider theme={theme}>
|
||||
{/* All components should be within this provider */}
|
||||
<RootSiblingParent>
|
||||
<GestureHandlerRootView style={s.h100pct}>
|
||||
<TestCtrls />
|
||||
<Shell />
|
||||
</GestureHandlerRootView>
|
||||
</RootSiblingParent>
|
||||
</ThemeProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</LabelDefsProvider>
|
||||
</StatsigProvider>
|
||||
</React.Fragment>
|
||||
</Splash>
|
||||
</Alf>
|
||||
@@ -110,7 +125,9 @@ function App() {
|
||||
* that is set up in the InnerApp component above.
|
||||
*/
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<PersistQueryClientProvider
|
||||
client={queryClient}
|
||||
persistOptions={{persister: asyncStoragePersister, dehydrateOptions}}>
|
||||
<SessionProvider>
|
||||
<ShellStateProvider>
|
||||
<PrefsStateProvider>
|
||||
@@ -132,7 +149,7 @@ function App() {
|
||||
</PrefsStateProvider>
|
||||
</ShellStateProvider>
|
||||
</SessionProvider>
|
||||
</QueryClientProvider>
|
||||
</PersistQueryClientProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'lib/sentry' // must be near top
|
||||
|
||||
import React, {useState, useEffect} from 'react'
|
||||
import {QueryClientProvider} from '@tanstack/react-query'
|
||||
import {PersistQueryClientProvider} from '@tanstack/react-query-persist-client'
|
||||
import {SafeAreaProvider} from 'react-native-safe-area-context'
|
||||
import {RootSiblingParent} from 'react-native-root-siblings'
|
||||
|
||||
@@ -13,7 +13,11 @@ import {init as initPersistedState} from '#/state/persisted'
|
||||
import {Shell} from 'view/shell/index'
|
||||
import {ToastContainer} from 'view/com/util/Toast.web'
|
||||
import {ThemeProvider} from 'lib/ThemeContext'
|
||||
import {queryClient} from 'lib/react-query'
|
||||
import {
|
||||
queryClient,
|
||||
asyncStoragePersister,
|
||||
dehydrateOptions,
|
||||
} from 'lib/react-query'
|
||||
import {Provider as ShellStateProvider} from 'state/shell'
|
||||
import {Provider as ModalStateProvider} from 'state/modals'
|
||||
import {Provider as DialogStateProvider} from 'state/dialogs'
|
||||
@@ -23,6 +27,7 @@ import {Provider as InvitesStateProvider} from 'state/invites'
|
||||
import {Provider as PrefsStateProvider} from 'state/preferences'
|
||||
import {Provider as LoggedOutViewProvider} from 'state/shell/logged-out'
|
||||
import {Provider as SelectedFeedProvider} from 'state/shell/selected-feed'
|
||||
import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs'
|
||||
import I18nProvider from './locale/i18nProvider'
|
||||
import {
|
||||
Provider as SessionProvider,
|
||||
@@ -32,11 +37,14 @@ import {
|
||||
import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread'
|
||||
import * as persisted from '#/state/persisted'
|
||||
import {Provider as PortalProvider} from '#/components/Portal'
|
||||
import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
|
||||
import {useIntentHandler} from 'lib/hooks/useIntentHandler'
|
||||
|
||||
function InnerApp() {
|
||||
const {isInitialLoad, currentAccount} = useSession()
|
||||
const {resumeSession} = useSessionApi()
|
||||
const theme = useColorModeTheme()
|
||||
useIntentHandler()
|
||||
|
||||
// init
|
||||
useEffect(() => {
|
||||
@@ -52,21 +60,25 @@ function InnerApp() {
|
||||
<React.Fragment
|
||||
// Resets the entire tree below when it changes:
|
||||
key={currentAccount?.did}>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<ThemeProvider theme={theme}>
|
||||
{/* All components should be within this provider */}
|
||||
<RootSiblingParent>
|
||||
<SafeAreaProvider>
|
||||
<Shell />
|
||||
</SafeAreaProvider>
|
||||
</RootSiblingParent>
|
||||
<ToastContainer />
|
||||
</ThemeProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
<StatsigProvider>
|
||||
<LabelDefsProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<ThemeProvider theme={theme}>
|
||||
{/* All components should be within this provider */}
|
||||
<RootSiblingParent>
|
||||
<SafeAreaProvider>
|
||||
<Shell />
|
||||
</SafeAreaProvider>
|
||||
</RootSiblingParent>
|
||||
<ToastContainer />
|
||||
</ThemeProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</LabelDefsProvider>
|
||||
</StatsigProvider>
|
||||
</React.Fragment>
|
||||
</Alf>
|
||||
)
|
||||
@@ -88,7 +100,9 @@ function App() {
|
||||
* that is set up in the InnerApp component above.
|
||||
*/
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<PersistQueryClientProvider
|
||||
client={queryClient}
|
||||
persistOptions={{persister: asyncStoragePersister, dehydrateOptions}}>
|
||||
<SessionProvider>
|
||||
<ShellStateProvider>
|
||||
<PrefsStateProvider>
|
||||
@@ -110,7 +124,7 @@ function App() {
|
||||
</PrefsStateProvider>
|
||||
</ShellStateProvider>
|
||||
</SessionProvider>
|
||||
</QueryClientProvider>
|
||||
</PersistQueryClientProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ import {SearchScreen} from './view/screens/Search'
|
||||
import {FeedsScreen} from './view/screens/Feeds'
|
||||
import {NotificationsScreen} from './view/screens/Notifications'
|
||||
import {ListsScreen} from './view/screens/Lists'
|
||||
import {ModerationScreen} from './view/screens/Moderation'
|
||||
import {ModerationScreen} from '#/screens/Moderation'
|
||||
import {ModerationModlistsScreen} from './view/screens/ModerationModlists'
|
||||
import {NotFoundScreen} from './view/screens/NotFound'
|
||||
import {SettingsScreen} from './view/screens/Settings'
|
||||
@@ -61,6 +61,7 @@ import {PostThreadScreen} from './view/screens/PostThread'
|
||||
import {PostLikedByScreen} from './view/screens/PostLikedBy'
|
||||
import {PostRepostedByScreen} from './view/screens/PostRepostedBy'
|
||||
import {Storybook} from './view/screens/Storybook'
|
||||
import {DebugModScreen} from './view/screens/DebugMod'
|
||||
import {LogScreen} from './view/screens/Log'
|
||||
import {SupportScreen} from './view/screens/Support'
|
||||
import {PrivacyPolicyScreen} from './view/screens/PrivacyPolicy'
|
||||
@@ -71,12 +72,15 @@ import {AppPasswords} from 'view/screens/AppPasswords'
|
||||
import {ModerationMutedAccounts} from 'view/screens/ModerationMutedAccounts'
|
||||
import {ModerationBlockedAccounts} from 'view/screens/ModerationBlockedAccounts'
|
||||
import {SavedFeeds} from 'view/screens/SavedFeeds'
|
||||
import {PreferencesHomeFeed} from 'view/screens/PreferencesHomeFeed'
|
||||
import {PreferencesFollowingFeed} from 'view/screens/PreferencesFollowingFeed'
|
||||
import {PreferencesThreads} from 'view/screens/PreferencesThreads'
|
||||
import {PreferencesExternalEmbeds} from '#/view/screens/PreferencesExternalEmbeds'
|
||||
import {createNativeStackNavigatorWithAuth} from './view/shell/createNativeStackNavigatorWithAuth'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {i18n, MessageDescriptor} from '@lingui/core'
|
||||
import HashtagScreen from '#/screens/Hashtag'
|
||||
import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy'
|
||||
import {logEvent, attachRouteToLogEvents} from './lib/statsig/statsig'
|
||||
|
||||
const navigationRef = createNavigationContainerRef<AllNavigatorParams>()
|
||||
|
||||
@@ -196,11 +200,21 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
|
||||
getComponent={() => ProfileFeedLikedByScreen}
|
||||
options={{title: title(msg`Liked by`)}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="ProfileLabelerLikedBy"
|
||||
getComponent={() => ProfileLabelerLikedByScreen}
|
||||
options={{title: title(msg`Liked by`)}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="Debug"
|
||||
getComponent={() => Storybook}
|
||||
options={{title: title(msg`Storybook`), requireAuth: true}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="DebugMod"
|
||||
getComponent={() => DebugModScreen}
|
||||
options={{title: title(msg`Moderation states`), requireAuth: true}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="Log"
|
||||
getComponent={() => LogScreen}
|
||||
@@ -242,9 +256,12 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
|
||||
options={{title: title(msg`Edit My Feeds`), requireAuth: true}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="PreferencesHomeFeed"
|
||||
getComponent={() => PreferencesHomeFeed}
|
||||
options={{title: title(msg`Home Feed Preferences`), requireAuth: true}}
|
||||
name="PreferencesFollowingFeed"
|
||||
getComponent={() => PreferencesFollowingFeed}
|
||||
options={{
|
||||
title: title(msg`Following Feed Preferences`),
|
||||
requireAuth: true,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="PreferencesThreads"
|
||||
@@ -259,6 +276,11 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
|
||||
requireAuth: true,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="Hashtag"
|
||||
getComponent={() => HashtagScreen}
|
||||
options={{title: title(msg`Hashtag`)}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -457,7 +479,8 @@ const FlatNavigator = () => {
|
||||
*/
|
||||
|
||||
const LINKING = {
|
||||
prefixes: ['bsky://', 'https://bsky.app'],
|
||||
// TODO figure out what we are going to use
|
||||
prefixes: ['bsky://', 'bluesky://', 'https://bsky.app'],
|
||||
|
||||
getPathFromState(state: State) {
|
||||
// find the current node in the navigation tree
|
||||
@@ -476,6 +499,18 @@ const LINKING = {
|
||||
|
||||
getStateFromPath(path: string) {
|
||||
const [name, params] = router.matchPath(path)
|
||||
|
||||
// Any time we receive a url that starts with `intent/` we want to ignore it here. It will be handled in the
|
||||
// intent handler hook. We should check for the trailing slash, because if there isn't one then it isn't a valid
|
||||
// intent
|
||||
// On web, there is no route state that's created by default, so we should initialize it as the home route. On
|
||||
// native, since the home tab and the home screen are defined as initial routes, we don't need to return a state
|
||||
// since it will be created by react-navigation.
|
||||
if (path.includes('intent/')) {
|
||||
if (isNative) return
|
||||
return buildStateObject('Flat', 'Home', params)
|
||||
}
|
||||
|
||||
if (isNative) {
|
||||
if (name === 'Search') {
|
||||
return buildStateObject('SearchTab', 'Search', params)
|
||||
@@ -494,7 +529,8 @@ const LINKING = {
|
||||
},
|
||||
])
|
||||
} else {
|
||||
return buildStateObject('Flat', name, params)
|
||||
const res = buildStateObject('Flat', name, params)
|
||||
return res
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -519,6 +555,7 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
|
||||
linking={LINKING}
|
||||
theme={theme}
|
||||
onReady={() => {
|
||||
attachRouteToLogEvents(getCurrentRouteName)
|
||||
logModuleInitTime()
|
||||
onReady()
|
||||
}}>
|
||||
@@ -527,6 +564,10 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
|
||||
)
|
||||
}
|
||||
|
||||
function getCurrentRouteName() {
|
||||
return navigationRef.getCurrentRoute()?.name
|
||||
}
|
||||
|
||||
/**
|
||||
* These helpers can be used from outside of the RoutesContainer
|
||||
* (eg in the state models).
|
||||
@@ -626,11 +667,16 @@ function logModuleInitTime() {
|
||||
return
|
||||
}
|
||||
didInit = true
|
||||
|
||||
const initMs = Math.round(
|
||||
// @ts-ignore Emitted by Metro in the bundle prelude
|
||||
performance.now() - global.__BUNDLE_START_TIME__,
|
||||
)
|
||||
console.log(`Time to first paint: ${initMs} ms`)
|
||||
logEvent('init', {
|
||||
initMs,
|
||||
})
|
||||
|
||||
if (__DEV__) {
|
||||
// This log is noisy, so keep false committed
|
||||
const shouldLog = false
|
||||
|
||||
@@ -181,6 +181,8 @@ export function Splash(props: React.PropsWithChildren<Props>) {
|
||||
|
||||
const logoAnimations =
|
||||
reduceMotion === true ? reducedLogoAnimation : logoAnimation
|
||||
// special off-spec color for dark mode
|
||||
const logoBg = isDarkMode ? '#0F1824' : '#fff'
|
||||
|
||||
return (
|
||||
<View style={{flex: 1}} onLayout={onLayout}>
|
||||
@@ -232,7 +234,7 @@ export function Splash(props: React.PropsWithChildren<Props>) {
|
||||
},
|
||||
]}>
|
||||
<AnimatedLogo
|
||||
fill="#fff"
|
||||
fill={logoBg}
|
||||
style={[{opacity: 0}, logoAnimations]}
|
||||
/>
|
||||
</Animated.View>
|
||||
@@ -253,7 +255,7 @@ export function Splash(props: React.PropsWithChildren<Props>) {
|
||||
transform: [{translateY: -(insets.top / 2)}, {scale: 0.1}], // scale from 1000px to 100px
|
||||
},
|
||||
]}>
|
||||
<AnimatedLogo fill="#fff" style={[logoAnimations]} />
|
||||
<AnimatedLogo fill={logoBg} style={[logoAnimations]} />
|
||||
</Animated.View>
|
||||
}>
|
||||
{!isAnimationComplete && (
|
||||
@@ -261,10 +263,7 @@ export function Splash(props: React.PropsWithChildren<Props>) {
|
||||
style={[
|
||||
StyleSheet.absoluteFillObject,
|
||||
{
|
||||
backgroundColor: isDarkMode
|
||||
? // special off-spec color for dark mode
|
||||
'#0F1824'
|
||||
: '#fff',
|
||||
backgroundColor: logoBg,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import {Platform} from 'react-native'
|
||||
import {web, native} from '#/alf/util/platform'
|
||||
import * as tokens from '#/alf/tokens'
|
||||
|
||||
export const atoms = {
|
||||
@@ -5,7 +7,7 @@ export const atoms = {
|
||||
* Positioning
|
||||
*/
|
||||
fixed: {
|
||||
position: 'fixed',
|
||||
position: Platform.select({web: 'fixed', native: 'absolute'}) as 'absolute',
|
||||
},
|
||||
absolute: {
|
||||
position: 'absolute',
|
||||
@@ -48,6 +50,9 @@ export const atoms = {
|
||||
h_full: {
|
||||
height: '100%',
|
||||
},
|
||||
h_full_vh: web({
|
||||
height: '100vh',
|
||||
}),
|
||||
|
||||
/*
|
||||
* Border radius
|
||||
@@ -110,9 +115,18 @@ export const atoms = {
|
||||
flex_row: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
flex_col_reverse: {
|
||||
flexDirection: 'column-reverse',
|
||||
},
|
||||
flex_row_reverse: {
|
||||
flexDirection: 'row-reverse',
|
||||
},
|
||||
flex_wrap: {
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
flex_0: {
|
||||
flex: web('0 0 auto') || (native(0) as number),
|
||||
},
|
||||
flex_1: {
|
||||
flex: 1,
|
||||
},
|
||||
@@ -176,46 +190,65 @@ export const atoms = {
|
||||
},
|
||||
text_2xs: {
|
||||
fontSize: tokens.fontSize._2xs,
|
||||
letterSpacing: 0.25,
|
||||
},
|
||||
text_xs: {
|
||||
fontSize: tokens.fontSize.xs,
|
||||
letterSpacing: 0.25,
|
||||
},
|
||||
text_sm: {
|
||||
fontSize: tokens.fontSize.sm,
|
||||
letterSpacing: 0.25,
|
||||
},
|
||||
text_md: {
|
||||
fontSize: tokens.fontSize.md,
|
||||
letterSpacing: 0.25,
|
||||
},
|
||||
text_lg: {
|
||||
fontSize: tokens.fontSize.lg,
|
||||
letterSpacing: 0.25,
|
||||
},
|
||||
text_xl: {
|
||||
fontSize: tokens.fontSize.xl,
|
||||
letterSpacing: 0.25,
|
||||
},
|
||||
text_2xl: {
|
||||
fontSize: tokens.fontSize._2xl,
|
||||
letterSpacing: 0.25,
|
||||
},
|
||||
text_3xl: {
|
||||
fontSize: tokens.fontSize._3xl,
|
||||
letterSpacing: 0.25,
|
||||
},
|
||||
text_4xl: {
|
||||
fontSize: tokens.fontSize._4xl,
|
||||
letterSpacing: 0.25,
|
||||
},
|
||||
text_5xl: {
|
||||
fontSize: tokens.fontSize._5xl,
|
||||
letterSpacing: 0.25,
|
||||
},
|
||||
leading_tight: {
|
||||
lineHeight: 1.15,
|
||||
},
|
||||
leading_snug: {
|
||||
lineHeight: 1.25,
|
||||
lineHeight: 1.3,
|
||||
},
|
||||
leading_normal: {
|
||||
lineHeight: 1.5,
|
||||
},
|
||||
tracking_normal: {
|
||||
letterSpacing: 0,
|
||||
},
|
||||
tracking_wide: {
|
||||
letterSpacing: 0.25,
|
||||
},
|
||||
font_normal: {
|
||||
fontWeight: tokens.fontWeight.normal,
|
||||
},
|
||||
font_semibold: {
|
||||
fontWeight: '500',
|
||||
},
|
||||
font_bold: {
|
||||
fontWeight: tokens.fontWeight.semibold,
|
||||
},
|
||||
@@ -501,6 +534,10 @@ export const atoms = {
|
||||
/*
|
||||
* Margin
|
||||
*/
|
||||
mx_auto: {
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
},
|
||||
m_2xs: {
|
||||
margin: tokens.space._2xs,
|
||||
},
|
||||
|
||||
@@ -17,7 +17,7 @@ const breakpoints: {
|
||||
[key: string]: number
|
||||
} = {
|
||||
gtMobile: 800,
|
||||
gtTablet: 1200,
|
||||
gtTablet: 1300,
|
||||
}
|
||||
function getActiveBreakpoints({width}: {width: number}) {
|
||||
const active: (keyof typeof breakpoints)[] = Object.keys(breakpoints).filter(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as tokens from '#/alf/tokens'
|
||||
import type {Mutable} from '#/alf/types'
|
||||
import {atoms} from '#/alf/atoms'
|
||||
import {BLUE_HUE, GREEN_HUE, RED_HUE} from '#/alf/util/colorGeneration'
|
||||
|
||||
export type ThemeName = 'light' | 'dim' | 'dark'
|
||||
export type ReadonlyTheme = typeof light
|
||||
@@ -132,21 +133,63 @@ export const darkPalette: Palette = {
|
||||
|
||||
export const dimPalette: Palette = {
|
||||
...darkPalette,
|
||||
black: tokens.color.gray_1000,
|
||||
black: `hsl(${BLUE_HUE}, 28%, ${tokens.dimScale[0]}%)`,
|
||||
|
||||
contrast_25: tokens.color.gray_975,
|
||||
contrast_50: tokens.color.gray_950,
|
||||
contrast_100: tokens.color.gray_900,
|
||||
contrast_200: tokens.color.gray_800,
|
||||
contrast_300: tokens.color.gray_700,
|
||||
contrast_400: tokens.color.gray_600,
|
||||
contrast_500: tokens.color.gray_500,
|
||||
contrast_600: tokens.color.gray_400,
|
||||
contrast_700: tokens.color.gray_300,
|
||||
contrast_800: tokens.color.gray_200,
|
||||
contrast_900: tokens.color.gray_100,
|
||||
contrast_950: tokens.color.gray_50,
|
||||
contrast_975: tokens.color.gray_25,
|
||||
contrast_25: `hsl(${BLUE_HUE}, 28%, ${tokens.dimScale[1]}%)`,
|
||||
contrast_50: `hsl(${BLUE_HUE}, 28%, ${tokens.dimScale[2]}%)`,
|
||||
contrast_100: `hsl(${BLUE_HUE}, 28%, ${tokens.dimScale[3]}%)`,
|
||||
contrast_200: `hsl(${BLUE_HUE}, 28%, ${tokens.dimScale[4]}%)`,
|
||||
contrast_300: `hsl(${BLUE_HUE}, 24%, ${tokens.dimScale[5]}%)`,
|
||||
contrast_400: `hsl(${BLUE_HUE}, 24%, ${tokens.dimScale[6]}%)`,
|
||||
contrast_500: `hsl(${BLUE_HUE}, 20%, ${tokens.dimScale[7]}%)`,
|
||||
contrast_600: `hsl(${BLUE_HUE}, 20%, ${tokens.dimScale[8]}%)`,
|
||||
contrast_700: `hsl(${BLUE_HUE}, 20%, ${tokens.dimScale[9]}%)`,
|
||||
contrast_800: `hsl(${BLUE_HUE}, 20%, ${tokens.dimScale[10]}%)`,
|
||||
contrast_900: `hsl(${BLUE_HUE}, 20%, ${tokens.dimScale[11]}%)`,
|
||||
contrast_950: `hsl(${BLUE_HUE}, 20%, ${tokens.dimScale[12]}%)`,
|
||||
contrast_975: `hsl(${BLUE_HUE}, 20%, ${tokens.dimScale[13]}%)`,
|
||||
|
||||
primary_25: `hsl(${BLUE_HUE}, 99%, ${tokens.dimScale[13]}%)`,
|
||||
primary_50: `hsl(${BLUE_HUE}, 99%, ${tokens.dimScale[12]}%)`,
|
||||
primary_100: `hsl(${BLUE_HUE}, 99%, ${tokens.dimScale[11]}%)`,
|
||||
primary_200: `hsl(${BLUE_HUE}, 99%, ${tokens.dimScale[10]}%)`,
|
||||
primary_300: `hsl(${BLUE_HUE}, 99%, ${tokens.dimScale[9]}%)`,
|
||||
primary_400: `hsl(${BLUE_HUE}, 99%, ${tokens.dimScale[8]}%)`,
|
||||
primary_500: `hsl(${BLUE_HUE}, 99%, ${tokens.dimScale[7]}%)`,
|
||||
primary_600: `hsl(${BLUE_HUE}, 95%, ${tokens.dimScale[6]}%)`,
|
||||
primary_700: `hsl(${BLUE_HUE}, 90%, ${tokens.dimScale[5]}%)`,
|
||||
primary_800: `hsl(${BLUE_HUE}, 82%, ${tokens.dimScale[4]}%)`,
|
||||
primary_900: `hsl(${BLUE_HUE}, 70%, ${tokens.dimScale[3]}%)`,
|
||||
primary_950: `hsl(${BLUE_HUE}, 60%, ${tokens.dimScale[2]}%)`,
|
||||
primary_975: `hsl(${BLUE_HUE}, 50%, ${tokens.dimScale[1]}%)`,
|
||||
|
||||
positive_25: `hsl(${GREEN_HUE}, 82%, ${tokens.dimScale[13]}%)`,
|
||||
positive_50: `hsl(${GREEN_HUE}, 82%, ${tokens.dimScale[12]}%)`,
|
||||
positive_100: `hsl(${GREEN_HUE}, 82%, ${tokens.dimScale[11]}%)`,
|
||||
positive_200: `hsl(${GREEN_HUE}, 82%, ${tokens.dimScale[10]}%)`,
|
||||
positive_300: `hsl(${GREEN_HUE}, 82%, ${tokens.dimScale[9]}%)`,
|
||||
positive_400: `hsl(${GREEN_HUE}, 82%, ${tokens.dimScale[8]}%)`,
|
||||
positive_500: `hsl(${GREEN_HUE}, 82%, ${tokens.dimScale[7]}%)`,
|
||||
positive_600: `hsl(${GREEN_HUE}, 82%, ${tokens.dimScale[6]}%)`,
|
||||
positive_700: `hsl(${GREEN_HUE}, 82%, ${tokens.dimScale[5]}%)`,
|
||||
positive_800: `hsl(${GREEN_HUE}, 82%, ${tokens.dimScale[4]}%)`,
|
||||
positive_900: `hsl(${GREEN_HUE}, 70%, ${tokens.dimScale[3]}%)`,
|
||||
positive_950: `hsl(${GREEN_HUE}, 60%, ${tokens.dimScale[2]}%)`,
|
||||
positive_975: `hsl(${GREEN_HUE}, 50%, ${tokens.dimScale[1]}%)`,
|
||||
|
||||
negative_25: `hsl(${RED_HUE}, 91%, ${tokens.dimScale[13]}%)`,
|
||||
negative_50: `hsl(${RED_HUE}, 91%, ${tokens.dimScale[12]}%)`,
|
||||
negative_100: `hsl(${RED_HUE}, 91%, ${tokens.dimScale[11]}%)`,
|
||||
negative_200: `hsl(${RED_HUE}, 91%, ${tokens.dimScale[10]}%)`,
|
||||
negative_300: `hsl(${RED_HUE}, 91%, ${tokens.dimScale[9]}%)`,
|
||||
negative_400: `hsl(${RED_HUE}, 91%, ${tokens.dimScale[8]}%)`,
|
||||
negative_500: `hsl(${RED_HUE}, 91%, ${tokens.dimScale[7]}%)`,
|
||||
negative_600: `hsl(${RED_HUE}, 91%, ${tokens.dimScale[6]}%)`,
|
||||
negative_700: `hsl(${RED_HUE}, 91%, ${tokens.dimScale[5]}%)`,
|
||||
negative_800: `hsl(${RED_HUE}, 88%, ${tokens.dimScale[4]}%)`,
|
||||
negative_900: `hsl(${RED_HUE}, 84%, ${tokens.dimScale[3]}%)`,
|
||||
negative_950: `hsl(${RED_HUE}, 80%, ${tokens.dimScale[2]}%)`,
|
||||
negative_975: `hsl(${RED_HUE}, 70%, ${tokens.dimScale[1]}%)`,
|
||||
} as const
|
||||
|
||||
export const light = {
|
||||
@@ -325,6 +368,7 @@ export const dark: Theme = {
|
||||
export const dim: Theme = {
|
||||
...dark,
|
||||
name: 'dim',
|
||||
palette: dimPalette,
|
||||
atoms: {
|
||||
...dark.atoms,
|
||||
text: {
|
||||
@@ -393,5 +437,20 @@ export const dim: Theme = {
|
||||
border_contrast_high: {
|
||||
borderColor: dimPalette.contrast_300,
|
||||
},
|
||||
shadow_sm: {
|
||||
...atoms.shadow_sm,
|
||||
shadowOpacity: 0.7,
|
||||
shadowColor: `hsl(${BLUE_HUE}, 28%, 6%)`,
|
||||
},
|
||||
shadow_md: {
|
||||
...atoms.shadow_md,
|
||||
shadowOpacity: 0.7,
|
||||
shadowColor: `hsl(${BLUE_HUE}, 28%, 6%)`,
|
||||
},
|
||||
shadow_lg: {
|
||||
...atoms.shadow_lg,
|
||||
shadowOpacity: 0.7,
|
||||
shadowColor: `hsl(${BLUE_HUE}, 28%, 6%)`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,25 +1,35 @@
|
||||
const BLUE_HUE = 211
|
||||
const RED_HUE = 346
|
||||
const GREEN_HUE = 152
|
||||
import {
|
||||
BLUE_HUE,
|
||||
RED_HUE,
|
||||
GREEN_HUE,
|
||||
generateScale,
|
||||
} from '#/alf/util/colorGeneration'
|
||||
|
||||
export const scale = generateScale(6, 100)
|
||||
// dim shifted 6% lighter
|
||||
export const dimScale = generateScale(12, 100)
|
||||
|
||||
export const color = {
|
||||
trueBlack: '#000000',
|
||||
|
||||
gray_0: `hsl(${BLUE_HUE}, 20%, 100%)`,
|
||||
gray_25: `hsl(${BLUE_HUE}, 20%, 97%)`,
|
||||
gray_50: `hsl(${BLUE_HUE}, 20%, 95%)`,
|
||||
gray_100: `hsl(${BLUE_HUE}, 20%, 90%)`,
|
||||
gray_200: `hsl(${BLUE_HUE}, 20%, 80%)`,
|
||||
gray_300: `hsl(${BLUE_HUE}, 20%, 70%)`,
|
||||
gray_400: `hsl(${BLUE_HUE}, 20%, 60%)`,
|
||||
gray_500: `hsl(${BLUE_HUE}, 20%, 50%)`,
|
||||
gray_600: `hsl(${BLUE_HUE}, 24%, 42%)`,
|
||||
gray_700: `hsl(${BLUE_HUE}, 24%, 34%)`,
|
||||
gray_800: `hsl(${BLUE_HUE}, 28%, 26%)`,
|
||||
gray_900: `hsl(${BLUE_HUE}, 28%, 18%)`,
|
||||
gray_950: `hsl(${BLUE_HUE}, 28%, 10%)`,
|
||||
gray_975: `hsl(${BLUE_HUE}, 28%, 7%)`,
|
||||
gray_1000: `hsl(${BLUE_HUE}, 28%, 4%)`,
|
||||
temp_purple: 'rgb(105 0 255)',
|
||||
temp_purple_dark: 'rgb(83 0 202)',
|
||||
|
||||
gray_0: `hsl(${BLUE_HUE}, 20%, ${scale[14]}%)`,
|
||||
gray_25: `hsl(${BLUE_HUE}, 20%, ${scale[13]}%)`,
|
||||
gray_50: `hsl(${BLUE_HUE}, 20%, ${scale[12]}%)`,
|
||||
gray_100: `hsl(${BLUE_HUE}, 20%, ${scale[11]}%)`,
|
||||
gray_200: `hsl(${BLUE_HUE}, 20%, ${scale[10]}%)`,
|
||||
gray_300: `hsl(${BLUE_HUE}, 20%, ${scale[9]}%)`,
|
||||
gray_400: `hsl(${BLUE_HUE}, 20%, ${scale[8]}%)`,
|
||||
gray_500: `hsl(${BLUE_HUE}, 20%, ${scale[7]}%)`,
|
||||
gray_600: `hsl(${BLUE_HUE}, 24%, ${scale[6]}%)`,
|
||||
gray_700: `hsl(${BLUE_HUE}, 24%, ${scale[5]}%)`,
|
||||
gray_800: `hsl(${BLUE_HUE}, 28%, ${scale[4]}%)`,
|
||||
gray_900: `hsl(${BLUE_HUE}, 28%, ${scale[3]}%)`,
|
||||
gray_950: `hsl(${BLUE_HUE}, 28%, ${scale[2]}%)`,
|
||||
gray_975: `hsl(${BLUE_HUE}, 28%, ${scale[1]}%)`,
|
||||
gray_1000: `hsl(${BLUE_HUE}, 28%, ${scale[0]}%)`,
|
||||
|
||||
blue_25: `hsl(${BLUE_HUE}, 99%, 97%)`,
|
||||
blue_50: `hsl(${BLUE_HUE}, 99%, 95%)`,
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
export const BLUE_HUE = 211
|
||||
export const RED_HUE = 346
|
||||
export const GREEN_HUE = 152
|
||||
|
||||
/**
|
||||
* Smooth progression of lightness "stops" for generating HSL colors.
|
||||
*/
|
||||
export const COLOR_STOPS = [
|
||||
0, 0.05, 0.1, 0.15, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.85, 0.9, 0.95, 1,
|
||||
]
|
||||
|
||||
export function generateScale(start: number, end: number) {
|
||||
const range = end - start
|
||||
return COLOR_STOPS.map(stop => {
|
||||
return start + range * stop
|
||||
})
|
||||
}
|
||||
@@ -1,25 +1,25 @@
|
||||
import {Platform} from 'react-native'
|
||||
import {isAndroid, isIOS, isNative, isWeb} from 'platform/detection'
|
||||
|
||||
export function web(value: any) {
|
||||
return Platform.select({
|
||||
web: value,
|
||||
})
|
||||
if (isWeb) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
export function ios(value: any) {
|
||||
return Platform.select({
|
||||
ios: value,
|
||||
})
|
||||
if (isIOS) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
export function android(value: any) {
|
||||
return Platform.select({
|
||||
android: value,
|
||||
})
|
||||
if (isAndroid) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
export function native(value: any) {
|
||||
return Platform.select({
|
||||
native: value,
|
||||
})
|
||||
if (isNative) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import LinearGradient from 'react-native-linear-gradient'
|
||||
|
||||
import {useTheme, atoms as a, tokens, android, flatten} from '#/alf'
|
||||
import {Props as SVGIconProps} from '#/components/icons/common'
|
||||
import {normalizeTextStyles} from '#/components/Typography'
|
||||
|
||||
export type ButtonVariant = 'solid' | 'outline' | 'ghost' | 'gradient'
|
||||
export type ButtonColor =
|
||||
@@ -27,7 +28,7 @@ export type ButtonColor =
|
||||
| 'gradient_sunset'
|
||||
| 'gradient_nordic'
|
||||
| 'gradient_bonfire'
|
||||
export type ButtonSize = 'tiny' | 'small' | 'large'
|
||||
export type ButtonSize = 'tiny' | 'small' | 'medium' | 'large'
|
||||
export type ButtonShape = 'round' | 'square' | 'default'
|
||||
export type VariantProps = {
|
||||
/**
|
||||
@@ -139,7 +140,7 @@ export function Button({
|
||||
}))
|
||||
}, [setState])
|
||||
|
||||
const {baseStyles, hoverStyles, focusStyles} = React.useMemo(() => {
|
||||
const {baseStyles, hoverStyles} = React.useMemo(() => {
|
||||
const baseStyles: ViewStyle[] = []
|
||||
const hoverStyles: ViewStyle[] = []
|
||||
const light = t.name === 'light'
|
||||
@@ -165,7 +166,7 @@ export function Button({
|
||||
|
||||
if (!disabled) {
|
||||
baseStyles.push(a.border, {
|
||||
borderColor: tokens.color.blue_500,
|
||||
borderColor: t.palette.primary_500,
|
||||
})
|
||||
hoverStyles.push(a.border, {
|
||||
backgroundColor: light
|
||||
@@ -174,7 +175,7 @@ export function Button({
|
||||
})
|
||||
} else {
|
||||
baseStyles.push(a.border, {
|
||||
borderColor: light ? tokens.color.blue_200 : tokens.color.blue_900,
|
||||
borderColor: light ? t.palette.primary_200 : t.palette.primary_900,
|
||||
})
|
||||
}
|
||||
} else if (variant === 'ghost') {
|
||||
@@ -191,20 +192,14 @@ export function Button({
|
||||
if (variant === 'solid') {
|
||||
if (!disabled) {
|
||||
baseStyles.push({
|
||||
backgroundColor: light
|
||||
? tokens.color.gray_50
|
||||
: tokens.color.gray_900,
|
||||
backgroundColor: t.palette.contrast_25,
|
||||
})
|
||||
hoverStyles.push({
|
||||
backgroundColor: light
|
||||
? tokens.color.gray_100
|
||||
: tokens.color.gray_950,
|
||||
backgroundColor: t.palette.contrast_50,
|
||||
})
|
||||
} else {
|
||||
baseStyles.push({
|
||||
backgroundColor: light
|
||||
? tokens.color.gray_200
|
||||
: tokens.color.gray_950,
|
||||
backgroundColor: t.palette.contrast_100,
|
||||
})
|
||||
}
|
||||
} else if (variant === 'outline') {
|
||||
@@ -214,21 +209,19 @@ export function Button({
|
||||
|
||||
if (!disabled) {
|
||||
baseStyles.push(a.border, {
|
||||
borderColor: light ? tokens.color.gray_300 : tokens.color.gray_700,
|
||||
borderColor: t.palette.contrast_300,
|
||||
})
|
||||
hoverStyles.push(a.border, t.atoms.bg_contrast_50)
|
||||
hoverStyles.push(t.atoms.bg_contrast_50)
|
||||
} else {
|
||||
baseStyles.push(a.border, {
|
||||
borderColor: light ? tokens.color.gray_200 : tokens.color.gray_800,
|
||||
borderColor: t.palette.contrast_200,
|
||||
})
|
||||
}
|
||||
} else if (variant === 'ghost') {
|
||||
if (!disabled) {
|
||||
baseStyles.push(t.atoms.bg)
|
||||
hoverStyles.push({
|
||||
backgroundColor: light
|
||||
? tokens.color.gray_100
|
||||
: tokens.color.gray_900,
|
||||
backgroundColor: t.palette.contrast_100,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -236,14 +229,14 @@ export function Button({
|
||||
if (variant === 'solid') {
|
||||
if (!disabled) {
|
||||
baseStyles.push({
|
||||
backgroundColor: t.palette.negative_400,
|
||||
backgroundColor: t.palette.negative_500,
|
||||
})
|
||||
hoverStyles.push({
|
||||
backgroundColor: t.palette.negative_500,
|
||||
backgroundColor: t.palette.negative_600,
|
||||
})
|
||||
} else {
|
||||
baseStyles.push({
|
||||
backgroundColor: t.palette.negative_600,
|
||||
backgroundColor: t.palette.negative_700,
|
||||
})
|
||||
}
|
||||
} else if (variant === 'outline') {
|
||||
@@ -253,7 +246,7 @@ export function Button({
|
||||
|
||||
if (!disabled) {
|
||||
baseStyles.push(a.border, {
|
||||
borderColor: t.palette.negative_400,
|
||||
borderColor: t.palette.negative_500,
|
||||
})
|
||||
hoverStyles.push(a.border, {
|
||||
backgroundColor: light
|
||||
@@ -273,7 +266,7 @@ export function Button({
|
||||
hoverStyles.push({
|
||||
backgroundColor: light
|
||||
? t.palette.negative_100
|
||||
: t.palette.negative_950,
|
||||
: t.palette.negative_975,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -282,6 +275,8 @@ export function Button({
|
||||
if (shape === 'default') {
|
||||
if (size === 'large') {
|
||||
baseStyles.push({paddingVertical: 15}, a.px_2xl, a.rounded_sm, a.gap_md)
|
||||
} else if (size === 'medium') {
|
||||
baseStyles.push({paddingVertical: 12}, a.px_2xl, a.rounded_sm, a.gap_md)
|
||||
} else if (size === 'small') {
|
||||
baseStyles.push({paddingVertical: 9}, a.px_lg, a.rounded_sm, a.gap_sm)
|
||||
} else if (size === 'tiny') {
|
||||
@@ -314,12 +309,6 @@ export function Button({
|
||||
return {
|
||||
baseStyles,
|
||||
hoverStyles,
|
||||
focusStyles: [
|
||||
...hoverStyles,
|
||||
{
|
||||
outline: 0,
|
||||
} as ViewStyle,
|
||||
],
|
||||
}
|
||||
}, [t, variant, color, size, shape, disabled])
|
||||
|
||||
@@ -382,10 +371,8 @@ export function Button({
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
a.justify_center,
|
||||
flattenedBaseStyles,
|
||||
...(state.hovered || state.pressed ? hoverStyles : []),
|
||||
...(state.focused ? focusStyles : []),
|
||||
flatten(style),
|
||||
]}
|
||||
onPressIn={onPressIn}
|
||||
@@ -404,7 +391,7 @@ export function Button({
|
||||
]}>
|
||||
<LinearGradient
|
||||
colors={
|
||||
state.hovered || state.pressed || state.focused
|
||||
state.hovered || state.pressed
|
||||
? gradientHoverColors
|
||||
: gradientColors
|
||||
}
|
||||
@@ -461,31 +448,31 @@ export function useSharedButtonTextStyles() {
|
||||
if (variant === 'solid' || variant === 'gradient') {
|
||||
if (!disabled) {
|
||||
baseStyles.push({
|
||||
color: light ? tokens.color.gray_700 : tokens.color.gray_100,
|
||||
color: t.palette.contrast_700,
|
||||
})
|
||||
} else {
|
||||
baseStyles.push({
|
||||
color: light ? tokens.color.gray_400 : tokens.color.gray_700,
|
||||
color: t.palette.contrast_400,
|
||||
})
|
||||
}
|
||||
} else if (variant === 'outline') {
|
||||
if (!disabled) {
|
||||
baseStyles.push({
|
||||
color: light ? tokens.color.gray_600 : tokens.color.gray_300,
|
||||
color: t.palette.contrast_600,
|
||||
})
|
||||
} else {
|
||||
baseStyles.push({
|
||||
color: light ? tokens.color.gray_400 : tokens.color.gray_700,
|
||||
color: t.palette.contrast_300,
|
||||
})
|
||||
}
|
||||
} else if (variant === 'ghost') {
|
||||
if (!disabled) {
|
||||
baseStyles.push({
|
||||
color: light ? tokens.color.gray_600 : tokens.color.gray_300,
|
||||
color: t.palette.contrast_600,
|
||||
})
|
||||
} else {
|
||||
baseStyles.push({
|
||||
color: light ? tokens.color.gray_400 : tokens.color.gray_600,
|
||||
color: t.palette.contrast_300,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -533,7 +520,14 @@ export function ButtonText({children, style, ...rest}: ButtonTextProps) {
|
||||
const textStyles = useSharedButtonTextStyles()
|
||||
|
||||
return (
|
||||
<Text {...rest} style={[a.font_bold, a.text_center, textStyles, style]}>
|
||||
<Text
|
||||
{...rest}
|
||||
style={normalizeTextStyles([
|
||||
a.font_bold,
|
||||
a.text_center,
|
||||
textStyles,
|
||||
style,
|
||||
])}>
|
||||
{children}
|
||||
</Text>
|
||||
)
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import React from 'react'
|
||||
|
||||
import {useDialogStateContext} from '#/state/dialogs'
|
||||
import {DialogContextProps, DialogControlProps} from '#/components/Dialog/types'
|
||||
import {
|
||||
DialogContextProps,
|
||||
DialogControlRefProps,
|
||||
DialogOuterProps,
|
||||
} from '#/components/Dialog/types'
|
||||
|
||||
export const Context = React.createContext<DialogContextProps>({
|
||||
close: () => {},
|
||||
@@ -11,9 +15,9 @@ export function useDialogContext() {
|
||||
return React.useContext(Context)
|
||||
}
|
||||
|
||||
export function useDialogControl() {
|
||||
export function useDialogControl(): DialogOuterProps['control'] {
|
||||
const id = React.useId()
|
||||
const control = React.useRef<DialogControlProps>({
|
||||
const control = React.useRef<DialogControlRefProps>({
|
||||
open: () => {},
|
||||
close: () => {},
|
||||
})
|
||||
@@ -27,9 +31,17 @@ export function useDialogControl() {
|
||||
}
|
||||
}, [id, activeDialogs])
|
||||
|
||||
return {
|
||||
ref: control,
|
||||
open: () => control.current.open(),
|
||||
close: () => control.current.close(),
|
||||
}
|
||||
return React.useMemo<DialogOuterProps['control']>(
|
||||
() => ({
|
||||
id,
|
||||
ref: control,
|
||||
open: () => {
|
||||
control.current.open()
|
||||
},
|
||||
close: cb => {
|
||||
control.current.close(cb)
|
||||
},
|
||||
}),
|
||||
[id, control],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import React, {useImperativeHandle} from 'react'
|
||||
import {View, Dimensions} from 'react-native'
|
||||
import {View, Dimensions, Keyboard, Pressable} from 'react-native'
|
||||
import BottomSheet, {
|
||||
BottomSheetBackdrop,
|
||||
BottomSheetBackdropProps,
|
||||
BottomSheetScrollView,
|
||||
BottomSheetTextInput,
|
||||
BottomSheetView,
|
||||
useBottomSheet,
|
||||
WINDOW_HEIGHT,
|
||||
} from '@gorhom/bottom-sheet'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import Animated, {useAnimatedStyle} from 'react-native-reanimated'
|
||||
|
||||
import {useTheme, atoms as a} from '#/alf'
|
||||
import {useTheme, atoms as a, flatten} from '#/alf'
|
||||
import {Portal} from '#/components/Portal'
|
||||
import {createInput} from '#/components/forms/TextField'
|
||||
import {logger} from '#/logger'
|
||||
import {useDialogStateControlContext} from '#/state/dialogs'
|
||||
|
||||
import {
|
||||
DialogOuterProps,
|
||||
@@ -24,23 +29,85 @@ export * from '#/components/Dialog/types'
|
||||
// @ts-ignore
|
||||
export const Input = createInput(BottomSheetTextInput)
|
||||
|
||||
function Backdrop(props: BottomSheetBackdropProps) {
|
||||
const t = useTheme()
|
||||
const bottomSheet = useBottomSheet()
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => {
|
||||
const opacity =
|
||||
(Math.abs(WINDOW_HEIGHT - props.animatedPosition.value) - 50) / 1000
|
||||
|
||||
return {
|
||||
opacity: Math.min(Math.max(opacity, 0), 0.55),
|
||||
}
|
||||
})
|
||||
|
||||
const onPress = React.useCallback(() => {
|
||||
bottomSheet.close()
|
||||
}, [bottomSheet])
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
style={[
|
||||
t.atoms.bg_contrast_300,
|
||||
{
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
position: 'absolute',
|
||||
},
|
||||
animatedStyle,
|
||||
]}>
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Dialog backdrop"
|
||||
accessibilityHint="Press the backdrop to close the dialog"
|
||||
style={{flex: 1}}
|
||||
onPress={onPress}
|
||||
/>
|
||||
</Animated.View>
|
||||
)
|
||||
}
|
||||
|
||||
export function Outer({
|
||||
children,
|
||||
control,
|
||||
onClose,
|
||||
nativeOptions,
|
||||
testID,
|
||||
}: React.PropsWithChildren<DialogOuterProps>) {
|
||||
const t = useTheme()
|
||||
const sheet = React.useRef<BottomSheet>(null)
|
||||
const sheetOptions = nativeOptions?.sheet || {}
|
||||
const hasSnapPoints = !!sheetOptions.snapPoints
|
||||
const insets = useSafeAreaInsets()
|
||||
const closeCallback = React.useRef<() => void>()
|
||||
const {setDialogIsOpen} = useDialogStateControlContext()
|
||||
|
||||
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} = {}) => {
|
||||
setDialogIsOpen(control.id, true)
|
||||
// can be set to any index of `snapPoints`, but `0` is the first i.e. "open"
|
||||
setOpenIndex(index || 0)
|
||||
},
|
||||
[setOpenIndex, setDialogIsOpen, control.id],
|
||||
)
|
||||
|
||||
const close = React.useCallback<DialogControlProps['close']>(cb => {
|
||||
if (cb && typeof cb === 'function') {
|
||||
closeCallback.current = cb
|
||||
}
|
||||
sheet.current?.close()
|
||||
}, [])
|
||||
|
||||
@@ -53,81 +120,92 @@ export function Outer({
|
||||
[open, close],
|
||||
)
|
||||
|
||||
const onChange = React.useCallback(
|
||||
(index: number) => {
|
||||
if (index === -1) {
|
||||
onClose?.()
|
||||
}
|
||||
},
|
||||
[onClose],
|
||||
)
|
||||
const onCloseInner = React.useCallback(() => {
|
||||
Keyboard.dismiss()
|
||||
try {
|
||||
closeCallback.current?.()
|
||||
} catch (e: any) {
|
||||
logger.error(`Dialog closeCallback failed`, {
|
||||
message: e.message,
|
||||
})
|
||||
} finally {
|
||||
closeCallback.current = undefined
|
||||
}
|
||||
setDialogIsOpen(control.id, false)
|
||||
onClose?.()
|
||||
setOpenIndex(-1)
|
||||
}, [control.id, onClose, setDialogIsOpen])
|
||||
|
||||
const context = React.useMemo(() => ({close}), [close])
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<BottomSheet
|
||||
enableDynamicSizing={!hasSnapPoints}
|
||||
enablePanDownToClose
|
||||
keyboardBehavior="interactive"
|
||||
android_keyboardInputMode="adjustResize"
|
||||
keyboardBlurBehavior="restore"
|
||||
topInset={insets.top}
|
||||
{...sheetOptions}
|
||||
ref={sheet}
|
||||
index={-1}
|
||||
backgroundStyle={{backgroundColor: 'transparent'}}
|
||||
backdropComponent={props => (
|
||||
<BottomSheetBackdrop
|
||||
opacity={0.4}
|
||||
appearsOnIndex={0}
|
||||
disappearsOnIndex={-1}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
handleIndicatorStyle={{backgroundColor: t.palette.primary_500}}
|
||||
handleStyle={{display: 'none'}}
|
||||
onChange={onChange}>
|
||||
<Context.Provider value={context}>
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.inset_0,
|
||||
t.atoms.bg,
|
||||
{
|
||||
borderTopLeftRadius: 40,
|
||||
borderTopRightRadius: 40,
|
||||
height: Dimensions.get('window').height * 2,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{children}
|
||||
</Context.Provider>
|
||||
</BottomSheet>
|
||||
</Portal>
|
||||
isOpen && (
|
||||
<Portal>
|
||||
<View
|
||||
// iOS
|
||||
accessibilityViewIsModal
|
||||
// Android
|
||||
importantForAccessibility="yes"
|
||||
style={[a.absolute, a.inset_0]}
|
||||
testID={testID}>
|
||||
<BottomSheet
|
||||
enableDynamicSizing={!hasSnapPoints}
|
||||
enablePanDownToClose
|
||||
keyboardBehavior="interactive"
|
||||
android_keyboardInputMode="adjustResize"
|
||||
keyboardBlurBehavior="restore"
|
||||
topInset={insets.top}
|
||||
{...sheetOptions}
|
||||
snapPoints={sheetOptions.snapPoints || ['100%']}
|
||||
ref={sheet}
|
||||
index={openIndex}
|
||||
backgroundStyle={{backgroundColor: 'transparent'}}
|
||||
backdropComponent={Backdrop}
|
||||
handleIndicatorStyle={{backgroundColor: t.palette.primary_500}}
|
||||
handleStyle={{display: 'none'}}
|
||||
onClose={onCloseInner}>
|
||||
<Context.Provider value={context}>
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.inset_0,
|
||||
t.atoms.bg,
|
||||
{
|
||||
borderTopLeftRadius: 40,
|
||||
borderTopRightRadius: 40,
|
||||
height: Dimensions.get('window').height * 2,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{children}
|
||||
</Context.Provider>
|
||||
</BottomSheet>
|
||||
</View>
|
||||
</Portal>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// TODO a11y props here, or is that handled by the sheet?
|
||||
export function Inner(props: DialogInnerProps) {
|
||||
export function Inner({children, style}: DialogInnerProps) {
|
||||
const insets = useSafeAreaInsets()
|
||||
return (
|
||||
<BottomSheetView
|
||||
style={[
|
||||
a.p_lg,
|
||||
a.p_xl,
|
||||
{
|
||||
paddingTop: 40,
|
||||
borderTopLeftRadius: 40,
|
||||
borderTopRightRadius: 40,
|
||||
paddingBottom: insets.bottom + a.pb_5xl.paddingBottom,
|
||||
},
|
||||
flatten(style),
|
||||
]}>
|
||||
{props.children}
|
||||
{children}
|
||||
</BottomSheetView>
|
||||
)
|
||||
}
|
||||
|
||||
export function ScrollableInner(props: DialogInnerProps) {
|
||||
export function ScrollableInner({children, style}: DialogInnerProps) {
|
||||
const insets = useSafeAreaInsets()
|
||||
return (
|
||||
<BottomSheetScrollView
|
||||
@@ -136,13 +214,15 @@ export function ScrollableInner(props: DialogInnerProps) {
|
||||
style={[
|
||||
a.flex_1, // main diff is this
|
||||
a.p_xl,
|
||||
a.h_full,
|
||||
{
|
||||
paddingTop: 40,
|
||||
borderTopLeftRadius: 40,
|
||||
borderTopRightRadius: 40,
|
||||
},
|
||||
flatten(style),
|
||||
]}>
|
||||
{props.children}
|
||||
{children}
|
||||
<View style={{height: insets.bottom + a.pt_5xl.paddingTop}} />
|
||||
</BottomSheetScrollView>
|
||||
)
|
||||
@@ -150,8 +230,15 @@ export function ScrollableInner(props: DialogInnerProps) {
|
||||
|
||||
export function Handle() {
|
||||
const t = useTheme()
|
||||
|
||||
const onTouchStart = React.useCallback(() => {
|
||||
Keyboard.dismiss()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<View style={[a.absolute, a.w_full, a.align_center, a.z_10, {height: 40}]}>
|
||||
<View
|
||||
style={[a.absolute, a.w_full, a.align_center, a.z_10, {height: 40}]}
|
||||
onTouchStart={onTouchStart}>
|
||||
<View
|
||||
style={[
|
||||
a.rounded_sm,
|
||||
|
||||
@@ -5,11 +5,14 @@ import Animated, {FadeInDown, FadeIn} from 'react-native-reanimated'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useTheme, atoms as a, useBreakpoints, web} from '#/alf'
|
||||
import {useTheme, atoms as a, useBreakpoints, web, flatten} from '#/alf'
|
||||
import {Portal} from '#/components/Portal'
|
||||
|
||||
import {DialogOuterProps, DialogInnerProps} from '#/components/Dialog/types'
|
||||
import {Context} from '#/components/Dialog/context'
|
||||
import {Button, ButtonIcon} from '#/components/Button'
|
||||
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||
import {useDialogStateControlContext} from '#/state/dialogs'
|
||||
|
||||
export {useDialogControl, useDialogContext} from '#/components/Dialog/context'
|
||||
export * from '#/components/Dialog/types'
|
||||
@@ -18,27 +21,30 @@ export {Input} from '#/components/forms/TextField'
|
||||
const stopPropagation = (e: any) => e.stopPropagation()
|
||||
|
||||
export function Outer({
|
||||
children,
|
||||
control,
|
||||
onClose,
|
||||
children,
|
||||
}: React.PropsWithChildren<DialogOuterProps>) {
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const [isOpen, setIsOpen] = React.useState(false)
|
||||
const [isVisible, setIsVisible] = React.useState(true)
|
||||
const {setDialogIsOpen} = useDialogStateControlContext()
|
||||
|
||||
const open = React.useCallback(() => {
|
||||
setIsOpen(true)
|
||||
}, [setIsOpen])
|
||||
setDialogIsOpen(control.id, true)
|
||||
}, [setIsOpen, setDialogIsOpen, control.id])
|
||||
|
||||
const close = React.useCallback(async () => {
|
||||
setIsVisible(false)
|
||||
await new Promise(resolve => setTimeout(resolve, 150))
|
||||
setIsOpen(false)
|
||||
setIsVisible(true)
|
||||
setDialogIsOpen(control.id, false)
|
||||
onClose?.()
|
||||
}, [onClose, setIsOpen])
|
||||
}, [onClose, setIsOpen, setDialogIsOpen, control.id])
|
||||
|
||||
useImperativeHandle(
|
||||
control.ref,
|
||||
@@ -93,7 +99,7 @@ export function Outer({
|
||||
style={[
|
||||
web(a.fixed),
|
||||
a.inset_0,
|
||||
{opacity: 0.5, backgroundColor: t.palette.black},
|
||||
{opacity: 0.8, backgroundColor: t.palette.black},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
@@ -147,7 +153,7 @@ export function Inner({
|
||||
a.rounded_md,
|
||||
a.w_full,
|
||||
a.border,
|
||||
gtMobile ? a.p_xl : a.p_lg,
|
||||
gtMobile ? a.p_2xl : a.p_xl,
|
||||
t.atoms.bg,
|
||||
{
|
||||
maxWidth: 600,
|
||||
@@ -156,7 +162,7 @@ export function Inner({
|
||||
shadowOpacity: t.name === 'light' ? 0.1 : 0.4,
|
||||
shadowRadius: 30,
|
||||
},
|
||||
...(Array.isArray(style) ? style : [style || {}]),
|
||||
flatten(style),
|
||||
]}>
|
||||
{children}
|
||||
</Animated.View>
|
||||
@@ -170,25 +176,28 @@ export function Handle() {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO(eric) unused rn
|
||||
*/
|
||||
// export function Close() {
|
||||
// const {_} = useLingui()
|
||||
// const t = useTheme()
|
||||
// const {close} = useDialogContext()
|
||||
// return (
|
||||
// <View
|
||||
// style={[
|
||||
// a.absolute,
|
||||
// a.z_10,
|
||||
// {
|
||||
// top: a.pt_lg.paddingTop,
|
||||
// right: a.pr_lg.paddingRight,
|
||||
// },
|
||||
// ]}>
|
||||
// <Button onPress={close} label={_(msg`Close active dialog`)}>
|
||||
// </Button>
|
||||
// </View>
|
||||
// )
|
||||
// }
|
||||
export function Close() {
|
||||
const {_} = useLingui()
|
||||
const {close} = React.useContext(Context)
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.z_10,
|
||||
{
|
||||
top: a.pt_md.paddingTop,
|
||||
right: a.pr_md.paddingRight,
|
||||
},
|
||||
]}>
|
||||
<Button
|
||||
size="small"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
shape="round"
|
||||
onPress={() => close()}
|
||||
label={_(msg`Close active dialog`)}>
|
||||
<ButtonIcon icon={X} size="md" />
|
||||
</Button>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,35 +1,55 @@
|
||||
import React from 'react'
|
||||
import type {ViewStyle, AccessibilityProps} from 'react-native'
|
||||
import type {AccessibilityProps} from 'react-native'
|
||||
import {BottomSheetProps} from '@gorhom/bottom-sheet'
|
||||
|
||||
import {ViewStyleProp} from '#/alf'
|
||||
|
||||
type A11yProps = Required<AccessibilityProps>
|
||||
|
||||
export type DialogContextProps = {
|
||||
close: () => void
|
||||
/**
|
||||
* Mutated by useImperativeHandle to provide a public API for controlling the
|
||||
* dialog. The methods here will actually become the handlers defined within
|
||||
* the `Dialog.Outer` component.
|
||||
*/
|
||||
export type DialogControlRefProps = {
|
||||
open: (options?: DialogControlOpenOptions) => void
|
||||
close: (callback?: () => void) => void
|
||||
}
|
||||
|
||||
export type DialogControlProps = {
|
||||
open: (index?: number) => void
|
||||
close: () => void
|
||||
/**
|
||||
* The return type of the useDialogControl hook.
|
||||
*/
|
||||
export type DialogControlProps = DialogControlRefProps & {
|
||||
id: string
|
||||
ref: React.RefObject<DialogControlRefProps>
|
||||
isOpen?: boolean
|
||||
}
|
||||
|
||||
export type DialogContextProps = {
|
||||
close: DialogControlProps['close']
|
||||
}
|
||||
|
||||
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 DialogOuterProps = {
|
||||
control: {
|
||||
ref: React.RefObject<DialogControlProps>
|
||||
open: (index?: number) => void
|
||||
close: () => void
|
||||
}
|
||||
control: DialogControlProps
|
||||
onClose?: () => void
|
||||
nativeOptions?: {
|
||||
sheet?: Omit<BottomSheetProps, 'children'>
|
||||
}
|
||||
webOptions?: {}
|
||||
testID?: string
|
||||
}
|
||||
|
||||
type DialogInnerPropsBase<T> = React.PropsWithChildren<{
|
||||
style?: ViewStyle
|
||||
}> &
|
||||
T
|
||||
type DialogInnerPropsBase<T> = React.PropsWithChildren<ViewStyleProp> & T
|
||||
export type DialogInnerProps =
|
||||
| DialogInnerPropsBase<{
|
||||
label?: undefined
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from 'react'
|
||||
import LinearGradient from 'react-native-linear-gradient'
|
||||
|
||||
import {atoms as a, tokens} from '#/alf'
|
||||
|
||||
export function GradientFill({
|
||||
gradient,
|
||||
}: {
|
||||
gradient:
|
||||
| typeof tokens.gradients.sky
|
||||
| typeof tokens.gradients.midnight
|
||||
| typeof tokens.gradients.sunrise
|
||||
| typeof tokens.gradients.sunset
|
||||
| typeof tokens.gradients.bonfire
|
||||
| typeof tokens.gradients.summer
|
||||
| typeof tokens.gradients.nordic
|
||||
}) {
|
||||
return (
|
||||
<LinearGradient
|
||||
colors={gradient.values.map(c => c[1])}
|
||||
locations={gradient.values.map(c => c[0])}
|
||||
start={{x: 0, y: 0}}
|
||||
end={{x: 1, y: 1}}
|
||||
style={[a.absolute, a.inset_0]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {AppBskyLabelerDefs} from '@atproto/api'
|
||||
|
||||
import {getLabelingServiceTitle} from '#/lib/moderation'
|
||||
import {Link as InternalLink, LinkProps} from '#/components/Link'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {useLabelerInfoQuery} from '#/state/queries/labeler'
|
||||
import {atoms as a, useTheme, ViewStyleProp} from '#/alf'
|
||||
import {RichText} from '#/components/RichText'
|
||||
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '../icons/Chevron'
|
||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {pluralize} from '#/lib/strings/helpers'
|
||||
|
||||
type LabelingServiceProps = {
|
||||
labeler: AppBskyLabelerDefs.LabelerViewDetailed
|
||||
}
|
||||
|
||||
export function Outer({
|
||||
children,
|
||||
style,
|
||||
}: React.PropsWithChildren<ViewStyleProp>) {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.flex_row,
|
||||
a.gap_md,
|
||||
a.w_full,
|
||||
a.p_lg,
|
||||
a.pr_md,
|
||||
a.overflow_hidden,
|
||||
style,
|
||||
]}>
|
||||
{children}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export function Avatar({avatar}: {avatar?: string}) {
|
||||
return <UserAvatar type="labeler" size={40} avatar={avatar} />
|
||||
}
|
||||
|
||||
export function Title({value}: {value: string}) {
|
||||
return <Text style={[a.text_md, a.font_bold]}>{value}</Text>
|
||||
}
|
||||
|
||||
export function Description({value, handle}: {value?: string; handle: string}) {
|
||||
return value ? (
|
||||
<Text numberOfLines={2}>
|
||||
<RichText value={value} style={[]} />
|
||||
</Text>
|
||||
) : (
|
||||
<Text>
|
||||
<Trans>By {sanitizeHandle(handle, '@')}</Trans>
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
export function LikeCount({count}: {count: number}) {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<Text
|
||||
style={[
|
||||
a.mt_sm,
|
||||
a.text_sm,
|
||||
t.atoms.text_contrast_medium,
|
||||
{fontWeight: '500'},
|
||||
]}>
|
||||
<Trans>
|
||||
Liked by {count} {pluralize(count, 'user')}
|
||||
</Trans>
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
export function Content({children}: React.PropsWithChildren<{}>) {
|
||||
const t = useTheme()
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.flex_row,
|
||||
a.gap_md,
|
||||
a.align_center,
|
||||
a.justify_between,
|
||||
]}>
|
||||
<View style={[a.gap_xs, a.flex_1]}>{children}</View>
|
||||
|
||||
<ChevronRight size="md" style={[a.z_10, t.atoms.text_contrast_low]} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The canonical view for a labeling service. Use this or compose your own.
|
||||
*/
|
||||
export function Default({
|
||||
labeler,
|
||||
style,
|
||||
}: LabelingServiceProps & ViewStyleProp) {
|
||||
return (
|
||||
<Outer style={style}>
|
||||
<Avatar />
|
||||
<Content>
|
||||
<Title
|
||||
value={getLabelingServiceTitle({
|
||||
displayName: labeler.creator.displayName,
|
||||
handle: labeler.creator.handle,
|
||||
})}
|
||||
/>
|
||||
<Description
|
||||
value={labeler.creator.description}
|
||||
handle={labeler.creator.handle}
|
||||
/>
|
||||
{labeler.likeCount ? <LikeCount count={labeler.likeCount} /> : null}
|
||||
</Content>
|
||||
</Outer>
|
||||
)
|
||||
}
|
||||
|
||||
export function Link({
|
||||
children,
|
||||
labeler,
|
||||
}: LabelingServiceProps & Pick<LinkProps, 'children'>) {
|
||||
const {_} = useLingui()
|
||||
|
||||
return (
|
||||
<InternalLink
|
||||
to={{
|
||||
screen: 'Profile',
|
||||
params: {
|
||||
name: labeler.creator.handle,
|
||||
},
|
||||
}}
|
||||
label={_(
|
||||
msg`View the labeling service provided by @${labeler.creator.handle}`,
|
||||
)}>
|
||||
{children}
|
||||
</InternalLink>
|
||||
)
|
||||
}
|
||||
|
||||
// TODO not finished yet
|
||||
export function DefaultSkeleton() {
|
||||
return (
|
||||
<View>
|
||||
<Text>Loading</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export function Loader({
|
||||
did,
|
||||
loading: LoadingComponent = DefaultSkeleton,
|
||||
error: ErrorComponent,
|
||||
component: Component,
|
||||
}: {
|
||||
did: string
|
||||
loading?: React.ComponentType<{}>
|
||||
error?: React.ComponentType<{error: string}>
|
||||
component: React.ComponentType<{
|
||||
labeler: AppBskyLabelerDefs.LabelerViewDetailed
|
||||
}>
|
||||
}) {
|
||||
const {isLoading, data, error} = useLabelerInfoQuery({did})
|
||||
|
||||
return isLoading ? (
|
||||
LoadingComponent ? (
|
||||
<LoadingComponent />
|
||||
) : null
|
||||
) : error || !data ? (
|
||||
ErrorComponent ? (
|
||||
<ErrorComponent error={error?.message || 'Unknown error'} />
|
||||
) : null
|
||||
) : (
|
||||
<Component labeler={data} />
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {logger} from '#/logger'
|
||||
import {List} from '#/view/com/util/List'
|
||||
import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
|
||||
import {useResolveUriQuery} from '#/state/queries/resolve-uri'
|
||||
import {useLikedByQuery} from '#/state/queries/post-liked-by'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as Refresh} from './icons/ArrowRotateCounterClockwise'
|
||||
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
|
||||
export function LikedByList({uri}: {uri: string}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const [isPTRing, setIsPTRing] = React.useState(false)
|
||||
const {
|
||||
data: resolvedUri,
|
||||
error: resolveError,
|
||||
isFetching: isFetchingResolvedUri,
|
||||
} = useResolveUriQuery(uri)
|
||||
const {
|
||||
data,
|
||||
isFetching,
|
||||
isFetched,
|
||||
isFetchingNextPage,
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
isError,
|
||||
error,
|
||||
refetch,
|
||||
} = useLikedByQuery(resolvedUri?.uri)
|
||||
const likes = React.useMemo(() => {
|
||||
if (data?.pages) {
|
||||
return data.pages.flatMap(page => page.likes)
|
||||
}
|
||||
return []
|
||||
}, [data])
|
||||
|
||||
const onRefresh = React.useCallback(async () => {
|
||||
setIsPTRing(true)
|
||||
try {
|
||||
await refetch()
|
||||
} catch (err) {
|
||||
logger.error('Failed to refresh likes', {message: err})
|
||||
}
|
||||
setIsPTRing(false)
|
||||
}, [refetch, setIsPTRing])
|
||||
|
||||
const onEndReached = React.useCallback(async () => {
|
||||
if (isFetching || !hasNextPage || isError) return
|
||||
try {
|
||||
await fetchNextPage()
|
||||
} catch (err) {
|
||||
logger.error('Failed to load more likes', {message: err})
|
||||
}
|
||||
}, [isFetching, hasNextPage, isError, fetchNextPage])
|
||||
|
||||
const renderItem = React.useCallback(({item}: {item: GetLikes.Like}) => {
|
||||
return (
|
||||
<ProfileCardWithFollowBtn key={item.actor.did} profile={item.actor} />
|
||||
)
|
||||
}, [])
|
||||
|
||||
if (isFetchingResolvedUri || !isFetched) {
|
||||
return (
|
||||
<View style={[a.w_full, a.align_center, a.p_lg]}>
|
||||
<Loader size="xl" />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
if (resolveError || isError) {
|
||||
return (
|
||||
<View style={[a.p_lg]}>
|
||||
<View style={[a.p_lg, a.rounded_sm, t.atoms.bg_contrast_25]}>
|
||||
<Text style={[a.text_md, a.pb_lg]}>
|
||||
{cleanError(resolveError || error)}
|
||||
</Text>
|
||||
|
||||
<View style={[a.flex_row, a.justify_end]}>
|
||||
<Button
|
||||
label={_(msg``)}
|
||||
onPress={onRefresh}
|
||||
size="small"
|
||||
variant="solid"
|
||||
color="primary">
|
||||
<ButtonText>
|
||||
<Trans>Try again</Trans>
|
||||
</ButtonText>
|
||||
<ButtonIcon icon={Refresh} position="right" />
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return likes.length ? (
|
||||
<List
|
||||
data={likes}
|
||||
keyExtractor={item => item.actor.did}
|
||||
refreshing={isPTRing}
|
||||
onRefresh={onRefresh}
|
||||
onEndReached={onEndReached}
|
||||
renderItem={renderItem}
|
||||
initialNumToRender={15}
|
||||
contentContainerStyle={{borderWidth: 0}}
|
||||
// FIXME(dan)
|
||||
// eslint-disable-next-line react/no-unstable-nested-components
|
||||
ListFooterComponent={() => (
|
||||
<View style={[a.w_full, a.align_center, a.p_lg]}>
|
||||
{(isFetching || isFetchingNextPage) && <Loader size="xl" />}
|
||||
</View>
|
||||
)}
|
||||
// @ts-ignore our .web version only -prf
|
||||
desktopFixedHeight
|
||||
/>
|
||||
) : (
|
||||
<View style={[a.p_lg]}>
|
||||
<View style={[a.p_lg, a.rounded_sm, t.atoms.bg_contrast_25]}>
|
||||
<Text style={[a.text_md, a.leading_snug]}>
|
||||
<Trans>
|
||||
Nobody has liked this yet. Maybe you should be the first!
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import React, {useMemo, useCallback} from 'react'
|
||||
import {ActivityIndicator, FlatList, View} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api'
|
||||
|
||||
import {useResolveUriQuery} from '#/state/queries/resolve-uri'
|
||||
import {useLikedByQuery} from '#/state/queries/post-liked-by'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {logger} from '#/logger'
|
||||
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Text} from '#/components/Typography'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
|
||||
import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
|
||||
import {Loader} from '#/components/Loader'
|
||||
|
||||
interface LikesDialogProps {
|
||||
control: Dialog.DialogOuterProps['control']
|
||||
uri: string
|
||||
}
|
||||
|
||||
export function LikesDialog(props: LikesDialogProps) {
|
||||
return (
|
||||
<Dialog.Outer control={props.control}>
|
||||
<Dialog.Handle />
|
||||
|
||||
<LikesDialogInner {...props} />
|
||||
</Dialog.Outer>
|
||||
)
|
||||
}
|
||||
|
||||
export function LikesDialogInner({control, uri}: LikesDialogProps) {
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
|
||||
const {
|
||||
data: resolvedUri,
|
||||
error: resolveError,
|
||||
isFetched: hasFetchedResolvedUri,
|
||||
} = useResolveUriQuery(uri)
|
||||
const {
|
||||
data,
|
||||
isFetching: isFetchingLikedBy,
|
||||
isFetched: hasFetchedLikedBy,
|
||||
isFetchingNextPage,
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
isError,
|
||||
error: likedByError,
|
||||
} = useLikedByQuery(resolvedUri?.uri)
|
||||
|
||||
const isLoading = !hasFetchedResolvedUri || !hasFetchedLikedBy
|
||||
const likes = useMemo(() => {
|
||||
if (data?.pages) {
|
||||
return data.pages.flatMap(page => page.likes)
|
||||
}
|
||||
return []
|
||||
}, [data])
|
||||
|
||||
const onEndReached = useCallback(async () => {
|
||||
if (isFetchingLikedBy || !hasNextPage || isError) return
|
||||
try {
|
||||
await fetchNextPage()
|
||||
} catch (err) {
|
||||
logger.error('Failed to load more likes', {message: err})
|
||||
}
|
||||
}, [isFetchingLikedBy, hasNextPage, isError, fetchNextPage])
|
||||
|
||||
const renderItem = useCallback(
|
||||
({item}: {item: GetLikes.Like}) => {
|
||||
return (
|
||||
<ProfileCardWithFollowBtn
|
||||
key={item.actor.did}
|
||||
profile={item.actor}
|
||||
onPress={() => control.close()}
|
||||
/>
|
||||
)
|
||||
},
|
||||
[control],
|
||||
)
|
||||
|
||||
return (
|
||||
<Dialog.Inner label={_(msg`Users that have liked this content or profile`)}>
|
||||
<Text style={[a.text_2xl, a.font_bold, a.leading_tight, a.pb_lg]}>
|
||||
<Trans>Liked by</Trans>
|
||||
</Text>
|
||||
|
||||
{isLoading ? (
|
||||
<View style={{minHeight: 300}}>
|
||||
<Loader size="xl" />
|
||||
</View>
|
||||
) : resolveError || likedByError || !data ? (
|
||||
<ErrorMessage message={cleanError(resolveError || likedByError)} />
|
||||
) : likes.length === 0 ? (
|
||||
<View style={[t.atoms.bg_contrast_50, a.px_md, a.py_xl, a.rounded_md]}>
|
||||
<Text style={[a.text_center]}>
|
||||
<Trans>
|
||||
Nobody has liked this yet. Maybe you should be the first!
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<FlatList
|
||||
data={likes}
|
||||
keyExtractor={item => item.actor.did}
|
||||
onEndReached={onEndReached}
|
||||
renderItem={renderItem}
|
||||
initialNumToRender={15}
|
||||
ListFooterComponent={
|
||||
<ListFooterComponent isFetching={isFetchingNextPage} />
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Dialog.Close />
|
||||
</Dialog.Inner>
|
||||
)
|
||||
}
|
||||
|
||||
function ListFooterComponent({isFetching}: {isFetching: boolean}) {
|
||||
if (isFetching) {
|
||||
return (
|
||||
<View style={a.pt_lg}>
|
||||
<ActivityIndicator />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -1,17 +1,13 @@
|
||||
import React from 'react'
|
||||
import {GestureResponderEvent, Linking} from 'react-native'
|
||||
import {
|
||||
useLinkProps,
|
||||
useNavigation,
|
||||
StackActions,
|
||||
} from '@react-navigation/native'
|
||||
import {GestureResponderEvent} from 'react-native'
|
||||
import {useLinkProps, StackActions} from '@react-navigation/native'
|
||||
import {sanitizeUrl} from '@braintree/sanitize-url'
|
||||
|
||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useTheme, web, flatten, TextStyleProp, atoms as a} from '#/alf'
|
||||
import {Button, ButtonProps} from '#/components/Button'
|
||||
import {AllNavigatorParams, NavigationProp} from '#/lib/routes/types'
|
||||
import {AllNavigatorParams} from '#/lib/routes/types'
|
||||
import {
|
||||
convertBskyAppUrlIfNeeded,
|
||||
isExternalUrl,
|
||||
@@ -20,6 +16,8 @@ import {
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {router} from '#/routes'
|
||||
import {Text, TextProps} from '#/components/Typography'
|
||||
import {useOpenLink} from 'state/preferences/in-app-browser'
|
||||
import {useNavigationDeduped} from 'lib/hooks/useNavigationDeduped'
|
||||
|
||||
/**
|
||||
* Only available within a `Link`, since that inherits from `Button`.
|
||||
@@ -48,14 +46,15 @@ type BaseLinkProps = Pick<
|
||||
*
|
||||
* Note: atm this only works for `InlineLink`s with a string child.
|
||||
*/
|
||||
warnOnMismatchingTextChild?: boolean
|
||||
disableMismatchWarning?: boolean
|
||||
|
||||
/**
|
||||
* Callback for when the link is pressed.
|
||||
* Callback for when the link is pressed. Prevent default and return `false`
|
||||
* to exit early and prevent navigation.
|
||||
*
|
||||
* DO NOT use this for navigation, that's what the `to` prop is for.
|
||||
*/
|
||||
onPress?: (e: GestureResponderEvent) => void
|
||||
onPress?: (e: GestureResponderEvent) => void | false
|
||||
|
||||
/**
|
||||
* Web-only attribute. Sets `download` attr on web.
|
||||
@@ -67,25 +66,28 @@ export function useLink({
|
||||
to,
|
||||
displayText,
|
||||
action = 'push',
|
||||
warnOnMismatchingTextChild,
|
||||
disableMismatchWarning,
|
||||
onPress: outerOnPress,
|
||||
}: BaseLinkProps & {
|
||||
displayText: string
|
||||
}) {
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const navigation = useNavigationDeduped()
|
||||
const {href} = useLinkProps<AllNavigatorParams>({
|
||||
to:
|
||||
typeof to === 'string' ? convertBskyAppUrlIfNeeded(sanitizeUrl(to)) : to,
|
||||
})
|
||||
const isExternal = isExternalUrl(href)
|
||||
const {openModal, closeModal} = useModalControls()
|
||||
const openLink = useOpenLink()
|
||||
|
||||
const onPress = React.useCallback(
|
||||
(e: GestureResponderEvent) => {
|
||||
outerOnPress?.(e)
|
||||
const exitEarlyIfFalse = outerOnPress?.(e)
|
||||
|
||||
if (exitEarlyIfFalse === false) return
|
||||
|
||||
const requiresWarning = Boolean(
|
||||
warnOnMismatchingTextChild &&
|
||||
!disableMismatchWarning &&
|
||||
displayText &&
|
||||
isExternal &&
|
||||
linkRequiresWarning(href, displayText),
|
||||
@@ -103,7 +105,7 @@ export function useLink({
|
||||
e.preventDefault()
|
||||
|
||||
if (isExternal) {
|
||||
Linking.openURL(href)
|
||||
openLink(href)
|
||||
} else {
|
||||
/**
|
||||
* A `GestureResponderEvent`, but cast to `any` to avoid using a bunch
|
||||
@@ -121,7 +123,7 @@ export function useLink({
|
||||
href.startsWith('http') ||
|
||||
href.startsWith('mailto')
|
||||
) {
|
||||
Linking.openURL(href)
|
||||
openLink(href)
|
||||
} else {
|
||||
closeModal() // close any active modals
|
||||
|
||||
@@ -142,15 +144,16 @@ export function useLink({
|
||||
}
|
||||
},
|
||||
[
|
||||
href,
|
||||
isExternal,
|
||||
warnOnMismatchingTextChild,
|
||||
navigation,
|
||||
action,
|
||||
displayText,
|
||||
closeModal,
|
||||
openModal,
|
||||
outerOnPress,
|
||||
disableMismatchWarning,
|
||||
displayText,
|
||||
isExternal,
|
||||
href,
|
||||
openModal,
|
||||
openLink,
|
||||
closeModal,
|
||||
action,
|
||||
navigation,
|
||||
],
|
||||
)
|
||||
|
||||
@@ -161,7 +164,7 @@ export function useLink({
|
||||
}
|
||||
}
|
||||
|
||||
export type LinkProps = Omit<BaseLinkProps, 'warnOnMismatchingTextChild'> &
|
||||
export type LinkProps = Omit<BaseLinkProps, 'disableMismatchWarning'> &
|
||||
Omit<ButtonProps, 'onPress' | 'disabled' | 'label'>
|
||||
|
||||
/**
|
||||
@@ -220,11 +223,12 @@ export function InlineLink({
|
||||
children,
|
||||
to,
|
||||
action = 'push',
|
||||
warnOnMismatchingTextChild,
|
||||
disableMismatchWarning,
|
||||
style,
|
||||
onPress: outerOnPress,
|
||||
download,
|
||||
selectable,
|
||||
label,
|
||||
...rest
|
||||
}: InlineLinkProps) {
|
||||
const t = useTheme()
|
||||
@@ -233,7 +237,7 @@ export function InlineLink({
|
||||
to,
|
||||
displayText: stringChildren ? children : '',
|
||||
action,
|
||||
warnOnMismatchingTextChild,
|
||||
disableMismatchWarning,
|
||||
onPress: outerOnPress,
|
||||
})
|
||||
const {
|
||||
@@ -247,17 +251,18 @@ export function InlineLink({
|
||||
onIn: onPressIn,
|
||||
onOut: onPressOut,
|
||||
} = useInteractionState()
|
||||
const flattenedStyle = flatten(style)
|
||||
const flattenedStyle = flatten(style) || {}
|
||||
|
||||
return (
|
||||
<Text
|
||||
selectable={selectable}
|
||||
label={href}
|
||||
accessibilityHint=""
|
||||
accessibilityLabel={label || href}
|
||||
{...rest}
|
||||
style={[
|
||||
{color: t.palette.primary_500},
|
||||
(hovered || focused || pressed) && {
|
||||
outline: 0,
|
||||
...web({outline: 0}),
|
||||
textDecorationLine: 'underline',
|
||||
textDecorationColor: flattenedStyle.color ?? t.palette.primary_500,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
import React from 'react'
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
import {View} from 'react-native'
|
||||
import {CenteredView} from 'view/com/util/Views'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Trans} from '@lingui/macro'
|
||||
import {cleanError} from 'lib/strings/errors'
|
||||
import {Button} from '#/components/Button'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {StackActions} from '@react-navigation/native'
|
||||
import {router} from '#/routes'
|
||||
import {useNavigationDeduped} from 'lib/hooks/useNavigationDeduped'
|
||||
|
||||
export function ListFooter({
|
||||
isFetching,
|
||||
isError,
|
||||
error,
|
||||
onRetry,
|
||||
}: {
|
||||
isFetching: boolean
|
||||
isError: boolean
|
||||
error?: string
|
||||
onRetry?: () => Promise<unknown>
|
||||
}) {
|
||||
const t = useTheme()
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
a.border_t,
|
||||
a.pb_lg,
|
||||
t.atoms.border_contrast_low,
|
||||
{height: 100},
|
||||
]}>
|
||||
{isFetching ? (
|
||||
<Loader size="xl" />
|
||||
) : (
|
||||
<ListFooterMaybeError
|
||||
isError={isError}
|
||||
error={error}
|
||||
onRetry={onRetry}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function ListFooterMaybeError({
|
||||
isError,
|
||||
error,
|
||||
onRetry,
|
||||
}: {
|
||||
isError: boolean
|
||||
error?: string
|
||||
onRetry?: () => Promise<unknown>
|
||||
}) {
|
||||
const t = useTheme()
|
||||
|
||||
if (!isError) return null
|
||||
|
||||
return (
|
||||
<View style={[a.w_full, a.px_lg]}>
|
||||
<View
|
||||
style={[
|
||||
a.flex_row,
|
||||
a.gap_md,
|
||||
a.p_md,
|
||||
a.rounded_sm,
|
||||
a.align_center,
|
||||
t.atoms.bg_contrast_25,
|
||||
]}>
|
||||
<Text
|
||||
style={[a.flex_1, a.text_sm, t.atoms.text_contrast_medium]}
|
||||
numberOfLines={2}>
|
||||
{error ? (
|
||||
cleanError(error)
|
||||
) : (
|
||||
<Trans>Oops, something went wrong!</Trans>
|
||||
)}
|
||||
</Text>
|
||||
<Button
|
||||
variant="gradient"
|
||||
label="Press to retry"
|
||||
style={[
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
a.rounded_sm,
|
||||
a.overflow_hidden,
|
||||
a.px_md,
|
||||
a.py_sm,
|
||||
]}
|
||||
onPress={onRetry}>
|
||||
Retry
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export function ListHeaderDesktop({
|
||||
title,
|
||||
subtitle,
|
||||
}: {
|
||||
title: string
|
||||
subtitle?: string
|
||||
}) {
|
||||
const {gtTablet} = useBreakpoints()
|
||||
const t = useTheme()
|
||||
|
||||
if (!gtTablet) return null
|
||||
|
||||
return (
|
||||
<View style={[a.w_full, a.py_lg, a.px_xl, a.gap_xs]}>
|
||||
<Text style={[a.text_3xl, a.font_bold]}>{title}</Text>
|
||||
{subtitle ? (
|
||||
<Text style={[a.text_md, t.atoms.text_contrast_medium]}>
|
||||
{subtitle}
|
||||
</Text>
|
||||
) : undefined}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export function ListMaybePlaceholder({
|
||||
isLoading,
|
||||
isEmpty,
|
||||
isError,
|
||||
empty,
|
||||
error,
|
||||
notFoundType = 'page',
|
||||
onRetry,
|
||||
}: {
|
||||
isLoading: boolean
|
||||
isEmpty: boolean
|
||||
isError: boolean
|
||||
empty?: string
|
||||
error?: string
|
||||
notFoundType?: 'page' | 'results'
|
||||
onRetry?: () => Promise<unknown>
|
||||
}) {
|
||||
const navigation = useNavigationDeduped()
|
||||
const t = useTheme()
|
||||
const {gtMobile, gtTablet} = useBreakpoints()
|
||||
|
||||
const canGoBack = navigation.canGoBack()
|
||||
const onGoBack = React.useCallback(() => {
|
||||
if (canGoBack) {
|
||||
navigation.goBack()
|
||||
} else {
|
||||
navigation.navigate('HomeTab')
|
||||
|
||||
// Checking the state for routes ensures that web doesn't encounter errors while going back
|
||||
if (navigation.getState()?.routes) {
|
||||
navigation.dispatch(StackActions.push(...router.matchPath('/')))
|
||||
} else {
|
||||
navigation.navigate('HomeTab')
|
||||
navigation.dispatch(StackActions.popToTop())
|
||||
}
|
||||
}
|
||||
}, [navigation, canGoBack])
|
||||
|
||||
if (!isEmpty) return null
|
||||
|
||||
return (
|
||||
<CenteredView
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.align_center,
|
||||
!gtMobile ? a.justify_between : a.gap_5xl,
|
||||
t.atoms.border_contrast_low,
|
||||
{paddingTop: 175, paddingBottom: 110},
|
||||
]}
|
||||
sideBorders={gtMobile}
|
||||
topBorder={!gtTablet}>
|
||||
{isLoading ? (
|
||||
<View style={[a.w_full, a.align_center, {top: 100}]}>
|
||||
<Loader size="xl" />
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
<View style={[a.w_full, a.align_center, a.gap_lg]}>
|
||||
<Text style={[a.font_bold, a.text_3xl]}>
|
||||
{isError ? (
|
||||
<Trans>Oops!</Trans>
|
||||
) : isEmpty ? (
|
||||
<>
|
||||
{notFoundType === 'results' ? (
|
||||
<Trans>No results found</Trans>
|
||||
) : (
|
||||
<Trans>Page not found</Trans>
|
||||
)}
|
||||
</>
|
||||
) : undefined}
|
||||
</Text>
|
||||
|
||||
{isError ? (
|
||||
<Text
|
||||
style={[a.text_md, a.text_center, t.atoms.text_contrast_high]}>
|
||||
{error ? error : <Trans>Something went wrong!</Trans>}
|
||||
</Text>
|
||||
) : isEmpty ? (
|
||||
<Text
|
||||
style={[a.text_md, a.text_center, t.atoms.text_contrast_high]}>
|
||||
{empty ? (
|
||||
empty
|
||||
) : (
|
||||
<Trans>
|
||||
We're sorry! We can't find the page you were looking for.
|
||||
</Trans>
|
||||
)}
|
||||
</Text>
|
||||
) : undefined}
|
||||
</View>
|
||||
<View
|
||||
style={[a.gap_md, !gtMobile ? [a.w_full, a.px_lg] : {width: 350}]}>
|
||||
{isError && onRetry && (
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
label="Click here"
|
||||
onPress={onRetry}
|
||||
size="large"
|
||||
style={[
|
||||
a.rounded_sm,
|
||||
a.overflow_hidden,
|
||||
{paddingVertical: 10},
|
||||
]}>
|
||||
Retry
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="solid"
|
||||
color={isError && onRetry ? 'secondary' : 'primary'}
|
||||
label="Click here"
|
||||
onPress={onGoBack}
|
||||
size="large"
|
||||
style={[a.rounded_sm, a.overflow_hidden, {paddingVertical: 10}]}>
|
||||
Go Back
|
||||
</Button>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</CenteredView>
|
||||
)
|
||||
}
|
||||