Compare commits
183 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1ae23cc8e5 | |||
| 1504ce28e2 | |||
| 6d0c859d8f | |||
| 41998c59b5 | |||
| 872fe324a0 | |||
| 810918b434 | |||
| 4d210e33cf | |||
| 15abdc67cf | |||
| c6b578fe91 | |||
| caeaff157c | |||
| 5c2867d580 | |||
| 7f3835792e | |||
| c37ab83823 | |||
| 26755445e3 | |||
| 101b3262b8 | |||
| 1ee91d2c90 | |||
| ead6862fc6 | |||
| 9e65f00c93 | |||
| c48f35d726 | |||
| d1dbc4f852 | |||
| fc2fae42bd | |||
| 742397bb1a | |||
| 269d70e66c | |||
| 0685de9b7f | |||
| c1d2aea885 | |||
| 229f275246 | |||
| 66950a08cb | |||
| b89645a172 | |||
| 12e67f53dd | |||
| ce02e4aa3d | |||
| 605c50d6c7 | |||
| dff0332dd0 | |||
| f0b869aeeb | |||
| 0610c822cf | |||
| a3b188baec | |||
| 3775aeb146 | |||
| 8f1cd86115 | |||
| 6a7b8a279b | |||
| 3ae3ed245b | |||
| 8fdcc3ee31 | |||
| b4938bc9df | |||
| cf8d8b3bb9 | |||
| bbc022c0a6 | |||
| bbc39806fe | |||
| 9e2b99e860 | |||
| fc4bf4e919 | |||
| 2a30a344b5 | |||
| 4a853794dc | |||
| 7635a49001 | |||
| 8ce9bdb27e | |||
| e1a0beff7d | |||
| e5d44923fb | |||
| 6d90d3c2fe | |||
| 9ebfbd0e40 | |||
| 74f7a44d71 | |||
| 760b184d28 | |||
| 36fba20a01 | |||
| ad16ed3096 | |||
| 0995e81b58 | |||
| 6a15679fb1 | |||
| 4dd9459cba | |||
| 95ab4fa0a1 | |||
| cfaf741647 | |||
| f4909e5e05 | |||
| 08a1a806ef | |||
| 964eed54ea | |||
| 00b017804b | |||
| d13a2e5f69 | |||
| 2c0c9bf803 | |||
| 2e16ef4384 | |||
| 42bfee5d9d | |||
| 94a3e5bbe1 | |||
| 1dbc331314 | |||
| 712c3ad421 | |||
| 60c0689236 | |||
| 4ccbae7c30 | |||
| 1f82b1d557 | |||
| c9c91fb113 | |||
| 6375c54aa4 | |||
| 1da123b12b | |||
| b76f95c9bd | |||
| 587c14bac8 | |||
| be75e00d21 | |||
| 27d6de0b18 | |||
| ff57e75b12 | |||
| 0e3124a97e | |||
| 8f5a8f37af | |||
| 3b329430ac | |||
| 377d80e21d | |||
| c2a5b3efa8 | |||
| 22bda086d0 | |||
| 07b028ee66 | |||
| 1fcd313424 | |||
| 9adcf5a12e | |||
| 34291131b4 | |||
| a9469cc111 | |||
| 1ed019bd72 | |||
| 42af417e8c | |||
| dbfb9dad2f | |||
| 9d44673acb | |||
| bc072570d2 | |||
| 8f9a8ddce0 | |||
| 9fdd98d8b4 | |||
| 5f291b5ca8 | |||
| 806d3792a9 | |||
| 5e20d5ffab | |||
| 130f795dfb | |||
| 7837c8f07d | |||
| 3e76a4725e | |||
| 9b306598cc | |||
| bde3fdaa42 | |||
| 494758da11 | |||
| ad54ec8ef9 | |||
| e5f9377a69 | |||
| 954e7d2a89 | |||
| 92ffe66ae1 | |||
| bb760400fe | |||
| ab4148dc06 | |||
| 929393a77a | |||
| 4c1515169a | |||
| cd820709b6 | |||
| c894dda0bf | |||
| c634cd9071 | |||
| 3c92714e4e | |||
| 25baf1f661 | |||
| dcbcd1bb80 | |||
| 92ee6260c1 | |||
| 9c7b330f92 | |||
| bc95c0e50c | |||
| aeafb14fb4 | |||
| 4c75b568df | |||
| 9702b210cc | |||
| d53fdb0dda | |||
| 9be3b0794c | |||
| 77db98de65 | |||
| 51babe0164 | |||
| 04a2a227fc | |||
| 42510c0515 | |||
| f36776a91e | |||
| eb18b6a49a | |||
| 3eaa2d9ddf | |||
| 066fd8cbf9 | |||
| 06ebd2964a | |||
| 3304cd0424 | |||
| 32ba17a4f1 | |||
| f83335f110 | |||
| 4da86e5864 | |||
| dd86402763 | |||
| 13d21692bf | |||
| e672f43ec8 | |||
| afd3d2829f | |||
| 619fa0d0bb | |||
| 9cf457acf8 | |||
| f1f9ca9606 | |||
| 438ec7c3e1 | |||
| 21989b558b | |||
| 7dc6bb57a6 | |||
| c16d5ce821 | |||
| c2b71a6a9e | |||
| 585dbebb69 | |||
| 5c50e10205 | |||
| 2c8dd12281 | |||
| 9fb2a63466 | |||
| 45e64757d0 | |||
| 00469314ca | |||
| ed9691511b | |||
| 7cd607f523 | |||
| d2de074dc4 | |||
| 45f0f7eefe | |||
| ba0f5a9bde | |||
| 18b4fcf8eb | |||
| 8a5a7db146 | |||
| 477e5f4ecf | |||
| a26b20b56c | |||
| 18b258d060 | |||
| 093454f5b2 | |||
| 9e186dd2b5 | |||
| 32dfcc3a54 | |||
| 61004b887b | |||
| 143d5f3b81 | |||
| 7341294df6 | |||
| 269105371b | |||
| 1380c5ac67 |
@@ -34,6 +34,7 @@ module.exports = {
|
||||
'P',
|
||||
'Admonition',
|
||||
'Admonition.Admonition',
|
||||
'AgeAssuranceAdmonition',
|
||||
'Span',
|
||||
],
|
||||
impliedTextProps: [],
|
||||
|
||||
@@ -13,6 +13,7 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.repository == 'bluesky-social/social-app'
|
||||
name: Build and Submit Android
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
@@ -13,6 +13,7 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.repository == 'bluesky-social/social-app'
|
||||
name: Build and Submit iOS
|
||||
runs-on: macos-15
|
||||
steps:
|
||||
|
||||
@@ -20,6 +20,7 @@ on:
|
||||
|
||||
jobs:
|
||||
bundleDeploy:
|
||||
if: github.repository == 'bluesky-social/social-app'
|
||||
name: Bundle and Deploy EAS Update
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
@@ -150,7 +151,7 @@ jobs:
|
||||
needs: [bundleDeploy]
|
||||
# Gotta check if its NOT '[]' because any md5 hash in the outputs is detected as a possible secret and won't be
|
||||
# available here
|
||||
if: ${{ inputs.channel != 'production' && needs.bundleDeploy.outputs.changes-detected }}
|
||||
if: ${{ inputs.channel != 'production' && needs.bundleDeploy.outputs.changes-detected && github.repository == 'bluesky-social/social-app' }}
|
||||
steps:
|
||||
- name: Check for EXPO_TOKEN
|
||||
run: >
|
||||
@@ -239,7 +240,7 @@ jobs:
|
||||
needs: [bundleDeploy]
|
||||
# Gotta check if its NOT '[]' because any md5 hash in the outputs is detected as a possible secret and won't be
|
||||
# available here
|
||||
if: ${{ inputs.channel != 'production' && needs.bundleDeploy.outputs.changes-detected }}
|
||||
if: ${{ inputs.channel != 'production' && needs.bundleDeploy.outputs.changes-detected && github.repository == 'bluesky-social/social-app'}}
|
||||
|
||||
steps:
|
||||
- name: Check for EXPO_TOKEN
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
---
|
||||
name: PR Comment Trigger
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
# Permissiosn to make comments in the pull request
|
||||
permissions:
|
||||
pull-requests: write
|
||||
actions: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
handle-comment:
|
||||
if: github.event.issue.pull_request
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should-deploy: ${{ steps.check-org.outputs.result }}
|
||||
|
||||
steps:
|
||||
- name: Check if bot is mentioned
|
||||
id: check-mention
|
||||
env:
|
||||
COMMENT: ${{ github.event.comment.body }}
|
||||
run: |
|
||||
if [[ "$COMMENT" == *"@github-actions"* ]] || \
|
||||
[[ "$COMMENT" == *"github-actions[bot]"* ]]; then
|
||||
bot_mentioned=true
|
||||
else
|
||||
bot_mentioned=false
|
||||
fi
|
||||
|
||||
|
||||
if [[ "${{ github.event.comment.body }}" == *"ota"* ]]; then
|
||||
has_ota=true
|
||||
else
|
||||
has_ota=false
|
||||
fi
|
||||
|
||||
|
||||
if [[ "$bot_mentioned" == "true" ]] && [[ "$has_ota" == "true" ]]; then
|
||||
echo "mentioned=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "mentioned=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Check organization membership
|
||||
if: steps.check-mention.outputs.mentioned == 'true'
|
||||
id: check-org
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
try {
|
||||
const { data: perm } = await github.rest.repos.getCollaboratorPermissionLevel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
username: context.payload.comment.user.login
|
||||
});
|
||||
|
||||
const hasAccess = ['admin', 'write'].includes(perm.permission);
|
||||
console.log(`User has ${perm.permission} access`);
|
||||
|
||||
return hasAccess;
|
||||
} catch(error) {
|
||||
console.log('User has no repository access');
|
||||
return false;
|
||||
}
|
||||
|
||||
bundle-deploy:
|
||||
name: Bundle and Deploy EAS Update
|
||||
runs-on: ubuntu-latest
|
||||
needs: [handle-comment]
|
||||
if: needs.handle-comment.outputs.should-deploy == 'true'
|
||||
|
||||
steps:
|
||||
- name: Get PR HEAD SHA
|
||||
id: pr-info
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const pr = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: ${{ github.event.issue.number }}
|
||||
});
|
||||
|
||||
console.log(`PR HEAD SHA: ${pr.data.head.sha}`);
|
||||
console.log(`PR HEAD REF: ${pr.data.head.ref}`);
|
||||
|
||||
core.setOutput('head-sha', pr.data.head.sha);
|
||||
core.setOutput('head-ref', pr.data.head.ref);
|
||||
|
||||
- name: 💬 Drop a comment
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
with:
|
||||
header: pull-request-eas-build-${{ steps.pr-info.outputs.head-sha }}
|
||||
number: ${{ github.event.issue.number }}
|
||||
message: |
|
||||
An OTA deployment has been requested and is now running for `${{ steps.pr-info.outputs.head-sha }}`.
|
||||
|
||||
[Here is some music to listen to while you wait...](https://www.youtube.com/watch?v=VBlFHuCzPgY)
|
||||
---
|
||||
*Generated by [PR labeler](https://github.com/expo/expo/actions/workflows/pr-labeler.yml) 🤖*
|
||||
|
||||
- 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
|
||||
with:
|
||||
ref: ${{ steps.pr-info.outputs.head-sha }}
|
||||
|
||||
- name: 🔧 Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: yarn
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Lint check
|
||||
run: yarn lint
|
||||
|
||||
- name: Lint lockfile
|
||||
run: yarn lockfile-lint
|
||||
|
||||
- name: 🔤 Compile translations
|
||||
run: yarn intl:build 2>&1 | tee i18n.log
|
||||
|
||||
- name: Check for i18n compilation errors
|
||||
run: if grep -q "invalid syntax" "i18n.log"; then echo "\n\nFound compilation errors!\n\n" && exit 1; else echo "\n\nNo compilation errors!\n\n"; fi
|
||||
|
||||
- name: Type check
|
||||
run: yarn typecheck
|
||||
|
||||
- name: 🔨 Setup EAS
|
||||
uses: expo/expo-github-action@v8
|
||||
with:
|
||||
expo-version: latest
|
||||
eas-version: latest
|
||||
token: ${{ secrets.EXPO_TOKEN }}
|
||||
|
||||
- name: ⛏️ Setup Expo
|
||||
run: yarn global add eas-cli-local-build-plugin
|
||||
|
||||
- name: 🪛 Setup jq
|
||||
uses: dcarbone/install-jq-action@v2
|
||||
|
||||
- name: ✏️ Write environment variables
|
||||
run: |
|
||||
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
|
||||
echo "${{ secrets.ENV_TOKEN }}" > .env
|
||||
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
|
||||
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
|
||||
echo "BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
|
||||
echo "$json" > google-services.json
|
||||
|
||||
- name: Setup Sentry vars for build-time injection
|
||||
id: sentry
|
||||
run: |
|
||||
echo "SENTRY_DIST=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
|
||||
echo "SENTRY_RELEASE=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: 🏗️ Create Bundle
|
||||
run: SENTRY_DIST=${{ steps.sentry.outputs.SENTRY_DIST }} SENTRY_RELEASE=${{ steps.sentry.outputs.SENTRY_RELEASE }} SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_DSN=${{ secrets.SENTRY_DSN }} EXPO_PUBLIC_ENV="testflight" yarn export
|
||||
|
||||
- name: 📦 Package Bundle and 🚀 Deploy
|
||||
run: yarn use-build-number bash scripts/bundleUpdate.sh
|
||||
env:
|
||||
DENIS_API_KEY: ${{ secrets.DENIS_API_KEY }}
|
||||
CHANNEL_NAME: pull-request-${{ github.event.issue.number }}
|
||||
RUNTIME_VERSION:
|
||||
|
||||
- name: 💬 Drop a comment
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
with:
|
||||
header: pull-request-eas-build-${{ steps.pr-info.outputs.head-sha }}
|
||||
number: ${{ github.event.issue.number }}
|
||||
message: |
|
||||
Your requested OTA deployment was successful! You may now apply it by opening the deep link below in your browser:
|
||||
|
||||
`bluesky://intent/apply-ota?channel=pull-request-${{ github.event.issue.number }}`
|
||||
---
|
||||
*Generated by [PR labeler](https://github.com/expo/expo/actions/workflows/pr-labeler.yml) 🤖*
|
||||
|
||||
|
||||
- name: 💬 Drop a comment
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
if: failure()
|
||||
with:
|
||||
header: pull-request-eas-build-${{ steps.pr-info.outputs.head-sha }}
|
||||
number: ${{ github.event.issue.number }}
|
||||
message: |
|
||||
Your requested OTA deployment was unsuccessful. See action logs for more details.
|
||||
---
|
||||
*Generated by [PR labeler](https://github.com/expo/expo/actions/workflows/pr-labeler.yml) 🤖*
|
||||
@@ -0,0 +1,31 @@
|
||||
name: Sync to internal repo
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'bluesky-social/social-app'
|
||||
steps:
|
||||
- name: Checkout public repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Generate GitHub App Token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@v1
|
||||
with:
|
||||
app-id: ${{ vars.SYNC_INTERNAL_APP_ID }}
|
||||
private-key: ${{ secrets.SYNC_INTERNAL_PK }}
|
||||
repositories: social-app-internal
|
||||
- name: Push to internal repo
|
||||
env:
|
||||
TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
run: |
|
||||
git config user.name "github-actions"
|
||||
git config user.email "test@users.noreply.github.com"
|
||||
git config --unset-all http.https://github.com/.extraheader
|
||||
git remote add internal https://x-access-token:${TOKEN}@github.com/bluesky-social/social-app-internal.git
|
||||
git push internal main --force
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
||||
npx lint-staged --concurrent false
|
||||
|
||||
@@ -28,7 +28,6 @@ appId: xyz.blueskyweb.app
|
||||
- hideKeyboard
|
||||
- tapOn:
|
||||
id: "nextBtn"
|
||||
- tapOn: "Not now"
|
||||
- inputText: "e2e-test"
|
||||
- tapOn:
|
||||
id: "nextBtn"
|
||||
|
||||
@@ -135,7 +135,7 @@ appId: xyz.blueskyweb.app
|
||||
id: "e2eGotoLists"
|
||||
- tapOn: "Good Ppl"
|
||||
|
||||
# - tapOn: "People"
|
||||
- tapOn: "People"
|
||||
- assertVisible: "View Bob's profile"
|
||||
- tapOn:
|
||||
point: "90%,43%"
|
||||
|
||||
@@ -23,5 +23,4 @@ appId: xyz.blueskyweb.app
|
||||
id: "loginPasswordInput"
|
||||
- inputText: "hunter2"
|
||||
- pressKey: Enter
|
||||
- tapOn: "Not now"
|
||||
- assertVisible: "Following"
|
||||
|
||||
@@ -25,6 +25,8 @@ appId: xyz.blueskyweb.app
|
||||
# Can follow/unfollow another user
|
||||
- tapOn:
|
||||
id: "followBtn"
|
||||
- tapOn:
|
||||
point: 65%,35% # dismiss the subscribe prompt
|
||||
- tapOn:
|
||||
id: "unfollowBtn"
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import {deleteAsync} from 'expo-file-system'
|
||||
import {createDownloadResumable, deleteAsync} from 'expo-file-system'
|
||||
import {manipulateAsync, SaveFormat} from 'expo-image-manipulator'
|
||||
import RNFetchBlob from 'rn-fetch-blob'
|
||||
|
||||
import {
|
||||
downloadAndResize,
|
||||
DownloadAndResizeOpts,
|
||||
type DownloadAndResizeOpts,
|
||||
getResizedDimensions,
|
||||
} from '../../src/lib/media/manip'
|
||||
|
||||
@@ -32,11 +31,12 @@ describe('downloadAndResize', () => {
|
||||
})
|
||||
|
||||
it('should return resized image for valid URI and options', async () => {
|
||||
const mockedFetch = RNFetchBlob.fetch as jest.Mock
|
||||
mockedFetch.mockResolvedValueOnce({
|
||||
path: jest.fn().mockReturnValue('file://downloaded-image.jpg'),
|
||||
info: jest.fn().mockReturnValue({status: 200}),
|
||||
flush: jest.fn(),
|
||||
const mockedFetch = createDownloadResumable as jest.Mock
|
||||
mockedFetch.mockReturnValue({
|
||||
cancelAsync: jest.fn(),
|
||||
downloadAsync: jest
|
||||
.fn()
|
||||
.mockResolvedValue({uri: 'file://resized-image.jpg'}),
|
||||
})
|
||||
|
||||
const opts: DownloadAndResizeOpts = {
|
||||
@@ -50,13 +50,12 @@ describe('downloadAndResize', () => {
|
||||
|
||||
const result = await downloadAndResize(opts)
|
||||
expect(result).toEqual(mockResizedImage)
|
||||
expect(RNFetchBlob.config).toHaveBeenCalledWith({
|
||||
fileCache: true,
|
||||
appendExt: 'jpeg',
|
||||
})
|
||||
expect(RNFetchBlob.fetch).toHaveBeenCalledWith(
|
||||
'GET',
|
||||
'https://example.com/image.jpg',
|
||||
expect(createDownloadResumable).toHaveBeenCalledWith(
|
||||
opts.uri,
|
||||
expect.anything(),
|
||||
{
|
||||
cache: true,
|
||||
},
|
||||
)
|
||||
|
||||
// First time it gets called is to get dimensions
|
||||
@@ -86,28 +85,6 @@ describe('downloadAndResize', () => {
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should return undefined for non-200 response', async () => {
|
||||
const mockedFetch = RNFetchBlob.fetch as jest.Mock
|
||||
mockedFetch.mockResolvedValueOnce({
|
||||
path: jest.fn().mockReturnValue('file://downloaded-image'),
|
||||
info: jest.fn().mockReturnValue({status: 400}),
|
||||
flush: jest.fn(),
|
||||
})
|
||||
|
||||
const opts: DownloadAndResizeOpts = {
|
||||
uri: 'https://example.com/image',
|
||||
width: 100,
|
||||
height: 100,
|
||||
maxSize: 500000,
|
||||
mode: 'cover',
|
||||
timeout: 10000,
|
||||
}
|
||||
|
||||
const result = await downloadAndResize(opts)
|
||||
expect(errorSpy).not.toHaveBeenCalled()
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should not downsize whenever dimensions are below the max dimensions', () => {
|
||||
const initialDimensionsOne = {
|
||||
width: 1200,
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
createStarterPackLinkFromAndroidReferrer,
|
||||
parseStarterPackUri,
|
||||
} from '#/lib/strings/starter-pack'
|
||||
import {tenorUrlToBskyGifUrl} from '#/state/queries/tenor'
|
||||
import {cleanError} from '../../src/lib/strings/errors'
|
||||
import {createFullHandle, makeValidHandle} from '../../src/lib/strings/handles'
|
||||
import {enforceLen} from '../../src/lib/strings/helpers'
|
||||
@@ -997,3 +998,18 @@ describe('createStarterPackGooglePlayUri', () => {
|
||||
expect(createStarterPackGooglePlayUri(undefined, 'rkey')).toEqual(null)
|
||||
})
|
||||
})
|
||||
|
||||
describe('tenorUrlToBskyGifUrl', () => {
|
||||
const inputs = [
|
||||
'https://media.tenor.com/someID_AAAAC/someName.gif',
|
||||
'https://media.tenor.com/someID/someName.gif',
|
||||
]
|
||||
|
||||
it.each(inputs)(
|
||||
'returns url with t.gifs.bsky.app as hostname for input url',
|
||||
input => {
|
||||
const out = tenorUrlToBskyGifUrl(input)
|
||||
expect(out.startsWith('https://t.gifs.bsky.app/')).toEqual(true)
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
@@ -32,6 +32,7 @@ describe('getMentionAt', () => {
|
||||
['@alice hello', 7, undefined],
|
||||
['alice@alice', 0, undefined],
|
||||
['alice@alice', 6, undefined],
|
||||
['hello @alice-com goodbye', 8, 'alice-com'],
|
||||
]
|
||||
|
||||
it.each(cases)(
|
||||
@@ -72,6 +73,7 @@ describe('insertMentionAt', () => {
|
||||
['@alice hello', 7, '@alice hello'],
|
||||
['alice@alice', 0, 'alice@alice'],
|
||||
['alice@alice', 6, 'alice@alice'],
|
||||
['hello @alice-com goodbye', 10, 'hello @alice.com goodbye'],
|
||||
]
|
||||
|
||||
it.each(cases)(
|
||||
|
||||
@@ -26,12 +26,7 @@ module.exports = function (_config) {
|
||||
...(IS_DEV || IS_TESTFLIGHT ? [] : []),
|
||||
]
|
||||
|
||||
const UPDATES_CHANNEL = IS_TESTFLIGHT
|
||||
? 'testflight'
|
||||
: IS_PRODUCTION
|
||||
? 'production'
|
||||
: undefined
|
||||
const UPDATES_ENABLED = !!UPDATES_CHANNEL
|
||||
const UPDATES_ENABLED = IS_TESTFLIGHT || IS_PRODUCTION
|
||||
|
||||
const USE_SENTRY = Boolean(process.env.SENTRY_AUTH_TOKEN)
|
||||
|
||||
@@ -190,7 +185,6 @@ module.exports = function (_config) {
|
||||
}
|
||||
: undefined,
|
||||
checkAutomatically: 'NEVER',
|
||||
channel: UPDATES_CHANNEL,
|
||||
},
|
||||
plugins: [
|
||||
'expo-video',
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2Zm0 2a8 8 0 1 0 0 16 8 8 0 0 0 0-16Zm-.63 3.225a1 1 0 0 1 1.337.068l3 3 .068.076a1 1 0 0 1-1.406 1.406l-.076-.068L13 10.414V16a1 1 0 1 1-2 0v-5.586l-1.293 1.293a1 1 0 1 1-1.414-1.414l3-3 .076-.068Z"/></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" d="M12 2a1 1 0 0 1 0 2 5.85 5.85 0 0 0-5.802 5.08L5.143 17h13.715l-.382-2.868-.01-.102a1 1 0 0 1 1.973-.262l.02.1.532 4a1 1 0 0 1-.99 1.132h-3.357c-.905 1.747-2.606 3-4.644 3s-3.74-1.253-4.643-3H4a1 1 0 0 1-.991-1.132l1.207-9.053A7.85 7.85 0 0 1 12 2ZM9.78 19c.61.637 1.397 1 2.22 1s1.611-.363 2.22-1H9.78ZM17 2.5a1 1 0 0 1 1 1V6h2.5a1 1 0 0 1 0 2H18v2.5a1 1 0 0 1-2 0V8h-2.5a1 1 0 1 1 0-2H16V3.5a1 1 0 0 1 1-1Z"/></svg>
|
||||
|
After Width: | Height: | Size: 511 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M12 2a7.854 7.854 0 0 1 7.784 6.815l1.207 9.053a1 1 0 0 1-.99 1.132h-3.354c-.904 1.748-2.608 3-4.647 3-2.038 0-3.742-1.252-4.646-3H4a1.002 1.002 0 0 1-.991-1.132l1.207-9.053A7.85 7.85 0 0 1 12 2ZM9.78 19c.608.637 1.398 1 2.221 1s1.613-.363 2.222-1H9.779ZM3.193 2.104a1 1 0 0 1 1.53 1.288A9.5 9.5 0 0 0 2.72 7.464a1 1 0 0 1-1.954-.427 11.46 11.46 0 0 1 2.428-4.933Zm16.205-.122a1 1 0 0 1 1.409.122 11.5 11.5 0 0 1 2.429 4.933 1 1 0 0 1-1.954.427 9.5 9.5 0 0 0-2.006-4.072 1 1 0 0 1 .122-1.41Z"/></svg>
|
||||
|
After Width: | Height: | Size: 594 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M12 2a7.854 7.854 0 0 1 7.785 6.815l1.055 7.92.018.224a2 2 0 0 1-2 2.041h-2.215c-.904 1.747-2.605 3-4.643 3s-3.739-1.253-4.643-3H5.142a2 2 0 0 1-1.982-2.265l1.056-7.92.057-.363A7.854 7.854 0 0 1 12 2ZM9.78 19c.609.637 1.398 1 2.22 1s1.611-.363 2.22-1H9.78ZM12 4a5.854 5.854 0 0 0-5.76 4.81l-.041.27L5.142 17h13.716l-1.056-7.92A5.854 5.854 0 0 0 12 4ZM2.718 7.464a1 1 0 1 1-1.953-.427l1.953.427Zm20.518-.427a1 1 0 0 1-1.954.427l1.954-.427ZM3.193 2.105a1 1 0 0 1 1.531 1.287 9.5 9.5 0 0 0-2.006 4.072L.765 7.037a11.46 11.46 0 0 1 2.428-4.932Zm16.205-.123a1 1 0 0 1 1.34.047l.069.076.217.265a11.46 11.46 0 0 1 2.212 4.667l-.978.213-.976.214a9.46 9.46 0 0 0-1.826-3.853l-.18-.22-.062-.081a1 1 0 0 1 .184-1.328Z"/></svg>
|
||||
|
After Width: | Height: | Size: 809 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2Zm0 2a8 8 0 1 0 0 16 8 8 0 0 0 0-16Zm0 3a1 1 0 0 1 1 1v3h3l.102.005a1 1 0 0 1 0 1.99L16 13h-3v3a1 1 0 1 1-2 0v-3H8a1 1 0 0 1 0-2h3V8a1 1 0 0 1 1-1Z"/></svg>
|
||||
|
After Width: | Height: | Size: 321 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M3.92 19v-4.153a1 1 0 0 1 1-1H9l.103.005a1 1 0 0 1 0 1.99L9 15.847H7.285c.854.737 1.784 1.38 2.631 1.9.702.431 1.329.769 1.78.997l.291.143a25.6 25.6 0 0 0 3.67-2.326c2.144-1.642 4.073-3.756 4.315-6.023a1 1 0 0 1 1.988.212c-.336 3.154-2.89 5.717-5.086 7.398a27.6 27.6 0 0 1-4.34 2.704l-.078.038-.021.01-.007.003-.002.001-.001.001a1 1 0 0 1-.827.01v0h-.002l-.004-.002-.013-.006q-.016-.006-.045-.02l-.162-.075a27 27 0 0 1-2.503-1.361 22 22 0 0 1-2.95-2.143V19a1 1 0 0 1-2 0ZM2 10c0-2.214.696-3.971 1.833-5.184A5.7 5.7 0 0 1 8 3a7.1 7.1 0 0 1 4 1.228A7.1 7.1 0 0 1 16 3a5.68 5.68 0 0 1 3.469 1.185l.031-1.702a1 1 0 0 1 2 .035l-.081 4.5a1 1 0 0 1-1 .983H16.5a1 1 0 1 1 0-2h2.02A3.68 3.68 0 0 0 16 5a5.12 5.12 0 0 0-3.11 1.053 3 3 0 0 0-.155.129l-.029.025v.002l-.003.002-.072.064a1 1 0 0 1-1.338-.068l-.028-.025a3 3 0 0 0-.155-.13A5.12 5.12 0 0 0 8 5c-.982 0-1.965.392-2.708 1.185C4.554 6.97 4 8.214 4 10q0 .507.099 1.002l.075.328.02.1a1 1 0 0 1-1.925.5l-.03-.097-.102-.446A7 7 0 0 1 2 10Z"/></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" d="M16 6a1 1 0 0 0-1-1H9a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V6ZM2.87 7.225a1 1 0 0 1 1.337 1.482L3.155 9.759a.546.546 0 0 0-.05.714l.119.173c.52.827.52 1.88 0 2.707l-.12.174a.546.546 0 0 0 .051.714l1.052 1.052.069.076a1 1 0 0 1-1.407 1.406l-.076-.068-1.052-1.052a2.55 2.55 0 0 1-.237-3.328l.048-.075a.55.55 0 0 0 0-.504l-.048-.075a2.55 2.55 0 0 1 .237-3.328l1.052-1.052.076-.068Zm16.923.068a1 1 0 0 1 1.338-.068l.076.068 1.052 1.052.16.174c.696.837.78 2.03.209 2.958l-.133.196a.55.55 0 0 0 0 .654l.133.196a2.55 2.55 0 0 1-.21 2.958l-.16.174-1.05 1.052a1 1 0 1 1-1.415-1.414l1.052-1.052.064-.077a.55.55 0 0 0 .04-.552l-.053-.085a2.545 2.545 0 0 1 0-3.054l.052-.085a.55.55 0 0 0-.039-.552l-.064-.077-1.052-1.052-.068-.076a1 1 0 0 1 .068-1.338ZM13 6l.103.005a1 1 0 0 1 0 1.99L13 8h-2a1 1 0 1 1 0-2h2Zm5 12a3 3 0 0 1-3 3H9a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v12Z"/></svg>
|
||||
|
After Width: | Height: | Size: 979 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M6.043 14.293a1 1 0 1 1 1.414 1.414L5.164 18l2.293 2.293.068.076a1 1 0 0 1-1.406 1.406l-.076-.068-2.47-2.47a1.75 1.75 0 0 1 0-2.474l2.47-2.47Zm6.22 0a1 1 0 0 1 1.414 1.414L12.384 17H18a1 1 0 0 0 1-1v-3a1 1 0 1 1 2 0v3a3 3 0 0 1-3 3h-5.616l1.293 1.293.068.076a1 1 0 0 1-1.406 1.406l-.076-.068-2.47-2.47a1.75 1.75 0 0 1 0-2.474l2.47-2.47ZM3 11V8a3 3 0 0 1 3-3h5.586l-1.293-1.293-.068-.076a1 1 0 0 1 1.406-1.406l.076.068 2.47 2.47.12.133a1.75 1.75 0 0 1 0 2.209l-.12.132-2.47 2.47a1 1 0 1 1-1.414-1.414L11.586 7H6a1 1 0 0 0-1 1v3a1 1 0 1 1-2 0Zm13.543-8.707a1 1 0 0 1 1.338-.068l.076.068 2.47 2.47.12.133a1.75 1.75 0 0 1 0 2.209l-.12.132-2.47 2.47a1 1 0 1 1-1.414-1.414L18.836 6l-2.293-2.293-.068-.076a1 1 0 0 1 .068-1.338Z"/></svg>
|
||||
|
After Width: | Height: | Size: 823 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M6 2a2.998 2.998 0 0 1 1 5.825V8a2 2 0 0 0 2 2h1.174c.412-1.165 1.52-2 2.826-2h5a3 3 0 1 1 0 6h-5a3 3 0 0 1-2.826-2H9a4 4 0 0 1-2-.537V16a2 2 0 0 0 2 2h1.174c.412-1.165 1.52-2 2.826-2h5a3 3 0 1 1 0 6h-5a3 3 0 0 1-2.826-2H9a4 4 0 0 1-4-4V7.825A2.998 2.998 0 0 1 6 2Zm7 16a1 1 0 1 0 0 2h5a1 1 0 1 0 0-2h-5Zm0-8a1 1 0 1 0 0 2h5a1 1 0 1 0 0-2h-5ZM6 4a1 1 0 1 0 0 2 1 1 0 0 0 0-2Z"/></svg>
|
||||
|
After Width: | Height: | Size: 478 B |
|
After Width: | Height: | Size: 22 KiB |
@@ -160,7 +160,7 @@ function PostContent({record}: {record: AppBskyFeedPost.Record | null}) {
|
||||
richText.push(
|
||||
<Link
|
||||
key={counter}
|
||||
href={`/tag/${segment.tag.tag}`}
|
||||
href={`/hashtag/${segment.tag.tag}`}
|
||||
className="text-blue-500 hover:underline">
|
||||
{segment.text}
|
||||
</Link>,
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||
darkMode: ['variant', [
|
||||
'&:is(.dark *):not(:is(.dark .light *))',
|
||||
]],
|
||||
darkMode: ['variant', ['&:is(.dark *):not(:is(.dark .light *))']],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES5",
|
||||
|
||||
@@ -1,27 +1,30 @@
|
||||
import assert from 'assert'
|
||||
import {
|
||||
Kysely,
|
||||
KyselyPlugin,
|
||||
type KyselyPlugin,
|
||||
Migrator,
|
||||
PluginTransformQueryArgs,
|
||||
PluginTransformResultArgs,
|
||||
type PluginTransformQueryArgs,
|
||||
type PluginTransformResultArgs,
|
||||
PostgresDialect,
|
||||
QueryResult,
|
||||
RootOperationNode,
|
||||
UnknownRow,
|
||||
type QueryResult,
|
||||
type RootOperationNode,
|
||||
type UnknownRow,
|
||||
} from 'kysely'
|
||||
import {default as Pg} from 'pg'
|
||||
|
||||
import {dbLogger as log} from '../logger.js'
|
||||
import {default as migrations} from './migrations/index.js'
|
||||
import {DbMigrationProvider} from './migrations/provider.js'
|
||||
import {DbSchema} from './schema.js'
|
||||
import {type DbSchema} from './schema.js'
|
||||
|
||||
export class Database {
|
||||
migrator: Migrator
|
||||
destroyed = false
|
||||
|
||||
constructor(public db: Kysely<DbSchema>, public cfg: PgConfig) {
|
||||
constructor(
|
||||
public db: Kysely<DbSchema>,
|
||||
public cfg: PgConfig,
|
||||
) {
|
||||
this.migrator = new Migrator({
|
||||
db,
|
||||
migrationTableSchema: cfg.schema,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import events from 'node:events'
|
||||
import http from 'node:http'
|
||||
import type http from 'node:http'
|
||||
|
||||
import cors from 'cors'
|
||||
import express from 'express'
|
||||
import {createHttpTerminator, HttpTerminator} from 'http-terminator'
|
||||
import {createHttpTerminator, type HttpTerminator} from 'http-terminator'
|
||||
|
||||
import {Config} from './config.js'
|
||||
import {type Config} from './config.js'
|
||||
import {AppContext} from './context.js'
|
||||
import {default as routes, errorHandler} from './routes/index.js'
|
||||
|
||||
@@ -17,7 +17,10 @@ export class LinkService {
|
||||
public server?: http.Server
|
||||
private terminator?: HttpTerminator
|
||||
|
||||
constructor(public app: express.Application, public ctx: AppContext) {}
|
||||
constructor(
|
||||
public app: express.Application,
|
||||
public ctx: AppContext,
|
||||
) {}
|
||||
|
||||
static async create(cfg: Config): Promise<LinkService> {
|
||||
let app = express()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {Express} from 'express'
|
||||
import {type Express} from 'express'
|
||||
|
||||
import {AppContext} from '../context.js'
|
||||
import {type AppContext} from '../context.js'
|
||||
import {default as createShortLink} from './createShortLink.js'
|
||||
import {default as health} from './health.js'
|
||||
import {default as redirect} from './redirect.js'
|
||||
|
||||
@@ -2,9 +2,9 @@ import assert from 'node:assert'
|
||||
|
||||
import {DAY, SECOND} from '@atproto/common'
|
||||
import escapeHTML from 'escape-html'
|
||||
import {Express} from 'express'
|
||||
import {type Express} from 'express'
|
||||
|
||||
import {AppContext} from '../context.js'
|
||||
import {type AppContext} from '../context.js'
|
||||
import {handler} from './util.js'
|
||||
|
||||
const INTERNAL_IP_REGEX = new RegExp(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {Express} from 'express'
|
||||
import {type Express} from 'express'
|
||||
|
||||
import {AppContext} from '../context.js'
|
||||
import {type AppContext} from '../context.js'
|
||||
import {handler} from './util.js'
|
||||
|
||||
export default function (ctx: AppContext, app: Express) {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import events from 'node:events'
|
||||
import http from 'node:http'
|
||||
import type http from 'node:http'
|
||||
|
||||
import express from 'express'
|
||||
import {createHttpTerminator, HttpTerminator} from 'http-terminator'
|
||||
import {createHttpTerminator, type HttpTerminator} from 'http-terminator'
|
||||
|
||||
import {Config} from './config.js'
|
||||
import {type Config} from './config.js'
|
||||
import {AppContext} from './context.js'
|
||||
import {default as routes, errorHandler} from './routes/index.js'
|
||||
|
||||
@@ -15,7 +15,10 @@ export class CardService {
|
||||
public server?: http.Server
|
||||
private terminator?: HttpTerminator
|
||||
|
||||
constructor(public app: express.Application, public ctx: AppContext) {}
|
||||
constructor(
|
||||
public app: express.Application,
|
||||
public ctx: AppContext,
|
||||
) {}
|
||||
|
||||
static async create(cfg: Config): Promise<CardService> {
|
||||
let app = express()
|
||||
|
||||
@@ -258,6 +258,7 @@ func serve(cctx *cli.Context) error {
|
||||
e.GET("/feeds", server.WebGeneric)
|
||||
e.GET("/notifications", server.WebGeneric)
|
||||
e.GET("/notifications/settings", server.WebGeneric)
|
||||
e.GET("/notifications/activity", server.WebGeneric)
|
||||
e.GET("/lists", server.WebGeneric)
|
||||
e.GET("/moderation", server.WebGeneric)
|
||||
e.GET("/moderation/modlists", server.WebGeneric)
|
||||
@@ -275,9 +276,21 @@ func serve(cctx *cli.Context) error {
|
||||
e.GET("/settings/appearance", server.WebGeneric)
|
||||
e.GET("/settings/account", server.WebGeneric)
|
||||
e.GET("/settings/privacy-and-security", server.WebGeneric)
|
||||
e.GET("/settings/privacy-and-security/activity", server.WebGeneric)
|
||||
e.GET("/settings/content-and-media", server.WebGeneric)
|
||||
e.GET("/settings/interests", server.WebGeneric)
|
||||
e.GET("/settings/about", server.WebGeneric)
|
||||
e.GET("/settings/notifications", server.WebGeneric)
|
||||
e.GET("/settings/notifications/replies", server.WebGeneric)
|
||||
e.GET("/settings/notifications/mentions", server.WebGeneric)
|
||||
e.GET("/settings/notifications/quotes", server.WebGeneric)
|
||||
e.GET("/settings/notifications/likes", server.WebGeneric)
|
||||
e.GET("/settings/notifications/reposts", server.WebGeneric)
|
||||
e.GET("/settings/notifications/new-followers", server.WebGeneric)
|
||||
e.GET("/settings/notifications/likes-on-reposts", server.WebGeneric)
|
||||
e.GET("/settings/notifications/reposts-on-reposts", server.WebGeneric)
|
||||
e.GET("/settings/notifications/activity", server.WebGeneric)
|
||||
e.GET("/settings/notifications/miscellaneous", server.WebGeneric)
|
||||
e.GET("/settings/app-icon", server.WebGeneric)
|
||||
e.GET("/sys/debug", server.WebGeneric)
|
||||
e.GET("/sys/debug-mod", server.WebGeneric)
|
||||
@@ -289,6 +302,7 @@ func serve(cctx *cli.Context) error {
|
||||
e.GET("/support/copyright", server.WebGeneric)
|
||||
e.GET("/intent/compose", server.WebGeneric)
|
||||
e.GET("/intent/verify-email", server.WebGeneric)
|
||||
e.GET("/intent/age-assurance", server.WebGeneric)
|
||||
e.GET("/messages", server.WebGeneric)
|
||||
e.GET("/messages/:conversation", server.WebGeneric)
|
||||
|
||||
@@ -590,9 +604,12 @@ type IPCCRequest struct {
|
||||
IP string `json:"ip"`
|
||||
}
|
||||
type IPCCResponse struct {
|
||||
CC string `json:"countryCode"`
|
||||
CC string `json:"countryCode"`
|
||||
AgeRestrictedGeo bool `json:"isAgeRestrictedGeo,omitempty"`
|
||||
}
|
||||
|
||||
// IP address data is powered by IPinfo
|
||||
// https://ipinfo.io
|
||||
func (srv *Server) WebIpCC(c echo.Context) error {
|
||||
realIP := c.RealIP()
|
||||
addr, err := netip.ParseAddr(realIP)
|
||||
|
||||
@@ -33,15 +33,10 @@ jest.mock('react-native-safe-area-context', () => {
|
||||
}
|
||||
})
|
||||
|
||||
jest.mock('rn-fetch-blob', () => ({
|
||||
config: jest.fn().mockReturnThis(),
|
||||
cancel: jest.fn(),
|
||||
fetch: jest.fn(),
|
||||
}))
|
||||
|
||||
jest.mock('expo-file-system', () => ({
|
||||
getInfoAsync: jest.fn().mockResolvedValue({exists: true, size: 100}),
|
||||
deleteAsync: jest.fn(),
|
||||
createDownloadResumable: jest.fn(),
|
||||
}))
|
||||
|
||||
jest.mock('expo-image-manipulator', () => ({
|
||||
@@ -101,7 +96,7 @@ jest.mock('expo-modules-core', () => ({
|
||||
}
|
||||
}
|
||||
}),
|
||||
requireNativeViewManager: jest.fn().mockImplementation(moduleName => {
|
||||
requireNativeViewManager: jest.fn().mockImplementation(_ => {
|
||||
return () => null
|
||||
}),
|
||||
}))
|
||||
|
||||
@@ -15,6 +15,8 @@ class BackgroundNotificationHandler(
|
||||
|
||||
if (remoteMessage.data["reason"] == "chat-message") {
|
||||
mutateWithChatMessage(remoteMessage)
|
||||
} else {
|
||||
mutateWithOtherReason(remoteMessage)
|
||||
}
|
||||
|
||||
notifInterface.showMessage(remoteMessage)
|
||||
@@ -39,4 +41,17 @@ class BackgroundNotificationHandler(
|
||||
// TODO - Remove this once we have more backend capability
|
||||
remoteMessage.data["badge"] = null
|
||||
}
|
||||
|
||||
private fun mutateWithOtherReason(remoteMessage: RemoteMessage) {
|
||||
// If oreo or higher
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
||||
// If one of "like", "repost", "follow", "mention", "reply", "quote", "like-via-repost", "repost-via-repost", "subscribed-post"
|
||||
// assign to it's eponymous channel. otherwise do nothing, let expo handle it
|
||||
when (remoteMessage.data["reason"]) {
|
||||
"like", "repost", "follow", "mention", "reply", "quote", "like-via-repost", "repost-via-repost", "subscribed-post" -> {
|
||||
remoteMessage.data["channelId"] = remoteMessage.data["reason"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ Pod::Spec.new do |s|
|
||||
s.static_framework = true
|
||||
|
||||
s.dependency 'ExpoModulesCore'
|
||||
s.dependency 'MCEmojiPicker'
|
||||
s.dependency 'MCEmojiPicker', '1.2.3'
|
||||
|
||||
# Swift/Objective-C compatibility
|
||||
s.pod_target_xcconfig = {
|
||||
|
||||
@@ -12,7 +12,7 @@ const EmojiPicker = ({onEmojiSelected}: EmojiPickerViewProps) => {
|
||||
flex: 1,
|
||||
width: '100%',
|
||||
backgroundColor: scheme === 'dark' ? '#000' : '#fff',
|
||||
} as const),
|
||||
}) as const,
|
||||
[scheme],
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bsky.app",
|
||||
"version": "1.104.0",
|
||||
"version": "1.106.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
@@ -12,7 +12,8 @@
|
||||
"buildFromSource": [
|
||||
"expo-notifications",
|
||||
"expo-haptics",
|
||||
"expo-media-library"
|
||||
"expo-media-library",
|
||||
"expo-image-picker"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -69,12 +70,12 @@
|
||||
"icons:optimize": "svgo -f ./assets/icons"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "^0.15.9",
|
||||
"@atproto/api": "^0.15.26",
|
||||
"@bitdrift/react-native": "^0.6.8",
|
||||
"@braintree/sanitize-url": "^6.0.2",
|
||||
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
"@expo/html-elements": "^0.12.4",
|
||||
"@expo/html-elements": "^0.12.5",
|
||||
"@expo/webpack-config": "^19.0.1",
|
||||
"@floating-ui/dom": "^1.6.3",
|
||||
"@floating-ui/react-dom": "^2.0.8",
|
||||
@@ -85,12 +86,12 @@
|
||||
"@fortawesome/free-regular-svg-icons": "^6.1.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.1.1",
|
||||
"@fortawesome/react-native-fontawesome": "^0.3.2",
|
||||
"@haileyok/bluesky-video": "0.2.6",
|
||||
"@haileyok/bluesky-video": "0.3.2",
|
||||
"@ipld/dag-cbor": "^9.2.0",
|
||||
"@lingui/react": "^4.14.1",
|
||||
"@mattermost/react-native-paste-input": "^0.7.1",
|
||||
"@miblanchard/react-native-slider": "^2.3.1",
|
||||
"@mozzius/expo-dynamic-app-icon": "^1.5.0",
|
||||
"@mattermost/react-native-paste-input": "mattermost/react-native-paste-input",
|
||||
"@miblanchard/react-native-slider": "^2.6.0",
|
||||
"@mozzius/expo-dynamic-app-icon": "1.5.0",
|
||||
"@react-native-async-storage/async-storage": "2.1.2",
|
||||
"@react-native-menu/menu": "^1.2.3",
|
||||
"@react-native-picker/picker": "2.11.0",
|
||||
@@ -98,7 +99,7 @@
|
||||
"@react-navigation/drawer": "^7.3.12",
|
||||
"@react-navigation/native": "^7.1.9",
|
||||
"@react-navigation/native-stack": "^7.3.13",
|
||||
"@sentry/react-native": "~6.10.0",
|
||||
"@sentry/react-native": "~6.14.0",
|
||||
"@tanstack/query-async-storage-persister": "^5.25.0",
|
||||
"@tanstack/react-query": "^5.8.1",
|
||||
"@tanstack/react-query-persist-client": "^5.25.0",
|
||||
@@ -130,33 +131,33 @@
|
||||
"emoji-mart": "^5.5.2",
|
||||
"emoji-regex": "^10.4.0",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"expo": "^53.0.5",
|
||||
"expo": "53.0.11",
|
||||
"expo-application": "~6.1.4",
|
||||
"expo-blur": "~14.1.4",
|
||||
"expo-blur": "~14.1.5",
|
||||
"expo-build-properties": "~0.14.6",
|
||||
"expo-camera": "~16.1.6",
|
||||
"expo-camera": "~16.1.8",
|
||||
"expo-clipboard": "~7.1.4",
|
||||
"expo-dev-client": "~5.1.7",
|
||||
"expo-dev-client": "~5.2.0",
|
||||
"expo-device": "~7.1.4",
|
||||
"expo-file-system": "~18.1.8",
|
||||
"expo-font": "~13.3.0",
|
||||
"expo-file-system": "~18.1.10",
|
||||
"expo-font": "~13.3.1",
|
||||
"expo-haptics": "~14.1.4",
|
||||
"expo-image": "~2.1.6",
|
||||
"expo-image": "~2.2.1",
|
||||
"expo-image-crop-tool": "^0.1.8",
|
||||
"expo-image-manipulator": "~13.1.5",
|
||||
"expo-image-manipulator": "~13.1.7",
|
||||
"expo-image-picker": "~16.1.4",
|
||||
"expo-linear-gradient": "~14.1.4",
|
||||
"expo-linking": "~7.1.4",
|
||||
"expo-linear-gradient": "~14.1.5",
|
||||
"expo-linking": "~7.1.5",
|
||||
"expo-localization": "~16.1.5",
|
||||
"expo-media-library": "~17.1.6",
|
||||
"expo-notifications": "~0.31.1",
|
||||
"expo-screen-orientation": "~8.1.5",
|
||||
"expo-media-library": "~17.1.7",
|
||||
"expo-notifications": "~0.31.3",
|
||||
"expo-screen-orientation": "~8.1.7",
|
||||
"expo-sharing": "~13.1.5",
|
||||
"expo-splash-screen": "~0.30.8",
|
||||
"expo-system-ui": "~5.0.7",
|
||||
"expo-splash-screen": "~0.30.9",
|
||||
"expo-system-ui": "~5.0.8",
|
||||
"expo-task-manager": "~13.1.5",
|
||||
"expo-updates": "~0.28.12",
|
||||
"expo-video": "~2.1.8",
|
||||
"expo-updates": "~0.28.14",
|
||||
"expo-video": "~2.2.1",
|
||||
"expo-web-browser": "~14.1.6",
|
||||
"fast-text-encoding": "^1.0.6",
|
||||
"history": "^5.3.0",
|
||||
@@ -182,35 +183,34 @@
|
||||
"react-image-crop": "^11.0.7",
|
||||
"react-is": "19",
|
||||
"react-keyed-flatten-children": "^5.0.0",
|
||||
"react-native": "0.79.2",
|
||||
"react-native-compressor": "1.11.0",
|
||||
"react-native": "^0.79.3",
|
||||
"react-native-compressor": "^1.11.0",
|
||||
"react-native-date-picker": "^5.0.12",
|
||||
"react-native-drawer-layout": "^4.1.6",
|
||||
"react-native-drawer-layout": "^4.1.8",
|
||||
"react-native-edge-to-edge": "^1.6.0",
|
||||
"react-native-gesture-handler": "2.25.0",
|
||||
"react-native-get-random-values": "~1.11.0",
|
||||
"react-native-ios-context-menu": "^1.15.3",
|
||||
"react-native-keyboard-controller": "^1.17.1",
|
||||
"react-native-keyboard-controller": "^1.17.5",
|
||||
"react-native-mmkv": "^2.12.2",
|
||||
"react-native-pager-view": "6.7.1",
|
||||
"react-native-pager-view": "6.8.0",
|
||||
"react-native-progress": "bluesky-social/react-native-progress",
|
||||
"react-native-qrcode-styled": "^0.3.3",
|
||||
"react-native-reanimated": "~3.17.5",
|
||||
"react-native-root-siblings": "^4.1.1",
|
||||
"react-native-root-siblings": "^5.0.1",
|
||||
"react-native-safe-area-context": "5.4.0",
|
||||
"react-native-screens": "^4.11.1",
|
||||
"react-native-svg": "15.11.2",
|
||||
"react-native-svg": "15.12.0",
|
||||
"react-native-uitextview": "^1.4.0",
|
||||
"react-native-url-polyfill": "^1.3.0",
|
||||
"react-native-uuid": "^2.0.3",
|
||||
"react-native-view-shot": "^4.0.3",
|
||||
"react-native-web": "~0.20.0",
|
||||
"react-native-web-webview": "^1.0.2",
|
||||
"react-native-webview": "13.13.5",
|
||||
"react-native-webview": "^13.13.5",
|
||||
"react-remove-scroll-bar": "^2.3.8",
|
||||
"react-responsive": "^9.0.2",
|
||||
"react-textarea-autosize": "^8.5.3",
|
||||
"rn-fetch-blob": "^0.12.0",
|
||||
"statsig-react-native-expo": "^4.6.1",
|
||||
"tippy.js": "^6.3.7",
|
||||
"tlds": "^1.234.0",
|
||||
@@ -219,7 +219,7 @@
|
||||
"zod": "^3.20.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@atproto/dev-env": "^0.3.133",
|
||||
"@atproto/dev-env": "^0.3.155",
|
||||
"@babel/core": "^7.26.0",
|
||||
"@babel/preset-env": "^7.26.0",
|
||||
"@babel/runtime": "^7.26.0",
|
||||
@@ -227,8 +227,9 @@
|
||||
"@lingui/cli": "^4.14.1",
|
||||
"@lingui/macro": "^4.14.1",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
|
||||
"@react-native/eslint-config": "^0.79.2",
|
||||
"@react-native/typescript-config": "^0.79.2",
|
||||
"@react-native/babel-preset": "0.79.3",
|
||||
"@react-native/eslint-config": "^0.79.3",
|
||||
"@react-native/typescript-config": "^0.79.3",
|
||||
"@sentry/webpack-plugin": "^3.2.2",
|
||||
"@testing-library/jest-native": "^5.4.3",
|
||||
"@testing-library/react-native": "^13.2.0",
|
||||
@@ -260,12 +261,12 @@
|
||||
"husky": "^8.0.3",
|
||||
"is-ci": "^3.0.1",
|
||||
"jest": "^29.7.0",
|
||||
"jest-expo": "~53.0.3",
|
||||
"jest-expo": "~53.0.7",
|
||||
"jest-junit": "^16.0.0",
|
||||
"lint-staged": "^13.2.3",
|
||||
"lockfile-lint": "^4.14.0",
|
||||
"metro-react-native-babel-preset": "^0.77.0",
|
||||
"prettier": "^2.8.3",
|
||||
"prettier": "^3.6.0",
|
||||
"react-native-dotenv": "^3.4.11",
|
||||
"react-refresh": "^0.14.0",
|
||||
"svgo": "^3.3.2",
|
||||
@@ -275,11 +276,11 @@
|
||||
},
|
||||
"resolutions": {
|
||||
"@expo/image-utils": "0.6.3",
|
||||
"@react-native/babel-preset": "0.79.2",
|
||||
"@react-native/normalize-colors": "0.79.2",
|
||||
"@react-native/babel-preset": "0.79.3",
|
||||
"@react-native/normalize-colors": "0.79.3",
|
||||
"@types/react": "^18",
|
||||
"**/expo-constants": "17.0.3",
|
||||
"**/expo-device": "7.0.1",
|
||||
"**/expo-device": "7.1.4",
|
||||
"**/zod": "3.23.8",
|
||||
"**/multiformats": "9.9.0"
|
||||
},
|
||||
@@ -359,7 +360,8 @@
|
||||
],
|
||||
"allowedUrls": [
|
||||
"https://codeload.github.com/bluesky-social/react-native-bottom-sheet/tar.gz/28a87d1bb55e10fc355fa1455545a30734995908",
|
||||
"https://codeload.github.com/bluesky-social/react-native-progress/tar.gz/5a372f4f2ce5feb26f4f47b6a4d187ab9b923ab4"
|
||||
"https://codeload.github.com/bluesky-social/react-native-progress/tar.gz/5a372f4f2ce5feb26f4f47b6a4d187ab9b923ab4",
|
||||
"https://codeload.github.com/mattermost/react-native-paste-input/tar.gz/f260447edc645a817ab1ba7b46d8341d84dba8e9"
|
||||
],
|
||||
"emptyHostname": false,
|
||||
"validatePackageNames": true,
|
||||
|
||||
@@ -114,7 +114,7 @@ index e916023..5049c33 100644
|
||||
}
|
||||
|
||||
#pragma mark - UIScrollViewDelegate
|
||||
@@ -62,7 +92,6 @@
|
||||
@@ -62,7 +92,6 @@ - (void)setSmartPunctuation:(NSString *)smartPunctuation {
|
||||
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
|
||||
{
|
||||
RCTDirectEventBlock onScroll = self.onScroll;
|
||||
@@ -122,7 +122,7 @@ index e916023..5049c33 100644
|
||||
if (onScroll) {
|
||||
CGPoint contentOffset = scrollView.contentOffset;
|
||||
CGSize contentSize = scrollView.contentSize;
|
||||
@@ -71,22 +100,22 @@
|
||||
@@ -71,22 +100,22 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView
|
||||
|
||||
onScroll(@{
|
||||
@"contentOffset": @{
|
||||
@@ -155,3 +155,110 @@ index e916023..5049c33 100644
|
||||
},
|
||||
@"zoomScale": @(scrollView.zoomScale ?: 1),
|
||||
});
|
||||
diff --git a/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInput.mm b/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInput.mm
|
||||
index dd50053..2ed7017 100644
|
||||
--- a/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInput.mm
|
||||
+++ b/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInput.mm
|
||||
@@ -122,8 +122,8 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
|
||||
const auto &newTextInputProps = static_cast<const PasteTextInputProps &>(*props);
|
||||
|
||||
// Traits:
|
||||
- if (newTextInputProps.traits.multiline != oldTextInputProps.traits.multiline) {
|
||||
- [self _setMultiline:newTextInputProps.traits.multiline];
|
||||
+ if (newTextInputProps.multiline != oldTextInputProps.multiline) {
|
||||
+ [self _setMultiline:newTextInputProps.multiline];
|
||||
}
|
||||
|
||||
if (newTextInputProps.traits.autocapitalizationType != oldTextInputProps.traits.autocapitalizationType) {
|
||||
@@ -421,7 +421,7 @@ - (void)textInputDidChangeSelection
|
||||
return;
|
||||
}
|
||||
const auto &props = static_cast<const PasteTextInputProps &>(*_props);
|
||||
- if (props.traits.multiline && ![_lastStringStateWasUpdatedWith isEqual:_backedTextInputView.attributedText]) {
|
||||
+ if (props.multiline && ![_lastStringStateWasUpdatedWith isEqual:_backedTextInputView.attributedText]) {
|
||||
[self textInputDidChange];
|
||||
_ignoreNextTextInputCall = YES;
|
||||
}
|
||||
@@ -708,11 +708,11 @@ - (BOOL)_textOf:(NSAttributedString *)newText equals:(NSAttributedString *)oldTe
|
||||
- (SubmitBehavior)getSubmitBehavior
|
||||
{
|
||||
const auto &props = static_cast<const PasteTextInputProps &>(*_props);
|
||||
- const SubmitBehavior submitBehaviorDefaultable = props.traits.submitBehavior;
|
||||
+ const SubmitBehavior submitBehaviorDefaultable = props.submitBehavior;
|
||||
|
||||
// We should always have a non-default `submitBehavior`, but in case we don't, set it based on multiline.
|
||||
if (submitBehaviorDefaultable == SubmitBehavior::Default) {
|
||||
- return props.traits.multiline ? SubmitBehavior::Newline : SubmitBehavior::BlurAndSubmit;
|
||||
+ return props.multiline ? SubmitBehavior::Newline : SubmitBehavior::BlurAndSubmit;
|
||||
}
|
||||
|
||||
return submitBehaviorDefaultable;
|
||||
diff --git a/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/Props.cpp b/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/Props.cpp
|
||||
index 29e094f..7ef519a 100644
|
||||
--- a/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/Props.cpp
|
||||
+++ b/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/Props.cpp
|
||||
@@ -22,8 +22,7 @@ PasteTextInputProps::PasteTextInputProps(
|
||||
const PropsParserContext &context,
|
||||
const PasteTextInputProps &sourceProps,
|
||||
const RawProps& rawProps)
|
||||
- : ViewProps(context, sourceProps, rawProps),
|
||||
- BaseTextProps(context, sourceProps, rawProps),
|
||||
+ : BaseTextInputProps(context, sourceProps, rawProps),
|
||||
traits(convertRawProp(context, rawProps, sourceProps.traits, {})),
|
||||
smartPunctuation(convertRawProp(context, rawProps, "smartPunctuation", sourceProps.smartPunctuation, {})),
|
||||
disableCopyPaste(convertRawProp(context, rawProps, "disableCopyPaste", sourceProps.disableCopyPaste, {false})),
|
||||
@@ -133,7 +132,7 @@ TextAttributes PasteTextInputProps::getEffectiveTextAttributes(Float fontSizeMul
|
||||
ParagraphAttributes PasteTextInputProps::getEffectiveParagraphAttributes() const {
|
||||
auto result = paragraphAttributes;
|
||||
|
||||
- if (!traits.multiline) {
|
||||
+ if (!multiline) {
|
||||
result.maximumNumberOfLines = 1;
|
||||
}
|
||||
|
||||
diff --git a/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/Props.h b/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/Props.h
|
||||
index 723d00c..31cfe66 100644
|
||||
--- a/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/Props.h
|
||||
+++ b/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/Props.h
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <react/renderer/components/iostextinput/conversions.h>
|
||||
#include <react/renderer/components/iostextinput/primitives.h>
|
||||
#include <react/renderer/components/text/BaseTextProps.h>
|
||||
+#include <react/renderer/components/textinput/BaseTextInputProps.h>
|
||||
#include <react/renderer/components/view/ViewProps.h>
|
||||
#include <react/renderer/core/Props.h>
|
||||
#include <react/renderer/core/PropsParserContext.h>
|
||||
@@ -25,7 +26,7 @@
|
||||
|
||||
namespace facebook::react {
|
||||
|
||||
-class PasteTextInputProps final : public ViewProps, public BaseTextProps {
|
||||
+class PasteTextInputProps final : public BaseTextInputProps {
|
||||
public:
|
||||
PasteTextInputProps() = default;
|
||||
PasteTextInputProps(const PropsParserContext& context, const PasteTextInputProps& sourceProps, const RawProps& rawProps);
|
||||
diff --git a/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/ShadowNodes.cpp b/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/ShadowNodes.cpp
|
||||
index 31e07e3..7f0ebfb 100644
|
||||
--- a/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/ShadowNodes.cpp
|
||||
+++ b/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/ShadowNodes.cpp
|
||||
@@ -91,20 +91,11 @@ void PasteTextInputShadowNode::updateStateIfNeeded(
|
||||
const auto& state = getStateData();
|
||||
|
||||
react_native_assert(textLayoutManager_);
|
||||
- react_native_assert(
|
||||
- (!state.layoutManager || state.layoutManager == textLayoutManager_) &&
|
||||
- "`StateData` refers to a different `TextLayoutManager`");
|
||||
-
|
||||
- if (state.reactTreeAttributedString == reactTreeAttributedString &&
|
||||
- state.layoutManager == textLayoutManager_) {
|
||||
- return;
|
||||
- }
|
||||
|
||||
auto newState = TextInputState{};
|
||||
newState.attributedStringBox = AttributedStringBox{reactTreeAttributedString};
|
||||
newState.paragraphAttributes = getConcreteProps().paragraphAttributes;
|
||||
newState.reactTreeAttributedString = reactTreeAttributedString;
|
||||
- newState.layoutManager = textLayoutManager_;
|
||||
newState.mostRecentEventCount = getConcreteProps().mostRecentEventCount;
|
||||
setStateData(std::move(newState));
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
diff --git a/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/MediaHandler.kt b/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/MediaHandler.kt
|
||||
index c863fb8..cde8859 100644
|
||||
--- a/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/MediaHandler.kt
|
||||
+++ b/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/MediaHandler.kt
|
||||
@@ -101,16 +101,30 @@ internal class MediaHandler(
|
||||
val fileData = getAdditionalFileData(sourceUri)
|
||||
val mimeType = getType(context.contentResolver, sourceUri)
|
||||
|
||||
+ // Extract basic metadata
|
||||
+ var width = metadataRetriever.extractInt(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)
|
||||
+ var height = metadataRetriever.extractInt(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)
|
||||
+ val rotation = metadataRetriever.extractInt(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)
|
||||
+
|
||||
+ // Android returns the encoded width/height which do not take the display rotation into
|
||||
+ // account. For videos recorded in portrait mode the encoded dimensions are often landscape
|
||||
+ // (e.g. 1920x1080) paired with a 90°/270° rotation flag. iOS adjusts these values before
|
||||
+ // reporting them, so to keep the behaviour consistent across platforms we swap the width
|
||||
+ // and height when the rotation indicates the video should be displayed in portrait.
|
||||
+ if (rotation % 180 != 0) {
|
||||
+ width = height.also { height = width }
|
||||
+ }
|
||||
+
|
||||
return ImagePickerAsset(
|
||||
type = MediaType.VIDEO,
|
||||
uri = outputUri.toString(),
|
||||
- width = metadataRetriever.extractInt(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH),
|
||||
- height = metadataRetriever.extractInt(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT),
|
||||
+ width = width,
|
||||
+ height = height,
|
||||
fileName = fileData?.fileName,
|
||||
fileSize = fileData?.fileSize,
|
||||
mimeType = mimeType,
|
||||
duration = metadataRetriever.extractInt(MediaMetadataRetriever.METADATA_KEY_DURATION),
|
||||
- rotation = metadataRetriever.extractInt(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION),
|
||||
+ rotation = rotation,
|
||||
assetId = sourceUri.getMediaStoreAssetId()
|
||||
)
|
||||
} catch (cause: FailedToExtractVideoMetadataException) {
|
||||
@@ -0,0 +1,5 @@
|
||||
# Expo Image Picker patch
|
||||
|
||||
Cherry-picked https://github.com/expo/expo/pull/37849
|
||||
|
||||
Remove when we update to a version that includes this commit.
|
||||
@@ -1,3 +1,32 @@
|
||||
diff --git a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.h b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.h
|
||||
index 914a249..0deac55 100644
|
||||
--- a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.h
|
||||
+++ b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.h
|
||||
@@ -19,6 +19,8 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
*/
|
||||
@interface RCTPullToRefreshViewComponentView : RCTViewComponentView <RCTCustomPullToRefreshViewProtocol>
|
||||
|
||||
+- (void)beginRefreshingProgrammatically;
|
||||
+
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
diff --git a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm
|
||||
index d029337..0f63ea3 100644
|
||||
--- a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm
|
||||
+++ b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm
|
||||
@@ -1003,6 +1003,11 @@ - (void)_adjustForMaintainVisibleContentPosition
|
||||
}
|
||||
}
|
||||
|
||||
++ (BOOL)shouldBeRecycled
|
||||
+{
|
||||
+ return NO;
|
||||
+}
|
||||
+
|
||||
@end
|
||||
|
||||
Class<RCTComponentViewProtocol> RCTScrollViewCls(void)
|
||||
diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h
|
||||
index e9b330f..ec5f58c 100644
|
||||
--- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h
|
||||
@@ -15,7 +44,7 @@ diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshCont
|
||||
index 53bfd04..ff1b1ed 100644
|
||||
--- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
|
||||
+++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
|
||||
@@ -23,6 +23,7 @@
|
||||
@@ -23,6 +23,7 @@ @implementation RCTRefreshControl {
|
||||
UIColor *_titleColor;
|
||||
CGFloat _progressViewOffset;
|
||||
BOOL _hasMovedToWindow;
|
||||
@@ -23,7 +52,7 @@ index 53bfd04..ff1b1ed 100644
|
||||
}
|
||||
|
||||
- (instancetype)init
|
||||
@@ -58,6 +59,12 @@ RCT_NOT_IMPLEMENTED(-(instancetype)initWithCoder : (NSCoder *)aDecoder)
|
||||
@@ -58,6 +59,12 @@ - (void)layoutSubviews
|
||||
_isInitialRender = false;
|
||||
}
|
||||
|
||||
@@ -36,7 +65,7 @@ index 53bfd04..ff1b1ed 100644
|
||||
- (void)didMoveToWindow
|
||||
{
|
||||
[super didMoveToWindow];
|
||||
@@ -221,4 +228,50 @@ RCT_NOT_IMPLEMENTED(-(instancetype)initWithCoder : (NSCoder *)aDecoder)
|
||||
@@ -221,4 +228,50 @@ - (void)refreshControlValueChanged
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +120,7 @@ diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshCont
|
||||
index 40aaf9c..1c60164 100644
|
||||
--- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m
|
||||
+++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m
|
||||
@@ -22,11 +22,12 @@ RCT_EXPORT_MODULE()
|
||||
@@ -22,11 +22,12 @@ - (UIView *)view
|
||||
|
||||
RCT_EXPORT_VIEW_PROPERTY(onRefresh, RCTDirectEventBlock)
|
||||
RCT_EXPORT_VIEW_PROPERTY(refreshing, BOOL)
|
||||
@@ -105,15 +134,3 @@ index 40aaf9c..1c60164 100644
|
||||
RCT_EXPORT_METHOD(setNativeRefreshing : (nonnull NSNumber *)viewTag toRefreshing : (BOOL)refreshing)
|
||||
{
|
||||
[self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
|
||||
diff --git a/node_modules/react-native/React/Views/ScrollView/RCTScrollViewManager.m b/node_modules/react-native/React/Views/ScrollView/RCTScrollViewManager.m
|
||||
index cd1e7eb..c1d0172 100644
|
||||
--- a/node_modules/react-native/React/Views/ScrollView/RCTScrollViewManager.m
|
||||
+++ b/node_modules/react-native/React/Views/ScrollView/RCTScrollViewManager.m
|
||||
@@ -83,6 +83,7 @@ RCT_EXPORT_VIEW_PROPERTY(showsVerticalScrollIndicator, BOOL)
|
||||
RCT_EXPORT_VIEW_PROPERTY(scrollEventThrottle, NSTimeInterval)
|
||||
RCT_EXPORT_VIEW_PROPERTY(zoomScale, CGFloat)
|
||||
RCT_EXPORT_VIEW_PROPERTY(contentInset, UIEdgeInsets)
|
||||
+RCT_EXPORT_VIEW_PROPERTY(scrollIndicatorInsets, UIEdgeInsets)
|
||||
RCT_EXPORT_VIEW_PROPERTY(verticalScrollIndicatorInsets, UIEdgeInsets)
|
||||
RCT_EXPORT_VIEW_PROPERTY(scrollToOverflowEnabled, BOOL)
|
||||
RCT_EXPORT_VIEW_PROPERTY(snapToInterval, int)
|
||||
@@ -9,10 +9,9 @@ const templateFile = path.join(
|
||||
'scripts.html',
|
||||
)
|
||||
|
||||
const {entrypoints} = require(path.join(
|
||||
projectRoot,
|
||||
'web-build/asset-manifest.json',
|
||||
))
|
||||
const {entrypoints} = require(
|
||||
path.join(projectRoot, 'web-build/asset-manifest.json'),
|
||||
)
|
||||
|
||||
console.log(`Found ${entrypoints.length} entrypoints`)
|
||||
console.log(`Writing ${templateFile}`)
|
||||
|
||||
@@ -17,6 +17,7 @@ import {useLingui} from '@lingui/react'
|
||||
import * as Sentry from '@sentry/react-native'
|
||||
|
||||
import {KeyboardControllerProvider} from '#/lib/hooks/useEnableKeyboardController'
|
||||
import {Provider as HideBottomBarBorderProvider} from '#/lib/hooks/useHideBottomBarBorder'
|
||||
import {QueryProvider} from '#/lib/react-query'
|
||||
import {Provider as StatsigProvider, tryFetchGates} from '#/lib/statsig/statsig'
|
||||
import {s} from '#/lib/styles'
|
||||
@@ -25,6 +26,7 @@ import I18nProvider from '#/locale/i18nProvider'
|
||||
import {logger} from '#/logger'
|
||||
import {isAndroid, isIOS} from '#/platform/detection'
|
||||
import {Provider as A11yProvider} from '#/state/a11y'
|
||||
import {Provider as AgeAssuranceProvider} from '#/state/ageAssurance'
|
||||
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
|
||||
import {Provider as DialogStateProvider} from '#/state/dialogs'
|
||||
import {listenSessionDropped} from '#/state/events'
|
||||
@@ -33,6 +35,7 @@ import {
|
||||
ensureGeolocationResolved,
|
||||
Provider as GeolocationProvider,
|
||||
} from '#/state/geolocation'
|
||||
import {GlobalGestureEventsProvider} from '#/state/global-gesture-events'
|
||||
import {Provider as HomeBadgeProvider} from '#/state/home-badge'
|
||||
import {Provider as InvitesStateProvider} from '#/state/invites'
|
||||
import {Provider as LightboxStateProvider} from '#/state/lightbox'
|
||||
@@ -58,9 +61,7 @@ import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide'
|
||||
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
|
||||
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
|
||||
import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
|
||||
import {Provider as UnstablePostSourceProvider} from '#/state/unstable-post-source'
|
||||
import {TestCtrls} from '#/view/com/testing/TestCtrls'
|
||||
import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {Shell} from '#/view/shell'
|
||||
import {ThemeProvider as Alf} from '#/alf'
|
||||
@@ -70,6 +71,7 @@ import {NuxDialogs} from '#/components/dialogs/nuxs'
|
||||
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
|
||||
import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs'
|
||||
import {Provider as PortalProvider} from '#/components/Portal'
|
||||
import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
|
||||
import {Splash} from '#/Splash'
|
||||
import {BottomSheetProvider} from '../modules/bottom-sheet'
|
||||
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
||||
@@ -94,7 +96,6 @@ function InnerApp() {
|
||||
const {resumeSession} = useSessionApi()
|
||||
const theme = useColorModeTheme()
|
||||
const {_} = useLingui()
|
||||
|
||||
const hasCheckedReferrer = useStarterPackEntry()
|
||||
|
||||
// init
|
||||
@@ -136,45 +137,49 @@ function InnerApp() {
|
||||
// Resets the entire tree below when it changes:
|
||||
key={currentAccount?.did}>
|
||||
<QueryProvider currentDid={currentAccount?.did}>
|
||||
<ComposerProvider>
|
||||
<StatsigProvider>
|
||||
<MessagesProvider>
|
||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||
<LabelDefsProvider>
|
||||
<ModerationOptsProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<HiddenRepliesProvider>
|
||||
<HomeBadgeProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<ProgressGuideProvider>
|
||||
<ServiceAccountManager>
|
||||
<UnstablePostSourceProvider>
|
||||
<GestureHandlerRootView
|
||||
style={s.h100pct}>
|
||||
<IntentDialogProvider>
|
||||
<TestCtrls />
|
||||
<Shell />
|
||||
<NuxDialogs />
|
||||
</IntentDialogProvider>
|
||||
</GestureHandlerRootView>
|
||||
</UnstablePostSourceProvider>
|
||||
</ServiceAccountManager>
|
||||
</ProgressGuideProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</HomeBadgeProvider>
|
||||
</HiddenRepliesProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</ModerationOptsProvider>
|
||||
</LabelDefsProvider>
|
||||
</MessagesProvider>
|
||||
</StatsigProvider>
|
||||
</ComposerProvider>
|
||||
<StatsigProvider>
|
||||
<AgeAssuranceProvider>
|
||||
<ComposerProvider>
|
||||
<MessagesProvider>
|
||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||
<LabelDefsProvider>
|
||||
<ModerationOptsProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<HiddenRepliesProvider>
|
||||
<HomeBadgeProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<ProgressGuideProvider>
|
||||
<ServiceAccountManager>
|
||||
<HideBottomBarBorderProvider>
|
||||
<GestureHandlerRootView
|
||||
style={s.h100pct}>
|
||||
<GlobalGestureEventsProvider>
|
||||
<IntentDialogProvider>
|
||||
<TestCtrls />
|
||||
<Shell />
|
||||
<NuxDialogs />
|
||||
</IntentDialogProvider>
|
||||
</GlobalGestureEventsProvider>
|
||||
</GestureHandlerRootView>
|
||||
</HideBottomBarBorderProvider>
|
||||
</ServiceAccountManager>
|
||||
</ProgressGuideProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</HomeBadgeProvider>
|
||||
</HiddenRepliesProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</ModerationOptsProvider>
|
||||
</LabelDefsProvider>
|
||||
</MessagesProvider>
|
||||
</ComposerProvider>
|
||||
</AgeAssuranceProvider>
|
||||
</StatsigProvider>
|
||||
</QueryProvider>
|
||||
</React.Fragment>
|
||||
</VideoVolumeProvider>
|
||||
|
||||
@@ -15,6 +15,7 @@ import {ThemeProvider} from '#/lib/ThemeContext'
|
||||
import I18nProvider from '#/locale/i18nProvider'
|
||||
import {logger} from '#/logger'
|
||||
import {Provider as A11yProvider} from '#/state/a11y'
|
||||
import {Provider as AgeAssuranceProvider} from '#/state/ageAssurance'
|
||||
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
|
||||
import {Provider as DialogStateProvider} from '#/state/dialogs'
|
||||
import {listenSessionDropped} from '#/state/events'
|
||||
@@ -48,9 +49,6 @@ import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide'
|
||||
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
|
||||
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
|
||||
import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
|
||||
import {Provider as UnstablePostSourceProvider} from '#/state/unstable-post-source'
|
||||
import {Provider as ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoWebContext'
|
||||
import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {ToastContainer} from '#/view/com/util/Toast.web'
|
||||
import {Shell} from '#/view/shell/index'
|
||||
@@ -61,7 +59,10 @@ import {NuxDialogs} from '#/components/dialogs/nuxs'
|
||||
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
|
||||
import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs'
|
||||
import {Provider as PortalProvider} from '#/components/Portal'
|
||||
import {Provider as ActiveVideoProvider} from '#/components/Post/Embed/VideoEmbed/ActiveVideoWebContext'
|
||||
import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
|
||||
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
||||
import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottomBarBorder'
|
||||
|
||||
/**
|
||||
* Begin geolocation ASAP
|
||||
@@ -116,43 +117,45 @@ function InnerApp() {
|
||||
// Resets the entire tree below when it changes:
|
||||
key={currentAccount?.did}>
|
||||
<QueryProvider currentDid={currentAccount?.did}>
|
||||
<ComposerProvider>
|
||||
<StatsigProvider>
|
||||
<MessagesProvider>
|
||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||
<LabelDefsProvider>
|
||||
<ModerationOptsProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<HiddenRepliesProvider>
|
||||
<HomeBadgeProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<SafeAreaProvider>
|
||||
<ProgressGuideProvider>
|
||||
<ServiceConfigProvider>
|
||||
<UnstablePostSourceProvider>
|
||||
<IntentDialogProvider>
|
||||
<Shell />
|
||||
<NuxDialogs />
|
||||
</IntentDialogProvider>
|
||||
</UnstablePostSourceProvider>
|
||||
</ServiceConfigProvider>
|
||||
</ProgressGuideProvider>
|
||||
</SafeAreaProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</HomeBadgeProvider>
|
||||
</HiddenRepliesProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</ModerationOptsProvider>
|
||||
</LabelDefsProvider>
|
||||
</MessagesProvider>
|
||||
</StatsigProvider>
|
||||
</ComposerProvider>
|
||||
<StatsigProvider>
|
||||
<AgeAssuranceProvider>
|
||||
<ComposerProvider>
|
||||
<MessagesProvider>
|
||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||
<LabelDefsProvider>
|
||||
<ModerationOptsProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<HiddenRepliesProvider>
|
||||
<HomeBadgeProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<SafeAreaProvider>
|
||||
<ProgressGuideProvider>
|
||||
<ServiceConfigProvider>
|
||||
<HideBottomBarBorderProvider>
|
||||
<IntentDialogProvider>
|
||||
<Shell />
|
||||
<NuxDialogs />
|
||||
</IntentDialogProvider>
|
||||
</HideBottomBarBorderProvider>
|
||||
</ServiceConfigProvider>
|
||||
</ProgressGuideProvider>
|
||||
</SafeAreaProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</HomeBadgeProvider>
|
||||
</HiddenRepliesProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</ModerationOptsProvider>
|
||||
</LabelDefsProvider>
|
||||
</MessagesProvider>
|
||||
</ComposerProvider>
|
||||
</AgeAssuranceProvider>
|
||||
</StatsigProvider>
|
||||
</QueryProvider>
|
||||
<ToastContainer />
|
||||
</React.Fragment>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import * as React from 'react'
|
||||
import {useCallback, useRef} from 'react'
|
||||
import {Linking} from 'react-native'
|
||||
import * as Notifications from 'expo-notifications'
|
||||
import {i18n, type MessageDescriptor} from '@lingui/core'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {
|
||||
@@ -10,13 +12,21 @@ import {
|
||||
createNavigationContainerRef,
|
||||
DarkTheme,
|
||||
DefaultTheme,
|
||||
type LinkingOptions,
|
||||
NavigationContainer,
|
||||
StackActions,
|
||||
} from '@react-navigation/native'
|
||||
|
||||
import {timeout} from '#/lib/async/timeout'
|
||||
import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle'
|
||||
import {
|
||||
getNotificationPayload,
|
||||
type NotificationPayload,
|
||||
notificationToURL,
|
||||
storePayloadForAccountSwitch,
|
||||
} from '#/lib/hooks/useNotificationHandler'
|
||||
import {useWebScrollRestoration} from '#/lib/hooks/useWebScrollRestoration'
|
||||
import {logger as notyLogger} from '#/lib/notifications/util'
|
||||
import {buildStateObject} from '#/lib/routes/helpers'
|
||||
import {
|
||||
type AllNavigatorParams,
|
||||
@@ -71,6 +81,7 @@ import {MessagesSettingsScreen} from '#/screens/Messages/Settings'
|
||||
import {ModerationScreen} from '#/screens/Moderation'
|
||||
import {Screen as ModerationVerificationSettings} from '#/screens/Moderation/VerificationSettings'
|
||||
import {Screen as ModerationInteractionSettings} from '#/screens/ModerationInteractionSettings'
|
||||
import {NotificationsActivityListScreen} from '#/screens/Notifications/ActivityList'
|
||||
import {PostLikedByScreen} from '#/screens/Post/PostLikedBy'
|
||||
import {PostQuotesScreen} from '#/screens/Post/PostQuotes'
|
||||
import {PostRepostedByScreen} from '#/screens/Post/PostRepostedBy'
|
||||
@@ -84,17 +95,29 @@ import {SearchScreen} from '#/screens/Search'
|
||||
import {AboutSettingsScreen} from '#/screens/Settings/AboutSettings'
|
||||
import {AccessibilitySettingsScreen} from '#/screens/Settings/AccessibilitySettings'
|
||||
import {AccountSettingsScreen} from '#/screens/Settings/AccountSettings'
|
||||
import {ActivityPrivacySettingsScreen} from '#/screens/Settings/ActivityPrivacySettings'
|
||||
import {AppearanceSettingsScreen} from '#/screens/Settings/AppearanceSettings'
|
||||
import {AppIconSettingsScreen} from '#/screens/Settings/AppIconSettings'
|
||||
import {AppPasswordsScreen} from '#/screens/Settings/AppPasswords'
|
||||
import {ContentAndMediaSettingsScreen} from '#/screens/Settings/ContentAndMediaSettings'
|
||||
import {ExternalMediaPreferencesScreen} from '#/screens/Settings/ExternalMediaPreferences'
|
||||
import {FollowingFeedPreferencesScreen} from '#/screens/Settings/FollowingFeedPreferences'
|
||||
import {InterestsSettingsScreen} from '#/screens/Settings/InterestsSettings'
|
||||
import {LanguageSettingsScreen} from '#/screens/Settings/LanguageSettings'
|
||||
import {LegacyNotificationSettingsScreen} from '#/screens/Settings/LegacyNotificationSettings'
|
||||
import {NotificationSettingsScreen} from '#/screens/Settings/NotificationSettings'
|
||||
import {ActivityNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/ActivityNotificationSettings'
|
||||
import {LikeNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/LikeNotificationSettings'
|
||||
import {LikesOnRepostsNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/LikesOnRepostsNotificationSettings'
|
||||
import {MentionNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/MentionNotificationSettings'
|
||||
import {MiscellaneousNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/MiscellaneousNotificationSettings'
|
||||
import {NewFollowerNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/NewFollowerNotificationSettings'
|
||||
import {QuoteNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/QuoteNotificationSettings'
|
||||
import {ReplyNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/ReplyNotificationSettings'
|
||||
import {RepostNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/RepostNotificationSettings'
|
||||
import {RepostsOnRepostsNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/RepostsOnRepostsNotificationSettings'
|
||||
import {PrivacyAndSecuritySettingsScreen} from '#/screens/Settings/PrivacyAndSecuritySettings'
|
||||
import {SettingsScreen} from '#/screens/Settings/Settings'
|
||||
import {SettingsInterests} from '#/screens/Settings/SettingsInterests'
|
||||
import {ThreadPreferencesScreen} from '#/screens/Settings/ThreadPreferences'
|
||||
import {
|
||||
StarterPackScreen,
|
||||
@@ -110,6 +133,10 @@ import {
|
||||
} from '#/components/dialogs/EmailDialog'
|
||||
import {router} from '#/routes'
|
||||
import {Referrer} from '../modules/expo-bluesky-swiss-army'
|
||||
import {useAccountSwitcher} from './lib/hooks/useAccountSwitcher'
|
||||
import {useNonReactiveCallback} from './lib/hooks/useNonReactiveCallback'
|
||||
import {useLoggedOutViewControls} from './state/shell/logged-out'
|
||||
import {useCloseAllActiveElements} from './state/util'
|
||||
|
||||
const navigationRef = createNavigationContainerRef<AllNavigatorParams>()
|
||||
|
||||
@@ -380,6 +407,99 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) {
|
||||
requireAuth: true,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="ActivityPrivacySettings"
|
||||
getComponent={() => ActivityPrivacySettingsScreen}
|
||||
options={{
|
||||
title: title(msg`Privacy and Security`),
|
||||
requireAuth: true,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="NotificationSettings"
|
||||
getComponent={() => NotificationSettingsScreen}
|
||||
options={{title: title(msg`Notification settings`), requireAuth: true}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="ReplyNotificationSettings"
|
||||
getComponent={() => ReplyNotificationSettingsScreen}
|
||||
options={{
|
||||
title: title(msg`Reply notifications`),
|
||||
requireAuth: true,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="MentionNotificationSettings"
|
||||
getComponent={() => MentionNotificationSettingsScreen}
|
||||
options={{
|
||||
title: title(msg`Mention notifications`),
|
||||
requireAuth: true,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="QuoteNotificationSettings"
|
||||
getComponent={() => QuoteNotificationSettingsScreen}
|
||||
options={{
|
||||
title: title(msg`Quote notifications`),
|
||||
requireAuth: true,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="LikeNotificationSettings"
|
||||
getComponent={() => LikeNotificationSettingsScreen}
|
||||
options={{
|
||||
title: title(msg`Like notifications`),
|
||||
requireAuth: true,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="RepostNotificationSettings"
|
||||
getComponent={() => RepostNotificationSettingsScreen}
|
||||
options={{
|
||||
title: title(msg`Repost notifications`),
|
||||
requireAuth: true,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="NewFollowerNotificationSettings"
|
||||
getComponent={() => NewFollowerNotificationSettingsScreen}
|
||||
options={{
|
||||
title: title(msg`New follower notifications`),
|
||||
requireAuth: true,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="LikesOnRepostsNotificationSettings"
|
||||
getComponent={() => LikesOnRepostsNotificationSettingsScreen}
|
||||
options={{
|
||||
title: title(msg`Likes of your reposts notifications`),
|
||||
requireAuth: true,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="RepostsOnRepostsNotificationSettings"
|
||||
getComponent={() => RepostsOnRepostsNotificationSettingsScreen}
|
||||
options={{
|
||||
title: title(msg`Reposts of your reposts notifications`),
|
||||
requireAuth: true,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="ActivityNotificationSettings"
|
||||
getComponent={() => ActivityNotificationSettingsScreen}
|
||||
options={{
|
||||
title: title(msg`Activity notifications`),
|
||||
requireAuth: true,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="MiscellaneousNotificationSettings"
|
||||
getComponent={() => MiscellaneousNotificationSettingsScreen}
|
||||
options={{
|
||||
title: title(msg`Miscellaneous notifications`),
|
||||
requireAuth: true,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="ContentAndMediaSettings"
|
||||
getComponent={() => ContentAndMediaSettingsScreen}
|
||||
@@ -389,8 +509,8 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) {
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="SettingsInterests"
|
||||
getComponent={() => SettingsInterests}
|
||||
name="InterestsSettings"
|
||||
getComponent={() => InterestsSettingsScreen}
|
||||
options={{
|
||||
title: title(msg`Your interests`),
|
||||
requireAuth: true,
|
||||
@@ -438,8 +558,13 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) {
|
||||
options={{title: title(msg`Chat request inbox`), requireAuth: true}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="NotificationSettings"
|
||||
getComponent={() => NotificationSettingsScreen}
|
||||
name="NotificationsActivityList"
|
||||
getComponent={() => NotificationsActivityListScreen}
|
||||
options={{title: title(msg`Notifications`), requireAuth: true}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="LegacyNotificationSettings"
|
||||
getComponent={() => LegacyNotificationSettingsScreen}
|
||||
options={{title: title(msg`Notification settings`), requireAuth: true}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
@@ -484,7 +609,7 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) {
|
||||
* in 3 distinct tab-stacks with a different root screen on each.
|
||||
*/
|
||||
function TabsNavigator() {
|
||||
const tabBar = React.useCallback(
|
||||
const tabBar = useCallback(
|
||||
(props: JSX.IntrinsicAttributes & BottomTabBarProps) => (
|
||||
<BottomBar {...props} />
|
||||
),
|
||||
@@ -651,6 +776,7 @@ const FlatNavigator = () => {
|
||||
|
||||
const LINKING = {
|
||||
// TODO figure out what we are going to use
|
||||
// note: `bluesky://` is what is used in app.config.js
|
||||
prefixes: ['bsky://', 'bluesky://', 'https://bsky.app'],
|
||||
|
||||
getPathFromState(state: State) {
|
||||
@@ -707,13 +833,107 @@ const LINKING = {
|
||||
return res
|
||||
}
|
||||
},
|
||||
}
|
||||
} satisfies LinkingOptions<AllNavigatorParams>
|
||||
|
||||
/**
|
||||
* Used to ensure we don't handle the same notification twice
|
||||
*/
|
||||
let lastHandledNotificationDateDedupe: number | undefined
|
||||
|
||||
function RoutesContainer({children}: React.PropsWithChildren<{}>) {
|
||||
const theme = useColorSchemeStyle(DefaultTheme, DarkTheme)
|
||||
const {currentAccount} = useSession()
|
||||
const prevLoggedRouteName = React.useRef<string | undefined>(undefined)
|
||||
const {currentAccount, accounts} = useSession()
|
||||
const {onPressSwitchAccount} = useAccountSwitcher()
|
||||
const {setShowLoggedOut} = useLoggedOutViewControls()
|
||||
const prevLoggedRouteName = useRef<string | undefined>(undefined)
|
||||
const emailDialogControl = useEmailDialogControl()
|
||||
const closeAllActiveElements = useCloseAllActiveElements()
|
||||
|
||||
/**
|
||||
* Handle navigation to a conversation, or prepares for account switch.
|
||||
*
|
||||
* Non-reactive because we need the latest data from some hooks
|
||||
* after an async call - sfn
|
||||
*/
|
||||
const handleChatMessage = useNonReactiveCallback(
|
||||
(payload: Extract<NotificationPayload, {reason: 'chat-message'}>) => {
|
||||
notyLogger.debug(`handleChatMessage`, {payload})
|
||||
|
||||
if (payload.recipientDid !== currentAccount?.did) {
|
||||
// handled in useNotificationHandler after account switch finishes
|
||||
storePayloadForAccountSwitch(payload)
|
||||
closeAllActiveElements()
|
||||
|
||||
const account = accounts.find(a => a.did === payload.recipientDid)
|
||||
if (account) {
|
||||
onPressSwitchAccount(account, 'Notification')
|
||||
} else {
|
||||
setShowLoggedOut(true)
|
||||
}
|
||||
} else {
|
||||
// @ts-expect-error nested navigators aren't typed -sfn
|
||||
navigate('MessagesTab', {
|
||||
screen: 'Messages',
|
||||
params: {
|
||||
pushToConversation: payload.convoId,
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
async function handlePushNotificationEntry() {
|
||||
if (!isNative) return
|
||||
|
||||
// deep links take precedence - on android,
|
||||
// getLastNotificationResponseAsync returns a "notification"
|
||||
// that is actually a deep link. avoid handling it twice -sfn
|
||||
if (await Linking.getInitialURL()) {
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
* The notification that caused the app to open, if applicable
|
||||
*/
|
||||
const response = await Notifications.getLastNotificationResponseAsync()
|
||||
|
||||
if (response) {
|
||||
notyLogger.debug(`handlePushNotificationEntry: response`, {response})
|
||||
|
||||
if (response.notification.date === lastHandledNotificationDateDedupe)
|
||||
return
|
||||
lastHandledNotificationDateDedupe = response.notification.date
|
||||
|
||||
const payload = getNotificationPayload(response.notification)
|
||||
|
||||
if (payload) {
|
||||
notyLogger.metric(
|
||||
'notifications:openApp',
|
||||
{reason: payload.reason, causedBoot: true},
|
||||
{statsig: false},
|
||||
)
|
||||
|
||||
if (payload.reason === 'chat-message') {
|
||||
handleChatMessage(payload)
|
||||
} else {
|
||||
const path = notificationToURL(payload)
|
||||
|
||||
if (path === '/notifications') {
|
||||
resetToTab('NotificationsTab')
|
||||
notyLogger.debug(`handlePushNotificationEntry: default navigate`)
|
||||
} else if (path) {
|
||||
const [screen, params] = router.matchPath(path)
|
||||
// @ts-expect-error nested navigators aren't typed -sfn
|
||||
navigate('HomeTab', {screen, params})
|
||||
notyLogger.debug(`handlePushNotificationEntry: navigate`, {
|
||||
screen,
|
||||
params,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onReady() {
|
||||
prevLoggedRouteName.current = getCurrentRouteName()
|
||||
@@ -734,9 +954,7 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
|
||||
onStateChange={() => {
|
||||
logger.metric(
|
||||
'router:navigate',
|
||||
{
|
||||
from: prevLoggedRouteName.current,
|
||||
},
|
||||
{from: prevLoggedRouteName.current},
|
||||
{statsig: false},
|
||||
)
|
||||
prevLoggedRouteName.current = getCurrentRouteName()
|
||||
@@ -746,6 +964,7 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
|
||||
logModuleInitTime()
|
||||
onReady()
|
||||
logger.metric('router:navigate', {}, {statsig: false})
|
||||
handlePushNotificationEntry()
|
||||
}}
|
||||
// WARNING: Implicit navigation to nested navigators is depreciated in React Navigation 7.x
|
||||
// However, there's a fair amount of places we do that, especially in when popping to the top of stacks.
|
||||
@@ -795,7 +1014,9 @@ function navigate<K extends keyof AllNavigatorParams>(
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
function resetToTab(tabName: 'HomeTab' | 'SearchTab' | 'NotificationsTab') {
|
||||
function resetToTab(
|
||||
tabName: 'HomeTab' | 'SearchTab' | 'MessagesTab' | 'NotificationsTab',
|
||||
) {
|
||||
if (navigationRef.isReady()) {
|
||||
navigate(tabName)
|
||||
if (navigationRef.canGoBack()) {
|
||||
@@ -828,39 +1049,6 @@ function reset(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
function handleLink(url: string) {
|
||||
let path
|
||||
if (url.startsWith('/')) {
|
||||
path = url
|
||||
} else if (url.startsWith('http')) {
|
||||
try {
|
||||
path = new URL(url).pathname
|
||||
} catch (e) {
|
||||
console.error('Invalid url', url, e)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
console.error('Invalid url', url)
|
||||
return
|
||||
}
|
||||
|
||||
const [name, params] = router.matchPath(path)
|
||||
if (isNative) {
|
||||
if (name === 'Search') {
|
||||
resetToTab('SearchTab')
|
||||
} else if (name === 'Notifications') {
|
||||
resetToTab('NotificationsTab')
|
||||
} else {
|
||||
resetToTab('HomeTab')
|
||||
// @ts-ignore matchPath doesnt give us type-checked output -prf
|
||||
navigate(name, params)
|
||||
}
|
||||
} else {
|
||||
// @ts-ignore matchPath doesnt give us type-checked output -prf
|
||||
navigate(name, params)
|
||||
}
|
||||
}
|
||||
|
||||
let didInit = false
|
||||
function logModuleInitTime() {
|
||||
if (didInit) {
|
||||
@@ -901,7 +1089,6 @@ function logModuleInitTime() {
|
||||
|
||||
export {
|
||||
FlatNavigator,
|
||||
handleLink,
|
||||
navigate,
|
||||
reset,
|
||||
resetToTab,
|
||||
|
||||
@@ -67,6 +67,9 @@ export const atoms = {
|
||||
zIndex: 50,
|
||||
},
|
||||
|
||||
overflow_visible: {
|
||||
overflow: 'visible',
|
||||
},
|
||||
overflow_hidden: {
|
||||
overflow: 'hidden',
|
||||
},
|
||||
@@ -983,6 +986,10 @@ export const atoms = {
|
||||
transition_none: web({
|
||||
transitionProperty: 'none',
|
||||
}),
|
||||
transition_timing_default: web({
|
||||
transitionTimingFunction: 'cubic-bezier(0.17, 0.73, 0.14, 1)',
|
||||
transitionDuration: '100ms',
|
||||
}),
|
||||
transition_all: web({
|
||||
transitionProperty: 'all',
|
||||
transitionTimingFunction: 'cubic-bezier(0.17, 0.73, 0.14, 1)',
|
||||
@@ -1051,4 +1058,8 @@ export const atoms = {
|
||||
transform: [],
|
||||
},
|
||||
}) as {transform: Exclude<ViewStyle['transform'], string | undefined>},
|
||||
|
||||
pointer: web({
|
||||
cursor: 'pointer',
|
||||
}),
|
||||
} as const
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import {jest} from '@jest/globals'
|
||||
|
||||
import {logger} from '#/logger'
|
||||
import {transparentifyColor} from '../colorGeneration'
|
||||
|
||||
jest.mock('#/logger', () => ({
|
||||
logger: {warn: jest.fn()},
|
||||
}))
|
||||
|
||||
describe('transparentifyColor', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('converts hsl() to hsla()', () => {
|
||||
const result = transparentifyColor('hsl(120 100% 50%)', 0.5)
|
||||
expect(result).toBe('hsla(120 100% 50%, 0.5)')
|
||||
})
|
||||
|
||||
it('converts hsl() to hsla() - fully transparent', () => {
|
||||
const result = transparentifyColor('hsl(120 100% 50%)', 0)
|
||||
expect(result).toBe('hsla(120 100% 50%, 0)')
|
||||
})
|
||||
|
||||
it('converts rgb() to rgba()', () => {
|
||||
const result = transparentifyColor('rgb(255 0 0)', 0.75)
|
||||
expect(result).toBe('rgba(255 0 0, 0.75)')
|
||||
})
|
||||
|
||||
it('expands 3-digit hex and appends alpha channel', () => {
|
||||
const result = transparentifyColor('#abc', 0.4)
|
||||
expect(result).toBe('#aabbcc66')
|
||||
})
|
||||
|
||||
it('appends alpha to 6-digit hex', () => {
|
||||
const result = transparentifyColor('#aabbcc', 0.4)
|
||||
expect(result).toBe('#aabbcc66')
|
||||
})
|
||||
|
||||
it('returns the original string and warns for unsupported formats', () => {
|
||||
const unsupported = 'blue'
|
||||
const result = transparentifyColor(unsupported, 0.5)
|
||||
expect(result).toBe(unsupported)
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
`Could not make '${unsupported}' transparent`,
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,3 +1,5 @@
|
||||
import {logger} from '#/logger'
|
||||
|
||||
export const BLUE_HUE = 211
|
||||
export const RED_HUE = 346
|
||||
export const GREEN_HUE = 152
|
||||
@@ -19,3 +21,29 @@ export function generateScale(start: number, end: number) {
|
||||
export const defaultScale = generateScale(6, 100)
|
||||
// dim shifted 6% lighter
|
||||
export const dimScale = generateScale(12, 100)
|
||||
|
||||
export function transparentifyColor(color: string, alpha: number) {
|
||||
if (color.startsWith('hsl(')) {
|
||||
return 'hsla(' + color.slice('hsl('.length, -1) + `, ${alpha})`
|
||||
} else if (color.startsWith('rgb(')) {
|
||||
return 'rgba(' + color.slice('rgb('.length, -1) + `, ${alpha})`
|
||||
} else if (color.startsWith('#')) {
|
||||
if (color.length === 7) {
|
||||
const alphaHex = Math.round(alpha * 255).toString(16)
|
||||
// Per MDN: If there is only one number, it is duplicated: e means ee
|
||||
// https://developer.mozilla.org/en-US/docs/Web/CSS/hex-color
|
||||
return color.slice(0, 7) + alphaHex.padStart(2, alphaHex)
|
||||
} else if (color.length === 4) {
|
||||
// convert to 6-digit hex before adding alpha
|
||||
const [r, g, b] = color.slice(1).split('')
|
||||
const alphaHex = Math.round(alpha * 255).toString(16)
|
||||
return `#${r.repeat(2)}${g.repeat(2)}${b.repeat(2)}${alphaHex.padStart(
|
||||
2,
|
||||
alphaHex,
|
||||
)}`
|
||||
}
|
||||
} else {
|
||||
logger.warn(`Could not make '${color}' transparent`)
|
||||
}
|
||||
return color
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as SystemUI from 'expo-system-ui'
|
||||
|
||||
import {isAndroid} from '#/platform/detection'
|
||||
import {Theme} from '../types'
|
||||
import {type Theme} from '../types'
|
||||
|
||||
export function setSystemUITheme(themeType: 'theme' | 'lightbox', t: Theme) {
|
||||
if (isAndroid) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import {Pressable} from 'react-native'
|
||||
import Animated, {
|
||||
Extrapolation,
|
||||
interpolate,
|
||||
SharedValue,
|
||||
type SharedValue,
|
||||
useAnimatedStyle,
|
||||
} from 'react-native-reanimated'
|
||||
import {msg} from '@lingui/macro'
|
||||
|
||||
@@ -190,7 +190,7 @@ export function Root({children}: {children: React.ReactNode}) {
|
||||
if (item) playHaptic('Light')
|
||||
setHoveredMenuItem(item)
|
||||
},
|
||||
} satisfies ContextType),
|
||||
}) satisfies ContextType,
|
||||
[
|
||||
measurement,
|
||||
setMeasurement,
|
||||
@@ -710,8 +710,8 @@ export function Item({
|
||||
const xOffset = position
|
||||
? position.x
|
||||
: align === 'left'
|
||||
? measurement.x
|
||||
: measurement.x + measurement.width - layout.width
|
||||
? measurement.x
|
||||
: measurement.x + measurement.width - layout.width
|
||||
|
||||
registerHoverable(
|
||||
id,
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
import {View} from 'react-native'
|
||||
|
||||
import {atoms as a, flatten, useTheme, ViewStyleProp} from '#/alf'
|
||||
import {atoms as a, useTheme, type ViewStyleProp} from '#/alf'
|
||||
|
||||
export function Divider({style}: ViewStyleProp) {
|
||||
const t = useTheme()
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
a.border_t,
|
||||
t.atoms.border_contrast_low,
|
||||
flatten(style),
|
||||
]}
|
||||
/>
|
||||
<View style={[a.w_full, a.border_t, t.atoms.border_contrast_low, style]} />
|
||||
)
|
||||
}
|
||||
|
||||
@@ -214,7 +214,7 @@ export function DescriptionPlaceholder() {
|
||||
export function Likes({count}: {count: number}) {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
|
||||
<Text style={[a.text_sm, t.atoms.text_contrast_medium, a.font_bold]}>
|
||||
<Trans>
|
||||
Liked by <Plural value={count || 0} one="# user" other="# users" />
|
||||
</Trans>
|
||||
|
||||
@@ -1,24 +1,30 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {ScrollView} from 'react-native-gesture-handler'
|
||||
import {AppBskyFeedDefs, AtUri} from '@atproto/api'
|
||||
import {type AppBskyFeedDefs, AtUri} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {NavigationProp} from '#/lib/routes/types'
|
||||
import {type NavigationProp} from '#/lib/routes/types'
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {logger} from '#/logger'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useGetPopularFeedsQuery} from '#/state/queries/feed'
|
||||
import {FeedDescriptor} from '#/state/queries/post-feed'
|
||||
import {type FeedDescriptor} from '#/state/queries/post-feed'
|
||||
import {useProfilesQuery} from '#/state/queries/profile'
|
||||
import {useSuggestedFollowsByActorQuery} from '#/state/queries/suggested-follows'
|
||||
import {useSession} from '#/state/session'
|
||||
import * as userActionHistory from '#/state/userActionHistory'
|
||||
import {SeenPost} from '#/state/userActionHistory'
|
||||
import {type SeenPost} from '#/state/userActionHistory'
|
||||
import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
|
||||
import {atoms as a, useBreakpoints, useTheme, ViewStyleProp, web} from '#/alf'
|
||||
import {
|
||||
atoms as a,
|
||||
useBreakpoints,
|
||||
useTheme,
|
||||
type ViewStyleProp,
|
||||
web,
|
||||
} from '#/alf'
|
||||
import {Button} from '#/components/Button'
|
||||
import * as FeedCard from '#/components/FeedCard'
|
||||
import {ArrowRight_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow'
|
||||
@@ -27,7 +33,7 @@ import {PersonPlus_Stroke2_Corner0_Rounded as Person} from '#/components/icons/P
|
||||
import {InlineLinkText} from '#/components/Link'
|
||||
import * as ProfileCard from '#/components/ProfileCard'
|
||||
import {Text} from '#/components/Typography'
|
||||
import * as bsky from '#/types/bsky'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
import {ProgressGuideList} from './ProgressGuide/List'
|
||||
|
||||
const MOBILE_CARD_WIDTH = 300
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
|
||||
import {
|
||||
type AppBskyActorDefs,
|
||||
moderateProfile,
|
||||
type ModerationOpts,
|
||||
} from '@atproto/api'
|
||||
import {msg, Plural, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
@@ -8,9 +12,9 @@ import {makeProfileLink} from '#/lib/routes/links'
|
||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Link, LinkProps} from '#/components/Link'
|
||||
import {Link, type LinkProps} from '#/components/Link'
|
||||
import {Text} from '#/components/Typography'
|
||||
import * as bsky from '#/types/bsky'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
|
||||
const AVI_SIZE = 30
|
||||
const AVI_SIZE_SMALL = 20
|
||||
@@ -137,9 +141,9 @@ function KnownFollowersInner({
|
||||
<>
|
||||
<View
|
||||
style={[
|
||||
a.flex_row,
|
||||
{
|
||||
height: SIZE,
|
||||
width: SIZE + (slice.length - 1) * a.gap_md.gap,
|
||||
},
|
||||
pressed && {
|
||||
opacity: 0.5,
|
||||
@@ -149,15 +153,14 @@ function KnownFollowersInner({
|
||||
<View
|
||||
key={prof.did}
|
||||
style={[
|
||||
a.absolute,
|
||||
a.rounded_full,
|
||||
{
|
||||
borderWidth: AVI_BORDER,
|
||||
borderColor: t.atoms.bg.backgroundColor,
|
||||
width: SIZE + AVI_BORDER * 2,
|
||||
height: SIZE + AVI_BORDER * 2,
|
||||
left: i * a.gap_md.gap,
|
||||
zIndex: AVI_BORDER - i,
|
||||
marginLeft: i > 0 ? -8 : 0,
|
||||
},
|
||||
]}>
|
||||
<UserAvatar
|
||||
@@ -165,6 +168,7 @@ function KnownFollowersInner({
|
||||
avatar={prof.avatar}
|
||||
moderation={moderation.ui('avatar')}
|
||||
type={prof.associated?.labeler ? 'labeler' : 'user'}
|
||||
noBorder
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import React from 'react'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {sanitizeAppLanguageSetting} from '#/locale/helpers'
|
||||
import {APP_LANGUAGES} from '#/locale/languages'
|
||||
import * as Select from '#/components/Select'
|
||||
|
||||
export function LanguageSelect({
|
||||
value,
|
||||
onChange,
|
||||
items = APP_LANGUAGES.map(l => ({
|
||||
label: l.name,
|
||||
value: l.code2,
|
||||
})),
|
||||
}: {
|
||||
value?: string
|
||||
onChange: (value: string) => void
|
||||
items?: {label: string; value: string}[]
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
|
||||
const handleOnChange = React.useCallback(
|
||||
(value: string) => {
|
||||
if (!value) return
|
||||
onChange(sanitizeAppLanguageSetting(value))
|
||||
},
|
||||
[onChange],
|
||||
)
|
||||
|
||||
return (
|
||||
<Select.Root
|
||||
value={value ? sanitizeAppLanguageSetting(value) : undefined}
|
||||
onValueChange={handleOnChange}>
|
||||
<Select.Trigger label={_(msg`Select language`)}>
|
||||
<Select.ValueText placeholder={_(msg`Select language`)} />
|
||||
<Select.Icon />
|
||||
</Select.Trigger>
|
||||
<Select.Content
|
||||
renderItem={({label, value}) => (
|
||||
<Select.Item value={value} label={label}>
|
||||
<Select.ItemIndicator />
|
||||
<Select.ItemText>{label}</Select.ItemText>
|
||||
</Select.Item>
|
||||
)}
|
||||
items={items}
|
||||
/>
|
||||
</Select.Root>
|
||||
)
|
||||
}
|
||||
@@ -1,24 +1,24 @@
|
||||
import {createContext, useCallback, useContext} from 'react'
|
||||
import {GestureResponderEvent, Keyboard, View} from 'react-native'
|
||||
import {type GestureResponderEvent, Keyboard, View} from 'react-native'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {HITSLOP_30} from '#/lib/constants'
|
||||
import {NavigationProp} from '#/lib/routes/types'
|
||||
import {type NavigationProp} from '#/lib/routes/types'
|
||||
import {isIOS} from '#/platform/detection'
|
||||
import {useSetDrawerOpen} from '#/state/shell'
|
||||
import {
|
||||
atoms as a,
|
||||
platform,
|
||||
TextStyleProp,
|
||||
type TextStyleProp,
|
||||
useBreakpoints,
|
||||
useGutters,
|
||||
useLayoutBreakpoints,
|
||||
useTheme,
|
||||
web,
|
||||
} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonProps} from '#/components/Button'
|
||||
import {Button, ButtonIcon, type ButtonProps} from '#/components/Button'
|
||||
import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeft} from '#/components/icons/Arrow'
|
||||
import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
|
||||
import {
|
||||
|
||||
@@ -4,13 +4,12 @@ import {sanitizeUrl} from '@braintree/sanitize-url'
|
||||
import {
|
||||
type LinkProps as RNLinkProps,
|
||||
StackActions,
|
||||
useLinkBuilder,
|
||||
} from '@react-navigation/native'
|
||||
|
||||
import {BSKY_DOWNLOAD_URL} from '#/lib/constants'
|
||||
import {useNavigationDeduped} from '#/lib/hooks/useNavigationDeduped'
|
||||
import {useOpenLink} from '#/lib/hooks/useOpenLink'
|
||||
import {type AllNavigatorParams} from '#/lib/routes/types'
|
||||
import {type AllNavigatorParams, type RouteParams} from '#/lib/routes/types'
|
||||
import {shareUrl} from '#/lib/sharing'
|
||||
import {
|
||||
convertBskyAppUrlIfNeeded,
|
||||
@@ -25,6 +24,7 @@ import {Button, type ButtonProps} from '#/components/Button'
|
||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||
import {Text, type TextProps} from '#/components/Typography'
|
||||
import {router} from '#/routes'
|
||||
import {useGlobalDialogsControlContext} from './dialogs/Context'
|
||||
|
||||
/**
|
||||
* Only available within a `Link`, since that inherits from `Button`.
|
||||
@@ -95,25 +95,25 @@ export function useLink({
|
||||
shouldProxy?: boolean
|
||||
}) {
|
||||
const navigation = useNavigationDeduped()
|
||||
const {buildHref} = useLinkBuilder()
|
||||
const href = useMemo(() => {
|
||||
return typeof to === 'string'
|
||||
? convertBskyAppUrlIfNeeded(sanitizeUrl(to))
|
||||
: to.screen
|
||||
? buildHref(to.screen, to.params)
|
||||
: to.href
|
||||
? convertBskyAppUrlIfNeeded(sanitizeUrl(to.href))
|
||||
: undefined
|
||||
}, [to, buildHref])
|
||||
? router.matchName(to.screen)?.build(to.params)
|
||||
: to.href
|
||||
? convertBskyAppUrlIfNeeded(sanitizeUrl(to.href))
|
||||
: undefined
|
||||
}, [to])
|
||||
|
||||
if (!href) {
|
||||
throw new Error(
|
||||
'Link `to` prop must be a string or an object with `screen` and `params` properties',
|
||||
'Could not resolve screen. Link `to` prop must be a string or an object with `screen` and `params` properties',
|
||||
)
|
||||
}
|
||||
|
||||
const isExternal = isExternalUrl(href)
|
||||
const {openModal, closeModal} = useModalControls()
|
||||
const {closeModal} = useModalControls()
|
||||
const {linkWarningDialogControl} = useGlobalDialogsControlContext()
|
||||
const openLink = useOpenLink()
|
||||
|
||||
const onPress = React.useCallback(
|
||||
@@ -134,10 +134,9 @@ export function useLink({
|
||||
}
|
||||
|
||||
if (requiresWarning) {
|
||||
openModal({
|
||||
name: 'link-warning',
|
||||
text: displayText,
|
||||
href: href,
|
||||
linkWarningDialogControl.open({
|
||||
displayText,
|
||||
href,
|
||||
})
|
||||
} else {
|
||||
if (isExternal) {
|
||||
@@ -156,15 +155,44 @@ export function useLink({
|
||||
} else {
|
||||
closeModal() // close any active modals
|
||||
|
||||
const [screen, params] = router.matchPath(href) as [
|
||||
screen: keyof AllNavigatorParams,
|
||||
params?: RouteParams,
|
||||
]
|
||||
|
||||
// does not apply to web's flat navigator
|
||||
if (isNative && screen !== 'NotFound') {
|
||||
const state = navigation.getState()
|
||||
// if screen is not in the current navigator, it means it's
|
||||
// most likely a tab screen
|
||||
if (!state.routeNames.includes(screen)) {
|
||||
const parent = navigation.getParent()
|
||||
if (
|
||||
parent &&
|
||||
parent.getState().routeNames.includes(`${screen}Tab`)
|
||||
) {
|
||||
// yep, it's a tab screen. i.e. SearchTab
|
||||
// thus we need to navigate to the child screen
|
||||
// via the parent navigator
|
||||
// see https://reactnavigation.org/docs/upgrading-from-6.x/#changes-to-the-navigate-action
|
||||
// TODO: can we support the other kinds of actions? push/replace -sfn
|
||||
|
||||
// @ts-expect-error include does not narrow the type unfortunately
|
||||
parent.navigate(`${screen}Tab`, {screen, params})
|
||||
return
|
||||
} else {
|
||||
// will probably fail, but let's try anyway
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (action === 'push') {
|
||||
navigation.dispatch(StackActions.push(...router.matchPath(href)))
|
||||
navigation.dispatch(StackActions.push(screen, params))
|
||||
} else if (action === 'replace') {
|
||||
navigation.dispatch(
|
||||
StackActions.replace(...router.matchPath(href)),
|
||||
)
|
||||
navigation.dispatch(StackActions.replace(screen, params))
|
||||
} else if (action === 'navigate') {
|
||||
// @ts-ignore
|
||||
navigation.navigate(...router.matchPath(href))
|
||||
// @ts-expect-error not typed
|
||||
navigation.navigate(screen, params, {pop: true})
|
||||
} else {
|
||||
throw Error('Unsupported navigator action.')
|
||||
}
|
||||
@@ -178,13 +206,13 @@ export function useLink({
|
||||
displayText,
|
||||
isExternal,
|
||||
href,
|
||||
openModal,
|
||||
openLink,
|
||||
closeModal,
|
||||
action,
|
||||
navigation,
|
||||
overridePresentation,
|
||||
shouldProxy,
|
||||
linkWarningDialogControl,
|
||||
],
|
||||
)
|
||||
|
||||
@@ -197,16 +225,21 @@ export function useLink({
|
||||
)
|
||||
|
||||
if (requiresWarning) {
|
||||
openModal({
|
||||
name: 'link-warning',
|
||||
text: displayText,
|
||||
href: href,
|
||||
linkWarningDialogControl.open({
|
||||
displayText,
|
||||
href,
|
||||
share: true,
|
||||
})
|
||||
} else {
|
||||
shareUrl(href)
|
||||
}
|
||||
}, [disableMismatchWarning, displayText, href, isExternal, openModal])
|
||||
}, [
|
||||
disableMismatchWarning,
|
||||
displayText,
|
||||
href,
|
||||
isExternal,
|
||||
linkWarningDialogControl,
|
||||
])
|
||||
|
||||
const onLongPress = React.useCallback(
|
||||
(e: GestureResponderEvent) => {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {
|
||||
AppBskyGraphDefs,
|
||||
type AppBskyGraphDefs,
|
||||
AtUri,
|
||||
moderateUserList,
|
||||
ModerationUI,
|
||||
type ModerationUI,
|
||||
} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
@@ -22,10 +22,10 @@ import {
|
||||
Outer,
|
||||
SaveButton,
|
||||
} from '#/components/FeedCard'
|
||||
import {Link as InternalLink, LinkProps} from '#/components/Link'
|
||||
import {Link as InternalLink, type LinkProps} from '#/components/Link'
|
||||
import * as Hider from '#/components/moderation/Hider'
|
||||
import {Text} from '#/components/Typography'
|
||||
import * as bsky from '#/types/bsky'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
|
||||
/*
|
||||
* This component is based on `FeedCard` and is tightly coupled with that
|
||||
@@ -50,7 +50,9 @@ type Props = {
|
||||
showPinButton?: boolean
|
||||
}
|
||||
|
||||
export function Default(props: Props) {
|
||||
export function Default(
|
||||
props: Props & Omit<LinkProps, 'to' | 'label' | 'children'>,
|
||||
) {
|
||||
const {view, showPinButton} = props
|
||||
const moderationOpts = useModerationOpts()
|
||||
const moderation = moderationOpts
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
|
||||
import type {ContextType, ItemContextType} from '#/components/Menu/types'
|
||||
import {type ContextType, type ItemContextType} from '#/components/Menu/types'
|
||||
|
||||
export const Context = React.createContext<ContextType | null>(null)
|
||||
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import React from 'react'
|
||||
import {ActivityIndicator, GestureResponderEvent, Pressable} from 'react-native'
|
||||
import {
|
||||
ActivityIndicator,
|
||||
type GestureResponderEvent,
|
||||
Pressable,
|
||||
} from 'react-native'
|
||||
import {Image} from 'expo-image'
|
||||
import {AppBskyEmbedExternal} from '@atproto/api'
|
||||
import {type AppBskyEmbedExternal} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {EmbedPlayerParams} from '#/lib/strings/embed-player'
|
||||
import {type EmbedPlayerParams} from '#/lib/strings/embed-player'
|
||||
import {isIOS, isNative, isWeb} from '#/platform/detection'
|
||||
import {useExternalEmbedsPrefs} from '#/state/preferences'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
@@ -14,7 +18,7 @@ import {EmbedConsentDialog} from '#/components/dialogs/EmbedConsent'
|
||||
import {Fill} from '#/components/Fill'
|
||||
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
|
||||
|
||||
export function ExternalGifEmbed({
|
||||
export function ExternalGif({
|
||||
link,
|
||||
params,
|
||||
}: {
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
ActivityIndicator,
|
||||
GestureResponderEvent,
|
||||
type GestureResponderEvent,
|
||||
Pressable,
|
||||
StyleSheet,
|
||||
useWindowDimensions,
|
||||
@@ -16,21 +16,24 @@ import Animated, {
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {WebView} from 'react-native-webview'
|
||||
import {Image} from 'expo-image'
|
||||
import {AppBskyEmbedExternal} from '@atproto/api'
|
||||
import {type AppBskyEmbedExternal} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {NavigationProp} from '#/lib/routes/types'
|
||||
import {EmbedPlayerParams, getPlayerAspect} from '#/lib/strings/embed-player'
|
||||
import {type NavigationProp} from '#/lib/routes/types'
|
||||
import {
|
||||
type EmbedPlayerParams,
|
||||
getPlayerAspect,
|
||||
} from '#/lib/strings/embed-player'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {useExternalEmbedsPrefs} from '#/state/preferences'
|
||||
import {EventStopper} from '#/view/com/util/EventStopper'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {useDialogControl} from '#/components/Dialog'
|
||||
import {EmbedConsentDialog} from '#/components/dialogs/EmbedConsent'
|
||||
import {Fill} from '#/components/Fill'
|
||||
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
|
||||
import {EventStopper} from '../EventStopper'
|
||||
|
||||
interface ShouldStartLoadRequest {
|
||||
url: string
|
||||
@@ -1,17 +1,17 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
Pressable,
|
||||
StyleProp,
|
||||
type StyleProp,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
ViewStyle,
|
||||
type ViewStyle,
|
||||
} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {HITSLOP_20} from '#/lib/constants'
|
||||
import {EmbedPlayerParams} from '#/lib/strings/embed-player'
|
||||
import {type EmbedPlayerParams} from '#/lib/strings/embed-player'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useAutoplayDisabled} from '#/state/preferences'
|
||||
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
|
||||
@@ -22,7 +22,7 @@ import * as Prompt from '#/components/Prompt'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
|
||||
import {GifView} from '../../../../../modules/expo-bluesky-gif-view'
|
||||
import {GifViewStateChangeEvent} from '../../../../../modules/expo-bluesky-gif-view/src/GifView.types'
|
||||
import {type GifViewStateChangeEvent} from '../../../../../modules/expo-bluesky-gif-view/src/GifView.types'
|
||||
|
||||
function PlaybackControls({
|
||||
onPress,
|
||||
@@ -12,16 +12,16 @@ import {parseEmbedPlayerFromUrl} from '#/lib/strings/embed-player'
|
||||
import {toNiceDomain} from '#/lib/strings/url-helpers'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {useExternalEmbedsPrefs} from '#/state/preferences'
|
||||
import {ExternalGifEmbed} from '#/view/com/util/post-embeds/ExternalGifEmbed'
|
||||
import {ExternalPlayer} from '#/view/com/util/post-embeds/ExternalPlayerEmbed'
|
||||
import {GifEmbed} from '#/view/com/util/post-embeds/GifEmbed'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Divider} from '#/components/Divider'
|
||||
import {Earth_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
|
||||
import {Link} from '#/components/Link'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {ExternalGif} from './ExternalGif'
|
||||
import {ExternalPlayer} from './ExternalPlayer'
|
||||
import {GifEmbed} from './Gif'
|
||||
|
||||
export const ExternalLinkEmbed = ({
|
||||
export const ExternalEmbed = ({
|
||||
link,
|
||||
onOpen,
|
||||
style,
|
||||
@@ -106,7 +106,7 @@ export const ExternalLinkEmbed = ({
|
||||
) : undefined}
|
||||
|
||||
{embedPlayerParams?.isGif ? (
|
||||
<ExternalGifEmbed link={link} params={embedPlayerParams} />
|
||||
<ExternalGif link={link} params={embedPlayerParams} />
|
||||
) : embedPlayerParams ? (
|
||||
<ExternalPlayer link={link} params={embedPlayerParams} />
|
||||
) : undefined}
|
||||
@@ -0,0 +1,53 @@
|
||||
import {useMemo} from 'react'
|
||||
import {moderateFeedGenerator} from '@atproto/api'
|
||||
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import * as FeedCard from '#/components/FeedCard'
|
||||
import {ContentHider} from '#/components/moderation/ContentHider'
|
||||
import {type EmbedType} from '#/types/bsky/post'
|
||||
import {type CommonProps} from './types'
|
||||
|
||||
export function FeedEmbed({
|
||||
embed,
|
||||
}: CommonProps & {
|
||||
embed: EmbedType<'feed'>
|
||||
}) {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<FeedCard.Link
|
||||
view={embed.view}
|
||||
style={[a.border, t.atoms.border_contrast_medium, a.p_md, a.rounded_sm]}>
|
||||
<FeedCard.Outer>
|
||||
<FeedCard.Header>
|
||||
<FeedCard.Avatar src={embed.view.avatar} />
|
||||
<FeedCard.TitleAndByline
|
||||
title={embed.view.displayName}
|
||||
creator={embed.view.creator}
|
||||
/>
|
||||
</FeedCard.Header>
|
||||
<FeedCard.Likes count={embed.view.likeCount || 0} />
|
||||
</FeedCard.Outer>
|
||||
</FeedCard.Link>
|
||||
)
|
||||
}
|
||||
|
||||
export function ModeratedFeedEmbed({
|
||||
embed,
|
||||
}: CommonProps & {
|
||||
embed: EmbedType<'feed'>
|
||||
}) {
|
||||
const moderationOpts = useModerationOpts()
|
||||
const moderation = useMemo(() => {
|
||||
return moderationOpts
|
||||
? moderateFeedGenerator(embed.view, moderationOpts)
|
||||
: undefined
|
||||
}, [embed.view, moderationOpts])
|
||||
return (
|
||||
<ContentHider
|
||||
modui={moderation?.ui('contentList')}
|
||||
childContainerStyle={[a.pt_xs]}>
|
||||
<FeedEmbed embed={embed} />
|
||||
</ContentHider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import {InteractionManager, View} from 'react-native'
|
||||
import {
|
||||
type AnimatedRef,
|
||||
measure,
|
||||
type MeasuredDimensions,
|
||||
runOnJS,
|
||||
runOnUI,
|
||||
} from 'react-native-reanimated'
|
||||
import {Image} from 'expo-image'
|
||||
|
||||
import {useLightboxControls} from '#/state/lightbox'
|
||||
import {type Dimensions} from '#/view/com/lightbox/ImageViewing/@types'
|
||||
import {AutoSizedImage} from '#/view/com/util/images/AutoSizedImage'
|
||||
import {ImageLayoutGrid} from '#/view/com/util/images/ImageLayoutGrid'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {PostEmbedViewContext} from '#/components/Post/Embed/types'
|
||||
import {type EmbedType} from '#/types/bsky/post'
|
||||
import {type CommonProps} from './types'
|
||||
|
||||
export function ImageEmbed({
|
||||
embed,
|
||||
...rest
|
||||
}: CommonProps & {
|
||||
embed: EmbedType<'images'>
|
||||
}) {
|
||||
const {openLightbox} = useLightboxControls()
|
||||
const {images} = embed.view
|
||||
|
||||
if (images.length > 0) {
|
||||
const items = images.map(img => ({
|
||||
uri: img.fullsize,
|
||||
thumbUri: img.thumb,
|
||||
alt: img.alt,
|
||||
dimensions: img.aspectRatio ?? null,
|
||||
}))
|
||||
const _openLightbox = (
|
||||
index: number,
|
||||
thumbRects: (MeasuredDimensions | null)[],
|
||||
fetchedDims: (Dimensions | null)[],
|
||||
) => {
|
||||
openLightbox({
|
||||
images: items.map((item, i) => ({
|
||||
...item,
|
||||
thumbRect: thumbRects[i] ?? null,
|
||||
thumbDimensions: fetchedDims[i] ?? null,
|
||||
type: 'image',
|
||||
})),
|
||||
index,
|
||||
})
|
||||
}
|
||||
const onPress = (
|
||||
index: number,
|
||||
refs: AnimatedRef<any>[],
|
||||
fetchedDims: (Dimensions | null)[],
|
||||
) => {
|
||||
runOnUI(() => {
|
||||
'worklet'
|
||||
const rects: (MeasuredDimensions | null)[] = []
|
||||
for (const r of refs) {
|
||||
rects.push(measure(r))
|
||||
}
|
||||
runOnJS(_openLightbox)(index, rects, fetchedDims)
|
||||
})()
|
||||
}
|
||||
const onPressIn = (_: number) => {
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
Image.prefetch(items.map(i => i.uri))
|
||||
})
|
||||
}
|
||||
|
||||
if (images.length === 1) {
|
||||
const image = images[0]
|
||||
return (
|
||||
<View style={[a.mt_sm, rest.style]}>
|
||||
<AutoSizedImage
|
||||
crop={
|
||||
rest.viewContext === PostEmbedViewContext.ThreadHighlighted
|
||||
? 'none'
|
||||
: rest.viewContext ===
|
||||
PostEmbedViewContext.FeedEmbedRecordWithMedia
|
||||
? 'square'
|
||||
: 'constrained'
|
||||
}
|
||||
image={image}
|
||||
onPress={(containerRef, dims) => onPress(0, [containerRef], [dims])}
|
||||
onPressIn={() => onPressIn(0)}
|
||||
hideBadge={
|
||||
rest.viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[a.mt_sm, rest.style]}>
|
||||
<ImageLayoutGrid
|
||||
images={images}
|
||||
onPress={onPress}
|
||||
onPressIn={onPressIn}
|
||||
viewContext={rest.viewContext}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import {useMemo} from 'react'
|
||||
import {View} from 'react-native'
|
||||
|
||||
import {createEmbedViewRecordFromPost} from '#/state/queries/postgate/util'
|
||||
import {useResolveLinkQuery} from '#/state/queries/resolve-link'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {QuoteEmbed} from '#/components/Post/Embed'
|
||||
|
||||
export function LazyQuoteEmbed({uri}: {uri: string}) {
|
||||
const t = useTheme()
|
||||
const {data} = useResolveLinkQuery(uri)
|
||||
|
||||
const view = useMemo(() => {
|
||||
if (!data || data.type !== 'record' || data.kind !== 'post') return
|
||||
return createEmbedViewRecordFromPost(data.view)
|
||||
}, [data])
|
||||
|
||||
return view ? (
|
||||
<QuoteEmbed
|
||||
embed={{
|
||||
type: 'post',
|
||||
view,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
a.rounded_md,
|
||||
t.atoms.bg_contrast_25,
|
||||
{
|
||||
height: 68,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import {useMemo} from 'react'
|
||||
import {moderateUserList} from '@atproto/api'
|
||||
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import * as ListCard from '#/components/ListCard'
|
||||
import {ContentHider} from '#/components/moderation/ContentHider'
|
||||
import {type EmbedType} from '#/types/bsky/post'
|
||||
import {type CommonProps} from './types'
|
||||
|
||||
export function ListEmbed({
|
||||
embed,
|
||||
}: CommonProps & {
|
||||
embed: EmbedType<'list'>
|
||||
}) {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<ListCard.Default
|
||||
view={embed.view}
|
||||
style={[a.border, t.atoms.border_contrast_medium, a.p_md, a.rounded_sm]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function ModeratedListEmbed({
|
||||
embed,
|
||||
}: CommonProps & {
|
||||
embed: EmbedType<'list'>
|
||||
}) {
|
||||
const moderationOpts = useModerationOpts()
|
||||
const moderation = useMemo(() => {
|
||||
return moderationOpts
|
||||
? moderateUserList(embed.view, moderationOpts)
|
||||
: undefined
|
||||
}, [embed.view, moderationOpts])
|
||||
return (
|
||||
<ContentHider
|
||||
modui={moderation?.ui('contentList')}
|
||||
childContainerStyle={[a.pt_xs]}>
|
||||
<ListEmbed embed={embed} />
|
||||
</ContentHider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import {StyleSheet, View} from 'react-native'
|
||||
|
||||
import {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {InfoCircleIcon} from '#/lib/icons'
|
||||
import {Text} from '#/view/com/util/text/Text'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
|
||||
export function PostPlaceholder({children}: {children: React.ReactNode}) {
|
||||
const t = useTheme()
|
||||
const pal = usePalette('default')
|
||||
return (
|
||||
<View
|
||||
style={[styles.errorContainer, a.border, t.atoms.border_contrast_low]}>
|
||||
<InfoCircleIcon size={18} style={pal.text} />
|
||||
<Text type="lg" style={pal.text}>
|
||||
{children}
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
errorContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
borderRadius: 8,
|
||||
marginTop: 8,
|
||||
paddingVertical: 14,
|
||||
paddingHorizontal: 14,
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import {StyleProp, ViewStyle} from 'react-native'
|
||||
import {type StyleProp, type ViewStyle} from 'react-native'
|
||||
import {View} from 'react-native'
|
||||
import {msg, plural} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
@@ -1,13 +1,12 @@
|
||||
import React, {useRef} from 'react'
|
||||
import {Pressable, StyleProp, View, ViewStyle} from 'react-native'
|
||||
import {AppBskyEmbedVideo} from '@atproto/api'
|
||||
import {Pressable, type StyleProp, View, type ViewStyle} from 'react-native'
|
||||
import {type AppBskyEmbedVideo} from '@atproto/api'
|
||||
import {BlueskyVideoView} from '@haileyok/bluesky-video'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {HITSLOP_30} from '#/lib/constants'
|
||||
import {useAutoplayDisabled} from '#/state/preferences'
|
||||
import {useVideoMuteState} from '#/view/com/util/post-embeds/VideoVolumeContext'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {useIsWithinMessage} from '#/components/dms/MessageContext'
|
||||
import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute'
|
||||
@@ -15,6 +14,7 @@ import {Pause_Filled_Corner0_Rounded as PauseIcon} from '#/components/icons/Paus
|
||||
import {Play_Filled_Corner0_Rounded as PlayIcon} from '#/components/icons/Play'
|
||||
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker'
|
||||
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
||||
import {useVideoMuteState} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
|
||||
import {TimeIndicator} from './TimeIndicator'
|
||||
|
||||
export const VideoEmbedInnerNative = React.forwardRef(
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, {useEffect, useId, useRef, useState} from 'react'
|
||||
import {useEffect, useId, useRef, useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {type AppBskyEmbedVideo} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
@@ -28,7 +28,7 @@ export function VideoEmbedInnerWeb({
|
||||
const videoRef = useRef<HTMLVideoElement>(null)
|
||||
const [focused, setFocused] = useState(false)
|
||||
const [hasSubtitleTrack, setHasSubtitleTrack] = useState(false)
|
||||
const [hlsLoading, setHlsLoading] = React.useState(false)
|
||||
const [hlsLoading, setHlsLoading] = useState(false)
|
||||
const figId = useId()
|
||||
const {_} = useLingui()
|
||||
|
||||
@@ -101,8 +101,8 @@ export function VideoEmbedInnerWeb({
|
||||
fullscreenRef={containerRef}
|
||||
hasSubtitleTrack={hasSubtitleTrack}
|
||||
/>
|
||||
<MediaInsetBorder />
|
||||
</div>
|
||||
<MediaInsetBorder />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import type React from 'react'
|
||||
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react'
|
||||
import {SvgProps} from 'react-native-svg'
|
||||
import {type SvgProps} from 'react-native-svg'
|
||||
import type React from 'react'
|
||||
|
||||
import {PressableWithHover} from '#/view/com/util/PressableWithHover'
|
||||
import {atoms as a, useTheme, web} from '#/alf'
|
||||
import {PressableWithHover} from '../../../PressableWithHover'
|
||||
|
||||
export function ControlButton({
|
||||
active,
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, {useCallback, useEffect, useRef, useState} from 'react'
|
||||
import {useCallback, useEffect, useRef, useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import type React from 'react'
|
||||
|
||||
import {isFirefox, isTouchDevice} from '#/lib/browser'
|
||||
import {clamp} from '#/lib/numbers'
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, {useCallback, useEffect, useRef, useState} from 'react'
|
||||
import {useCallback, useEffect, useRef, useState} from 'react'
|
||||
import {Pressable, View} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
@@ -317,8 +317,8 @@ export function Controls({
|
||||
!focused
|
||||
? msg`Unmute video`
|
||||
: playing
|
||||
? msg`Pause video`
|
||||
: msg`Play video`,
|
||||
? msg`Pause video`
|
||||
: msg`Play video`,
|
||||
)}
|
||||
accessibilityHint=""
|
||||
style={[
|
||||
@@ -1,14 +1,15 @@
|
||||
import React, {useCallback} from 'react'
|
||||
import {useCallback} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import Animated, {FadeIn, FadeOut} from 'react-native-reanimated'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import type React from 'react'
|
||||
|
||||
import {isSafari, isTouchDevice} from '#/lib/browser'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute'
|
||||
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker'
|
||||
import {useVideoVolumeState} from '../../VideoVolumeContext'
|
||||
import {useVideoVolumeState} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
|
||||
import {ControlButton} from './ControlButton'
|
||||
|
||||
export function VolumeControl({
|
||||
@@ -1,9 +1,10 @@
|
||||
import React, {useCallback, useEffect, useRef, useState} from 'react'
|
||||
import {type RefObject, useCallback, useEffect, useRef, useState} from 'react'
|
||||
|
||||
import {isSafari} from '#/lib/browser'
|
||||
import {useVideoVolumeState} from '../../VideoVolumeContext'
|
||||
import {logger} from '#/logger'
|
||||
import {useVideoVolumeState} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
|
||||
|
||||
export function useVideoElement(ref: React.RefObject<HTMLVideoElement>) {
|
||||
export function useVideoElement(ref: RefObject<HTMLVideoElement>) {
|
||||
const [playing, setPlaying] = useState(false)
|
||||
const [muted, setMuted] = useState(true)
|
||||
const [currentTime, setCurrentTime] = useState(0)
|
||||
@@ -79,7 +80,12 @@ export function useVideoElement(ref: React.RefObject<HTMLVideoElement>) {
|
||||
await ref.current.play()
|
||||
} catch (e: any) {
|
||||
if (
|
||||
!e.message?.includes(`The request is not allowed by the user agent`)
|
||||
!e.message?.includes(
|
||||
`The request is not allowed by the user agent`,
|
||||
) &&
|
||||
!e.message?.includes(
|
||||
`The play() request was interrupted by a call to pause()`,
|
||||
)
|
||||
) {
|
||||
throw e
|
||||
}
|
||||
@@ -176,8 +182,15 @@ export function useVideoElement(ref: React.RefObject<HTMLVideoElement>) {
|
||||
} else {
|
||||
const promise = ref.current.play()
|
||||
if (promise !== undefined) {
|
||||
promise.catch(err => {
|
||||
console.error('Error playing video:', err)
|
||||
promise.catch((err: any) => {
|
||||
if (
|
||||
// ignore this common error. it's fine
|
||||
!err.message?.includes(
|
||||
`The play() request was interrupted by a call to pause()`,
|
||||
)
|
||||
) {
|
||||
logger.error('Error playing video:', {message: err})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||