Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fde5fb01dc |
@@ -13,7 +13,6 @@ module.exports = {
|
||||
'lingui',
|
||||
'simple-import-sort',
|
||||
'bsky-internal',
|
||||
'eslint-plugin-react-compiler',
|
||||
],
|
||||
rules: {
|
||||
// Temporary until https://github.com/facebook/react-native/pull/43756 gets into a release.
|
||||
@@ -68,8 +67,6 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
'simple-import-sort/exports': 'warn',
|
||||
// TODO: Reenable when we figure out why it gets stuck on CI.
|
||||
// 'react-compiler/react-compiler': 'error',
|
||||
},
|
||||
ignorePatterns: [
|
||||
'**/__mocks__/*.ts',
|
||||
|
||||
@@ -119,15 +119,3 @@ jobs:
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
|
||||
- name: ⬇️ Restore Cache
|
||||
id: get-base-commit
|
||||
uses: actions/cache@v4
|
||||
if: ${{ inputs.profile == 'testflight' }}
|
||||
with:
|
||||
path: most-recent-testflight-commit.txt
|
||||
key: most-recent-testflight-commit
|
||||
|
||||
- name: ✏️ Write commit hash to cache
|
||||
if: ${{ inputs.profile == 'testflight' }}
|
||||
run: echo ${{ github.sha }} > most-recent-testflight-commit.txt
|
||||
|
||||
@@ -47,10 +47,6 @@ jobs:
|
||||
- name: ⚙️ Install dependencies
|
||||
run: yarn install
|
||||
|
||||
- uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: '15.3'
|
||||
|
||||
- name: ☕️ Setup Cocoapods
|
||||
uses: maxim-lobanov/setup-cocoapods@v1
|
||||
with:
|
||||
@@ -80,15 +76,3 @@ jobs:
|
||||
|
||||
- name: 🚀 Deploy
|
||||
run: eas submit -p ios --non-interactive --path build.ipa
|
||||
|
||||
- name: ⬇️ Restore Cache
|
||||
id: get-base-commit
|
||||
uses: actions/cache@v4
|
||||
if: ${{ inputs.profile == 'testflight' }}
|
||||
with:
|
||||
path: most-recent-testflight-commit.txt
|
||||
key: most-recent-testflight-commit
|
||||
|
||||
- name: ✏️ Write commit hash to cache
|
||||
if: ${{ inputs.profile == 'testflight' }}
|
||||
run: echo ${{ github.sha }} > most-recent-testflight-commit.txt
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-deploy
|
||||
cancel-in-progress: true
|
||||
outputs:
|
||||
changes-detected: ${{ steps.fingerprint.outputs.includes-changes }}
|
||||
fingerprint-is-different: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different }}
|
||||
|
||||
steps:
|
||||
- name: Check for EXPO_TOKEN
|
||||
@@ -49,22 +49,69 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: ⬇️ Get last successful deployment commit from the cache
|
||||
id: get-base-commit
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: last-successful-commit-hash.txt
|
||||
key: last-successful-deployment-commit-${{ github.ref_name }}-${{ github.sha }}
|
||||
restore-keys: |
|
||||
last-successful-deployment-commit-${{ github.ref_name }}-
|
||||
|
||||
- name: Add the last successful deployment commit to the output
|
||||
id: last-successful-commit
|
||||
run: echo base-commit=$(cat last-successful-commit-hash.txt) >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: ⬇️ Fetch commits from base branch
|
||||
if: ${{ github.ref != 'refs/heads/main' }}
|
||||
run: git fetch origin main:main --depth 100
|
||||
|
||||
# This should get the current production release's commit's hash to see if the update is compatible
|
||||
- name: 🕵️ Get the base commit
|
||||
id: base-commit
|
||||
run: |
|
||||
if ${{ inputs.channel == 'production' }}; then
|
||||
echo base-commit=$(git show-ref -s ${{ inputs.runtimeVersion }}) >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo base-commit=${{ steps.last-successful-commit.base-commit }} >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: ✓ Make sure we found a base commit
|
||||
run: |
|
||||
if [ -z "${{ steps.base-commit.outputs.base-commit }}" && ${{ inputs.channel == 'production' }} ]; then
|
||||
echo "Could not find a base commit for this release. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: 🔧 Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: yarn
|
||||
|
||||
- name: 📷 Check fingerprint and install dependencies
|
||||
- name: ⚙️ Install Dependencies
|
||||
run: yarn install
|
||||
|
||||
# Run the fingerprint
|
||||
- name: 📷 Check fingerprint
|
||||
id: fingerprint
|
||||
uses: bluesky-social/github-actions/fingerprint-native@main
|
||||
uses: expo/expo-github-action/fingerprint@main
|
||||
with:
|
||||
profile: ${{ inputs.channel || 'testflight' }}
|
||||
previous-commit-tag: ${{ inputs.runtimeVersion }}
|
||||
previous-git-commit: ${{ steps.base-commit.outputs.base-commit }}
|
||||
|
||||
- name: 👀 Debug fingerprint
|
||||
id: fingerprint-debug
|
||||
run: |
|
||||
echo "previousGitCommit=${{ steps.fingerprint.outputs.previous-git-commit }} currentGitCommit=${{ steps.fingerprint.outputs.current-git-commit }}"
|
||||
echo "isPreviousFingerprintEmpty=${{ steps.fingerprint.outputs.previous-fingerprint == '' }}"
|
||||
|
||||
fingerprintDiff='$(echo "${{ steps.fingerprint.outputs.fingerprint-diff }}")'
|
||||
|
||||
if [[ $fingerprintDiff =~ "bareRncliAutolinking" || $fingerprintDiff =~ "expoAutolinkingAndroid" || $fingerprintDiff =~ "expoAutolinkingIos" ]]; then
|
||||
echo fingerprint-is-different="true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo fingerprint-is-different="false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Lint check
|
||||
run: yarn lint
|
||||
@@ -80,22 +127,22 @@ jobs:
|
||||
|
||||
- name: 🔨 Setup EAS
|
||||
uses: expo/expo-github-action@v8
|
||||
if: ${{ !steps.fingerprint.outputs.includes-changes }}
|
||||
if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
|
||||
with:
|
||||
expo-version: latest
|
||||
eas-version: latest
|
||||
token: ${{ secrets.EXPO_TOKEN }}
|
||||
|
||||
- name: ⛏️ Setup Expo
|
||||
if: ${{ !steps.fingerprint.outputs.includes-changes }}
|
||||
if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
|
||||
run: yarn global add eas-cli-local-build-plugin
|
||||
|
||||
- name: 🪛 Setup jq
|
||||
if: ${{ !steps.fingerprint.outputs.includes-changes }}
|
||||
if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
|
||||
uses: dcarbone/install-jq-action@v2
|
||||
|
||||
- name: ✏️ Write environment variables
|
||||
if: ${{ !steps.fingerprint.outputs.includes-changes }}
|
||||
if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
|
||||
run: |
|
||||
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
|
||||
echo "${{ secrets.ENV_TOKEN }}" > .env
|
||||
@@ -104,28 +151,19 @@ jobs:
|
||||
echo "$json" > google-services.json
|
||||
|
||||
- name: 🏗️ Create Bundle
|
||||
if: ${{ !steps.fingerprint.outputs.includes-changes }}
|
||||
if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
|
||||
run: EXPO_PUBLIC_ENV="${{ inputs.channel || 'testflight' }}" yarn export
|
||||
|
||||
- name: 📦 Package Bundle and 🚀 Deploy
|
||||
if: ${{ !steps.fingerprint.outputs.includes-changes }}
|
||||
if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
|
||||
run: yarn use-build-number bash scripts/bundleUpdate.sh
|
||||
env:
|
||||
DENIS_API_KEY: ${{ secrets.DENIS_API_KEY }}
|
||||
RUNTIME_VERSION: ${{ inputs.runtimeVersion }}
|
||||
CHANNEL_NAME: ${{ inputs.channel || 'testflight' }}
|
||||
|
||||
- name: ⬇️ Restore Cache
|
||||
id: get-base-commit
|
||||
uses: actions/cache@v4
|
||||
if: ${{ !steps.fingerprint.outputs.includes-changes }}
|
||||
with:
|
||||
path: most-recent-testflight-commit.txt
|
||||
key: most-recent-testflight-commit
|
||||
|
||||
- name: ✏️ Write commit hash to cache
|
||||
if: ${{ !steps.fingerprint.outputs.includes-changes }}
|
||||
run: echo ${{ github.sha }} > most-recent-testflight-commit.txt
|
||||
- name: Save successful deployment commit hash
|
||||
run: echo ${{ steps.fingerprint.outputs.current-git-commit }} > last-successful-commit-hash.txt
|
||||
|
||||
# GitHub actions are horrible so let's just copy paste this in
|
||||
buildIfNecessaryIOS:
|
||||
@@ -133,11 +171,11 @@ jobs:
|
||||
runs-on: macos-14
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-build-ios
|
||||
cancel-in-progress: true
|
||||
cancel-in-progress: false
|
||||
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.fingerprint-is-different == 'true' }}
|
||||
steps:
|
||||
- name: Check for EXPO_TOKEN
|
||||
run: >
|
||||
@@ -170,10 +208,6 @@ jobs:
|
||||
- name: ⚙️ Install dependencies
|
||||
run: yarn install
|
||||
|
||||
- uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: '15.3'
|
||||
|
||||
- name: ☕️ Setup Cocoapods
|
||||
uses: maxim-lobanov/setup-cocoapods@v1
|
||||
with:
|
||||
@@ -204,18 +238,6 @@ jobs:
|
||||
- name: 🚀 Deploy
|
||||
run: eas submit -p ios --non-interactive --path build.ipa
|
||||
|
||||
- name: ⬇️ Restore Cache
|
||||
id: get-base-commit
|
||||
uses: actions/cache@v4
|
||||
if: ${{ inputs.channel == 'testflight' }}
|
||||
with:
|
||||
path: most-recent-testflight-commit.txt
|
||||
key: most-recent-testflight-commit
|
||||
|
||||
- name: ✏️ Write commit hash to cache
|
||||
if: ${{ inputs.channel == 'testflight' }}
|
||||
run: echo ${{ github.sha }} > most-recent-testflight-commit.txt
|
||||
|
||||
buildIfNecessaryAndroid:
|
||||
name: Build and Submit Android
|
||||
runs-on: ubuntu-latest
|
||||
@@ -225,7 +247,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.fingerprint-is-different == 'true' }}
|
||||
|
||||
steps:
|
||||
- name: Check for EXPO_TOKEN
|
||||
@@ -303,15 +325,3 @@ jobs:
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
|
||||
- name: ⬇️ Restore Cache
|
||||
id: get-base-commit
|
||||
uses: actions/cache@v4
|
||||
if: ${{ inputs.channel != 'testflight' && inputs.channel != 'production' }}
|
||||
with:
|
||||
path: most-recent-testflight-commit.txt
|
||||
key: most-recent-testflight-commit
|
||||
|
||||
- name: ✏️ Write commit hash to cache
|
||||
if: ${{ inputs.channel != 'testflight' && inputs.channel != 'production' }}
|
||||
run: echo ${{ github.sha }} > most-recent-testflight-commit.txt
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Credit for fingerprint action https://github.com/expo/expo
|
||||
# https://github.com/expo/expo/blob/main/.github/workflows/pr-labeler.yml
|
||||
---
|
||||
name: PR Tests
|
||||
name: PR labeler
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -21,7 +21,7 @@ permissions:
|
||||
jobs:
|
||||
webpack-analyzer:
|
||||
runs-on: ubuntu-22.04
|
||||
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.event_name == 'pull_request'}}
|
||||
if: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
|
||||
steps:
|
||||
- name: ⬇️ Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -94,9 +94,10 @@ jobs:
|
||||
| ${{ steps.get-diff.outputs.base_file_string }} | ${{ steps.get-diff.outputs.pr_file_string }} | ${{ steps.get-diff.outputs.diff_file_string }} (${{ steps.get-diff.outputs.percent }}%) |
|
||||
---
|
||||
|
||||
fingerprint-native:
|
||||
test-suite-fingerprint:
|
||||
runs-on: ubuntu-22.04
|
||||
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.event_name == 'pull_request'}}
|
||||
if: ${{ github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'push' }}
|
||||
concurrency: fingerprint-${{ github.event_name != 'pull_request' && 'main' || github.run_id }}
|
||||
steps:
|
||||
- name: ⬇️ Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -113,23 +114,35 @@ jobs:
|
||||
node-version-file: .nvmrc
|
||||
cache: yarn
|
||||
|
||||
- name: 📷 Check fingerprint and install dependencies
|
||||
- name: ⚙️ Install Dependencies
|
||||
run: yarn install
|
||||
|
||||
- name: Get the base commit
|
||||
id: base-commit
|
||||
run: echo base-commit=$(git log -n 1 main --pretty=format:'%H') >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: 📷 Check fingerprint
|
||||
id: fingerprint
|
||||
uses: bluesky-social/github-actions/fingerprint-native@main
|
||||
uses: expo/expo-github-action/fingerprint@main
|
||||
with:
|
||||
profile: pull-request
|
||||
previous-git-commit: ${{ steps.base-commit.outputs.base-commit }}
|
||||
|
||||
- name: 👀 Debug fingerprint
|
||||
run: |
|
||||
echo "previousGitCommit=${{ steps.fingerprint.outputs.previous-git-commit }} currentGitCommit=${{ steps.fingerprint.outputs.current-git-commit }}"
|
||||
echo "isPreviousFingerprintEmpty=${{ steps.fingerprint.outputs.previous-fingerprint == '' }}"
|
||||
|
||||
- name: 💬 Drop a comment
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
if: ${{ steps.fingerprint.outputs.includes-changes }}
|
||||
if: ${{ github.event_name == 'pull_request' && steps.fingerprint.outputs.fingerprint-diff != '[]' }}
|
||||
with:
|
||||
header: fingerprint-diff
|
||||
message: |
|
||||
The Pull Request introduced fingerprint changes against the base commit:
|
||||
The Pull Request introduced fingerprint changes against the base commit: ${{ steps.fingerprint.outputs.previous-git-commit }}
|
||||
<details><summary>Fingerprint diff</summary>
|
||||
|
||||
```json
|
||||
${{ steps.fingerprint.outputs.diff }}
|
||||
${{ steps.fingerprint.outputs.fingerprint-diff }}
|
||||
```
|
||||
|
||||
</details>
|
||||
@@ -139,7 +152,7 @@ jobs:
|
||||
|
||||
- name: 💬 Delete comment
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
if: ${{ !steps.fingerprint.outputs.includes-changes }}
|
||||
if: ${{ github.event_name == 'pull_request' && steps.fingerprint.outputs.fingerprint-diff == '[]' }}
|
||||
with:
|
||||
header: fingerprint-diff
|
||||
delete: true
|
||||
|
||||
@@ -12,4 +12,3 @@
|
||||
android
|
||||
ios
|
||||
src/locale/locales
|
||||
lib/react-compiler-runtime
|
||||
|
||||
@@ -131,7 +131,7 @@ appId: xyz.blueskyweb.app
|
||||
- assertVisible:
|
||||
id: "feedItem-by-bob.test"
|
||||
- tapOn:
|
||||
id: "e2eGotoFeeds"
|
||||
id: "bottomBarFeedsBtn"
|
||||
- tapOn:
|
||||
id: "saved-feed-Good Ppl"
|
||||
- assertVisible:
|
||||
|
||||
+2
-2
@@ -175,6 +175,7 @@ module.exports = function (config) {
|
||||
checkAutomatically: 'NEVER',
|
||||
channel: UPDATES_CHANNEL,
|
||||
},
|
||||
assetBundlePatterns: ['**/*'],
|
||||
plugins: [
|
||||
'expo-localization',
|
||||
Boolean(process.env.SENTRY_AUTH_TOKEN) && 'sentry-expo',
|
||||
@@ -182,7 +183,7 @@ module.exports = function (config) {
|
||||
'expo-build-properties',
|
||||
{
|
||||
ios: {
|
||||
deploymentTarget: '14.0',
|
||||
deploymentTarget: '13.4',
|
||||
newArchEnabled: false,
|
||||
},
|
||||
android: {
|
||||
@@ -204,7 +205,6 @@ module.exports = function (config) {
|
||||
],
|
||||
'./plugins/withAndroidManifestPlugin.js',
|
||||
'./plugins/withAndroidManifestFCMIconPlugin.js',
|
||||
'./plugins/withAndroidManifestLaunchModePlugin.js',
|
||||
'./plugins/withAndroidStylesWindowBackgroundPlugin.js',
|
||||
'./plugins/withAndroidStylesAccentColorPlugin.js',
|
||||
'./plugins/withAndroidSplashScreenStatusBarTranslucentPlugin.js',
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M4.4 9.493C4.14 10.28 4 11.124 4 12a8 8 0 1 0 10.899-7.459l-.953 3.81a1 1 0 0 1-.726.727l-3.444.866-.772 1.533a1 1 0 0 1-1.493.35L4.4 9.493Zm.883-1.84L7.756 9.51l.44-.874a1 1 0 0 1 .649-.52l3.306-.832.807-3.227a7.993 7.993 0 0 0-7.676 3.597ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm8.43.162a1 1 0 0 1 .77-.29l1.89.121a1 1 0 0 1 .494.168l2.869 1.928a1 1 0 0 1 .336 1.277l-.973 1.946a1 1 0 0 1-.894.553h-2.92a1 1 0 0 1-.831-.445L9.225 14.5a1 1 0 0 1 .126-1.262l1.08-1.076Zm.915 1.913.177-.177 1.171.074 1.914 1.286-.303.607h-1.766l-1.194-1.79Z" clip-rule="evenodd"/></svg>
|
||||
|
Before Width: | Height: | Size: 714 B |
+4
-7
@@ -8,6 +8,9 @@ module.exports = function (api) {
|
||||
{
|
||||
lazyImports: true,
|
||||
native: {
|
||||
// We should be able to remove this after upgrading Expo
|
||||
// to a version that includes https://github.com/expo/expo/pull/24672.
|
||||
unstable_transformProfile: 'hermes-stable',
|
||||
// Disable ESM -> CJS compilation because Metro takes care of it.
|
||||
// However, we need it in Jest tests since those run without Metro.
|
||||
disableImportExportTransform: !isTestEnv,
|
||||
@@ -16,13 +19,6 @@ module.exports = function (api) {
|
||||
],
|
||||
],
|
||||
plugins: [
|
||||
'macros',
|
||||
[
|
||||
'babel-plugin-react-compiler',
|
||||
{
|
||||
runtimeModule: 'react-compiler-runtime',
|
||||
},
|
||||
],
|
||||
[
|
||||
'module:react-native-dotenv',
|
||||
{
|
||||
@@ -50,6 +46,7 @@ module.exports = function (api) {
|
||||
},
|
||||
},
|
||||
],
|
||||
'macros',
|
||||
'react-native-reanimated/plugin', // NOTE: this plugin MUST be last
|
||||
],
|
||||
env: {
|
||||
|
||||
+1
-1
@@ -15,6 +15,6 @@
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"ecmaVersion": "latest",
|
||||
"project": "./bskyembed/tsconfig.json"
|
||||
"project": "./tsconfig.json"
|
||||
}
|
||||
}
|
||||
@@ -193,7 +193,7 @@ export function Embed({
|
||||
function Info({children}: {children: ComponentChildren}) {
|
||||
return (
|
||||
<div className="w-full rounded-lg border py-2 px-2.5 flex-row flex gap-2 bg-neutral-50">
|
||||
<img src={infoIcon} className="w-4 h-4 shrink-0 mt-0.5" />
|
||||
<img src={infoIcon as string} className="w-4 h-4 shrink-0 mt-0.5" />
|
||||
<p className="text-sm text-textLight">{children}</p>
|
||||
</div>
|
||||
)
|
||||
@@ -293,8 +293,7 @@ function ExternalEmbed({
|
||||
return (
|
||||
<Link
|
||||
href={content.external.uri}
|
||||
className="w-full rounded-lg overflow-hidden border flex flex-col items-stretch"
|
||||
disableTracking>
|
||||
className="w-full rounded-lg overflow-hidden border flex flex-col items-stretch">
|
||||
{content.external.thumb && (
|
||||
<img
|
||||
src={content.external.thumb}
|
||||
|
||||
@@ -3,12 +3,10 @@ import {h} from 'preact'
|
||||
export function Link({
|
||||
href,
|
||||
className,
|
||||
disableTracking,
|
||||
...props
|
||||
}: {
|
||||
href: string
|
||||
className?: string
|
||||
disableTracking?: boolean
|
||||
} & h.JSX.HTMLAttributes<HTMLAnchorElement>) {
|
||||
const searchParam = new URLSearchParams(window.location.search)
|
||||
const ref_url = searchParam.get('ref_url')
|
||||
@@ -21,9 +19,9 @@ export function Link({
|
||||
|
||||
return (
|
||||
<a
|
||||
href={`${href.startsWith('http') ? href : `https://bsky.app${href}`}${
|
||||
disableTracking ? '' : `?${newSearchParam.toString()}`
|
||||
}`}
|
||||
href={`${
|
||||
href.startsWith('http') ? href : `https://bsky.app${href}`
|
||||
}?${newSearchParam.toString()}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
onClick={evt => evt.stopPropagation()}
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
AppBskyFeedDefs,
|
||||
AppBskyFeedPost,
|
||||
AppBskyRichtextFacet,
|
||||
RichText,
|
||||
} from '@atproto/api'
|
||||
import {AppBskyFeedDefs, AppBskyFeedPost, RichText} from '@atproto/api'
|
||||
import {h} from 'preact'
|
||||
|
||||
import replyIcon from '../../assets/bubble_filled_stroke2_corner2_rounded.svg'
|
||||
@@ -61,7 +56,7 @@ export function Post({thread}: Props) {
|
||||
<Link
|
||||
href={href}
|
||||
className="transition-transform hover:scale-110 shrink-0 self-start">
|
||||
<img src={logo} className="h-8" />
|
||||
<img src={logo as string} className="h-8" />
|
||||
</Link>
|
||||
</div>
|
||||
<PostContent record={record} />
|
||||
@@ -76,7 +71,7 @@ export function Post({thread}: Props) {
|
||||
<div className="border-t w-full pt-2.5 flex items-center gap-5 text-sm cursor-pointer">
|
||||
{!!post.likeCount && (
|
||||
<div className="flex items-center gap-2 cursor-pointer">
|
||||
<img src={likeIcon} className="w-5 h-5" />
|
||||
<img src={likeIcon as string} className="w-5 h-5" />
|
||||
<p className="font-bold text-neutral-500 mb-px">
|
||||
{post.likeCount}
|
||||
</p>
|
||||
@@ -84,14 +79,14 @@ export function Post({thread}: Props) {
|
||||
)}
|
||||
{!!post.repostCount && (
|
||||
<div className="flex items-center gap-2 cursor-pointer">
|
||||
<img src={repostIcon} className="w-5 h-5" />
|
||||
<img src={repostIcon as string} className="w-5 h-5" />
|
||||
<p className="font-bold text-neutral-500 mb-px">
|
||||
{post.repostCount}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2 cursor-pointer">
|
||||
<img src={replyIcon} className="w-5 h-5" />
|
||||
<img src={replyIcon as string} className="w-5 h-5" />
|
||||
<p className="font-bold text-neutral-500 mb-px">Reply</p>
|
||||
</div>
|
||||
<div className="flex-1" />
|
||||
@@ -123,23 +118,16 @@ function PostContent({record}: {record: AppBskyFeedPost.Record | null}) {
|
||||
|
||||
let counter = 0
|
||||
for (const segment of rt.segments()) {
|
||||
if (
|
||||
segment.link &&
|
||||
AppBskyRichtextFacet.validateLink(segment.link).success
|
||||
) {
|
||||
if (segment.isLink() && segment.link) {
|
||||
richText.push(
|
||||
<Link
|
||||
key={counter}
|
||||
href={segment.link.uri}
|
||||
className="text-blue-400 hover:underline"
|
||||
disableTracking={!segment.link.uri.startsWith('https://bsky.app')}>
|
||||
className="text-blue-400 hover:underline">
|
||||
{segment.text}
|
||||
</Link>,
|
||||
)
|
||||
} else if (
|
||||
segment.mention &&
|
||||
AppBskyRichtextFacet.validateMention(segment.mention).success
|
||||
) {
|
||||
} else if (segment.isMention() && segment.mention) {
|
||||
richText.push(
|
||||
<Link
|
||||
key={counter}
|
||||
@@ -148,10 +136,7 @@ function PostContent({record}: {record: AppBskyFeedPost.Record | null}) {
|
||||
{segment.text}
|
||||
</Link>,
|
||||
)
|
||||
} else if (
|
||||
segment.tag &&
|
||||
AppBskyRichtextFacet.validateTag(segment.tag).success
|
||||
) {
|
||||
} else if (segment.isTag() && segment.tag) {
|
||||
richText.push(
|
||||
<Link
|
||||
key={counter}
|
||||
|
||||
@@ -112,7 +112,7 @@ function LandingPage() {
|
||||
<Link
|
||||
href="https://bsky.social/about"
|
||||
className="transition-transform hover:scale-110">
|
||||
<img src={logo} className="h-10" />
|
||||
<img src={logo as string} className="h-10" />
|
||||
</Link>
|
||||
|
||||
<h1 className="text-4xl font-bold text-center">Embed a Bluesky Post</h1>
|
||||
@@ -125,7 +125,7 @@ function LandingPage() {
|
||||
placeholder={DEFAULT_POST}
|
||||
/>
|
||||
|
||||
<img src={arrowBottom} className="w-6" />
|
||||
<img src={arrowBottom as string} className="w-6" />
|
||||
|
||||
{loading ? (
|
||||
<Skeleton />
|
||||
|
||||
@@ -52,7 +52,7 @@ function PwiOptOut({thread}: {thread: AppBskyFeedDefs.ThreadViewPost}) {
|
||||
<Link
|
||||
href={href}
|
||||
className="transition-transform hover:scale-110 absolute top-4 right-4">
|
||||
<img src={logo} className="h-6" />
|
||||
<img src={logo as string} className="h-6" />
|
||||
</Link>
|
||||
<div className="w-full py-12 gap-4 flex flex-col items-center">
|
||||
<p className="max-w-80 text-center w-full text-textLight">
|
||||
@@ -75,7 +75,7 @@ function ErrorMessage() {
|
||||
<Link
|
||||
href="https://bsky.app/"
|
||||
className="transition-transform hover:scale-110 absolute top-4 right-4">
|
||||
<img src={logo} className="h-6" />
|
||||
<img src={logo as string} className="h-6" />
|
||||
</Link>
|
||||
<p className="my-16 text-center w-full text-textLight">
|
||||
Post not found, it may have been deleted.
|
||||
|
||||
+1
-4
@@ -4,10 +4,7 @@
|
||||
|
||||
- Set up your environment [using the expo instructions](https://docs.expo.dev/guides/local-app-development/).
|
||||
- make sure that the JAVA_HOME points to the zulu-17 directory in your `.zshrc` or `.bashrc` file: `export JAVA_HOME=/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home`. DO NOT use another JDK or you will encounter build errors.
|
||||
- If you're running macOS, make sure you are running the correct versions of Ruby and Cocoapods:-
|
||||
- If you are using Apple Silicon and this is the first time you are building for RN 0.74+, you may need to run:
|
||||
- `arch -arm64 brew install llvm`
|
||||
- `sudo gem install ffi`
|
||||
- If you're running macOS, make sure you are running the correct versions of Ruby and Cocoapods:
|
||||
- Check if you've installed Cocoapods through `homebrew`. If you have, remove it:
|
||||
- `brew info cocoapods`
|
||||
- If output says `Installed`:
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
# OTA Deployments
|
||||
|
||||
## Overview
|
||||
|
||||

|
||||
|
||||
## Internal Deployments
|
||||
|
||||
Internal OTA deployments should be performed automatically upon all merges into main. In cases where the fingerprint
|
||||
diff results in incompatible native changes, a new client build will automatically be ran and deployed to TestFlight
|
||||
(iOS) or delivered in Slack (Android).
|
||||
|
||||
## Production Deployments
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Remove any internal client from your device and download the client from the App Store/Google Play. This will help for
|
||||
testing as well as retrieving the build number.
|
||||
- You should have signed in to EAS locally through npx eas login. You will need to modify the build number in a
|
||||
subsequent step.
|
||||
- Identify the build number of the production app you want to deploy an update for. iOS and Android build numbers are
|
||||
divergent, so you will need to find both
|
||||
|
||||

|
||||
|
||||
- Ensure that the commit the initial client was cut from is properly tagged in git. The tag should be in the format of 1.X.0
|
||||
- Note: If the commit is not properly tagged, then the OTA deployment will simply fail since the GitHub Action will
|
||||
not be able to find a commit to fingerprint and diff against.
|
||||
|
||||
### Preparation
|
||||
|
||||
- Create a new branch from the git tag that the initial release was cut from if no OTA deployment has been made yet for this
|
||||
client. Name this branch `1.X.0-ota-1`
|
||||
- If a deployment has been made previously for this release, increment the branch name, i.e. `1.x.0-ota-2`
|
||||
- If necessary, cherry-pick the commit(s) that you wish to deploy
|
||||
- Ensure that the package.json’s version field is set to the appropriate value. As long as used the correct git tag
|
||||
to create your branch from, this should be properly set.
|
||||
|
||||
### Deployment
|
||||
|
||||
- Update the build number through EAS
|
||||
- Note: This isn’t strictly necessary, but having a step that takes you off of GitHub and into the terminal provides
|
||||
a little “friction” to avoid fat fingering a release. Since there are legitimate reasons to just “click and deploy”
|
||||
for internal builds, I felt it useful to make sure it doesn’t accidentally become a prod deployment.
|
||||
- Set the build number to the appropriate build number found in the prerequisite steps. Again, this should be the
|
||||
build number for the current production release you want to deploy for.
|
||||
- `npx eas build:version:set -p ios`
|
||||
- `npx eas build:version:set -p android`
|
||||
- Run the deployment
|
||||
- Navigate to https://github.com/bluesky-social/social-app/actions/workflows/bundle-deploy-eas-update.yml
|
||||
- Select the “Run Workflow” dropdown
|
||||
|
||||

|
||||
|
||||
- Select the branch for the deployment you are releasing.
|
||||
|
||||

|
||||
|
||||
- Double check the branch selection.
|
||||
- Select the production channel
|
||||
- Enter the version for the client you are releasing to, i.e. 1.80.0
|
||||
- Note: If you do enter an incorrect version here, the deployment will either:
|
||||
- Fail because the action cannot find a commit with your misentered version
|
||||
- Succeed - but with no users receiving the update. This is because the version you entered will not properly
|
||||
correlate to a _build number_ as well, so no clients in the wild will be able to receive the update.
|
||||
|
||||

|
||||
|
||||
- Triple check the branch selection.
|
||||
- You selected the correct branch
|
||||
- You selected the "Production" channel
|
||||
- You entered the correct version in the format of `1.X.0`.
|
||||
- Press “Run Workflow”
|
||||
|
||||
In about five minutes, the new deployment should be available for download. To test:
|
||||
|
||||
- Remove the internal build of the app from your device
|
||||
- Download the app from the App Store/Google Play
|
||||
- Launch the app once and wait approximately 15 seconds
|
||||
- Relaunch the app
|
||||
- Check the Settings page and scroll to the bottom. The commit hash should now be the latest commit on your deployed branch.
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 27 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 38 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 189 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 34 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 144 KiB |
+2
-7
@@ -1,10 +1,10 @@
|
||||
/* global jest */
|
||||
import {configure} from '@testing-library/react-native'
|
||||
import 'react-native-gesture-handler/jestSetup'
|
||||
|
||||
// IMPORTANT: this is what's used in the native runtime
|
||||
import 'react-native-url-polyfill/auto'
|
||||
|
||||
import {configure} from '@testing-library/react-native'
|
||||
|
||||
configure({asyncUtilTimeout: 20000})
|
||||
|
||||
jest.mock('@react-native-async-storage/async-storage', () =>
|
||||
@@ -90,8 +90,3 @@ jest.mock('sentry-expo', () => ({
|
||||
}))
|
||||
|
||||
jest.mock('crypto', () => ({}))
|
||||
|
||||
jest.mock('expo-application', () => ({
|
||||
nativeApplicationVersion: '1.0.0',
|
||||
nativeBuildVersion: '1',
|
||||
}))
|
||||
|
||||
Vendored
-21
@@ -1,21 +0,0 @@
|
||||
const React = require('react')
|
||||
const $empty = Symbol.for('react.memo_cache_sentinel')
|
||||
/**
|
||||
* DANGER: this hook is NEVER meant to be called directly!
|
||||
*
|
||||
* Note that this is a temporary userspace implementation of this function
|
||||
* from React 19. It is not as efficient and may invalidate more frequently
|
||||
* than the official API. Please upgrade to React 19 as soon as you can.
|
||||
**/
|
||||
export function c(size) {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
return React.useState(() => {
|
||||
const $ = new Array(size)
|
||||
for (let ii = 0; ii < size; ii++) {
|
||||
$[ii] = $empty
|
||||
}
|
||||
// @ts-ignore
|
||||
$[$empty] = true
|
||||
return $
|
||||
})[0]
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"name": "react-compiler-runtime",
|
||||
"version": "0.0.1",
|
||||
"license": "MIT",
|
||||
"main": "index.js",
|
||||
"peerDependencies": {
|
||||
"react": "^18.2.0"
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
var prefs = UserDefaults(suiteName: APP_GROUP)
|
||||
|
||||
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
|
||||
guard let bestAttempt = createCopy(request.content),
|
||||
guard var bestAttempt = createCopy(request.content),
|
||||
let reason = request.content.userInfo["reason"] as? String
|
||||
else {
|
||||
contentHandler(request.content)
|
||||
@@ -15,10 +15,11 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
|
||||
if reason == "chat-message" {
|
||||
mutateWithChatMessage(bestAttempt)
|
||||
} else {
|
||||
mutateWithBadge(bestAttempt)
|
||||
}
|
||||
|
||||
// The badge should always be incremented when in the background
|
||||
mutateWithBadge(bestAttempt)
|
||||
|
||||
contentHandler(bestAttempt)
|
||||
}
|
||||
|
||||
|
||||
-3
@@ -35,8 +35,5 @@ class BackgroundNotificationHandler(
|
||||
remoteMessage.data["sound"] = null
|
||||
}
|
||||
}
|
||||
|
||||
// TODO - Remove this once we have more backend capability
|
||||
remoteMessage.data["badge"] = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ Pod::Spec.new do |s|
|
||||
s.static_framework = true
|
||||
|
||||
s.dependency 'ExpoModulesCore'
|
||||
s.dependency 'SDWebImage', '~> 5.19.1'
|
||||
s.dependency 'SDWebImageWebPCoder', '~> 0.14.6'
|
||||
s.dependency 'SDWebImage', '~> 5.17.0'
|
||||
s.dependency 'SDWebImageWebPCoder', '~> 0.13.0'
|
||||
|
||||
# Swift/Objective-C compatibility
|
||||
s.pod_target_xcconfig = {
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"platforms": ["ios"],
|
||||
"ios": {
|
||||
"modules": ["ExpoBlueskyTranslateModule"]
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
export {
|
||||
isAvailable,
|
||||
isLanguageSupported,
|
||||
NativeTranslationModule,
|
||||
NativeTranslationView,
|
||||
} from './src/ExpoBlueskyTranslateView'
|
||||
@@ -1,20 +0,0 @@
|
||||
import ExpoModulesCore
|
||||
import SwiftUI
|
||||
|
||||
// Thanks to Andrew Levy for this code snippet
|
||||
// https://github.com/andrew-levy/swiftui-react-native/blob/d3fbb2abf07601ff0d4b83055e7717bb980910d6/ios/Common/ExpoView%2BUIHostingController.swift
|
||||
|
||||
extension ExpoView {
|
||||
func setupHostingController(_ hostingController: UIHostingController<some View>) {
|
||||
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
hostingController.view.backgroundColor = .clear
|
||||
|
||||
addSubview(hostingController.view)
|
||||
NSLayoutConstraint.activate([
|
||||
hostingController.view.topAnchor.constraint(equalTo: self.topAnchor),
|
||||
hostingController.view.bottomAnchor.constraint(equalTo: self.bottomAnchor),
|
||||
hostingController.view.leftAnchor.constraint(equalTo: self.leftAnchor),
|
||||
hostingController.view.rightAnchor.constraint(equalTo: self.rightAnchor),
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'ExpoBlueskyTranslate'
|
||||
s.version = '1.0.0'
|
||||
s.summary = 'Uses SwiftUI translation to translate text.'
|
||||
s.description = 'Uses SwiftUI translation to translate text.'
|
||||
s.author = ''
|
||||
s.homepage = 'https://docs.expo.dev/modules/'
|
||||
s.platforms = { :ios => '13.4' }
|
||||
s.source = { git: '' }
|
||||
s.static_framework = true
|
||||
|
||||
s.dependency 'ExpoModulesCore'
|
||||
|
||||
# Swift/Objective-C compatibility
|
||||
s.pod_target_xcconfig = {
|
||||
'DEFINES_MODULE' => 'YES',
|
||||
'SWIFT_COMPILATION_MODE' => 'wholemodule'
|
||||
}
|
||||
|
||||
s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
|
||||
end
|
||||
@@ -1,18 +0,0 @@
|
||||
import ExpoModulesCore
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
public class ExpoBlueskyTranslateModule: Module {
|
||||
public func definition() -> ModuleDefinition {
|
||||
Name("ExpoBlueskyTranslate")
|
||||
|
||||
AsyncFunction("presentAsync") { (text: String) in
|
||||
DispatchQueue.main.async { [weak state = TranslateViewState.shared] in
|
||||
state?.isPresented = true
|
||||
state?.text = text
|
||||
}
|
||||
}
|
||||
|
||||
View(ExpoBlueskyTranslateView.self) {}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import ExpoModulesCore
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
class TranslateViewState: ObservableObject {
|
||||
static var shared = TranslateViewState()
|
||||
|
||||
@Published var isPresented = false
|
||||
@Published var text = ""
|
||||
}
|
||||
|
||||
class ExpoBlueskyTranslateView: ExpoView {
|
||||
required init(appContext: AppContext? = nil) {
|
||||
if #available(iOS 14.0, *) {
|
||||
let hostingController = UIHostingController(rootView: TranslateView())
|
||||
super.init(appContext: appContext)
|
||||
setupHostingController(hostingController)
|
||||
} else {
|
||||
super.init(appContext: appContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import SwiftUI
|
||||
// conditionally import the Translation module
|
||||
#if canImport(Translation)
|
||||
import Translation
|
||||
#endif
|
||||
|
||||
struct TranslateView: View {
|
||||
@ObservedObject var state = TranslateViewState.shared
|
||||
|
||||
var body: some View {
|
||||
if #available(iOS 17.4, *) {
|
||||
VStack {
|
||||
UIViewRepresentableWrapper(view: UIView(frame: .zero))
|
||||
}
|
||||
.translationPresentation(
|
||||
isPresented: $state.isPresented,
|
||||
text: state.text
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct UIViewRepresentableWrapper: UIViewRepresentable {
|
||||
let view: UIView
|
||||
|
||||
func makeUIView(context: Context) -> UIView {
|
||||
return view
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: UIView, context: Context) {}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export type ExpoBlueskyTranslateModule = {
|
||||
presentAsync: (text: string) => Promise<void>
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import React from 'react'
|
||||
import {Platform} from 'react-native'
|
||||
import {requireNativeModule, requireNativeViewManager} from 'expo-modules-core'
|
||||
|
||||
import {ExpoBlueskyTranslateModule} from './ExpoBlueskyTranslate.types'
|
||||
|
||||
export const NativeTranslationModule =
|
||||
requireNativeModule<ExpoBlueskyTranslateModule>('ExpoBlueskyTranslate')
|
||||
|
||||
const NativeView: React.ComponentType = requireNativeViewManager(
|
||||
'ExpoBlueskyTranslate',
|
||||
)
|
||||
|
||||
export function NativeTranslationView() {
|
||||
return <NativeView />
|
||||
}
|
||||
|
||||
export const isAvailable = Number(Platform.Version) >= 17.4
|
||||
|
||||
// https://en.wikipedia.org/wiki/Translate_(Apple)#Languages
|
||||
const SUPPORTED_LANGUAGES = [
|
||||
'ar',
|
||||
'zh',
|
||||
'zh',
|
||||
'nl',
|
||||
'en',
|
||||
'en',
|
||||
'fr',
|
||||
'de',
|
||||
'id',
|
||||
'it',
|
||||
'ja',
|
||||
'ko',
|
||||
'pl',
|
||||
'pt',
|
||||
'ru',
|
||||
'es',
|
||||
'th',
|
||||
'tr',
|
||||
'uk',
|
||||
'vi',
|
||||
]
|
||||
|
||||
export function isLanguageSupported(lang?: string) {
|
||||
// If the language is not provided, we assume it is supported
|
||||
if (!lang) return true
|
||||
return SUPPORTED_LANGUAGES.includes(lang)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
export const NativeTranslationModule = {
|
||||
presentAsync: async (_: string) => {},
|
||||
}
|
||||
|
||||
export function NativeTranslationView() {
|
||||
return null
|
||||
}
|
||||
|
||||
export const isAvailable = false
|
||||
|
||||
export function isLanguageSupported(_lang?: string) {
|
||||
return false
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
import React from 'react'
|
||||
|
||||
import {ExpoScrollForwarderViewProps} from './ExpoScrollForwarder.types'
|
||||
|
||||
export function ExpoScrollForwarderView({
|
||||
children,
|
||||
}: React.PropsWithChildren<ExpoScrollForwarderViewProps>) {
|
||||
|
||||
+50
-56
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bsky.app",
|
||||
"version": "1.85.0",
|
||||
"version": "1.83.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
@@ -49,7 +49,7 @@
|
||||
"open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "^0.12.13",
|
||||
"@atproto/api": "^0.12.10",
|
||||
"@bam.tech/react-native-image-resizer": "^3.0.4",
|
||||
"@braintree/sanitize-url": "^6.0.2",
|
||||
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
||||
@@ -65,17 +65,17 @@
|
||||
"@fortawesome/free-solid-svg-icons": "^6.1.1",
|
||||
"@fortawesome/react-native-fontawesome": "^0.3.0",
|
||||
"@lingui/react": "^4.5.0",
|
||||
"@mattermost/react-native-paste-input": "^0.7.1",
|
||||
"@mattermost/react-native-paste-input": "^0.6.4",
|
||||
"@miblanchard/react-native-slider": "^2.3.1",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@react-native-async-storage/async-storage": "1.23.1",
|
||||
"@react-native-masked-view/masked-view": "0.3.0",
|
||||
"@react-native-menu/menu": "^1.1.0",
|
||||
"@react-native-menu/menu": "^0.8.0",
|
||||
"@react-native-picker/picker": "2.6.1",
|
||||
"@react-navigation/bottom-tabs": "^6.5.20",
|
||||
"@react-navigation/drawer": "^6.6.15",
|
||||
"@react-navigation/native": "^6.1.17",
|
||||
"@react-navigation/native-stack": "^6.9.26",
|
||||
"@react-navigation/bottom-tabs": "^6.5.7",
|
||||
"@react-navigation/drawer": "^6.6.2",
|
||||
"@react-navigation/native": "^6.1.6",
|
||||
"@react-navigation/native-stack": "^6.9.12",
|
||||
"@segment/analytics-next": "^1.51.3",
|
||||
"@segment/analytics-react": "^1.0.0-rc1",
|
||||
"@segment/analytics-react-native": "^2.10.1",
|
||||
@@ -107,36 +107,35 @@
|
||||
"base64-js": "^1.5.1",
|
||||
"bcp-47-match": "^2.0.3",
|
||||
"date-fns": "^2.30.0",
|
||||
"deprecated-react-native-prop-types": "^5.0.0",
|
||||
"email-validator": "^2.0.4",
|
||||
"emoji-mart": "^5.5.2",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"expo": "^51.0.8",
|
||||
"expo-application": "^5.9.1",
|
||||
"expo-build-properties": "^0.12.1",
|
||||
"expo-camera": "~15.0.9",
|
||||
"expo-clipboard": "^6.0.3",
|
||||
"expo-constants": "~16.0.1",
|
||||
"expo-dev-client": "^4.0.14",
|
||||
"expo-device": "~6.0.2",
|
||||
"expo-file-system": "^17.0.1",
|
||||
"expo-haptics": "^13.0.1",
|
||||
"expo-image": "~1.12.9",
|
||||
"expo-image-manipulator": "^12.0.5",
|
||||
"expo-image-picker": "~15.0.5",
|
||||
"expo-linear-gradient": "^13.0.2",
|
||||
"expo-linking": "^6.3.1",
|
||||
"expo-localization": "~15.0.3",
|
||||
"expo-media-library": "~16.0.3",
|
||||
"expo-navigation-bar": "~3.0.4",
|
||||
"expo-notifications": "~0.28.3",
|
||||
"expo-sharing": "^12.0.1",
|
||||
"expo-splash-screen": "~0.27.4",
|
||||
"expo-status-bar": "~1.12.1",
|
||||
"expo-system-ui": "~3.0.4",
|
||||
"expo-task-manager": "~11.8.1",
|
||||
"expo-updates": "~0.25.14",
|
||||
"expo-web-browser": "~13.0.3",
|
||||
"expo": "^50.0.17",
|
||||
"expo-application": "^5.8.3",
|
||||
"expo-build-properties": "^0.11.1",
|
||||
"expo-camera": "~14.0.4",
|
||||
"expo-clipboard": "^5.0.1",
|
||||
"expo-constants": "~15.4.5",
|
||||
"expo-dev-client": "~3.3.8",
|
||||
"expo-device": "~5.9.3",
|
||||
"expo-file-system": "^16.0.9",
|
||||
"expo-haptics": "^12.8.1",
|
||||
"expo-image": "~1.10.6",
|
||||
"expo-image-manipulator": "^11.8.0",
|
||||
"expo-image-picker": "~14.7.1",
|
||||
"expo-linear-gradient": "^12.7.2",
|
||||
"expo-linking": "^6.2.2",
|
||||
"expo-localization": "~14.8.3",
|
||||
"expo-media-library": "~15.9.1",
|
||||
"expo-navigation-bar": "~2.8.1",
|
||||
"expo-notifications": "~0.27.6",
|
||||
"expo-sharing": "^11.10.0",
|
||||
"expo-splash-screen": "~0.26.4",
|
||||
"expo-status-bar": "~1.11.1",
|
||||
"expo-system-ui": "~2.9.3",
|
||||
"expo-task-manager": "~11.7.2",
|
||||
"expo-updates": "~0.24.10",
|
||||
"expo-web-browser": "~12.8.2",
|
||||
"fast-text-encoding": "^1.0.6",
|
||||
"history": "^5.3.0",
|
||||
"js-sha256": "^0.9.0",
|
||||
@@ -162,33 +161,31 @@
|
||||
"psl": "^1.9.0",
|
||||
"react": "18.2.0",
|
||||
"react-avatar-editor": "^13.0.0",
|
||||
"react-compiler-runtime": "file:./lib/react-compiler-runtime",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-keyed-flatten-children": "^3.0.0",
|
||||
"react-native": "0.74.1",
|
||||
"react-native-date-picker": "^4.4.2",
|
||||
"react-native": "0.73.2",
|
||||
"react-native-date-picker": "^4.4.0",
|
||||
"react-native-drawer-layout": "^4.0.0-alpha.3",
|
||||
"react-native-fs": "^2.20.0",
|
||||
"react-native-gesture-handler": "~2.16.2",
|
||||
"react-native-gesture-handler": "~2.14.0",
|
||||
"react-native-get-random-values": "~1.11.0",
|
||||
"react-native-image-crop-picker": "0.40.3",
|
||||
"react-native-image-crop-picker": "^0.38.1",
|
||||
"react-native-ios-context-menu": "^1.15.3",
|
||||
"react-native-keyboard-controller": "^1.12.1",
|
||||
"react-native-pager-view": "6.2.3",
|
||||
"react-native-picker-select": "^9.1.3",
|
||||
"react-native-picker-select": "^8.1.0",
|
||||
"react-native-progress": "bluesky-social/react-native-progress",
|
||||
"react-native-reanimated": "^3.11.0",
|
||||
"react-native-reanimated": "^3.6.0",
|
||||
"react-native-root-siblings": "^4.1.1",
|
||||
"react-native-safe-area-context": "4.10.1",
|
||||
"react-native-screens": "~3.31.1",
|
||||
"react-native-svg": "^15.3.0",
|
||||
"react-native-uitextview": "^1.1.7",
|
||||
"react-native-safe-area-context": "4.8.2",
|
||||
"react-native-screens": "~3.29.0",
|
||||
"react-native-svg": "14.1.0",
|
||||
"react-native-uitextview": "^1.1.6",
|
||||
"react-native-url-polyfill": "^1.3.0",
|
||||
"react-native-uuid": "^2.0.2",
|
||||
"react-native-uuid": "^2.0.1",
|
||||
"react-native-view-shot": "^3.8.0",
|
||||
"react-native-web": "~0.19.11",
|
||||
"react-native-web": "~0.19.6",
|
||||
"react-native-web-webview": "^1.0.2",
|
||||
"react-native-webview": "13.10.2",
|
||||
"react-native-webview": "13.6.4",
|
||||
"react-responsive": "^9.0.2",
|
||||
"react-textarea-autosize": "^8.5.3",
|
||||
"rn-fetch-blob": "^0.12.0",
|
||||
@@ -211,7 +208,7 @@
|
||||
"@lingui/macro": "^4.5.0",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.11",
|
||||
"@react-native-community/eslint-config": "^3.0.0",
|
||||
"@react-native/typescript-config": "^0.74.1",
|
||||
"@react-native/typescript-config": "^0.74.0",
|
||||
"@testing-library/jest-native": "^5.4.1",
|
||||
"@testing-library/react-native": "^11.5.2",
|
||||
"@tsconfig/react-native": "^2.0.3",
|
||||
@@ -237,7 +234,6 @@
|
||||
"babel-loader": "^9.1.2",
|
||||
"babel-plugin-macros": "^3.1.0",
|
||||
"babel-plugin-module-resolver": "^5.0.0",
|
||||
"babel-plugin-react-compiler": "^0.0.0-experimental-592953e-20240517",
|
||||
"babel-plugin-react-native-web": "^0.18.12",
|
||||
"babel-preset-expo": "^10.0.0",
|
||||
"eslint": "^8.19.0",
|
||||
@@ -245,7 +241,6 @@
|
||||
"eslint-plugin-ft-flow": "^2.0.3",
|
||||
"eslint-plugin-lingui": "^0.2.0",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-compiler": "^0.0.0-experimental-c8b3f72-20240517",
|
||||
"eslint-plugin-react-native-a11y": "^3.3.0",
|
||||
"eslint-plugin-simple-import-sort": "^12.0.0",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
@@ -255,7 +250,7 @@
|
||||
"jest-expo": "^50.0.1",
|
||||
"jest-junit": "^15.0.0",
|
||||
"lint-staged": "^13.2.3",
|
||||
"metro-react-native-babel-preset": "^0.74.1",
|
||||
"metro-react-native-babel-preset": "^0.73.7",
|
||||
"prettier": "^2.8.3",
|
||||
"react-native-dotenv": "^3.3.1",
|
||||
"react-refresh": "^0.14.0",
|
||||
@@ -271,8 +266,7 @@
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "^18",
|
||||
"**/zeed-dom": "0.10.9",
|
||||
"@react-native/babel-preset": "0.74.1"
|
||||
"**/zeed-dom": "0.10.9"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "jest-expo/ios",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,16 +0,0 @@
|
||||
diff --git a/node_modules/@mattermost/react-native-paste-input/ios/PasteInputView.m b/node_modules/@mattermost/react-native-paste-input/ios/PasteInputView.m
|
||||
index e916023..0564d97 100644
|
||||
--- a/node_modules/@mattermost/react-native-paste-input/ios/PasteInputView.m
|
||||
+++ b/node_modules/@mattermost/react-native-paste-input/ios/PasteInputView.m
|
||||
@@ -22,6 +22,11 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge
|
||||
_backedTextInputView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
||||
_backedTextInputView.textInputDelegate = self;
|
||||
|
||||
+ // Disable inline predictions to prevent jank in the composer
|
||||
+ if (@available(iOS 17.0, *)) {
|
||||
+ _backedTextInputView.inlinePredictionType = UITextInlinePredictionTypeNo;
|
||||
+ }
|
||||
+
|
||||
[self addSubview:_backedTextInputView];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
diff --git a/node_modules/@react-navigation/native/lib/commonjs/useLinking.js b/node_modules/@react-navigation/native/lib/commonjs/useLinking.js
|
||||
index ef4f368..2b0da35 100644
|
||||
--- a/node_modules/@react-navigation/native/lib/commonjs/useLinking.js
|
||||
+++ b/node_modules/@react-navigation/native/lib/commonjs/useLinking.js
|
||||
@@ -273,8 +273,12 @@ function useLinking(ref, _ref) {
|
||||
});
|
||||
const currentIndex = history.index;
|
||||
try {
|
||||
- if (nextIndex !== -1 && nextIndex < currentIndex) {
|
||||
- // An existing entry for this path exists and it's less than current index, go back to that
|
||||
+ if (
|
||||
+ nextIndex !== -1 &&
|
||||
+ nextIndex < currentIndex &&
|
||||
+ // We should only go back if the entry exists and it's less than current index
|
||||
+ history.get(nextIndex - currentIndex)
|
||||
+ ) { // An existing entry for this path exists and it's less than current index, go back to that
|
||||
await history.go(nextIndex - currentIndex);
|
||||
} else {
|
||||
// We couldn't find an existing entry to go back to, so we'll go back by the delta
|
||||
diff --git a/node_modules/@react-navigation/native/lib/module/useLinking.js b/node_modules/@react-navigation/native/lib/module/useLinking.js
|
||||
index 62a3b43..11a5a28 100644
|
||||
--- a/node_modules/@react-navigation/native/lib/module/useLinking.js
|
||||
+++ b/node_modules/@react-navigation/native/lib/module/useLinking.js
|
||||
@@ -264,8 +264,12 @@ export default function useLinking(ref, _ref) {
|
||||
});
|
||||
const currentIndex = history.index;
|
||||
try {
|
||||
- if (nextIndex !== -1 && nextIndex < currentIndex) {
|
||||
- // An existing entry for this path exists and it's less than current index, go back to that
|
||||
+ if (
|
||||
+ nextIndex !== -1 &&
|
||||
+ nextIndex < currentIndex &&
|
||||
+ // We should only go back if the entry exists and it's less than current index
|
||||
+ history.get(nextIndex - currentIndex)
|
||||
+ ) { // An existing entry for this path exists and it's less than current index, go back to that
|
||||
await history.go(nextIndex - currentIndex);
|
||||
} else {
|
||||
// We couldn't find an existing entry to go back to, so we'll go back by the delta
|
||||
diff --git a/node_modules/@react-navigation/native/src/useLinking.tsx b/node_modules/@react-navigation/native/src/useLinking.tsx
|
||||
index 3db40b7..9ba4ecd 100644
|
||||
--- a/node_modules/@react-navigation/native/src/useLinking.tsx
|
||||
+++ b/node_modules/@react-navigation/native/src/useLinking.tsx
|
||||
@@ -381,7 +381,12 @@ export default function useLinking(
|
||||
const currentIndex = history.index;
|
||||
|
||||
try {
|
||||
- if (nextIndex !== -1 && nextIndex < currentIndex) {
|
||||
+ if (
|
||||
+ nextIndex !== -1 &&
|
||||
+ nextIndex < currentIndex &&
|
||||
+ // We should only go back if the entry exists and it's less than current index
|
||||
+ history.get(nextIndex - currentIndex)
|
||||
+ ) {
|
||||
// An existing entry for this path exists and it's less than current index, go back to that
|
||||
await history.go(nextIndex - currentIndex);
|
||||
} else {
|
||||
@@ -0,0 +1,5 @@
|
||||
# React Navigation history bug patch
|
||||
|
||||
This patches react-navigation to fix the issues in https://github.com/bluesky-social/social-app/issues/710.
|
||||
|
||||
This is based on the PR found at https://github.com/react-navigation/react-navigation/pull/11833
|
||||
@@ -0,0 +1,112 @@
|
||||
diff --git a/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/ImagePickerModule.kt b/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/ImagePickerModule.kt
|
||||
index 3f50f8c..ee47fa1 100644
|
||||
--- a/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/ImagePickerModule.kt
|
||||
+++ b/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/ImagePickerModule.kt
|
||||
@@ -33,7 +33,9 @@ import kotlin.coroutines.resumeWithException
|
||||
// TODO(@bbarthec): rename to ExpoImagePicker
|
||||
private const val moduleName = "ExponentImagePicker"
|
||||
|
||||
+
|
||||
class ImagePickerModule : Module() {
|
||||
+ private var isPickerOpen = false
|
||||
|
||||
override fun definition() = ModuleDefinition {
|
||||
Name(moduleName)
|
||||
@@ -129,6 +131,11 @@ class ImagePickerModule : Module() {
|
||||
options: ImagePickerOptions
|
||||
): Any {
|
||||
return try {
|
||||
+ if(isPickerOpen) {
|
||||
+ return ImagePickerResponse(canceled = true)
|
||||
+ }
|
||||
+
|
||||
+ isPickerOpen = true
|
||||
var result = launchPicker(pickerLauncher)
|
||||
if (
|
||||
!options.allowsMultipleSelection &&
|
||||
@@ -143,6 +150,8 @@ class ImagePickerModule : Module() {
|
||||
mediaHandler.readExtras(result.data, options)
|
||||
} catch (cause: OperationCanceledException) {
|
||||
return ImagePickerResponse(canceled = true)
|
||||
+ } finally {
|
||||
+ isPickerOpen = false
|
||||
}
|
||||
}
|
||||
|
||||
diff --git a/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/contracts/ImageLibraryContract.kt b/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/contracts/ImageLibraryContract.kt
|
||||
index ff15c91..9763012 100644
|
||||
--- a/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/contracts/ImageLibraryContract.kt
|
||||
+++ b/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/contracts/ImageLibraryContract.kt
|
||||
@@ -5,12 +5,7 @@ import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
-import androidx.activity.result.PickVisualMediaRequest
|
||||
-import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia
|
||||
-import androidx.activity.result.contract.ActivityResultContracts.PickMultipleVisualMedia
|
||||
import expo.modules.imagepicker.ImagePickerOptions
|
||||
-import expo.modules.imagepicker.MediaTypes
|
||||
-import expo.modules.imagepicker.UNLIMITED_SELECTION
|
||||
import expo.modules.imagepicker.getAllDataUris
|
||||
import expo.modules.imagepicker.toMediaType
|
||||
import expo.modules.kotlin.activityresult.AppContextActivityResultContract
|
||||
@@ -26,51 +21,26 @@ import java.io.Serializable
|
||||
* @see [androidx.activity.result.contract.ActivityResultContracts.GetMultipleContents]
|
||||
*/
|
||||
internal class ImageLibraryContract(
|
||||
- private val appContextProvider: AppContextProvider
|
||||
+ private val appContextProvider: AppContextProvider,
|
||||
) : AppContextActivityResultContract<ImageLibraryContractOptions, ImagePickerContractResult> {
|
||||
private val contentResolver: ContentResolver
|
||||
get() = appContextProvider.appContext.reactContext?.contentResolver
|
||||
?: throw Exceptions.ReactContextLost()
|
||||
|
||||
override fun createIntent(context: Context, input: ImageLibraryContractOptions): Intent {
|
||||
- val request = PickVisualMediaRequest.Builder()
|
||||
- .setMediaType(
|
||||
- when (input.options.mediaTypes) {
|
||||
- MediaTypes.VIDEOS -> {
|
||||
- PickVisualMedia.VideoOnly
|
||||
- }
|
||||
-
|
||||
- MediaTypes.IMAGES -> {
|
||||
- PickVisualMedia.ImageOnly
|
||||
- }
|
||||
-
|
||||
- else -> {
|
||||
- PickVisualMedia.ImageAndVideo
|
||||
- }
|
||||
- }
|
||||
- )
|
||||
- .build()
|
||||
+ val intent = Intent(Intent.ACTION_GET_CONTENT)
|
||||
+ .addCategory(Intent.CATEGORY_OPENABLE)
|
||||
+ .setType("image/*")
|
||||
|
||||
if (input.options.allowsMultipleSelection) {
|
||||
- val selectionLimit = input.options.selectionLimit
|
||||
-
|
||||
- if (selectionLimit == 1) {
|
||||
- // If multiple selection is allowed but the limit is 1, we should ignore
|
||||
- // the multiple selection flag and just treat it as a single selection.
|
||||
- return PickVisualMedia().createIntent(context, request)
|
||||
+ if(input.options.selectionLimit == 1) {
|
||||
+ return intent
|
||||
}
|
||||
|
||||
- if (selectionLimit > 1) {
|
||||
- return PickMultipleVisualMedia(selectionLimit).createIntent(context, request)
|
||||
- }
|
||||
-
|
||||
- // If the selection limit is 0, it is the same as unlimited selection.
|
||||
- if (selectionLimit == UNLIMITED_SELECTION) {
|
||||
- return PickMultipleVisualMedia().createIntent(context, request)
|
||||
- }
|
||||
+ intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
|
||||
}
|
||||
|
||||
- return PickVisualMedia().createIntent(context, request)
|
||||
+ return intent
|
||||
}
|
||||
|
||||
override fun parseResult(input: ImageLibraryContractOptions, resultCode: Int, intent: Intent?) =
|
||||
@@ -0,0 +1,3 @@
|
||||
added by https://github.com/bluesky-social/social-app/pull/2384#pullrequestreview-1800985521
|
||||
|
||||
hackfixes the image picker on android so that the user can select from their typical image sources
|
||||
@@ -1,8 +1,8 @@
|
||||
diff --git a/node_modules/expo-notifications/android/build.gradle b/node_modules/expo-notifications/android/build.gradle
|
||||
index d233e1f..cc2f856 100644
|
||||
index 97bf4f4..6e9d427 100644
|
||||
--- a/node_modules/expo-notifications/android/build.gradle
|
||||
+++ b/node_modules/expo-notifications/android/build.gradle
|
||||
@@ -32,6 +32,7 @@ dependencies {
|
||||
@@ -118,6 +118,7 @@ dependencies {
|
||||
api 'com.google.firebase:firebase-messaging:22.0.0'
|
||||
|
||||
api 'me.leolin:ShortcutBadger:1.1.22@aar'
|
||||
@@ -46,7 +46,7 @@ index 0af7fe0..8f2c8d8 100644
|
||||
try {
|
||||
return payload.getString(TITLE_KEY);
|
||||
diff --git a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationContent.java b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationContent.java
|
||||
index f1fed19..80afe9e 100644
|
||||
index f1fed19..1619f59 100644
|
||||
--- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationContent.java
|
||||
+++ b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationContent.java
|
||||
@@ -20,6 +20,7 @@ import expo.modules.notifications.notifications.enums.NotificationPriority;
|
||||
@@ -57,19 +57,17 @@ index f1fed19..80afe9e 100644
|
||||
private String mTitle;
|
||||
private String mText;
|
||||
private String mSubtitle;
|
||||
@@ -50,6 +51,11 @@ public class NotificationContent implements Parcelable, Serializable {
|
||||
@@ -50,6 +51,9 @@ public class NotificationContent implements Parcelable, Serializable {
|
||||
}
|
||||
};
|
||||
|
||||
+ @Nullable
|
||||
+ public String getChannelId() {
|
||||
+ return mTitle;
|
||||
+ }
|
||||
+ public String getChannelId() { return mChannelId; }
|
||||
+
|
||||
@Nullable
|
||||
public String getTitle() {
|
||||
return mTitle;
|
||||
@@ -121,6 +127,7 @@ public class NotificationContent implements Parcelable, Serializable {
|
||||
@@ -121,6 +125,7 @@ public class NotificationContent implements Parcelable, Serializable {
|
||||
}
|
||||
|
||||
protected NotificationContent(Parcel in) {
|
||||
@@ -77,7 +75,7 @@ index f1fed19..80afe9e 100644
|
||||
mTitle = in.readString();
|
||||
mText = in.readString();
|
||||
mSubtitle = in.readString();
|
||||
@@ -146,6 +153,7 @@ public class NotificationContent implements Parcelable, Serializable {
|
||||
@@ -146,6 +151,7 @@ public class NotificationContent implements Parcelable, Serializable {
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
@@ -85,7 +83,7 @@ index f1fed19..80afe9e 100644
|
||||
dest.writeString(mTitle);
|
||||
dest.writeString(mText);
|
||||
dest.writeString(mSubtitle);
|
||||
@@ -166,6 +174,7 @@ public class NotificationContent implements Parcelable, Serializable {
|
||||
@@ -166,6 +172,7 @@ public class NotificationContent implements Parcelable, Serializable {
|
||||
private static final long serialVersionUID = 397666843266836802L;
|
||||
|
||||
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
|
||||
@@ -93,7 +91,7 @@ index f1fed19..80afe9e 100644
|
||||
out.writeObject(mTitle);
|
||||
out.writeObject(mText);
|
||||
out.writeObject(mSubtitle);
|
||||
@@ -190,6 +199,7 @@ public class NotificationContent implements Parcelable, Serializable {
|
||||
@@ -190,6 +197,7 @@ public class NotificationContent implements Parcelable, Serializable {
|
||||
}
|
||||
|
||||
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
|
||||
@@ -101,7 +99,7 @@ index f1fed19..80afe9e 100644
|
||||
mTitle = (String) in.readObject();
|
||||
mText = (String) in.readObject();
|
||||
mSubtitle = (String) in.readObject();
|
||||
@@ -240,6 +250,7 @@ public class NotificationContent implements Parcelable, Serializable {
|
||||
@@ -240,6 +248,7 @@ public class NotificationContent implements Parcelable, Serializable {
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
@@ -109,7 +107,7 @@ index f1fed19..80afe9e 100644
|
||||
private String mTitle;
|
||||
private String mText;
|
||||
private String mSubtitle;
|
||||
@@ -260,6 +271,11 @@ public class NotificationContent implements Parcelable, Serializable {
|
||||
@@ -260,6 +269,11 @@ public class NotificationContent implements Parcelable, Serializable {
|
||||
useDefaultVibrationPattern();
|
||||
}
|
||||
|
||||
@@ -121,7 +119,7 @@ index f1fed19..80afe9e 100644
|
||||
public Builder setTitle(String title) {
|
||||
mTitle = title;
|
||||
return this;
|
||||
@@ -336,6 +352,7 @@ public class NotificationContent implements Parcelable, Serializable {
|
||||
@@ -336,6 +350,7 @@ public class NotificationContent implements Parcelable, Serializable {
|
||||
|
||||
public NotificationContent build() {
|
||||
NotificationContent content = new NotificationContent();
|
||||
@@ -130,10 +128,18 @@ index f1fed19..80afe9e 100644
|
||||
content.mSubtitle = mSubtitle;
|
||||
content.mText = mText;
|
||||
diff --git a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/ExpoNotificationBuilder.java b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/ExpoNotificationBuilder.java
|
||||
index 6bd9928..ee93d70 100644
|
||||
index 6bd9928..aab71ea 100644
|
||||
--- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/ExpoNotificationBuilder.java
|
||||
+++ b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/ExpoNotificationBuilder.java
|
||||
@@ -48,6 +48,10 @@ public class ExpoNotificationBuilder extends ChannelAwareNotificationBuilder {
|
||||
@@ -7,7 +7,6 @@ import android.content.pm.PackageManager;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
-import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
import android.provider.Settings;
|
||||
@@ -48,6 +47,10 @@ public class ExpoNotificationBuilder extends ChannelAwareNotificationBuilder {
|
||||
|
||||
NotificationContent content = getNotificationContent();
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
diff --git a/node_modules/expo-updates/ios/EXUpdates/Update/ExpoUpdatesUpdate.swift b/node_modules/expo-updates/ios/EXUpdates/Update/ExpoUpdatesUpdate.swift
|
||||
index b85291e..07a5d3c 100644
|
||||
--- a/node_modules/expo-updates/ios/EXUpdates/Update/ExpoUpdatesUpdate.swift
|
||||
+++ b/node_modules/expo-updates/ios/EXUpdates/Update/ExpoUpdatesUpdate.swift
|
||||
@@ -78,13 +78,20 @@ public final class ExpoUpdatesUpdate: Update {
|
||||
status = UpdateStatus.StatusPending
|
||||
diff --git a/node_modules/expo-updates/ios/EXUpdates/Update/NewUpdate.swift b/node_modules/expo-updates/ios/EXUpdates/Update/NewUpdate.swift
|
||||
index 189a5f5..8d5b8e6 100644
|
||||
--- a/node_modules/expo-updates/ios/EXUpdates/Update/NewUpdate.swift
|
||||
+++ b/node_modules/expo-updates/ios/EXUpdates/Update/NewUpdate.swift
|
||||
@@ -68,13 +68,20 @@ public final class NewUpdate: Update {
|
||||
processedAssets.append(asset)
|
||||
}
|
||||
|
||||
|
||||
+ // Instead of relying on various hacks to get the correct format for the specific
|
||||
+ // platform on the backend, we can just add this little patch..
|
||||
+ let dateFormatter = DateFormatter()
|
||||
@@ -23,4 +23,4 @@ index b85291e..07a5d3c 100644
|
||||
+ commitTime: date,
|
||||
runtimeVersion: runtimeVersion,
|
||||
keep: true,
|
||||
status: status,
|
||||
status: UpdateStatus.StatusPending,
|
||||
@@ -4,4 +4,4 @@ This is a small patch to convert timestamp formats that are returned from the ba
|
||||
backend to return the correct format for a specific format (the format required on Android is not the same as on iOS)
|
||||
we can just add this conversion in.
|
||||
|
||||
Don't remove unless we make changes on the backend to support both platforms.
|
||||
Don't remove unless we make changes on the backend to support both platforms.
|
||||
@@ -1,237 +0,0 @@
|
||||
diff --git a/node_modules/react-native-reanimated/src/createAnimatedComponent/commonTypes.ts b/node_modules/react-native-reanimated/src/createAnimatedComponent/commonTypes.ts
|
||||
index 92ebe62..5f8207e 100644
|
||||
--- a/node_modules/react-native-reanimated/src/createAnimatedComponent/commonTypes.ts
|
||||
+++ b/node_modules/react-native-reanimated/src/createAnimatedComponent/commonTypes.ts
|
||||
@@ -96,7 +96,8 @@ export interface AnimatedComponentRef extends Component {
|
||||
export interface IAnimatedComponentInternal {
|
||||
_styles: StyleProps[] | null;
|
||||
_animatedProps?: Partial<AnimatedComponentProps<AnimatedProps>>;
|
||||
- _viewTag: number;
|
||||
+ _componentViewTag: number;
|
||||
+ _eventViewTag: number;
|
||||
_isFirstRender: boolean;
|
||||
jestAnimatedStyle: { value: StyleProps };
|
||||
_component: AnimatedComponentRef | HTMLElement | null;
|
||||
diff --git a/node_modules/react-native-reanimated/src/createAnimatedComponent/createAnimatedComponent.tsx b/node_modules/react-native-reanimated/src/createAnimatedComponent/createAnimatedComponent.tsx
|
||||
index 88b3fdf..2488ebc 100644
|
||||
--- a/node_modules/react-native-reanimated/src/createAnimatedComponent/createAnimatedComponent.tsx
|
||||
+++ b/node_modules/react-native-reanimated/src/createAnimatedComponent/createAnimatedComponent.tsx
|
||||
@@ -122,7 +122,8 @@ export function createAnimatedComponent(
|
||||
{
|
||||
_styles: StyleProps[] | null = null;
|
||||
_animatedProps?: Partial<AnimatedComponentProps<AnimatedProps>>;
|
||||
- _viewTag = -1;
|
||||
+ _componentViewTag = -1;
|
||||
+ _eventViewTag = -1;
|
||||
_isFirstRender = true;
|
||||
jestAnimatedStyle: { value: StyleProps } = { value: {} };
|
||||
_component: AnimatedComponentRef | HTMLElement | null = null;
|
||||
@@ -143,7 +144,8 @@ export function createAnimatedComponent(
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
- this._viewTag = this._getViewInfo().viewTag as number;
|
||||
+ this._setComponentViewTag();
|
||||
+ this._setEventViewTag();
|
||||
this._attachNativeEvents();
|
||||
this._jsPropsUpdater.addOnJSPropsChangeListener(this);
|
||||
this._attachAnimatedStyles();
|
||||
@@ -185,7 +187,10 @@ export function createAnimatedComponent(
|
||||
if (this.props.sharedTransitionTag) {
|
||||
this._configureSharedTransition(true);
|
||||
}
|
||||
- this._sharedElementTransition?.unregisterTransition(this._viewTag, true);
|
||||
+ this._sharedElementTransition?.unregisterTransition(
|
||||
+ this._componentViewTag,
|
||||
+ true
|
||||
+ );
|
||||
|
||||
const exiting = this.props.exiting;
|
||||
if (
|
||||
@@ -209,7 +214,7 @@ export function createAnimatedComponent(
|
||||
: getReduceMotionFromConfig();
|
||||
if (!reduceMotionInExiting) {
|
||||
updateLayoutAnimations(
|
||||
- this._viewTag,
|
||||
+ this._componentViewTag,
|
||||
LayoutAnimationType.EXITING,
|
||||
maybeBuild(
|
||||
exiting,
|
||||
@@ -221,12 +226,22 @@ export function createAnimatedComponent(
|
||||
}
|
||||
}
|
||||
|
||||
- _getEventViewRef() {
|
||||
- // Make sure to get the scrollable node for components that implement
|
||||
- // `ScrollResponder.Mixin`.
|
||||
- return (this._component as AnimatedComponentRef)?.getScrollableNode
|
||||
- ? (this._component as AnimatedComponentRef).getScrollableNode?.()
|
||||
- : this._component;
|
||||
+ _setComponentViewTag() {
|
||||
+ this._componentViewTag = this._getViewInfo().viewTag as number;
|
||||
+ }
|
||||
+
|
||||
+ _setEventViewTag() {
|
||||
+ // Setting the tag for registering events - since the event emitting view can be nested inside the main component
|
||||
+ const componentAnimatedRef = this._component as AnimatedComponentRef;
|
||||
+ if (componentAnimatedRef.getScrollableNode) {
|
||||
+ const scrollableNode = componentAnimatedRef.getScrollableNode();
|
||||
+ this._eventViewTag = findNodeHandle(scrollableNode) ?? -1;
|
||||
+ } else {
|
||||
+ this._eventViewTag =
|
||||
+ findNodeHandle(
|
||||
+ options?.setNativeProps ? this : componentAnimatedRef
|
||||
+ ) ?? -1;
|
||||
+ }
|
||||
}
|
||||
|
||||
_attachNativeEvents() {
|
||||
@@ -236,7 +251,7 @@ export function createAnimatedComponent(
|
||||
has('workletEventHandler', prop) &&
|
||||
prop.workletEventHandler instanceof WorkletEventHandler
|
||||
) {
|
||||
- prop.workletEventHandler.registerForEvents(this._viewTag, key);
|
||||
+ prop.workletEventHandler.registerForEvents(this._eventViewTag, key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -248,7 +263,7 @@ export function createAnimatedComponent(
|
||||
has('workletEventHandler', prop) &&
|
||||
prop.workletEventHandler instanceof WorkletEventHandler
|
||||
) {
|
||||
- prop.workletEventHandler.unregisterFromEvents(this._viewTag);
|
||||
+ prop.workletEventHandler.unregisterFromEvents(this._eventViewTag);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -258,15 +273,17 @@ export function createAnimatedComponent(
|
||||
for (const style of this._styles) {
|
||||
style.viewsRef.remove(this);
|
||||
}
|
||||
- } else if (this._viewTag !== -1 && this._styles !== null) {
|
||||
+ } else if (this._componentViewTag !== -1 && this._styles !== null) {
|
||||
for (const style of this._styles) {
|
||||
- style.viewDescriptors.remove(this._viewTag);
|
||||
+ style.viewDescriptors.remove(this._componentViewTag);
|
||||
}
|
||||
if (this.props.animatedProps?.viewDescriptors) {
|
||||
- this.props.animatedProps.viewDescriptors.remove(this._viewTag);
|
||||
+ this.props.animatedProps.viewDescriptors.remove(
|
||||
+ this._componentViewTag
|
||||
+ );
|
||||
}
|
||||
if (isFabric()) {
|
||||
- removeFromPropsRegistry(this._viewTag);
|
||||
+ removeFromPropsRegistry(this._componentViewTag);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -283,15 +300,19 @@ export function createAnimatedComponent(
|
||||
const newProp = this.props[key];
|
||||
if (!newProp) {
|
||||
// Prop got deleted
|
||||
- prevProp.workletEventHandler.unregisterFromEvents(this._viewTag);
|
||||
+ prevProp.workletEventHandler.unregisterFromEvents(
|
||||
+ this._eventViewTag
|
||||
+ );
|
||||
} else if (
|
||||
has('workletEventHandler', newProp) &&
|
||||
newProp.workletEventHandler instanceof WorkletEventHandler &&
|
||||
newProp.workletEventHandler !== prevProp.workletEventHandler
|
||||
) {
|
||||
// Prop got changed
|
||||
- prevProp.workletEventHandler.unregisterFromEvents(this._viewTag);
|
||||
- newProp.workletEventHandler.registerForEvents(this._viewTag);
|
||||
+ prevProp.workletEventHandler.unregisterFromEvents(
|
||||
+ this._eventViewTag
|
||||
+ );
|
||||
+ newProp.workletEventHandler.registerForEvents(this._eventViewTag);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -304,7 +325,7 @@ export function createAnimatedComponent(
|
||||
!prevProps[key]
|
||||
) {
|
||||
// Prop got added
|
||||
- newProp.workletEventHandler.registerForEvents(this._viewTag);
|
||||
+ newProp.workletEventHandler.registerForEvents(this._eventViewTag);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -381,7 +402,7 @@ export function createAnimatedComponent(
|
||||
adaptViewConfig(viewConfig);
|
||||
}
|
||||
|
||||
- this._viewTag = viewTag as number;
|
||||
+ this._componentViewTag = viewTag as number;
|
||||
|
||||
// remove old styles
|
||||
if (prevStyles) {
|
||||
@@ -487,7 +508,11 @@ export function createAnimatedComponent(
|
||||
AnimatedComponent.displayName
|
||||
)
|
||||
: undefined;
|
||||
- updateLayoutAnimations(this._viewTag, LayoutAnimationType.LAYOUT, layout);
|
||||
+ updateLayoutAnimations(
|
||||
+ this._componentViewTag,
|
||||
+ LayoutAnimationType.LAYOUT,
|
||||
+ layout
|
||||
+ );
|
||||
}
|
||||
|
||||
_configureSharedTransition(isUnmounting = false) {
|
||||
@@ -497,7 +522,7 @@ export function createAnimatedComponent(
|
||||
const { sharedTransitionTag } = this.props;
|
||||
if (!sharedTransitionTag) {
|
||||
this._sharedElementTransition?.unregisterTransition(
|
||||
- this._viewTag,
|
||||
+ this._componentViewTag,
|
||||
isUnmounting
|
||||
);
|
||||
this._sharedElementTransition = null;
|
||||
@@ -508,7 +533,7 @@ export function createAnimatedComponent(
|
||||
this._sharedElementTransition ??
|
||||
new SharedTransition();
|
||||
sharedElementTransition.registerTransition(
|
||||
- this._viewTag,
|
||||
+ this._componentViewTag,
|
||||
sharedTransitionTag,
|
||||
isUnmounting
|
||||
);
|
||||
@@ -527,7 +552,7 @@ export function createAnimatedComponent(
|
||||
? (ref as HTMLElement)
|
||||
: findNodeHandle(ref as Component);
|
||||
|
||||
- this._viewTag = tag as number;
|
||||
+ this._componentViewTag = tag as number;
|
||||
|
||||
const { layout, entering, exiting, sharedTransitionTag } = this.props;
|
||||
if (
|
||||
diff --git a/node_modules/react-native-reanimated/lib/module/reanimated2/index.js b/node_modules/react-native-reanimated/lib/module/reanimated2/index.js
|
||||
index ac9be5d..86d4605 100644
|
||||
--- a/node_modules/react-native-reanimated/lib/module/reanimated2/index.js
|
||||
+++ b/node_modules/react-native-reanimated/lib/module/reanimated2/index.js
|
||||
@@ -47,4 +47,5 @@ export { LayoutAnimationConfig } from './component/LayoutAnimationConfig';
|
||||
export { PerformanceMonitor } from './component/PerformanceMonitor';
|
||||
export { startMapper, stopMapper } from './mappers';
|
||||
export { startScreenTransition, finishScreenTransition, ScreenTransition } from './screenTransition';
|
||||
+export { isReducedMotion } from './PlatformChecker';
|
||||
//# sourceMappingURL=index.js.map
|
||||
diff --git a/node_modules/react-native-reanimated/lib/typescript/reanimated2/index.d.ts b/node_modules/react-native-reanimated/lib/typescript/reanimated2/index.d.ts
|
||||
index f01dc57..161ef22 100644
|
||||
--- a/node_modules/react-native-reanimated/lib/typescript/reanimated2/index.d.ts
|
||||
+++ b/node_modules/react-native-reanimated/lib/typescript/reanimated2/index.d.ts
|
||||
@@ -36,3 +36,4 @@ export type { FlatListPropsWithLayout } from './component/FlatList';
|
||||
export { startMapper, stopMapper } from './mappers';
|
||||
export { startScreenTransition, finishScreenTransition, ScreenTransition, } from './screenTransition';
|
||||
export type { AnimatedScreenTransition, GoBackGesture, ScreenTransitionConfig, } from './screenTransition';
|
||||
+export { isReducedMotion } from './PlatformChecker';
|
||||
diff --git a/node_modules/react-native-reanimated/src/reanimated2/index.ts b/node_modules/react-native-reanimated/src/reanimated2/index.ts
|
||||
index 5885fa1..a3c693f 100644
|
||||
--- a/node_modules/react-native-reanimated/src/reanimated2/index.ts
|
||||
+++ b/node_modules/react-native-reanimated/src/reanimated2/index.ts
|
||||
@@ -284,3 +284,4 @@ export type {
|
||||
GoBackGesture,
|
||||
ScreenTransitionConfig,
|
||||
} from './screenTransition';
|
||||
+export { isReducedMotion } from './PlatformChecker';
|
||||
@@ -0,0 +1,30 @@
|
||||
diff --git a/node_modules/react-native-reanimated/lib/module/reanimated2/index.js b/node_modules/react-native-reanimated/lib/module/reanimated2/index.js
|
||||
index 91e49f4..c10d3fc 100644
|
||||
--- a/node_modules/react-native-reanimated/lib/module/reanimated2/index.js
|
||||
+++ b/node_modules/react-native-reanimated/lib/module/reanimated2/index.js
|
||||
@@ -45,4 +45,5 @@ export { getUseOfValueInStyleWarning } from './pluginUtils';
|
||||
export { withReanimatedTimer, advanceAnimationByTime, advanceAnimationByFrame, setUpTests, getAnimatedStyle } from './jestUtils';
|
||||
export { LayoutAnimationConfig } from './component/LayoutAnimationConfig';
|
||||
export { startMapper, stopMapper } from './mappers';
|
||||
+export { isReducedMotion } from './PlatformChecker';
|
||||
//# sourceMappingURL=index.js.map
|
||||
\ No newline at end of file
|
||||
diff --git a/node_modules/react-native-reanimated/lib/typescript/reanimated2/index.d.ts b/node_modules/react-native-reanimated/lib/typescript/reanimated2/index.d.ts
|
||||
index 96bd913..ad63a09 100644
|
||||
--- a/node_modules/react-native-reanimated/lib/typescript/reanimated2/index.d.ts
|
||||
+++ b/node_modules/react-native-reanimated/lib/typescript/reanimated2/index.d.ts
|
||||
@@ -33,3 +33,4 @@ export type { Adaptable, AdaptTransforms, AnimateProps, AnimatedProps, AnimatedT
|
||||
export type { AnimatedScrollViewProps } from './component/ScrollView';
|
||||
export type { FlatListPropsWithLayout } from './component/FlatList';
|
||||
export { startMapper, stopMapper } from './mappers';
|
||||
+export { isReducedMotion } from './PlatformChecker';
|
||||
diff --git a/node_modules/react-native-reanimated/src/reanimated2/index.ts b/node_modules/react-native-reanimated/src/reanimated2/index.ts
|
||||
index 096dc05..38fc01d 100644
|
||||
--- a/node_modules/react-native-reanimated/src/reanimated2/index.ts
|
||||
+++ b/node_modules/react-native-reanimated/src/reanimated2/index.ts
|
||||
@@ -271,3 +271,4 @@ export type {
|
||||
export type { AnimatedScrollViewProps } from './component/ScrollView';
|
||||
export type { FlatListPropsWithLayout } from './component/FlatList';
|
||||
export { startMapper, stopMapper } from './mappers';
|
||||
+export { isReducedMotion } from './PlatformChecker';
|
||||
\ No newline at end of file
|
||||
@@ -1,17 +0,0 @@
|
||||
const {withAndroidManifest} = require('expo/config-plugins')
|
||||
|
||||
module.exports = function withAndroidManifestLaunchModePlugin(appConfig) {
|
||||
return withAndroidManifest(appConfig, function (decoratedAppConfig) {
|
||||
try {
|
||||
const mainApplication =
|
||||
decoratedAppConfig.modResults.manifest.application[0]
|
||||
const mainActivity = mainApplication.activity.find(
|
||||
elem => elem.$['android:name'] === '.MainActivity',
|
||||
)
|
||||
mainActivity.$['android:launchMode'] = 'singleTop'
|
||||
} catch (e) {
|
||||
console.error(`withAndroidManifestLaunchModePlugin failed`, e)
|
||||
}
|
||||
return decoratedAppConfig
|
||||
})
|
||||
}
|
||||
+21
-24
@@ -4,7 +4,6 @@ import 'view/icons'
|
||||
|
||||
import React, {useEffect, useState} from 'react'
|
||||
import {GestureHandlerRootView} from 'react-native-gesture-handler'
|
||||
import {KeyboardProvider} from 'react-native-keyboard-controller'
|
||||
import {RootSiblingParent} from 'react-native-root-siblings'
|
||||
import {
|
||||
initialWindowMetrics,
|
||||
@@ -143,29 +142,27 @@ function App() {
|
||||
* that is set up in the InnerApp component above.
|
||||
*/
|
||||
return (
|
||||
<KeyboardProvider enabled={true} statusBarTranslucent={true}>
|
||||
<SessionProvider>
|
||||
<ShellStateProvider>
|
||||
<PrefsStateProvider>
|
||||
<MutedThreadsProvider>
|
||||
<InvitesStateProvider>
|
||||
<ModalStateProvider>
|
||||
<DialogStateProvider>
|
||||
<LightboxStateProvider>
|
||||
<I18nProvider>
|
||||
<PortalProvider>
|
||||
<InnerApp />
|
||||
</PortalProvider>
|
||||
</I18nProvider>
|
||||
</LightboxStateProvider>
|
||||
</DialogStateProvider>
|
||||
</ModalStateProvider>
|
||||
</InvitesStateProvider>
|
||||
</MutedThreadsProvider>
|
||||
</PrefsStateProvider>
|
||||
</ShellStateProvider>
|
||||
</SessionProvider>
|
||||
</KeyboardProvider>
|
||||
<SessionProvider>
|
||||
<ShellStateProvider>
|
||||
<PrefsStateProvider>
|
||||
<MutedThreadsProvider>
|
||||
<InvitesStateProvider>
|
||||
<ModalStateProvider>
|
||||
<DialogStateProvider>
|
||||
<LightboxStateProvider>
|
||||
<I18nProvider>
|
||||
<PortalProvider>
|
||||
<InnerApp />
|
||||
</PortalProvider>
|
||||
</I18nProvider>
|
||||
</LightboxStateProvider>
|
||||
</DialogStateProvider>
|
||||
</ModalStateProvider>
|
||||
</InvitesStateProvider>
|
||||
</MutedThreadsProvider>
|
||||
</PrefsStateProvider>
|
||||
</ShellStateProvider>
|
||||
</SessionProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+33
-36
@@ -2,7 +2,6 @@ import 'lib/sentry' // must be near top
|
||||
import 'view/icons'
|
||||
|
||||
import React, {useEffect, useState} from 'react'
|
||||
import {KeyboardProvider} from 'react-native-keyboard-controller'
|
||||
import {RootSiblingParent} from 'react-native-root-siblings'
|
||||
import {SafeAreaProvider} from 'react-native-safe-area-context'
|
||||
import {msg} from '@lingui/macro'
|
||||
@@ -79,41 +78,39 @@ function InnerApp() {
|
||||
if (!isReady) return null
|
||||
|
||||
return (
|
||||
<KeyboardProvider enabled={false}>
|
||||
<Alf theme={theme}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<RootSiblingParent>
|
||||
<React.Fragment
|
||||
// Resets the entire tree below when it changes:
|
||||
key={currentAccount?.did}>
|
||||
<QueryProvider currentDid={currentAccount?.did}>
|
||||
<StatsigProvider>
|
||||
<MessagesProvider>
|
||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||
<LabelDefsProvider>
|
||||
<ModerationOptsProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<SafeAreaProvider>
|
||||
<Shell />
|
||||
</SafeAreaProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</ModerationOptsProvider>
|
||||
</LabelDefsProvider>
|
||||
</MessagesProvider>
|
||||
</StatsigProvider>
|
||||
</QueryProvider>
|
||||
</React.Fragment>
|
||||
<ToastContainer />
|
||||
</RootSiblingParent>
|
||||
</ThemeProvider>
|
||||
</Alf>
|
||||
</KeyboardProvider>
|
||||
<Alf theme={theme}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<RootSiblingParent>
|
||||
<React.Fragment
|
||||
// Resets the entire tree below when it changes:
|
||||
key={currentAccount?.did}>
|
||||
<QueryProvider currentDid={currentAccount?.did}>
|
||||
<StatsigProvider>
|
||||
<MessagesProvider>
|
||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||
<LabelDefsProvider>
|
||||
<ModerationOptsProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<SafeAreaProvider>
|
||||
<Shell />
|
||||
</SafeAreaProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</ModerationOptsProvider>
|
||||
</LabelDefsProvider>
|
||||
</MessagesProvider>
|
||||
</StatsigProvider>
|
||||
</QueryProvider>
|
||||
</React.Fragment>
|
||||
<ToastContainer />
|
||||
</RootSiblingParent>
|
||||
</ThemeProvider>
|
||||
</Alf>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+16
-20
@@ -353,11 +353,11 @@ function HomeTabNavigator() {
|
||||
return (
|
||||
<HomeTab.Navigator
|
||||
screenOptions={{
|
||||
animation: isAndroid ? 'ios' : undefined,
|
||||
animationDuration: 285,
|
||||
animation: isAndroid ? 'none' : undefined,
|
||||
gestureEnabled: true,
|
||||
fullScreenGestureEnabled: true,
|
||||
headerShown: false,
|
||||
animationDuration: 250,
|
||||
contentStyle: pal.view,
|
||||
}}>
|
||||
<HomeTab.Screen name="Home" getComponent={() => HomeScreen} />
|
||||
@@ -371,11 +371,11 @@ function SearchTabNavigator() {
|
||||
return (
|
||||
<SearchTab.Navigator
|
||||
screenOptions={{
|
||||
animation: isAndroid ? 'ios' : undefined,
|
||||
animationDuration: 285,
|
||||
animation: isAndroid ? 'none' : undefined,
|
||||
gestureEnabled: true,
|
||||
fullScreenGestureEnabled: true,
|
||||
headerShown: false,
|
||||
animationDuration: 250,
|
||||
contentStyle: pal.view,
|
||||
}}>
|
||||
<SearchTab.Screen name="Search" getComponent={() => SearchScreen} />
|
||||
@@ -389,11 +389,11 @@ function FeedsTabNavigator() {
|
||||
return (
|
||||
<FeedsTab.Navigator
|
||||
screenOptions={{
|
||||
animation: isAndroid ? 'ios' : undefined,
|
||||
animationDuration: 285,
|
||||
animation: isAndroid ? 'none' : undefined,
|
||||
gestureEnabled: true,
|
||||
fullScreenGestureEnabled: true,
|
||||
headerShown: false,
|
||||
animationDuration: 250,
|
||||
contentStyle: pal.view,
|
||||
}}>
|
||||
<FeedsTab.Screen name="Feeds" getComponent={() => FeedsScreen} />
|
||||
@@ -407,11 +407,11 @@ function NotificationsTabNavigator() {
|
||||
return (
|
||||
<NotificationsTab.Navigator
|
||||
screenOptions={{
|
||||
animation: isAndroid ? 'ios' : undefined,
|
||||
animationDuration: 285,
|
||||
animation: isAndroid ? 'none' : undefined,
|
||||
gestureEnabled: true,
|
||||
fullScreenGestureEnabled: true,
|
||||
headerShown: false,
|
||||
animationDuration: 250,
|
||||
contentStyle: pal.view,
|
||||
}}>
|
||||
<NotificationsTab.Screen
|
||||
@@ -429,11 +429,11 @@ function MyProfileTabNavigator() {
|
||||
return (
|
||||
<MyProfileTab.Navigator
|
||||
screenOptions={{
|
||||
animation: isAndroid ? 'ios' : undefined,
|
||||
animationDuration: 285,
|
||||
animation: isAndroid ? 'none' : undefined,
|
||||
gestureEnabled: true,
|
||||
fullScreenGestureEnabled: true,
|
||||
headerShown: false,
|
||||
animationDuration: 250,
|
||||
contentStyle: pal.view,
|
||||
}}>
|
||||
<MyProfileTab.Screen
|
||||
@@ -454,20 +454,17 @@ function MessagesTabNavigator() {
|
||||
return (
|
||||
<MessagesTab.Navigator
|
||||
screenOptions={{
|
||||
animation: isAndroid ? 'ios' : undefined,
|
||||
animationDuration: 285,
|
||||
animation: isAndroid ? 'none' : undefined,
|
||||
gestureEnabled: true,
|
||||
fullScreenGestureEnabled: true,
|
||||
headerShown: false,
|
||||
animationDuration: 250,
|
||||
contentStyle: pal.view,
|
||||
}}>
|
||||
<MessagesTab.Screen
|
||||
name="Messages"
|
||||
getComponent={() => MessagesScreen}
|
||||
options={({route}) => ({
|
||||
requireAuth: true,
|
||||
animationTypeForReplace: route.params?.animation ?? 'push',
|
||||
})}
|
||||
options={{requireAuth: true}}
|
||||
/>
|
||||
{commonScreens(MessagesTab as typeof HomeTab)}
|
||||
</MessagesTab.Navigator>
|
||||
@@ -488,11 +485,10 @@ const FlatNavigator = () => {
|
||||
<Flat.Navigator
|
||||
screenListeners={screenListeners}
|
||||
screenOptions={{
|
||||
animation: isAndroid ? 'ios' : undefined,
|
||||
animationDuration: 285,
|
||||
gestureEnabled: true,
|
||||
fullScreenGestureEnabled: true,
|
||||
headerShown: false,
|
||||
animationDuration: 250,
|
||||
contentStyle: pal.view,
|
||||
}}>
|
||||
<Flat.Screen
|
||||
@@ -612,7 +608,7 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
|
||||
linking={LINKING}
|
||||
theme={theme}
|
||||
onStateChange={() => {
|
||||
logEvent('router:navigate:sampled', {
|
||||
logEvent('router:navigate', {
|
||||
from: prevLoggedRouteName.current,
|
||||
})
|
||||
prevLoggedRouteName.current = getCurrentRouteName()
|
||||
@@ -621,7 +617,7 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
|
||||
attachRouteToLogEvents(getCurrentRouteName)
|
||||
logModuleInitTime()
|
||||
onReady()
|
||||
logEvent('router:navigate:sampled', {})
|
||||
logEvent('router:navigate', {})
|
||||
}}>
|
||||
{children}
|
||||
</NavigationContainer>
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
import React, {useImperativeHandle} from 'react'
|
||||
import {
|
||||
Dimensions,
|
||||
Keyboard,
|
||||
Pressable,
|
||||
StyleProp,
|
||||
View,
|
||||
ViewStyle,
|
||||
} from 'react-native'
|
||||
import {Dimensions, Pressable, View} from 'react-native'
|
||||
import Animated, {useAnimatedStyle} from 'react-native-reanimated'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import BottomSheet, {
|
||||
@@ -176,8 +169,7 @@ export function Outer({
|
||||
// Android
|
||||
importantForAccessibility="yes"
|
||||
style={[a.absolute, a.inset_0]}
|
||||
testID={testID}
|
||||
onTouchMove={() => Keyboard.dismiss()}>
|
||||
testID={testID}>
|
||||
<BottomSheet
|
||||
enableDynamicSizing={!hasSnapPoints}
|
||||
enablePanDownToClose
|
||||
@@ -265,10 +257,9 @@ export const ScrollableInner = React.forwardRef<
|
||||
|
||||
export const InnerFlatList = React.forwardRef<
|
||||
BottomSheetFlatListMethods,
|
||||
BottomSheetFlatListProps<any> & {webInnerStyle?: StyleProp<ViewStyle>}
|
||||
BottomSheetFlatListProps<any>
|
||||
>(function InnerFlatList({style, contentContainerStyle, ...props}, ref) {
|
||||
const insets = useSafeAreaInsets()
|
||||
|
||||
return (
|
||||
<BottomSheetFlatList
|
||||
keyboardShouldPersistTaps="handled"
|
||||
|
||||
@@ -2,10 +2,8 @@ import React, {useImperativeHandle} from 'react'
|
||||
import {
|
||||
FlatList,
|
||||
FlatListProps,
|
||||
StyleProp,
|
||||
TouchableWithoutFeedback,
|
||||
View,
|
||||
ViewStyle,
|
||||
} from 'react-native'
|
||||
import Animated, {FadeIn, FadeInDown} from 'react-native-reanimated'
|
||||
import {msg} from '@lingui/macro'
|
||||
@@ -201,21 +199,18 @@ export const ScrollableInner = Inner
|
||||
|
||||
export const InnerFlatList = React.forwardRef<
|
||||
FlatList,
|
||||
FlatListProps<any> & {label: string} & {webInnerStyle?: StyleProp<ViewStyle>}
|
||||
>(function InnerFlatList({label, style, webInnerStyle, ...props}, ref) {
|
||||
FlatListProps<any> & {label: string}
|
||||
>(function InnerFlatList({label, style, ...props}, ref) {
|
||||
const {gtMobile} = useBreakpoints()
|
||||
return (
|
||||
<Inner
|
||||
label={label}
|
||||
style={[
|
||||
// @ts-ignore web only -sfn
|
||||
{
|
||||
paddingHorizontal: 0,
|
||||
maxHeight: 'calc(-36px + 100vh)',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
webInnerStyle,
|
||||
]}>
|
||||
// @ts-ignore web only -sfn
|
||||
style={{
|
||||
paddingHorizontal: 0,
|
||||
maxHeight: 'calc(-36px + 100vh)',
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
<FlatList
|
||||
ref={ref}
|
||||
style={[gtMobile ? a.px_2xl : a.px_xl, flatten(style)]}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import React from 'react'
|
||||
import {useKeyboardHandler} from 'react-native-keyboard-controller'
|
||||
import Animated, {
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
} from 'react-native-reanimated'
|
||||
|
||||
export function KeyboardPadding({maxHeight}: {maxHeight?: number}) {
|
||||
const keyboardHeight = useSharedValue(0)
|
||||
|
||||
useKeyboardHandler(
|
||||
{
|
||||
onMove: e => {
|
||||
'worklet'
|
||||
|
||||
if (maxHeight && e.height > maxHeight) {
|
||||
keyboardHeight.value = maxHeight
|
||||
} else {
|
||||
keyboardHeight.value = e.height
|
||||
}
|
||||
},
|
||||
},
|
||||
[maxHeight],
|
||||
)
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
height: keyboardHeight.value,
|
||||
}))
|
||||
|
||||
return <Animated.View style={animatedStyle} />
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export function KeyboardPadding({maxHeight: _}: {maxHeight?: number}) {
|
||||
return null
|
||||
}
|
||||
@@ -13,22 +13,16 @@ import {Text} from '#/components/Typography'
|
||||
|
||||
export function ListFooter({
|
||||
isFetchingNextPage,
|
||||
hasNextPage,
|
||||
error,
|
||||
onRetry,
|
||||
height,
|
||||
style,
|
||||
showEndMessage = false,
|
||||
endMessageText,
|
||||
}: {
|
||||
isFetchingNextPage?: boolean
|
||||
hasNextPage?: boolean
|
||||
error?: string
|
||||
onRetry?: () => Promise<unknown>
|
||||
height?: number
|
||||
style?: StyleProp<ViewStyle>
|
||||
showEndMessage?: boolean
|
||||
endMessageText?: string
|
||||
}) {
|
||||
const t = useTheme()
|
||||
|
||||
@@ -45,13 +39,9 @@ export function ListFooter({
|
||||
]}>
|
||||
{isFetchingNextPage ? (
|
||||
<Loader size="xl" />
|
||||
) : error ? (
|
||||
) : (
|
||||
<ListFooterMaybeError error={error} onRetry={onRetry} />
|
||||
) : !hasNextPage && showEndMessage ? (
|
||||
<Text style={[a.text_sm, t.atoms.text_contrast_low]}>
|
||||
{endMessageText ?? <Trans>You have reached the end</Trans>}
|
||||
</Text>
|
||||
) : null}
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {usePrefetchProfileQuery, useProfileQuery} from '#/state/queries/profile'
|
||||
import {useSession} from '#/state/session'
|
||||
import {isTouchDevice} from 'lib/browser'
|
||||
import {useProfileShadow} from 'state/cache/profile-shadow'
|
||||
import {formatCount} from '#/view/com/util/numeric/format'
|
||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||
@@ -44,6 +43,8 @@ const floatingMiddlewares = [
|
||||
}),
|
||||
]
|
||||
|
||||
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0
|
||||
|
||||
export function ProfileHoverCard(props: ProfileHoverCardProps) {
|
||||
if (props.disable || isTouchDevice) {
|
||||
return props.children
|
||||
|
||||
@@ -172,7 +172,6 @@ export function Basic({
|
||||
confirmButtonCta,
|
||||
onConfirm,
|
||||
confirmButtonColor,
|
||||
showCancel = true,
|
||||
}: React.PropsWithChildren<{
|
||||
control: Dialog.DialogOuterProps['control']
|
||||
title: string
|
||||
@@ -188,7 +187,6 @@ export function Basic({
|
||||
*/
|
||||
onConfirm: () => void
|
||||
confirmButtonColor?: ButtonColor
|
||||
showCancel?: boolean
|
||||
}>) {
|
||||
return (
|
||||
<Outer control={control} testID="confirmModal">
|
||||
@@ -201,7 +199,7 @@ export function Basic({
|
||||
color={confirmButtonColor}
|
||||
testID="confirmBtn"
|
||||
/>
|
||||
{showCancel && <Cancel cta={cancelButtonCta} />}
|
||||
<Cancel cta={cancelButtonCta} />
|
||||
</Actions>
|
||||
</Outer>
|
||||
)
|
||||
|
||||
@@ -25,10 +25,12 @@ import {SquareArrowTopRight_Stroke2_Corner0_Rounded as SquareArrowTopRight} from
|
||||
import {Text} from '#/components/Typography'
|
||||
import {ReportDialogProps} from './types'
|
||||
|
||||
type ParamsWithMessages = ReportDialogProps['params'] | {type: 'message'}
|
||||
|
||||
export function SelectReportOptionView({
|
||||
...props
|
||||
}: {
|
||||
params: ReportDialogProps['params']
|
||||
params: ParamsWithMessages
|
||||
labelers: AppBskyLabelerDefs.LabelerViewDetailed[]
|
||||
onSelectReportOption: (reportOption: ReportOption) => void
|
||||
goBack: () => void
|
||||
@@ -55,7 +57,7 @@ export function SelectReportOptionView({
|
||||
} else if (props.params.type === 'feedgen') {
|
||||
title = _(msg`Report this feed`)
|
||||
description = _(msg`Why should this feed be reviewed?`)
|
||||
} else if (props.params.type === 'convoMessage') {
|
||||
} else if (props.params.type === 'message') {
|
||||
title = _(msg`Report this message`)
|
||||
description = _(msg`Why should this message be reviewed?`)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import * as Dialog from '#/components/Dialog'
|
||||
import * as Toggle from '#/components/forms/Toggle'
|
||||
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
||||
import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
|
||||
import {KeyboardPadding} from '#/components/KeyboardPadding'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {ReportDialogProps} from './types'
|
||||
@@ -36,7 +35,7 @@ export function SubmitView({
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const agent = useAgent()
|
||||
const {getAgent} = useAgent()
|
||||
const [details, setDetails] = React.useState<string>('')
|
||||
const [submitting, setSubmitting] = React.useState<boolean>(false)
|
||||
const [selectedServices, setSelectedServices] = React.useState<string[]>([
|
||||
@@ -62,7 +61,7 @@ export function SubmitView({
|
||||
}
|
||||
const results = await Promise.all(
|
||||
selectedServices.map(did =>
|
||||
agent
|
||||
getAgent()
|
||||
.withProxy('atproto_labeler', did)
|
||||
.createModerationReport(report)
|
||||
.then(
|
||||
@@ -92,7 +91,7 @@ export function SubmitView({
|
||||
selectedServices,
|
||||
onSubmitComplete,
|
||||
setError,
|
||||
agent,
|
||||
getAgent,
|
||||
])
|
||||
|
||||
return (
|
||||
@@ -222,7 +221,6 @@ export function SubmitView({
|
||||
{submitting && <ButtonIcon icon={Loader} />}
|
||||
</Button>
|
||||
</View>
|
||||
<KeyboardPadding />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,5 +12,4 @@ export type ReportDialogProps = {
|
||||
type: 'account'
|
||||
did: string
|
||||
}
|
||||
| {type: 'convoMessage'}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Has
|
||||
import {PageText_Stroke2_Corner0_Rounded as PageText} from '#/components/icons/PageText'
|
||||
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
||||
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||
import {KeyboardPadding} from '#/components/KeyboardPadding'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {Text} from '#/components/Typography'
|
||||
@@ -257,7 +256,6 @@ function MutedWordsInner() {
|
||||
</View>
|
||||
|
||||
<Dialog.Close />
|
||||
<KeyboardPadding maxHeight={100} />
|
||||
</Dialog.ScrollableInner>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react'
|
||||
import {Keyboard} from 'react-native'
|
||||
import {Gesture, GestureDetector} from 'react-native-gesture-handler'
|
||||
import {Keyboard, Pressable, View} from 'react-native'
|
||||
import Animated, {
|
||||
cancelAnimation,
|
||||
runOnJS,
|
||||
@@ -9,8 +8,6 @@ import Animated, {
|
||||
withTiming,
|
||||
} from 'react-native-reanimated'
|
||||
import {ChatBskyConvoDefs} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {HITSLOP_10} from 'lib/constants'
|
||||
import {useHaptics} from 'lib/haptics'
|
||||
@@ -18,6 +15,8 @@ import {atoms as a} from '#/alf'
|
||||
import {MessageMenu} from '#/components/dms/MessageMenu'
|
||||
import {useMenuControl} from '#/components/Menu'
|
||||
|
||||
const AnimatedPressable = Animated.createAnimatedComponent(Pressable)
|
||||
|
||||
export function ActionsWrapper({
|
||||
message,
|
||||
isFromSelf,
|
||||
@@ -27,69 +26,60 @@ export function ActionsWrapper({
|
||||
isFromSelf: boolean
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const playHaptic = useHaptics()
|
||||
const menuControl = useMenuControl()
|
||||
|
||||
const scale = useSharedValue(1)
|
||||
const animationDidComplete = useSharedValue(false)
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [{scale: scale.value}],
|
||||
}))
|
||||
|
||||
// Reanimated's `runOnJS` doesn't like refs, so we can't use `runOnJS(menuControl.open)()`. Instead, we'll use this
|
||||
// function
|
||||
const open = React.useCallback(() => {
|
||||
playHaptic()
|
||||
Keyboard.dismiss()
|
||||
menuControl.open()
|
||||
}, [menuControl, playHaptic])
|
||||
}, [menuControl])
|
||||
|
||||
const shrink = React.useCallback(() => {
|
||||
'worklet'
|
||||
cancelAnimation(scale)
|
||||
scale.value = withTiming(1, {duration: 200})
|
||||
}, [scale])
|
||||
|
||||
const doubleTapGesture = Gesture.Tap()
|
||||
.numberOfTaps(2)
|
||||
.hitSlop(HITSLOP_10)
|
||||
.onEnd(open)
|
||||
|
||||
const pressAndHoldGesture = Gesture.LongPress()
|
||||
.onStart(() => {
|
||||
scale.value = withTiming(1.05, {duration: 200}, finished => {
|
||||
if (!finished) return
|
||||
runOnJS(open)()
|
||||
shrink()
|
||||
})
|
||||
scale.value = withTiming(1, {duration: 200}, () => {
|
||||
animationDidComplete.value = false
|
||||
})
|
||||
.onTouchesUp(shrink)
|
||||
.onTouchesMove(shrink)
|
||||
.cancelsTouchesInView(false)
|
||||
.runOnJS(true)
|
||||
}, [animationDidComplete, scale])
|
||||
|
||||
const composedGestures = Gesture.Exclusive(
|
||||
doubleTapGesture,
|
||||
pressAndHoldGesture,
|
||||
)
|
||||
const grow = React.useCallback(() => {
|
||||
'worklet'
|
||||
scale.value = withTiming(1.05, {duration: 450}, finished => {
|
||||
if (!finished) return
|
||||
animationDidComplete.value = true
|
||||
runOnJS(playHaptic)()
|
||||
runOnJS(open)()
|
||||
|
||||
shrink()
|
||||
})
|
||||
}, [scale, animationDidComplete, playHaptic, shrink, open])
|
||||
|
||||
return (
|
||||
<GestureDetector gesture={composedGestures}>
|
||||
<Animated.View
|
||||
style={[
|
||||
{
|
||||
maxWidth: '80%',
|
||||
},
|
||||
isFromSelf ? a.self_end : a.self_start,
|
||||
animatedStyle,
|
||||
]}
|
||||
accessible={true}
|
||||
accessibilityActions={[
|
||||
{name: 'activate', label: _(msg`Open message options`)},
|
||||
]}
|
||||
onAccessibilityAction={open}>
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
maxWidth: '80%',
|
||||
},
|
||||
isFromSelf ? a.self_end : a.self_start,
|
||||
]}>
|
||||
<AnimatedPressable
|
||||
style={animatedStyle}
|
||||
unstable_pressDelay={200}
|
||||
onPressIn={grow}
|
||||
onTouchEnd={shrink}
|
||||
hitSlop={HITSLOP_10}>
|
||||
{children}
|
||||
<MessageMenu message={message} control={menuControl} />
|
||||
</Animated.View>
|
||||
</GestureDetector>
|
||||
</AnimatedPressable>
|
||||
<MessageMenu message={message} control={menuControl} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {ModerationCause} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {listUriToHref} from 'lib/strings/url-helpers'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {DialogControlProps} from '#/components/Dialog'
|
||||
import {InlineLinkText} from '#/components/Link'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function BlockedByListDialog({
|
||||
control,
|
||||
listBlocks,
|
||||
}: {
|
||||
control: DialogControlProps
|
||||
listBlocks: ModerationCause[]
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
|
||||
return (
|
||||
<Prompt.Outer control={control} testID="blockedByListDialog">
|
||||
<Prompt.TitleText>{_(msg`User blocked by list`)}</Prompt.TitleText>
|
||||
|
||||
<View style={[a.gap_sm, a.pb_lg]}>
|
||||
<Text
|
||||
selectable
|
||||
style={[a.text_md, a.leading_snug, t.atoms.text_contrast_high]}>
|
||||
{_(
|
||||
msg`This account is blocked by one or more of your moderation lists. To unblock, please visit the lists directly and remove this user.`,
|
||||
)}{' '}
|
||||
</Text>
|
||||
|
||||
<Text style={[a.text_md, a.leading_snug, t.atoms.text_contrast_high]}>
|
||||
{_(msg`Lists blocking this user:`)}{' '}
|
||||
{listBlocks.map((block, i) =>
|
||||
block.source.type === 'list' ? (
|
||||
<React.Fragment key={block.source.list.uri}>
|
||||
{i === 0 ? null : ', '}
|
||||
<InlineLinkText
|
||||
to={listUriToHref(block.source.list.uri)}
|
||||
style={[a.text_md, a.leading_snug]}>
|
||||
{block.source.list.name}
|
||||
</InlineLinkText>
|
||||
</React.Fragment>
|
||||
) : null,
|
||||
)}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Prompt.Actions>
|
||||
<Prompt.Action cta={_(msg`I understand`)} onPress={() => {}} />
|
||||
</Prompt.Actions>
|
||||
|
||||
<Dialog.Close />
|
||||
</Prompt.Outer>
|
||||
)
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
import React from 'react'
|
||||
import {Pressable, View} from 'react-native'
|
||||
import Animated, {
|
||||
runOnJS,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withTiming,
|
||||
} from 'react-native-reanimated'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {ScaleAndFadeIn} from 'lib/custom-animations/ScaleAndFade'
|
||||
import {ShrinkAndPop} from 'lib/custom-animations/ShrinkAndPop'
|
||||
import {useHaptics} from 'lib/haptics'
|
||||
import {isWeb} from 'platform/detection'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
const AnimatedPressable = Animated.createAnimatedComponent(Pressable)
|
||||
|
||||
let lastIndex = 0
|
||||
|
||||
export function ChatEmptyPill() {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const playHaptic = useHaptics()
|
||||
const [promptIndex, setPromptIndex] = React.useState(lastIndex)
|
||||
|
||||
const scale = useSharedValue(1)
|
||||
|
||||
const prompts = React.useMemo(() => {
|
||||
return [
|
||||
_(msg`Say hello!`),
|
||||
_(msg`Share your favorite feed!`),
|
||||
_(msg`Tell a joke!`),
|
||||
_(msg`Share a fun fact!`),
|
||||
_(msg`Share a cool story!`),
|
||||
_(msg`Send a neat website!`),
|
||||
_(msg`Clip 🐴 clop 🐴`),
|
||||
]
|
||||
}, [_])
|
||||
|
||||
const onPressIn = React.useCallback(() => {
|
||||
if (isWeb) return
|
||||
scale.value = withTiming(1.075, {duration: 100})
|
||||
}, [scale])
|
||||
|
||||
const onPressOut = React.useCallback(() => {
|
||||
if (isWeb) return
|
||||
scale.value = withTiming(1, {duration: 100})
|
||||
}, [scale])
|
||||
|
||||
const onPress = React.useCallback(() => {
|
||||
runOnJS(playHaptic)()
|
||||
let randomPromptIndex = Math.floor(Math.random() * prompts.length)
|
||||
while (randomPromptIndex === lastIndex) {
|
||||
randomPromptIndex = Math.floor(Math.random() * prompts.length)
|
||||
}
|
||||
setPromptIndex(randomPromptIndex)
|
||||
lastIndex = randomPromptIndex
|
||||
}, [playHaptic, prompts.length])
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [{scale: scale.value}],
|
||||
}))
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.w_full,
|
||||
a.z_10,
|
||||
a.align_center,
|
||||
{
|
||||
top: -50,
|
||||
},
|
||||
]}>
|
||||
<AnimatedPressable
|
||||
style={[
|
||||
a.px_xl,
|
||||
a.py_md,
|
||||
a.rounded_full,
|
||||
t.atoms.bg_contrast_25,
|
||||
a.align_center,
|
||||
animatedStyle,
|
||||
]}
|
||||
entering={ScaleAndFadeIn}
|
||||
exiting={ShrinkAndPop}
|
||||
onPress={onPress}
|
||||
onPressIn={onPressIn}
|
||||
onPressOut={onPressOut}>
|
||||
<Text style={[a.font_bold, a.pointer_events_none]} selectable={false}>
|
||||
{prompts[promptIndex]}
|
||||
</Text>
|
||||
</AnimatedPressable>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
+151
-100
@@ -3,25 +3,25 @@ import {Keyboard, Pressable, View} from 'react-native'
|
||||
import {
|
||||
AppBskyActorDefs,
|
||||
ChatBskyConvoDefs,
|
||||
ModerationCause,
|
||||
ModerationDecision,
|
||||
} 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 {listUriToHref} from '#/lib/strings/url-helpers'
|
||||
import {Shadow} from '#/state/cache/types'
|
||||
import {
|
||||
useConvoQuery,
|
||||
useMarkAsReadMutation,
|
||||
} from '#/state/queries/messages/conversation'
|
||||
import {useLeaveConvo} from '#/state/queries/messages/leave-conversation'
|
||||
import {useMuteConvo} from '#/state/queries/messages/mute-conversation'
|
||||
import {useProfileBlockMutationQueue} from '#/state/queries/profile'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {atoms as a, useTheme, ViewStyleProp} from '#/alf'
|
||||
import {BlockedByListDialog} from '#/components/dms/BlockedByListDialog'
|
||||
import {LeaveConvoPrompt} from '#/components/dms/LeaveConvoPrompt'
|
||||
import {ReportConversationPrompt} from '#/components/dms/ReportConversationPrompt'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {ArrowBoxLeft_Stroke2_Corner0_Rounded as ArrowBoxLeft} from '#/components/icons/ArrowBoxLeft'
|
||||
import {DotGrid_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid'
|
||||
import {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag'
|
||||
@@ -30,8 +30,10 @@ import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Perso
|
||||
import {PersonCheck_Stroke2_Corner0_Rounded as PersonCheck} from '#/components/icons/PersonCheck'
|
||||
import {PersonX_Stroke2_Corner0_Rounded as PersonX} from '#/components/icons/PersonX'
|
||||
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker'
|
||||
import {InlineLinkText} from '#/components/Link'
|
||||
import * as Menu from '#/components/Menu'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {Bubble_Stroke2_Corner2_Rounded as Bubble} from '../icons/Bubble'
|
||||
|
||||
let ConvoMenu = ({
|
||||
@@ -41,8 +43,8 @@ let ConvoMenu = ({
|
||||
currentScreen,
|
||||
showMarkAsRead,
|
||||
hideTrigger,
|
||||
blockInfo,
|
||||
style,
|
||||
triggerOpacity,
|
||||
moderation,
|
||||
}: {
|
||||
convo: ChatBskyConvoDefs.ConvoView
|
||||
profile: Shadow<AppBskyActorDefs.ProfileViewBasic>
|
||||
@@ -50,11 +52,8 @@ let ConvoMenu = ({
|
||||
currentScreen: 'list' | 'conversation'
|
||||
showMarkAsRead?: boolean
|
||||
hideTrigger?: boolean
|
||||
blockInfo: {
|
||||
listBlocks: ModerationCause[]
|
||||
userBlock?: ModerationCause
|
||||
}
|
||||
style?: ViewStyleProp['style']
|
||||
triggerOpacity?: number
|
||||
moderation: ModerationDecision
|
||||
}): React.ReactNode => {
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const {_} = useLingui()
|
||||
@@ -63,10 +62,17 @@ let ConvoMenu = ({
|
||||
const reportControl = Prompt.usePromptControl()
|
||||
const blockedByListControl = Prompt.usePromptControl()
|
||||
const {mutate: markAsRead} = useMarkAsReadMutation()
|
||||
|
||||
const {listBlocks, userBlock} = blockInfo
|
||||
const isBlocking = userBlock || !!listBlocks.length
|
||||
const isDeletedAccount = profile.handle === 'missing.invalid'
|
||||
const modui = moderation.ui('profileView')
|
||||
const {listBlocks, userBlock} = React.useMemo(() => {
|
||||
const blocks = modui.alerts.filter(alert => alert.type === 'blocking')
|
||||
const listBlocks = blocks.filter(alert => alert.source.type === 'list')
|
||||
const userBlock = blocks.find(alert => alert.source.type === 'user')
|
||||
return {
|
||||
listBlocks,
|
||||
userBlock,
|
||||
}
|
||||
}, [modui])
|
||||
const isBlocking = !!userBlock || !!listBlocks.length
|
||||
|
||||
const {data: convo} = useConvoQuery(initialConvo)
|
||||
|
||||
@@ -102,11 +108,22 @@ let ConvoMenu = ({
|
||||
}
|
||||
}, [userBlock, listBlocks, blockedByListControl, queueBlock, queueUnblock])
|
||||
|
||||
const {mutate: leaveConvo} = useLeaveConvo(convo?.id, {
|
||||
onSuccess: () => {
|
||||
if (currentScreen === 'conversation') {
|
||||
navigation.replace('Messages')
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
Toast.show(_(msg`Could not leave chat`))
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu.Root control={control}>
|
||||
{!hideTrigger && (
|
||||
<View style={[style]}>
|
||||
<View style={{opacity: triggerOpacity}}>
|
||||
<Menu.Trigger label={_(msg`Chat settings`)}>
|
||||
{({props, state}) => (
|
||||
<Pressable
|
||||
@@ -129,9 +146,66 @@ let ConvoMenu = ({
|
||||
</Menu.Trigger>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{isDeletedAccount ? (
|
||||
<Menu.Outer>
|
||||
<Menu.Outer>
|
||||
<Menu.Group>
|
||||
{showMarkAsRead && (
|
||||
<Menu.Item
|
||||
label={_(msg`Mark as read`)}
|
||||
onPress={() =>
|
||||
markAsRead({
|
||||
convoId: convo?.id,
|
||||
})
|
||||
}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Mark as read</Trans>
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Bubble} />
|
||||
</Menu.Item>
|
||||
)}
|
||||
<Menu.Item
|
||||
label={_(msg`Go to user's profile`)}
|
||||
onPress={onNavigateToProfile}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Go to profile</Trans>
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Person} />
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
label={_(msg`Mute conversation`)}
|
||||
onPress={() => muteConvo({mute: !convo?.muted})}>
|
||||
<Menu.ItemText>
|
||||
{convo?.muted ? (
|
||||
<Trans>Unmute conversation</Trans>
|
||||
) : (
|
||||
<Trans>Mute conversation</Trans>
|
||||
)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={convo?.muted ? Unmute : Mute} />
|
||||
</Menu.Item>
|
||||
</Menu.Group>
|
||||
<Menu.Divider />
|
||||
<Menu.Group>
|
||||
<Menu.Item
|
||||
label={
|
||||
isBlocking ? _(msg`Unblock account`) : _(msg`Block account`)
|
||||
}
|
||||
onPress={toggleBlock}>
|
||||
<Menu.ItemText>
|
||||
{isBlocking ? _(msg`Unblock account`) : _(msg`Block account`)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={isBlocking ? PersonX : PersonCheck} />
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
label={_(msg`Report conversation`)}
|
||||
onPress={reportControl.open}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Report conversation</Trans>
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Flag} />
|
||||
</Menu.Item>
|
||||
</Menu.Group>
|
||||
<Menu.Divider />
|
||||
<Menu.Group>
|
||||
<Menu.Item
|
||||
label={_(msg`Leave conversation`)}
|
||||
onPress={leaveConvoControl.open}>
|
||||
@@ -140,94 +214,71 @@ let ConvoMenu = ({
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={ArrowBoxLeft} />
|
||||
</Menu.Item>
|
||||
</Menu.Outer>
|
||||
) : (
|
||||
<Menu.Outer>
|
||||
<Menu.Group>
|
||||
{showMarkAsRead && (
|
||||
<Menu.Item
|
||||
label={_(msg`Mark as read`)}
|
||||
onPress={() =>
|
||||
markAsRead({
|
||||
convoId: convo?.id,
|
||||
})
|
||||
}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Mark as read</Trans>
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Bubble} />
|
||||
</Menu.Item>
|
||||
)}
|
||||
<Menu.Item
|
||||
label={_(msg`Go to user's profile`)}
|
||||
onPress={onNavigateToProfile}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Go to profile</Trans>
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Person} />
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
label={_(msg`Mute conversation`)}
|
||||
onPress={() => muteConvo({mute: !convo?.muted})}>
|
||||
<Menu.ItemText>
|
||||
{convo?.muted ? (
|
||||
<Trans>Unmute conversation</Trans>
|
||||
) : (
|
||||
<Trans>Mute conversation</Trans>
|
||||
)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={convo?.muted ? Unmute : Mute} />
|
||||
</Menu.Item>
|
||||
</Menu.Group>
|
||||
<Menu.Divider />
|
||||
<Menu.Group>
|
||||
<Menu.Item
|
||||
label={
|
||||
isBlocking ? _(msg`Unblock account`) : _(msg`Block account`)
|
||||
}
|
||||
onPress={toggleBlock}>
|
||||
<Menu.ItemText>
|
||||
{isBlocking ? _(msg`Unblock account`) : _(msg`Block account`)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={isBlocking ? PersonCheck : PersonX} />
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
label={_(msg`Report conversation`)}
|
||||
onPress={reportControl.open}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Report conversation</Trans>
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Flag} />
|
||||
</Menu.Item>
|
||||
</Menu.Group>
|
||||
<Menu.Divider />
|
||||
<Menu.Group>
|
||||
<Menu.Item
|
||||
label={_(msg`Leave conversation`)}
|
||||
onPress={leaveConvoControl.open}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Leave conversation</Trans>
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={ArrowBoxLeft} />
|
||||
</Menu.Item>
|
||||
</Menu.Group>
|
||||
</Menu.Outer>
|
||||
)}
|
||||
</Menu.Group>
|
||||
</Menu.Outer>
|
||||
</Menu.Root>
|
||||
|
||||
<LeaveConvoPrompt
|
||||
<Prompt.Basic
|
||||
control={leaveConvoControl}
|
||||
convoId={convo.id}
|
||||
currentScreen={currentScreen}
|
||||
title={_(msg`Leave conversation`)}
|
||||
description={_(
|
||||
msg`Are you sure you want to leave this conversation? Your messages will be deleted for you, but not for the other participant.`,
|
||||
)}
|
||||
confirmButtonCta={_(msg`Leave`)}
|
||||
confirmButtonColor="negative"
|
||||
onConfirm={() => leaveConvo()}
|
||||
/>
|
||||
<ReportConversationPrompt control={reportControl} />
|
||||
<BlockedByListDialog
|
||||
control={blockedByListControl}
|
||||
listBlocks={listBlocks}
|
||||
|
||||
<Prompt.Basic
|
||||
control={reportControl}
|
||||
title={_(msg`Report conversation`)}
|
||||
description={_(
|
||||
msg`To report a conversation, please report one of its messages via the conversation screen. This lets our moderators understand the context of your issue.`,
|
||||
)}
|
||||
confirmButtonCta={_(msg`I understand`)}
|
||||
onConfirm={noop}
|
||||
/>
|
||||
|
||||
<Prompt.Outer control={blockedByListControl} testID="blockedByListDialog">
|
||||
<Prompt.TitleText>{_(msg`User blocked by list`)}</Prompt.TitleText>
|
||||
|
||||
<View style={[a.gap_sm, a.pb_lg]}>
|
||||
<Text
|
||||
selectable
|
||||
style={[a.text_md, a.leading_snug, t.atoms.text_contrast_high]}>
|
||||
{_(
|
||||
msg`This account is blocked by one or more of your moderation lists. To unblock, please visit the lists directly and remove this user.`,
|
||||
)}{' '}
|
||||
</Text>
|
||||
|
||||
<Text style={[a.text_md, a.leading_snug, t.atoms.text_contrast_high]}>
|
||||
{_(msg`Lists blocking this user:`)}{' '}
|
||||
{listBlocks.map((block, i) =>
|
||||
block.source.type === 'list' ? (
|
||||
<React.Fragment key={block.source.list.uri}>
|
||||
{i === 0 ? null : ', '}
|
||||
<InlineLinkText
|
||||
to={listUriToHref(block.source.list.uri)}
|
||||
style={[a.text_md, a.leading_snug]}>
|
||||
{block.source.list.name}
|
||||
</InlineLinkText>
|
||||
</React.Fragment>
|
||||
) : null,
|
||||
)}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Prompt.Actions>
|
||||
<Prompt.Cancel cta={_(msg`I understand`)} />
|
||||
</Prompt.Actions>
|
||||
|
||||
<Dialog.Close />
|
||||
</Prompt.Outer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
ConvoMenu = React.memo(ConvoMenu)
|
||||
|
||||
export {ConvoMenu}
|
||||
|
||||
function noop() {}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import React from 'react'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {NavigationProp} from 'lib/routes/types'
|
||||
import {isNative} from 'platform/detection'
|
||||
import {useLeaveConvo} from 'state/queries/messages/leave-conversation'
|
||||
import * as Toast from 'view/com/util/Toast'
|
||||
import {DialogOuterProps} from '#/components/Dialog'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
|
||||
export function LeaveConvoPrompt({
|
||||
control,
|
||||
convoId,
|
||||
currentScreen,
|
||||
}: {
|
||||
control: DialogOuterProps['control']
|
||||
convoId: string
|
||||
currentScreen: 'list' | 'conversation'
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
|
||||
const {mutate: leaveConvo} = useLeaveConvo(convoId, {
|
||||
onSuccess: () => {
|
||||
if (currentScreen === 'conversation') {
|
||||
navigation.replace(
|
||||
'Messages',
|
||||
isNative
|
||||
? {
|
||||
animation: 'pop',
|
||||
}
|
||||
: {},
|
||||
)
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
Toast.show(_(msg`Could not leave chat`))
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<Prompt.Basic
|
||||
control={control}
|
||||
title={_(msg`Leave conversation`)}
|
||||
description={_(
|
||||
msg`Are you sure you want to leave this conversation? Your messages will be deleted for you, but not for the other participant.`,
|
||||
)}
|
||||
confirmButtonCta={_(msg`Leave`)}
|
||||
confirmButtonColor="negative"
|
||||
onConfirm={leaveConvo}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -75,7 +75,7 @@ let MessageItem = ({
|
||||
}, [message.text, message.facets])
|
||||
|
||||
return (
|
||||
<View style={[isFromSelf ? a.mr_md : a.ml_md]}>
|
||||
<View>
|
||||
<ActionsWrapper isFromSelf={isFromSelf} message={message}>
|
||||
<View
|
||||
style={[
|
||||
|
||||
@@ -11,7 +11,6 @@ import {useConvoActive} from 'state/messages/convo'
|
||||
import {useSession} from 'state/session'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {ReportDialog} from '#/components/dms/ReportDialog'
|
||||
import {DotGrid_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid'
|
||||
import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
|
||||
import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning'
|
||||
@@ -19,6 +18,7 @@ import * as Menu from '#/components/Menu'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {usePromptControl} from '#/components/Prompt'
|
||||
import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '../icons/Clipboard'
|
||||
import {MessageReportDialog} from './MessageReportDialog'
|
||||
|
||||
export let MessageMenu = ({
|
||||
message,
|
||||
@@ -34,6 +34,7 @@ export let MessageMenu = ({
|
||||
const {currentAccount} = useSession()
|
||||
const convo = useConvoActive()
|
||||
const deleteControl = usePromptControl()
|
||||
const retryDeleteControl = usePromptControl()
|
||||
const reportControl = usePromptControl()
|
||||
|
||||
const isFromSelf = message.sender?.did === currentAccount?.did
|
||||
@@ -56,8 +57,8 @@ export let MessageMenu = ({
|
||||
convo
|
||||
.deleteMessage(message.id)
|
||||
.then(() => Toast.show(_(msg`Message deleted`)))
|
||||
.catch(() => Toast.show(_(msg`Failed to delete message`)))
|
||||
}, [_, convo, message.id])
|
||||
.catch(() => retryDeleteControl.open())
|
||||
}, [_, convo, message.id, retryDeleteControl])
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -112,10 +113,7 @@ export let MessageMenu = ({
|
||||
</Menu.Outer>
|
||||
</Menu.Root>
|
||||
|
||||
<ReportDialog
|
||||
params={{type: 'convoMessage', convoId: convo.convo.id, message}}
|
||||
control={reportControl}
|
||||
/>
|
||||
<MessageReportDialog message={message} control={reportControl} />
|
||||
|
||||
<Prompt.Basic
|
||||
control={deleteControl}
|
||||
@@ -127,6 +125,17 @@ export let MessageMenu = ({
|
||||
confirmButtonColor="negative"
|
||||
onConfirm={onDelete}
|
||||
/>
|
||||
|
||||
<Prompt.Basic
|
||||
control={retryDeleteControl}
|
||||
title={_(msg`Failed to delete message`)}
|
||||
description={_(
|
||||
msg`An error occurred while trying to delete the message. Please try again.`,
|
||||
)}
|
||||
confirmButtonCta={_(msg`Retry`)}
|
||||
confirmButtonColor="negative"
|
||||
onConfirm={onDelete}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {AppBskyActorDefs} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useMaybeConvoForUser} from '#/state/queries/messages/get-convo-for-members'
|
||||
import {logEvent} from 'lib/statsig/statsig'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Message_Stroke2_Corner0_Rounded as Message} from '../icons/Message'
|
||||
import {Link} from '../Link'
|
||||
import {canBeMessaged} from './util'
|
||||
|
||||
export function MessageProfileButton({
|
||||
profile,
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileView
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
|
||||
const {data: convo, isPending} = useMaybeConvoForUser(profile.did)
|
||||
|
||||
const onPress = React.useCallback(() => {
|
||||
if (convo && !convo.lastMessage) {
|
||||
logEvent('chat:create', {logContext: 'ProfileHeader'})
|
||||
}
|
||||
logEvent('chat:open', {logContext: 'ProfileHeader'})
|
||||
}, [convo])
|
||||
|
||||
if (isPending) {
|
||||
// show pending state based on declaration
|
||||
if (canBeMessaged(profile)) {
|
||||
return (
|
||||
<View
|
||||
testID="dmBtnLoading"
|
||||
aria-hidden={true}
|
||||
style={[
|
||||
a.justify_center,
|
||||
a.align_center,
|
||||
t.atoms.bg_contrast_25,
|
||||
a.rounded_full,
|
||||
{width: 36, height: 36},
|
||||
]}>
|
||||
<Message
|
||||
style={[
|
||||
t.atoms.text,
|
||||
{marginLeft: 1, marginBottom: 1, opacity: 0.3},
|
||||
]}
|
||||
size="md"
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
if (convo) {
|
||||
return (
|
||||
<Link
|
||||
testID="dmBtn"
|
||||
size="small"
|
||||
color="secondary"
|
||||
variant="solid"
|
||||
shape="round"
|
||||
label={_(msg`Message ${profile.handle}`)}
|
||||
to={`/messages/${convo.id}`}
|
||||
style={[a.justify_center, {width: 36, height: 36}]}
|
||||
onPress={onPress}>
|
||||
<Message
|
||||
style={[t.atoms.text, {marginLeft: 1, marginBottom: 1}]}
|
||||
size="md"
|
||||
/>
|
||||
</Link>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -25,18 +25,12 @@ import {RichText} from '../RichText'
|
||||
import {Text} from '../Typography'
|
||||
import {MessageItemMetadata} from './MessageItem'
|
||||
|
||||
type ReportDialogParams = {
|
||||
type: 'convoMessage'
|
||||
convoId: string
|
||||
message: ChatBskyConvoDefs.MessageView
|
||||
}
|
||||
|
||||
let ReportDialog = ({
|
||||
let MessageReportDialog = ({
|
||||
control,
|
||||
params,
|
||||
message,
|
||||
}: {
|
||||
control: Dialog.DialogControlProps
|
||||
params: ReportDialogParams
|
||||
message: ChatBskyConvoDefs.MessageView
|
||||
}): React.ReactNode => {
|
||||
const {_} = useLingui()
|
||||
return (
|
||||
@@ -45,26 +39,26 @@ let ReportDialog = ({
|
||||
nativeOptions={isAndroid ? {sheet: {snapPoints: ['100%']}} : {}}>
|
||||
<Dialog.Handle />
|
||||
<Dialog.ScrollableInner label={_(msg`Report this message`)}>
|
||||
<DialogInner params={params} />
|
||||
<DialogInner message={message} />
|
||||
<Dialog.Close />
|
||||
</Dialog.ScrollableInner>
|
||||
</Dialog.Outer>
|
||||
)
|
||||
}
|
||||
ReportDialog = memo(ReportDialog)
|
||||
export {ReportDialog}
|
||||
MessageReportDialog = memo(MessageReportDialog)
|
||||
export {MessageReportDialog}
|
||||
|
||||
function DialogInner({params}: {params: ReportDialogParams}) {
|
||||
function DialogInner({message}: {message: ChatBskyConvoDefs.MessageView}) {
|
||||
const [reportOption, setReportOption] = useState<ReportOption | null>(null)
|
||||
|
||||
return reportOption ? (
|
||||
<SubmitStep
|
||||
params={params}
|
||||
message={message}
|
||||
reportOption={reportOption}
|
||||
goBack={() => setReportOption(null)}
|
||||
/>
|
||||
) : (
|
||||
<ReasonStep params={params} setReportOption={setReportOption} />
|
||||
<ReasonStep setReportOption={setReportOption} />
|
||||
)
|
||||
}
|
||||
|
||||
@@ -72,7 +66,6 @@ function ReasonStep({
|
||||
setReportOption,
|
||||
}: {
|
||||
setReportOption: (reportOption: ReportOption) => void
|
||||
params: ReportDialogParams
|
||||
}) {
|
||||
const control = Dialog.useDialogContext()
|
||||
|
||||
@@ -80,20 +73,18 @@ function ReasonStep({
|
||||
<SelectReportOptionView
|
||||
labelers={[]}
|
||||
goBack={control.close}
|
||||
params={{
|
||||
type: 'convoMessage',
|
||||
}}
|
||||
params={{type: 'message'}}
|
||||
onSelectReportOption={setReportOption}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SubmitStep({
|
||||
params,
|
||||
message,
|
||||
reportOption,
|
||||
goBack,
|
||||
}: {
|
||||
params: ReportDialogParams
|
||||
message: ChatBskyConvoDefs.MessageView
|
||||
reportOption: ReportOption
|
||||
goBack: () => void
|
||||
}) {
|
||||
@@ -102,7 +93,7 @@ function SubmitStep({
|
||||
const t = useTheme()
|
||||
const [details, setDetails] = useState('')
|
||||
const control = Dialog.useDialogContext()
|
||||
const agent = useAgent()
|
||||
const {getAgent} = useAgent()
|
||||
|
||||
const {
|
||||
mutate: submit,
|
||||
@@ -110,22 +101,17 @@ function SubmitStep({
|
||||
isPending: submitting,
|
||||
} = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (params.type === 'convoMessage') {
|
||||
const {convoId, message} = params
|
||||
const report = {
|
||||
reasonType: reportOption.reason,
|
||||
subject: {
|
||||
$type: 'chat.bsky.convo.defs#messageRef',
|
||||
messageId: message.id,
|
||||
did: message.sender!.did,
|
||||
} satisfies ChatBskyConvoDefs.MessageRef,
|
||||
reason: details,
|
||||
} satisfies ComAtprotoModerationCreateReport.InputSchema
|
||||
|
||||
const report = {
|
||||
reasonType: reportOption.reason,
|
||||
subject: {
|
||||
$type: 'chat.bsky.convo.defs#messageRef',
|
||||
messageId: message.id,
|
||||
convoId,
|
||||
did: message.sender.did,
|
||||
} satisfies ChatBskyConvoDefs.MessageRef,
|
||||
reason: details,
|
||||
} satisfies ComAtprotoModerationCreateReport.InputSchema
|
||||
|
||||
await agent.createModerationReport(report)
|
||||
}
|
||||
await getAgent().createModerationReport(report)
|
||||
},
|
||||
onSuccess: () => {
|
||||
control.close(() => {
|
||||
@@ -134,14 +120,6 @@ function SubmitStep({
|
||||
},
|
||||
})
|
||||
|
||||
const copy = useMemo(() => {
|
||||
return {
|
||||
convoMessage: {
|
||||
title: _(msg`Report this message`),
|
||||
},
|
||||
}[params.type]
|
||||
}, [_, params])
|
||||
|
||||
return (
|
||||
<View style={a.gap_lg}>
|
||||
<Button
|
||||
@@ -155,7 +133,9 @@ function SubmitStep({
|
||||
</Button>
|
||||
|
||||
<View style={[a.justify_center, gtMobile ? a.gap_sm : a.gap_xs]}>
|
||||
<Text style={[a.text_2xl, a.font_bold]}>{copy.title}</Text>
|
||||
<Text style={[a.text_2xl, a.font_bold]}>
|
||||
<Trans>Report this message</Trans>
|
||||
</Text>
|
||||
<Text style={[a.text_md, t.atoms.text_contrast_medium]}>
|
||||
<Trans>
|
||||
Your report will be sent to the Bluesky Moderation Service
|
||||
@@ -163,15 +143,10 @@ function SubmitStep({
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{params.type === 'convoMessage' && (
|
||||
<PreviewMessage message={params.message} />
|
||||
)}
|
||||
<PreviewMessage message={message} />
|
||||
|
||||
<Text style={[a.text_md, t.atoms.text_contrast_medium]}>
|
||||
<Text style={[a.font_bold, a.text_md, t.atoms.text_contrast_medium]}>
|
||||
<Trans>Reason:</Trans>
|
||||
</Text>{' '}
|
||||
<Text style={[a.font_bold, a.text_md]}>{reportOption.title}</Text>
|
||||
<Trans>Reason: {reportOption.title}</Trans>
|
||||
</Text>
|
||||
|
||||
<Divider />
|
||||
@@ -1,131 +0,0 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {AppBskyActorDefs, ModerationCause} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useProfileShadow} from 'state/cache/profile-shadow'
|
||||
import {useProfileBlockMutationQueue} from 'state/queries/profile'
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import {useDialogControl} from '#/components/Dialog'
|
||||
import {Divider} from '#/components/Divider'
|
||||
import {BlockedByListDialog} from '#/components/dms/BlockedByListDialog'
|
||||
import {LeaveConvoPrompt} from '#/components/dms/LeaveConvoPrompt'
|
||||
import {ReportConversationPrompt} from '#/components/dms/ReportConversationPrompt'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function MessagesListBlockedFooter({
|
||||
recipient: initialRecipient,
|
||||
convoId,
|
||||
hasMessages,
|
||||
blockInfo,
|
||||
}: {
|
||||
recipient: AppBskyActorDefs.ProfileViewBasic
|
||||
convoId: string
|
||||
hasMessages: boolean
|
||||
blockInfo: {
|
||||
listBlocks: ModerationCause[]
|
||||
userBlock: ModerationCause | undefined
|
||||
}
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const {_} = useLingui()
|
||||
const recipient = useProfileShadow(initialRecipient)
|
||||
const [__, queueUnblock] = useProfileBlockMutationQueue(recipient)
|
||||
|
||||
const leaveConvoControl = useDialogControl()
|
||||
const reportControl = useDialogControl()
|
||||
const blockedByListControl = useDialogControl()
|
||||
|
||||
const {listBlocks, userBlock} = blockInfo
|
||||
const isBlocking = !!userBlock || !!listBlocks.length
|
||||
|
||||
const onUnblockPress = React.useCallback(() => {
|
||||
if (listBlocks.length) {
|
||||
blockedByListControl.open()
|
||||
} else {
|
||||
queueUnblock()
|
||||
}
|
||||
}, [blockedByListControl, listBlocks, queueUnblock])
|
||||
|
||||
return (
|
||||
<View style={[hasMessages && a.pt_md, a.pb_xl, a.gap_lg]}>
|
||||
<Divider />
|
||||
<Text style={[a.text_md, a.font_bold, a.text_center]}>
|
||||
{isBlocking ? (
|
||||
<Trans>You have blocked this user</Trans>
|
||||
) : (
|
||||
<Trans>This user has blocked you</Trans>
|
||||
)}
|
||||
</Text>
|
||||
|
||||
<View style={[a.flex_row, a.justify_between, a.gap_lg, a.px_md]}>
|
||||
<Button
|
||||
label={_(msg`Leave chat`)}
|
||||
color="secondary"
|
||||
variant="solid"
|
||||
size="small"
|
||||
style={[a.flex_1]}
|
||||
onPress={leaveConvoControl.open}>
|
||||
<ButtonText style={{color: t.palette.negative_500}}>
|
||||
<Trans>Leave chat</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
<Button
|
||||
label={_(msg`Report`)}
|
||||
color="secondary"
|
||||
variant="solid"
|
||||
size="small"
|
||||
style={[a.flex_1]}
|
||||
onPress={reportControl.open}>
|
||||
<ButtonText style={{color: t.palette.negative_500}}>
|
||||
<Trans>Report</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
{isBlocking && gtMobile && (
|
||||
<Button
|
||||
label={_(msg`Unblock`)}
|
||||
color="secondary"
|
||||
variant="solid"
|
||||
size="small"
|
||||
style={[a.flex_1]}
|
||||
onPress={onUnblockPress}>
|
||||
<ButtonText style={{color: t.palette.primary_500}}>
|
||||
<Trans>Unblock</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
)}
|
||||
</View>
|
||||
{isBlocking && !gtMobile && (
|
||||
<View style={[a.flex_row, a.justify_center, a.px_md]}>
|
||||
<Button
|
||||
label={_(msg`Unblock`)}
|
||||
color="secondary"
|
||||
variant="solid"
|
||||
size="small"
|
||||
style={[a.flex_1]}
|
||||
onPress={onUnblockPress}>
|
||||
<ButtonText style={{color: t.palette.primary_500}}>
|
||||
<Trans>Unblock</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<LeaveConvoPrompt
|
||||
control={leaveConvoControl}
|
||||
currentScreen="conversation"
|
||||
convoId={convoId}
|
||||
/>
|
||||
|
||||
<ReportConversationPrompt control={reportControl} />
|
||||
|
||||
<BlockedByListDialog
|
||||
control={blockedByListControl}
|
||||
listBlocks={listBlocks}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -1,202 +0,0 @@
|
||||
import React, {useCallback} from 'react'
|
||||
import {TouchableOpacity, View} from 'react-native'
|
||||
import {
|
||||
AppBskyActorDefs,
|
||||
ModerationCause,
|
||||
ModerationDecision,
|
||||
} from '@atproto/api'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {BACK_HITSLOP} from 'lib/constants'
|
||||
import {makeProfileLink} from 'lib/routes/links'
|
||||
import {NavigationProp} from 'lib/routes/types'
|
||||
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
||||
import {isWeb} from 'platform/detection'
|
||||
import {useProfileShadow} from 'state/cache/profile-shadow'
|
||||
import {isConvoActive, useConvo} from 'state/messages/convo'
|
||||
import {PreviewableUserAvatar} from 'view/com/util/UserAvatar'
|
||||
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
|
||||
import {ConvoMenu} from '#/components/dms/ConvoMenu'
|
||||
import {Bell2Off_Filled_Corner0_Rounded as BellStroke} from '#/components/icons/Bell2'
|
||||
import {Link} from '#/components/Link'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
const PFP_SIZE = isWeb ? 40 : 34
|
||||
|
||||
export let MessagesListHeader = ({
|
||||
profile,
|
||||
moderation,
|
||||
blockInfo,
|
||||
}: {
|
||||
profile?: AppBskyActorDefs.ProfileViewBasic
|
||||
moderation?: ModerationDecision
|
||||
blockInfo?: {
|
||||
listBlocks: ModerationCause[]
|
||||
userBlock?: ModerationCause
|
||||
}
|
||||
}): React.ReactNode => {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const {gtTablet} = useBreakpoints()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
|
||||
const onPressBack = useCallback(() => {
|
||||
if (isWeb) {
|
||||
navigation.replace('Messages', {})
|
||||
} else {
|
||||
navigation.goBack()
|
||||
}
|
||||
}, [navigation])
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
t.atoms.bg,
|
||||
t.atoms.border_contrast_low,
|
||||
a.border_b,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.gap_sm,
|
||||
gtTablet ? a.pl_lg : a.pl_xl,
|
||||
a.pr_lg,
|
||||
a.py_sm,
|
||||
]}>
|
||||
{!gtTablet && (
|
||||
<TouchableOpacity
|
||||
testID="conversationHeaderBackBtn"
|
||||
onPress={onPressBack}
|
||||
hitSlop={BACK_HITSLOP}
|
||||
style={{width: 30, height: 30}}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Back`)}
|
||||
accessibilityHint="">
|
||||
<FontAwesomeIcon
|
||||
size={18}
|
||||
icon="angle-left"
|
||||
style={{
|
||||
marginTop: 6,
|
||||
}}
|
||||
color={t.atoms.text.color}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
{profile && moderation && blockInfo ? (
|
||||
<HeaderReady
|
||||
profile={profile}
|
||||
moderation={moderation}
|
||||
blockInfo={blockInfo}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<View style={[a.flex_row, a.align_center, a.gap_md, a.flex_1]}>
|
||||
<View
|
||||
style={[
|
||||
{width: PFP_SIZE, height: PFP_SIZE},
|
||||
a.rounded_full,
|
||||
t.atoms.bg_contrast_25,
|
||||
]}
|
||||
/>
|
||||
<View style={a.gap_xs}>
|
||||
<View
|
||||
style={[
|
||||
{width: 120, height: 16},
|
||||
a.rounded_xs,
|
||||
t.atoms.bg_contrast_25,
|
||||
a.mt_xs,
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
{width: 175, height: 12},
|
||||
a.rounded_xs,
|
||||
t.atoms.bg_contrast_25,
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={{width: 30}} />
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
MessagesListHeader = React.memo(MessagesListHeader)
|
||||
|
||||
function HeaderReady({
|
||||
profile: profileUnshadowed,
|
||||
moderation,
|
||||
blockInfo,
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileViewBasic
|
||||
moderation: ModerationDecision
|
||||
blockInfo: {
|
||||
listBlocks: ModerationCause[]
|
||||
userBlock?: ModerationCause
|
||||
}
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const convoState = useConvo()
|
||||
const profile = useProfileShadow(profileUnshadowed)
|
||||
|
||||
const isDeletedAccount = profile?.handle === 'missing.invalid'
|
||||
const displayName = isDeletedAccount
|
||||
? 'Deleted Account'
|
||||
: sanitizeDisplayName(
|
||||
profile.displayName || profile.handle,
|
||||
moderation.ui('displayName'),
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Link
|
||||
style={[a.flex_row, a.align_center, a.gap_md, a.flex_1, a.pr_md]}
|
||||
to={makeProfileLink(profile)}>
|
||||
<PreviewableUserAvatar
|
||||
size={PFP_SIZE}
|
||||
profile={profile}
|
||||
moderation={moderation.ui('avatar')}
|
||||
disableHoverCard={moderation.blocked}
|
||||
/>
|
||||
<View style={a.flex_1}>
|
||||
<Text
|
||||
style={[a.text_md, a.font_bold, web(a.leading_normal)]}
|
||||
numberOfLines={1}>
|
||||
{displayName}
|
||||
</Text>
|
||||
{!isDeletedAccount && (
|
||||
<Text
|
||||
style={[
|
||||
t.atoms.text_contrast_medium,
|
||||
a.text_sm,
|
||||
web([a.leading_normal, {marginTop: -2}]),
|
||||
]}
|
||||
numberOfLines={1}>
|
||||
@{profile.handle}
|
||||
{convoState.convo?.muted && (
|
||||
<>
|
||||
{' '}
|
||||
·{' '}
|
||||
<BellStroke size="xs" style={t.atoms.text_contrast_medium} />
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</Link>
|
||||
|
||||
{isConvoActive(convoState) && (
|
||||
<ConvoMenu
|
||||
convo={convoState.convo}
|
||||
profile={profile}
|
||||
currentScreen="conversation"
|
||||
blockInfo={blockInfo}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,175 +0,0 @@
|
||||
import React, {useCallback, useEffect} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {ChatBskyActorDeclaration} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useUpdateActorDeclaration} from '#/state/queries/messages/actor-declaration'
|
||||
import {useProfileQuery} from '#/state/queries/profile'
|
||||
import {useSession} from '#/state/session'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {atoms as a, useTheme, web} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import * as Toggle from '#/components/forms/Toggle'
|
||||
import {Message_Stroke2_Corner0_Rounded} from '#/components/icons/Message'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function MessagesNUX() {
|
||||
const control = Dialog.useDialogControl()
|
||||
|
||||
const {currentAccount} = useSession()
|
||||
const {data: profile} = useProfileQuery({
|
||||
did: currentAccount!.did,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (profile && typeof profile.associated?.chat === 'undefined') {
|
||||
const timeout = setTimeout(() => {
|
||||
control.open()
|
||||
}, 1000)
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
}
|
||||
}, [profile, control])
|
||||
|
||||
if (!profile) return null
|
||||
|
||||
return (
|
||||
<Dialog.Outer control={control}>
|
||||
<Dialog.Handle />
|
||||
<DialogInner chatDeclation={profile.associated?.chat} />
|
||||
</Dialog.Outer>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogInner({
|
||||
chatDeclation,
|
||||
}: {
|
||||
chatDeclation?: ChatBskyActorDeclaration.Record
|
||||
}) {
|
||||
const control = Dialog.useDialogContext()
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
|
||||
const [initialized, setInitialzed] = React.useState(false)
|
||||
const {mutate: updateDeclaration} = useUpdateActorDeclaration({
|
||||
onError: () => {
|
||||
Toast.show(_(msg`Failed to update settings`))
|
||||
},
|
||||
})
|
||||
|
||||
const onSelectItem = useCallback(
|
||||
(keys: string[]) => {
|
||||
const key = keys[0]
|
||||
if (!key) return
|
||||
updateDeclaration(key as 'all' | 'none' | 'following')
|
||||
},
|
||||
[updateDeclaration],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!chatDeclation && !initialized) {
|
||||
updateDeclaration('following')
|
||||
setInitialzed(true)
|
||||
}
|
||||
}, [chatDeclation, updateDeclaration, initialized])
|
||||
|
||||
return (
|
||||
<Dialog.ScrollableInner
|
||||
label={_(msg`Introducing Direct Messages`)}
|
||||
style={web({maxWidth: 440})}>
|
||||
<View style={a.gap_xl}>
|
||||
<View style={[a.align_center, a.pt_sm, a.pb_xs]}>
|
||||
<Message_Stroke2_Corner0_Rounded width={64} />
|
||||
<Text style={[a.text_2xl, a.font_bold, a.text_center, a.mt_md]}>
|
||||
<Trans>Direct messages are here!</Trans>
|
||||
</Text>
|
||||
<Text style={[a.text_md, a.text_center, a.mt_sm]}>
|
||||
<Trans>Privately chat with other users.</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
a.gap_xs,
|
||||
a.border,
|
||||
a.overflow_hidden,
|
||||
a.rounded_sm,
|
||||
t.atoms.border_contrast_low,
|
||||
]}>
|
||||
<View
|
||||
style={[
|
||||
a.p_md,
|
||||
a.border_b,
|
||||
t.atoms.bg_contrast_25,
|
||||
t.atoms.border_contrast_low,
|
||||
]}>
|
||||
<Text style={[a.text_sm, a.font_bold]}>
|
||||
<Trans>Who can message you?</Trans>
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
a.mt_xs,
|
||||
a.text_sm,
|
||||
a.italic,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}>
|
||||
<Trans>You can change this at any time.</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[a.px_md, a.py_xs]}>
|
||||
<Toggle.Group
|
||||
label={_(msg`Who can message you?`)}
|
||||
type="radio"
|
||||
values={[chatDeclation?.allowIncoming ?? 'following']}
|
||||
onChange={onSelectItem}>
|
||||
<View>
|
||||
<Toggle.Item
|
||||
name="all"
|
||||
label={_(msg`Everyone`)}
|
||||
style={[a.justify_between, a.py_sm, a.rounded_2xs]}>
|
||||
<Toggle.LabelText>
|
||||
<Trans>Everyone</Trans>
|
||||
</Toggle.LabelText>
|
||||
<Toggle.Radio />
|
||||
</Toggle.Item>
|
||||
<Toggle.Item
|
||||
name="following"
|
||||
label={_(msg`Users I follow`)}
|
||||
style={[a.justify_between, a.py_sm, a.rounded_2xs]}>
|
||||
<Toggle.LabelText>
|
||||
<Trans>Users I follow</Trans>
|
||||
</Toggle.LabelText>
|
||||
<Toggle.Radio />
|
||||
</Toggle.Item>
|
||||
<Toggle.Item
|
||||
name="none"
|
||||
label={_(msg`No one`)}
|
||||
style={[a.justify_between, a.py_sm, a.rounded_2xs]}>
|
||||
<Toggle.LabelText>
|
||||
<Trans>No one</Trans>
|
||||
</Toggle.LabelText>
|
||||
<Toggle.Radio />
|
||||
</Toggle.Item>
|
||||
</View>
|
||||
</Toggle.Group>
|
||||
</View>
|
||||
</View>
|
||||
<Button
|
||||
label={_(msg`Start chatting`)}
|
||||
accessibilityHint={_(msg`Close modal`)}
|
||||
size="medium"
|
||||
color="primary"
|
||||
variant="solid"
|
||||
onPress={() => control.close()}>
|
||||
<ButtonText>
|
||||
<Trans>Get started</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
<Dialog.Close />
|
||||
</Dialog.ScrollableInner>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
import React, {useCallback, useMemo, useRef, useState} from 'react'
|
||||
import {Keyboard, View} from 'react-native'
|
||||
import {AppBskyActorDefs, moderateProfile} from '@atproto/api'
|
||||
import {BottomSheetFlatListMethods} from '@discord/bottom-sheet'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
|
||||
import {useSession} from '#/state/session'
|
||||
import {useActorAutocompleteQuery} from 'state/queries/actor-autocomplete'
|
||||
import {FAB} from '#/view/com/util/fab/FAB'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {atoms as a, useTheme, web} from '#/alf'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import * as TextField from '#/components/forms/TextField'
|
||||
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
|
||||
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
||||
import {Button} from '../Button'
|
||||
import {Envelope_Stroke2_Corner0_Rounded as Envelope} from '../icons/Envelope'
|
||||
import {ListMaybePlaceholder} from '../Lists'
|
||||
import {Text} from '../Typography'
|
||||
import {canBeMessaged} from './util'
|
||||
|
||||
export function NewChat({
|
||||
control,
|
||||
onNewChat,
|
||||
}: {
|
||||
control: Dialog.DialogControlProps
|
||||
onNewChat: (chatId: string) => void
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
|
||||
const {mutate: createChat} = useGetConvoForMembers({
|
||||
onSuccess: data => {
|
||||
onNewChat(data.convo.id)
|
||||
},
|
||||
onError: error => {
|
||||
Toast.show(error.message)
|
||||
},
|
||||
})
|
||||
|
||||
const onCreateChat = useCallback(
|
||||
(did: string) => {
|
||||
control.close(() => createChat([did]))
|
||||
},
|
||||
[control, createChat],
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<FAB
|
||||
testID="newChatFAB"
|
||||
onPress={control.open}
|
||||
icon={<Plus size="lg" fill={t.palette.white} />}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`New chat`)}
|
||||
accessibilityHint=""
|
||||
/>
|
||||
|
||||
<Dialog.Outer
|
||||
control={control}
|
||||
testID="newChatDialog"
|
||||
nativeOptions={{sheet: {snapPoints: ['100%']}}}>
|
||||
<Dialog.Handle />
|
||||
<SearchablePeopleList onCreateChat={onCreateChat} />
|
||||
</Dialog.Outer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function SearchablePeopleList({
|
||||
onCreateChat,
|
||||
}: {
|
||||
onCreateChat: (did: string) => void
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const moderationOpts = useModerationOpts()
|
||||
const control = Dialog.useDialogContext()
|
||||
const listRef = useRef<BottomSheetFlatListMethods>(null)
|
||||
const {currentAccount} = useSession()
|
||||
|
||||
const [searchText, setSearchText] = useState('')
|
||||
|
||||
const {
|
||||
data: actorAutocompleteData,
|
||||
isFetching,
|
||||
isError,
|
||||
refetch,
|
||||
} = useActorAutocompleteQuery(searchText, true)
|
||||
|
||||
const renderItem = useCallback(
|
||||
({item: profile}: {item: AppBskyActorDefs.ProfileView}) => {
|
||||
if (!moderationOpts) return null
|
||||
|
||||
const moderation = moderateProfile(profile, moderationOpts)
|
||||
|
||||
const disabled = !canBeMessaged(profile)
|
||||
const handle = sanitizeHandle(profile.handle, '@')
|
||||
|
||||
return (
|
||||
<Button
|
||||
label={profile.displayName || sanitizeHandle(profile.handle)}
|
||||
onPress={() => !disabled && onCreateChat(profile.did)}>
|
||||
{({hovered, pressed, focused}) => (
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.px_md,
|
||||
a.py_sm,
|
||||
a.gap_md,
|
||||
a.align_center,
|
||||
a.flex_row,
|
||||
a.rounded_sm,
|
||||
disabled
|
||||
? {opacity: 0.5}
|
||||
: pressed || focused
|
||||
? t.atoms.bg_contrast_25
|
||||
: hovered
|
||||
? t.atoms.bg_contrast_50
|
||||
: t.atoms.bg,
|
||||
]}>
|
||||
<UserAvatar
|
||||
size={40}
|
||||
avatar={profile.avatar}
|
||||
moderation={moderation.ui('avatar')}
|
||||
type={profile.associated?.labeler ? 'labeler' : 'user'}
|
||||
/>
|
||||
<View style={{flex: 1}}>
|
||||
<Text
|
||||
style={[t.atoms.text, a.font_bold, a.leading_snug]}
|
||||
numberOfLines={1}>
|
||||
{sanitizeDisplayName(
|
||||
profile.displayName || sanitizeHandle(profile.handle),
|
||||
moderation.ui('displayName'),
|
||||
)}
|
||||
</Text>
|
||||
<Text style={t.atoms.text_contrast_high} numberOfLines={2}>
|
||||
{disabled ? (
|
||||
<Trans>{handle} can't be messaged</Trans>
|
||||
) : (
|
||||
handle
|
||||
)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</Button>
|
||||
)
|
||||
},
|
||||
[
|
||||
moderationOpts,
|
||||
onCreateChat,
|
||||
t.atoms.bg_contrast_25,
|
||||
t.atoms.bg_contrast_50,
|
||||
t.atoms.bg,
|
||||
t.atoms.text,
|
||||
t.atoms.text_contrast_high,
|
||||
],
|
||||
)
|
||||
|
||||
const listHeader = useMemo(() => {
|
||||
return (
|
||||
<View style={[a.relative, a.mb_lg]}>
|
||||
{/* cover top corners */}
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.inset_0,
|
||||
{
|
||||
borderBottomLeftRadius: 8,
|
||||
borderBottomRightRadius: 8,
|
||||
},
|
||||
t.atoms.bg,
|
||||
]}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
a.text_2xl,
|
||||
a.font_bold,
|
||||
a.leading_tight,
|
||||
a.pb_lg,
|
||||
web(a.pt_lg),
|
||||
]}>
|
||||
<Trans>Start a new chat</Trans>
|
||||
</Text>
|
||||
<TextField.Root>
|
||||
<TextField.Icon icon={Search} />
|
||||
<Dialog.Input
|
||||
label={_(msg`Search profiles`)}
|
||||
placeholder={_(msg`Search`)}
|
||||
value={searchText}
|
||||
onChangeText={text => {
|
||||
setSearchText(text)
|
||||
listRef.current?.scrollToOffset({offset: 0, animated: false})
|
||||
}}
|
||||
returnKeyType="search"
|
||||
clearButtonMode="while-editing"
|
||||
maxLength={50}
|
||||
onKeyPress={({nativeEvent}) => {
|
||||
if (nativeEvent.key === 'Escape') {
|
||||
control.close()
|
||||
}
|
||||
}}
|
||||
autoCorrect={false}
|
||||
autoComplete="off"
|
||||
autoCapitalize="none"
|
||||
autoFocus
|
||||
/>
|
||||
</TextField.Root>
|
||||
<Dialog.Close />
|
||||
</View>
|
||||
)
|
||||
}, [t.atoms.bg, _, control, searchText])
|
||||
|
||||
const dataWithoutSelf = useMemo(() => {
|
||||
return (
|
||||
actorAutocompleteData?.filter(
|
||||
profile => profile.did !== currentAccount?.did,
|
||||
) ?? []
|
||||
)
|
||||
}, [actorAutocompleteData, currentAccount?.did])
|
||||
|
||||
return (
|
||||
<Dialog.InnerFlatList
|
||||
ref={listRef}
|
||||
data={dataWithoutSelf}
|
||||
renderItem={renderItem}
|
||||
ListHeaderComponent={
|
||||
<>
|
||||
{listHeader}
|
||||
{searchText.length === 0 ? (
|
||||
<View style={[a.pt_4xl, a.align_center, a.px_lg]}>
|
||||
<Envelope width={64} fill={t.palette.contrast_200} />
|
||||
<Text
|
||||
style={[
|
||||
a.text_lg,
|
||||
a.text_center,
|
||||
a.mt_md,
|
||||
t.atoms.text_contrast_low,
|
||||
]}>
|
||||
<Trans>Search for someone to start a conversation with.</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
!actorAutocompleteData?.length && (
|
||||
<ListMaybePlaceholder
|
||||
isLoading={isFetching}
|
||||
isError={isError}
|
||||
onRetry={refetch}
|
||||
hideBackButton={true}
|
||||
emptyType="results"
|
||||
sideBorders={false}
|
||||
topBorder={false}
|
||||
emptyMessage={
|
||||
isError
|
||||
? _(msg`No search results found for "${searchText}".`)
|
||||
: _(msg`Could not load profiles. Please try again later.`)
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
}
|
||||
stickyHeaderIndices={[0]}
|
||||
keyExtractor={(item: AppBskyActorDefs.ProfileView) => item.did}
|
||||
// @ts-expect-error web only
|
||||
style={isWeb && {minHeight: '100vh'}}
|
||||
onScrollBeginDrag={() => Keyboard.dismiss()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export {BottomSheetTextInput as TextInput} from '@discord/bottom-sheet/src'
|
||||
@@ -1 +0,0 @@
|
||||
export {TextInput} from 'react-native'
|
||||
@@ -1,509 +0,0 @@
|
||||
import React, {
|
||||
useCallback,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import type {TextInput as TextInputType} from 'react-native'
|
||||
import {View} from 'react-native'
|
||||
import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
|
||||
import {BottomSheetFlatListMethods} from '@discord/bottom-sheet'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
|
||||
import {useProfileFollowsQuery} from '#/state/queries/profile-follows'
|
||||
import {useSession} from '#/state/session'
|
||||
import {logEvent} from 'lib/statsig/statsig'
|
||||
import {useActorAutocompleteQuery} from 'state/queries/actor-autocomplete'
|
||||
import {FAB} from '#/view/com/util/fab/FAB'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {atoms as a, native, useTheme, web} from '#/alf'
|
||||
import {Button} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {TextInput} from '#/components/dms/NewChatDialog/TextInput'
|
||||
import {canBeMessaged} from '#/components/dms/util'
|
||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||
import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
|
||||
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
|
||||
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
||||
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
type Item =
|
||||
| {
|
||||
type: 'profile'
|
||||
key: string
|
||||
enabled: boolean
|
||||
profile: AppBskyActorDefs.ProfileView
|
||||
}
|
||||
| {
|
||||
type: 'empty'
|
||||
key: string
|
||||
message: string
|
||||
}
|
||||
| {
|
||||
type: 'placeholder'
|
||||
key: string
|
||||
}
|
||||
| {
|
||||
type: 'error'
|
||||
key: string
|
||||
}
|
||||
|
||||
export function NewChat({
|
||||
control,
|
||||
onNewChat,
|
||||
}: {
|
||||
control: Dialog.DialogControlProps
|
||||
onNewChat: (chatId: string) => void
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
|
||||
const {mutate: createChat} = useGetConvoForMembers({
|
||||
onSuccess: data => {
|
||||
onNewChat(data.convo.id)
|
||||
|
||||
if (!data.convo.lastMessage) {
|
||||
logEvent('chat:create', {logContext: 'NewChatDialog'})
|
||||
}
|
||||
logEvent('chat:open', {logContext: 'NewChatDialog'})
|
||||
},
|
||||
onError: error => {
|
||||
Toast.show(error.message)
|
||||
},
|
||||
})
|
||||
|
||||
const onCreateChat = useCallback(
|
||||
(did: string) => {
|
||||
control.close(() => createChat([did]))
|
||||
},
|
||||
[control, createChat],
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<FAB
|
||||
testID="newChatFAB"
|
||||
onPress={control.open}
|
||||
icon={<Plus size="lg" fill={t.palette.white} />}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`New chat`)}
|
||||
accessibilityHint=""
|
||||
/>
|
||||
|
||||
<Dialog.Outer
|
||||
control={control}
|
||||
testID="newChatDialog"
|
||||
nativeOptions={{sheet: {snapPoints: ['100%']}}}>
|
||||
<SearchablePeopleList onCreateChat={onCreateChat} />
|
||||
</Dialog.Outer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function ProfileCard({
|
||||
enabled,
|
||||
profile,
|
||||
moderationOpts,
|
||||
onPress,
|
||||
}: {
|
||||
enabled: boolean
|
||||
profile: AppBskyActorDefs.ProfileView
|
||||
moderationOpts: ModerationOpts
|
||||
onPress: (did: string) => void
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const moderation = moderateProfile(profile, moderationOpts)
|
||||
const handle = sanitizeHandle(profile.handle, '@')
|
||||
const displayName = sanitizeDisplayName(
|
||||
profile.displayName || sanitizeHandle(profile.handle),
|
||||
moderation.ui('displayName'),
|
||||
)
|
||||
|
||||
const handleOnPress = useCallback(() => {
|
||||
onPress(profile.did)
|
||||
}, [onPress, profile.did])
|
||||
|
||||
return (
|
||||
<Button
|
||||
disabled={!enabled}
|
||||
label={_(msg`Start chat with ${displayName}`)}
|
||||
onPress={handleOnPress}>
|
||||
{({hovered, pressed, focused}) => (
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.py_md,
|
||||
a.px_lg,
|
||||
a.gap_md,
|
||||
a.align_center,
|
||||
a.flex_row,
|
||||
!enabled
|
||||
? {opacity: 0.5}
|
||||
: pressed || focused
|
||||
? t.atoms.bg_contrast_25
|
||||
: hovered
|
||||
? t.atoms.bg_contrast_50
|
||||
: t.atoms.bg,
|
||||
]}>
|
||||
<UserAvatar
|
||||
size={42}
|
||||
avatar={profile.avatar}
|
||||
moderation={moderation.ui('avatar')}
|
||||
type={profile.associated?.labeler ? 'labeler' : 'user'}
|
||||
/>
|
||||
<View style={[a.flex_1, a.gap_2xs]}>
|
||||
<Text
|
||||
style={[t.atoms.text, a.font_bold, a.leading_snug]}
|
||||
numberOfLines={1}>
|
||||
{displayName}
|
||||
</Text>
|
||||
<Text style={t.atoms.text_contrast_high} numberOfLines={2}>
|
||||
{!enabled ? <Trans>{handle} can't be messaged</Trans> : handle}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
function ProfileCardSkeleton() {
|
||||
const t = useTheme()
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.py_md,
|
||||
a.px_lg,
|
||||
a.gap_md,
|
||||
a.align_center,
|
||||
a.flex_row,
|
||||
]}>
|
||||
<View
|
||||
style={[
|
||||
a.rounded_full,
|
||||
{width: 42, height: 42},
|
||||
t.atoms.bg_contrast_25,
|
||||
]}
|
||||
/>
|
||||
|
||||
<View style={[a.flex_1, a.gap_sm]}>
|
||||
<View
|
||||
style={[
|
||||
a.rounded_xs,
|
||||
{width: 80, height: 14},
|
||||
t.atoms.bg_contrast_25,
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.rounded_xs,
|
||||
{width: 120, height: 10},
|
||||
t.atoms.bg_contrast_25,
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function Empty({message}: {message: string}) {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<View style={[a.p_lg, a.py_xl, a.align_center, a.gap_md]}>
|
||||
<Text style={[a.text_sm, a.italic, t.atoms.text_contrast_high]}>
|
||||
{message}
|
||||
</Text>
|
||||
|
||||
<Text style={[a.text_xs, t.atoms.text_contrast_low]}>(╯°□°)╯︵ ┻━┻</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function SearchInput({
|
||||
value,
|
||||
onChangeText,
|
||||
onEscape,
|
||||
inputRef,
|
||||
}: {
|
||||
value: string
|
||||
onChangeText: (text: string) => void
|
||||
onEscape: () => void
|
||||
inputRef: React.RefObject<TextInputType>
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const {
|
||||
state: hovered,
|
||||
onIn: onMouseEnter,
|
||||
onOut: onMouseLeave,
|
||||
} = useInteractionState()
|
||||
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
|
||||
const interacted = hovered || focused
|
||||
|
||||
return (
|
||||
<View
|
||||
{...web({
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
})}
|
||||
style={[a.flex_row, a.align_center, a.gap_sm]}>
|
||||
<Search
|
||||
size="md"
|
||||
fill={interacted ? t.palette.primary_500 : t.palette.contrast_300}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
// @ts-ignore bottom sheet input types issue — esb
|
||||
ref={inputRef}
|
||||
placeholder={_(msg`Search`)}
|
||||
value={value}
|
||||
onChangeText={onChangeText}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
style={[a.flex_1, a.py_md, a.text_md, t.atoms.text]}
|
||||
placeholderTextColor={t.palette.contrast_500}
|
||||
keyboardAppearance={t.name === 'light' ? 'light' : 'dark'}
|
||||
returnKeyType="search"
|
||||
clearButtonMode="while-editing"
|
||||
maxLength={50}
|
||||
onKeyPress={({nativeEvent}) => {
|
||||
if (nativeEvent.key === 'Escape') {
|
||||
onEscape()
|
||||
}
|
||||
}}
|
||||
autoCorrect={false}
|
||||
autoComplete="off"
|
||||
autoCapitalize="none"
|
||||
autoFocus
|
||||
accessibilityLabel={_(msg`Search profiles`)}
|
||||
accessibilityHint={_(msg`Search profiles`)}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function SearchablePeopleList({
|
||||
onCreateChat,
|
||||
}: {
|
||||
onCreateChat: (did: string) => void
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const moderationOpts = useModerationOpts()
|
||||
const control = Dialog.useDialogContext()
|
||||
const listRef = useRef<BottomSheetFlatListMethods>(null)
|
||||
const {currentAccount} = useSession()
|
||||
const inputRef = useRef<TextInputType>(null)
|
||||
|
||||
const [searchText, setSearchText] = useState('')
|
||||
|
||||
const {
|
||||
data: results,
|
||||
isError,
|
||||
isFetching,
|
||||
} = useActorAutocompleteQuery(searchText, true, 12)
|
||||
const {data: follows} = useProfileFollowsQuery(currentAccount?.did)
|
||||
|
||||
const items = useMemo(() => {
|
||||
let _items: Item[] = []
|
||||
|
||||
if (isError) {
|
||||
_items.push({
|
||||
type: 'empty',
|
||||
key: 'empty',
|
||||
message: _(msg`We're having network issues, try again`),
|
||||
})
|
||||
} else if (searchText.length) {
|
||||
if (results?.length) {
|
||||
for (const profile of results) {
|
||||
if (profile.did === currentAccount?.did) continue
|
||||
_items.push({
|
||||
type: 'profile',
|
||||
key: profile.did,
|
||||
enabled: canBeMessaged(profile),
|
||||
profile,
|
||||
})
|
||||
}
|
||||
|
||||
_items = _items.sort(a => {
|
||||
// @ts-ignore
|
||||
return a.enabled ? -1 : 1
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if (follows) {
|
||||
for (const page of follows.pages) {
|
||||
for (const profile of page.follows) {
|
||||
_items.push({
|
||||
type: 'profile',
|
||||
key: profile.did,
|
||||
enabled: canBeMessaged(profile),
|
||||
profile,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
_items = _items.sort(a => {
|
||||
// @ts-ignore
|
||||
return a.enabled ? -1 : 1
|
||||
})
|
||||
} else {
|
||||
Array(10)
|
||||
.fill(0)
|
||||
.forEach((_, i) => {
|
||||
_items.push({
|
||||
type: 'placeholder',
|
||||
key: i + '',
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return _items
|
||||
}, [_, searchText, results, isError, currentAccount?.did, follows])
|
||||
|
||||
if (searchText && !isFetching && !items.length && !isError) {
|
||||
items.push({type: 'empty', key: 'empty', message: _(msg`No results`)})
|
||||
}
|
||||
|
||||
const renderItems = useCallback(
|
||||
({item}: {item: Item}) => {
|
||||
switch (item.type) {
|
||||
case 'profile': {
|
||||
return (
|
||||
<ProfileCard
|
||||
key={item.key}
|
||||
enabled={item.enabled}
|
||||
profile={item.profile}
|
||||
moderationOpts={moderationOpts!}
|
||||
onPress={onCreateChat}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case 'placeholder': {
|
||||
return <ProfileCardSkeleton key={item.key} />
|
||||
}
|
||||
case 'empty': {
|
||||
return <Empty key={item.key} message={item.message} />
|
||||
}
|
||||
default:
|
||||
return null
|
||||
}
|
||||
},
|
||||
[moderationOpts, onCreateChat],
|
||||
)
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (isWeb) {
|
||||
setImmediate(() => {
|
||||
inputRef?.current?.focus()
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
const listHeader = useMemo(() => {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.relative,
|
||||
a.pt_md,
|
||||
a.pb_xs,
|
||||
a.px_lg,
|
||||
a.border_b,
|
||||
t.atoms.border_contrast_low,
|
||||
t.atoms.bg,
|
||||
native([a.pt_lg]),
|
||||
]}>
|
||||
<View
|
||||
style={[
|
||||
a.relative,
|
||||
native(a.align_center),
|
||||
a.justify_center,
|
||||
{height: 32},
|
||||
]}>
|
||||
<Button
|
||||
label={_(msg`Close`)}
|
||||
size="small"
|
||||
shape="round"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
style={[
|
||||
a.absolute,
|
||||
a.z_20,
|
||||
native({
|
||||
left: -7,
|
||||
}),
|
||||
web({
|
||||
right: -4,
|
||||
}),
|
||||
]}
|
||||
onPress={() => control.close()}>
|
||||
{isWeb ? (
|
||||
<X size="md" fill={t.palette.contrast_500} />
|
||||
) : (
|
||||
<ChevronLeft size="md" fill={t.palette.contrast_500} />
|
||||
)}
|
||||
</Button>
|
||||
<Text
|
||||
style={[
|
||||
a.z_10,
|
||||
a.text_lg,
|
||||
a.font_bold,
|
||||
a.leading_tight,
|
||||
t.atoms.text_contrast_high,
|
||||
]}>
|
||||
<Trans>Start a new chat</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={[native([a.pt_sm]), web([a.pt_xs])]}>
|
||||
<SearchInput
|
||||
inputRef={inputRef}
|
||||
value={searchText}
|
||||
onChangeText={text => {
|
||||
setSearchText(text)
|
||||
listRef.current?.scrollToOffset({offset: 0, animated: false})
|
||||
}}
|
||||
onEscape={control.close}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}, [t, _, control, searchText])
|
||||
|
||||
return (
|
||||
<Dialog.InnerFlatList
|
||||
ref={listRef}
|
||||
data={items}
|
||||
renderItem={renderItems}
|
||||
ListHeaderComponent={listHeader}
|
||||
stickyHeaderIndices={[0]}
|
||||
keyExtractor={(item: Item) => item.key}
|
||||
style={[
|
||||
web([a.py_0, {height: '100vh', maxHeight: 600}, a.px_0]),
|
||||
native({
|
||||
height: '100%',
|
||||
paddingHorizontal: 0,
|
||||
marginTop: 0,
|
||||
paddingTop: 0,
|
||||
borderTopLeftRadius: 40,
|
||||
borderTopRightRadius: 40,
|
||||
}),
|
||||
]}
|
||||
webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]}
|
||||
keyboardDismissMode="on-drag"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,97 +1,47 @@
|
||||
import React from 'react'
|
||||
import {Pressable, View} from 'react-native'
|
||||
import Animated, {
|
||||
runOnJS,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withTiming,
|
||||
} from 'react-native-reanimated'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {View} from 'react-native'
|
||||
import Animated from 'react-native-reanimated'
|
||||
import {Trans} from '@lingui/macro'
|
||||
|
||||
import {
|
||||
ScaleAndFadeIn,
|
||||
ScaleAndFadeOut,
|
||||
} from 'lib/custom-animations/ScaleAndFade'
|
||||
import {useHaptics} from 'lib/haptics'
|
||||
import {isAndroid, isIOS, isWeb} from 'platform/detection'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
const AnimatedPressable = Animated.createAnimatedComponent(Pressable)
|
||||
|
||||
export function NewMessagesPill({
|
||||
onPress: onPressInner,
|
||||
}: {
|
||||
onPress: () => void
|
||||
}) {
|
||||
export function NewMessagesPill() {
|
||||
const t = useTheme()
|
||||
const playHaptic = useHaptics()
|
||||
const {bottom: bottomInset} = useSafeAreaInsets()
|
||||
const bottomBarHeight = isIOS ? 42 : isAndroid ? 60 : 0
|
||||
const bottomOffset = isWeb ? 0 : bottomInset + bottomBarHeight
|
||||
|
||||
const scale = useSharedValue(1)
|
||||
|
||||
const onPressIn = React.useCallback(() => {
|
||||
if (isWeb) return
|
||||
scale.value = withTiming(1.075, {duration: 100})
|
||||
}, [scale])
|
||||
|
||||
const onPressOut = React.useCallback(() => {
|
||||
if (isWeb) return
|
||||
scale.value = withTiming(1, {duration: 100})
|
||||
}, [scale])
|
||||
|
||||
const onPress = React.useCallback(() => {
|
||||
runOnJS(playHaptic)()
|
||||
onPressInner?.()
|
||||
}, [onPressInner, playHaptic])
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [{scale: scale.value}],
|
||||
}))
|
||||
React.useEffect(() => {}, [])
|
||||
|
||||
return (
|
||||
<View
|
||||
<Animated.View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.w_full,
|
||||
a.z_10,
|
||||
a.align_center,
|
||||
a.py_sm,
|
||||
a.rounded_full,
|
||||
a.shadow_sm,
|
||||
a.border,
|
||||
t.atoms.bg_contrast_50,
|
||||
t.atoms.border_contrast_medium,
|
||||
{
|
||||
bottom: bottomOffset + 70,
|
||||
// Don't prevent scrolling in this area _except_ for in the pill itself
|
||||
pointerEvents: 'box-none',
|
||||
position: 'absolute',
|
||||
bottom: 70,
|
||||
width: '40%',
|
||||
left: '30%',
|
||||
alignItems: 'center',
|
||||
shadowOpacity: 0.125,
|
||||
shadowRadius: 12,
|
||||
shadowOffset: {width: 0, height: 5},
|
||||
},
|
||||
]}>
|
||||
<AnimatedPressable
|
||||
style={[
|
||||
a.py_sm,
|
||||
a.rounded_full,
|
||||
a.shadow_sm,
|
||||
a.border,
|
||||
t.atoms.bg_contrast_50,
|
||||
t.atoms.border_contrast_medium,
|
||||
{
|
||||
width: 160,
|
||||
alignItems: 'center',
|
||||
shadowOpacity: 0.125,
|
||||
shadowRadius: 12,
|
||||
shadowOffset: {width: 0, height: 5},
|
||||
pointerEvents: 'box-only',
|
||||
},
|
||||
animatedStyle,
|
||||
]}
|
||||
entering={ScaleAndFadeIn}
|
||||
exiting={ScaleAndFadeOut}
|
||||
onPress={onPress}
|
||||
onPressIn={onPressIn}
|
||||
onPressOut={onPressOut}>
|
||||
]}
|
||||
entering={ScaleAndFadeIn}
|
||||
exiting={ScaleAndFadeOut}>
|
||||
<View style={{flex: 1}}>
|
||||
<Text style={[a.font_bold]}>
|
||||
<Trans>New messages</Trans>
|
||||
</Text>
|
||||
</AnimatedPressable>
|
||||
</View>
|
||||
</View>
|
||||
</Animated.View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import React from 'react'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {DialogControlProps} from '#/components/Dialog'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
|
||||
export function ReportConversationPrompt({
|
||||
control,
|
||||
}: {
|
||||
control: DialogControlProps
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
|
||||
return (
|
||||
<Prompt.Basic
|
||||
control={control}
|
||||
title={_(msg`Report conversation`)}
|
||||
description={_(
|
||||
msg`To report a conversation, please report one of its messages via the conversation screen. This lets our moderators understand the context of your issue.`,
|
||||
)}
|
||||
confirmButtonCta={_(msg`I understand`)}
|
||||
onConfirm={() => {}}
|
||||
showCancel={false}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -7,7 +7,7 @@ export function useRichText(text: string): [RichTextAPI, boolean] {
|
||||
const [prevText, setPrevText] = React.useState(text)
|
||||
const [rawRT, setRawRT] = React.useState(() => new RichTextAPI({text}))
|
||||
const [resolvedRT, setResolvedRT] = React.useState<RichTextAPI | null>(null)
|
||||
const agent = useAgent()
|
||||
const {getAgent} = useAgent()
|
||||
if (text !== prevText) {
|
||||
setPrevText(text)
|
||||
setRawRT(new RichTextAPI({text}))
|
||||
@@ -19,7 +19,7 @@ export function useRichText(text: string): [RichTextAPI, boolean] {
|
||||
async function resolveRTFacets() {
|
||||
// new each time
|
||||
const resolvedRT = new RichTextAPI({text})
|
||||
await resolvedRT.detectFacets(agent)
|
||||
await resolvedRT.detectFacets(getAgent())
|
||||
if (!ignore) {
|
||||
setResolvedRT(resolvedRT)
|
||||
}
|
||||
@@ -28,7 +28,7 @@ export function useRichText(text: string): [RichTextAPI, boolean] {
|
||||
return () => {
|
||||
ignore = true
|
||||
}
|
||||
}, [text, agent])
|
||||
}, [text, getAgent])
|
||||
const isResolving = resolvedRT === null
|
||||
return [resolvedRT ?? rawRT, isResolving]
|
||||
}
|
||||
|
||||
@@ -3,7 +3,3 @@ import {createSinglePathSVG} from './TEMPLATE'
|
||||
export const Globe_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M4.062 11h2.961c.103-2.204.545-4.218 1.235-5.77.06-.136.123-.269.188-.399A8.007 8.007 0 0 0 4.062 11ZM12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2Zm0 2c-.227 0-.518.1-.868.432-.354.337-.719.872-1.047 1.61-.561 1.263-.958 2.991-1.06 4.958h5.95c-.102-1.967-.499-3.695-1.06-4.958-.328-.738-.693-1.273-1.047-1.61C12.518 4.099 12.227 4 12 4Zm4.977 7c-.103-2.204-.545-4.218-1.235-5.77a9.78 9.78 0 0 0-.188-.399A8.006 8.006 0 0 1 19.938 11h-2.961Zm-2.003 2H9.026c.101 1.966.498 3.695 1.06 4.958.327.738.692 1.273 1.046 1.61.35.333.641.432.868.432.227 0 .518-.1.868-.432.354-.337.719-.872 1.047-1.61.561-1.263.958-2.991 1.06-4.958Zm.58 6.169c.065-.13.128-.263.188-.399.69-1.552 1.132-3.566 1.235-5.77h2.961a8.006 8.006 0 0 1-4.384 6.169Zm-7.108 0a9.877 9.877 0 0 1-.188-.399c-.69-1.552-1.132-3.566-1.235-5.77H4.062a8.006 8.006 0 0 0 4.384 6.169Z',
|
||||
})
|
||||
|
||||
export const Earth_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M4.4 9.493C4.14 10.28 4 11.124 4 12a8 8 0 1 0 10.899-7.459l-.953 3.81a1 1 0 0 1-.726.727l-3.444.866-.772 1.533a1 1 0 0 1-1.493.35L4.4 9.493Zm.883-1.84L7.756 9.51l.44-.874a1 1 0 0 1 .649-.52l3.306-.832.807-3.227a7.993 7.993 0 0 0-7.676 3.597ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm8.43.162a1 1 0 0 1 .77-.29l1.89.121a1 1 0 0 1 .494.168l2.869 1.928a1 1 0 0 1 .336 1.277l-.973 1.946a1 1 0 0 1-.894.553h-2.92a1 1 0 0 1-.831-.445L9.225 14.5a1 1 0 0 1 .126-1.262l1.08-1.076Zm.915 1.913.177-.177 1.171.074 1.914 1.286-.303.607h-1.766l-1.194-1.79Z',
|
||||
})
|
||||
|
||||
@@ -55,13 +55,13 @@ export function LabelsOnMe({
|
||||
<Plural
|
||||
value={labels.length}
|
||||
one="# label has been placed on this account"
|
||||
other="# labels have been placed on this account"
|
||||
other="# labels has been placed on this account"
|
||||
/>
|
||||
) : (
|
||||
<Plural
|
||||
value={labels.length}
|
||||
one="# label has been placed on this content"
|
||||
other="# labels have been placed on this content"
|
||||
other="# labels has been placed on this content"
|
||||
/>
|
||||
)}
|
||||
</ButtonText>
|
||||
|
||||
@@ -3,22 +3,19 @@ import {View} from 'react-native'
|
||||
import {ComAtprotoLabelDefs, ComAtprotoModerationDefs} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useMutation} from '@tanstack/react-query'
|
||||
|
||||
import {useLabelInfo} from '#/lib/moderation/useLabelInfo'
|
||||
import {makeProfileLink} from '#/lib/routes/links'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {logger} from '#/logger'
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {KeyboardPadding} from '#/components/KeyboardPadding'
|
||||
import {InlineLinkText} from '#/components/Link'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {Divider} from '../Divider'
|
||||
import {Loader} from '../Loader'
|
||||
|
||||
export {useDialogControl as useLabelsOnMeDialogControl} from '#/components/Dialog'
|
||||
|
||||
type Subject =
|
||||
@@ -103,14 +100,14 @@ function LabelsOnMeDialogInner(props: LabelsOnMeDialogProps) {
|
||||
label={label}
|
||||
isSelfLabel={label.src === currentAccount?.did}
|
||||
control={props.control}
|
||||
onPressAppeal={setAppealingLabel}
|
||||
onPressAppeal={label => setAppealingLabel(label)}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Dialog.Close />
|
||||
<KeyboardPadding />
|
||||
</Dialog.ScrollableInner>
|
||||
)
|
||||
}
|
||||
@@ -202,14 +199,14 @@ function AppealForm({
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const [details, setDetails] = React.useState('')
|
||||
const isAccountReport = 'did' in subject
|
||||
const agent = useAgent()
|
||||
const {getAgent} = useAgent()
|
||||
|
||||
const {mutate, isPending} = useMutation({
|
||||
mutationFn: async () => {
|
||||
const onSubmit = async () => {
|
||||
try {
|
||||
const $type = !isAccountReport
|
||||
? 'com.atproto.repo.strongRef'
|
||||
: 'com.atproto.admin.defs#repoRef'
|
||||
await agent
|
||||
await getAgent()
|
||||
.withProxy('atproto_labeler', label.src)
|
||||
.createModerationReport({
|
||||
reasonType: ComAtprotoModerationDefs.REASONAPPEAL,
|
||||
@@ -219,18 +216,11 @@ function AppealForm({
|
||||
},
|
||||
reason: details,
|
||||
})
|
||||
},
|
||||
onError: err => {
|
||||
logger.error('Failed to submit label appeal', {message: err})
|
||||
Toast.show(_(msg`Failed to submit appeal, please try again.`))
|
||||
},
|
||||
onSuccess: () => {
|
||||
control.close()
|
||||
Toast.show(_(msg`Appeal submitted`))
|
||||
},
|
||||
})
|
||||
|
||||
const onSubmit = React.useCallback(() => mutate(), [mutate])
|
||||
} finally {
|
||||
control.close()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -291,7 +281,6 @@ function AppealForm({
|
||||
onPress={onSubmit}
|
||||
label={_(msg`Submit`)}>
|
||||
<ButtonText>{_(msg`Submit`)}</ButtonText>
|
||||
{isPending && <ButtonIcon icon={Loader} />}
|
||||
</Button>
|
||||
</View>
|
||||
</>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import React from 'react'
|
||||
import {StyleProp, View, ViewStyle} from 'react-native'
|
||||
import {ModerationCause, ModerationUI} from '@atproto/api'
|
||||
import {ModerationUI, ModerationCause} from '@atproto/api'
|
||||
|
||||
import {getModerationCauseKey} from '#/lib/moderation'
|
||||
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button} from '#/components/Button'
|
||||
import {getModerationCauseKey} from '#/lib/moderation'
|
||||
|
||||
import {atoms as a} from '#/alf'
|
||||
import {Button, ButtonText, ButtonIcon} from '#/components/Button'
|
||||
import {
|
||||
ModerationDetailsDialog,
|
||||
useModerationDetailsDialogControl,
|
||||
} from '#/components/moderation/ModerationDetailsDialog'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function PostAlerts({
|
||||
modui,
|
||||
@@ -41,41 +41,23 @@ export function PostAlerts({
|
||||
function PostLabel({cause}: {cause: ModerationCause}) {
|
||||
const control = useModerationDetailsDialogControl()
|
||||
const desc = useModerationCauseDescription(cause)
|
||||
const t = useTheme()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
label={desc.name}
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
size="small"
|
||||
shape="default"
|
||||
onPress={() => {
|
||||
control.open()
|
||||
}}>
|
||||
{({hovered, pressed}) => (
|
||||
<View
|
||||
style={[
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
{paddingLeft: 4, paddingRight: 6, paddingVertical: 1},
|
||||
a.gap_xs,
|
||||
a.rounded_sm,
|
||||
hovered || pressed
|
||||
? t.atoms.bg_contrast_50
|
||||
: t.atoms.bg_contrast_25,
|
||||
]}>
|
||||
<desc.icon size="xs" fill={t.atoms.text_contrast_medium.color} />
|
||||
<Text
|
||||
style={[
|
||||
a.text_left,
|
||||
a.leading_snug,
|
||||
a.text_xs,
|
||||
t.atoms.text_contrast_medium,
|
||||
a.font_semibold,
|
||||
]}>
|
||||
{desc.name}
|
||||
{desc.source ? ` – ${desc.source}` : ''}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
}}
|
||||
style={[a.px_sm, a.py_xs, a.gap_xs]}>
|
||||
<ButtonIcon icon={desc.icon} position="left" />
|
||||
<ButtonText style={[a.text_left, a.leading_snug]}>
|
||||
{desc.name}
|
||||
</ButtonText>
|
||||
</Button>
|
||||
|
||||
<ModerationDetailsDialog control={control} modcause={cause} />
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
interface Props extends ComponentProps<typeof Link> {
|
||||
disabled: boolean
|
||||
iconSize: number
|
||||
iconStyles: StyleProp<ViewStyle>
|
||||
modui: ModerationUI
|
||||
@@ -28,7 +27,6 @@ interface Props extends ComponentProps<typeof Link> {
|
||||
export function PostHider({
|
||||
testID,
|
||||
href,
|
||||
disabled,
|
||||
modui,
|
||||
style,
|
||||
children,
|
||||
@@ -49,7 +47,7 @@ export function PostHider({
|
||||
precacheProfile(queryClient, profile)
|
||||
}, [queryClient, profile])
|
||||
|
||||
if (!blur || (disabled && !modui.noOverride)) {
|
||||
if (!blur) {
|
||||
return (
|
||||
<Link
|
||||
testID={testID}
|
||||
|
||||
@@ -2,15 +2,15 @@ import React from 'react'
|
||||
import {StyleProp, View, ViewStyle} from 'react-native'
|
||||
import {ModerationCause, ModerationDecision} from '@atproto/api'
|
||||
|
||||
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
|
||||
import {getModerationCauseKey} from 'lib/moderation'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button} from '#/components/Button'
|
||||
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
|
||||
|
||||
import {atoms as a} from '#/alf'
|
||||
import {Button, ButtonText, ButtonIcon} from '#/components/Button'
|
||||
import {
|
||||
ModerationDetailsDialog,
|
||||
useModerationDetailsDialogControl,
|
||||
} from '#/components/moderation/ModerationDetailsDialog'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function ProfileHeaderAlerts({
|
||||
moderation,
|
||||
@@ -39,7 +39,6 @@ export function ProfileHeaderAlerts({
|
||||
}
|
||||
|
||||
function ProfileLabel({cause}: {cause: ModerationCause}) {
|
||||
const t = useTheme()
|
||||
const control = useModerationDetailsDialogControl()
|
||||
const desc = useModerationCauseDescription(cause)
|
||||
|
||||
@@ -47,35 +46,18 @@ function ProfileLabel({cause}: {cause: ModerationCause}) {
|
||||
<>
|
||||
<Button
|
||||
label={desc.name}
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
size="small"
|
||||
shape="default"
|
||||
onPress={() => {
|
||||
control.open()
|
||||
}}>
|
||||
{({hovered, pressed}) => (
|
||||
<View
|
||||
style={[
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
{paddingLeft: 6, paddingRight: 8, paddingVertical: 4},
|
||||
a.gap_xs,
|
||||
a.rounded_md,
|
||||
hovered || pressed
|
||||
? t.atoms.bg_contrast_50
|
||||
: t.atoms.bg_contrast_25,
|
||||
]}>
|
||||
<desc.icon size="sm" fill={t.atoms.text_contrast_medium.color} />
|
||||
<Text
|
||||
style={[
|
||||
a.text_left,
|
||||
a.leading_snug,
|
||||
a.text_sm,
|
||||
t.atoms.text_contrast_medium,
|
||||
a.font_semibold,
|
||||
]}>
|
||||
{desc.name}
|
||||
{desc.source ? ` – ${desc.source}` : ''}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
}}
|
||||
style={[a.px_sm, a.py_xs, a.gap_xs]}>
|
||||
<ButtonIcon icon={desc.icon} position="left" />
|
||||
<ButtonText style={[a.text_left, a.leading_snug]}>
|
||||
{desc.name}
|
||||
</ButtonText>
|
||||
</Button>
|
||||
|
||||
<ModerationDetailsDialog control={control} modcause={cause} />
|
||||
|
||||
@@ -7,22 +7,22 @@ import {
|
||||
import {FeedAPI, FeedAPIResponse} from './types'
|
||||
|
||||
export class AuthorFeedAPI implements FeedAPI {
|
||||
agent: BskyAgent
|
||||
getAgent: () => BskyAgent
|
||||
params: GetAuthorFeed.QueryParams
|
||||
|
||||
constructor({
|
||||
agent,
|
||||
getAgent,
|
||||
feedParams,
|
||||
}: {
|
||||
agent: BskyAgent
|
||||
getAgent: () => BskyAgent
|
||||
feedParams: GetAuthorFeed.QueryParams
|
||||
}) {
|
||||
this.agent = agent
|
||||
this.getAgent = getAgent
|
||||
this.params = feedParams
|
||||
}
|
||||
|
||||
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
|
||||
const res = await this.agent.getAuthorFeed({
|
||||
const res = await this.getAgent().getAuthorFeed({
|
||||
...this.params,
|
||||
limit: 1,
|
||||
})
|
||||
@@ -36,7 +36,7 @@ export class AuthorFeedAPI implements FeedAPI {
|
||||
cursor: string | undefined
|
||||
limit: number
|
||||
}): Promise<FeedAPIResponse> {
|
||||
const res = await this.agent.getAuthorFeed({
|
||||
const res = await this.getAgent().getAuthorFeed({
|
||||
...this.params,
|
||||
cursor,
|
||||
limit,
|
||||
|
||||
@@ -10,27 +10,27 @@ import {FeedAPI, FeedAPIResponse} from './types'
|
||||
import {createBskyTopicsHeader, isBlueskyOwnedFeed} from './utils'
|
||||
|
||||
export class CustomFeedAPI implements FeedAPI {
|
||||
agent: BskyAgent
|
||||
getAgent: () => BskyAgent
|
||||
params: GetCustomFeed.QueryParams
|
||||
userInterests?: string
|
||||
|
||||
constructor({
|
||||
agent,
|
||||
getAgent,
|
||||
feedParams,
|
||||
userInterests,
|
||||
}: {
|
||||
agent: BskyAgent
|
||||
getAgent: () => BskyAgent
|
||||
feedParams: GetCustomFeed.QueryParams
|
||||
userInterests?: string
|
||||
}) {
|
||||
this.agent = agent
|
||||
this.getAgent = getAgent
|
||||
this.params = feedParams
|
||||
this.userInterests = userInterests
|
||||
}
|
||||
|
||||
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
|
||||
const contentLangs = getContentLanguages().join(',')
|
||||
const res = await this.agent.app.bsky.feed.getFeed(
|
||||
const res = await this.getAgent().app.bsky.feed.getFeed(
|
||||
{
|
||||
...this.params,
|
||||
limit: 1,
|
||||
@@ -48,11 +48,11 @@ export class CustomFeedAPI implements FeedAPI {
|
||||
limit: number
|
||||
}): Promise<FeedAPIResponse> {
|
||||
const contentLangs = getContentLanguages().join(',')
|
||||
const agent = this.agent
|
||||
const agent = this.getAgent()
|
||||
const isBlueskyOwned = isBlueskyOwnedFeed(this.params.feed)
|
||||
|
||||
const res = agent.session
|
||||
? await this.agent.app.bsky.feed.getFeed(
|
||||
? await this.getAgent().app.bsky.feed.getFeed(
|
||||
{
|
||||
...this.params,
|
||||
cursor,
|
||||
|
||||
@@ -3,14 +3,14 @@ import {AppBskyFeedDefs, BskyAgent} from '@atproto/api'
|
||||
import {FeedAPI, FeedAPIResponse} from './types'
|
||||
|
||||
export class FollowingFeedAPI implements FeedAPI {
|
||||
agent: BskyAgent
|
||||
getAgent: () => BskyAgent
|
||||
|
||||
constructor({agent}: {agent: BskyAgent}) {
|
||||
this.agent = agent
|
||||
constructor({getAgent}: {getAgent: () => BskyAgent}) {
|
||||
this.getAgent = getAgent
|
||||
}
|
||||
|
||||
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
|
||||
const res = await this.agent.getTimeline({
|
||||
const res = await this.getAgent().getTimeline({
|
||||
limit: 1,
|
||||
})
|
||||
return res.data.feed[0]
|
||||
@@ -23,7 +23,7 @@ export class FollowingFeedAPI implements FeedAPI {
|
||||
cursor: string | undefined
|
||||
limit: number
|
||||
}): Promise<FeedAPIResponse> {
|
||||
const res = await this.agent.getTimeline({
|
||||
const res = await this.getAgent().getTimeline({
|
||||
cursor,
|
||||
limit,
|
||||
})
|
||||
|
||||
@@ -27,7 +27,7 @@ export const FALLBACK_MARKER_POST: AppBskyFeedDefs.FeedViewPost = {
|
||||
}
|
||||
|
||||
export class HomeFeedAPI implements FeedAPI {
|
||||
agent: BskyAgent
|
||||
getAgent: () => BskyAgent
|
||||
following: FollowingFeedAPI
|
||||
discover: CustomFeedAPI
|
||||
usingDiscover = false
|
||||
@@ -36,24 +36,24 @@ export class HomeFeedAPI implements FeedAPI {
|
||||
|
||||
constructor({
|
||||
userInterests,
|
||||
agent,
|
||||
getAgent,
|
||||
}: {
|
||||
userInterests?: string
|
||||
agent: BskyAgent
|
||||
getAgent: () => BskyAgent
|
||||
}) {
|
||||
this.agent = agent
|
||||
this.following = new FollowingFeedAPI({agent})
|
||||
this.getAgent = getAgent
|
||||
this.following = new FollowingFeedAPI({getAgent})
|
||||
this.discover = new CustomFeedAPI({
|
||||
agent,
|
||||
getAgent,
|
||||
feedParams: {feed: PROD_DEFAULT_FEED('whats-hot')},
|
||||
})
|
||||
this.userInterests = userInterests
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.following = new FollowingFeedAPI({agent: this.agent})
|
||||
this.following = new FollowingFeedAPI({getAgent: this.getAgent})
|
||||
this.discover = new CustomFeedAPI({
|
||||
agent: this.agent,
|
||||
getAgent: this.getAgent,
|
||||
feedParams: {feed: PROD_DEFAULT_FEED('whats-hot')},
|
||||
userInterests: this.userInterests,
|
||||
})
|
||||
|
||||
@@ -7,22 +7,22 @@ import {
|
||||
import {FeedAPI, FeedAPIResponse} from './types'
|
||||
|
||||
export class LikesFeedAPI implements FeedAPI {
|
||||
agent: BskyAgent
|
||||
getAgent: () => BskyAgent
|
||||
params: GetActorLikes.QueryParams
|
||||
|
||||
constructor({
|
||||
agent,
|
||||
getAgent,
|
||||
feedParams,
|
||||
}: {
|
||||
agent: BskyAgent
|
||||
getAgent: () => BskyAgent
|
||||
feedParams: GetActorLikes.QueryParams
|
||||
}) {
|
||||
this.agent = agent
|
||||
this.getAgent = getAgent
|
||||
this.params = feedParams
|
||||
}
|
||||
|
||||
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
|
||||
const res = await this.agent.getActorLikes({
|
||||
const res = await this.getAgent().getActorLikes({
|
||||
...this.params,
|
||||
limit: 1,
|
||||
})
|
||||
@@ -36,7 +36,7 @@ export class LikesFeedAPI implements FeedAPI {
|
||||
cursor: string | undefined
|
||||
limit: number
|
||||
}): Promise<FeedAPIResponse> {
|
||||
const res = await this.agent.getActorLikes({
|
||||
const res = await this.getAgent().getActorLikes({
|
||||
...this.params,
|
||||
cursor,
|
||||
limit,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user