Compare commits
102 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 83046f6b01 | |||
| 3c64d1d740 | |||
| 57e531fac8 | |||
| 5f63b8d40f | |||
| 480a40862f | |||
| c58aedf050 | |||
| 29a4a5f90c | |||
| fefae27396 | |||
| 2540616f43 | |||
| bd1e6fced9 | |||
| 885ad2c756 | |||
| fef16e060f | |||
| ac25e43bef | |||
| 094c08ea60 | |||
| 559764156d | |||
| 3801932902 | |||
| 72f46ed734 | |||
| 85e676257e | |||
| 48796449ea | |||
| 1f954c1065 | |||
| 5d2119ffe5 | |||
| 492c271a06 | |||
| 42477d8bbb | |||
| aefbab3850 | |||
| af2aaa1f39 | |||
| 22858192dc | |||
| 618a3c1ebe | |||
| 5e0452fc6b | |||
| 47033e5270 | |||
| bb9afa9ce8 | |||
| 504bd28e80 | |||
| afbcac3ff3 | |||
| deea2f3839 | |||
| 7613cdb89b | |||
| 3ece21cb45 | |||
| e64b7cf698 | |||
| a2d5343a87 | |||
| b5ac450442 | |||
| 551af88f22 | |||
| a49fe13223 | |||
| 9f001526d3 | |||
| d6b8313932 | |||
| e4b4d854d6 | |||
| c352e0f8b4 | |||
| e7968bc8d7 | |||
| 6f1589971c | |||
| 2ffb98e22a | |||
| d918f8dc2a | |||
| d032734278 | |||
| 3e1f076891 | |||
| de93e8de74 | |||
| da96fb1ef5 | |||
| b02445883a | |||
| bd4703ca1e | |||
| 891b432ead | |||
| 3b55f61d5f | |||
| 8c596b61c0 | |||
| 44670c7ee2 | |||
| 8d8323421c | |||
| 21d4d5f600 | |||
| bda10510a4 | |||
| 16f295ca85 | |||
| f05aebf78e | |||
| de257a1186 | |||
| d93acb25f4 | |||
| 708a80e7a7 | |||
| b51640fbc0 | |||
| 5cda807d9d | |||
| f868821cfc | |||
| 2bb3694819 | |||
| cd3b502b34 | |||
| 22e1eb18c8 | |||
| 8eb3cebb36 | |||
| 455937dd0f | |||
| 05b55c1966 | |||
| d614f6cb71 | |||
| 89c9fd3be1 | |||
| 8569e2e389 | |||
| 9879159438 | |||
| 8e2c21a882 | |||
| d6275e98c2 | |||
| 9431201026 | |||
| 9d4622e713 | |||
| 8de028387c | |||
| 8feb2ab449 | |||
| 16925baf8d | |||
| b077cbe399 | |||
| 3bdceac2fb | |||
| 76f860dad2 | |||
| 13c08f56ba | |||
| a72f55a11f | |||
| cd497a3974 | |||
| d92036f2c5 | |||
| c4abaa1abc | |||
| fba4a9ca02 | |||
| e48f8e15eb | |||
| 5fe1e171f0 | |||
| 4cc55f05c2 | |||
| 9edb487949 | |||
| 9628070e52 | |||
| eb6f44853d | |||
| 165feedb86 |
@@ -34,6 +34,9 @@ jobs:
|
||||
node-version-file: .nvmrc
|
||||
cache: yarn
|
||||
|
||||
- name: 🪛 Setup jq
|
||||
uses: dcarbone/install-jq-action@v2
|
||||
|
||||
- name: 🔨 Setup EAS
|
||||
uses: expo/expo-github-action@v8
|
||||
with:
|
||||
@@ -96,13 +99,17 @@ jobs:
|
||||
name: build-${{ steps.timestamp.outputs.time }}.apk
|
||||
path: build.apk
|
||||
|
||||
- name: 📚 Get version from package.json
|
||||
id: get-build-info
|
||||
run: bash scripts/setGitHubOutput.sh
|
||||
|
||||
- name: 🔔 Notify Slack of Production Build
|
||||
if: ${{ inputs.profile == 'production' }}
|
||||
uses: slackapi/slack-github-action@v1.25.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"text": "Android build is ready for submission. This is a production build! Download the artifact here: ${{ steps.upload-artifact-production.outputs.artifact-url }}"
|
||||
"text": "Android production build for Google Play Store submission is ready!\n```Artifact: ${{ steps.upload-artifact-production.outputs.artifact-url }}\nVersion Number: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}\nBuild Number: ${{ steps.get-build-info.outputs.BSKY_ANDROID_VERSION_CODE }}```"
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
|
||||
@@ -120,6 +127,32 @@ jobs:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
|
||||
- name: 🏗️ Build Production APK
|
||||
if: ${{ inputs.profile == 'production' }}
|
||||
run: yarn use-build-number-with-bump eas build -p android --profile production-apk --local --output build.apk --non-interactive
|
||||
|
||||
- name: 🚀 Upload Production APK Artifact
|
||||
id: upload-artifact-production-apk
|
||||
if: ${{ inputs.profile == 'production' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
retention-days: 30
|
||||
compression-level: 6
|
||||
name: build-${{ steps.timestamp.outputs.time }}.apk
|
||||
path: build.apk
|
||||
|
||||
- name: 🔔 Notify Slack of Production APK Build
|
||||
if: ${{ inputs.profile == 'production' }}
|
||||
uses: slackapi/slack-github-action@v1.25.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"text": "Android production build for GitHub/Obtanium is ready!\n```Artifact: ${{ steps.upload-artifact-production-apk.outputs.artifact-url }}\nVersion Number: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}\nBuild Number: ${{ steps.get-build-info.outputs.BSKY_ANDROID_VERSION_CODE }}```"
|
||||
}
|
||||
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
|
||||
|
||||
@@ -34,6 +34,9 @@ jobs:
|
||||
node-version-file: .nvmrc
|
||||
cache: yarn
|
||||
|
||||
- name: 🪛 Setup jq
|
||||
uses: dcarbone/install-jq-action@v2
|
||||
|
||||
- name: 🔨 Setup EAS
|
||||
uses: expo/expo-github-action@v8
|
||||
with:
|
||||
@@ -81,6 +84,22 @@ jobs:
|
||||
- name: 🚀 Deploy
|
||||
run: eas submit -p ios --non-interactive --path build.ipa
|
||||
|
||||
- name: 📚 Get version from package.json
|
||||
id: get-build-info
|
||||
run: bash scripts/setGitHubOutput.sh
|
||||
|
||||
- name: 🔔 Notify Slack of Production Build
|
||||
if: ${{ inputs.profile == 'production' }}
|
||||
uses: slackapi/slack-github-action@v1.25.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"text": "iOS production build for App Store submission is ready!\n```Artifact: Check TestFlight to know when it is available\nVersion Number: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}\nBuild Number: ${{ steps.get-build-info.outputs.BSKY_IOS_BUILD_NUMBER }}```"
|
||||
}
|
||||
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
|
||||
|
||||
@@ -50,7 +50,7 @@ appId: xyz.blueskyweb.app
|
||||
text: "1"
|
||||
- tapOn:
|
||||
id: "repostBtn"
|
||||
- tapOn: "Undo repost"
|
||||
- tapOn: "Remove repost"
|
||||
- assertNotVisible:
|
||||
id: "repostCount"
|
||||
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
# Remove this test when the old onboarding is deprecated
|
||||
appId: xyz.blueskyweb.app
|
||||
---
|
||||
- runScript:
|
||||
file: ../setupServer.js
|
||||
env:
|
||||
SERVER_PATH: "?users"
|
||||
- runFlow:
|
||||
file: ../setupApp.yml
|
||||
- tapOn:
|
||||
id: "e2eSignInAlice"
|
||||
- tapOn:
|
||||
id: "e2eStartLongboarding"
|
||||
- tapOn: "Continue to next step"
|
||||
- tapOn: "Continue to the next step without following any accounts"
|
||||
- tapOn: Show replies in Following feed
|
||||
- tapOn: Show quote-posts in Following feed
|
||||
- tapOn: Show re-posts in Following feed
|
||||
- tapOn: Show replies in Following feed
|
||||
- waitForAnimationToEnd
|
||||
- tapOn: Continue to next step
|
||||
- waitForAnimationToEnd
|
||||
- tapOn: "Continue to the next step"
|
||||
- waitForAnimationToEnd
|
||||
- tapOn: Continue to next step
|
||||
- waitForAnimationToEnd
|
||||
- tapOn: Continue to next step
|
||||
- waitForAnimationToEnd
|
||||
- tapOn: "Complete onboarding and start using your account"
|
||||
- waitForAnimationToEnd
|
||||
- assertVisible: Following
|
||||
@@ -142,7 +142,8 @@ describe('makeRecordUri', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('ago', () => {
|
||||
// FIXME: Reenable after fixing non-deterministic test.
|
||||
describe.skip('ago', () => {
|
||||
const oneYearDate = new Date(
|
||||
new Date().setMonth(new Date().getMonth() - 11),
|
||||
).setDate(new Date().getDate() - 28)
|
||||
@@ -479,6 +480,26 @@ describe('parseEmbedPlayerFromUrl', () => {
|
||||
'https://media.tenor.com/someID/someName.gif',
|
||||
'https://media.tenor.com/someID',
|
||||
'https://media.tenor.com',
|
||||
|
||||
'https://www.flickr.com/photos/username/albums/72177720308493661',
|
||||
'https://flickr.com/photos/username/albums/72177720308493661',
|
||||
'https://flickr.com/photos/username/albums/72177720308493661/',
|
||||
'https://flickr.com/photos/username/albums/72177720308493661//',
|
||||
'https://flic.kr/s/aHBqjAES3i',
|
||||
|
||||
'https://flickr.com/foetoes/username/albums/3903',
|
||||
'https://flickr.com/albums/3903',
|
||||
'https://flic.kr/s/OolI',
|
||||
'https://flic.kr/t/aHBqjAES3i',
|
||||
|
||||
'https://www.flickr.com/groups/898944@N23/pool',
|
||||
'https://flickr.com/groups/898944@N23/pool',
|
||||
'https://flickr.com/groups/898944@N23/pool/',
|
||||
'https://flickr.com/groups/898944@N23/pool//',
|
||||
'https://flic.kr/go/8WJtR',
|
||||
|
||||
'https://www.flickr.com/groups/898944@N23/',
|
||||
'https://www.flickr.com/groups',
|
||||
]
|
||||
|
||||
const outputs = [
|
||||
@@ -776,6 +797,66 @@ describe('parseEmbedPlayerFromUrl', () => {
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
|
||||
{
|
||||
type: 'flickr_album',
|
||||
source: 'flickr',
|
||||
playerUri: 'https://embedr.flickr.com/photosets/72177720308493661',
|
||||
},
|
||||
{
|
||||
type: 'flickr_album',
|
||||
source: 'flickr',
|
||||
playerUri: 'https://embedr.flickr.com/photosets/72177720308493661',
|
||||
},
|
||||
{
|
||||
type: 'flickr_album',
|
||||
source: 'flickr',
|
||||
playerUri: 'https://embedr.flickr.com/photosets/72177720308493661',
|
||||
},
|
||||
{
|
||||
type: 'flickr_album',
|
||||
source: 'flickr',
|
||||
playerUri: 'https://embedr.flickr.com/photosets/72177720308493661',
|
||||
},
|
||||
{
|
||||
type: 'flickr_album',
|
||||
source: 'flickr',
|
||||
playerUri: 'https://embedr.flickr.com/photosets/72177720308493661',
|
||||
},
|
||||
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
|
||||
{
|
||||
type: 'flickr_album',
|
||||
source: 'flickr',
|
||||
playerUri: 'https://embedr.flickr.com/groups/898944@N23',
|
||||
},
|
||||
{
|
||||
type: 'flickr_album',
|
||||
source: 'flickr',
|
||||
playerUri: 'https://embedr.flickr.com/groups/898944@N23',
|
||||
},
|
||||
{
|
||||
type: 'flickr_album',
|
||||
source: 'flickr',
|
||||
playerUri: 'https://embedr.flickr.com/groups/898944@N23',
|
||||
},
|
||||
{
|
||||
type: 'flickr_album',
|
||||
source: 'flickr',
|
||||
playerUri: 'https://embedr.flickr.com/groups/898944@N23',
|
||||
},
|
||||
{
|
||||
type: 'flickr_album',
|
||||
source: 'flickr',
|
||||
playerUri: 'https://embedr.flickr.com/groups/898944@N23',
|
||||
},
|
||||
|
||||
undefined,
|
||||
undefined,
|
||||
]
|
||||
|
||||
it('correctly grabs the correct id from uri', () => {
|
||||
|
||||
@@ -204,7 +204,6 @@ module.exports = function (config) {
|
||||
],
|
||||
'./plugins/withAndroidManifestPlugin.js',
|
||||
'./plugins/withAndroidManifestFCMIconPlugin.js',
|
||||
'./plugins/withAndroidManifestLaunchModePlugin.js',
|
||||
'./plugins/withAndroidStylesWindowBackgroundPlugin.js',
|
||||
'./plugins/withAndroidStylesAccentColorPlugin.js',
|
||||
'./plugins/withAndroidSplashScreenStatusBarTranslucentPlugin.js',
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M3.004 4a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h2v5a1 1 0 0 0 .43.822c.57.395 1.176.031 1.685-.255.428-.24.998-.614 1.569-1.15 1.154-1.082 2.316-2.832 2.316-5.417V5a1 1 0 0 0-1-1h-7ZM14.004 4a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h2v5a1 1 0 0 0 .43.822c.57.395 1.176.031 1.685-.255.428-.24.998-.614 1.569-1.15 1.154-1.082 2.316-2.832 2.316-5.417V5a1 1 0 0 0-1-1h-7Z"/></svg>
|
||||
|
After Width: | Height: | Size: 450 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M2.004 5a1 1 0 0 1 1-1h7a1 1 0 0 1 1 1v8c0 2.585-1.162 4.335-2.316 5.417-.571.536-1.14.91-1.569 1.15a7.01 7.01 0 0 1-.738.36l-.016.006-.006.002h-.002l-.001.001L6.004 19l.351.936A1 1 0 0 1 5.004 19v-5h-2a1 1 0 0 1-1-1V5Zm5 12.234c.104-.085.21-.177.316-.276.846-.793 1.684-2.043 1.684-3.958V6h-5v6h2a1 1 0 0 1 1 1v4.234Zm6-12.234a1 1 0 0 1 1-1h7a1 1 0 0 1 1 1v8c0 2.585-1.162 4.335-2.316 5.417-.571.536-1.14.91-1.569 1.15a7.018 7.018 0 0 1-.738.36l-.016.006-.006.002h-.002l-.001.001-.352-.936.351.936A1 1 0 0 1 16.004 19v-5h-2a1 1 0 0 1-1-1V5Zm5 12.234V13a1 1 0 0 0-1-1h-2V6h5v7c0 1.915-.838 3.165-1.684 3.958-.106.1-.212.191-.316.276Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 775 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M2.003 5.999a2 2 0 0 1 2-1.999h5c1.104 0 2 .893 2 1.999V13c0 2.585-1.16 4.335-2.315 5.417-.571.536-1.14.91-1.569 1.15a7.01 7.01 0 0 1-.738.36l-.016.006-.006.002h-.002l-.001.001L6.004 19l.351.936a1 1 0 0 1-1.351-.935L5 14H4a2 2 0 0 1-2-2.001l.002-6Zm5 11.236L7 12.999A1 1 0 0 0 6 12H4l.003-6h5v7c0 1.915-.837 3.165-1.683 3.958-.106.1-.213.192-.317.277Zm6-11.235a2 2 0 0 1 2-2h5c1.104 0 2 .893 2 1.999V13c0 2.585-1.16 4.335-2.315 5.417-.571.536-1.14.91-1.569 1.15a7.018 7.018 0 0 1-.738.36l-.016.006-.006.002h-.002l-.001.001-.352-.936.351.936A1 1 0 0 1 16.004 19v-5h-1a2 2 0 0 1-2-2V6Zm7 0h-5v6h2a1 1 0 0 1 1 1v4.234c.105-.085.211-.177.317-.276.846-.793 1.684-2.043 1.684-3.958V6Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 821 B |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M3 5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5Zm16 0H5v7.213l1.246-.932.044-.03a3 3 0 0 1 3.863.454c1.468 1.58 2.941 2.749 4.847 2.749 1.703 0 2.855-.555 4-1.618V5Zm0 10.357c-1.112.697-2.386 1.097-4 1.097-2.81 0-4.796-1.755-6.313-3.388a1 1 0 0 0-1.269-.164L5 14.712V19h14v-3.643ZM15 8a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm-3 1a3 3 0 1 1 6 0 3 3 0 0 1-6 0Z" clip-rule="evenodd"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M3 4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm2 1v7.213l1.246-.932.044-.03a3 3 0 0 1 3.863.454c1.468 1.58 2.941 2.749 4.847 2.749 1.703 0 2.855-.555 4-1.618V5H5Zm14 10.357c-1.112.697-2.386 1.097-4 1.097-2.81 0-4.796-1.755-6.313-3.388a1 1 0 0 0-1.269-.164L5 14.712V19h14v-3.643ZM15 8a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm-3 1a3 3 0 1 1 6 0 3 3 0 0 1-6 0Z" clip-rule="evenodd"/></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 513 B After Width: | Height: | Size: 514 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M8.004 5a1 1 0 0 0-.43-.822c-.57-.395-1.176-.031-1.685.255-.428.24-.998.614-1.569 1.15C3.166 6.665 2.004 8.415 2.004 11v8a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1v-8a1 1 0 0 0-1-1h-2V5ZM19.004 5a1 1 0 0 0-.43-.822c-.57-.395-1.176-.031-1.685.255-.428.24-.998.614-1.569 1.15-1.154 1.082-2.316 2.832-2.316 5.417v8a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1v-8a1 1 0 0 0-1-1h-2V5Z"/></svg>
|
||||
|
After Width: | Height: | Size: 455 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M7.574 4.178a1 1 0 0 1 .43.822v5h2a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1h-7a1 1 0 0 1-1-1v-8c0-2.585 1.162-4.335 2.316-5.417a8.163 8.163 0 0 1 1.569-1.15 7.029 7.029 0 0 1 .738-.36l.016-.005.005-.003h.003v-.001c.001 0 .002 0 .353.936l-.351-.936a1 1 0 0 1 .92.114Zm-1.57 2.588a5.99 5.99 0 0 0-.316.276C4.842 7.835 4.004 9.085 4.004 11v7h5v-6h-2a1 1 0 0 1-1-1V6.766Zm12.57-2.588a1 1 0 0 1 .43.822v5h2a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1h-7a1 1 0 0 1-1-1v-8c0-2.585 1.162-4.335 2.316-5.417a8.166 8.166 0 0 1 1.569-1.15 7.038 7.038 0 0 1 .738-.36l.016-.005.005-.003h.003v-.001c.001 0 .002 0 .353.936l-.351-.936a1 1 0 0 1 .92.114Zm-1.57 2.588c-.105.085-.21.177-.316.276-.846.793-1.684 2.043-1.684 3.958v7h5v-6h-2a1 1 0 0 1-1-1V6.766Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 857 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM12 12c-4.758 0-8.083 3.521-8.496 7.906A1 1 0 0 0 4.5 21H15a3 3 0 1 1 0-6c0-.824.332-1.571.87-2.113C14.739 12.32 13.435 12 12 12Zm6 2a1 1 0 0 1 1 1v2h2a1 1 0 1 1 0 2h-2v2a1 1 0 1 1-2 0v-2h-2a1 1 0 1 1 0-2h2v-2a1 1 0 0 1 1-1Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 412 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM5.678 19c.71-2.909 3.092-5 6.322-5 .621 0 1.206.077 1.748.218a1 1 0 1 0 .504-1.936A8.931 8.931 0 0 0 12 12c-4.758 0-8.083 3.521-8.496 7.906A1 1 0 0 0 4.5 21H11a1 1 0 1 0 0-2H5.678ZM18 14a1 1 0 0 1 1 1v2h2a1 1 0 1 1 0 2h-2v2a1 1 0 1 1-2 0v-2h-2a1 1 0 1 1 0-2h2v-2a1 1 0 0 1 1-1Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 508 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M16.293 2.293a1 1 0 0 1 1.414 0l3 3a1 1 0 0 1 0 1.414l-3 3a1 1 0 0 1-1.414-1.414L17.586 7H5v4a1 1 0 1 1-2 0V6a1 1 0 0 1 1-1h13.586l-1.293-1.293a1 1 0 0 1 0-1.414ZM21 13v5a1 1 0 0 1-1 1H6.414l1.293 1.293a1 1 0 1 1-1.414 1.414l-3-3a1 1 0 0 1 0-1.414l3-3a1 1 0 0 1 1.414 1.414L6.414 17H19v-4a1 1 0 1 1 2 0Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 446 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M16.793 2.293a1 1 0 0 1 1.414 0L20.5 4.586a2 2 0 0 1 0 2.828l-2.293 2.293a1 1 0 0 1-1.414-1.414L18.086 7H7a2 2 0 0 0-2 2v2a1 1 0 1 1-2 0V9a4 4 0 0 1 4-4h11.086l-1.293-1.293a1 1 0 0 1 0-1.414ZM20 12a1 1 0 0 1 1 1v2a4 4 0 0 1-4 4H5.914l1.293 1.293a1 1 0 1 1-1.414 1.414L3.5 19.414a2 2 0 0 1 0-2.828l2.293-2.293a1 1 0 0 1 1.414 1.414L5.914 17H17a2 2 0 0 0 2-2v-2a1 1 0 0 1 1-1Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 517 B |
@@ -14,15 +14,20 @@ diff results in incompatible native changes, a new client build will automatical
|
||||
|
||||
### 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
|
||||
- Find the latest production build number for both iOS and Android in Slack. These are listed in #client-builds
|
||||
- Production builds always send the Version Number and Build Number in the Slack message. Search for the latest
|
||||
production version number, and you should find the correct information.
|
||||
|
||||

|
||||
|
||||
- It may also be useful to check the current production clients for these values. This will also help for testing. Note
|
||||
that you will need to _fully_ remove the existing internal client build from your device, otherwise the given values in
|
||||
the app may differ from the actual production values.
|
||||
|
||||

|
||||
|
||||
- You should have signed in to EAS locally through npx eas login. You will need to modify the build number in a
|
||||
subsequent step.
|
||||
- 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.
|
||||
@@ -38,14 +43,17 @@ to create your branch from, this should be properly set.
|
||||
|
||||
### Deployment
|
||||
|
||||
- Update the build number through EAS
|
||||
- Update the build number through EAS to match the build numbers of the
|
||||
production iOS/Android apps
|
||||
- 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
|
||||
- Set the build numbers to the values 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`
|
||||
- These steps should spit out what the current build number is, save those values
|
||||
for later too
|
||||
- Run the deployment
|
||||
- Navigate to https://github.com/bluesky-social/social-app/actions/workflows/bundle-deploy-eas-update.yml
|
||||
- Select the “Run Workflow” dropdown
|
||||
@@ -79,3 +87,9 @@ In about five minutes, the new deployment should be available for download. To t
|
||||
- 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.
|
||||
|
||||
### Post Deployment
|
||||
|
||||
- Reset both platforms build numbers to what they were before the OTA
|
||||
deployment. These values should have been logged by the EAS CLI when you
|
||||
reset them to the production values prior to OTA.
|
||||
|
||||
|
After Width: | Height: | Size: 99 KiB |
@@ -46,6 +46,20 @@
|
||||
"EXPO_PUBLIC_ENV": "production"
|
||||
}
|
||||
},
|
||||
"production-apk": {
|
||||
"extends": "base",
|
||||
"distribution": "internal",
|
||||
"ios": {
|
||||
"autoIncrement": false
|
||||
},
|
||||
"android": {
|
||||
"autoIncrement": false
|
||||
},
|
||||
"channel": "production",
|
||||
"env": {
|
||||
"EXPO_PUBLIC_ENV": "production"
|
||||
}
|
||||
},
|
||||
"testflight": {
|
||||
"extends": "base",
|
||||
"ios": {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import UserNotifications
|
||||
import UIKit
|
||||
|
||||
let APP_GROUP = "group.app.bsky"
|
||||
|
||||
@@ -31,7 +32,12 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
}
|
||||
|
||||
func mutateWithBadge(_ content: UNMutableNotificationContent) {
|
||||
content.badge = 1
|
||||
var count = prefs?.integer(forKey: "badgeCount") ?? 0
|
||||
count += 1
|
||||
|
||||
// Set the new badge number for the notification, then store that value for using later
|
||||
content.badge = NSNumber(value: count)
|
||||
prefs?.setValue(count, forKey: "badgeCount")
|
||||
}
|
||||
|
||||
func mutateWithChatMessage(_ content: UNMutableNotificationContent) {
|
||||
|
||||
@@ -66,5 +66,9 @@ class ExpoBackgroundNotificationHandlerModule : Module() {
|
||||
AsyncFunction("removeManyFromStringArrayAsync") { forKey: String, strings: Array<String> ->
|
||||
NotificationPrefs(appContext.reactContext).removeManyFromStringArray(forKey, strings)
|
||||
}
|
||||
|
||||
AsyncFunction("setBadgeCountAsync") { _: Int ->
|
||||
// This does nothing on Android
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@ let DEFAULTS: [String:Any] = [
|
||||
"playSoundQuote": false,
|
||||
"playSoundReply": false,
|
||||
"playSoundRepost": false,
|
||||
"mutedThreads": [:] as! [String:[String]]
|
||||
"mutedThreads": [:] as! [String:[String]],
|
||||
"badgeCount": 0,
|
||||
]
|
||||
|
||||
/*
|
||||
@@ -112,5 +113,9 @@ public class ExpoBackgroundNotificationHandlerModule: Module {
|
||||
userDefaults?.setValue(curr, forKey: forKey)
|
||||
}
|
||||
}
|
||||
|
||||
AsyncFunction("setBadgeCountAsync") { (count: Int) in
|
||||
userDefaults?.setValue(count, forKey: "badgeCount")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ export type ExpoBackgroundNotificationHandlerModule = {
|
||||
forKey: keyof BackgroundNotificationHandlerPreferences,
|
||||
value: string[],
|
||||
) => Promise<void>
|
||||
setBadgeCountAsync: (count: number) => Promise<void>
|
||||
}
|
||||
|
||||
// TODO there are more preferences in the native code, however they have not been added here yet.
|
||||
|
||||
@@ -24,4 +24,5 @@ export const BackgroundNotificationHandler = {
|
||||
removeFromStringArrayAsync: async (_: string, __: string) => {},
|
||||
addManyToStringArrayAsync: async (_: string, __: string[]) => {},
|
||||
removeManyFromStringArrayAsync: async (_: string, __: string[]) => {},
|
||||
setBadgeCountAsync: async (_: number) => {},
|
||||
} as ExpoBackgroundNotificationHandlerModule
|
||||
|
||||
@@ -15,7 +15,10 @@ export function NativeTranslationView() {
|
||||
return <NativeView />
|
||||
}
|
||||
|
||||
export const isAvailable = Number(Platform.Version) >= 17.4
|
||||
// can be something like "17.5.1", so just take the first two parts
|
||||
const version = String(Platform.Version).split('.').slice(0, 2).join('.')
|
||||
|
||||
export const isAvailable = Number(version) >= 17.4
|
||||
|
||||
// https://en.wikipedia.org/wiki/Translate_(Apple)#Languages
|
||||
const SUPPORTED_LANGUAGES = [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bsky.app",
|
||||
"version": "1.85.0",
|
||||
"version": "1.86.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.16",
|
||||
"@bam.tech/react-native-image-resizer": "^3.0.4",
|
||||
"@braintree/sanitize-url": "^6.0.2",
|
||||
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
||||
@@ -63,7 +63,7 @@
|
||||
"@fortawesome/fontawesome-svg-core": "^6.1.1",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.1.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.1.1",
|
||||
"@fortawesome/react-native-fontawesome": "^0.3.0",
|
||||
"@fortawesome/react-native-fontawesome": "^0.3.2",
|
||||
"@lingui/react": "^4.5.0",
|
||||
"@mattermost/react-native-paste-input": "^0.7.1",
|
||||
"@miblanchard/react-native-slider": "^2.3.1",
|
||||
@@ -116,7 +116,6 @@
|
||||
"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",
|
||||
@@ -129,7 +128,7 @@
|
||||
"expo-localization": "~15.0.3",
|
||||
"expo-media-library": "~16.0.3",
|
||||
"expo-navigation-bar": "~3.0.4",
|
||||
"expo-notifications": "~0.28.3",
|
||||
"expo-notifications": "~0.28.7",
|
||||
"expo-sharing": "^12.0.1",
|
||||
"expo-splash-screen": "~0.27.4",
|
||||
"expo-status-bar": "~1.12.1",
|
||||
@@ -272,6 +271,8 @@
|
||||
"resolutions": {
|
||||
"@types/react": "^18",
|
||||
"**/zeed-dom": "0.10.9",
|
||||
"**/expo-constants": "16.0.1",
|
||||
"**/expo-device": "6.0.2",
|
||||
"@react-native/babel-preset": "0.74.1"
|
||||
},
|
||||
"jest": {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
## expo-modules-core Patch
|
||||
|
||||
This patch fixes crashes seen in some Android clients when using intents to open the app. See https://github.com/expo/expo/pull/29513.
|
||||
|
||||
Do not remove this patch until that PR lands in Expo and is released.
|
||||
@@ -0,0 +1,14 @@
|
||||
diff --git a/node_modules/expo-modules-core/android/src/main/java/expo/modules/adapters/react/NativeModulesProxy.java b/node_modules/expo-modules-core/android/src/main/java/expo/modules/adapters/react/NativeModulesProxy.java
|
||||
index bb74e80..0aa0202 100644
|
||||
--- a/node_modules/expo-modules-core/android/src/main/java/expo/modules/adapters/react/NativeModulesProxy.java
|
||||
+++ b/node_modules/expo-modules-core/android/src/main/java/expo/modules/adapters/react/NativeModulesProxy.java
|
||||
@@ -90,8 +90,8 @@ public class NativeModulesProxy extends ReactContextBaseJavaModule {
|
||||
mModuleRegistry.ensureIsInitialized();
|
||||
|
||||
KotlinInteropModuleRegistry kotlinModuleRegistry = getKotlinInteropModuleRegistry();
|
||||
- kotlinModuleRegistry.emitOnCreate();
|
||||
kotlinModuleRegistry.installJSIInterop();
|
||||
+ kotlinModuleRegistry.emitOnCreate();
|
||||
|
||||
Map<String, Object> constants = new HashMap<>(3);
|
||||
constants.put(MODULES_CONSTANTS_KEY, new HashMap<>());
|
||||
@@ -67,3 +67,19 @@ index b09e653..4c32b31 100644
|
||||
+}
|
||||
+
|
||||
@end
|
||||
diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.java
|
||||
index 5f5e1ab..aac00b6 100644
|
||||
--- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.java
|
||||
+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.java
|
||||
@@ -99,8 +99,9 @@ public class JavaTimerManager {
|
||||
}
|
||||
|
||||
// If the JS thread is busy for multiple frames we cancel any other pending runnable.
|
||||
- if (mCurrentIdleCallbackRunnable != null) {
|
||||
- mCurrentIdleCallbackRunnable.cancel();
|
||||
+ IdleCallbackRunnable currentRunnable = mCurrentIdleCallbackRunnable;
|
||||
+ if (currentRunnable != null) {
|
||||
+ currentRunnable.cancel();
|
||||
}
|
||||
|
||||
mCurrentIdleCallbackRunnable = new IdleCallbackRunnable(frameTimeNanos);
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
outputIos=$(eas build:version:get -p ios)
|
||||
outputAndroid=$(eas build:version:get -p android)
|
||||
BSKY_IOS_BUILD_NUMBER=${outputIos#*buildNumber - }
|
||||
BSKY_ANDROID_VERSION_CODE=${outputAndroid#*versionCode - }
|
||||
|
||||
echo PACKAGE_VERSION="$(jq -r '.version' package.json)" > "$GITHUB_OUTPUT"
|
||||
echo BSKY_IOS_BUILD_NUMBER=$BSKY_IOS_BUILD_NUMBER >> "$GITHUB_OUTPUT"
|
||||
echo BSKY_ANDROID_VERSION_CODE=$BSKY_ANDROID_VERSION_CODE >> "$GITHUB_OUTPUT"
|
||||
@@ -1,7 +1,8 @@
|
||||
import {Platform} from 'react-native'
|
||||
import {Platform, StyleSheet} from 'react-native'
|
||||
|
||||
import * as tokens from '#/alf/tokens'
|
||||
import {native, web} from '#/alf/util/platform'
|
||||
import hairlineWidth = StyleSheet.hairlineWidth
|
||||
|
||||
export const atoms = {
|
||||
/*
|
||||
@@ -55,6 +56,13 @@ export const atoms = {
|
||||
height: '100vh',
|
||||
}),
|
||||
|
||||
/*
|
||||
* Theme-independent bg colors
|
||||
*/
|
||||
bg_transparent: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
|
||||
/*
|
||||
* Border radius
|
||||
*/
|
||||
@@ -270,19 +278,19 @@ export const atoms = {
|
||||
borderWidth: 0,
|
||||
},
|
||||
border: {
|
||||
borderWidth: 1,
|
||||
borderWidth: hairlineWidth,
|
||||
},
|
||||
border_t: {
|
||||
borderTopWidth: 1,
|
||||
borderTopWidth: hairlineWidth,
|
||||
},
|
||||
border_b: {
|
||||
borderBottomWidth: 1,
|
||||
borderBottomWidth: hairlineWidth,
|
||||
},
|
||||
border_l: {
|
||||
borderLeftWidth: 1,
|
||||
borderLeftWidth: hairlineWidth,
|
||||
},
|
||||
border_r: {
|
||||
borderRightWidth: 1,
|
||||
borderRightWidth: hairlineWidth,
|
||||
},
|
||||
|
||||
/*
|
||||
@@ -841,7 +849,7 @@ export const atoms = {
|
||||
marginRight: 'auto',
|
||||
},
|
||||
/*
|
||||
* Pointer events
|
||||
* Pointer events & user select
|
||||
*/
|
||||
pointer_events_none: {
|
||||
pointerEvents: 'none',
|
||||
@@ -849,6 +857,15 @@ export const atoms = {
|
||||
pointer_events_auto: {
|
||||
pointerEvents: 'auto',
|
||||
},
|
||||
user_select_none: {
|
||||
userSelect: 'none',
|
||||
},
|
||||
user_select_text: {
|
||||
userSelect: 'text',
|
||||
},
|
||||
user_select_all: {
|
||||
userSelect: 'all',
|
||||
},
|
||||
/*
|
||||
* Text decoration
|
||||
*/
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import React from 'react'
|
||||
import {Dimensions} from 'react-native'
|
||||
|
||||
import * as themes from '#/alf/themes'
|
||||
|
||||
export * from '#/alf/types'
|
||||
export * as tokens from '#/alf/tokens'
|
||||
export {atoms} from '#/alf/atoms'
|
||||
export * from '#/alf/util/platform'
|
||||
export * as tokens from '#/alf/tokens'
|
||||
export * from '#/alf/types'
|
||||
export * from '#/alf/util/flatten'
|
||||
export * from '#/alf/util/platform'
|
||||
export * from '#/alf/util/themeSelector'
|
||||
|
||||
type BreakpointName = keyof typeof breakpoints
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import {atoms} from '#/alf/atoms'
|
||||
import * as tokens from '#/alf/tokens'
|
||||
import type {Mutable} from '#/alf/types'
|
||||
import {atoms} from '#/alf/atoms'
|
||||
import {BLUE_HUE, GREEN_HUE, RED_HUE} from '#/alf/util/colorGeneration'
|
||||
|
||||
export type ThemeName = 'light' | 'dim' | 'dark'
|
||||
export type ReadonlyTheme = typeof light
|
||||
export type Theme = Mutable<ReadonlyTheme>
|
||||
export type Theme = Mutable<ReadonlyTheme> & {name: ThemeName}
|
||||
export type ReadonlyPalette = typeof lightPalette
|
||||
export type Palette = Mutable<ReadonlyPalette>
|
||||
|
||||
@@ -193,7 +193,7 @@ export const dimPalette: Palette = {
|
||||
} as const
|
||||
|
||||
export const light = {
|
||||
name: 'light',
|
||||
name: 'light' as ThemeName,
|
||||
palette: lightPalette,
|
||||
atoms: {
|
||||
text: {
|
||||
@@ -278,7 +278,7 @@ export const light = {
|
||||
}
|
||||
|
||||
export const dark: Theme = {
|
||||
name: 'dark',
|
||||
name: 'dark' as ThemeName,
|
||||
palette: darkPalette,
|
||||
atoms: {
|
||||
text: {
|
||||
@@ -367,7 +367,7 @@ export const dark: Theme = {
|
||||
|
||||
export const dim: Theme = {
|
||||
...dark,
|
||||
name: 'dim',
|
||||
name: 'dim' as ThemeName,
|
||||
palette: dimPalette,
|
||||
atoms: {
|
||||
...dark.atoms,
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import {ThemeName} from '#/alf/themes'
|
||||
|
||||
export function select<T>(name: ThemeName, options: Record<ThemeName, T>) {
|
||||
switch (name) {
|
||||
case 'light':
|
||||
return options.light
|
||||
case 'dark':
|
||||
return options.dark || options.dim
|
||||
case 'dim':
|
||||
return options.dim || options.dark
|
||||
default:
|
||||
throw new Error(`select(theme, options) received unknown theme ${name}`)
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,27 @@
|
||||
import React from 'react'
|
||||
import {ColorSchemeName, useColorScheme} from 'react-native'
|
||||
|
||||
import {useThemePrefs} from 'state/shell'
|
||||
import {isWeb} from 'platform/detection'
|
||||
import {ThemeName, light, dark, dim} from '#/alf/themes'
|
||||
import * as SystemUI from 'expo-system-ui'
|
||||
|
||||
import {isWeb} from 'platform/detection'
|
||||
import {useThemePrefs} from 'state/shell'
|
||||
import {dark, dim, light, ThemeName} from '#/alf/themes'
|
||||
|
||||
export function useColorModeTheme(): ThemeName {
|
||||
const theme = useThemeName()
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
updateDocument(theme)
|
||||
SystemUI.setBackgroundColorAsync(getBackgroundColor(theme))
|
||||
}, [theme])
|
||||
|
||||
return theme
|
||||
}
|
||||
|
||||
export function useThemeName(): ThemeName {
|
||||
const colorScheme = useColorScheme()
|
||||
const {colorMode, darkTheme} = useThemePrefs()
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
const theme = getThemeName(colorScheme, colorMode, darkTheme)
|
||||
updateDocument(theme)
|
||||
SystemUI.setBackgroundColorAsync(getBackgroundColor(theme))
|
||||
}, [colorMode, colorScheme, darkTheme])
|
||||
|
||||
return React.useMemo(
|
||||
() => getThemeName(colorScheme, colorMode, darkTheme),
|
||||
[colorScheme, colorMode, darkTheme],
|
||||
)
|
||||
return getThemeName(colorScheme, colorMode, darkTheme)
|
||||
}
|
||||
|
||||
function getThemeName(
|
||||
@@ -53,7 +55,7 @@ function updateDocument(theme: ThemeName) {
|
||||
}
|
||||
}
|
||||
|
||||
function getBackgroundColor(theme: ThemeName): string {
|
||||
export function getBackgroundColor(theme: ThemeName): string {
|
||||
switch (theme) {
|
||||
case 'light':
|
||||
return light.atoms.bg.backgroundColor
|
||||
|
||||
@@ -37,7 +37,7 @@ export function AccountList({
|
||||
style={[
|
||||
a.rounded_md,
|
||||
a.overflow_hidden,
|
||||
a.border,
|
||||
{borderWidth: 1},
|
||||
t.atoms.border_contrast_low,
|
||||
]}>
|
||||
{accounts.map(account => (
|
||||
@@ -48,7 +48,7 @@ export function AccountList({
|
||||
isCurrentAccount={account.did === currentAccount?.did}
|
||||
isPendingAccount={account.did === pendingDid}
|
||||
/>
|
||||
<View style={[a.border_b, t.atoms.border_contrast_low]} />
|
||||
<View style={[{borderBottomWidth: 1}, t.atoms.border_contrast_low]} />
|
||||
</React.Fragment>
|
||||
))}
|
||||
<Button
|
||||
|
||||
@@ -28,7 +28,7 @@ export type ButtonColor =
|
||||
| 'gradient_sunset'
|
||||
| 'gradient_nordic'
|
||||
| 'gradient_bonfire'
|
||||
export type ButtonSize = 'tiny' | 'small' | 'medium' | 'large'
|
||||
export type ButtonSize = 'tiny' | 'xsmall' | 'small' | 'medium' | 'large'
|
||||
export type ButtonShape = 'round' | 'square' | 'default'
|
||||
export type VariantProps = {
|
||||
/**
|
||||
@@ -64,13 +64,14 @@ type NonTextElements =
|
||||
|
||||
export type ButtonProps = Pick<
|
||||
PressableProps,
|
||||
'disabled' | 'onPress' | 'testID' | 'onLongPress'
|
||||
'disabled' | 'onPress' | 'testID' | 'onLongPress' | 'hitSlop'
|
||||
> &
|
||||
AccessibilityProps &
|
||||
VariantProps & {
|
||||
testID?: string
|
||||
label: string
|
||||
style?: StyleProp<ViewStyle>
|
||||
hoverStyle?: StyleProp<ViewStyle>
|
||||
children: NonTextElements | ((context: ButtonContext) => NonTextElements)
|
||||
}
|
||||
|
||||
@@ -96,6 +97,7 @@ export function Button({
|
||||
label,
|
||||
disabled = false,
|
||||
style,
|
||||
hoverStyle: hoverStyleProp,
|
||||
...rest
|
||||
}: ButtonProps) {
|
||||
const t = useTheme()
|
||||
@@ -281,6 +283,8 @@ export function Button({
|
||||
baseStyles.push({paddingVertical: 12}, a.px_2xl, a.rounded_sm, a.gap_md)
|
||||
} else if (size === 'small') {
|
||||
baseStyles.push({paddingVertical: 9}, a.px_lg, a.rounded_sm, a.gap_sm)
|
||||
} else if (size === 'xsmall') {
|
||||
baseStyles.push({paddingVertical: 6}, a.px_sm, a.rounded_sm, a.gap_sm)
|
||||
} else if (size === 'tiny') {
|
||||
baseStyles.push({paddingVertical: 4}, a.px_sm, a.rounded_xs, a.gap_xs)
|
||||
}
|
||||
@@ -293,6 +297,8 @@ export function Button({
|
||||
}
|
||||
} else if (size === 'small') {
|
||||
baseStyles.push({height: 34, width: 34})
|
||||
} else if (size === 'xsmall') {
|
||||
baseStyles.push({height: 28, width: 28})
|
||||
} else if (size === 'tiny') {
|
||||
baseStyles.push({height: 20, width: 20})
|
||||
}
|
||||
@@ -374,8 +380,10 @@ export function Button({
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
flattenedBaseStyles,
|
||||
...(state.hovered || state.pressed ? hoverStyles : []),
|
||||
flatten(style),
|
||||
...(state.hovered || state.pressed
|
||||
? [hoverStyles, flatten(hoverStyleProp)]
|
||||
: []),
|
||||
]}
|
||||
onPressIn={onPressIn}
|
||||
onPressOut={onPressOut}
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
DialogOuterProps,
|
||||
} from '#/components/Dialog/types'
|
||||
import {createInput} from '#/components/forms/TextField'
|
||||
import {FullWindowOverlay} from '#/components/FullWindowOverlay'
|
||||
import {Portal} from '#/components/Portal'
|
||||
|
||||
export {useDialogContext, useDialogControl} from '#/components/Dialog/context'
|
||||
@@ -170,47 +171,49 @@ export function Outer({
|
||||
return (
|
||||
isOpen && (
|
||||
<Portal>
|
||||
<View
|
||||
// iOS
|
||||
accessibilityViewIsModal
|
||||
// Android
|
||||
importantForAccessibility="yes"
|
||||
style={[a.absolute, a.inset_0]}
|
||||
testID={testID}
|
||||
onTouchMove={() => Keyboard.dismiss()}>
|
||||
<BottomSheet
|
||||
enableDynamicSizing={!hasSnapPoints}
|
||||
enablePanDownToClose
|
||||
keyboardBehavior="interactive"
|
||||
android_keyboardInputMode="adjustResize"
|
||||
keyboardBlurBehavior="restore"
|
||||
topInset={insets.top}
|
||||
{...sheetOptions}
|
||||
snapPoints={sheetOptions.snapPoints || ['100%']}
|
||||
ref={sheet}
|
||||
index={openIndex}
|
||||
backgroundStyle={{backgroundColor: 'transparent'}}
|
||||
backdropComponent={Backdrop}
|
||||
handleIndicatorStyle={{backgroundColor: t.palette.primary_500}}
|
||||
handleStyle={{display: 'none'}}
|
||||
onClose={onCloseAnimationComplete}>
|
||||
<Context.Provider value={context}>
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.inset_0,
|
||||
t.atoms.bg,
|
||||
{
|
||||
borderTopLeftRadius: 40,
|
||||
borderTopRightRadius: 40,
|
||||
height: Dimensions.get('window').height * 2,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{children}
|
||||
</Context.Provider>
|
||||
</BottomSheet>
|
||||
</View>
|
||||
<FullWindowOverlay>
|
||||
<View
|
||||
// iOS
|
||||
accessibilityViewIsModal
|
||||
// Android
|
||||
importantForAccessibility="yes"
|
||||
style={[a.absolute, a.inset_0]}
|
||||
testID={testID}
|
||||
onTouchMove={() => Keyboard.dismiss()}>
|
||||
<BottomSheet
|
||||
enableDynamicSizing={!hasSnapPoints}
|
||||
enablePanDownToClose
|
||||
keyboardBehavior="interactive"
|
||||
android_keyboardInputMode="adjustResize"
|
||||
keyboardBlurBehavior="restore"
|
||||
topInset={insets.top}
|
||||
{...sheetOptions}
|
||||
snapPoints={sheetOptions.snapPoints || ['100%']}
|
||||
ref={sheet}
|
||||
index={openIndex}
|
||||
backgroundStyle={{backgroundColor: 'transparent'}}
|
||||
backdropComponent={Backdrop}
|
||||
handleIndicatorStyle={{backgroundColor: t.palette.primary_500}}
|
||||
handleStyle={{display: 'none'}}
|
||||
onClose={onCloseAnimationComplete}>
|
||||
<Context.Provider value={context}>
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.inset_0,
|
||||
t.atoms.bg,
|
||||
{
|
||||
borderTopLeftRadius: 40,
|
||||
borderTopRightRadius: 40,
|
||||
height: Dimensions.get('window').height * 2,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{children}
|
||||
</Context.Provider>
|
||||
</BottomSheet>
|
||||
</View>
|
||||
</FullWindowOverlay>
|
||||
</Portal>
|
||||
)
|
||||
)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export {FullWindowOverlay} from 'react-native-screens'
|
||||
@@ -0,0 +1 @@
|
||||
export {Fragment as FullWindowOverlay} from 'react'
|
||||
@@ -28,6 +28,7 @@ export function RichText({
|
||||
authorHandle,
|
||||
onLinkPress,
|
||||
interactiveStyle,
|
||||
emojiMultiplier = 1.85,
|
||||
}: TextStyleProp &
|
||||
Pick<TextProps, 'selectable'> & {
|
||||
value: RichTextAPI | string
|
||||
@@ -38,6 +39,7 @@ export function RichText({
|
||||
authorHandle?: string
|
||||
onLinkPress?: LinkProps['onPress']
|
||||
interactiveStyle?: TextStyle
|
||||
emojiMultiplier?: number
|
||||
}) {
|
||||
const richText = React.useMemo(
|
||||
() =>
|
||||
@@ -57,17 +59,14 @@ export function RichText({
|
||||
const {text, facets} = richText
|
||||
|
||||
if (!facets?.length) {
|
||||
if (text.length <= 5 && /^\p{Extended_Pictographic}+$/u.test(text)) {
|
||||
if (isOnlyEmoji(text)) {
|
||||
const fontSize =
|
||||
(flattenedStyle.fontSize ?? a.text_sm.fontSize) * emojiMultiplier
|
||||
return (
|
||||
<Text
|
||||
selectable={selectable}
|
||||
testID={testID}
|
||||
style={[
|
||||
{
|
||||
fontSize: 26,
|
||||
lineHeight: 30,
|
||||
},
|
||||
]}
|
||||
style={[plainStyles, {fontSize}]}
|
||||
// @ts-ignore web only -prf
|
||||
dataSet={WORD_WRAP}>
|
||||
{text}
|
||||
@@ -247,3 +246,10 @@ function RichTextTag({
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
export function isOnlyEmoji(text: string) {
|
||||
return (
|
||||
text.length <= 15 &&
|
||||
/^[\p{Emoji_Presentation}\p{Extended_Pictographic}]+$/u.test(text)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
import React, {
|
||||
useCallback,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {Modal, ScrollView, TextInput, View} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {
|
||||
Gif,
|
||||
useFeaturedGifsQuery,
|
||||
useGifSearchQuery,
|
||||
} from '#/state/queries/tenor'
|
||||
import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
|
||||
import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
|
||||
import {FlatList_INTERNAL} from '#/view/com/util/Views'
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
import * as TextField from '#/components/forms/TextField'
|
||||
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
|
||||
import {Button, ButtonText} from '../Button'
|
||||
import {Handle} from '../Dialog'
|
||||
import {useThrottledValue} from '../hooks/useThrottledValue'
|
||||
import {ListFooter, ListMaybePlaceholder} from '../Lists'
|
||||
import {GifPreview} from './GifSelect.shared'
|
||||
|
||||
export function GifSelectDialog({
|
||||
controlRef,
|
||||
onClose,
|
||||
onSelectGif: onSelectGifProp,
|
||||
}: {
|
||||
controlRef: React.RefObject<{open: () => void}>
|
||||
onClose: () => void
|
||||
onSelectGif: (gif: Gif) => void
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
useImperativeHandle(controlRef, () => ({
|
||||
open: () => setOpen(true),
|
||||
}))
|
||||
|
||||
const close = useCallback(() => {
|
||||
setOpen(false)
|
||||
onClose()
|
||||
}, [onClose])
|
||||
|
||||
const onSelectGif = useCallback(
|
||||
(gif: Gif) => {
|
||||
onSelectGifProp(gif)
|
||||
close()
|
||||
},
|
||||
[onSelectGifProp, close],
|
||||
)
|
||||
|
||||
const renderErrorBoundary = useCallback(
|
||||
(error: any) => <ModalError details={String(error)} close={close} />,
|
||||
[close],
|
||||
)
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={open}
|
||||
animationType="slide"
|
||||
presentationStyle="formSheet"
|
||||
onRequestClose={close}
|
||||
aria-modal
|
||||
accessibilityViewIsModal>
|
||||
<View style={[a.flex_1, t.atoms.bg]}>
|
||||
<Handle />
|
||||
<ErrorBoundary renderError={renderErrorBoundary}>
|
||||
<GifList onSelectGif={onSelectGif} close={close} />
|
||||
</ErrorBoundary>
|
||||
</View>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
function GifList({
|
||||
onSelectGif,
|
||||
}: {
|
||||
close: () => void
|
||||
onSelectGif: (gif: Gif) => void
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const textInputRef = useRef<TextInput>(null)
|
||||
const listRef = useRef<FlatList_INTERNAL>(null)
|
||||
const [undeferredSearch, setSearch] = useState('')
|
||||
const search = useThrottledValue(undeferredSearch, 500)
|
||||
|
||||
const isSearching = search.length > 0
|
||||
|
||||
const trendingQuery = useFeaturedGifsQuery()
|
||||
const searchQuery = useGifSearchQuery(search)
|
||||
|
||||
const {
|
||||
data,
|
||||
fetchNextPage,
|
||||
isFetchingNextPage,
|
||||
hasNextPage,
|
||||
error,
|
||||
isLoading,
|
||||
isError,
|
||||
refetch,
|
||||
} = isSearching ? searchQuery : trendingQuery
|
||||
|
||||
const flattenedData = useMemo(() => {
|
||||
return data?.pages.flatMap(page => page.results) || []
|
||||
}, [data])
|
||||
|
||||
const renderItem = useCallback(
|
||||
({item}: {item: Gif}) => {
|
||||
return <GifPreview gif={item} onSelectGif={onSelectGif} />
|
||||
},
|
||||
[onSelectGif],
|
||||
)
|
||||
|
||||
const onEndReached = React.useCallback(() => {
|
||||
if (isFetchingNextPage || !hasNextPage || error) return
|
||||
fetchNextPage()
|
||||
}, [isFetchingNextPage, hasNextPage, error, fetchNextPage])
|
||||
|
||||
const hasData = flattenedData.length > 0
|
||||
|
||||
const onGoBack = useCallback(() => {
|
||||
if (isSearching) {
|
||||
// clear the input and reset the state
|
||||
textInputRef.current?.clear()
|
||||
setSearch('')
|
||||
} else {
|
||||
close()
|
||||
}
|
||||
}, [isSearching])
|
||||
|
||||
const listHeader = useMemo(() => {
|
||||
return (
|
||||
<View style={[a.relative, a.mb_lg, a.pt_4xl, a.flex_row, a.align_center]}>
|
||||
{/* cover top corners */}
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.inset_0,
|
||||
{
|
||||
borderBottomLeftRadius: 8,
|
||||
borderBottomRightRadius: 8,
|
||||
},
|
||||
t.atoms.bg,
|
||||
]}
|
||||
/>
|
||||
|
||||
<TextField.Root>
|
||||
<TextField.Icon icon={Search} />
|
||||
<TextField.Input
|
||||
label={_(msg`Search GIFs`)}
|
||||
placeholder={_(msg`Search Tenor`)}
|
||||
onChangeText={text => {
|
||||
setSearch(text)
|
||||
listRef.current?.scrollToOffset({offset: 0, animated: false})
|
||||
}}
|
||||
returnKeyType="search"
|
||||
clearButtonMode="while-editing"
|
||||
inputRef={textInputRef}
|
||||
maxLength={50}
|
||||
/>
|
||||
</TextField.Root>
|
||||
</View>
|
||||
)
|
||||
}, [t.atoms.bg, _])
|
||||
|
||||
return (
|
||||
<FlatList_INTERNAL
|
||||
ref={listRef}
|
||||
key={gtMobile ? '3 cols' : '2 cols'}
|
||||
data={flattenedData}
|
||||
renderItem={renderItem}
|
||||
numColumns={gtMobile ? 3 : 2}
|
||||
columnWrapperStyle={a.gap_sm}
|
||||
contentContainerStyle={a.px_lg}
|
||||
ListHeaderComponent={
|
||||
<>
|
||||
{listHeader}
|
||||
{!hasData && (
|
||||
<ListMaybePlaceholder
|
||||
isLoading={isLoading}
|
||||
isError={isError}
|
||||
onRetry={refetch}
|
||||
onGoBack={onGoBack}
|
||||
emptyType="results"
|
||||
sideBorders={false}
|
||||
topBorder={false}
|
||||
errorTitle={_(msg`Failed to load GIFs`)}
|
||||
errorMessage={_(msg`There was an issue connecting to Tenor.`)}
|
||||
emptyMessage={
|
||||
isSearching
|
||||
? _(msg`No search results found for "${search}".`)
|
||||
: _(
|
||||
msg`No featured GIFs found. There may be an issue with Tenor.`,
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
stickyHeaderIndices={[0]}
|
||||
onEndReached={onEndReached}
|
||||
onEndReachedThreshold={4}
|
||||
keyExtractor={(item: Gif) => item.id}
|
||||
keyboardDismissMode="on-drag"
|
||||
ListFooterComponent={
|
||||
hasData ? (
|
||||
<ListFooter
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
error={cleanError(error)}
|
||||
onRetry={fetchNextPage}
|
||||
style={{borderTopWidth: 0}}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function ModalError({details, close}: {details?: string; close: () => void}) {
|
||||
const {_} = useLingui()
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
style={[a.flex_1, a.gap_md]}
|
||||
centerContent
|
||||
contentContainerStyle={a.px_lg}>
|
||||
<ErrorScreen
|
||||
title={_(msg`Oh no!`)}
|
||||
message={_(
|
||||
msg`There was an unexpected issue in the application. Please let us know if this happened to you!`,
|
||||
)}
|
||||
details={details}
|
||||
/>
|
||||
<Button
|
||||
label={_(msg`Close dialog`)}
|
||||
onPress={close}
|
||||
color="primary"
|
||||
size="medium"
|
||||
variant="solid">
|
||||
<ButtonText>
|
||||
<Trans>Close</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import React, {useCallback} from 'react'
|
||||
import {Image} from 'expo-image'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {Gif} from '#/state/queries/tenor'
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
import {Button} from '../Button'
|
||||
|
||||
export function GifPreview({
|
||||
gif,
|
||||
onSelectGif,
|
||||
}: {
|
||||
gif: Gif
|
||||
onSelectGif: (gif: Gif) => void
|
||||
}) {
|
||||
const {gtTablet} = useBreakpoints()
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
|
||||
const onPress = useCallback(() => {
|
||||
logEvent('composer:gif:select', {})
|
||||
onSelectGif(gif)
|
||||
}, [onSelectGif, gif])
|
||||
|
||||
return (
|
||||
<Button
|
||||
label={_(msg`Select GIF "${gif.title}"`)}
|
||||
style={[a.flex_1, gtTablet ? {maxWidth: '33%'} : {maxWidth: '50%'}]}
|
||||
onPress={onPress}>
|
||||
{({pressed}) => (
|
||||
<Image
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.mb_sm,
|
||||
a.rounded_sm,
|
||||
{aspectRatio: 1, opacity: pressed ? 0.8 : 1},
|
||||
t.atoms.bg_contrast_25,
|
||||
]}
|
||||
source={{
|
||||
uri: gif.media_formats.tinygif.url,
|
||||
}}
|
||||
contentFit="cover"
|
||||
accessibilityLabel={gif.title}
|
||||
accessibilityHint=""
|
||||
cachePolicy="none"
|
||||
accessibilityIgnoresInvertColors
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
import React, {useCallback, useMemo, useRef, useState} from 'react'
|
||||
import React, {
|
||||
useCallback,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {TextInput, View} from 'react-native'
|
||||
import {Image} from 'expo-image'
|
||||
import {BottomSheetFlatListMethods} from '@discord/bottom-sheet'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {
|
||||
@@ -23,16 +27,23 @@ import {ArrowLeft_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arr
|
||||
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
|
||||
import {Button, ButtonIcon, ButtonText} from '../Button'
|
||||
import {ListFooter, ListMaybePlaceholder} from '../Lists'
|
||||
import {GifPreview} from './GifSelect.shared'
|
||||
|
||||
export function GifSelectDialog({
|
||||
control,
|
||||
controlRef,
|
||||
onClose,
|
||||
onSelectGif: onSelectGifProp,
|
||||
}: {
|
||||
control: Dialog.DialogControlProps
|
||||
controlRef: React.RefObject<{open: () => void}>
|
||||
onClose: () => void
|
||||
onSelectGif: (gif: Gif) => void
|
||||
}) {
|
||||
const control = Dialog.useDialogControl()
|
||||
|
||||
useImperativeHandle(controlRef, () => ({
|
||||
open: () => control.open(),
|
||||
}))
|
||||
|
||||
const onSelectGif = useCallback(
|
||||
(gif: Gif) => {
|
||||
control.close(() => onSelectGifProp(gif))
|
||||
@@ -233,50 +244,6 @@ function GifList({
|
||||
)
|
||||
}
|
||||
|
||||
function GifPreview({
|
||||
gif,
|
||||
onSelectGif,
|
||||
}: {
|
||||
gif: Gif
|
||||
onSelectGif: (gif: Gif) => void
|
||||
}) {
|
||||
const {gtTablet} = useBreakpoints()
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
|
||||
const onPress = useCallback(() => {
|
||||
logEvent('composer:gif:select', {})
|
||||
onSelectGif(gif)
|
||||
}, [onSelectGif, gif])
|
||||
|
||||
return (
|
||||
<Button
|
||||
label={_(msg`Select GIF "${gif.title}"`)}
|
||||
style={[a.flex_1, gtTablet ? {maxWidth: '33%'} : {maxWidth: '50%'}]}
|
||||
onPress={onPress}>
|
||||
{({pressed}) => (
|
||||
<Image
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.mb_sm,
|
||||
a.rounded_sm,
|
||||
{aspectRatio: 1, opacity: pressed ? 0.8 : 1},
|
||||
t.atoms.bg_contrast_25,
|
||||
]}
|
||||
source={{
|
||||
uri: gif.media_formats.tinygif.url,
|
||||
}}
|
||||
contentFit="cover"
|
||||
accessibilityLabel={gif.title}
|
||||
accessibilityHint=""
|
||||
cachePolicy="none"
|
||||
accessibilityIgnoresInvertColors
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogError({details}: {details?: string}) {
|
||||
const {_} = useLingui()
|
||||
const control = Dialog.useDialogContext()
|
||||
|
||||
@@ -26,9 +26,11 @@ import {ArrowBoxLeft_Stroke2_Corner0_Rounded as ArrowBoxLeft} from '#/components
|
||||
import {DotGrid_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid'
|
||||
import {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag'
|
||||
import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute'
|
||||
import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person'
|
||||
import {PersonCheck_Stroke2_Corner0_Rounded as PersonCheck} from '#/components/icons/PersonCheck'
|
||||
import {PersonX_Stroke2_Corner0_Rounded as PersonX} from '#/components/icons/PersonX'
|
||||
import {
|
||||
Person_Stroke2_Corner0_Rounded as Person,
|
||||
PersonCheck_Stroke2_Corner0_Rounded as PersonCheck,
|
||||
PersonX_Stroke2_Corner0_Rounded as PersonX,
|
||||
} from '#/components/icons/Person'
|
||||
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker'
|
||||
import * as Menu from '#/components/Menu'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
|
||||
@@ -6,7 +6,11 @@ import {
|
||||
TextStyle,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import {ChatBskyConvoDefs, RichText as RichTextAPI} from '@atproto/api'
|
||||
import {
|
||||
AppBskyEmbedRecord,
|
||||
ChatBskyConvoDefs,
|
||||
RichText as RichTextAPI,
|
||||
} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
@@ -17,7 +21,8 @@ import {atoms as a, useTheme} from '#/alf'
|
||||
import {ActionsWrapper} from '#/components/dms/ActionsWrapper'
|
||||
import {InlineLinkText} from '#/components/Link'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {RichText} from '../RichText'
|
||||
import {isOnlyEmoji, RichText} from '../RichText'
|
||||
import {MessageItemEmbed} from './MessageItemEmbed'
|
||||
|
||||
let MessageItem = ({
|
||||
item,
|
||||
@@ -77,37 +82,46 @@ let MessageItem = ({
|
||||
return (
|
||||
<View style={[isFromSelf ? a.mr_md : a.ml_md]}>
|
||||
<ActionsWrapper isFromSelf={isFromSelf} message={message}>
|
||||
<View
|
||||
style={[
|
||||
a.py_sm,
|
||||
a.my_2xs,
|
||||
a.rounded_md,
|
||||
{
|
||||
paddingLeft: 14,
|
||||
paddingRight: 14,
|
||||
backgroundColor: isFromSelf
|
||||
? isPending
|
||||
? pendingColor
|
||||
: t.palette.primary_500
|
||||
: t.palette.contrast_50,
|
||||
borderRadius: 17,
|
||||
},
|
||||
isFromSelf
|
||||
? {borderBottomRightRadius: isLastInGroup ? 2 : 17}
|
||||
: {borderBottomLeftRadius: isLastInGroup ? 2 : 17},
|
||||
]}>
|
||||
<RichText
|
||||
value={rt}
|
||||
style={[
|
||||
a.text_md,
|
||||
a.leading_snug,
|
||||
isFromSelf && {color: t.palette.white},
|
||||
isPending && t.name !== 'light' && {color: t.palette.primary_300},
|
||||
]}
|
||||
interactiveStyle={a.underline}
|
||||
enableTags
|
||||
/>
|
||||
</View>
|
||||
{AppBskyEmbedRecord.isView(message.embed) && (
|
||||
<MessageItemEmbed embed={message.embed} />
|
||||
)}
|
||||
{rt.text.length > 0 && (
|
||||
<View
|
||||
style={
|
||||
!isOnlyEmoji(message.text) && [
|
||||
a.py_sm,
|
||||
a.my_2xs,
|
||||
a.rounded_md,
|
||||
{
|
||||
paddingLeft: 14,
|
||||
paddingRight: 14,
|
||||
backgroundColor: isFromSelf
|
||||
? isPending
|
||||
? pendingColor
|
||||
: t.palette.primary_500
|
||||
: t.palette.contrast_50,
|
||||
borderRadius: 17,
|
||||
},
|
||||
isFromSelf ? a.self_end : a.self_start,
|
||||
isFromSelf
|
||||
? {borderBottomRightRadius: isLastInGroup ? 2 : 17}
|
||||
: {borderBottomLeftRadius: isLastInGroup ? 2 : 17},
|
||||
]
|
||||
}>
|
||||
<RichText
|
||||
value={rt}
|
||||
style={[
|
||||
a.text_md,
|
||||
isFromSelf && {color: t.palette.white},
|
||||
isPending &&
|
||||
t.name !== 'light' && {color: t.palette.primary_300},
|
||||
]}
|
||||
interactiveStyle={a.underline}
|
||||
enableTags
|
||||
emojiMultiplier={3}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</ActionsWrapper>
|
||||
|
||||
{isLastInGroup && (
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {AppBskyEmbedRecord} from '@atproto/api'
|
||||
|
||||
import {PostEmbeds} from '#/view/com/util/post-embeds'
|
||||
import {atoms as a, native, useTheme} from '#/alf'
|
||||
|
||||
let MessageItemEmbed = ({
|
||||
embed,
|
||||
}: {
|
||||
embed: AppBskyEmbedRecord.View
|
||||
}): React.ReactNode => {
|
||||
const t = useTheme()
|
||||
|
||||
return (
|
||||
<View style={[a.my_xs, t.atoms.bg, native({flexBasis: 0})]}>
|
||||
<PostEmbeds embed={embed} allowNestedQuotes />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
MessageItemEmbed = React.memo(MessageItemEmbed)
|
||||
export {MessageItemEmbed}
|
||||
@@ -6,12 +6,16 @@ import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {richTextToString} from '#/lib/strings/rich-text-helpers'
|
||||
import {getTranslatorLink} from '#/locale/helpers'
|
||||
import {useLanguagePrefs} from '#/state/preferences'
|
||||
import {useOpenLink} from '#/state/preferences/in-app-browser'
|
||||
import {isWeb} from 'platform/detection'
|
||||
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 {BubbleQuestion_Stroke2_Corner0_Rounded as Translate} from '#/components/icons/Bubble'
|
||||
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'
|
||||
@@ -35,10 +39,12 @@ export let MessageMenu = ({
|
||||
const convo = useConvoActive()
|
||||
const deleteControl = usePromptControl()
|
||||
const reportControl = usePromptControl()
|
||||
const langPrefs = useLanguagePrefs()
|
||||
const openLink = useOpenLink()
|
||||
|
||||
const isFromSelf = message.sender?.did === currentAccount?.did
|
||||
|
||||
const onCopyPostText = React.useCallback(() => {
|
||||
const onCopyMessage = React.useCallback(() => {
|
||||
const str = richTextToString(
|
||||
new RichText({
|
||||
text: message.text,
|
||||
@@ -51,6 +57,14 @@ export let MessageMenu = ({
|
||||
Toast.show(_(msg`Copied to clipboard`))
|
||||
}, [_, message.text, message.facets])
|
||||
|
||||
const onPressTranslateMessage = React.useCallback(() => {
|
||||
const translatorUrl = getTranslatorLink(
|
||||
message.text,
|
||||
langPrefs.primaryLanguage,
|
||||
)
|
||||
openLink(translatorUrl)
|
||||
}, [langPrefs.primaryLanguage, message.text, openLink])
|
||||
|
||||
const onDelete = React.useCallback(() => {
|
||||
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
|
||||
convo
|
||||
@@ -81,16 +95,27 @@ export let MessageMenu = ({
|
||||
)}
|
||||
|
||||
<Menu.Outer>
|
||||
<Menu.Group>
|
||||
<Menu.Item
|
||||
testID="messageDropdownCopyBtn"
|
||||
label={_(msg`Copy message text`)}
|
||||
onPress={onCopyPostText}>
|
||||
<Menu.ItemText>{_(msg`Copy message text`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={ClipboardIcon} position="right" />
|
||||
</Menu.Item>
|
||||
</Menu.Group>
|
||||
<Menu.Divider />
|
||||
{message.text.length > 0 && (
|
||||
<>
|
||||
<Menu.Group>
|
||||
<Menu.Item
|
||||
testID="messageDropdownTranslateBtn"
|
||||
label={_(msg`Translate`)}
|
||||
onPress={onPressTranslateMessage}>
|
||||
<Menu.ItemText>{_(msg`Translate`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Translate} position="right" />
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
testID="messageDropdownCopyBtn"
|
||||
label={_(msg`Copy message text`)}
|
||||
onPress={onCopyMessage}>
|
||||
<Menu.ItemText>{_(msg`Copy message text`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={ClipboardIcon} position="right" />
|
||||
</Menu.Item>
|
||||
</Menu.Group>
|
||||
<Menu.Divider />
|
||||
</>
|
||||
)}
|
||||
<Menu.Group>
|
||||
<Menu.Item
|
||||
testID="messageDropdownDeleteBtn"
|
||||
|
||||
@@ -22,6 +22,7 @@ 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 {PostAlerts} from '#/components/moderation/PostAlerts'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
const PFP_SIZE = isWeb ? 40 : 34
|
||||
@@ -58,7 +59,7 @@ export let MessagesListHeader = ({
|
||||
t.atoms.border_contrast_low,
|
||||
a.border_b,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.align_start,
|
||||
a.gap_sm,
|
||||
gtTablet ? a.pl_lg : a.pl_xl,
|
||||
a.pr_lg,
|
||||
@@ -69,7 +70,7 @@ export let MessagesListHeader = ({
|
||||
testID="conversationHeaderBackBtn"
|
||||
onPress={onPressBack}
|
||||
hitSlop={BACK_HITSLOP}
|
||||
style={{width: 30, height: 30}}
|
||||
style={{width: 30, height: 30, marginTop: isWeb ? 6 : 4}}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Back`)}
|
||||
accessibilityHint="">
|
||||
@@ -152,51 +153,71 @@ function HeaderReady({
|
||||
)
|
||||
|
||||
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 && (
|
||||
<View style={[a.flex_1]}>
|
||||
<View style={[a.w_full, a.flex_row, a.align_center, a.justify_between]}>
|
||||
<Link
|
||||
style={[a.flex_row, a.align_start, a.gap_md, a.flex_1, a.pr_md]}
|
||||
to={makeProfileLink(profile)}>
|
||||
<View style={[a.pt_2xs]}>
|
||||
<PreviewableUserAvatar
|
||||
size={PFP_SIZE}
|
||||
profile={profile}
|
||||
moderation={moderation.ui('avatar')}
|
||||
disableHoverCard={moderation.blocked}
|
||||
/>
|
||||
</View>
|
||||
<View style={a.flex_1}>
|
||||
<Text
|
||||
style={[
|
||||
t.atoms.text_contrast_medium,
|
||||
a.text_sm,
|
||||
web([a.leading_normal, {marginTop: -2}]),
|
||||
]}
|
||||
style={[a.text_md, a.font_bold, web(a.leading_normal)]}
|
||||
numberOfLines={1}>
|
||||
@{profile.handle}
|
||||
{convoState.convo?.muted && (
|
||||
<>
|
||||
{' '}
|
||||
·{' '}
|
||||
<BellStroke size="xs" style={t.atoms.text_contrast_medium} />
|
||||
</>
|
||||
)}
|
||||
{displayName}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</Link>
|
||||
{!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}
|
||||
{isConvoActive(convoState) && (
|
||||
<ConvoMenu
|
||||
convo={convoState.convo}
|
||||
profile={profile}
|
||||
currentScreen="conversation"
|
||||
blockInfo={blockInfo}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
paddingLeft: PFP_SIZE + a.gap_md.gap,
|
||||
},
|
||||
]}>
|
||||
<PostAlerts
|
||||
modui={moderation.ui('contentList')}
|
||||
size="large"
|
||||
style={[a.pt_xs]}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import React, {useCallback} from 'react'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
|
||||
import {logEvent} from 'lib/statsig/statsig'
|
||||
import {FAB} from '#/view/com/util/fab/FAB'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {useTheme} from '#/alf'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
||||
import {SearchablePeopleList} from './SearchablePeopleList'
|
||||
|
||||
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
|
||||
title={_(msg`Start a new chat`)}
|
||||
onSelectChat={onCreateChat}
|
||||
/>
|
||||
</Dialog.Outer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -16,23 +16,19 @@ 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 {useListConvosQuery} from '#/state/queries/messages/list-converations'
|
||||
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 {TextInput} from '#/components/dms/dialogs/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'
|
||||
|
||||
@@ -57,55 +53,291 @@ type Item =
|
||||
key: string
|
||||
}
|
||||
|
||||
export function NewChat({
|
||||
control,
|
||||
onNewChat,
|
||||
export function SearchablePeopleList({
|
||||
title,
|
||||
onSelectChat,
|
||||
showRecentConvos,
|
||||
}: {
|
||||
control: Dialog.DialogControlProps
|
||||
onNewChat: (chatId: string) => void
|
||||
title: string
|
||||
onSelectChat: (did: string) => void
|
||||
showRecentConvos?: boolean
|
||||
}) {
|
||||
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 {mutate: createChat} = useGetConvoForMembers({
|
||||
onSuccess: data => {
|
||||
onNewChat(data.convo.id)
|
||||
const [searchText, setSearchText] = useState('')
|
||||
|
||||
if (!data.convo.lastMessage) {
|
||||
logEvent('chat:create', {logContext: 'NewChatDialog'})
|
||||
const {
|
||||
data: results,
|
||||
isError,
|
||||
isFetching,
|
||||
} = useActorAutocompleteQuery(searchText, true, 12)
|
||||
const {data: follows} = useProfileFollowsQuery(currentAccount?.did)
|
||||
const {data: convos} = useListConvosQuery({enabled: showRecentConvos})
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
logEvent('chat:open', {logContext: 'NewChatDialog'})
|
||||
},
|
||||
onError: error => {
|
||||
Toast.show(error.message)
|
||||
},
|
||||
})
|
||||
} else {
|
||||
const placeholders: Item[] = Array(10)
|
||||
.fill(0)
|
||||
.map((_, i) => ({
|
||||
type: 'placeholder',
|
||||
key: i + '',
|
||||
}))
|
||||
|
||||
const onCreateChat = useCallback(
|
||||
(did: string) => {
|
||||
control.close(() => createChat([did]))
|
||||
if (showRecentConvos) {
|
||||
if (convos && follows) {
|
||||
const usedDids = new Set()
|
||||
|
||||
for (const page of convos.pages) {
|
||||
for (const convo of page.convos) {
|
||||
const profiles = convo.members.filter(
|
||||
m => m.did !== currentAccount?.did,
|
||||
)
|
||||
|
||||
for (const profile of profiles) {
|
||||
if (usedDids.has(profile.did)) continue
|
||||
|
||||
usedDids.add(profile.did)
|
||||
|
||||
_items.push({
|
||||
type: 'profile',
|
||||
key: profile.did,
|
||||
enabled: true,
|
||||
profile,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let followsItems: typeof _items = []
|
||||
|
||||
for (const page of follows.pages) {
|
||||
for (const profile of page.follows) {
|
||||
if (usedDids.has(profile.did)) continue
|
||||
|
||||
followsItems.push({
|
||||
type: 'profile',
|
||||
key: profile.did,
|
||||
enabled: canBeMessaged(profile),
|
||||
profile,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// only sort follows
|
||||
followsItems = followsItems.sort(a => {
|
||||
// @ts-ignore
|
||||
return a.enabled ? -1 : 1
|
||||
})
|
||||
|
||||
// then append
|
||||
_items.push(...followsItems)
|
||||
} else {
|
||||
_items.push(...placeholders)
|
||||
}
|
||||
} 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 {
|
||||
_items.push(...placeholders)
|
||||
}
|
||||
}
|
||||
|
||||
return _items
|
||||
}, [
|
||||
_,
|
||||
searchText,
|
||||
results,
|
||||
isError,
|
||||
currentAccount?.did,
|
||||
follows,
|
||||
convos,
|
||||
showRecentConvos,
|
||||
])
|
||||
|
||||
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={onSelectChat}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case 'placeholder': {
|
||||
return <ProfileCardSkeleton key={item.key} />
|
||||
}
|
||||
case 'empty': {
|
||||
return <Empty key={item.key} message={item.message} />
|
||||
}
|
||||
default:
|
||||
return null
|
||||
}
|
||||
},
|
||||
[control, createChat],
|
||||
[moderationOpts, onSelectChat],
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<FAB
|
||||
testID="newChatFAB"
|
||||
onPress={control.open}
|
||||
icon={<Plus size="lg" fill={t.palette.white} />}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`New chat`)}
|
||||
accessibilityHint=""
|
||||
/>
|
||||
useLayoutEffect(() => {
|
||||
if (isWeb) {
|
||||
setImmediate(() => {
|
||||
inputRef?.current?.focus()
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
<Dialog.Outer
|
||||
control={control}
|
||||
testID="newChatDialog"
|
||||
nativeOptions={{sheet: {snapPoints: ['100%']}}}>
|
||||
<SearchablePeopleList onCreateChat={onCreateChat} />
|
||||
</Dialog.Outer>
|
||||
</>
|
||||
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,
|
||||
]}>
|
||||
{title}
|
||||
</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.atoms.border_contrast_low,
|
||||
t.atoms.bg,
|
||||
t.atoms.text_contrast_high,
|
||||
t.palette.contrast_500,
|
||||
_,
|
||||
title,
|
||||
searchText,
|
||||
control,
|
||||
])
|
||||
|
||||
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"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -163,11 +395,13 @@ function ProfileCard({
|
||||
/>
|
||||
<View style={[a.flex_1, a.gap_2xs]}>
|
||||
<Text
|
||||
style={[t.atoms.text, a.font_bold, a.leading_snug]}
|
||||
style={[t.atoms.text, a.font_bold, a.leading_tight]}
|
||||
numberOfLines={1}>
|
||||
{displayName}
|
||||
</Text>
|
||||
<Text style={t.atoms.text_contrast_high} numberOfLines={2}>
|
||||
<Text
|
||||
style={[a.leading_tight, t.atoms.text_contrast_high]}
|
||||
numberOfLines={2}>
|
||||
{!enabled ? <Trans>{handle} can't be messaged</Trans> : handle}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -293,217 +527,3 @@ function SearchInput({
|
||||
</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"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import React, {useCallback} from 'react'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
|
||||
import {logEvent} from 'lib/statsig/statsig'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {SearchablePeopleList} from './SearchablePeopleList'
|
||||
|
||||
export function SendViaChatDialog({
|
||||
control,
|
||||
onSelectChat,
|
||||
}: {
|
||||
control: Dialog.DialogControlProps
|
||||
onSelectChat: (chatId: string) => void
|
||||
}) {
|
||||
return (
|
||||
<Dialog.Outer
|
||||
control={control}
|
||||
testID="sendViaChatChatDialog"
|
||||
nativeOptions={{sheet: {snapPoints: ['100%']}}}>
|
||||
<SendViaChatDialogInner control={control} onSelectChat={onSelectChat} />
|
||||
</Dialog.Outer>
|
||||
)
|
||||
}
|
||||
|
||||
function SendViaChatDialogInner({
|
||||
control,
|
||||
onSelectChat,
|
||||
}: {
|
||||
control: Dialog.DialogControlProps
|
||||
onSelectChat: (chatId: string) => void
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const {mutate: createChat} = useGetConvoForMembers({
|
||||
onSuccess: data => {
|
||||
onSelectChat(data.convo.id)
|
||||
|
||||
if (!data.convo.lastMessage) {
|
||||
logEvent('chat:create', {logContext: 'SendViaChatDialog'})
|
||||
}
|
||||
logEvent('chat:open', {logContext: 'SendViaChatDialog'})
|
||||
},
|
||||
onError: error => {
|
||||
Toast.show(error.message)
|
||||
},
|
||||
})
|
||||
|
||||
const onCreateChat = useCallback(
|
||||
(did: string) => {
|
||||
control.close(() => createChat([did]))
|
||||
},
|
||||
[control, createChat],
|
||||
)
|
||||
|
||||
return (
|
||||
<SearchablePeopleList
|
||||
title={_(msg`Send post to...`)}
|
||||
onSelectChat={onCreateChat}
|
||||
showRecentConvos
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import {createSinglePathSVG} from './TEMPLATE'
|
||||
|
||||
export const Image_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M3 5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5Zm16 0H5v7.213l1.246-.932.044-.03a3 3 0 0 1 3.863.454c1.468 1.58 2.941 2.749 4.847 2.749 1.703 0 2.855-.555 4-1.618V5Zm0 10.357c-1.112.697-2.386 1.097-4 1.097-2.81 0-4.796-1.755-6.313-3.388a1 1 0 0 0-1.269-.164L5 14.712V19h14v-3.643ZM15 8a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm-3 1a3 3 0 1 1 6 0 3 3 0 0 1-6 0Z',
|
||||
path: 'M3 4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm2 1v7.213l1.246-.932.044-.03a3 3 0 0 1 3.863.454c1.468 1.58 2.941 2.749 4.847 2.749 1.703 0 2.855-.555 4-1.618V5H5Zm14 10.357c-1.112.697-2.386 1.097-4 1.097-2.81 0-4.796-1.755-6.313-3.388a1 1 0 0 0-1.269-.164L5 14.712V19h14v-3.643ZM15 8a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm-3 1a3 3 0 1 1 6 0 3 3 0 0 1-6 0Z',
|
||||
})
|
||||
|
||||
@@ -3,3 +3,19 @@ import {createSinglePathSVG} from './TEMPLATE'
|
||||
export const Person_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM5.678 19h12.644c-.71-2.909-3.092-5-6.322-5s-5.613 2.091-6.322 5Zm-2.174.906C3.917 15.521 7.242 12 12 12c4.758 0 8.083 3.521 8.496 7.906A1 1 0 0 1 19.5 21h-15a1 1 0 0 1-.996-1.094Z',
|
||||
})
|
||||
|
||||
export const PersonCheck_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM5.679 19c.709-2.902 3.079-5 6.321-5a6.69 6.69 0 0 1 2.612.51 1 1 0 0 0 .776-1.844A8.687 8.687 0 0 0 12 12c-4.3 0-7.447 2.884-8.304 6.696-.29 1.29.767 2.304 1.902 2.304H11a1 1 0 1 0 0-2H5.679Zm14.835-4.857a1 1 0 0 1 .344 1.371l-3 5a1 1 0 0 1-1.458.286l-2-1.5a1 1 0 0 1 1.2-1.6l1.113.835 2.43-4.05a1 1 0 0 1 1.372-.342Z',
|
||||
})
|
||||
|
||||
export const PersonX_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM5.679 19c.709-2.902 3.079-5 6.321-5 .302 0 .595.018.878.053a1 1 0 0 0 .243-1.985A9.235 9.235 0 0 0 12 12c-4.3 0-7.447 2.884-8.304 6.696-.29 1.29.767 2.304 1.902 2.304H12a1 1 0 1 0 0-2H5.679Zm9.614-3.707a1 1 0 0 1 1.414 0L18 16.586l1.293-1.293a1 1 0 0 1 1.414 1.414L19.414 18l1.293 1.293a1 1 0 0 1-1.414 1.414L18 19.414l-1.293 1.293a1 1 0 0 1-1.414-1.414L16.586 18l-1.293-1.293a1 1 0 0 1 0-1.414Z',
|
||||
})
|
||||
|
||||
export const PersonPlus_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM5.678 19c.71-2.909 3.092-5 6.322-5 .621 0 1.206.077 1.748.218a1 1 0 1 0 .504-1.936A8.931 8.931 0 0 0 12 12c-4.758 0-8.083 3.521-8.496 7.906A1 1 0 0 0 4.5 21H11a1 1 0 1 0 0-2H5.678ZM18 14a1 1 0 0 1 1 1v2h2a1 1 0 1 1 0 2h-2v2a1 1 0 1 1-2 0v-2h-2a1 1 0 1 1 0-2h2v-2a1 1 0 0 1 1-1Z',
|
||||
})
|
||||
|
||||
export const PersonPlus_Filled_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM12 12c-4.758 0-8.083 3.521-8.496 7.906A1 1 0 0 0 4.5 21H15a3 3 0 1 1 0-6c0-.824.332-1.571.87-2.113C14.739 12.32 13.435 12 12 12Zm6 2a1 1 0 0 1 1 1v2h2a1 1 0 1 1 0 2h-2v2a1 1 0 1 1-2 0v-2h-2a1 1 0 1 1 0-2h2v-2a1 1 0 0 1 1-1Z',
|
||||
})
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import {createSinglePathSVG} from './TEMPLATE'
|
||||
|
||||
export const PersonCheck_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM5.679 19c.709-2.902 3.079-5 6.321-5a6.69 6.69 0 0 1 2.612.51 1 1 0 0 0 .776-1.844A8.687 8.687 0 0 0 12 12c-4.3 0-7.447 2.884-8.304 6.696-.29 1.29.767 2.304 1.902 2.304H11a1 1 0 1 0 0-2H5.679Zm14.835-4.857a1 1 0 0 1 .344 1.371l-3 5a1 1 0 0 1-1.458.286l-2-1.5a1 1 0 0 1 1.2-1.6l1.113.835 2.43-4.05a1 1 0 0 1 1.372-.342Z',
|
||||
})
|
||||
@@ -1,5 +0,0 @@
|
||||
import {createSinglePathSVG} from './TEMPLATE'
|
||||
|
||||
export const PersonX_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM5.679 19c.709-2.902 3.079-5 6.321-5 .302 0 .595.018.878.053a1 1 0 0 0 .243-1.985A9.235 9.235 0 0 0 12 12c-4.3 0-7.447 2.884-8.304 6.696-.29 1.29.767 2.304 1.902 2.304H12a1 1 0 1 0 0-2H5.679Zm9.614-3.707a1 1 0 0 1 1.414 0L18 16.586l1.293-1.293a1 1 0 0 1 1.414 1.414L19.414 18l1.293 1.293a1 1 0 0 1-1.414 1.414L18 19.414l-1.293 1.293a1 1 0 0 1-1.414-1.414L16.586 18l-1.293-1.293a1 1 0 0 1 0-1.414Z',
|
||||
})
|
||||
@@ -0,0 +1,21 @@
|
||||
import {createSinglePathSVG} from './TEMPLATE'
|
||||
|
||||
export const OpenQuote_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M7.574 4.178a1 1 0 0 1 .43.822v5h2a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1h-7a1 1 0 0 1-1-1v-8c0-2.585 1.162-4.335 2.316-5.417a8.163 8.163 0 0 1 1.569-1.15 7.029 7.029 0 0 1 .738-.36l.016-.005.005-.003h.003v-.001c.001 0 .002 0 .353.936l-.351-.936a1 1 0 0 1 .92.114Zm-1.57 2.588a5.99 5.99 0 0 0-.316.276C4.842 7.835 4.004 9.085 4.004 11v7h5v-6h-2a1 1 0 0 1-1-1V6.766Zm12.57-2.588a1 1 0 0 1 .43.822v5h2a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1h-7a1 1 0 0 1-1-1v-8c0-2.585 1.162-4.335 2.316-5.417a8.166 8.166 0 0 1 1.569-1.15 7.038 7.038 0 0 1 .738-.36l.016-.005.005-.003h.003v-.001c.001 0 .002 0 .353.936l-.351-.936a1 1 0 0 1 .92.114Zm-1.57 2.588c-.105.085-.21.177-.316.276-.846.793-1.684 2.043-1.684 3.958v7h5v-6h-2a1 1 0 0 1-1-1V6.766Z',
|
||||
})
|
||||
|
||||
export const OpenQuote_Filled_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M8.004 5a1 1 0 0 0-.43-.822c-.57-.395-1.176-.031-1.685.255-.428.24-.998.614-1.569 1.15C3.166 6.665 2.004 8.415 2.004 11v8a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1v-8a1 1 0 0 0-1-1h-2V5ZM19.004 5a1 1 0 0 0-.43-.822c-.57-.395-1.176-.031-1.685.255-.428.24-.998.614-1.569 1.15-1.154 1.082-2.316 2.832-2.316 5.417v8a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1v-8a1 1 0 0 0-1-1h-2V5Z',
|
||||
})
|
||||
|
||||
export const CloseQuote_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M2.004 5a1 1 0 0 1 1-1h7a1 1 0 0 1 1 1v8c0 2.585-1.162 4.335-2.316 5.417-.571.536-1.14.91-1.569 1.15a7.01 7.01 0 0 1-.738.36l-.016.006-.006.002h-.002l-.001.001L6.004 19l.351.936A1 1 0 0 1 5.004 19v-5h-2a1 1 0 0 1-1-1V5Zm5 12.234c.104-.085.21-.177.316-.276.846-.793 1.684-2.043 1.684-3.958V6h-5v6h2a1 1 0 0 1 1 1v4.234Zm6-12.234a1 1 0 0 1 1-1h7a1 1 0 0 1 1 1v8c0 2.585-1.162 4.335-2.316 5.417-.571.536-1.14.91-1.569 1.15a7.018 7.018 0 0 1-.738.36l-.016.006-.006.002h-.002l-.001.001-.352-.936.351.936A1 1 0 0 1 16.004 19v-5h-2a1 1 0 0 1-1-1V5Zm5 12.234V13a1 1 0 0 0-1-1h-2V6h5v7c0 1.915-.838 3.165-1.684 3.958-.106.1-.212.191-.316.276Z',
|
||||
})
|
||||
|
||||
export const CloseQuote_Stroke2_Corner1_Rounded = createSinglePathSVG({
|
||||
path: 'M2.003 5.999a2 2 0 0 1 2-1.999h5c1.104 0 2 .893 2 1.999V13c0 2.585-1.16 4.335-2.315 5.417-.571.536-1.14.91-1.569 1.15a7.01 7.01 0 0 1-.738.36l-.016.006-.006.002h-.002l-.001.001L6.004 19l.351.936a1 1 0 0 1-1.351-.935L5 14H4a2 2 0 0 1-2-2.001l.002-6Zm5 11.236L7 12.999A1 1 0 0 0 6 12H4l.003-6h5v7c0 1.915-.837 3.165-1.683 3.958-.106.1-.213.192-.317.277Zm6-11.235a2 2 0 0 1 2-2h5c1.104 0 2 .893 2 1.999V13c0 2.585-1.16 4.335-2.315 5.417-.571.536-1.14.91-1.569 1.15a7.018 7.018 0 0 1-.738.36l-.016.006-.006.002h-.002l-.001.001-.352-.936.351.936A1 1 0 0 1 16.004 19v-5h-1a2 2 0 0 1-2-2V6Zm7 0h-5v6h2a1 1 0 0 1 1 1v4.234c.105-.085.211-.177.317-.276.846-.793 1.684-2.043 1.684-3.958V6Z',
|
||||
})
|
||||
|
||||
export const CloseQuote_Filled_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M3.004 4a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h2v5a1 1 0 0 0 .43.822c.57.395 1.176.031 1.685-.255.428-.24.998-.614 1.569-1.15 1.154-1.082 2.316-2.832 2.316-5.417V5a1 1 0 0 0-1-1h-7ZM14.004 4a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h2v5a1 1 0 0 0 .43.822c.57.395 1.176.031 1.685-.255.428-.24.998-.614 1.569-1.15 1.154-1.082 2.316-2.832 2.316-5.417V5a1 1 0 0 0-1-1h-7Z',
|
||||
})
|
||||
@@ -0,0 +1,13 @@
|
||||
import {createSinglePathSVG} from './TEMPLATE'
|
||||
|
||||
export const Repost_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M16.293 2.293a1 1 0 0 1 1.414 0l3 3a1 1 0 0 1 0 1.414l-3 3a1 1 0 0 1-1.414-1.414L17.586 7H5v4a1 1 0 1 1-2 0V6a1 1 0 0 1 1-1h13.586l-1.293-1.293a1 1 0 0 1 0-1.414ZM21 13v5a1 1 0 0 1-1 1H6.414l1.293 1.293a1 1 0 1 1-1.414 1.414l-3-3a1 1 0 0 1 0-1.414l3-3a1 1 0 0 1 1.414 1.414L6.414 17H19v-4a1 1 0 1 1 2 0Z',
|
||||
})
|
||||
|
||||
export const Repost_Stroke2_Corner2_Rounded = createSinglePathSVG({
|
||||
path: 'M17.957 2.293a1 1 0 1 0-1.414 1.414L17.836 5H6a3 3 0 0 0-3 3v3a1 1 0 1 0 2 0V8a1 1 0 0 1 1-1h11.836l-1.293 1.293a1 1 0 0 0 1.414 1.414l2.47-2.47a1.75 1.75 0 0 0 0-2.474l-2.47-2.47ZM20 12a1 1 0 0 1 1 1v3a3 3 0 0 1-3 3H6.164l1.293 1.293a1 1 0 1 1-1.414 1.414l-2.47-2.47a1.75 1.75 0 0 1 0-2.474l2.47-2.47a1 1 0 0 1 1.414 1.414L6.164 17H18a1 1 0 0 0 1-1v-3a1 1 0 0 1 1-1Z',
|
||||
})
|
||||
|
||||
export const Repost_Stroke2_Corner3_Rounded = createSinglePathSVG({
|
||||
path: 'M16.793 2.293a1 1 0 0 1 1.414 0L20.5 4.586a2 2 0 0 1 0 2.828l-2.293 2.293a1 1 0 0 1-1.414-1.414L18.086 7H7a2 2 0 0 0-2 2v2a1 1 0 1 1-2 0V9a4 4 0 0 1 4-4h11.086l-1.293-1.293a1 1 0 0 1 0-1.414ZM20 12a1 1 0 0 1 1 1v2a4 4 0 0 1-4 4H5.914l1.293 1.293a1 1 0 1 1-1.414 1.414L3.5 19.414a2 2 0 0 1 0-2.828l2.293-2.293a1 1 0 0 1 1.414 1.414L5.914 17H17a2 2 0 0 0 2-2v-2a1 1 0 0 1 1-1Z',
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
import {StyleSheet, TextProps} from 'react-native'
|
||||
import type {SvgProps, PathProps} from 'react-native-svg'
|
||||
import type {PathProps, SvgProps} from 'react-native-svg'
|
||||
|
||||
import {tokens} from '#/alf'
|
||||
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import React from 'react'
|
||||
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
|
||||
import {ModerationUI} from '@atproto/api'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
|
||||
import {isJustAMute} from '#/lib/moderation'
|
||||
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
|
||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||
|
||||
import {atoms as a, useTheme, useBreakpoints, web} from '#/alf'
|
||||
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
|
||||
import {Button} from '#/components/Button'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {
|
||||
ModerationDetailsDialog,
|
||||
useModerationDetailsDialogControl,
|
||||
} from '#/components/moderation/ModerationDetailsDialog'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function ContentHider({
|
||||
testID,
|
||||
@@ -52,7 +51,9 @@ export function ContentHider({
|
||||
<ModerationDetailsDialog control={control} modcause={blur} />
|
||||
|
||||
<Button
|
||||
onPress={() => {
|
||||
onPress={e => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
if (!modui.noOverride) {
|
||||
setOverride(v => !v)
|
||||
} else {
|
||||
@@ -121,7 +122,9 @@ export function ContentHider({
|
||||
|
||||
{desc.source && blur.type === 'label' && !override && (
|
||||
<Button
|
||||
onPress={() => {
|
||||
onPress={e => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
control.open()
|
||||
}}
|
||||
label={_(
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React from 'react'
|
||||
import {StyleProp, View, ViewStyle} from 'react-native'
|
||||
import {ModerationCause, ModerationUI} from '@atproto/api'
|
||||
import {BSKY_LABELER_DID, ModerationCause, ModerationUI} from '@atproto/api'
|
||||
|
||||
import {getModerationCauseKey} from '#/lib/moderation'
|
||||
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
|
||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button} from '#/components/Button'
|
||||
import {
|
||||
@@ -14,9 +15,11 @@ import {Text} from '#/components/Typography'
|
||||
|
||||
export function PostAlerts({
|
||||
modui,
|
||||
size,
|
||||
style,
|
||||
}: {
|
||||
modui: ModerationUI
|
||||
size?: 'medium' | 'large'
|
||||
includeMute?: boolean
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
@@ -28,17 +31,31 @@ export function PostAlerts({
|
||||
<View style={[a.flex_col, a.gap_xs, style]}>
|
||||
<View style={[a.flex_row, a.flex_wrap, a.gap_xs]}>
|
||||
{modui.alerts.map(cause => (
|
||||
<PostLabel key={getModerationCauseKey(cause)} cause={cause} />
|
||||
<PostLabel
|
||||
key={getModerationCauseKey(cause)}
|
||||
cause={cause}
|
||||
size={size}
|
||||
/>
|
||||
))}
|
||||
{modui.informs.map(cause => (
|
||||
<PostLabel key={getModerationCauseKey(cause)} cause={cause} />
|
||||
<PostLabel
|
||||
key={getModerationCauseKey(cause)}
|
||||
cause={cause}
|
||||
size={size}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function PostLabel({cause}: {cause: ModerationCause}) {
|
||||
function PostLabel({
|
||||
cause,
|
||||
size,
|
||||
}: {
|
||||
cause: ModerationCause
|
||||
size?: 'medium' | 'large'
|
||||
}) {
|
||||
const control = useModerationDetailsDialogControl()
|
||||
const desc = useModerationCauseDescription(cause)
|
||||
const t = useTheme()
|
||||
@@ -47,7 +64,9 @@ function PostLabel({cause}: {cause: ModerationCause}) {
|
||||
<>
|
||||
<Button
|
||||
label={desc.name}
|
||||
onPress={() => {
|
||||
onPress={e => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
control.open()
|
||||
}}>
|
||||
{({hovered, pressed}) => (
|
||||
@@ -55,24 +74,36 @@ function PostLabel({cause}: {cause: ModerationCause}) {
|
||||
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,
|
||||
? size === 'large'
|
||||
? t.atoms.bg_contrast_50
|
||||
: t.atoms.bg_contrast_25
|
||||
: size === 'large'
|
||||
? t.atoms.bg_contrast_25
|
||||
: undefined,
|
||||
size === 'large'
|
||||
? {paddingLeft: 4, paddingRight: 6, paddingVertical: 2}
|
||||
: {paddingRight: 4, paddingVertical: 1},
|
||||
]}>
|
||||
<desc.icon size="xs" fill={t.atoms.text_contrast_medium.color} />
|
||||
{desc.sourceType === 'labeler' &&
|
||||
desc.sourceDid !== BSKY_LABELER_DID ? (
|
||||
<UserAvatar
|
||||
avatar={desc.sourceAvi}
|
||||
size={size === 'large' ? 16 : 12}
|
||||
/>
|
||||
) : (
|
||||
<desc.icon size="sm" 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,
|
||||
size === 'large' ? {fontSize: 13} : a.text_xs,
|
||||
size === 'large' ? t.atoms.text : t.atoms.text_contrast_high,
|
||||
]}>
|
||||
{desc.name}
|
||||
{desc.source ? ` – ${desc.source}` : ''}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
@@ -23,6 +23,7 @@ interface Props extends ComponentProps<typeof Link> {
|
||||
iconStyles: StyleProp<ViewStyle>
|
||||
modui: ModerationUI
|
||||
profile: AppBskyActorDefs.ProfileViewBasic
|
||||
interpretFilterAsBlur?: boolean
|
||||
}
|
||||
|
||||
export function PostHider({
|
||||
@@ -35,6 +36,7 @@ export function PostHider({
|
||||
iconSize,
|
||||
iconStyles,
|
||||
profile,
|
||||
interpretFilterAsBlur,
|
||||
...props
|
||||
}: Props) {
|
||||
const queryClient = useQueryClient()
|
||||
@@ -42,7 +44,8 @@ export function PostHider({
|
||||
const {_} = useLingui()
|
||||
const [override, setOverride] = React.useState(false)
|
||||
const control = useModerationDetailsDialogControl()
|
||||
const blur = modui.blurs[0]
|
||||
const blur =
|
||||
modui.blurs[0] || (interpretFilterAsBlur ? modui.filters[0] : undefined)
|
||||
const desc = useModerationCauseDescription(blur)
|
||||
|
||||
const onBeforePress = React.useCallback(() => {
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import React from 'react'
|
||||
import {StyleProp, View, ViewStyle} from 'react-native'
|
||||
import {ModerationCause, ModerationDecision} from '@atproto/api'
|
||||
import {
|
||||
BSKY_LABELER_DID,
|
||||
ModerationCause,
|
||||
ModerationDecision,
|
||||
} from '@atproto/api'
|
||||
|
||||
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
|
||||
import {getModerationCauseKey} from 'lib/moderation'
|
||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button} from '#/components/Button'
|
||||
import {
|
||||
@@ -62,7 +67,12 @@ function ProfileLabel({cause}: {cause: ModerationCause}) {
|
||||
? t.atoms.bg_contrast_50
|
||||
: t.atoms.bg_contrast_25,
|
||||
]}>
|
||||
<desc.icon size="sm" fill={t.atoms.text_contrast_medium.color} />
|
||||
{desc.sourceType === 'labeler' &&
|
||||
desc.sourceDid !== BSKY_LABELER_DID ? (
|
||||
<UserAvatar avatar={desc.sourceAvi} size={16} />
|
||||
) : (
|
||||
<desc.icon size="sm" fill={t.atoms.text_contrast_medium.color} />
|
||||
)}
|
||||
<Text
|
||||
style={[
|
||||
a.text_left,
|
||||
@@ -72,7 +82,6 @@ function ProfileLabel({cause}: {cause: ModerationCause}) {
|
||||
a.font_semibold,
|
||||
]}>
|
||||
{desc.name}
|
||||
{desc.source ? ` – ${desc.source}` : ''}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
AppBskyEmbedRecord,
|
||||
AppBskyEmbedRecordWithMedia,
|
||||
AppBskyFeedThreadgate,
|
||||
AppBskyRichtextFacet,
|
||||
BskyAgent,
|
||||
ComAtprotoLabelDefs,
|
||||
ComAtprotoRepoUploadBlob,
|
||||
@@ -15,7 +14,7 @@ import {AtUri} from '@atproto/api'
|
||||
import {logger} from '#/logger'
|
||||
import {ThreadgateSetting} from '#/state/queries/threadgate'
|
||||
import {isNetworkError} from 'lib/strings/errors'
|
||||
import {shortenLinks} from 'lib/strings/rich-text-manip'
|
||||
import {shortenLinks, stripInvalidMentions} from 'lib/strings/rich-text-manip'
|
||||
import {isNative, isWeb} from 'platform/detection'
|
||||
import {ImageModel} from 'state/models/media/image'
|
||||
import {LinkMeta} from '../link-meta/link-meta'
|
||||
@@ -81,17 +80,7 @@ export async function post(agent: BskyAgent, opts: PostOpts) {
|
||||
opts.onStateChange?.('Processing...')
|
||||
await rt.detectFacets(agent)
|
||||
rt = shortenLinks(rt)
|
||||
|
||||
// filter out any mention facets that didn't map to a user
|
||||
rt.facets = rt.facets?.filter(facet => {
|
||||
const mention = facet.features.find(feature =>
|
||||
AppBskyRichtextFacet.isMention(feature),
|
||||
)
|
||||
if (mention && !mention.did) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
rt = stripInvalidMentions(rt)
|
||||
|
||||
// add quote embed if present
|
||||
if (opts.quote) {
|
||||
|
||||
@@ -2,98 +2,6 @@ import React from 'react'
|
||||
import {StyleProp, TextStyle, ViewStyle} from 'react-native'
|
||||
import Svg, {Ellipse, Line, Path, Rect} from 'react-native-svg'
|
||||
|
||||
export function GridIcon({
|
||||
style,
|
||||
solid,
|
||||
}: {
|
||||
style?: StyleProp<ViewStyle>
|
||||
solid?: boolean
|
||||
}) {
|
||||
const DIM = 4
|
||||
const ARC = 2
|
||||
return (
|
||||
<Svg width="24" height="24" style={style}>
|
||||
<Path
|
||||
d={`M4,1 h${DIM} a${ARC},${ARC} 0 0 1 ${ARC},${ARC} v${DIM} a${ARC},${ARC} 0 0 1 -${ARC},${ARC} h-${DIM} a${ARC},${ARC} 0 0 1 -${ARC},-${ARC} v-${DIM} a${ARC},${ARC} 0 0 1 ${ARC},-${ARC} z`}
|
||||
strokeWidth={2}
|
||||
stroke="#000"
|
||||
fill={solid ? '#000' : undefined}
|
||||
/>
|
||||
<Path
|
||||
d={`M16,1 h${DIM} a${ARC},${ARC} 0 0 1 ${ARC},${ARC} v${DIM} a${ARC},${ARC} 0 0 1 -${ARC},${ARC} h-${DIM} a${ARC},${ARC} 0 0 1 -${ARC},-${ARC} v-${DIM} a${ARC},${ARC} 0 0 1 ${ARC},-${ARC} z`}
|
||||
strokeWidth={2}
|
||||
stroke="#000"
|
||||
fill={solid ? '#000' : undefined}
|
||||
/>
|
||||
<Path
|
||||
d={`M4,13 h${DIM} a${ARC},${ARC} 0 0 1 ${ARC},${ARC} v${DIM} a${ARC},${ARC} 0 0 1 -${ARC},${ARC} h-${DIM} a${ARC},${ARC} 0 0 1 -${ARC},-${ARC} v-${DIM} a${ARC},${ARC} 0 0 1 ${ARC},-${ARC} z`}
|
||||
strokeWidth={2}
|
||||
stroke="#000"
|
||||
fill={solid ? '#000' : undefined}
|
||||
/>
|
||||
<Path
|
||||
d={`M16,13 h${DIM} a${ARC},${ARC} 0 0 1 ${ARC},${ARC} v${DIM} a${ARC},${ARC} 0 0 1 -${ARC},${ARC} h-${DIM} a${ARC},${ARC} 0 0 1 -${ARC},-${ARC} v-${DIM} a${ARC},${ARC} 0 0 1 ${ARC},-${ARC} z`}
|
||||
strokeWidth={2}
|
||||
stroke="#000"
|
||||
fill={solid ? '#000' : undefined}
|
||||
/>
|
||||
</Svg>
|
||||
)
|
||||
}
|
||||
export function GridIconSolid({style}: {style?: StyleProp<ViewStyle>}) {
|
||||
return <GridIcon style={style} solid />
|
||||
}
|
||||
|
||||
export function HomeIcon({
|
||||
style,
|
||||
size,
|
||||
strokeWidth = 4,
|
||||
}: {
|
||||
style?: StyleProp<ViewStyle>
|
||||
size?: string | number
|
||||
strokeWidth?: number
|
||||
}) {
|
||||
return (
|
||||
<Svg
|
||||
viewBox="0 0 48 48"
|
||||
width={size || 24}
|
||||
height={size || 24}
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
style={style}>
|
||||
<Path
|
||||
strokeWidth={strokeWidth}
|
||||
d="M 23.951 2 C 23.631 2.011 23.323 2.124 23.072 2.322 L 8.859 13.52 C 7.055 14.941 6 17.114 6 19.41 L 6 38.5 C 6 39.864 7.136 41 8.5 41 L 18.5 41 C 19.864 41 21 39.864 21 38.5 L 21 28.5 C 21 28.205 21.205 28 21.5 28 L 26.5 28 C 26.795 28 27 28.205 27 28.5 L 27 38.5 C 27 39.864 28.136 41 29.5 41 L 39.5 41 C 40.864 41 42 39.864 42 38.5 L 42 19.41 C 42 17.114 40.945 14.941 39.141 13.52 L 24.928 2.322 C 24.65 2.103 24.304 1.989 23.951 2 Z"
|
||||
/>
|
||||
</Svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function HomeIconSolid({
|
||||
style,
|
||||
size,
|
||||
strokeWidth = 4,
|
||||
}: {
|
||||
style?: StyleProp<ViewStyle>
|
||||
size?: string | number
|
||||
strokeWidth?: number
|
||||
}) {
|
||||
return (
|
||||
<Svg
|
||||
viewBox="0 0 48 48"
|
||||
width={size || 24}
|
||||
height={size || 24}
|
||||
stroke="currentColor"
|
||||
style={style}>
|
||||
<Path
|
||||
fill="currentColor"
|
||||
strokeWidth={strokeWidth}
|
||||
d="m 23.951,2 c -0.32,0.011 -0.628,0.124 -0.879,0.322 L 8.859,13.52 C 7.055,14.941 6,17.114 6,19.41 V 38.5 C 6,39.864 7.136,41 8.5,41 h 8 c 1.364,0 2.5,-1.136 2.5,-2.5 v -12 C 19,26.205 19.205,26 19.5,26 h 9 c 0.295,0 0.5,0.205 0.5,0.5 v 12 c 0,1.364 1.136,2.5 2.5,2.5 h 8 C 40.864,41 42,39.864 42,38.5 V 19.41 c 0,-2.296 -1.055,-4.469 -2.859,-5.89 L 24.928,2.322 C 24.65,2.103 24.304,1.989 23.951,2 Z"
|
||||
/>
|
||||
</Svg>
|
||||
)
|
||||
}
|
||||
|
||||
// Copyright (c) 2020 Refactoring UI Inc.
|
||||
// https://github.com/tailwindlabs/heroicons/blob/master/LICENSE
|
||||
export function MagnifyingGlassIcon({
|
||||
@@ -147,90 +55,6 @@ export function MagnifyingGlassIcon2({
|
||||
)
|
||||
}
|
||||
|
||||
export function MagnifyingGlassIcon2Solid({
|
||||
style,
|
||||
size,
|
||||
strokeWidth = 2,
|
||||
}: {
|
||||
style?: StyleProp<ViewStyle>
|
||||
size?: string | number
|
||||
strokeWidth?: number
|
||||
}) {
|
||||
return (
|
||||
<Svg
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={strokeWidth}
|
||||
stroke="currentColor"
|
||||
width={size || 24}
|
||||
height={size || 24}
|
||||
style={style}>
|
||||
<Ellipse
|
||||
cx="12"
|
||||
cy="10.5"
|
||||
rx="6.5"
|
||||
ry="6.5"
|
||||
stroke="none"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<Ellipse cx="12" cy="10.5" rx="9" ry="9" />
|
||||
<Line x1="18.5" y1="17" x2="22" y2="20.5" strokeLinecap="round" />
|
||||
</Svg>
|
||||
)
|
||||
}
|
||||
|
||||
// https://github.com/Remix-Design/RemixIcon/blob/master/License
|
||||
export function BellIcon({
|
||||
style,
|
||||
size,
|
||||
strokeWidth = 1.5,
|
||||
}: {
|
||||
style?: StyleProp<ViewStyle>
|
||||
size?: string | number
|
||||
strokeWidth?: number
|
||||
}) {
|
||||
return (
|
||||
<Svg
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
width={size || 24}
|
||||
height={size || 24}
|
||||
strokeWidth={strokeWidth}
|
||||
stroke="currentColor"
|
||||
style={style}>
|
||||
<Path d="M 11.642 2 H 12.442 A 8.6 8.55 0 0 1 21.042 10.55 V 18.1 A 1 1 0 0 1 20.042 19.1 H 4.042 A 1 1 0 0 1 3.042 18.1 V 10.55 A 8.6 8.55 0 0 1 11.642 2 Z" />
|
||||
<Line x1="9" y1="22" x2="15" y2="22" />
|
||||
</Svg>
|
||||
)
|
||||
}
|
||||
|
||||
// https://github.com/Remix-Design/RemixIcon/blob/master/License
|
||||
export function BellIconSolid({
|
||||
style,
|
||||
size,
|
||||
strokeWidth = 1.5,
|
||||
}: {
|
||||
style?: StyleProp<ViewStyle>
|
||||
size?: string | number
|
||||
strokeWidth?: number
|
||||
}) {
|
||||
return (
|
||||
<Svg
|
||||
viewBox="0 0 24 24"
|
||||
width={size || 24}
|
||||
height={size || 24}
|
||||
strokeWidth={strokeWidth}
|
||||
stroke="currentColor"
|
||||
style={style}>
|
||||
<Path
|
||||
d="M 11.642 2 H 12.442 A 8.6 8.55 0 0 1 21.042 10.55 V 18.1 A 1 1 0 0 1 20.042 19.1 H 4.042 A 1 1 0 0 1 3.042 18.1 V 10.55 A 8.6 8.55 0 0 1 11.642 2 Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<Line x1="9" y1="22" x2="15" y2="22" />
|
||||
</Svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function CogIcon({
|
||||
style,
|
||||
size,
|
||||
@@ -263,160 +87,6 @@ export function CogIcon({
|
||||
)
|
||||
}
|
||||
|
||||
export function CogIconSolid({
|
||||
style,
|
||||
size,
|
||||
strokeWidth = 1.5,
|
||||
}: {
|
||||
style?: StyleProp<ViewStyle>
|
||||
size?: string | number
|
||||
strokeWidth: number
|
||||
}) {
|
||||
return (
|
||||
<Svg
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
width={size || 32}
|
||||
height={size || 32}
|
||||
strokeWidth={strokeWidth}
|
||||
stroke="currentColor"
|
||||
style={style}>
|
||||
<Path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M 9.594 3.94 C 9.684 3.398 10.154 3 10.704 3 L 13.297 3 C 13.847 3 14.317 3.398 14.407 3.94 L 14.62 5.221 C 14.683 5.595 14.933 5.907 15.265 6.091 C 15.339 6.131 15.412 6.174 15.485 6.218 C 15.809 6.414 16.205 6.475 16.56 6.342 L 17.777 5.886 C 18.292 5.692 18.872 5.9 19.147 6.376 L 20.443 8.623 C 20.718 9.099 20.608 9.705 20.183 10.054 L 19.18 10.881 C 18.887 11.121 18.742 11.494 18.749 11.873 C 18.751 11.958 18.751 12.043 18.749 12.128 C 18.742 12.506 18.887 12.878 19.179 13.118 L 20.184 13.946 C 20.608 14.296 20.718 14.9 20.444 15.376 L 19.146 17.623 C 18.871 18.099 18.292 18.307 17.777 18.114 L 16.56 17.658 C 16.205 17.525 15.81 17.586 15.484 17.782 C 15.412 17.826 15.338 17.869 15.264 17.91 C 14.933 18.093 14.683 18.405 14.62 18.779 L 14.407 20.059 C 14.317 20.602 13.847 21 13.297 21 L 10.703 21 C 10.153 21 9.683 20.602 9.593 20.06 L 9.38 18.779 C 9.318 18.405 9.068 18.093 8.736 17.909 C 8.662 17.868 8.589 17.826 8.516 17.782 C 8.191 17.586 7.796 17.525 7.44 17.658 L 6.223 18.114 C 5.708 18.307 5.129 18.1 4.854 17.624 L 3.557 15.377 C 3.282 14.901 3.392 14.295 3.817 13.946 L 4.821 13.119 C 5.113 12.879 5.258 12.506 5.251 12.127 C 5.249 12.042 5.249 11.957 5.251 11.872 C 5.258 11.494 5.113 11.122 4.821 10.882 L 3.817 10.054 C 3.393 9.705 3.283 9.1 3.557 8.624 L 4.854 6.377 C 5.129 5.9 5.709 5.692 6.224 5.886 L 7.44 6.342 C 7.796 6.475 8.191 6.414 8.516 6.218 C 8.588 6.174 8.662 6.131 8.736 6.09 C 9.068 5.907 9.318 5.595 9.38 5.221 Z M 13.5 9.402 C 11.5 8.247 9 9.691 9 12 C 9 13.072 9.572 14.062 10.5 14.598 C 12.5 15.753 15 14.309 15 12 C 15 10.928 14.428 9.938 13.5 9.402 Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</Svg>
|
||||
)
|
||||
}
|
||||
|
||||
// Copyright (c) 2020 Refactoring UI Inc.
|
||||
// https://github.com/tailwindlabs/heroicons/blob/master/LICENSE
|
||||
export function MoonIcon({
|
||||
style,
|
||||
size,
|
||||
strokeWidth = 1.5,
|
||||
}: {
|
||||
style?: StyleProp<ViewStyle>
|
||||
size?: string | number
|
||||
strokeWidth?: number
|
||||
}) {
|
||||
return (
|
||||
<Svg
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
width={size || 32}
|
||||
height={size || 32}
|
||||
strokeWidth={strokeWidth}
|
||||
stroke="currentColor"
|
||||
style={style}>
|
||||
<Path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z"
|
||||
/>
|
||||
</Svg>
|
||||
)
|
||||
}
|
||||
|
||||
// Copyright (c) 2020 Refactoring UI Inc.
|
||||
// https://github.com/tailwindlabs/heroicons/blob/master/LICENSE
|
||||
export function SunIcon({
|
||||
style,
|
||||
size,
|
||||
strokeWidth = 1.5,
|
||||
}: {
|
||||
style?: StyleProp<ViewStyle>
|
||||
size?: string | number
|
||||
strokeWidth?: number
|
||||
}) {
|
||||
return (
|
||||
<Svg
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
width={size || 32}
|
||||
height={size || 32}
|
||||
strokeWidth={strokeWidth}
|
||||
stroke="currentColor"
|
||||
style={style}>
|
||||
<Path
|
||||
d="M12 3V5.25M18.364 5.63604L16.773 7.22703M21 12H18.75M18.364 18.364L16.773 16.773M12 18.75V21M7.22703 16.773L5.63604 18.364M5.25 12H3M7.22703 7.22703L5.63604 5.63604M15.75 12C15.75 14.0711 14.0711 15.75 12 15.75C9.92893 15.75 8.25 14.0711 8.25 12C8.25 9.92893 9.92893 8.25 12 8.25C14.0711 8.25 15.75 9.92893 15.75 12Z"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</Svg>
|
||||
)
|
||||
}
|
||||
|
||||
// Copyright (c) 2020 Refactoring UI Inc.
|
||||
// https://github.com/tailwindlabs/heroicons/blob/master/LICENSE
|
||||
export function UserIcon({
|
||||
style,
|
||||
size,
|
||||
strokeWidth = 1.5,
|
||||
}: {
|
||||
style?: StyleProp<ViewStyle>
|
||||
size?: string | number
|
||||
strokeWidth?: number
|
||||
}) {
|
||||
return (
|
||||
<Svg
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
width={size || 32}
|
||||
height={size || 32}
|
||||
strokeWidth={strokeWidth}
|
||||
stroke="currentColor"
|
||||
style={style}>
|
||||
<Path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</Svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function UserIconSolid({
|
||||
style,
|
||||
size,
|
||||
strokeWidth = 1.5,
|
||||
}: {
|
||||
style?: StyleProp<ViewStyle>
|
||||
size?: string | number
|
||||
strokeWidth?: number
|
||||
}) {
|
||||
return (
|
||||
<Svg
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
width={size || 32}
|
||||
height={size || 32}
|
||||
strokeWidth={strokeWidth}
|
||||
stroke="currentColor"
|
||||
style={style}>
|
||||
<Path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
fill="currentColor"
|
||||
d="M 15 9.75 C 15 12.059 12.5 13.503 10.5 12.348 C 9.572 11.812 9 10.822 9 9.75 C 9 7.441 11.5 5.997 13.5 7.152 C 14.428 7.688 15 8.678 15 9.75 Z"
|
||||
/>
|
||||
<Path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
fill="currentColor"
|
||||
d="M 17.982 18.725 C 16.565 16.849 14.35 15.748 12 15.75 C 9.65 15.748 7.435 16.849 6.018 18.725 M 17.981 18.725 C 16.335 20.193 14.206 21.003 12 21 C 9.794 21.003 7.664 20.193 6.018 18.725"
|
||||
/>
|
||||
<Path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M 17.981 18.725 C 23.158 14.12 21.409 5.639 14.833 3.458 C 8.257 1.277 1.786 7.033 3.185 13.818 C 3.576 15.716 4.57 17.437 6.018 18.725 M 17.981 18.725 C 16.335 20.193 14.206 21.003 12 21 C 9.794 21.003 7.664 20.193 6.018 18.725"
|
||||
/>
|
||||
</Svg>
|
||||
)
|
||||
}
|
||||
|
||||
// Copyright (c) 2020 Refactoring UI Inc.
|
||||
// https://github.com/tailwindlabs/heroicons/blob/master/LICENSE
|
||||
export function UserGroupIcon({
|
||||
@@ -444,208 +114,6 @@ export function UserGroupIcon({
|
||||
)
|
||||
}
|
||||
|
||||
export function RepostIcon({
|
||||
style,
|
||||
size = 24,
|
||||
strokeWidth = 1.5,
|
||||
}: {
|
||||
style?: StyleProp<TextStyle>
|
||||
size?: string | number
|
||||
strokeWidth: number
|
||||
}) {
|
||||
return (
|
||||
<Svg viewBox="0 0 24 24" width={size} height={size} style={style}>
|
||||
<Path
|
||||
stroke="currentColor"
|
||||
strokeWidth={strokeWidth}
|
||||
strokeLinejoin="round"
|
||||
fill="none"
|
||||
d="M 14.437 18.081 L 5.475 18.095 C 4.7 18.095 4.072 17.467 4.072 16.692 L 4.082 6.65 L 1.22 10.854 M 4.082 6.65 L 7.006 10.854 M 9.859 6.65 L 18.625 6.654 C 19.4 6.654 20.028 7.282 20.028 8.057 L 20.031 18.081 L 17.167 13.646 M 20.031 18.081 L 22.866 13.646"
|
||||
/>
|
||||
</Svg>
|
||||
)
|
||||
}
|
||||
|
||||
// Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc.
|
||||
export function HeartIcon({
|
||||
style,
|
||||
size = 24,
|
||||
strokeWidth = 1.5,
|
||||
}: {
|
||||
style?: StyleProp<TextStyle>
|
||||
size?: string | number
|
||||
strokeWidth: number
|
||||
}) {
|
||||
return (
|
||||
<Svg viewBox="0 0 24 24" width={size} height={size} style={style}>
|
||||
<Path
|
||||
strokeWidth={strokeWidth}
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
d="M 3.859 13.537 L 10.918 20.127 C 11.211 20.4 11.598 20.552 12 20.552 C 12.402 20.552 12.789 20.4 13.082 20.127 L 20.141 13.537 C 21.328 12.431 22 10.88 22 9.259 L 22 9.033 C 22 6.302 20.027 3.974 17.336 3.525 C 15.555 3.228 13.742 3.81 12.469 5.084 L 12 5.552 L 11.531 5.084 C 10.258 3.81 8.445 3.228 6.664 3.525 C 3.973 3.974 2 6.302 2 9.033 L 2 9.259 C 2 10.88 2.672 12.431 3.859 13.537 Z"
|
||||
/>
|
||||
</Svg>
|
||||
)
|
||||
}
|
||||
|
||||
// Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc.
|
||||
export function HeartIconSolid({
|
||||
style,
|
||||
size = 24,
|
||||
}: {
|
||||
style?: StyleProp<TextStyle>
|
||||
size?: string | number
|
||||
}) {
|
||||
return (
|
||||
<Svg viewBox="0 0 24 24" width={size} height={size} style={style}>
|
||||
<Path
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1}
|
||||
d="M 3.859 13.537 L 10.918 20.127 C 11.211 20.4 11.598 20.552 12 20.552 C 12.402 20.552 12.789 20.4 13.082 20.127 L 20.141 13.537 C 21.328 12.431 22 10.88 22 9.259 L 22 9.033 C 22 6.302 20.027 3.974 17.336 3.525 C 15.555 3.228 13.742 3.81 12.469 5.084 L 12 5.552 L 11.531 5.084 C 10.258 3.81 8.445 3.228 6.664 3.525 C 3.973 3.974 2 6.302 2 9.033 L 2 9.259 C 2 10.88 2.672 12.431 3.859 13.537 Z"
|
||||
/>
|
||||
</Svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function UpIcon({
|
||||
style,
|
||||
size,
|
||||
strokeWidth = 1.3,
|
||||
}: {
|
||||
style?: StyleProp<ViewStyle>
|
||||
size?: string | number
|
||||
strokeWidth: number
|
||||
}) {
|
||||
return (
|
||||
<Svg
|
||||
viewBox="0 0 14 14"
|
||||
width={size || 24}
|
||||
height={size || 24}
|
||||
style={style}>
|
||||
<Path
|
||||
strokeWidth={strokeWidth}
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M 7 3 L 2 8 L 4.5 8 L 4.5 11.5 L 9.5 11.5 L 9.5 8 L 12 8 L 7 3 Z"
|
||||
/>
|
||||
</Svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function UpIconSolid({
|
||||
style,
|
||||
size,
|
||||
}: {
|
||||
style?: StyleProp<ViewStyle>
|
||||
size?: string | number
|
||||
}) {
|
||||
return (
|
||||
<Svg
|
||||
viewBox="0 0 14 14"
|
||||
width={size || 24}
|
||||
height={size || 24}
|
||||
style={style}>
|
||||
<Path
|
||||
strokeWidth={1.3}
|
||||
stroke="currentColor"
|
||||
fill="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M 7 3 L 2 8 L 4.5 8 L 4.5 11.5 L 9.5 11.5 L 9.5 8 L 12 8 L 7 3 Z"
|
||||
/>
|
||||
</Svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function DownIcon({
|
||||
style,
|
||||
size,
|
||||
}: {
|
||||
style?: StyleProp<ViewStyle>
|
||||
size?: string | number
|
||||
}) {
|
||||
return (
|
||||
<Svg
|
||||
viewBox="0 0 14 14"
|
||||
width={size || 24}
|
||||
height={size || 24}
|
||||
style={style}>
|
||||
<Path
|
||||
strokeWidth={1.3}
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M 7 11.5 L 2 6.5 L 4.5 6.5 L 4.5 3 L 9.5 3 L 9.5 6.5 L 12 6.5 L 7 11.5 Z"
|
||||
/>
|
||||
</Svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function DownIconSolid({
|
||||
style,
|
||||
size,
|
||||
}: {
|
||||
style?: StyleProp<ViewStyle>
|
||||
size?: string | number
|
||||
}) {
|
||||
return (
|
||||
<Svg
|
||||
viewBox="0 0 14 14"
|
||||
width={size || 24}
|
||||
height={size || 24}
|
||||
style={style}>
|
||||
<Path
|
||||
strokeWidth={1.3}
|
||||
stroke="currentColor"
|
||||
fill="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M 7 11.5 L 2 6.5 L 4.5 6.5 L 4.5 3 L 9.5 3 L 9.5 6.5 L 12 6.5 L 7 11.5 Z"
|
||||
/>
|
||||
</Svg>
|
||||
)
|
||||
}
|
||||
|
||||
// Copyright (c) 2020 Refactoring UI Inc.
|
||||
// https://github.com/tailwindlabs/heroicons/blob/master/LICENSE
|
||||
export function CommentBottomArrow({
|
||||
style,
|
||||
size,
|
||||
strokeWidth = 1.3,
|
||||
}: {
|
||||
style?: StyleProp<TextStyle>
|
||||
size?: string | number
|
||||
strokeWidth?: number
|
||||
}) {
|
||||
let color = 'currentColor'
|
||||
if (
|
||||
style &&
|
||||
typeof style === 'object' &&
|
||||
'color' in style &&
|
||||
typeof style.color === 'string'
|
||||
) {
|
||||
color = style.color
|
||||
}
|
||||
return (
|
||||
<Svg
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={strokeWidth || 2.5}
|
||||
stroke={color}
|
||||
width={size || 24}
|
||||
height={size || 24}
|
||||
style={style}>
|
||||
<Path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M2.25 12.76c0 1.6 1.123 2.994 2.707 3.227 1.068.157 2.148.279 3.238.364.466.037.893.281 1.153.671L12 21l2.652-3.978c.26-.39.687-.634 1.153-.67 1.09-.086 2.17-.208 3.238-.365 1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z"
|
||||
/>
|
||||
</Svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function SquareIcon({
|
||||
style,
|
||||
size,
|
||||
@@ -715,33 +183,6 @@ export function RectTallIcon({
|
||||
)
|
||||
}
|
||||
|
||||
export function ComposeIcon({
|
||||
style,
|
||||
size,
|
||||
strokeWidth = 1.5,
|
||||
}: {
|
||||
style?: StyleProp<TextStyle>
|
||||
size?: string | number
|
||||
strokeWidth?: number
|
||||
}) {
|
||||
return (
|
||||
<Svg
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={strokeWidth}
|
||||
stroke="currentColor"
|
||||
width={size || 24}
|
||||
height={size || 24}
|
||||
style={style}>
|
||||
<Path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"
|
||||
/>
|
||||
</Svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function ComposeIcon2({
|
||||
style,
|
||||
size,
|
||||
@@ -783,54 +224,6 @@ export function ComposeIcon2({
|
||||
)
|
||||
}
|
||||
|
||||
export function SquarePlusIcon({
|
||||
style,
|
||||
size,
|
||||
strokeWidth = 1.5,
|
||||
}: {
|
||||
style?: StyleProp<TextStyle>
|
||||
size?: string | number
|
||||
strokeWidth?: number
|
||||
}) {
|
||||
return (
|
||||
<Svg
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={strokeWidth}
|
||||
stroke="currentColor"
|
||||
width={size || 24}
|
||||
height={size || 24}
|
||||
style={style}>
|
||||
<Line
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
x1="12"
|
||||
y1="5.5"
|
||||
x2="12"
|
||||
y2="18.5"
|
||||
strokeWidth={strokeWidth * 1.5}
|
||||
/>
|
||||
<Line
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
x1="5.5"
|
||||
y1="12"
|
||||
x2="18.5"
|
||||
y2="12"
|
||||
strokeWidth={strokeWidth * 1.5}
|
||||
/>
|
||||
<Rect
|
||||
strokeWidth={strokeWidth}
|
||||
x="4"
|
||||
y="4"
|
||||
width="16"
|
||||
height="16"
|
||||
rx="4"
|
||||
ry="4"
|
||||
/>
|
||||
</Svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function InfoCircleIcon({
|
||||
style,
|
||||
size,
|
||||
@@ -947,30 +340,3 @@ export function ShieldExclamation({
|
||||
</Svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function ListIcon({
|
||||
style,
|
||||
size,
|
||||
strokeWidth = 1.5,
|
||||
}: {
|
||||
style?: StyleProp<TextStyle>
|
||||
size?: string | number
|
||||
strokeWidth?: number
|
||||
}) {
|
||||
return (
|
||||
<Svg
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={strokeWidth || 1.5}
|
||||
stroke="currentColor"
|
||||
width={size}
|
||||
height={size}
|
||||
style={style}>
|
||||
<Path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M8.25 6.75h12M8.25 12h12m-12 5.25h12M3.75 6.75h.007v.008H3.75V6.75zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zM3.75 12h.007v.008H3.75V12zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm-.375 5.25h.007v.008H3.75v-.008zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z"
|
||||
/>
|
||||
</Svg>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,8 +3,9 @@ import {
|
||||
launchImageLibraryAsync,
|
||||
MediaTypeOptions,
|
||||
} from 'expo-image-picker'
|
||||
import {getDataUriSize} from './util'
|
||||
|
||||
import * as Toast from 'view/com/util/Toast'
|
||||
import {getDataUriSize} from './util'
|
||||
|
||||
export async function openPicker(opts?: ImagePickerOptions) {
|
||||
const response = await launchImageLibraryAsync({
|
||||
@@ -12,6 +13,7 @@ export async function openPicker(opts?: ImagePickerOptions) {
|
||||
mediaTypes: MediaTypeOptions.Images,
|
||||
quality: 1,
|
||||
...opts,
|
||||
legacy: true,
|
||||
})
|
||||
|
||||
if (response.assets && response.assets.length > 4) {
|
||||
|
||||
@@ -6,15 +6,15 @@ import {
|
||||
} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {getDefinition, getLabelStrings} from './useLabelInfo'
|
||||
import {useLabelDefinitions} from '#/state/preferences'
|
||||
import {useGlobalLabelStrings} from './useGlobalLabelStrings'
|
||||
|
||||
import {Props as SVGIconProps} from '#/components/icons/common'
|
||||
import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning'
|
||||
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
|
||||
import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash'
|
||||
import {useLabelDefinitions} from '#/state/preferences'
|
||||
import {CircleBanSign_Stroke2_Corner0_Rounded as CircleBanSign} from '#/components/icons/CircleBanSign'
|
||||
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
|
||||
import {Props as SVGIconProps} from '#/components/icons/common'
|
||||
import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash'
|
||||
import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning'
|
||||
import {useGlobalLabelStrings} from './useGlobalLabelStrings'
|
||||
import {getDefinition, getLabelStrings} from './useLabelInfo'
|
||||
|
||||
export interface ModerationCauseDescription {
|
||||
icon: React.ComponentType<SVGIconProps>
|
||||
@@ -22,6 +22,8 @@ export interface ModerationCauseDescription {
|
||||
description: string
|
||||
source?: string
|
||||
sourceType?: ModerationCauseSource['type']
|
||||
sourceAvi?: string
|
||||
sourceDid?: string
|
||||
}
|
||||
|
||||
export function useModerationCauseDescription(
|
||||
@@ -138,6 +140,8 @@ export function useModerationCauseDescription(
|
||||
description: strings.description,
|
||||
source,
|
||||
sourceType: cause.source.type,
|
||||
sourceAvi: labeler?.creator.avatar,
|
||||
sourceDid: cause.label.src,
|
||||
}
|
||||
}
|
||||
// should never happen
|
||||
|
||||
@@ -7,6 +7,7 @@ import {logger} from '#/logger'
|
||||
import {SessionAccount, useAgent, useSession} from '#/state/session'
|
||||
import {logEvent, useGate} from 'lib/statsig/statsig'
|
||||
import {devicePlatform, isNative} from 'platform/detection'
|
||||
import BackgroundNotificationHandler from '../../../modules/expo-background-notification-handler'
|
||||
|
||||
const SERVICE_DID = (serviceUrl?: string) =>
|
||||
serviceUrl?.includes('staging')
|
||||
@@ -71,61 +72,57 @@ export function useNotificationsRegistration() {
|
||||
|
||||
export function useRequestNotificationsPermission() {
|
||||
const gate = useGate()
|
||||
const {currentAccount} = useSession()
|
||||
|
||||
return React.useCallback(
|
||||
async (context: 'StartOnboarding' | 'AfterOnboarding' | 'Login') => {
|
||||
const permissions = await Notifications.getPermissionsAsync()
|
||||
return async (context: 'StartOnboarding' | 'AfterOnboarding' | 'Login') => {
|
||||
const permissions = await Notifications.getPermissionsAsync()
|
||||
|
||||
if (
|
||||
!currentAccount ||
|
||||
!isNative ||
|
||||
permissions?.status === 'granted' ||
|
||||
(permissions?.status === 'denied' && !permissions?.canAskAgain)
|
||||
) {
|
||||
return
|
||||
}
|
||||
if (
|
||||
context === 'StartOnboarding' &&
|
||||
gate('request_notifications_permission_after_onboarding')
|
||||
) {
|
||||
return
|
||||
}
|
||||
if (
|
||||
context === 'AfterOnboarding' &&
|
||||
!gate('request_notifications_permission_after_onboarding')
|
||||
) {
|
||||
return
|
||||
}
|
||||
if (
|
||||
!isNative ||
|
||||
permissions?.status === 'granted' ||
|
||||
permissions?.status === 'denied'
|
||||
) {
|
||||
return
|
||||
}
|
||||
if (
|
||||
context === 'StartOnboarding' &&
|
||||
gate('request_notifications_permission_after_onboarding_v2')
|
||||
) {
|
||||
return
|
||||
}
|
||||
if (
|
||||
context === 'AfterOnboarding' &&
|
||||
!gate('request_notifications_permission_after_onboarding_v2')
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const res = await Notifications.requestPermissionsAsync()
|
||||
logEvent('notifications:request', {
|
||||
context: context,
|
||||
status: res.status,
|
||||
})
|
||||
const res = await Notifications.requestPermissionsAsync()
|
||||
logEvent('notifications:request', {
|
||||
context: context,
|
||||
status: res.status,
|
||||
})
|
||||
|
||||
if (res.granted) {
|
||||
// This will fire a pushTokenEvent, which will handle registration of the token
|
||||
getPushToken(true)
|
||||
}
|
||||
},
|
||||
[gate, currentAccount],
|
||||
)
|
||||
if (res.granted) {
|
||||
// This will fire a pushTokenEvent, which will handle registration of the token
|
||||
getPushToken(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function decrementBadgeCount(by: number | 'reset' = 1) {
|
||||
export async function decrementBadgeCount(by: number) {
|
||||
if (!isNative) return
|
||||
|
||||
const currCount = await getBadgeCountAsync()
|
||||
|
||||
if (by === 'reset') {
|
||||
await setBadgeCountAsync(0)
|
||||
return
|
||||
let count = await getBadgeCountAsync()
|
||||
count -= by
|
||||
if (count < 0) {
|
||||
count = 0
|
||||
}
|
||||
|
||||
let newCount = currCount - by
|
||||
if (newCount < 0) {
|
||||
newCount = 0
|
||||
}
|
||||
await setBadgeCountAsync(newCount)
|
||||
await BackgroundNotificationHandler.setBadgeCountAsync(count)
|
||||
await setBadgeCountAsync(count)
|
||||
}
|
||||
|
||||
export async function resetBadgeCount() {
|
||||
await BackgroundNotificationHandler.setBadgeCountAsync(0)
|
||||
await setBadgeCountAsync(0)
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ export type CommonNavigatorParams = {
|
||||
AccessibilitySettings: undefined
|
||||
Search: {q?: string}
|
||||
Hashtag: {tag: string; author?: string}
|
||||
MessagesConversation: {conversation: string}
|
||||
MessagesConversation: {conversation: string; embed?: string}
|
||||
MessagesSettings: undefined
|
||||
}
|
||||
|
||||
|
||||
@@ -13,17 +13,17 @@ export type LogEvents = {
|
||||
withPassword: boolean
|
||||
}
|
||||
'account:loggedOut': {
|
||||
logContext: 'SwitchAccount' | 'Settings' | 'Deactivated'
|
||||
logContext: 'SwitchAccount' | 'Settings' | 'SignupQueued' | 'Deactivated'
|
||||
}
|
||||
'notifications:openApp': {}
|
||||
'notifications:request': {
|
||||
context: 'StartOnboarding' | 'AfterOnboarding' | 'Login'
|
||||
status: 'granted' | 'denied' | 'undetermined'
|
||||
}
|
||||
'state:background': {
|
||||
'state:background:sampled': {
|
||||
secondsActive: number
|
||||
}
|
||||
'state:foreground': {}
|
||||
'state:foreground:sampled': {}
|
||||
'router:navigate:sampled': {}
|
||||
|
||||
// Screen events
|
||||
@@ -57,18 +57,18 @@ export type LogEvents = {
|
||||
'onboarding:finished:avatarResult': {
|
||||
avatarResult: 'default' | 'created' | 'uploaded'
|
||||
}
|
||||
'home:feedDisplayed': {
|
||||
'home:feedDisplayed:sampled': {
|
||||
feedUrl: string
|
||||
feedType: string
|
||||
index: number
|
||||
reason: 'focus' | 'tabbar-click' | 'pager-swipe' | 'desktop-sidebar-click'
|
||||
}
|
||||
'feed:endReached': {
|
||||
'feed:endReached:sampled': {
|
||||
feedUrl: string
|
||||
feedType: string
|
||||
itemCount: number
|
||||
}
|
||||
'feed:refresh': {
|
||||
'feed:refresh:sampled': {
|
||||
feedUrl: string
|
||||
feedType: string
|
||||
reason: 'pull-to-refresh' | 'soft-reset' | 'load-latest'
|
||||
@@ -115,6 +115,7 @@ export type LogEvents = {
|
||||
| 'ProfileHeaderSuggestedFollows'
|
||||
| 'ProfileMenu'
|
||||
| 'ProfileHoverCard'
|
||||
| 'AvatarButton'
|
||||
}
|
||||
'profile:unfollow': {
|
||||
logContext:
|
||||
@@ -126,12 +127,17 @@ export type LogEvents = {
|
||||
| 'ProfileMenu'
|
||||
| 'ProfileHoverCard'
|
||||
| 'Chat'
|
||||
| 'AvatarButton'
|
||||
}
|
||||
'chat:create': {
|
||||
logContext: 'ProfileHeader' | 'NewChatDialog'
|
||||
logContext: 'ProfileHeader' | 'NewChatDialog' | 'SendViaChatDialog'
|
||||
}
|
||||
'chat:open': {
|
||||
logContext: 'ProfileHeader' | 'NewChatDialog' | 'ChatsList'
|
||||
logContext:
|
||||
| 'ProfileHeader'
|
||||
| 'NewChatDialog'
|
||||
| 'ChatsList'
|
||||
| 'SendViaChatDialog'
|
||||
}
|
||||
|
||||
'test:all:always': {}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export type Gate =
|
||||
// Keep this alphabetic please.
|
||||
| 'request_notifications_permission_after_onboarding'
|
||||
| 'request_notifications_permission_after_onboarding_v2'
|
||||
| 'show_avi_follow_button'
|
||||
| 'show_follow_back_label_v2'
|
||||
| 'show_notification_badge_mobile_web'
|
||||
|
||||
@@ -87,7 +87,14 @@ export function toClout(n: number | null | undefined): number | undefined {
|
||||
}
|
||||
}
|
||||
|
||||
const DOWNSAMPLED_EVENTS = new Set(['router:navigate:sampled'])
|
||||
const DOWNSAMPLED_EVENTS: Set<keyof LogEvents> = new Set([
|
||||
'router:navigate:sampled',
|
||||
'state:background:sampled',
|
||||
'state:foreground:sampled',
|
||||
'home:feedDisplayed:sampled',
|
||||
'feed:endReached:sampled',
|
||||
'feed:refresh:sampled',
|
||||
])
|
||||
const isDownsampledSession = Math.random() < 0.9 // 90% likely
|
||||
|
||||
export function logEvent<E extends keyof LogEvents>(
|
||||
@@ -95,6 +102,16 @@ export function logEvent<E extends keyof LogEvents>(
|
||||
rawMetadata: LogEvents[E] & FlatJSONRecord,
|
||||
) {
|
||||
try {
|
||||
if (
|
||||
process.env.NODE_ENV === 'development' &&
|
||||
eventName.endsWith(':sampled') &&
|
||||
!DOWNSAMPLED_EVENTS.has(eventName)
|
||||
) {
|
||||
logger.error(
|
||||
'Did you forget to add ' + eventName + ' to DOWNSAMPLED_EVENTS?',
|
||||
)
|
||||
}
|
||||
|
||||
if (isDownsampledSession && DOWNSAMPLED_EVENTS.has(eventName)) {
|
||||
return
|
||||
}
|
||||
@@ -199,14 +216,14 @@ AppState.addEventListener('change', (state: AppStateStatus) => {
|
||||
lastState = state
|
||||
if (state === 'active') {
|
||||
lastActive = performance.now()
|
||||
logEvent('state:foreground', {})
|
||||
logEvent('state:foreground:sampled', {})
|
||||
} else {
|
||||
let secondsActive = 0
|
||||
if (lastActive != null) {
|
||||
secondsActive = Math.round((performance.now() - lastActive) / 1e3)
|
||||
}
|
||||
lastActive = null
|
||||
logEvent('state:background', {
|
||||
logEvent('state:background:sampled', {
|
||||
secondsActive,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ export const embedPlayerSources = [
|
||||
'vimeo',
|
||||
'giphy',
|
||||
'tenor',
|
||||
'flickr',
|
||||
] as const
|
||||
|
||||
export type EmbedPlayerSource = (typeof embedPlayerSources)[number]
|
||||
@@ -42,6 +43,7 @@ export type EmbedPlayerType =
|
||||
| 'vimeo_video'
|
||||
| 'giphy_gif'
|
||||
| 'tenor_gif'
|
||||
| 'flickr_album'
|
||||
|
||||
export const externalEmbedLabels: Record<EmbedPlayerSource, string> = {
|
||||
youtube: 'YouTube',
|
||||
@@ -53,6 +55,7 @@ export const externalEmbedLabels: Record<EmbedPlayerSource, string> = {
|
||||
spotify: 'Spotify',
|
||||
appleMusic: 'Apple Music',
|
||||
soundcloud: 'SoundCloud',
|
||||
flickr: 'Flickr',
|
||||
}
|
||||
|
||||
export interface EmbedPlayerParams {
|
||||
@@ -375,6 +378,79 @@ export function parseEmbedPlayerFromUrl(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// this is a standard flickr path! we can use the embedder for albums and groups, so validate the path
|
||||
if (urlp.hostname === 'www.flickr.com' || urlp.hostname === 'flickr.com') {
|
||||
let i = urlp.pathname.length - 1
|
||||
while (i > 0 && urlp.pathname.charAt(i) === '/') {
|
||||
--i
|
||||
}
|
||||
|
||||
const path_components = urlp.pathname.slice(1, i + 1).split('/')
|
||||
if (path_components.length === 4) {
|
||||
// discard username - it's not relevant
|
||||
const [photos, _, albums, id] = path_components
|
||||
if (photos === 'photos' && albums === 'albums') {
|
||||
// this at least has the shape of a valid photo-album URL!
|
||||
return {
|
||||
type: 'flickr_album',
|
||||
source: 'flickr',
|
||||
playerUri: `https://embedr.flickr.com/photosets/${id}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (path_components.length === 3) {
|
||||
const [groups, id, pool] = path_components
|
||||
if (groups === 'groups' && pool === 'pool') {
|
||||
return {
|
||||
type: 'flickr_album',
|
||||
source: 'flickr',
|
||||
playerUri: `https://embedr.flickr.com/groups/${id}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
// not an album or a group pool, don't know what to do with this!
|
||||
return undefined
|
||||
}
|
||||
|
||||
// link shortened flickr path
|
||||
if (urlp.hostname === 'flic.kr') {
|
||||
const b58alph = '123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ'
|
||||
let [_, type, idBase58Enc] = urlp.pathname.split('/')
|
||||
let id = 0n
|
||||
for (const char of idBase58Enc) {
|
||||
const nextIdx = b58alph.indexOf(char)
|
||||
if (nextIdx >= 0) {
|
||||
id = id * 58n + BigInt(nextIdx)
|
||||
} else {
|
||||
// not b58 encoded, ergo not a valid link to embed
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'go':
|
||||
const formattedGroupId = `${id}`
|
||||
return {
|
||||
type: 'flickr_album',
|
||||
source: 'flickr',
|
||||
playerUri: `https://embedr.flickr.com/groups/${formattedGroupId.slice(
|
||||
0,
|
||||
-2,
|
||||
)}@N${formattedGroupId.slice(-2)}`,
|
||||
}
|
||||
case 's':
|
||||
return {
|
||||
type: 'flickr_album',
|
||||
source: 'flickr',
|
||||
playerUri: `https://embedr.flickr.com/photosets/${id}`,
|
||||
}
|
||||
default:
|
||||
// we don't know what this is so we can't embed it
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getPlayerAspect({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {RichText, UnicodeString} from '@atproto/api'
|
||||
import {AppBskyRichtextFacet, RichText, UnicodeString} from '@atproto/api'
|
||||
|
||||
import {toShortUrl} from './url-helpers'
|
||||
|
||||
@@ -10,9 +10,7 @@ export function shortenLinks(rt: RichText): RichText {
|
||||
// enumerate the link facets
|
||||
if (rt.facets) {
|
||||
for (const facet of rt.facets) {
|
||||
const isLink = !!facet.features.find(
|
||||
f => f.$type === 'app.bsky.richtext.facet#link',
|
||||
)
|
||||
const isLink = !!facet.features.find(AppBskyRichtextFacet.isLink)
|
||||
if (!isLink) {
|
||||
continue
|
||||
}
|
||||
@@ -33,3 +31,21 @@ export function shortenLinks(rt: RichText): RichText {
|
||||
}
|
||||
return rt
|
||||
}
|
||||
|
||||
// filter out any mention facets that didn't map to a user
|
||||
export function stripInvalidMentions(rt: RichText): RichText {
|
||||
if (!rt.facets?.length) {
|
||||
return rt
|
||||
}
|
||||
rt = rt.clone()
|
||||
if (rt.facets) {
|
||||
rt.facets = rt.facets?.filter(facet => {
|
||||
const mention = facet.features.find(AppBskyRichtextFacet.isMention)
|
||||
if (mention && !mention.did) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
return rt
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import psl from 'psl'
|
||||
import TLDs from 'tlds'
|
||||
|
||||
import {BSKY_SERVICE} from 'lib/constants'
|
||||
import {isInvalidHandle} from 'lib/strings/handles'
|
||||
|
||||
export const BSKY_APP_HOST = 'https://bsky.app'
|
||||
const BSKY_TRUSTED_HOSTS = [
|
||||
@@ -83,6 +84,10 @@ export function toShareUrl(url: string): string {
|
||||
return url
|
||||
}
|
||||
|
||||
export function toBskyAppUrl(url: string): string {
|
||||
return new URL(url, BSKY_APP_HOST).toString()
|
||||
}
|
||||
|
||||
export function isBskyAppUrl(url: string): boolean {
|
||||
return url.startsWith('https://bsky.app/')
|
||||
}
|
||||
@@ -183,6 +188,22 @@ export function feedUriToHref(url: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
export function postUriToRelativePath(
|
||||
uri: string,
|
||||
options?: {handle?: string},
|
||||
): string | undefined {
|
||||
try {
|
||||
const {hostname, rkey} = new AtUri(uri)
|
||||
const handleOrDid =
|
||||
options?.handle && !isInvalidHandle(options.handle)
|
||||
? options.handle
|
||||
: hostname
|
||||
return `/profile/${handleOrDid}/post/${rkey}`
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the label in the post text matches the host of the link facet.
|
||||
*
|
||||
|
||||
@@ -1,19 +1,31 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {msg, plural, Trans} from '@lingui/macro'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useFocusEffect} from '@react-navigation/native'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
|
||||
import {logger} from '#/logger'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {isSessionDeactivated, useAgent, useSessionApi} from '#/state/session'
|
||||
import {useOnboardingDispatch} from '#/state/shell'
|
||||
import {
|
||||
type SessionAccount,
|
||||
useAgent,
|
||||
useSession,
|
||||
useSessionApi,
|
||||
} from '#/state/session'
|
||||
import {useSetMinimalShellMode} from '#/state/shell'
|
||||
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
||||
import {ScrollView} from '#/view/com/util/Views'
|
||||
import {Logo} from '#/view/icons/Logo'
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {AccountList} from '#/components/AccountList'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import {Divider} from '#/components/Divider'
|
||||
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {P, Text} from '#/components/Typography'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
const COL_WIDTH = 400
|
||||
|
||||
@@ -21,199 +33,199 @@ export function Deactivated() {
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
const insets = useSafeAreaInsets()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const onboardingDispatch = useOnboardingDispatch()
|
||||
const {currentAccount, accounts} = useSession()
|
||||
const {onPressSwitchAccount, pendingDid} = useAccountSwitcher()
|
||||
const {setShowLoggedOut} = useLoggedOutViewControls()
|
||||
const hasOtherAccounts = accounts.length > 1
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const {logout} = useSessionApi()
|
||||
const agent = useAgent()
|
||||
const [pending, setPending] = React.useState(false)
|
||||
const [error, setError] = React.useState<string | undefined>()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const [isProcessing, setProcessing] = React.useState(false)
|
||||
const [estimatedTime, setEstimatedTime] = React.useState<string | undefined>(
|
||||
undefined,
|
||||
)
|
||||
const [placeInQueue, setPlaceInQueue] = React.useState<number | undefined>(
|
||||
undefined,
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
setMinimalShellMode(true)
|
||||
}, [setMinimalShellMode]),
|
||||
)
|
||||
|
||||
const checkStatus = React.useCallback(async () => {
|
||||
setProcessing(true)
|
||||
try {
|
||||
const res = await agent.com.atproto.temp.checkSignupQueue()
|
||||
if (res.data.activated) {
|
||||
// ready to go, exchange the access token for a usable one and kick off onboarding
|
||||
await agent.refreshSession()
|
||||
if (!isSessionDeactivated(agent.session?.accessJwt)) {
|
||||
onboardingDispatch({type: 'start'})
|
||||
}
|
||||
} else {
|
||||
// not ready, update UI
|
||||
setEstimatedTime(msToString(res.data.estimatedTimeMs))
|
||||
if (typeof res.data.placeInQueue !== 'undefined') {
|
||||
setPlaceInQueue(Math.max(res.data.placeInQueue, 1))
|
||||
}
|
||||
const onSelectAccount = React.useCallback(
|
||||
(account: SessionAccount) => {
|
||||
if (account.did !== currentAccount?.did) {
|
||||
onPressSwitchAccount(account, 'SwitchAccount')
|
||||
}
|
||||
} catch (e: any) {
|
||||
logger.error('Failed to check signup queue', {err: e.toString()})
|
||||
} finally {
|
||||
setProcessing(false)
|
||||
}
|
||||
}, [
|
||||
setProcessing,
|
||||
setEstimatedTime,
|
||||
setPlaceInQueue,
|
||||
onboardingDispatch,
|
||||
agent,
|
||||
])
|
||||
|
||||
React.useEffect(() => {
|
||||
checkStatus()
|
||||
const interval = setInterval(checkStatus, 60e3)
|
||||
return () => clearInterval(interval)
|
||||
}, [checkStatus])
|
||||
|
||||
const checkBtn = (
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
size="large"
|
||||
label={_(msg`Check my status`)}
|
||||
onPress={checkStatus}
|
||||
disabled={isProcessing}>
|
||||
<ButtonText>
|
||||
<Trans>Check my status</Trans>
|
||||
</ButtonText>
|
||||
{isProcessing && <ButtonIcon icon={Loader} />}
|
||||
</Button>
|
||||
},
|
||||
[currentAccount, onPressSwitchAccount],
|
||||
)
|
||||
|
||||
const onPressAddAccount = React.useCallback(() => {
|
||||
setShowLoggedOut(true)
|
||||
}, [setShowLoggedOut])
|
||||
|
||||
const onPressLogout = React.useCallback(() => {
|
||||
if (isWeb) {
|
||||
// We're switching accounts, which remounts the entire app.
|
||||
// On mobile, this gets us Home, but on the web we also need reset the URL.
|
||||
// We can't change the URL via a navigate() call because the navigator
|
||||
// itself is about to unmount, and it calls pushState() too late.
|
||||
// So we change the URL ourselves. The navigator will pick it up on remount.
|
||||
history.pushState(null, '', '/')
|
||||
}
|
||||
logout('Deactivated')
|
||||
}, [logout])
|
||||
|
||||
const handleActivate = React.useCallback(async () => {
|
||||
try {
|
||||
setPending(true)
|
||||
await agent.com.atproto.server.activateAccount()
|
||||
await queryClient.resetQueries()
|
||||
await agent.resumeSession(agent.session!)
|
||||
} catch (e: any) {
|
||||
switch (e.message) {
|
||||
case 'Bad token scope':
|
||||
setError(
|
||||
_(
|
||||
msg`You're logged in with an App Password. Please log in with your main password to continue deactivating your account.`,
|
||||
),
|
||||
)
|
||||
break
|
||||
default:
|
||||
setError(_(msg`Something went wrong, please try again`))
|
||||
break
|
||||
}
|
||||
|
||||
logger.error(e, {
|
||||
context: 'Failed to activate account',
|
||||
})
|
||||
} finally {
|
||||
setPending(false)
|
||||
}
|
||||
}, [_, agent, setPending, setError, queryClient])
|
||||
|
||||
return (
|
||||
<View
|
||||
aria-modal
|
||||
role="dialog"
|
||||
aria-role="dialog"
|
||||
aria-label={_(msg`You're in line`)}
|
||||
accessibilityLabel={_(msg`You're in line`)}
|
||||
accessibilityHint=""
|
||||
style={[a.absolute, a.inset_0, a.flex_1, t.atoms.bg]}>
|
||||
<View style={[a.h_full_vh, a.flex_1, t.atoms.bg]}>
|
||||
<ScrollView
|
||||
style={[a.h_full, a.w_full]}
|
||||
contentContainerStyle={{borderWidth: 0}}>
|
||||
<View
|
||||
style={[a.flex_row, a.justify_center, gtMobile ? a.pt_4xl : a.px_xl]}>
|
||||
<View style={[a.flex_1, {maxWidth: COL_WIDTH}]}>
|
||||
<View
|
||||
style={[a.w_full, a.justify_center, a.align_center, a.my_4xl]}>
|
||||
<Logo width={120} />
|
||||
</View>
|
||||
|
||||
<Text style={[a.text_4xl, a.font_bold, a.pb_sm]}>
|
||||
<Trans>You're in line</Trans>
|
||||
</Text>
|
||||
<P style={[t.atoms.text_contrast_medium]}>
|
||||
<Trans>
|
||||
There's been a rush of new users to Bluesky! We'll activate your
|
||||
account as soon as we can.
|
||||
</Trans>
|
||||
</P>
|
||||
|
||||
<View
|
||||
style={[
|
||||
a.rounded_sm,
|
||||
a.px_2xl,
|
||||
a.py_4xl,
|
||||
a.mt_2xl,
|
||||
t.atoms.bg_contrast_50,
|
||||
]}>
|
||||
{typeof placeInQueue === 'number' && (
|
||||
<Text
|
||||
style={[a.text_5xl, a.text_center, a.font_bold, a.mb_2xl]}>
|
||||
{placeInQueue}
|
||||
</Text>
|
||||
)}
|
||||
<P style={[a.text_center]}>
|
||||
{typeof placeInQueue === 'number' ? (
|
||||
<Trans>left to go.</Trans>
|
||||
) : (
|
||||
<Trans>You are in line.</Trans>
|
||||
)}{' '}
|
||||
{estimatedTime ? (
|
||||
<Trans>
|
||||
We estimate {estimatedTime} until your account is ready.
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
We will let you know when your account is ready.
|
||||
</Trans>
|
||||
)}
|
||||
</P>
|
||||
</View>
|
||||
|
||||
{isWeb && gtMobile && (
|
||||
<View style={[a.w_full, a.flex_row, a.justify_between, a.pt_5xl]}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="large"
|
||||
label={_(msg`Log out`)}
|
||||
onPress={() => logout('Deactivated')}>
|
||||
<ButtonText style={[{color: t.palette.primary_500}]}>
|
||||
<Trans>Log out</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
{checkBtn}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={{height: 200}} />
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
{(!isWeb || !gtMobile) && (
|
||||
<View
|
||||
style={[
|
||||
a.align_center,
|
||||
gtMobile ? a.px_5xl : a.px_xl,
|
||||
a.px_2xl,
|
||||
{
|
||||
paddingBottom: Math.max(insets.bottom, a.pb_5xl.paddingBottom),
|
||||
paddingTop: isWeb ? 64 : insets.top,
|
||||
paddingBottom: isWeb ? 64 : insets.bottom,
|
||||
},
|
||||
]}>
|
||||
<View style={[a.w_full, a.gap_sm, {maxWidth: COL_WIDTH}]}>
|
||||
{checkBtn}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="large"
|
||||
label={_(msg`Log out`)}
|
||||
onPress={() => logout('Deactivated')}>
|
||||
<ButtonText style={[{color: t.palette.primary_500}]}>
|
||||
<Trans>Log out</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
<View style={[a.flex_row, a.justify_center]}>
|
||||
<View style={[a.w_full, {maxWidth: COL_WIDTH}]}>
|
||||
<View
|
||||
style={[a.w_full, a.justify_center, a.align_center, a.pb_5xl]}>
|
||||
<Logo width={40} />
|
||||
</View>
|
||||
|
||||
<View style={[a.gap_xs, a.pb_3xl]}>
|
||||
<Text style={[a.text_xl, a.font_bold, a.leading_snug]}>
|
||||
<Trans>Welcome back!</Trans>
|
||||
</Text>
|
||||
<Text style={[a.text_sm, a.leading_snug]}>
|
||||
<Trans>
|
||||
You previously deactivated @{currentAccount?.handle}.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Text style={[a.text_sm, a.leading_snug, a.pb_md]}>
|
||||
<Trans>
|
||||
You can reactivate your account to continue logging in. Your
|
||||
profile and posts will be visible to other users.
|
||||
</Trans>
|
||||
</Text>
|
||||
|
||||
<View style={[a.gap_sm]}>
|
||||
<Button
|
||||
label={_(msg`Reactivate your account`)}
|
||||
size="medium"
|
||||
variant="solid"
|
||||
color="primary"
|
||||
onPress={handleActivate}>
|
||||
<ButtonText>
|
||||
<Trans>Yes, reactivate my account</Trans>
|
||||
</ButtonText>
|
||||
{pending && <ButtonIcon icon={Loader} position="right" />}
|
||||
</Button>
|
||||
<Button
|
||||
label={_(msg`Cancel reactivation and log out`)}
|
||||
size="medium"
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
onPress={onPressLogout}>
|
||||
<ButtonText>
|
||||
<Trans>Cancel</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
{error && (
|
||||
<View
|
||||
style={[
|
||||
a.flex_row,
|
||||
a.gap_sm,
|
||||
a.mt_md,
|
||||
a.p_md,
|
||||
a.rounded_sm,
|
||||
t.atoms.bg_contrast_25,
|
||||
]}>
|
||||
<CircleInfo size="md" fill={t.palette.negative_400} />
|
||||
<Text style={[a.flex_1, a.leading_snug]}>{error}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={[a.pb_3xl]}>
|
||||
<Divider />
|
||||
</View>
|
||||
|
||||
{hasOtherAccounts ? (
|
||||
<>
|
||||
<Text
|
||||
style={[
|
||||
t.atoms.text_contrast_medium,
|
||||
a.pb_md,
|
||||
a.leading_snug,
|
||||
]}>
|
||||
<Trans>Or, log into one of your other accounts.</Trans>
|
||||
</Text>
|
||||
<AccountList
|
||||
onSelectAccount={onSelectAccount}
|
||||
onSelectOther={onPressAddAccount}
|
||||
otherLabel={_(msg`Add account`)}
|
||||
pendingDid={pendingDid}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Text
|
||||
style={[
|
||||
t.atoms.text_contrast_medium,
|
||||
a.pb_md,
|
||||
a.leading_snug,
|
||||
]}>
|
||||
<Trans>Or, continue with another account.</Trans>
|
||||
</Text>
|
||||
<Button
|
||||
label={_(msg`Log in or sign up`)}
|
||||
size="medium"
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
onPress={() => setShowLoggedOut(true)}>
|
||||
<ButtonText>
|
||||
<Trans>Log in or sign up</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function msToString(ms: number | undefined): string | undefined {
|
||||
if (ms && ms > 0) {
|
||||
const estimatedTimeMins = Math.ceil(ms / 60e3)
|
||||
if (estimatedTimeMins > 59) {
|
||||
const estimatedTimeHrs = Math.round(estimatedTimeMins / 60)
|
||||
if (estimatedTimeHrs > 6) {
|
||||
// dont even bother
|
||||
return undefined
|
||||
}
|
||||
// hours
|
||||
return `${estimatedTimeHrs} ${plural(estimatedTimeHrs, {
|
||||
one: 'hour',
|
||||
other: 'hours',
|
||||
})}`
|
||||
}
|
||||
// minutes
|
||||
return `${estimatedTimeMins} ${plural(estimatedTimeMins, {
|
||||
one: 'minute',
|
||||
other: 'minutes',
|
||||
})}`
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -27,13 +27,20 @@ import * as Toast from '#/view/com/util/Toast'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {useSharedInputStyles} from '#/components/forms/TextField'
|
||||
import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlane} from '#/components/icons/PaperPlane'
|
||||
import {useExtractEmbedFromFacets} from './MessageInputEmbed'
|
||||
|
||||
const AnimatedTextInput = Animated.createAnimatedComponent(TextInput)
|
||||
|
||||
export function MessageInput({
|
||||
onSendMessage,
|
||||
hasEmbed,
|
||||
setEmbed,
|
||||
children,
|
||||
}: {
|
||||
onSendMessage: (message: string) => void
|
||||
hasEmbed: boolean
|
||||
setEmbed: (embedUrl: string | undefined) => void
|
||||
children?: React.ReactNode
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
@@ -53,9 +60,10 @@ export function MessageInput({
|
||||
const inputRef = useAnimatedRef<TextInput>()
|
||||
|
||||
useSaveMessageDraft(message)
|
||||
useExtractEmbedFromFacets(message, setEmbed)
|
||||
|
||||
const onSubmit = React.useCallback(() => {
|
||||
if (message.trim() === '') {
|
||||
if (!hasEmbed && message.trim() === '') {
|
||||
return
|
||||
}
|
||||
if (new Graphemer().countGraphemes(message) > MAX_DM_GRAPHEME_LENGTH) {
|
||||
@@ -63,16 +71,26 @@ export function MessageInput({
|
||||
return
|
||||
}
|
||||
clearDraft()
|
||||
onSendMessage(message.trimEnd())
|
||||
onSendMessage(message)
|
||||
playHaptic()
|
||||
setMessage('')
|
||||
setEmbed(undefined)
|
||||
|
||||
// Pressing the send button causes the text input to lose focus, so we need to
|
||||
// re-focus it after sending
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus()
|
||||
}, 100)
|
||||
}, [message, clearDraft, onSendMessage, playHaptic, _, inputRef])
|
||||
}, [
|
||||
hasEmbed,
|
||||
message,
|
||||
clearDraft,
|
||||
onSendMessage,
|
||||
playHaptic,
|
||||
setEmbed,
|
||||
_,
|
||||
inputRef,
|
||||
])
|
||||
|
||||
useFocusedInputHandler(
|
||||
{
|
||||
@@ -101,6 +119,7 @@ export function MessageInput({
|
||||
|
||||
return (
|
||||
<View style={[a.px_md, a.pb_sm, a.pt_xs]}>
|
||||
{children}
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
|
||||
@@ -16,11 +16,18 @@ import * as Toast from '#/view/com/util/Toast'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {useSharedInputStyles} from '#/components/forms/TextField'
|
||||
import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlane} from '#/components/icons/PaperPlane'
|
||||
import {useExtractEmbedFromFacets} from './MessageInputEmbed'
|
||||
|
||||
export function MessageInput({
|
||||
onSendMessage,
|
||||
hasEmbed,
|
||||
setEmbed,
|
||||
children,
|
||||
}: {
|
||||
onSendMessage: (message: string) => void
|
||||
hasEmbed: boolean
|
||||
setEmbed: (embedUrl: string | undefined) => void
|
||||
children?: React.ReactNode
|
||||
}) {
|
||||
const {isTabletOrDesktop} = useWebMediaQueries()
|
||||
const {_} = useLingui()
|
||||
@@ -35,7 +42,7 @@ export function MessageInput({
|
||||
const [textAreaHeight, setTextAreaHeight] = React.useState(38)
|
||||
|
||||
const onSubmit = React.useCallback(() => {
|
||||
if (message.trim() === '') {
|
||||
if (!hasEmbed && message.trim() === '') {
|
||||
return
|
||||
}
|
||||
if (new Graphemer().countGraphemes(message) > MAX_DM_GRAPHEME_LENGTH) {
|
||||
@@ -43,9 +50,10 @@ export function MessageInput({
|
||||
return
|
||||
}
|
||||
clearDraft()
|
||||
onSendMessage(message.trimEnd())
|
||||
onSendMessage(message)
|
||||
setMessage('')
|
||||
}, [message, onSendMessage, _, clearDraft])
|
||||
setEmbed(undefined)
|
||||
}, [message, onSendMessage, _, clearDraft, hasEmbed, setEmbed])
|
||||
|
||||
const onKeyDown = React.useCallback(
|
||||
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
@@ -87,9 +95,11 @@ export function MessageInput({
|
||||
)
|
||||
|
||||
useSaveMessageDraft(message)
|
||||
useExtractEmbedFromFacets(message, setEmbed)
|
||||
|
||||
return (
|
||||
<View style={a.p_sm}>
|
||||
{children}
|
||||
<View
|
||||
style={[
|
||||
a.flex_row,
|
||||
|
||||