Compare commits

..

5 Commits

Author SHA1 Message Date
Eric Bailey bc6b0603da [🙅] Add new deactivated screen (#4270)
* Add new deactivated screen

* Update copy, handle logout

* Remove icons, adjust padding
2024-05-31 13:49:21 -05:00
Eric Bailey 2de8dff18e Add status to session state 2024-05-30 15:15:47 -05:00
Eric Bailey 0bcc08f920 Change log context 2024-05-30 15:15:22 -05:00
Eric Bailey 18f855939d Snapshot crackle pop 2024-05-29 18:24:10 -05:00
Eric Bailey 1b0cdf1b90 Disambiguation of the deactivation 2024-05-29 18:15:56 -05:00
157 changed files with 2794 additions and 4435 deletions
@@ -120,32 +120,6 @@ 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 APK build is ready for download. This is a production build, and you should add it to the GitHub release! Download the artifact here: ${{ steps.upload-artifact-production-apk.outputs.artifact-url }}"
}
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
+1 -82
View File
@@ -142,8 +142,7 @@ describe('makeRecordUri', () => {
})
})
// FIXME: Reenable after fixing non-deterministic test.
describe.skip('ago', () => {
describe('ago', () => {
const oneYearDate = new Date(
new Date().setMonth(new Date().getMonth() - 11),
).setDate(new Date().getDate() - 28)
@@ -480,26 +479,6 @@ 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 = [
@@ -797,66 +776,6 @@ 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', () => {
@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 450 B

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="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>

Before

Width:  |  Height:  |  Size: 775 B

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="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>

Before

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 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>
<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>

Before

Width:  |  Height:  |  Size: 514 B

After

Width:  |  Height:  |  Size: 513 B

@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 455 B

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="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>

Before

Width:  |  Height:  |  Size: 857 B

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="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>

Before

Width:  |  Height:  |  Size: 412 B

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="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>

Before

Width:  |  Height:  |  Size: 508 B

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="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>

Before

Width:  |  Height:  |  Size: 446 B

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="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>

Before

Width:  |  Height:  |  Size: 517 B

-14
View File
@@ -46,20 +46,6 @@
"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": {
@@ -15,10 +15,7 @@ export function NativeTranslationView() {
return <NativeView />
}
// 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
export const isAvailable = Number(Platform.Version) >= 17.4
// https://en.wikipedia.org/wiki/Translate_(Apple)#Languages
const SUPPORTED_LANGUAGES = [
+2 -2
View File
@@ -49,7 +49,7 @@
"open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web"
},
"dependencies": {
"@atproto/api": "^0.12.14",
"@atproto/api": "^0.12.13",
"@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.2",
"@fortawesome/react-native-fontawesome": "^0.3.0",
"@lingui/react": "^4.5.0",
"@mattermost/react-native-paste-input": "^0.7.1",
"@miblanchard/react-native-slider": "^2.3.1",
+7 -24
View File
@@ -1,8 +1,7 @@
import {Platform, StyleSheet} from 'react-native'
import {Platform} from 'react-native'
import * as tokens from '#/alf/tokens'
import {native, web} from '#/alf/util/platform'
import hairlineWidth = StyleSheet.hairlineWidth
export const atoms = {
/*
@@ -56,13 +55,6 @@ export const atoms = {
height: '100vh',
}),
/*
* Theme-independent bg colors
*/
bg_transparent: {
backgroundColor: 'transparent',
},
/*
* Border radius
*/
@@ -278,19 +270,19 @@ export const atoms = {
borderWidth: 0,
},
border: {
borderWidth: hairlineWidth,
borderWidth: 1,
},
border_t: {
borderTopWidth: hairlineWidth,
borderTopWidth: 1,
},
border_b: {
borderBottomWidth: hairlineWidth,
borderBottomWidth: 1,
},
border_l: {
borderLeftWidth: hairlineWidth,
borderLeftWidth: 1,
},
border_r: {
borderRightWidth: hairlineWidth,
borderRightWidth: 1,
},
/*
@@ -849,7 +841,7 @@ export const atoms = {
marginRight: 'auto',
},
/*
* Pointer events & user select
* Pointer events
*/
pointer_events_none: {
pointerEvents: 'none',
@@ -857,15 +849,6 @@ 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
*/
+3 -5
View File
@@ -1,14 +1,12 @@
import React from 'react'
import {Dimensions} from 'react-native'
import * as themes from '#/alf/themes'
export {atoms} from '#/alf/atoms'
export * as tokens from '#/alf/tokens'
export * from '#/alf/types'
export * from '#/alf/util/flatten'
export * as tokens from '#/alf/tokens'
export {atoms} from '#/alf/atoms'
export * from '#/alf/util/platform'
export * from '#/alf/util/themeSelector'
export * from '#/alf/util/flatten'
type BreakpointName = keyof typeof breakpoints
+5 -5
View File
@@ -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> & {name: ThemeName}
export type Theme = Mutable<ReadonlyTheme>
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' as ThemeName,
name: 'light',
palette: lightPalette,
atoms: {
text: {
@@ -278,7 +278,7 @@ export const light = {
}
export const dark: Theme = {
name: 'dark' as ThemeName,
name: 'dark',
palette: darkPalette,
atoms: {
text: {
@@ -367,7 +367,7 @@ export const dark: Theme = {
export const dim: Theme = {
...dark,
name: 'dim' as ThemeName,
name: 'dim',
palette: dimPalette,
atoms: {
...dark.atoms,
-14
View File
@@ -1,14 +0,0 @@
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}`)
}
}
+15 -17
View File
@@ -1,27 +1,25 @@
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()
return getThemeName(colorScheme, colorMode, darkTheme)
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],
)
}
function getThemeName(
@@ -55,7 +53,7 @@ function updateDocument(theme: ThemeName) {
}
}
export function getBackgroundColor(theme: ThemeName): string {
function getBackgroundColor(theme: ThemeName): string {
switch (theme) {
case 'light':
return light.atoms.bg.backgroundColor
+2 -2
View File
@@ -37,7 +37,7 @@ export function AccountList({
style={[
a.rounded_md,
a.overflow_hidden,
{borderWidth: 1},
a.border,
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={[{borderBottomWidth: 1}, t.atoms.border_contrast_low]} />
<View style={[a.border_b, t.atoms.border_contrast_low]} />
</React.Fragment>
))}
<Button
+3 -11
View File
@@ -28,7 +28,7 @@ export type ButtonColor =
| 'gradient_sunset'
| 'gradient_nordic'
| 'gradient_bonfire'
export type ButtonSize = 'tiny' | 'xsmall' | 'small' | 'medium' | 'large'
export type ButtonSize = 'tiny' | 'small' | 'medium' | 'large'
export type ButtonShape = 'round' | 'square' | 'default'
export type VariantProps = {
/**
@@ -64,14 +64,13 @@ type NonTextElements =
export type ButtonProps = Pick<
PressableProps,
'disabled' | 'onPress' | 'testID' | 'onLongPress' | 'hitSlop'
'disabled' | 'onPress' | 'testID' | 'onLongPress'
> &
AccessibilityProps &
VariantProps & {
testID?: string
label: string
style?: StyleProp<ViewStyle>
hoverStyle?: StyleProp<ViewStyle>
children: NonTextElements | ((context: ButtonContext) => NonTextElements)
}
@@ -97,7 +96,6 @@ export function Button({
label,
disabled = false,
style,
hoverStyle: hoverStyleProp,
...rest
}: ButtonProps) {
const t = useTheme()
@@ -283,8 +281,6 @@ 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)
}
@@ -297,8 +293,6 @@ 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})
}
@@ -380,10 +374,8 @@ 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}
+41 -44
View File
@@ -32,7 +32,6 @@ 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'
@@ -171,49 +170,47 @@ export function Outer({
return (
isOpen && (
<Portal>
<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>
<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>
</Portal>
)
)
-1
View File
@@ -1 +0,0 @@
export {FullWindowOverlay} from 'react-native-screens'
-1
View File
@@ -1 +0,0 @@
export {Fragment as FullWindowOverlay} from 'react'
+7 -13
View File
@@ -28,7 +28,6 @@ export function RichText({
authorHandle,
onLinkPress,
interactiveStyle,
emojiMultiplier = 1.85,
}: TextStyleProp &
Pick<TextProps, 'selectable'> & {
value: RichTextAPI | string
@@ -39,7 +38,6 @@ export function RichText({
authorHandle?: string
onLinkPress?: LinkProps['onPress']
interactiveStyle?: TextStyle
emojiMultiplier?: number
}) {
const richText = React.useMemo(
() =>
@@ -59,14 +57,17 @@ export function RichText({
const {text, facets} = richText
if (!facets?.length) {
if (isOnlyEmoji(text)) {
const fontSize =
(flattenedStyle.fontSize ?? a.text_sm.fontSize) * emojiMultiplier
if (text.length <= 5 && /^\p{Extended_Pictographic}+$/u.test(text)) {
return (
<Text
selectable={selectable}
testID={testID}
style={[plainStyles, {fontSize}]}
style={[
{
fontSize: 26,
lineHeight: 30,
},
]}
// @ts-ignore web only -prf
dataSet={WORD_WRAP}>
{text}
@@ -246,10 +247,3 @@ function RichTextTag({
</React.Fragment>
)
}
export function isOnlyEmoji(text: string) {
return (
text.length <= 15 &&
/^[\p{Emoji_Presentation}\p{Extended_Pictographic}]+$/u.test(text)
)
}
-255
View File
@@ -1,255 +0,0 @@
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>
)
}
@@ -1,53 +0,0 @@
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>
)
}
+49 -16
View File
@@ -1,15 +1,11 @@
import React, {
useCallback,
useImperativeHandle,
useMemo,
useRef,
useState,
} from 'react'
import React, {useCallback, 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 {
@@ -27,23 +23,16 @@ 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({
controlRef,
control,
onClose,
onSelectGif: onSelectGifProp,
}: {
controlRef: React.RefObject<{open: () => void}>
control: Dialog.DialogControlProps
onClose: () => void
onSelectGif: (gif: Gif) => void
}) {
const control = Dialog.useDialogControl()
useImperativeHandle(controlRef, () => ({
open: () => control.open(),
}))
const onSelectGif = useCallback(
(gif: Gif) => {
control.close(() => onSelectGifProp(gif))
@@ -244,6 +233,50 @@ 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()
+3 -5
View File
@@ -26,11 +26,9 @@ 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,
PersonCheck_Stroke2_Corner0_Rounded as PersonCheck,
PersonX_Stroke2_Corner0_Rounded as PersonX,
} from '#/components/icons/Person'
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 {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker'
import * as Menu from '#/components/Menu'
import * as Prompt from '#/components/Prompt'
+33 -47
View File
@@ -6,11 +6,7 @@ import {
TextStyle,
View,
} from 'react-native'
import {
AppBskyEmbedRecord,
ChatBskyConvoDefs,
RichText as RichTextAPI,
} from '@atproto/api'
import {ChatBskyConvoDefs, RichText as RichTextAPI} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -21,8 +17,7 @@ import {atoms as a, useTheme} from '#/alf'
import {ActionsWrapper} from '#/components/dms/ActionsWrapper'
import {InlineLinkText} from '#/components/Link'
import {Text} from '#/components/Typography'
import {isOnlyEmoji, RichText} from '../RichText'
import {MessageItemEmbed} from './MessageItemEmbed'
import {RichText} from '../RichText'
let MessageItem = ({
item,
@@ -82,46 +77,37 @@ let MessageItem = ({
return (
<View style={[isFromSelf ? a.mr_md : a.ml_md]}>
<ActionsWrapper isFromSelf={isFromSelf} message={message}>
{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>
)}
<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>
</ActionsWrapper>
{isLastInGroup && (
-22
View File
@@ -1,22 +0,0 @@
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}
+11 -36
View File
@@ -6,16 +6,12 @@ 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'
@@ -39,12 +35,10 @@ 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 onCopyMessage = React.useCallback(() => {
const onCopyPostText = React.useCallback(() => {
const str = richTextToString(
new RichText({
text: message.text,
@@ -57,14 +51,6 @@ 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
@@ -95,27 +81,16 @@ export let MessageMenu = ({
)}
<Menu.Outer>
{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="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 />
<Menu.Group>
<Menu.Item
testID="messageDropdownDeleteBtn"
+47 -68
View File
@@ -22,7 +22,6 @@ 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
@@ -59,7 +58,7 @@ export let MessagesListHeader = ({
t.atoms.border_contrast_low,
a.border_b,
a.flex_row,
a.align_start,
a.align_center,
a.gap_sm,
gtTablet ? a.pl_lg : a.pl_xl,
a.pr_lg,
@@ -70,7 +69,7 @@ export let MessagesListHeader = ({
testID="conversationHeaderBackBtn"
onPress={onPressBack}
hitSlop={BACK_HITSLOP}
style={{width: 30, height: 30, marginTop: isWeb ? 6 : 4}}
style={{width: 30, height: 30}}
accessibilityRole="button"
accessibilityLabel={_(msg`Back`)}
accessibilityHint="">
@@ -153,71 +152,51 @@ function HeaderReady({
)
return (
<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={[a.text_md, a.font_bold, web(a.leading_normal)]}
numberOfLines={1}>
{displayName}
</Text>
{!isDeletedAccount && (
<Text
style={[
t.atoms.text_contrast_medium,
a.text_sm,
web([a.leading_normal, {marginTop: -2}]),
]}
numberOfLines={1}>
@{profile.handle}
{convoState.convo?.muted && (
<>
{' '}
&middot;{' '}
<BellStroke
size="xs"
style={t.atoms.text_contrast_medium}
/>
</>
)}
</Text>
)}
</View>
</Link>
{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]}
<>
<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>
</View>
<View style={a.flex_1}>
<Text
style={[a.text_md, a.font_bold, web(a.leading_normal)]}
numberOfLines={1}>
{displayName}
</Text>
{!isDeletedAccount && (
<Text
style={[
t.atoms.text_contrast_medium,
a.text_sm,
web([a.leading_normal, {marginTop: -2}]),
]}
numberOfLines={1}>
@{profile.handle}
{convoState.convo?.muted && (
<>
{' '}
&middot;{' '}
<BellStroke size="xs" style={t.atoms.text_contrast_medium} />
</>
)}
</Text>
)}
</View>
</Link>
{isConvoActive(convoState) && (
<ConvoMenu
convo={convoState.convo}
profile={profile}
currentScreen="conversation"
blockInfo={blockInfo}
/>
)}
</>
)
}
@@ -16,19 +16,23 @@ 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 {useListConvosQuery} from '#/state/queries/messages/list-converations'
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
import {useProfileFollowsQuery} from '#/state/queries/profile-follows'
import {useSession} from '#/state/session'
import {logEvent} from 'lib/statsig/statsig'
import {useActorAutocompleteQuery} from 'state/queries/actor-autocomplete'
import {FAB} from '#/view/com/util/fab/FAB'
import * as Toast from '#/view/com/util/Toast'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, native, useTheme, web} from '#/alf'
import {Button} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {TextInput} from '#/components/dms/dialogs/TextInput'
import {TextInput} from '#/components/dms/NewChatDialog/TextInput'
import {canBeMessaged} from '#/components/dms/util'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import {Text} from '#/components/Typography'
@@ -53,291 +57,55 @@ type Item =
key: string
}
export function SearchablePeopleList({
title,
onSelectChat,
showRecentConvos,
export function NewChat({
control,
onNewChat,
}: {
title: string
onSelectChat: (did: string) => void
showRecentConvos?: boolean
control: Dialog.DialogControlProps
onNewChat: (chatId: 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 {mutate: createChat} = useGetConvoForMembers({
onSuccess: data => {
onNewChat(data.convo.id)
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
})
}
} else {
const placeholders: Item[] = Array(10)
.fill(0)
.map((_, i) => ({
type: 'placeholder',
key: i + '',
}))
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
if (!data.convo.lastMessage) {
logEvent('chat:create', {logContext: 'NewChatDialog'})
}
logEvent('chat:open', {logContext: 'NewChatDialog'})
},
[moderationOpts, onSelectChat],
onError: error => {
Toast.show(error.message)
},
})
const onCreateChat = useCallback(
(did: string) => {
control.close(() => createChat([did]))
},
[control, createChat],
)
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,
]}>
{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"
/>
<>
<FAB
testID="newChatFAB"
onPress={control.open}
icon={<Plus size="lg" fill={t.palette.white} />}
accessibilityRole="button"
accessibilityLabel={_(msg`New chat`)}
accessibilityHint=""
/>
<Dialog.Outer
control={control}
testID="newChatDialog"
nativeOptions={{sheet: {snapPoints: ['100%']}}}>
<SearchablePeopleList onCreateChat={onCreateChat} />
</Dialog.Outer>
</>
)
}
@@ -525,3 +293,217 @@ 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"
/>
)
}
@@ -1,67 +0,0 @@
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>
</>
)
}
@@ -1,53 +0,0 @@
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
}) {
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 (
<Dialog.Outer
control={control}
testID="sendViaChatChatDialog"
nativeOptions={{sheet: {snapPoints: ['100%']}}}>
<SearchablePeopleList
title={_(msg`Send post to...`)}
onSelectChat={onCreateChat}
showRecentConvos
/>
</Dialog.Outer>
)
}
+1 -1
View File
@@ -1,5 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const Image_Stroke2_Corner0_Rounded = createSinglePathSVG({
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',
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',
})
-16
View File
@@ -3,19 +3,3 @@ 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',
})
+5
View File
@@ -0,0 +1,5 @@
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',
})
+5
View File
@@ -0,0 +1,5 @@
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',
})
-21
View File
@@ -1,21 +0,0 @@
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',
})
-13
View File
@@ -1,13 +0,0 @@
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 -1
View File
@@ -1,5 +1,5 @@
import {StyleSheet, TextProps} from 'react-native'
import type {PathProps, SvgProps} from 'react-native-svg'
import type {SvgProps, PathProps} from 'react-native-svg'
import {tokens} from '#/alf'
+7 -10
View File
@@ -1,19 +1,20 @@
import React from 'react'
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import {ModerationUI} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {msg, Trans} from '@lingui/macro'
import {isJustAMute} from '#/lib/moderation'
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
import {isJustAMute} from '#/lib/moderation'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
import {atoms as a, useTheme, useBreakpoints, 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,
@@ -51,9 +52,7 @@ export function ContentHider({
<ModerationDetailsDialog control={control} modcause={blur} />
<Button
onPress={e => {
e.preventDefault()
e.stopPropagation()
onPress={() => {
if (!modui.noOverride) {
setOverride(v => !v)
} else {
@@ -122,9 +121,7 @@ export function ContentHider({
{desc.source && blur.type === 'label' && !override && (
<Button
onPress={e => {
e.preventDefault()
e.stopPropagation()
onPress={() => {
control.open()
}}
label={_(
+13 -44
View File
@@ -1,10 +1,9 @@
import React from 'react'
import {StyleProp, View, ViewStyle} from 'react-native'
import {BSKY_LABELER_DID, ModerationCause, ModerationUI} from '@atproto/api'
import {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 {
@@ -15,11 +14,9 @@ import {Text} from '#/components/Typography'
export function PostAlerts({
modui,
size,
style,
}: {
modui: ModerationUI
size?: 'medium' | 'large'
includeMute?: boolean
style?: StyleProp<ViewStyle>
}) {
@@ -31,31 +28,17 @@ 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}
size={size}
/>
<PostLabel key={getModerationCauseKey(cause)} cause={cause} />
))}
{modui.informs.map(cause => (
<PostLabel
key={getModerationCauseKey(cause)}
cause={cause}
size={size}
/>
<PostLabel key={getModerationCauseKey(cause)} cause={cause} />
))}
</View>
</View>
)
}
function PostLabel({
cause,
size,
}: {
cause: ModerationCause
size?: 'medium' | 'large'
}) {
function PostLabel({cause}: {cause: ModerationCause}) {
const control = useModerationDetailsDialogControl()
const desc = useModerationCauseDescription(cause)
const t = useTheme()
@@ -64,9 +47,7 @@ function PostLabel({
<>
<Button
label={desc.name}
onPress={e => {
e.preventDefault()
e.stopPropagation()
onPress={() => {
control.open()
}}>
{({hovered, pressed}) => (
@@ -74,36 +55,24 @@ function PostLabel({
style={[
a.flex_row,
a.align_center,
{paddingLeft: 4, paddingRight: 6, paddingVertical: 1},
a.gap_xs,
a.rounded_sm,
hovered || pressed
? 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},
? t.atoms.bg_contrast_50
: t.atoms.bg_contrast_25,
]}>
{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} />
)}
<desc.icon size="xs" fill={t.atoms.text_contrast_medium.color} />
<Text
style={[
a.text_left,
a.leading_snug,
size === 'large' ? {fontSize: 13} : a.text_xs,
size === 'large' ? t.atoms.text : t.atoms.text_contrast_high,
a.text_xs,
t.atoms.text_contrast_medium,
a.font_semibold,
]}>
{desc.name}
{desc.source ? ` ${desc.source}` : ''}
</Text>
</View>
)}
+1 -4
View File
@@ -23,7 +23,6 @@ interface Props extends ComponentProps<typeof Link> {
iconStyles: StyleProp<ViewStyle>
modui: ModerationUI
profile: AppBskyActorDefs.ProfileViewBasic
interpretFilterAsBlur?: boolean
}
export function PostHider({
@@ -36,7 +35,6 @@ export function PostHider({
iconSize,
iconStyles,
profile,
interpretFilterAsBlur,
...props
}: Props) {
const queryClient = useQueryClient()
@@ -44,8 +42,7 @@ export function PostHider({
const {_} = useLingui()
const [override, setOverride] = React.useState(false)
const control = useModerationDetailsDialogControl()
const blur =
modui.blurs[0] || (interpretFilterAsBlur ? modui.filters[0] : undefined)
const blur = modui.blurs[0]
const desc = useModerationCauseDescription(blur)
const onBeforePress = React.useCallback(() => {
@@ -1,14 +1,9 @@
import React from 'react'
import {StyleProp, View, ViewStyle} from 'react-native'
import {
BSKY_LABELER_DID,
ModerationCause,
ModerationDecision,
} from '@atproto/api'
import {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 {
@@ -67,12 +62,7 @@ function ProfileLabel({cause}: {cause: ModerationCause}) {
? t.atoms.bg_contrast_50
: t.atoms.bg_contrast_25,
]}>
{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} />
)}
<desc.icon size="sm" fill={t.atoms.text_contrast_medium.color} />
<Text
style={[
a.text_left,
@@ -82,6 +72,7 @@ function ProfileLabel({cause}: {cause: ModerationCause}) {
a.font_semibold,
]}>
{desc.name}
{desc.source ? ` ${desc.source}` : ''}
</Text>
</View>
)}
+13 -2
View File
@@ -4,6 +4,7 @@ import {
AppBskyEmbedRecord,
AppBskyEmbedRecordWithMedia,
AppBskyFeedThreadgate,
AppBskyRichtextFacet,
BskyAgent,
ComAtprotoLabelDefs,
ComAtprotoRepoUploadBlob,
@@ -14,7 +15,7 @@ import {AtUri} from '@atproto/api'
import {logger} from '#/logger'
import {ThreadgateSetting} from '#/state/queries/threadgate'
import {isNetworkError} from 'lib/strings/errors'
import {shortenLinks, stripInvalidMentions} from 'lib/strings/rich-text-manip'
import {shortenLinks} 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'
@@ -80,7 +81,17 @@ export async function post(agent: BskyAgent, opts: PostOpts) {
opts.onStateChange?.('Processing...')
await rt.detectFacets(agent)
rt = shortenLinks(rt)
rt = stripInvalidMentions(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
})
// add quote embed if present
if (opts.quote) {
+634
View File
@@ -2,6 +2,98 @@ 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({
@@ -55,6 +147,90 @@ 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,
@@ -87,6 +263,160 @@ 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({
@@ -114,6 +444,208 @@ 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,
@@ -183,6 +715,33 @@ 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,
@@ -224,6 +783,54 @@ 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,
@@ -340,3 +947,30 @@ 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>
)
}
@@ -6,15 +6,15 @@ import {
} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
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'
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 {CircleBanSign_Stroke2_Corner0_Rounded as CircleBanSign} from '#/components/icons/CircleBanSign'
export interface ModerationCauseDescription {
icon: React.ComponentType<SVGIconProps>
@@ -22,8 +22,6 @@ export interface ModerationCauseDescription {
description: string
source?: string
sourceType?: ModerationCauseSource['type']
sourceAvi?: string
sourceDid?: string
}
export function useModerationCauseDescription(
@@ -140,8 +138,6 @@ export function useModerationCauseDescription(
description: strings.description,
source,
sourceType: cause.source.type,
sourceAvi: labeler?.creator.avatar,
sourceDid: cause.label.src,
}
}
// should never happen
+36 -31
View File
@@ -71,41 +71,46 @@ export function useNotificationsRegistration() {
export function useRequestNotificationsPermission() {
const gate = useGate()
const {currentAccount} = useSession()
return async (context: 'StartOnboarding' | 'AfterOnboarding' | 'Login') => {
const permissions = await Notifications.getPermissionsAsync()
return React.useCallback(
async (context: 'StartOnboarding' | 'AfterOnboarding' | 'Login') => {
const permissions = await Notifications.getPermissionsAsync()
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
}
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
}
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)
}
}
if (res.granted) {
// This will fire a pushTokenEvent, which will handle registration of the token
getPushToken(true)
}
},
[gate, currentAccount],
)
}
export async function decrementBadgeCount(by: number | 'reset' = 1) {
+1 -1
View File
@@ -38,7 +38,7 @@ export type CommonNavigatorParams = {
AccessibilitySettings: undefined
Search: {q?: string}
Hashtag: {tag: string; author?: string}
MessagesConversation: {conversation: string; embed?: string}
MessagesConversation: {conversation: string}
MessagesSettings: undefined
}
+7 -13
View File
@@ -20,10 +20,10 @@ export type LogEvents = {
context: 'StartOnboarding' | 'AfterOnboarding' | 'Login'
status: 'granted' | 'denied' | 'undetermined'
}
'state:background:sampled': {
'state:background': {
secondsActive: number
}
'state:foreground:sampled': {}
'state:foreground': {}
'router:navigate:sampled': {}
// Screen events
@@ -57,18 +57,18 @@ export type LogEvents = {
'onboarding:finished:avatarResult': {
avatarResult: 'default' | 'created' | 'uploaded'
}
'home:feedDisplayed:sampled': {
'home:feedDisplayed': {
feedUrl: string
feedType: string
index: number
reason: 'focus' | 'tabbar-click' | 'pager-swipe' | 'desktop-sidebar-click'
}
'feed:endReached:sampled': {
'feed:endReached': {
feedUrl: string
feedType: string
itemCount: number
}
'feed:refresh:sampled': {
'feed:refresh': {
feedUrl: string
feedType: string
reason: 'pull-to-refresh' | 'soft-reset' | 'load-latest'
@@ -115,7 +115,6 @@ export type LogEvents = {
| 'ProfileHeaderSuggestedFollows'
| 'ProfileMenu'
| 'ProfileHoverCard'
| 'AvatarButton'
}
'profile:unfollow': {
logContext:
@@ -127,17 +126,12 @@ export type LogEvents = {
| 'ProfileMenu'
| 'ProfileHoverCard'
| 'Chat'
| 'AvatarButton'
}
'chat:create': {
logContext: 'ProfileHeader' | 'NewChatDialog' | 'SendViaChatDialog'
logContext: 'ProfileHeader' | 'NewChatDialog'
}
'chat:open': {
logContext:
| 'ProfileHeader'
| 'NewChatDialog'
| 'ChatsList'
| 'SendViaChatDialog'
logContext: 'ProfileHeader' | 'NewChatDialog' | 'ChatsList'
}
'test:all:always': {}
+1 -2
View File
@@ -1,5 +1,4 @@
export type Gate =
// Keep this alphabetic please.
| 'request_notifications_permission_after_onboarding_v2'
| 'show_avi_follow_button'
| 'request_notifications_permission_after_onboarding'
| 'show_follow_back_label_v2'
+3 -20
View File
@@ -87,14 +87,7 @@ export function toClout(n: number | null | undefined): number | undefined {
}
}
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 DOWNSAMPLED_EVENTS = new Set(['router:navigate:sampled'])
const isDownsampledSession = Math.random() < 0.9 // 90% likely
export function logEvent<E extends keyof LogEvents>(
@@ -102,16 +95,6 @@ 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
}
@@ -216,14 +199,14 @@ AppState.addEventListener('change', (state: AppStateStatus) => {
lastState = state
if (state === 'active') {
lastActive = performance.now()
logEvent('state:foreground:sampled', {})
logEvent('state:foreground', {})
} else {
let secondsActive = 0
if (lastActive != null) {
secondsActive = Math.round((performance.now() - lastActive) / 1e3)
}
lastActive = null
logEvent('state:background:sampled', {
logEvent('state:background', {
secondsActive,
})
}
-76
View File
@@ -23,7 +23,6 @@ export const embedPlayerSources = [
'vimeo',
'giphy',
'tenor',
'flickr',
] as const
export type EmbedPlayerSource = (typeof embedPlayerSources)[number]
@@ -43,7 +42,6 @@ export type EmbedPlayerType =
| 'vimeo_video'
| 'giphy_gif'
| 'tenor_gif'
| 'flickr_album'
export const externalEmbedLabels: Record<EmbedPlayerSource, string> = {
youtube: 'YouTube',
@@ -55,7 +53,6 @@ export const externalEmbedLabels: Record<EmbedPlayerSource, string> = {
spotify: 'Spotify',
appleMusic: 'Apple Music',
soundcloud: 'SoundCloud',
flickr: 'Flickr',
}
export interface EmbedPlayerParams {
@@ -378,79 +375,6 @@ 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({
+4 -20
View File
@@ -1,4 +1,4 @@
import {AppBskyRichtextFacet, RichText, UnicodeString} from '@atproto/api'
import {RichText, UnicodeString} from '@atproto/api'
import {toShortUrl} from './url-helpers'
@@ -10,7 +10,9 @@ export function shortenLinks(rt: RichText): RichText {
// enumerate the link facets
if (rt.facets) {
for (const facet of rt.facets) {
const isLink = !!facet.features.find(AppBskyRichtextFacet.isLink)
const isLink = !!facet.features.find(
f => f.$type === 'app.bsky.richtext.facet#link',
)
if (!isLink) {
continue
}
@@ -31,21 +33,3 @@ 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
}
-21
View File
@@ -3,7 +3,6 @@ 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 = [
@@ -84,10 +83,6 @@ 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/')
}
@@ -188,22 +183,6 @@ 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.
*
@@ -27,20 +27,13 @@ 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()
@@ -60,10 +53,9 @@ export function MessageInput({
const inputRef = useAnimatedRef<TextInput>()
useSaveMessageDraft(message)
useExtractEmbedFromFacets(message, setEmbed)
const onSubmit = React.useCallback(() => {
if (!hasEmbed && message.trim() === '') {
if (message.trim() === '') {
return
}
if (new Graphemer().countGraphemes(message) > MAX_DM_GRAPHEME_LENGTH) {
@@ -71,26 +63,16 @@ export function MessageInput({
return
}
clearDraft()
onSendMessage(message)
onSendMessage(message.trimEnd())
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)
}, [
hasEmbed,
message,
clearDraft,
onSendMessage,
playHaptic,
setEmbed,
_,
inputRef,
])
}, [message, clearDraft, onSendMessage, playHaptic, _, inputRef])
useFocusedInputHandler(
{
@@ -119,7 +101,6 @@ export function MessageInput({
return (
<View style={[a.px_md, a.pb_sm, a.pt_xs]}>
{children}
<View
style={[
a.w_full,
@@ -16,18 +16,11 @@ 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()
@@ -42,7 +35,7 @@ export function MessageInput({
const [textAreaHeight, setTextAreaHeight] = React.useState(38)
const onSubmit = React.useCallback(() => {
if (!hasEmbed && message.trim() === '') {
if (message.trim() === '') {
return
}
if (new Graphemer().countGraphemes(message) > MAX_DM_GRAPHEME_LENGTH) {
@@ -50,10 +43,9 @@ export function MessageInput({
return
}
clearDraft()
onSendMessage(message)
onSendMessage(message.trimEnd())
setMessage('')
setEmbed(undefined)
}, [message, onSendMessage, _, clearDraft, hasEmbed, setEmbed])
}, [message, onSendMessage, _, clearDraft])
const onKeyDown = React.useCallback(
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
@@ -95,11 +87,9 @@ export function MessageInput({
)
useSaveMessageDraft(message)
useExtractEmbedFromFacets(message, setEmbed)
return (
<View style={a.p_sm}>
{children}
<View
style={[
a.flex_row,
@@ -1,231 +0,0 @@
import React, {useCallback, useEffect, useMemo, useState} from 'react'
import {LayoutAnimation, View} from 'react-native'
import {
AppBskyEmbedImages,
AppBskyEmbedRecordWithMedia,
AppBskyFeedPost,
AppBskyRichtextFacet,
AtUri,
RichText as RichTextAPI,
} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {RouteProp, useNavigation, useRoute} from '@react-navigation/native'
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
import {makeProfileLink} from '#/lib/routes/links'
import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types'
import {
convertBskyAppUrlIfNeeded,
isBskyPostUrl,
makeRecordUri,
} from '#/lib/strings/url-helpers'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {usePostQuery} from '#/state/queries/post'
import {ImageHorzList} from '#/view/com/util/images/ImageHorzList'
import {PostMeta} from '#/view/com/util/PostMeta'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import {Loader} from '#/components/Loader'
import {ContentHider} from '#/components/moderation/ContentHider'
import {PostAlerts} from '#/components/moderation/PostAlerts'
import {RichText} from '#/components/RichText'
import {Text} from '#/components/Typography'
export function useMessageEmbed() {
const route =
useRoute<RouteProp<CommonNavigatorParams, 'MessagesConversation'>>()
const navigation = useNavigation<NavigationProp>()
const embedFromParams = route.params.embed
const [embedUri, setEmbed] = useState(embedFromParams)
if (embedFromParams && embedUri !== embedFromParams) {
setEmbed(embedFromParams)
}
return {
embedUri,
setEmbed: useCallback(
(embedUrl: string | undefined) => {
if (!embedUrl) {
navigation.setParams({embed: ''})
setEmbed(undefined)
return
}
if (embedFromParams) return
const url = convertBskyAppUrlIfNeeded(embedUrl)
const [_0, user, _1, rkey] = url.split('/').filter(Boolean)
const uri = makeRecordUri(user, 'app.bsky.feed.post', rkey)
setEmbed(uri)
},
[embedFromParams, navigation],
),
}
}
export function useExtractEmbedFromFacets(
message: string,
setEmbed: (embedUrl: string | undefined) => void,
) {
const rt = new RichTextAPI({text: message})
rt.detectFacetsWithoutResolution()
let uriFromFacet: string | undefined
for (const facet of rt.facets ?? []) {
for (const feature of facet.features) {
if (AppBskyRichtextFacet.isLink(feature) && isBskyPostUrl(feature.uri)) {
uriFromFacet = feature.uri
break
}
}
}
useEffect(() => {
if (uriFromFacet) {
setEmbed(uriFromFacet)
}
}, [uriFromFacet, setEmbed])
}
export function MessageInputEmbed({
embedUri,
setEmbed,
}: {
embedUri: string | undefined
setEmbed: (embedUrl: string | undefined) => void
}) {
const t = useTheme()
const {_} = useLingui()
const {data: post, status} = usePostQuery(embedUri)
const moderationOpts = useModerationOpts()
const moderation = useMemo(
() =>
moderationOpts && post ? moderatePost(post, moderationOpts) : undefined,
[moderationOpts, post],
)
const {rt, record} = useMemo(() => {
if (
post &&
AppBskyFeedPost.isRecord(post.record) &&
AppBskyFeedPost.validateRecord(post.record).success
) {
return {
rt: new RichTextAPI({
text: post.record.text,
facets: post.record.facets,
}),
record: post.record,
}
}
return {rt: undefined, record: undefined}
}, [post])
if (!embedUri) {
return null
}
let content = null
switch (status) {
case 'pending':
content = (
<View
style={[a.flex_1, {minHeight: 64}, a.justify_center, a.align_center]}>
<Loader />
</View>
)
break
case 'error':
content = (
<View
style={[a.flex_1, {minHeight: 64}, a.justify_center, a.align_center]}>
<Text style={a.text_center}>Could not fetch post</Text>
</View>
)
break
case 'success':
const itemUrip = new AtUri(post.uri)
const itemHref = makeProfileLink(post.author, 'post', itemUrip.rkey)
if (!post || !moderation || !rt || !record) {
return null
}
const images = AppBskyEmbedImages.isView(post.embed)
? post.embed.images
: AppBskyEmbedRecordWithMedia.isView(post.embed) &&
AppBskyEmbedImages.isView(post.embed.media)
? post.embed.media.images
: undefined
content = (
<View
style={[
a.flex_1,
t.atoms.bg,
t.atoms.border_contrast_low,
a.rounded_md,
a.border,
a.p_sm,
a.mb_sm,
]}
pointerEvents="none">
<PostMeta
showAvatar
author={post.author}
moderation={moderation}
authorHasWarning={!!post.author.labels?.length}
timestamp={post.indexedAt}
postHref={itemHref}
style={a.flex_0}
/>
<ContentHider modui={moderation.ui('contentView')}>
<PostAlerts modui={moderation.ui('contentView')} style={a.py_xs} />
{rt.text && (
<View style={a.mt_xs}>
<RichText
enableTags
testID="postText"
value={rt}
style={[a.text_sm, t.atoms.text_contrast_high]}
authorHandle={post.author.handle}
numberOfLines={3}
/>
</View>
)}
{images && images?.length > 0 && (
<ImageHorzList images={images} style={a.mt_xs} />
)}
</ContentHider>
</View>
)
break
}
return (
<View style={[a.flex_row, a.gap_sm]}>
{content}
<Button
label={_(msg`Remove embed`)}
onPress={() => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
setEmbed(undefined)
}}
size="tiny"
variant="solid"
color="secondary"
shape="round">
<ButtonIcon icon={X} />
</Button>
</View>
)
}
@@ -13,18 +13,12 @@ import {
} from 'react-native-reanimated'
import {ReanimatedScrollEvent} from 'react-native-reanimated/lib/typescript/reanimated2/hook/commonTypes'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {AppBskyEmbedRecord, AppBskyRichtextFacet, RichText} from '@atproto/api'
import {AppBskyRichtextFacet, RichText} from '@atproto/api'
import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip'
import {
convertBskyAppUrlIfNeeded,
isBskyPostUrl,
} from '#/lib/strings/url-helpers'
import {logger} from '#/logger'
import {shortenLinks} from '#/lib/strings/rich-text-manip'
import {isNative} from '#/platform/detection'
import {isConvoActive, useConvoActive} from '#/state/messages/convo'
import {ConvoItem, ConvoStatus} from '#/state/messages/convo/types'
import {useGetPost} from '#/state/queries/post'
import {useAgent} from '#/state/session'
import {clamp} from 'lib/numbers'
import {ScrollProvider} from 'lib/ScrollContext'
@@ -38,7 +32,6 @@ import {MessageItem} from '#/components/dms/MessageItem'
import {NewMessagesPill} from '#/components/dms/NewMessagesPill'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
import {MessageInputEmbed, useMessageEmbed} from './MessageInputEmbed'
function MaybeLoader({isLoading}: {isLoading: boolean}) {
return (
@@ -87,8 +80,6 @@ export function MessagesList({
}) {
const convoState = useConvoActive()
const agent = useAgent()
const getPost = useGetPost()
const {embedUri, setEmbed} = useMessageEmbed()
const flatListRef = useAnimatedRef<FlatList>()
@@ -273,69 +264,20 @@ export function MessagesList({
// -- Message sending
const onSendMessage = useCallback(
async (text: string) => {
let rt = new RichText({text: text.trimEnd()}, {cleanNewlines: true})
// detect facets without resolution first - this is used to see if there's
// any post links in the text that we can embed. We do this first because
// we want to remove the post link from the text, re-trim, then detect facets
rt.detectFacetsWithoutResolution()
let embed: AppBskyEmbedRecord.Main | undefined
if (embedUri) {
try {
const post = await getPost({uri: embedUri})
if (post) {
embed = {
$type: 'app.bsky.embed.record',
record: {
uri: post.uri,
cid: post.cid,
},
}
// look for the embed uri in the facets, so we can remove it from the text
const postLinkFacet = rt.facets?.find(facet => {
return facet.features.find(feature => {
if (AppBskyRichtextFacet.isLink(feature)) {
if (isBskyPostUrl(feature.uri)) {
const url = convertBskyAppUrlIfNeeded(feature.uri)
const [_0, _1, _2, rkey] = url.split('/').filter(Boolean)
// this might have a handle instead of a DID
// so just compare the rkey - not particularly dangerous
return post.uri.endsWith(rkey)
}
}
return false
})
})
if (postLinkFacet) {
const isAtStart = postLinkFacet.index.byteStart === 0
const isAtEnd =
postLinkFacet.index.byteEnd === rt.unicodeText.graphemeLength
// remove the post link from the text
if (isAtStart || isAtEnd) {
rt.delete(
postLinkFacet.index.byteStart,
postLinkFacet.index.byteEnd,
)
}
rt = new RichText({text: rt.text.trim()}, {cleanNewlines: true})
}
}
} catch (error) {
logger.error('Failed to get post as quote for DM', {error})
}
}
let rt = new RichText({text}, {cleanNewlines: true})
await rt.detectFacets(agent)
rt = shortenLinks(rt)
rt = stripInvalidMentions(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
})
if (!hasScrolled) {
setHasScrolled(true)
@@ -344,10 +286,9 @@ export function MessagesList({
convoState.sendMessage({
text: rt.text,
facets: rt.facets,
embed,
})
},
[agent, convoState, embedUri, getPost, hasScrolled, setHasScrolled],
[convoState, agent, hasScrolled, setHasScrolled],
)
// -- List layout changes (opening emoji keyboard, etc.)
@@ -387,9 +328,6 @@ export function MessagesList({
renderItem={renderItem}
keyExtractor={keyExtractor}
containWeb={true}
// Prevents wrong position in Firefox when sending a message
// as well as scroll getting stuck on Chome when scrolling upwards.
disableContentVisibility={true}
disableVirtualization={true}
style={animatedListStyle}
// The extra two items account for the header and the footer components
@@ -422,12 +360,7 @@ export function MessagesList({
{isConvoActive(convoState) &&
!convoState.isFetchingHistory &&
convoState.items.length === 0 && <ChatEmptyPill />}
<MessageInput
onSendMessage={onSendMessage}
hasEmbed={!!embedUri}
setEmbed={setEmbed}>
<MessageInputEmbed embedUri={embedUri} setEmbed={setEmbed} />
</MessageInput>
<MessageInput onSendMessage={onSendMessage} />
</>
)}
</KeyboardStickyView>
+14 -70
View File
@@ -2,7 +2,6 @@ import React, {useCallback, useState} from 'react'
import {GestureResponderEvent, View} from 'react-native'
import {
AppBskyActorDefs,
AppBskyEmbedRecord,
ChatBskyConvoDefs,
moderateProfile,
ModerationOpts,
@@ -10,11 +9,6 @@ import {
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {
postUriToRelativePath,
toBskyAppUrl,
toShortUrl,
} from '#/lib/strings/url-helpers'
import {isNative} from '#/platform/detection'
import {useProfileShadow} from '#/state/cache/profile-shadow'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
@@ -29,7 +23,6 @@ import {ConvoMenu} from '#/components/dms/ConvoMenu'
import {Bell2Off_Filled_Corner0_Rounded as BellStroke} from '#/components/icons/Bell2'
import {Link} from '#/components/Link'
import {useMenuControl} from '#/components/Menu'
import {PostAlerts} from '#/components/moderation/PostAlerts'
import {Text} from '#/components/Typography'
export let ChatListItem = ({
@@ -101,64 +94,21 @@ function ChatListItemReady({
const isDimStyle = convo.muted || moderation.blocked || isDeletedAccount
const {lastMessage, lastMessageSentAt} = React.useMemo(() => {
let lastMessage = _(msg`No messages yet`)
let lastMessageSentAt: string | null = null
if (ChatBskyConvoDefs.isMessageView(convo.lastMessage)) {
const isFromMe = convo.lastMessage.sender?.did === currentAccount?.did
if (convo.lastMessage.text) {
if (isFromMe) {
lastMessage = _(msg`You: ${convo.lastMessage.text}`)
} else {
lastMessage = convo.lastMessage.text
}
} else if (convo.lastMessage.embed) {
const defaultEmbeddedContentMessage = _(
msg`(contains embedded content)`,
)
if (AppBskyEmbedRecord.isView(convo.lastMessage.embed)) {
const embed = convo.lastMessage.embed
if (AppBskyEmbedRecord.isViewRecord(embed.record)) {
const record = embed.record
const path = postUriToRelativePath(record.uri, {
handle: record.author.handle,
})
const href = path ? toBskyAppUrl(path) : undefined
const short = href
? toShortUrl(href)
: defaultEmbeddedContentMessage
if (isFromMe) {
lastMessage = _(msg`You: ${short}`)
} else {
lastMessage = short
}
}
} else {
if (isFromMe) {
lastMessage = _(msg`You: ${defaultEmbeddedContentMessage}`)
} else {
lastMessage = defaultEmbeddedContentMessage
}
}
}
lastMessageSentAt = convo.lastMessage.sentAt
let lastMessage = _(msg`No messages yet`)
let lastMessageSentAt: string | null = null
if (ChatBskyConvoDefs.isMessageView(convo.lastMessage)) {
if (convo.lastMessage.sender?.did === currentAccount?.did) {
lastMessage = _(msg`You: ${convo.lastMessage.text}`)
} else {
lastMessage = convo.lastMessage.text
}
if (ChatBskyConvoDefs.isDeletedMessageView(convo.lastMessage)) {
lastMessage = isDeletedAccount
? _(msg`Conversation deleted`)
: _(msg`Message deleted`)
}
return {
lastMessage,
lastMessageSentAt,
}
}, [_, convo.lastMessage, currentAccount?.did, isDeletedAccount])
lastMessageSentAt = convo.lastMessage.sentAt
}
if (ChatBskyConvoDefs.isDeletedMessageView(convo.lastMessage)) {
lastMessage = isDeletedAccount
? _(msg`Conversation deleted`)
: _(msg`Message deleted`)
}
const [showActions, setShowActions] = useState(false)
@@ -314,12 +264,6 @@ function ChatListItemReady({
]}>
{lastMessage}
</Text>
<PostAlerts
modui={moderation.ui('contentList')}
size="large"
style={[a.pt_xs]}
/>
</View>
{convo.unreadCount > 0 && (
+1 -1
View File
@@ -21,8 +21,8 @@ import {CenteredView} from '#/view/com/util/Views'
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {DialogControlProps, useDialogControl} from '#/components/Dialog'
import {NewChat} from '#/components/dms/dialogs/NewChatDialog'
import {MessagesNUX} from '#/components/dms/MessagesNUX'
import {NewChat} from '#/components/dms/NewChatDialog'
import {useRefreshOnFocus} from '#/components/hooks/useRefreshOnFocus'
import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as Retry} from '#/components/icons/ArrowRotateCounterClockwise'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
+1 -12
View File
@@ -13,7 +13,6 @@ import {RQKEY as profileRQKey} from '#/state/queries/profile'
import {useAgent} from '#/state/session'
import {useOnboardingDispatch} from '#/state/shell'
import {uploadBlob} from 'lib/api'
import {useRequestNotificationsPermission} from 'lib/notifications/notifications'
import {
DescriptionText,
OnboardingControls,
@@ -40,7 +39,6 @@ export function StepFinished() {
const [saving, setSaving] = React.useState(false)
const queryClient = useQueryClient()
const agent = useAgent()
const requestNotificationsPermission = useRequestNotificationsPermission()
const finishOnboarding = React.useCallback(async () => {
setSaving(true)
@@ -74,7 +72,6 @@ export function StepFinished() {
: 'default',
})
})(),
requestNotificationsPermission('AfterOnboarding'),
])
} catch (e: any) {
logger.info(`onboarding: bulk save failed`)
@@ -101,15 +98,7 @@ export function StepFinished() {
track('OnboardingV2:StepFinished:End')
track('OnboardingV2:Complete')
logEvent('onboarding:finished:nextPressed', {})
}, [
state,
queryClient,
agent,
dispatch,
onboardDispatch,
track,
requestNotificationsPermission,
])
}, [state, dispatch, onboardDispatch, setSaving, track, agent, queryClient])
React.useEffect(() => {
track('OnboardingV2:StepFinished:Start')
@@ -1,60 +0,0 @@
import React from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {atoms as a, useTheme} from '#/alf'
import {DialogOuterProps} from '#/components/Dialog'
import {Divider} from '#/components/Divider'
import * as Prompt from '#/components/Prompt'
import {Text} from '#/components/Typography'
export function DeactivateAccountDialog({
control,
}: {
control: DialogOuterProps['control']
}) {
const t = useTheme()
const {_} = useLingui()
return (
<Prompt.Outer control={control} testID="confirmModal">
<Prompt.TitleText>{_(msg`Deactivate account`)}</Prompt.TitleText>
<Prompt.DescriptionText>
<Trans>
Your profile, posts, feeds, and lists will no longer be visible to
other Bluesky users. You can reactivate your account at any time by
logging in.
</Trans>
</Prompt.DescriptionText>
<View style={[a.pb_xl]}>
<Divider />
<View style={[a.gap_sm, a.pt_lg, a.pb_xl]}>
<Text style={[t.atoms.text_contrast_medium, a.leading_snug]}>
<Trans>
There is no time limit for account deactivation, come back any
time.
</Trans>
</Text>
<Text style={[t.atoms.text_contrast_medium, a.leading_snug]}>
<Trans>
If you're trying to change your handle or email, do so before you
deactivate.
</Trans>
</Text>
</View>
<Divider />
</View>
<Prompt.Actions>
<Prompt.Action
cta={_(msg`Yes, deactivate`)}
onPress={() => {}}
color="negative"
/>
<Prompt.Cancel />
</Prompt.Actions>
</Prompt.Outer>
)
}
-4
View File
@@ -9,10 +9,8 @@ import {findAllProfilesInQueryData as findAllProfilesInListMembersQueryData} fro
import {findAllProfilesInQueryData as findAllProfilesInListConvosQueryData} from '../queries/messages/list-converations'
import {findAllProfilesInQueryData as findAllProfilesInMyBlockedAccountsQueryData} from '../queries/my-blocked-accounts'
import {findAllProfilesInQueryData as findAllProfilesInMyMutedAccountsQueryData} from '../queries/my-muted-accounts'
import {findAllProfilesInQueryData as findAllProfilesInFeedsQueryData} from '../queries/post-feed'
import {findAllProfilesInQueryData as findAllProfilesInPostLikedByQueryData} from '../queries/post-liked-by'
import {findAllProfilesInQueryData as findAllProfilesInPostRepostedByQueryData} from '../queries/post-reposted-by'
import {findAllProfilesInQueryData as findAllProfilesInPostThreadQueryData} from '../queries/post-thread'
import {findAllProfilesInQueryData as findAllProfilesInProfileQueryData} from '../queries/profile'
import {findAllProfilesInQueryData as findAllProfilesInProfileFollowersQueryData} from '../queries/profile-followers'
import {findAllProfilesInQueryData as findAllProfilesInProfileFollowsQueryData} from '../queries/profile-follows'
@@ -109,6 +107,4 @@ function* findProfilesInCache(
yield* findAllProfilesInSuggestedFollowsQueryData(queryClient, did)
yield* findAllProfilesInActorSearchQueryData(queryClient, did)
yield* findAllProfilesInListConvosQueryData(queryClient, did)
yield* findAllProfilesInFeedsQueryData(queryClient, did)
yield* findAllProfilesInPostThreadQueryData(queryClient, did)
}
+1 -2
View File
@@ -753,7 +753,7 @@ export class Convo {
sendMessage(message: ChatBskyConvoSendMessage.InputSchema['message']) {
// Ignore empty messages for now since they have no other purpose atm
if (!message.text.trim() && !message.embed) return
if (!message.text.trim()) return
logger.debug('Convo: send message', {}, logger.DebugContext.convo)
@@ -1018,7 +1018,6 @@ export class Convo {
key: m.id,
message: {
...m.message,
embed: undefined,
$type: 'chat.bsky.convo.defs#messageView',
id: nanoid(),
rev: '__fake__',
+8
View File
@@ -60,6 +60,13 @@ export interface DeleteAccountModal {
name: 'delete-account'
}
export interface RepostModal {
name: 'repost'
onRepost: () => void
onQuote: () => void
isReposted: boolean
}
export interface SelfLabelModal {
name: 'self-label'
labels: string[]
@@ -147,6 +154,7 @@ export type Modal =
| AltTextImageModal
| CropImageModal
| EditImageModal
| RepostModal
| SelfLabelModal
| ThreadgateModal
-1
View File
@@ -68,7 +68,6 @@ export const schema = z.object({
spotify: z.enum(externalEmbedOptions).optional(),
appleMusic: z.enum(externalEmbedOptions).optional(),
soundcloud: z.enum(externalEmbedOptions).optional(),
flickr: z.enum(externalEmbedOptions).optional(),
})
.optional(),
mutedThreads: z.array(z.string()), // should move to server
+2 -13
View File
@@ -1,7 +1,7 @@
import AsyncStorage from '@react-native-async-storage/async-storage'
import {logger} from '#/logger'
import {Schema, schema} from '#/state/persisted/schema'
import {logger} from '#/logger'
const BSKY_STORAGE = 'BSKY_STORAGE'
@@ -13,19 +13,8 @@ export async function write(value: Schema) {
export async function read(): Promise<Schema | undefined> {
const rawData = await AsyncStorage.getItem(BSKY_STORAGE)
const objData = rawData ? JSON.parse(rawData) : undefined
const parsed = schema.safeParse(objData)
if (parsed.success) {
if (schema.safeParse(objData).success) {
return objData
} else {
const errors =
parsed.error?.errors?.map(e => ({
code: e.code,
// @ts-ignore exists on some types
expected: e?.expected,
path: e.path,
})) || []
logger.error(`persisted store: data failed validation on read`, {errors})
return undefined
}
}
@@ -26,15 +26,10 @@ import {useAgent, useSession} from '#/state/session'
export const RQKEY = ['convo-list']
type RQPageParam = string | undefined
export function useListConvosQuery({
enabled,
}: {
enabled?: boolean
} = {}) {
export function useListConvosQuery() {
const agent = useAgent()
return useInfiniteQuery({
enabled,
queryKey: RQKEY,
queryFn: async ({pageParam}) => {
const {data} = await agent.api.chat.bsky.convo.listConvos(
+5 -38
View File
@@ -17,7 +17,7 @@
*/
import {useEffect, useRef} from 'react'
import {AppBskyActorDefs, AppBskyFeedDefs, AtUri} from '@atproto/api'
import {AppBskyFeedDefs} from '@atproto/api'
import {
InfiniteData,
QueryClient,
@@ -30,11 +30,7 @@ import {useMutedThreads} from '#/state/muted-threads'
import {useAgent} from '#/state/session'
import {useModerationOpts} from '../../preferences/moderation-opts'
import {STALE} from '..'
import {
didOrHandleUriMatches,
embedViewRecordToPostView,
getEmbeddedPost,
} from '../util'
import {embedViewRecordToPostView, getEmbeddedPost} from '../util'
import {FeedPage} from './types'
import {useUnreadNotificationsApi} from './unread'
import {fetchPage} from './util'
@@ -146,8 +142,6 @@ export function* findAllPostsInQueryData(
queryClient: QueryClient,
uri: string,
): Generator<AppBskyFeedDefs.PostView, void> {
const atUri = new AtUri(uri)
const queryDatas = queryClient.getQueriesData<InfiniteData<FeedPage>>({
queryKey: [RQKEY_ROOT],
})
@@ -155,41 +149,14 @@ export function* findAllPostsInQueryData(
if (!queryData?.pages) {
continue
}
for (const page of queryData?.pages) {
for (const item of page.items) {
if (item.subject && didOrHandleUriMatches(atUri, item.subject)) {
if (item.subject?.uri === uri) {
yield item.subject
}
const quotedPost = getEmbeddedPost(item.subject?.embed)
if (quotedPost && didOrHandleUriMatches(atUri, quotedPost)) {
yield embedViewRecordToPostView(quotedPost!)
}
}
}
}
}
export function* findAllProfilesInQueryData(
queryClient: QueryClient,
did: string,
): Generator<AppBskyActorDefs.ProfileView, void> {
const queryDatas = queryClient.getQueriesData<InfiniteData<FeedPage>>({
queryKey: [RQKEY_ROOT],
})
for (const [_queryKey, queryData] of queryDatas) {
if (!queryData?.pages) {
continue
}
for (const page of queryData?.pages) {
for (const item of page.items) {
if (item.subject?.author.did === did) {
yield item.subject.author
}
const quotedPost = getEmbeddedPost(item.subject?.embed)
if (quotedPost?.author.did === did) {
yield quotedPost.author
if (quotedPost?.uri === uri) {
yield embedViewRecordToPostView(quotedPost)
}
}
}
+1 -1
View File
@@ -145,7 +145,7 @@ async function fetchSubjects(
): Promise<Map<string, AppBskyFeedDefs.PostView>> {
const uris = new Set<string>()
for (const notif of groupedNotifs) {
if (notif.subjectUri?.includes('app.bsky.feed.post')) {
if (notif.subjectUri && !notif.subjectUri.includes('feed.generator')) {
uris.add(notif.subjectUri)
}
}
+7 -66
View File
@@ -35,11 +35,7 @@ import {KnownError} from '#/view/com/posts/FeedErrorMessage'
import {useFeedTuners} from '../preferences/feed-tuners'
import {useModerationOpts} from '../preferences/moderation-opts'
import {usePreferencesQuery} from './preferences'
import {
didOrHandleUriMatches,
embedViewRecordToPostView,
getEmbeddedPost,
} from './util'
import {embedViewRecordToPostView, getEmbeddedPost} from './util'
type ActorDid = string
type AuthorFilter =
@@ -452,8 +448,6 @@ export function* findAllPostsInQueryData(
queryClient: QueryClient,
uri: string,
): Generator<AppBskyFeedDefs.PostView, undefined> {
const atUri = new AtUri(uri)
const queryDatas = queryClient.getQueriesData<
InfiniteData<FeedPageUnselected>
>({
@@ -465,77 +459,24 @@ export function* findAllPostsInQueryData(
}
for (const page of queryData?.pages) {
for (const item of page.feed) {
if (didOrHandleUriMatches(atUri, item.post)) {
if (item.post.uri === uri) {
yield item.post
}
const quotedPost = getEmbeddedPost(item.post.embed)
if (quotedPost && didOrHandleUriMatches(atUri, quotedPost)) {
if (quotedPost?.uri === uri) {
yield embedViewRecordToPostView(quotedPost)
}
if (AppBskyFeedDefs.isPostView(item.reply?.parent)) {
if (didOrHandleUriMatches(atUri, item.reply.parent)) {
yield item.reply.parent
}
const parentQuotedPost = getEmbeddedPost(item.reply.parent.embed)
if (
parentQuotedPost &&
didOrHandleUriMatches(atUri, parentQuotedPost)
) {
yield embedViewRecordToPostView(parentQuotedPost)
}
}
if (AppBskyFeedDefs.isPostView(item.reply?.root)) {
if (didOrHandleUriMatches(atUri, item.reply.root)) {
yield item.reply.root
}
const rootQuotedPost = getEmbeddedPost(item.reply.root.embed)
if (rootQuotedPost && didOrHandleUriMatches(atUri, rootQuotedPost)) {
yield embedViewRecordToPostView(rootQuotedPost)
}
}
}
}
}
}
export function* findAllProfilesInQueryData(
queryClient: QueryClient,
did: string,
): Generator<AppBskyActorDefs.ProfileView, undefined> {
const queryDatas = queryClient.getQueriesData<
InfiniteData<FeedPageUnselected>
>({
queryKey: [RQKEY_ROOT],
})
for (const [_queryKey, queryData] of queryDatas) {
if (!queryData?.pages) {
continue
}
for (const page of queryData?.pages) {
for (const item of page.feed) {
if (item.post.author.did === did) {
yield item.post.author
}
const quotedPost = getEmbeddedPost(item.post.embed)
if (quotedPost?.author.did === did) {
yield quotedPost.author
}
if (
AppBskyFeedDefs.isPostView(item.reply?.parent) &&
item.reply?.parent?.author.did === did
item.reply?.parent?.uri === uri
) {
yield item.reply.parent.author
yield item.reply.parent
}
if (
AppBskyFeedDefs.isPostView(item.reply?.root) &&
item.reply?.root?.author.did === did
item.reply?.root?.uri === uri
) {
yield item.reply.root.author
yield item.reply.root
}
}
}
+14 -60
View File
@@ -1,10 +1,8 @@
import {
AppBskyActorDefs,
AppBskyEmbedRecord,
AppBskyFeedDefs,
AppBskyFeedGetPostThread,
AppBskyFeedPost,
AtUri,
ModerationDecision,
ModerationOpts,
} from '@atproto/api'
@@ -13,23 +11,10 @@ import {QueryClient, useQuery, useQueryClient} from '@tanstack/react-query'
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
import {useAgent} from '#/state/session'
import {
findAllPostsInQueryData as findAllPostsInSearchQueryData,
findAllProfilesInQueryData as findAllProfilesInSearchQueryData,
} from 'state/queries/search-posts'
import {
findAllPostsInQueryData as findAllPostsInNotifsQueryData,
findAllProfilesInQueryData as findAllProfilesInNotifsQueryData,
} from './notifications/feed'
import {
findAllPostsInQueryData as findAllPostsInFeedQueryData,
findAllProfilesInQueryData as findAllProfilesInFeedQueryData,
} from './post-feed'
import {
didOrHandleUriMatches,
embedViewRecordToPostView,
getEmbeddedPost,
} from './util'
import {findAllPostsInQueryData as findAllPostsInSearchQueryData} from 'state/queries/search-posts'
import {findAllPostsInQueryData as findAllPostsInNotifsQueryData} from './notifications/feed'
import {findAllPostsInQueryData as findAllPostsInFeedQueryData} from './post-feed'
import {embedViewRecordToPostView, getEmbeddedPost} from './util'
const RQKEY_ROOT = 'post-thread'
export const RQKEY = (uri: string) => [RQKEY_ROOT, uri]
@@ -96,10 +81,14 @@ export function usePostThreadQuery(uri: string | undefined) {
},
enabled: !!uri,
placeholderData: () => {
if (!uri) return
const post = findPostInQueryData(queryClient, uri)
if (post) {
return post
if (!uri) {
return undefined
}
{
const post = findPostInQueryData(queryClient, uri)
if (post) {
return post
}
}
return undefined
},
@@ -272,8 +261,6 @@ export function* findAllPostsInQueryData(
queryClient: QueryClient,
uri: string,
): Generator<ThreadNode, void> {
const atUri = new AtUri(uri)
const queryDatas = queryClient.getQueriesData<ThreadNode>({
queryKey: [RQKEY_ROOT],
})
@@ -282,7 +269,7 @@ export function* findAllPostsInQueryData(
continue
}
for (const item of traverseThread(queryData)) {
if (item.type === 'post' && didOrHandleUriMatches(atUri, item.post)) {
if (item.uri === uri) {
const placeholder = threadNodeToPlaceholderThread(item)
if (placeholder) {
yield placeholder
@@ -290,7 +277,7 @@ export function* findAllPostsInQueryData(
}
const quotedPost =
item.type === 'post' ? getEmbeddedPost(item.post.embed) : undefined
if (quotedPost && didOrHandleUriMatches(atUri, quotedPost)) {
if (quotedPost?.uri === uri) {
yield embedViewRecordToPlaceholderThread(quotedPost)
}
}
@@ -306,39 +293,6 @@ export function* findAllPostsInQueryData(
}
}
export function* findAllProfilesInQueryData(
queryClient: QueryClient,
did: string,
): Generator<AppBskyActorDefs.ProfileView, void> {
const queryDatas = queryClient.getQueriesData<ThreadNode>({
queryKey: [RQKEY_ROOT],
})
for (const [_queryKey, queryData] of queryDatas) {
if (!queryData) {
continue
}
for (const item of traverseThread(queryData)) {
if (item.type === 'post' && item.post.author.did === did) {
yield item.post.author
}
const quotedPost =
item.type === 'post' ? getEmbeddedPost(item.post.embed) : undefined
if (quotedPost?.author.did === did) {
yield quotedPost?.author
}
}
}
for (let profile of findAllProfilesInFeedQueryData(queryClient, did)) {
yield profile
}
for (let profile of findAllProfilesInNotifsQueryData(queryClient, did)) {
yield profile
}
for (let profile of findAllProfilesInSearchQueryData(queryClient, did)) {
yield profile
}
}
function* traverseThread(node: ThreadNode): Generator<ThreadNode, void> {
if (node.type === 'post') {
if (node.parent) {
+2 -11
View File
@@ -18,16 +18,7 @@ export function usePostQuery(uri: string | undefined) {
return useQuery<AppBskyFeedDefs.PostView>({
queryKey: RQKEY(uri || ''),
async queryFn() {
const urip = new AtUri(uri!)
if (!urip.host.startsWith('did:')) {
const res = await agent.resolveHandle({
handle: urip.host,
})
urip.host = res.data.did
}
const res = await agent.getPosts({uris: [urip.toString()]})
const res = await agent.getPosts({uris: [uri!]})
if (res.success && res.data.posts[0]) {
return res.data.posts[0]
}
@@ -56,7 +47,7 @@ export function useGetPost() {
}
const res = await agent.getPosts({
uris: [urip.toString()],
uris: [urip.toString()!],
})
if (res.success && res.data.posts[0]) {
+4 -43
View File
@@ -1,9 +1,4 @@
import {
AppBskyActorDefs,
AppBskyFeedDefs,
AppBskyFeedSearchPosts,
AtUri,
} from '@atproto/api'
import {AppBskyFeedDefs, AppBskyFeedSearchPosts} from '@atproto/api'
import {
InfiniteData,
QueryClient,
@@ -12,11 +7,7 @@ import {
} from '@tanstack/react-query'
import {useAgent} from '#/state/session'
import {
didOrHandleUriMatches,
embedViewRecordToPostView,
getEmbeddedPost,
} from './util'
import {embedViewRecordToPostView, getEmbeddedPost} from './util'
const searchPostsQueryKeyRoot = 'search-posts'
const searchPostsQueryKey = ({query, sort}: {query: string; sort?: string}) => [
@@ -67,50 +58,20 @@ export function* findAllPostsInQueryData(
>({
queryKey: [searchPostsQueryKeyRoot],
})
const atUri = new AtUri(uri)
for (const [_queryKey, queryData] of queryDatas) {
if (!queryData?.pages) {
continue
}
for (const page of queryData?.pages) {
for (const post of page.posts) {
if (didOrHandleUriMatches(atUri, post)) {
if (post.uri === uri) {
yield post
}
const quotedPost = getEmbeddedPost(post.embed)
if (quotedPost && didOrHandleUriMatches(atUri, quotedPost)) {
if (quotedPost?.uri === uri) {
yield embedViewRecordToPostView(quotedPost)
}
}
}
}
}
export function* findAllProfilesInQueryData(
queryClient: QueryClient,
did: string,
): Generator<AppBskyActorDefs.ProfileView, undefined> {
const queryDatas = queryClient.getQueriesData<
InfiniteData<AppBskyFeedSearchPosts.OutputSchema>
>({
queryKey: [searchPostsQueryKeyRoot],
})
for (const [_queryKey, queryData] of queryDatas) {
if (!queryData?.pages) {
continue
}
for (const page of queryData?.pages) {
for (const post of page.posts) {
if (post.author.did === did) {
yield post.author
}
const quotedPost = getEmbeddedPost(post.embed)
if (quotedPost?.author.did === did) {
yield quotedPost.author
}
}
}
}
}
-19
View File
@@ -1,10 +1,8 @@
import {
AppBskyActorDefs,
AppBskyEmbedRecord,
AppBskyEmbedRecordWithMedia,
AppBskyFeedDefs,
AppBskyFeedPost,
AtUri,
} from '@atproto/api'
import {InfiniteData, QueryClient, QueryKey} from '@tanstack/react-query'
@@ -24,23 +22,6 @@ export function truncateAndInvalidate<T = any>(
queryClient.invalidateQueries({queryKey})
}
// Given an AtUri, this function will check if the AtUri matches a
// hit regardless of whether the AtUri uses a DID or handle as a host.
//
// AtUri should be the URI that is being searched for, while currentUri
// is the URI that is being checked. currentAuthor is the author
// of the currentUri that is being checked.
export function didOrHandleUriMatches(
atUri: AtUri,
record: {uri: string; author: AppBskyActorDefs.ProfileViewBasic},
) {
if (atUri.host.startsWith('did:')) {
return atUri.href === record.uri
}
return atUri.host === record.author.handle && record.uri.endsWith(atUri.rkey)
}
export function getEmbeddedPost(
v: unknown,
): AppBskyEmbedRecord.ViewRecord | undefined {
+29 -29
View File
@@ -59,7 +59,7 @@ describe('session', () => {
"refreshJwt": "alice-refresh-jwt-1",
"service": "https://alice.com/",
"signupQueued": false,
"status": undefined,
"status": "active",
},
],
"currentAgentState": {
@@ -97,7 +97,7 @@ describe('session', () => {
"refreshJwt": undefined,
"service": "https://alice.com/",
"signupQueued": false,
"status": undefined,
"status": "active",
},
],
"currentAgentState": {
@@ -147,7 +147,7 @@ describe('session', () => {
"refreshJwt": "alice-refresh-jwt-1",
"service": "https://alice.com/",
"signupQueued": false,
"status": undefined,
"status": "active",
},
],
"currentAgentState": {
@@ -195,7 +195,7 @@ describe('session', () => {
"refreshJwt": "bob-refresh-jwt-1",
"service": "https://bob.com/",
"signupQueued": false,
"status": undefined,
"status": "active",
},
{
"accessJwt": "alice-access-jwt-1",
@@ -208,7 +208,7 @@ describe('session', () => {
"refreshJwt": "alice-refresh-jwt-1",
"service": "https://alice.com/",
"signupQueued": false,
"status": undefined,
"status": "active",
},
],
"currentAgentState": {
@@ -256,7 +256,7 @@ describe('session', () => {
"refreshJwt": "alice-refresh-jwt-2",
"service": "https://alice.com/",
"signupQueued": false,
"status": undefined,
"status": "active",
},
{
"accessJwt": "bob-access-jwt-1",
@@ -269,7 +269,7 @@ describe('session', () => {
"refreshJwt": "bob-refresh-jwt-1",
"service": "https://bob.com/",
"signupQueued": false,
"status": undefined,
"status": "active",
},
],
"currentAgentState": {
@@ -315,7 +315,7 @@ describe('session', () => {
"refreshJwt": "jay-refresh-jwt-1",
"service": "https://jay.com/",
"signupQueued": false,
"status": undefined,
"status": "active",
},
{
"accessJwt": "alice-access-jwt-2",
@@ -328,7 +328,7 @@ describe('session', () => {
"refreshJwt": "alice-refresh-jwt-2",
"service": "https://alice.com/",
"signupQueued": false,
"status": undefined,
"status": "active",
},
{
"accessJwt": "bob-access-jwt-1",
@@ -341,7 +341,7 @@ describe('session', () => {
"refreshJwt": "bob-refresh-jwt-1",
"service": "https://bob.com/",
"signupQueued": false,
"status": undefined,
"status": "active",
},
],
"currentAgentState": {
@@ -383,7 +383,7 @@ describe('session', () => {
"refreshJwt": undefined,
"service": "https://jay.com/",
"signupQueued": false,
"status": undefined,
"status": "active",
},
{
"accessJwt": undefined,
@@ -396,7 +396,7 @@ describe('session', () => {
"refreshJwt": undefined,
"service": "https://alice.com/",
"signupQueued": false,
"status": undefined,
"status": "active",
},
{
"accessJwt": undefined,
@@ -409,7 +409,7 @@ describe('session', () => {
"refreshJwt": undefined,
"service": "https://bob.com/",
"signupQueued": false,
"status": undefined,
"status": "active",
},
],
"currentAgentState": {
@@ -468,7 +468,7 @@ describe('session', () => {
"refreshJwt": undefined,
"service": "https://alice.com/",
"signupQueued": false,
"status": undefined,
"status": "active",
},
],
"currentAgentState": {
@@ -513,7 +513,7 @@ describe('session', () => {
"refreshJwt": "alice-refresh-jwt-2",
"service": "https://alice.com/",
"signupQueued": false,
"status": undefined,
"status": "active",
},
],
"currentAgentState": {
@@ -625,7 +625,7 @@ describe('session', () => {
"refreshJwt": "bob-refresh-jwt-1",
"service": "https://bob.com/",
"signupQueued": false,
"status": undefined,
"status": "active",
},
],
"currentAgentState": {
@@ -706,7 +706,7 @@ describe('session', () => {
"refreshJwt": "alice-refresh-jwt-2",
"service": "https://alice.com/",
"signupQueued": false,
"status": undefined,
"status": "active",
},
],
"currentAgentState": {
@@ -757,7 +757,7 @@ describe('session', () => {
"refreshJwt": "alice-refresh-jwt-3",
"service": "https://alice.com/",
"signupQueued": false,
"status": undefined,
"status": "active",
},
],
"currentAgentState": {
@@ -808,7 +808,7 @@ describe('session', () => {
"refreshJwt": "alice-refresh-jwt-4",
"service": "https://alice.com/",
"signupQueued": false,
"status": undefined,
"status": "active",
},
],
"currentAgentState": {
@@ -965,7 +965,7 @@ describe('session', () => {
"refreshJwt": "bob-refresh-jwt-1",
"service": "https://bob.com/",
"signupQueued": false,
"status": undefined,
"status": "active",
},
{
"accessJwt": "alice-access-jwt-2",
@@ -978,7 +978,7 @@ describe('session', () => {
"refreshJwt": "alice-refresh-jwt-2",
"service": "https://alice.com/",
"signupQueued": false,
"status": undefined,
"status": "active",
},
],
"currentAgentState": {
@@ -1027,7 +1027,7 @@ describe('session', () => {
"refreshJwt": "bob-refresh-jwt-2",
"service": "https://bob.com/",
"signupQueued": false,
"status": undefined,
"status": "active",
},
{
"accessJwt": "alice-access-jwt-2",
@@ -1040,7 +1040,7 @@ describe('session', () => {
"refreshJwt": "alice-refresh-jwt-2",
"service": "https://alice.com/",
"signupQueued": false,
"status": undefined,
"status": "active",
},
],
"currentAgentState": {
@@ -1188,7 +1188,7 @@ describe('session', () => {
"refreshJwt": "alice-refresh-jwt-1",
"service": "https://alice.com/",
"signupQueued": false,
"status": undefined,
"status": "active",
},
],
"currentAgentState": {
@@ -1251,7 +1251,7 @@ describe('session', () => {
"refreshJwt": undefined,
"service": "https://alice.com/",
"signupQueued": false,
"status": undefined,
"status": "active",
},
],
"currentAgentState": {
@@ -1314,7 +1314,7 @@ describe('session', () => {
"refreshJwt": undefined,
"service": "https://alice.com/",
"signupQueued": false,
"status": undefined,
"status": "active",
},
],
"currentAgentState": {
@@ -1406,7 +1406,7 @@ describe('session', () => {
"refreshJwt": "jay-refresh-jwt-1",
"service": "https://jay.com/",
"signupQueued": false,
"status": undefined,
"status": "active",
},
{
"accessJwt": "bob-access-jwt-2",
@@ -1419,7 +1419,7 @@ describe('session', () => {
"refreshJwt": "bob-refresh-jwt-2",
"service": "https://alice.com/",
"signupQueued": false,
"status": undefined,
"status": "active",
},
],
"currentAgentState": {
@@ -1466,7 +1466,7 @@ describe('session', () => {
"refreshJwt": "clarence-refresh-jwt-2",
"service": "https://clarence.com/",
"signupQueued": false,
"status": undefined,
"status": "active",
},
],
"currentAgentState": {
+1 -1
View File
@@ -236,7 +236,7 @@ export function agentToSessionAccount(
accessJwt: agent.session.accessJwt,
signupQueued: isSignupQueued(agent.session.accessJwt),
// @ts-expect-error TODO remove when backend is ready
status: agent.session.status,
status: agent.session.status || 'active',
pdsUrl: agent.pdsUrl?.toString(),
}
}
+149 -276
View File
@@ -1,15 +1,9 @@
import React, {
useCallback,
useEffect,
useImperativeHandle,
useMemo,
useRef,
useState,
} from 'react'
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'
import {
ActivityIndicator,
BackHandler,
Keyboard,
LayoutChangeEvent,
ScrollView,
StyleSheet,
TouchableOpacity,
View,
@@ -18,12 +12,6 @@ import {
KeyboardAvoidingView,
KeyboardStickyView,
} from 'react-native-keyboard-controller'
import Animated, {
interpolateColor,
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {LinearGradient} from 'expo-linear-gradient'
import {RichText} from '@atproto/api'
@@ -36,7 +24,6 @@ import {
createGIFDescription,
parseAltFromGIFDescription,
} from '#/lib/gif-alt-text'
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
import {LikelyType} from '#/lib/link-meta/link-meta'
import {logEvent} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
@@ -55,7 +42,7 @@ import {useAgent, useSession} from '#/state/session'
import {useComposerControls} from '#/state/shell/composer'
import {useAnalytics} from 'lib/analytics/analytics'
import * as apilib from 'lib/api/index'
import {HITSLOP_10, MAX_GRAPHEME_LENGTH} from 'lib/constants'
import {MAX_GRAPHEME_LENGTH} from 'lib/constants'
import {useIsKeyboardVisible} from 'lib/hooks/useIsKeyboardVisible'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
@@ -68,7 +55,7 @@ import {useDialogStateControlContext} from 'state/dialogs'
import {GalleryModel} from 'state/models/media/gallery'
import {ComposerOpts} from 'state/shell/composer'
import {ComposerReplyTo} from 'view/com/composer/ComposerReplyTo'
import {atoms as a, useTheme} from '#/alf'
import {atoms as a} from '#/alf'
import {Button} from '#/components/Button'
import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji'
import * as Prompt from '#/components/Prompt'
@@ -91,11 +78,6 @@ import {SuggestedLanguage} from './select-language/SuggestedLanguage'
import {TextInput, TextInputRef} from './text-input/TextInput'
import {ThreadgateBtn} from './threadgate/ThreadgateBtn'
import {useExternalLinkFetch} from './useExternalLinkFetch'
import hairlineWidth = StyleSheet.hairlineWidth
type CancelRef = {
onPressCancel: () => void
}
type Props = ComposerOpts
export const ComposePost = observer(function ComposePost({
@@ -106,10 +88,7 @@ export const ComposePost = observer(function ComposePost({
openPicker,
text: initText,
imageUris: initImageUris,
cancelRef,
}: Props & {
cancelRef?: React.RefObject<CancelRef>
}) {
}: Props) {
const {currentAccount} = useSession()
const agent = useAgent()
const {data: currentProfile} = useProfileQuery({did: currentAccount!.did})
@@ -117,7 +96,7 @@ export const ComposePost = observer(function ComposePost({
const {closeComposer} = useComposerControls()
const {track} = useAnalytics()
const pal = usePalette('default')
const {isMobile} = useWebMediaQueries()
const {isDesktop, isMobile} = useWebMediaQueries()
const {_} = useLingui()
const requireAltTextEnabled = useRequireAltTextEnabled()
const langPrefs = useLanguagePrefs()
@@ -125,7 +104,6 @@ export const ComposePost = observer(function ComposePost({
const textInput = useRef<TextInputRef>(null)
const discardPromptControl = Prompt.usePromptControl()
const {closeAllDialogs} = useDialogStateControlContext()
const t = useTheme()
const [isKeyboardVisible] = useIsKeyboardVisible({iosUseWillEvents: true})
const [isProcessing, setIsProcessing] = useState(false)
@@ -167,12 +145,13 @@ export const ComposePost = observer(function ComposePost({
() => ({
paddingBottom:
isAndroid || (isIOS && !isKeyboardVisible) ? insets.bottom : 0,
paddingTop: isAndroid ? insets.top : isMobile ? 15 : 0,
}),
[insets, isKeyboardVisible],
[insets, isKeyboardVisible, isMobile],
)
const onPressCancel = useCallback(() => {
if (graphemeLength > 0 || !gallery.isEmpty || extGif) {
if (graphemeLength > 0 || !gallery.isEmpty) {
closeAllDialogs()
if (Keyboard) {
Keyboard.dismiss()
@@ -182,15 +161,29 @@ export const ComposePost = observer(function ComposePost({
onClose()
}
}, [
extGif,
graphemeLength,
gallery.isEmpty,
closeAllDialogs,
discardPromptControl,
onClose,
])
// android back button
useEffect(() => {
if (!isAndroid) {
return
}
const backHandler = BackHandler.addEventListener(
'hardwareBackPress',
() => {
onPressCancel()
return true
},
)
useImperativeHandle(cancelRef, () => ({onPressCancel}))
return () => {
backHandler.remove()
}
}, [onPressCancel])
// listen to escape key on desktop web
const onEscape = useCallback(
@@ -381,130 +374,113 @@ export const ComposePost = observer(function ComposePost({
[setExtLink],
)
const {
scrollHandler,
onScrollViewContentSizeChange,
onScrollViewLayout,
topBarAnimatedStyle,
bottomBarAnimatedStyle,
} = useAnimatedBorders()
return (
<>
<KeyboardAvoidingView
testID="composePostView"
behavior="padding"
style={a.flex_1}
keyboardVerticalOffset={replyTo ? 115 : isAndroid ? 180 : 162}>
<View
style={[a.flex_1, viewStyles]}
aria-modal
accessibilityViewIsModal>
<Animated.View style={topBarAnimatedStyle}>
<View style={styles.topbarInner}>
<TouchableOpacity
testID="composerDiscardButton"
onPress={onPressCancel}
onAccessibilityEscape={onPressCancel}
accessibilityRole="button"
accessibilityLabel={_(msg`Cancel`)}
accessibilityHint={_(
msg`Closes post composer and discards post draft`,
)}
hitSlop={HITSLOP_10}>
<Text style={[pal.link, s.f18]}>
<Trans>Cancel</Trans>
</Text>
</TouchableOpacity>
<View style={a.flex_1} />
{isProcessing ? (
<>
<Text style={pal.textLight}>{processingState}</Text>
<View style={styles.postBtn}>
<ActivityIndicator />
</View>
</>
) : (
<>
<LabelsBtn
labels={labels}
onChange={setLabels}
hasMedia={hasMedia}
/>
{canPost ? (
<TouchableOpacity
testID="composerPublishBtn"
onPress={onPressPublish}
accessibilityRole="button"
accessibilityLabel={
replyTo ? _(msg`Publish reply`) : _(msg`Publish post`)
}
accessibilityHint="">
<LinearGradient
colors={[
gradients.blueLight.start,
gradients.blueLight.end,
]}
start={{x: 0, y: 0}}
end={{x: 1, y: 1}}
style={styles.postBtn}>
<Text style={[s.white, s.f16, s.bold]}>
{replyTo ? (
<Trans context="action">Reply</Trans>
) : (
<Trans context="action">Post</Trans>
)}
</Text>
</LinearGradient>
</TouchableOpacity>
) : (
<View style={[styles.postBtn, pal.btn]}>
<Text style={[pal.textLight, s.f16, s.bold]}>
<Trans context="action">Post</Trans>
style={s.flex1}
keyboardVerticalOffset={replyTo ? 60 : isAndroid ? 120 : 100}>
<View style={[s.flex1, viewStyles]} aria-modal accessibilityViewIsModal>
<View style={[styles.topbar, isDesktop && styles.topbarDesktop]}>
<TouchableOpacity
testID="composerDiscardButton"
onPress={onPressCancel}
onAccessibilityEscape={onPressCancel}
accessibilityRole="button"
accessibilityLabel={_(msg`Cancel`)}
accessibilityHint={_(
msg`Closes post composer and discards post draft`,
)}>
<Text style={[pal.link, s.f18]}>
<Trans>Cancel</Trans>
</Text>
</TouchableOpacity>
<View style={s.flex1} />
{isProcessing ? (
<>
<Text style={pal.textLight}>{processingState}</Text>
<View style={styles.postBtn}>
<ActivityIndicator />
</View>
</>
) : (
<>
<LabelsBtn
labels={labels}
onChange={setLabels}
hasMedia={hasMedia}
/>
{canPost ? (
<TouchableOpacity
testID="composerPublishBtn"
onPress={onPressPublish}
accessibilityRole="button"
accessibilityLabel={
replyTo ? _(msg`Publish reply`) : _(msg`Publish post`)
}
accessibilityHint="">
<LinearGradient
colors={[
gradients.blueLight.start,
gradients.blueLight.end,
]}
start={{x: 0, y: 0}}
end={{x: 1, y: 1}}
style={styles.postBtn}>
<Text style={[s.white, s.f16, s.bold]}>
{replyTo ? (
<Trans context="action">Reply</Trans>
) : (
<Trans context="action">Post</Trans>
)}
</Text>
</View>
)}
</>
)}
</LinearGradient>
</TouchableOpacity>
) : (
<View style={[styles.postBtn, pal.btn]}>
<Text style={[pal.textLight, s.f16, s.bold]}>
<Trans context="action">Post</Trans>
</Text>
</View>
)}
</>
)}
</View>
{isAltTextRequiredAndMissing && (
<View style={[styles.reminderLine, pal.viewLight]}>
<View style={styles.errorIcon}>
<FontAwesomeIcon
icon="exclamation"
style={{color: colors.red4}}
size={10}
/>
</View>
<Text style={[pal.text, s.flex1]}>
<Trans>One or more images is missing alt text.</Trans>
</Text>
</View>
{isAltTextRequiredAndMissing && (
<View style={[styles.reminderLine, pal.viewLight]}>
<View style={styles.errorIcon}>
<FontAwesomeIcon
icon="exclamation"
style={{color: colors.red4}}
size={10}
/>
</View>
<Text style={[pal.text, a.flex_1]}>
<Trans>One or more images is missing alt text.</Trans>
</Text>
)}
{error !== '' && (
<View style={styles.errorLine}>
<View style={styles.errorIcon}>
<FontAwesomeIcon
icon="exclamation"
style={{color: colors.red4}}
size={10}
/>
</View>
)}
{error !== '' && (
<View style={styles.errorLine}>
<View style={styles.errorIcon}>
<FontAwesomeIcon
icon="exclamation"
style={{color: colors.red4}}
size={10}
/>
</View>
<Text style={[s.red4, a.flex_1]}>{error}</Text>
</View>
)}
</Animated.View>
<Animated.ScrollView
onScroll={scrollHandler}
<Text style={[s.red4, s.flex1]}>{error}</Text>
</View>
)}
<ScrollView
style={styles.scrollView}
keyboardShouldPersistTaps="always"
onContentSizeChange={onScrollViewContentSizeChange}
onLayout={onScrollViewLayout}>
keyboardShouldPersistTaps="always">
{replyTo ? <ComposerReplyTo replyTo={replyTo} /> : undefined}
<View
style={[
pal.border,
styles.textInputLayout,
isNative && styles.textInputLayoutMobile,
]}>
@@ -559,25 +535,16 @@ export const ComposePost = observer(function ComposePost({
)}
</View>
) : undefined}
</Animated.ScrollView>
</ScrollView>
<SuggestedLanguage text={richtext.text} />
</View>
</KeyboardAvoidingView>
<KeyboardStickyView
offset={{closed: isIOS ? -insets.bottom : 0, opened: 0}}>
{replyTo ? null : (
<ThreadgateBtn
threadgate={threadgate}
onChange={setThreadgate}
style={bottomBarAnimatedStyle}
/>
<ThreadgateBtn threadgate={threadgate} onChange={setThreadgate} />
)}
<View
style={[
t.atoms.bg,
t.atoms.border_contrast_medium,
styles.bottomBar,
]}>
<View style={[pal.border, styles.bottomBar]}>
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
<SelectPhotoBtn gallery={gallery} disabled={!canSelectImages} />
<OpenCameraBtn gallery={gallery} disabled={!canSelectImages} />
@@ -599,7 +566,7 @@ export const ComposePost = observer(function ComposePost({
</Button>
) : null}
</View>
<View style={a.flex_1} />
<View style={s.flex1} />
<SelectLangBtn />
<CharProgress count={graphemeLength} />
</View>
@@ -616,119 +583,25 @@ export const ComposePost = observer(function ComposePost({
)
})
export function useComposerCancelRef() {
return useRef<CancelRef>(null)
}
function useAnimatedBorders() {
const t = useTheme()
const hasScrolledTop = useSharedValue(0)
const hasScrolledBottom = useSharedValue(0)
const contentOffset = useSharedValue(0)
const scrollViewHeight = useSharedValue(Infinity)
const contentHeight = useSharedValue(0)
/**
* Make sure to run this on the UI thread!
*/
const showHideBottomBorder = useCallback(
({
newContentHeight,
newContentOffset,
newScrollViewHeight,
}: {
newContentHeight?: number
newContentOffset?: number
newScrollViewHeight?: number
}) => {
'worklet'
if (typeof newContentHeight === 'number')
contentHeight.value = Math.floor(newContentHeight)
if (typeof newContentOffset === 'number')
contentOffset.value = Math.floor(newContentOffset)
if (typeof newScrollViewHeight === 'number')
scrollViewHeight.value = Math.floor(newScrollViewHeight)
hasScrolledBottom.value = withTiming(
contentHeight.value - contentOffset.value - 5 > scrollViewHeight.value
? 1
: 0,
)
},
[contentHeight, contentOffset, scrollViewHeight, hasScrolledBottom],
)
const scrollHandler = useAnimatedScrollHandler({
onScroll: event => {
'worklet'
hasScrolledTop.value = withTiming(event.contentOffset.y > 0 ? 1 : 0)
showHideBottomBorder({
newContentOffset: event.contentOffset.y,
newContentHeight: event.contentSize.height,
newScrollViewHeight: event.layoutMeasurement.height,
})
},
})
const onScrollViewContentSizeChange = useCallback(
(_width: number, height: number) => {
'worklet'
showHideBottomBorder({
newContentHeight: height,
})
},
[showHideBottomBorder],
)
const onScrollViewLayout = useCallback(
(evt: LayoutChangeEvent) => {
'worklet'
showHideBottomBorder({
newScrollViewHeight: evt.nativeEvent.layout.height,
})
},
[showHideBottomBorder],
)
const topBarAnimatedStyle = useAnimatedStyle(() => {
return {
borderBottomWidth: hairlineWidth,
borderColor: interpolateColor(
hasScrolledTop.value,
[0, 1],
['transparent', t.atoms.border_contrast_medium.borderColor],
),
}
})
const bottomBarAnimatedStyle = useAnimatedStyle(() => {
return {
borderTopWidth: hairlineWidth,
borderColor: interpolateColor(
hasScrolledBottom.value,
[0, 1],
['transparent', t.atoms.border_contrast_medium.borderColor],
),
}
})
return {
scrollHandler,
onScrollViewContentSizeChange,
onScrollViewLayout,
topBarAnimatedStyle,
bottomBarAnimatedStyle,
}
}
const styles = StyleSheet.create({
topbarInner: {
outer: {
flexDirection: 'column',
flex: 1,
height: '100%',
},
topbar: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 16,
height: 54,
paddingTop: 6,
paddingBottom: 4,
paddingHorizontal: 20,
height: 55,
gap: 4,
},
topbarDesktop: {
paddingTop: 10,
paddingBottom: 10,
},
postBtn: {
borderRadius: 20,
paddingHorizontal: 20,
@@ -739,22 +612,22 @@ const styles = StyleSheet.create({
flexDirection: 'row',
backgroundColor: colors.red1,
borderRadius: 6,
marginHorizontal: 16,
marginHorizontal: 15,
paddingHorizontal: 8,
paddingVertical: 6,
marginBottom: 8,
marginVertical: 6,
},
reminderLine: {
flexDirection: 'row',
alignItems: 'center',
borderRadius: 6,
marginHorizontal: 16,
marginHorizontal: 15,
paddingHorizontal: 8,
paddingVertical: 6,
marginBottom: 8,
marginBottom: 6,
},
errorIcon: {
borderWidth: hairlineWidth,
borderWidth: 1,
borderColor: colors.red4,
color: colors.red4,
borderRadius: 30,
@@ -766,11 +639,12 @@ const styles = StyleSheet.create({
},
scrollView: {
flex: 1,
paddingHorizontal: 16,
paddingHorizontal: 15,
},
textInputLayout: {
flexDirection: 'row',
paddingTop: 4,
borderTopWidth: 1,
paddingTop: 16,
},
textInputLayoutMobile: {
flex: 1,
@@ -786,10 +660,9 @@ const styles = StyleSheet.create({
bottomBar: {
flexDirection: 'row',
paddingVertical: 4,
// should be 8 but due to visual alignment we have to fudge it
paddingLeft: 7,
paddingRight: 16,
paddingLeft: 15,
paddingRight: 20,
alignItems: 'center',
borderTopWidth: hairlineWidth,
borderTopWidth: 1,
},
})
+7 -8
View File
@@ -10,16 +10,16 @@ import {
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {usePalette} from 'lib/hooks/usePalette'
import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles'
import {ComposerOptsPostRef} from 'state/shell/composer'
import {QuoteEmbed} from 'view/com/util/post-embeds/QuoteEmbed'
import {Text} from 'view/com/util/text/Text'
import {PreviewableUserAvatar} from 'view/com/util/UserAvatar'
import {useTheme} from '#/alf'
export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
const t = useTheme()
const pal = usePalette('default')
const {_} = useLingui()
const {embed} = replyTo
@@ -75,7 +75,7 @@ export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
return (
<Pressable
style={[t.atoms.border_contrast_medium, styles.replyToLayout]}
style={[pal.border, styles.replyToLayout]}
onPress={onPress}
accessibilityRole="button"
accessibilityLabel={_(
@@ -91,7 +91,7 @@ export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
type={replyTo.author.associated?.labeler ? 'labeler' : 'user'}
/>
<View style={styles.replyToPost}>
<Text type="xl-medium" style={t.atoms.text}>
<Text type="xl-medium" style={[pal.text]}>
{sanitizeDisplayName(
replyTo.author.displayName || sanitizeHandle(replyTo.author.handle),
)}
@@ -100,7 +100,7 @@ export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
<View style={styles.replyToText}>
<Text
type="post-text"
style={t.atoms.text}
style={pal.text}
numberOfLines={!showFull ? 6 : undefined}>
{replyTo.text}
</Text>
@@ -218,10 +218,9 @@ const styles = StyleSheet.create({
replyToLayout: {
flexDirection: 'row',
alignItems: 'flex-start',
borderBottomWidth: StyleSheet.hairlineWidth,
paddingTop: 4,
borderTopWidth: 1,
paddingTop: 16,
paddingBottom: 16,
marginBottom: 12,
},
replyToPost: {
flex: 1,
@@ -1,34 +0,0 @@
import React from 'react'
import {View} from 'react-native'
import {KeyboardStickyView} from 'react-native-keyboard-controller'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {isWeb} from '#/platform/detection'
import {atoms as a, useTheme} from '#/alf'
export function KeyboardAccessory({children}: {children: React.ReactNode}) {
const t = useTheme()
const {bottom} = useSafeAreaInsets()
const style = [
a.flex_row,
a.py_xs,
a.pl_sm,
a.pr_xl,
a.align_center,
a.border_t,
t.atoms.border_contrast_medium,
t.atoms.bg,
]
// todo: when iPad support is added, it should also not use the KeyboardStickyView
if (isWeb) {
return <View style={style}>{children}</View>
}
return (
<KeyboardStickyView offset={{closed: -bottom}} style={style}>
{children}
</KeyboardStickyView>
)
}
+7 -9
View File
@@ -1,15 +1,13 @@
import React from 'react'
import {StyleSheet, TouchableOpacity} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useProfileQuery} from '#/state/queries/profile'
import {useSession} from '#/state/session'
import {UserAvatar} from '../util/UserAvatar'
import {Text} from '../util/text/Text'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {Text} from '../util/text/Text'
import {UserAvatar} from '../util/UserAvatar'
import hairlineWidth = StyleSheet.hairlineWidth
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useSession} from '#/state/session'
import {useProfileQuery} from '#/state/queries/profile'
export function ComposePrompt({onPressCompose}: {onPressCompose: () => void}) {
const {currentAccount} = useSession()
@@ -49,7 +47,7 @@ const styles = StyleSheet.create({
paddingBottom: 10,
flexDirection: 'row',
alignItems: 'center',
borderTopWidth: hairlineWidth,
borderTopWidth: 1,
},
labelMobile: {
paddingLeft: 12,
+6 -8
View File
@@ -1,15 +1,14 @@
import React from 'react'
import {Keyboard, StyleSheet} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {Button} from 'view/com/util/forms/Button'
import {usePalette} from 'lib/hooks/usePalette'
import {ShieldExclamation} from 'lib/icons'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome'
import {isNative} from 'platform/detection'
import {Button} from 'view/com/util/forms/Button'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
import {useModalControls} from '#/state/modals'
export function LabelsBtn({
labels,
@@ -55,7 +54,6 @@ const styles = StyleSheet.create({
button: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 2,
paddingHorizontal: 6,
},
dimmed: {
@@ -1,4 +1,4 @@
import React, {useCallback, useRef} from 'react'
import React, {useCallback} from 'react'
import {Keyboard} from 'react-native'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -7,6 +7,7 @@ import {logEvent} from '#/lib/statsig/statsig'
import {Gif} from '#/state/queries/tenor'
import {atoms as a, useTheme} from '#/alf'
import {Button} from '#/components/Button'
import {useDialogControl} from '#/components/Dialog'
import {GifSelectDialog} from '#/components/dialogs/GifSelect'
import {GifSquare_Stroke2_Corner0_Rounded as GifIcon} from '#/components/icons/Gif'
@@ -18,14 +19,14 @@ type Props = {
export function SelectGifBtn({onClose, onSelectGif, disabled}: Props) {
const {_} = useLingui()
const ref = useRef<{open: () => void}>(null)
const control = useDialogControl()
const t = useTheme()
const onPressSelectGif = useCallback(async () => {
logEvent('composer:gif:open', {})
Keyboard.dismiss()
ref.current?.open()
}, [])
control.open()
}, [control])
return (
<>
@@ -43,7 +44,7 @@ export function SelectGifBtn({onClose, onSelectGif, disabled}: Props) {
</Button>
<GifSelectDialog
controlRef={ref}
control={control}
onClose={onClose}
onSelectGif={onSelectGif}
/>
@@ -1,28 +1,27 @@
import React, {useCallback, useMemo} from 'react'
import {Keyboard, StyleSheet} from 'react-native'
import {StyleSheet, Keyboard} from 'react-native'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {
hasPostLanguage,
toPostLanguages,
useLanguagePrefs,
useLanguagePrefsApi,
} from '#/state/preferences/languages'
import {usePalette} from 'lib/hooks/usePalette'
import {isNative} from 'platform/detection'
import {Text} from 'view/com/util/text/Text'
import {
DropdownButton,
DropdownItem,
DropdownItemButton,
} from 'view/com/util/forms/DropdownButton'
import {Text} from 'view/com/util/text/Text'
import {usePalette} from 'lib/hooks/usePalette'
import {isNative} from 'platform/detection'
import {codeToLanguageName} from '../../../../locale/helpers'
import {useModalControls} from '#/state/modals'
import {
useLanguagePrefs,
useLanguagePrefsApi,
toPostLanguages,
hasPostLanguage,
} from '#/state/preferences/languages'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
export function SelectLangBtn() {
const pal = usePalette('default')
@@ -1,6 +1,5 @@
import React from 'react'
import {Keyboard, StyleProp, ViewStyle} from 'react-native'
import Animated, {AnimatedStyle} from 'react-native-reanimated'
import {Keyboard, View} from 'react-native'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -8,7 +7,7 @@ import {isNative} from '#/platform/detection'
import {useModalControls} from '#/state/modals'
import {ThreadgateSetting} from '#/state/queries/threadgate'
import {useAnalytics} from 'lib/analytics/analytics'
import {atoms as a, useTheme} from '#/alf'
import {atoms as a} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {CircleBanSign_Stroke2_Corner0_Rounded as CircleBanSign} from '#/components/icons/CircleBanSign'
import {Earth_Stroke2_Corner0_Rounded as Earth} from '#/components/icons/Globe'
@@ -17,15 +16,12 @@ import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group'
export function ThreadgateBtn({
threadgate,
onChange,
style,
}: {
threadgate: ThreadgateSetting[]
onChange: (v: ThreadgateSetting[]) => void
style?: StyleProp<AnimatedStyle<ViewStyle>>
}) {
const {track} = useAnalytics()
const {_} = useLingui()
const t = useTheme()
const {openModal} = useModalControls()
const onPress = () => {
@@ -49,11 +45,11 @@ export function ThreadgateBtn({
: _(msg`Some people can reply`)
return (
<Animated.View style={[a.flex_row, a.p_sm, t.atoms.bg, style]}>
<View style={[a.flex_row, a.pb_sm, a.px_md]}>
<Button
variant="solid"
color="secondary"
size="xsmall"
size="small"
testID="openReplyGateButton"
onPress={onPress}
label={label}>
@@ -62,6 +58,6 @@ export function ThreadgateBtn({
/>
<ButtonText>{label}</ButtonText>
</Button>
</Animated.View>
</View>
)
}
+2 -2
View File
@@ -76,7 +76,7 @@ export function FeedPage({
scrollToTop()
truncateAndInvalidate(queryClient, FEED_RQKEY(feed))
setHasNew(false)
logEvent('feed:refresh:sampled', {
logEvent('feed:refresh', {
feedType: feed.split('|')[0],
feedUrl: feed,
reason: 'soft-reset',
@@ -102,7 +102,7 @@ export function FeedPage({
scrollToTop()
truncateAndInvalidate(queryClient, FEED_RQKEY(feed))
setHasNew(false)
logEvent('feed:refresh:sampled', {
logEvent('feed:refresh', {
feedType: feed.split('|')[0],
feedUrl: feed,
reason: 'load-latest',
+3 -13
View File
@@ -25,7 +25,6 @@ import * as Prompt from '#/components/Prompt'
import {RichText} from '#/components/RichText'
import {Text} from '../util/text/Text'
import {UserAvatar} from '../util/UserAvatar'
import hairlineWidth = StyleSheet.hairlineWidth
export function FeedSourceCard({
feedUri,
@@ -35,7 +34,6 @@ export function FeedSourceCard({
showLikes = false,
pinOnSave = false,
showMinimalPlaceholder,
hideTopBorder,
}: {
feedUri: string
style?: StyleProp<ViewStyle>
@@ -44,7 +42,6 @@ export function FeedSourceCard({
showLikes?: boolean
pinOnSave?: boolean
showMinimalPlaceholder?: boolean
hideTopBorder?: boolean
}) {
const {data: preferences} = usePreferencesQuery()
const {data: feed} = useFeedSourceInfoQuery({uri: feedUri})
@@ -60,7 +57,6 @@ export function FeedSourceCard({
showLikes={showLikes}
pinOnSave={pinOnSave}
showMinimalPlaceholder={showMinimalPlaceholder}
hideTopBorder={hideTopBorder}
/>
)
}
@@ -75,7 +71,6 @@ export function FeedSourceCardLoaded({
showLikes = false,
pinOnSave = false,
showMinimalPlaceholder,
hideTopBorder,
}: {
feedUri: string
feed?: FeedSourceInfo
@@ -86,7 +81,6 @@ export function FeedSourceCardLoaded({
showLikes?: boolean
pinOnSave?: boolean
showMinimalPlaceholder?: boolean
hideTopBorder?: boolean
}) {
const t = useTheme()
const pal = usePalette('default')
@@ -155,7 +149,7 @@ export function FeedSourceCardLoaded({
style={[
pal.border,
{
borderTopWidth: showMinimalPlaceholder || hideTopBorder ? 0 : 1,
borderTopWidth: showMinimalPlaceholder ? 0 : 1,
flexDirection: 'row',
alignItems: 'center',
flex: 1,
@@ -197,12 +191,7 @@ export function FeedSourceCardLoaded({
<Pressable
testID={`feed-${feed.displayName}`}
accessibilityRole="button"
style={[
styles.container,
pal.border,
style,
{borderTopWidth: hideTopBorder ? 0 : hairlineWidth},
]}
style={[styles.container, pal.border, style]}
onPress={() => {
if (feed.type === 'feed') {
navigation.push('ProfileFeed', {
@@ -306,6 +295,7 @@ const styles = StyleSheet.create({
paddingVertical: 20,
flexDirection: 'column',
flex: 1,
borderTopWidth: 1,
gap: 14,
},
headerContainer: {
+13 -11
View File
@@ -1,28 +1,28 @@
import React from 'react'
import {
findNodeHandle,
ListRenderItemInfo,
StyleProp,
StyleSheet,
View,
ViewStyle,
} from 'react-native'
import {msg} from '@lingui/macro'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
import {cleanError} from '#/lib/strings/errors'
import {useTheme} from '#/lib/ThemeContext'
import {logger} from '#/logger'
import {isNative, isWeb} from '#/platform/detection'
import {isNative} from '#/platform/detection'
import {hydrateFeedGenerator} from '#/state/queries/feed'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {RQKEY, useProfileFeedgensQuery} from '#/state/queries/profile-feedgens'
import {usePalette} from 'lib/hooks/usePalette'
import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {EmptyState} from 'view/com/util/EmptyState'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {List, ListRef} from '../util/List'
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
import {Text} from '../util/text/Text'
import {FeedSourceCardLoaded} from './FeedSourceCard'
const LOADING = {_reactKey: '__loading__'}
@@ -51,6 +51,7 @@ export const ProfileFeedgens = React.forwardRef<
{did, scrollElRef, headerOffset, enabled, style, testID, setScrollViewTag},
ref,
) {
const pal = usePalette('default')
const {_} = useLingui()
const theme = useTheme()
const [isPTRing, setIsPTRing] = React.useState(false)
@@ -133,14 +134,16 @@ export const ProfileFeedgens = React.forwardRef<
// =
const renderItemInner = React.useCallback(
({item, index}: ListRenderItemInfo<any>) => {
({item}: {item: any}) => {
if (item === EMPTY) {
return (
<EmptyState
icon="hashtag"
message={_(msg`You have no feeds.`)}
<View
testID="listsEmpty"
/>
style={[{padding: 18, borderTopWidth: 1}, pal.border]}>
<Text style={pal.textLight}>
<Trans>You have no feeds.</Trans>
</Text>
</View>
)
} else if (item === ERROR_ITEM) {
return (
@@ -166,13 +169,12 @@ export const ProfileFeedgens = React.forwardRef<
preferences={preferences}
style={styles.item}
showLikes
hideTopBorder={index === 0 && !isWeb}
/>
)
}
return null
},
[error, refetch, onPressRetryLoadMore, preferences, _],
[error, refetch, onPressRetryLoadMore, pal, preferences, _],
)
React.useEffect(() => {
+11 -13
View File
@@ -1,20 +1,18 @@
import React from 'react'
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import {AppBskyGraphDefs, AtUri, RichText} from '@atproto/api'
import {Trans} from '@lingui/macro'
import {useSession} from '#/state/session'
import {usePalette} from 'lib/hooks/usePalette'
import {makeProfileLink} from 'lib/routes/links'
import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles'
import {s} from 'lib/styles'
import {atoms as a} from '#/alf'
import {RichText as RichTextCom} from '#/components/RichText'
import {AtUri, AppBskyGraphDefs, RichText} from '@atproto/api'
import {Link} from '../util/Link'
import {Text} from '../util/text/Text'
import {RichText as RichTextCom} from '#/components/RichText'
import {UserAvatar} from '../util/UserAvatar'
import hairlineWidth = StyleSheet.hairlineWidth
import {s} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
import {useSession} from '#/state/session'
import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles'
import {makeProfileLink} from 'lib/routes/links'
import {Trans} from '@lingui/macro'
import {atoms as a} from '#/alf'
export const ListCard = ({
testID,
@@ -134,7 +132,7 @@ export const ListCard = ({
const styles = StyleSheet.create({
outer: {
borderTopWidth: hairlineWidth,
borderTopWidth: 1,
paddingHorizontal: 6,
},
outerNoBorder: {
+14 -15
View File
@@ -9,19 +9,17 @@ import {
ViewStyle,
} from 'react-native'
import {AppBskyGraphDefs as GraphDefs} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {cleanError} from '#/lib/strings/errors'
import {logger} from '#/logger'
import {ListCard} from './ListCard'
import {MyListsFilter, useMyListsQuery} from '#/state/queries/my-lists'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {Text} from '../util/text/Text'
import {useAnalytics} from 'lib/analytics/analytics'
import {usePalette} from 'lib/hooks/usePalette'
import {s} from 'lib/styles'
import {EmptyState} from 'view/com/util/EmptyState'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {List} from '../util/List'
import {ListCard} from './ListCard'
import {s} from 'lib/styles'
import {logger} from '#/logger'
import {Trans} from '@lingui/macro'
import {cleanError} from '#/lib/strings/errors'
const LOADING = {_reactKey: '__loading__'}
const EMPTY = {_reactKey: '__empty__'}
@@ -42,7 +40,6 @@ export function MyLists({
}) {
const pal = usePalette('default')
const {track} = useAnalytics()
const {_} = useLingui()
const [isPTRing, setIsPTRing] = React.useState(false)
const {data, isFetching, isFetched, isError, error, refetch} =
useMyListsQuery(filter)
@@ -84,12 +81,14 @@ export function MyLists({
({item, index}: {item: any; index: number}) => {
if (item === EMPTY) {
return (
<EmptyState
<View
key={item._reactKey}
icon="list-ul"
message={_(msg`You have no lists.`)}
testID="listsEmpty"
/>
style={[{padding: 18, borderTopWidth: 1}, pal.border]}>
<Text style={pal.textLight}>
<Trans>You have no lists.</Trans>
</Text>
</View>
)
} else if (item === ERROR_ITEM) {
return (
@@ -117,7 +116,7 @@ export function MyLists({
/>
)
},
[error, onRefresh, renderItem, _],
[error, onRefresh, renderItem, pal],
)
if (inline) {
+13 -11
View File
@@ -1,27 +1,27 @@
import React from 'react'
import {
findNodeHandle,
ListRenderItemInfo,
StyleProp,
StyleSheet,
View,
ViewStyle,
} from 'react-native'
import {msg} from '@lingui/macro'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
import {cleanError} from '#/lib/strings/errors'
import {useTheme} from '#/lib/ThemeContext'
import {logger} from '#/logger'
import {isNative, isWeb} from '#/platform/detection'
import {isNative} from '#/platform/detection'
import {RQKEY, useProfileListsQuery} from '#/state/queries/profile-lists'
import {useAnalytics} from 'lib/analytics/analytics'
import {usePalette} from 'lib/hooks/usePalette'
import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {EmptyState} from 'view/com/util/EmptyState'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {List, ListRef} from '../util/List'
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
import {Text} from '../util/text/Text'
import {ListCard} from './ListCard'
const LOADING = {_reactKey: '__loading__'}
@@ -48,6 +48,7 @@ export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>(
{did, scrollElRef, headerOffset, enabled, style, testID, setScrollViewTag},
ref,
) {
const pal = usePalette('default')
const theme = useTheme()
const {track} = useAnalytics()
const {_} = useLingui()
@@ -137,14 +138,16 @@ export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>(
// =
const renderItemInner = React.useCallback(
({item, index}: ListRenderItemInfo<any>) => {
({item}: {item: any}) => {
if (item === EMPTY) {
return (
<EmptyState
icon="list-ul"
message={_(msg`You have no lists.`)}
<View
testID="listsEmpty"
/>
style={[{padding: 18, borderTopWidth: 1}, pal.border]}>
<Text style={pal.textLight}>
<Trans>You have no lists.</Trans>
</Text>
</View>
)
} else if (item === ERROR_ITEM) {
return (
@@ -170,11 +173,10 @@ export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>(
list={item}
testID={`list-${item.name}`}
style={styles.item}
noBorder={index === 0 && !isWeb}
/>
)
},
[error, refetch, onPressRetryLoadMore, _],
[error, refetch, onPressRetryLoadMore, pal, _],
)
React.useEffect(() => {
+17 -3
View File
@@ -10,12 +10,16 @@ import {
} from 'react-native'
import {Image as RNImage} from 'react-native-image-crop-picker'
import {LinearGradient} from 'expo-linear-gradient'
import {AppBskyGraphDefs, RichText as RichTextAPI} from '@atproto/api'
import {
AppBskyGraphDefs,
AppBskyRichtextFacet,
RichText as RichTextAPI,
} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {richTextToString} from '#/lib/strings/rich-text-helpers'
import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip'
import {shortenLinks} from '#/lib/strings/rich-text-manip'
import {useModalControls} from '#/state/modals'
import {
useListCreateMutation,
@@ -155,7 +159,17 @@ export function Component({
await richText.detectFacets(agent)
richText = shortenLinks(richText)
richText = stripInvalidMentions(richText)
// filter out any mention facets that didn't map to a user
richText.facets = richText.facets?.filter(facet => {
const mention = facet.features.find(feature =>
AppBskyRichtextFacet.isMention(feature),
)
if (mention && !mention.did) {
return false
}
return true
})
if (list) {
await listMetadataMutation.mutateAsync({
+1 -53
View File
@@ -18,13 +18,7 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {cleanError} from 'lib/strings/errors'
import {colors, gradients, s} from 'lib/styles'
import {useTheme} from 'lib/ThemeContext'
import {isAndroid, isWeb} from 'platform/detection'
import {DeactivateAccountDialog} from '#/screens/Settings/components/DeactivateAccountDialog'
import {atoms as a, useTheme as useNewTheme} from '#/alf'
import {useDialogControl} from '#/components/Dialog'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {InlineLinkText} from '#/components/Link'
import {Text as NewText} from '#/components/Typography'
import {isAndroid} from 'platform/detection'
import {resetToTab} from '../../../Navigation'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {Text} from '../util/text/Text'
@@ -36,7 +30,6 @@ export const snapPoints = isAndroid ? ['90%'] : ['55%']
export function Component({}: {}) {
const pal = usePalette('default')
const theme = useTheme()
const t = useNewTheme()
const {currentAccount} = useSession()
const agent = useAgent()
const {removeAccount} = useSessionApi()
@@ -48,7 +41,6 @@ export function Component({}: {}) {
const [password, setPassword] = React.useState<string>('')
const [isProcessing, setIsProcessing] = React.useState<boolean>(false)
const [error, setError] = React.useState<string>('')
const deactivateAccountControl = useDialogControl()
const onPressSendEmail = async () => {
setError('')
setIsProcessing(true)
@@ -176,50 +168,6 @@ export function Component({}: {}) {
</TouchableOpacity>
</>
)}
<View style={[!isWeb && a.px_xl]}>
<View
style={[
a.w_full,
a.flex_row,
a.gap_sm,
a.mt_lg,
a.p_lg,
a.rounded_sm,
t.atoms.bg_contrast_25,
]}>
<CircleInfo
size="md"
style={[
a.relative,
{
top: -1,
},
]}
/>
<NewText style={[a.leading_snug, a.flex_1]}>
<Trans>
You can also temporarily deactivate your account instead,
and reactivate it at any time.
</Trans>{' '}
<InlineLinkText
label={_(
msg`Click here for more information on deactivating your account`,
)}
to="#"
onPress={e => {
e.preventDefault()
deactivateAccountControl.open()
return false
}}>
<Trans>Click here for more information.</Trans>
</InlineLinkText>
</NewText>
</View>
</View>
<DeactivateAccountDialog control={deactivateAccountControl} />
</>
) : (
<>

Some files were not shown because too many files have changed in this diff Show More