Merge remote-tracking branch 'origin/main' into hailey/mod-metrics
* origin/main: (108 commits) tweak ci (#7916) Merge #7903 (#7917) fixing moderation blur gap when replying via the thread feed (#7783) fix cancel button on search posts screen (#7912) Add context for toast messages (#7902) Filter searchable people list by accepted (#7915) Nightly source-language update Add report dialog e2e tests (#7913) ota helper in dev mode (#7911) Add language: Esperanto (#7863) Add language: Scottish Gaelic (#7864) Add language: Welsh (#7866) Add initial test for post report flow mark label for translation (#7908) skeletons for chat list (#7907) fix layout shift in profile feed header (#7906) Add dark mode option to in-app embed snippet dialog (#7377) enhance(embed): add ability to pin color mode (#7186) Nightly source-language update use latest macos/xcode (#7893) ...
This commit is contained in:
+2
-15
@@ -32,6 +32,7 @@ module.exports = {
|
|||||||
'H6',
|
'H6',
|
||||||
'P',
|
'P',
|
||||||
'Admonition',
|
'Admonition',
|
||||||
|
'Admonition.Admonition',
|
||||||
],
|
],
|
||||||
impliedTextProps: [],
|
impliedTextProps: [],
|
||||||
suggestedTextWrappers: {
|
suggestedTextWrappers: {
|
||||||
@@ -80,20 +81,6 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
'simple-import-sort/exports': 'error',
|
'simple-import-sort/exports': 'error',
|
||||||
'react-compiler/react-compiler': 'warn',
|
'react-compiler/react-compiler': 'warn',
|
||||||
'no-restricted-imports': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
paths: [
|
|
||||||
{
|
|
||||||
name: '@atproto/api',
|
|
||||||
importNames: ['moderatePost'],
|
|
||||||
message:
|
|
||||||
'Please use `moderatePost_wrapped` from `#/lib/moderatePost_wrapped` instead.',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'@typescript-eslint/ban-ts-comment': 'warn',
|
|
||||||
},
|
},
|
||||||
ignorePatterns: [
|
ignorePatterns: [
|
||||||
'**/__mocks__/*.ts',
|
'**/__mocks__/*.ts',
|
||||||
@@ -105,9 +92,9 @@ module.exports = {
|
|||||||
'*.lock',
|
'*.lock',
|
||||||
'.husky',
|
'.husky',
|
||||||
'patches',
|
'patches',
|
||||||
'bskyweb',
|
|
||||||
'*.html',
|
'*.html',
|
||||||
'bskyweb',
|
'bskyweb',
|
||||||
|
'bskyembed',
|
||||||
'src/locale/locales/_build/',
|
'src/locale/locales/_build/',
|
||||||
'src/locale/locales/**/*.js',
|
'src/locale/locales/**/*.js',
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build and Submit iOS
|
name: Build and Submit iOS
|
||||||
runs-on: macos-14
|
runs-on: macos-15
|
||||||
steps:
|
steps:
|
||||||
- name: Check for EXPO_TOKEN
|
- name: Check for EXPO_TOKEN
|
||||||
run: >
|
run: >
|
||||||
@@ -52,7 +52,7 @@ jobs:
|
|||||||
|
|
||||||
- uses: maxim-lobanov/setup-xcode@v1
|
- uses: maxim-lobanov/setup-xcode@v1
|
||||||
with:
|
with:
|
||||||
xcode-version: '15.3'
|
xcode-version: '16.2'
|
||||||
|
|
||||||
- name: ☕️ Setup Cocoapods
|
- name: ☕️ Setup Cocoapods
|
||||||
uses: maxim-lobanov/setup-cocoapods@v1
|
uses: maxim-lobanov/setup-cocoapods@v1
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ jobs:
|
|||||||
# GitHub actions are horrible so let's just copy paste this in
|
# GitHub actions are horrible so let's just copy paste this in
|
||||||
buildIfNecessaryIOS:
|
buildIfNecessaryIOS:
|
||||||
name: Build and Submit iOS
|
name: Build and Submit iOS
|
||||||
runs-on: macos-14
|
runs-on: macos-15
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-build-ios
|
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-build-ios
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
@@ -176,7 +176,7 @@ jobs:
|
|||||||
|
|
||||||
- uses: maxim-lobanov/setup-xcode@v1
|
- uses: maxim-lobanov/setup-xcode@v1
|
||||||
with:
|
with:
|
||||||
xcode-version: '15.3'
|
xcode-version: '16.2'
|
||||||
|
|
||||||
- name: ☕️ Setup Cocoapods
|
- name: ☕️ Setup Cocoapods
|
||||||
uses: maxim-lobanov/setup-cocoapods@v1
|
uses: maxim-lobanov/setup-cocoapods@v1
|
||||||
|
|||||||
@@ -36,3 +36,14 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
commit_message: Nightly source-language update
|
commit_message: Nightly source-language update
|
||||||
file_pattern: ./src/locale/locales/en/messages.po
|
file_pattern: ./src/locale/locales/en/messages.po
|
||||||
|
- name: Push source lang to Crowdin
|
||||||
|
uses: crowdin/github-action@v2
|
||||||
|
with:
|
||||||
|
upload_sources: true
|
||||||
|
upload_sources_args: "-b main"
|
||||||
|
upload_translations: false
|
||||||
|
push_translations: false
|
||||||
|
push_sources: false
|
||||||
|
create_pull_request: false
|
||||||
|
env:
|
||||||
|
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||||
@@ -37,6 +37,6 @@ deps: ## Installs dependent libs using 'yarn install'
|
|||||||
|
|
||||||
.PHONY: nvm-setup
|
.PHONY: nvm-setup
|
||||||
nvm-setup: ## Use NVM to install and activate node+yarn
|
nvm-setup: ## Use NVM to install and activate node+yarn
|
||||||
nvm install 18
|
nvm install 20
|
||||||
nvm use 18
|
nvm use 20
|
||||||
npm install --global yarn
|
npm install --global yarn
|
||||||
|
|||||||
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
flows:
|
flows:
|
||||||
- "flows/*"
|
- "flows/**"
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ appId: xyz.blueskyweb.app
|
|||||||
- tapOn:
|
- tapOn:
|
||||||
label: "Adds and removes users on curatelists from the profile"
|
label: "Adds and removes users on curatelists from the profile"
|
||||||
id: "bottomBarSearchBtn"
|
id: "bottomBarSearchBtn"
|
||||||
- tapOn: "Search"
|
- tapOn: "Search for posts, users, or feeds"
|
||||||
- inputText: "bob"
|
- inputText: "bob"
|
||||||
- tapOn:
|
- tapOn:
|
||||||
id: "searchAutoCompleteResult-bob.test"
|
id: "searchAutoCompleteResult-bob.test"
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ appId: xyz.blueskyweb.app
|
|||||||
- tapOn:
|
- tapOn:
|
||||||
id: "profileCardButton"
|
id: "profileCardButton"
|
||||||
- tapOn:
|
- tapOn:
|
||||||
id: "profilePager-selector-4"
|
id: "profilePager-selector-5"
|
||||||
- tapOn: "alice-favs"
|
- tapOn: "alice-favs"
|
||||||
- tapOn: "Pin to Home"
|
- tapOn: "Pin to Home"
|
||||||
- tapOn:
|
- tapOn:
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
appId: xyz.blueskyweb.app
|
||||||
|
---
|
||||||
|
- runScript:
|
||||||
|
file: ../setupServer.js
|
||||||
|
env:
|
||||||
|
SERVER_PATH: "?users&follows&posts"
|
||||||
|
- runFlow:
|
||||||
|
file: ../setupApp.yml
|
||||||
|
- tapOn:
|
||||||
|
id: "e2eSignInAlice"
|
||||||
|
|
||||||
|
- tapOn:
|
||||||
|
id: "postDropdownBtn"
|
||||||
|
index: 0
|
||||||
|
- tapOn:
|
||||||
|
id: "postDropdownReportBtn"
|
||||||
|
- tapOn: "Create report for Misleading Post"
|
||||||
|
- tapOn: "Send report to Dev-env Moderation"
|
||||||
|
- tapOn:
|
||||||
|
point: "50%,90%"
|
||||||
@@ -15,7 +15,7 @@ appId: xyz.blueskyweb.app
|
|||||||
id: "bottomBarSearchBtn"
|
id: "bottomBarSearchBtn"
|
||||||
- tapOn:
|
- tapOn:
|
||||||
id: "bottomBarSearchBtn"
|
id: "bottomBarSearchBtn"
|
||||||
- tapOn: "Search"
|
- tapOn: "Search for posts, users, or feeds"
|
||||||
- inputText: "b"
|
- inputText: "b"
|
||||||
- tapOn:
|
- tapOn:
|
||||||
id: "searchAutoCompleteResult-bob.test"
|
id: "searchAutoCompleteResult-bob.test"
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
appId: xyz.blueskyweb.app
|
||||||
|
---
|
||||||
|
- runScript:
|
||||||
|
file: ../../setupServer.js
|
||||||
|
env:
|
||||||
|
SERVER_PATH: ?users&follows&posts&feeds
|
||||||
|
- runFlow:
|
||||||
|
file: ../../setupApp.yml
|
||||||
|
- tapOn:
|
||||||
|
id: "e2eSignInAlice"
|
||||||
|
|
||||||
|
- tapOn: "Carla's avatar"
|
||||||
|
- tapOn:
|
||||||
|
id: "profileHeaderDropdownBtn"
|
||||||
|
- tapOn:
|
||||||
|
id: "profileHeaderDropdownReportBtn"
|
||||||
|
- tapOn:
|
||||||
|
id: "report:option:com.atproto.moderation.defs#reasonSpam"
|
||||||
|
- assertVisible:
|
||||||
|
id: "report:labeler:mod-authority.test"
|
||||||
|
- tapOn:
|
||||||
|
id: "report:submit"
|
||||||
|
- assertNotVisible:
|
||||||
|
id: "report:dialog"
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
appId: xyz.blueskyweb.app
|
||||||
|
---
|
||||||
|
- runScript:
|
||||||
|
file: ../../setupServer.js
|
||||||
|
env:
|
||||||
|
SERVER_PATH: ?users&follows&posts&feeds
|
||||||
|
- runFlow:
|
||||||
|
file: ../../setupApp.yml
|
||||||
|
- tapOn:
|
||||||
|
id: "e2eSignInAlice"
|
||||||
|
|
||||||
|
- tapOn:
|
||||||
|
id: "postDropdownBtn"
|
||||||
|
index: 0
|
||||||
|
- tapOn:
|
||||||
|
id: "postDropdownReportBtn"
|
||||||
|
- tapOn:
|
||||||
|
id: "report:option:com.atproto.moderation.defs#reasonSpam"
|
||||||
|
- assertVisible:
|
||||||
|
id: "report:labeler:mod-authority.test"
|
||||||
|
- tapOn:
|
||||||
|
id: "report:submit"
|
||||||
|
- assertNotVisible:
|
||||||
|
id: "report:dialog"
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
appId: xyz.blueskyweb.app
|
||||||
|
---
|
||||||
|
- runScript:
|
||||||
|
file: ../../setupServer.js
|
||||||
|
env:
|
||||||
|
SERVER_PATH: ?users&follows&posts&feeds
|
||||||
|
- runFlow:
|
||||||
|
file: ../../setupApp.yml
|
||||||
|
- tapOn:
|
||||||
|
id: "e2eSignInAlice"
|
||||||
|
|
||||||
|
- tapOn:
|
||||||
|
id: "postDropdownBtn"
|
||||||
|
index: 0
|
||||||
|
- tapOn:
|
||||||
|
id: "postDropdownReportBtn"
|
||||||
|
- tapOn:
|
||||||
|
id: "report:option:com.atproto.moderation.defs#reasonOther"
|
||||||
|
- assertVisible:
|
||||||
|
id: "report:labeler:mod-authority.test"
|
||||||
|
# reason "other" defaults with details open
|
||||||
|
- assertVisible:
|
||||||
|
id: "report:details"
|
||||||
|
- tapOn:
|
||||||
|
id: "report:clearOption"
|
||||||
|
- assertNotVisible:
|
||||||
|
id: "report:details"
|
||||||
|
- tapOn:
|
||||||
|
id: "report:option:com.atproto.moderation.defs#reasonSpam"
|
||||||
|
- assertVisible:
|
||||||
|
id: "report:labeler:mod-authority.test"
|
||||||
|
- tapOn:
|
||||||
|
id: "report:submit"
|
||||||
|
- assertNotVisible:
|
||||||
|
id: "report:dialog"
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
appId: xyz.blueskyweb.app
|
||||||
|
---
|
||||||
|
- runScript:
|
||||||
|
file: ../../setupServer.js
|
||||||
|
env:
|
||||||
|
SERVER_PATH: ?users&follows&posts&feeds
|
||||||
|
- runFlow:
|
||||||
|
file: ../../setupApp.yml
|
||||||
|
- tapOn:
|
||||||
|
id: "e2eSignInAlice"
|
||||||
|
|
||||||
|
- tapOn:
|
||||||
|
id: "postDropdownBtn"
|
||||||
|
index: 0
|
||||||
|
- tapOn:
|
||||||
|
id: "postDropdownReportBtn"
|
||||||
|
- tapOn:
|
||||||
|
id: "report:option:com.atproto.moderation.defs#reasonOther"
|
||||||
|
- assertVisible:
|
||||||
|
id: "report:labeler:mod-authority.test"
|
||||||
|
# reason "other" defaults with details open
|
||||||
|
- assertVisible:
|
||||||
|
id: "report:details"
|
||||||
|
- tapOn:
|
||||||
|
id: "report:details"
|
||||||
|
- inputText: "This is a test report"
|
||||||
|
- tapOn:
|
||||||
|
id: "report:submit"
|
||||||
|
- assertNotVisible:
|
||||||
|
id: "report:dialog"
|
||||||
@@ -12,8 +12,8 @@ appId: xyz.blueskyweb.app
|
|||||||
# Navigate to another user profile via autocomplete
|
# Navigate to another user profile via autocomplete
|
||||||
- tapOn:
|
- tapOn:
|
||||||
id: "bottomBarSearchBtn"
|
id: "bottomBarSearchBtn"
|
||||||
- assertVisible: "Search"
|
- assertVisible: "Search for posts, users, or feeds"
|
||||||
- tapOn: "Search"
|
- tapOn: "Search for posts, users, or feeds"
|
||||||
- inputText: "b"
|
- inputText: "b"
|
||||||
- tapOn:
|
- tapOn:
|
||||||
id: "searchAutoCompleteResult-bob.test"
|
id: "searchAutoCompleteResult-bob.test"
|
||||||
|
|||||||
@@ -75,14 +75,17 @@ module.exports = function (config) {
|
|||||||
'an',
|
'an',
|
||||||
'ast',
|
'ast',
|
||||||
'ca',
|
'ca',
|
||||||
|
'cy',
|
||||||
'da',
|
'da',
|
||||||
'de',
|
'de',
|
||||||
'el',
|
'el',
|
||||||
|
'eo',
|
||||||
'es',
|
'es',
|
||||||
'eu',
|
'eu',
|
||||||
'fi',
|
'fi',
|
||||||
'fr',
|
'fr',
|
||||||
'ga',
|
'ga',
|
||||||
|
'gd',
|
||||||
'gl',
|
'gl',
|
||||||
'hi',
|
'hi',
|
||||||
'hu',
|
'hu',
|
||||||
@@ -242,6 +245,7 @@ module.exports = function (config) {
|
|||||||
'./plugins/withAndroidStylesAccentColorPlugin.js',
|
'./plugins/withAndroidStylesAccentColorPlugin.js',
|
||||||
'./plugins/withAndroidSplashScreenStatusBarTranslucentPlugin.js',
|
'./plugins/withAndroidSplashScreenStatusBarTranslucentPlugin.js',
|
||||||
'./plugins/withAndroidNoJitpackPlugin.js',
|
'./plugins/withAndroidNoJitpackPlugin.js',
|
||||||
|
'./plugins/withNoBundleCompression.js',
|
||||||
'./plugins/shareExtension/withShareExtensions.js',
|
'./plugins/shareExtension/withShareExtensions.js',
|
||||||
'./plugins/notificationsExtension/withNotificationsExtension.js',
|
'./plugins/notificationsExtension/withNotificationsExtension.js',
|
||||||
'./plugins/withAppDelegateReferrer.js',
|
'./plugins/withAppDelegateReferrer.js',
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm6.293-3.707a1 1 0 0 1 1.414 0L12 10.586l2.293-2.293a1 1 0 1 1 1.414 1.414L13.414 12l2.293 2.293a1 1 0 0 1-1.414 1.414L12 13.414l-2.293 2.293a1 1 0 0 1-1.414-1.414L10.586 12 8.293 9.707a1 1 0 0 1 0-1.414Z" clip-rule="evenodd"/></svg>
|
||||||
|
After Width: | Height: | Size: 454 B |
@@ -68,6 +68,7 @@ function scan(node = document) {
|
|||||||
if (ref_url.startsWith('http')) {
|
if (ref_url.startsWith('http')) {
|
||||||
searchParams.set('ref_url', encodeURIComponent(ref_url))
|
searchParams.set('ref_url', encodeURIComponent(ref_url))
|
||||||
}
|
}
|
||||||
|
searchParams.set('colorMode', embed.dataset.blueskyColorMode || 'system')
|
||||||
|
|
||||||
const iframe = document.createElement('iframe')
|
const iframe = document.createElement('iframe')
|
||||||
iframe.setAttribute('data-bluesky-id', id)
|
iframe.setAttribute('data-bluesky-id', id)
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
|
export type ColorModeValues = 'system' | 'light' | 'dark'
|
||||||
|
|
||||||
|
export function assertColorModeValues(value: string): value is ColorModeValues {
|
||||||
|
return ['system', 'light', 'dark'].includes(value)
|
||||||
|
}
|
||||||
|
|
||||||
export function applyTheme(theme: 'light' | 'dark') {
|
export function applyTheme(theme: 'light' | 'dark') {
|
||||||
document.documentElement.classList.remove('light', 'dark')
|
document.documentElement.classList.remove('light', 'dark')
|
||||||
document.documentElement.classList.add(theme)
|
document.documentElement.classList.add(theme)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initColorMode() {
|
export function initSystemColorMode() {
|
||||||
applyTheme(
|
applyTheme(
|
||||||
window.matchMedia('(prefers-color-scheme: dark)').matches
|
window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
? 'dark'
|
? 'dark'
|
||||||
|
|||||||
@@ -9,3 +9,14 @@
|
|||||||
:root {
|
:root {
|
||||||
color-scheme: light dark;
|
color-scheme: light dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' height='14px' width='14px' fill='none' viewBox='0 0 24 24'><path fill='black' fill-rule='evenodd' d='M3.293 8.293a1 1 0 0 1 1.414 0L12 15.586l7.293-7.293a1 1 0 1 1 1.414 1.414l-8 8a1 1 0 0 1-1.414 0l-8-8a1 1 0 0 1 0-1.414Z' clip-rule='evenodd'/></svg>");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: calc(100% - 0.75rem) center;
|
||||||
|
padding-right: 2rem;
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' height='14px' width='14px' fill='none' viewBox='0 0 24 24'><path fill='white' fill-rule='evenodd' d='M3.293 8.293a1 1 0 0 1 1.414 0L12 15.586l7.293-7.293a1 1 0 1 1 1.414 1.414l-8 8a1 1 0 0 1-1.414 0l-8-8a1 1 0 0 1 0-1.414Z' clip-rule='evenodd'/></svg>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export function labelsToInfo(
|
|||||||
return 'Adult Content'
|
return 'Adult Content'
|
||||||
case 'nudity':
|
case 'nudity':
|
||||||
return 'Non-sexual Nudity'
|
return 'Non-sexual Nudity'
|
||||||
|
case 'gore':
|
||||||
case 'graphic-media':
|
case 'graphic-media':
|
||||||
return 'Graphic Media'
|
return 'Graphic Media'
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import '../index.css'
|
import '../index.css'
|
||||||
|
|
||||||
import {AppBskyFeedDefs, AppBskyFeedPost, AtUri, BskyAgent} from '@atproto/api'
|
import {AppBskyFeedDefs, AppBskyFeedPost, AtpAgent, AtUri} from '@atproto/api'
|
||||||
import {h, render} from 'preact'
|
import {h, render} from 'preact'
|
||||||
import {useEffect, useMemo, useRef, useState} from 'preact/hooks'
|
import {useEffect, useMemo, useRef, useState} from 'preact/hooks'
|
||||||
|
|
||||||
import arrowBottom from '../../assets/arrowBottom_stroke2_corner0_rounded.svg'
|
import arrowBottom from '../../assets/arrowBottom_stroke2_corner0_rounded.svg'
|
||||||
import logo from '../../assets/logo.svg'
|
import logo from '../../assets/logo.svg'
|
||||||
import {initColorMode} from '../color-mode'
|
import {
|
||||||
|
assertColorModeValues,
|
||||||
|
ColorModeValues,
|
||||||
|
initSystemColorMode,
|
||||||
|
} from '../color-mode'
|
||||||
import {Container} from '../components/container'
|
import {Container} from '../components/container'
|
||||||
import {Link} from '../components/link'
|
import {Link} from '../components/link'
|
||||||
import {Post} from '../components/post'
|
import {Post} from '../components/post'
|
||||||
@@ -22,9 +26,9 @@ export const EMBED_SCRIPT = `${EMBED_SERVICE}/static/embed.js`
|
|||||||
const root = document.getElementById('app')
|
const root = document.getElementById('app')
|
||||||
if (!root) throw new Error('No root element')
|
if (!root) throw new Error('No root element')
|
||||||
|
|
||||||
initColorMode()
|
initSystemColorMode()
|
||||||
|
|
||||||
const agent = new BskyAgent({
|
const agent = new AtpAgent({
|
||||||
service: 'https://public.api.bsky.app',
|
service: 'https://public.api.bsky.app',
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -32,6 +36,7 @@ render(<LandingPage />, root)
|
|||||||
|
|
||||||
function LandingPage() {
|
function LandingPage() {
|
||||||
const [uri, setUri] = useState('')
|
const [uri, setUri] = useState('')
|
||||||
|
const [colorMode, setColorMode] = useState<ColorModeValues>('system')
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [thread, setThread] = useState<AppBskyFeedDefs.ThreadViewPost | null>(
|
const [thread, setThread] = useState<AppBskyFeedDefs.ThreadViewPost | null>(
|
||||||
@@ -120,24 +125,50 @@ function LandingPage() {
|
|||||||
|
|
||||||
<h1 className="text-4xl font-bold text-center">Embed a Bluesky Post</h1>
|
<h1 className="text-4xl font-bold text-center">Embed a Bluesky Post</h1>
|
||||||
|
|
||||||
|
<div className="flex flex-col w-full max-w-[600px] gap-6">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={uri}
|
value={uri}
|
||||||
onInput={e => setUri(e.currentTarget.value)}
|
onInput={e => setUri(e.currentTarget.value)}
|
||||||
className="border rounded-lg py-3 w-full max-w-[600px] px-4 dark:bg-dimmedBg dark:border-slate-500"
|
className="border rounded-lg py-3 px-4 dark:bg-dimmedBg dark:border-slate-500"
|
||||||
placeholder={DEFAULT_POST}
|
placeholder={DEFAULT_POST}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<label className="text-sm font-medium" for="colorModeSelect">
|
||||||
|
Theme
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={colorMode}
|
||||||
|
onChange={e => {
|
||||||
|
const value = e.currentTarget.value
|
||||||
|
if (assertColorModeValues(value)) {
|
||||||
|
setColorMode(value)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
id="colorModeSelect"
|
||||||
|
className="appearance-none bg-white border w-full rounded-lg text-sm px-3 py-2 dark:bg-dimmedBg dark:border-slate-500">
|
||||||
|
<option value="system">System</option>
|
||||||
|
<option value="light">Light</option>
|
||||||
|
<option value="dark">Dark</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<img src={arrowBottom} className="w-6 dark:invert" />
|
<img src={arrowBottom} className="w-6 dark:invert" />
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="w-full max-w-[600px]">
|
<div className={`${colorMode} w-full max-w-[600px]`}>
|
||||||
<Skeleton />
|
<Skeleton />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full max-w-[600px] gap-8 flex flex-col">
|
<div className="w-full max-w-[600px] gap-8 flex flex-col">
|
||||||
{!error && thread && uri && <Snippet thread={thread} />}
|
{!error && thread && uri && (
|
||||||
|
<Snippet thread={thread} colorMode={colorMode} />
|
||||||
|
)}
|
||||||
|
<div className={colorMode}>
|
||||||
{!error && thread && <Post thread={thread} key={thread.post.uri} />}
|
{!error && thread && <Post thread={thread} key={thread.post.uri} />}
|
||||||
|
</div>
|
||||||
{error && (
|
{error && (
|
||||||
<div className="w-full border border-red-500 bg-red-500/10 px-4 py-3 rounded-lg">
|
<div className="w-full border border-red-500 bg-red-500/10 px-4 py-3 rounded-lg">
|
||||||
<p className="text-red-500 text-center">{error}</p>
|
<p className="text-red-500 text-center">{error}</p>
|
||||||
@@ -168,7 +199,13 @@ function Skeleton() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Snippet({thread}: {thread: AppBskyFeedDefs.ThreadViewPost}) {
|
function Snippet({
|
||||||
|
thread,
|
||||||
|
colorMode,
|
||||||
|
}: {
|
||||||
|
thread: AppBskyFeedDefs.ThreadViewPost
|
||||||
|
colorMode: ColorModeValues
|
||||||
|
}) {
|
||||||
const ref = useRef<HTMLInputElement>(null)
|
const ref = useRef<HTMLInputElement>(null)
|
||||||
const [copied, setCopied] = useState(false)
|
const [copied, setCopied] = useState(false)
|
||||||
|
|
||||||
@@ -204,9 +241,11 @@ function Snippet({thread}: {thread: AppBskyFeedDefs.ThreadViewPost}) {
|
|||||||
// x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x
|
// x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x
|
||||||
return `<blockquote class="bluesky-embed" data-bluesky-uri="${escapeHtml(
|
return `<blockquote class="bluesky-embed" data-bluesky-uri="${escapeHtml(
|
||||||
thread.post.uri,
|
thread.post.uri,
|
||||||
)}" data-bluesky-cid="${escapeHtml(thread.post.cid)}"><p lang="${escapeHtml(
|
)}" data-bluesky-cid="${escapeHtml(
|
||||||
lang,
|
thread.post.cid,
|
||||||
)}">${escapeHtml(record.text)}${
|
)}" data-bluesky-embed-color-mode="${escapeHtml(
|
||||||
|
colorMode,
|
||||||
|
)}"><p lang="${escapeHtml(lang)}">${escapeHtml(record.text)}${
|
||||||
record.embed
|
record.embed
|
||||||
? `<br><br><a href="${escapeHtml(href)}">[image or embed]</a>`
|
? `<br><br><a href="${escapeHtml(href)}">[image or embed]</a>`
|
||||||
: ''
|
: ''
|
||||||
@@ -217,7 +256,7 @@ function Snippet({thread}: {thread: AppBskyFeedDefs.ThreadViewPost}) {
|
|||||||
)}</a>) <a href="${escapeHtml(href)}">${escapeHtml(
|
)}</a>) <a href="${escapeHtml(href)}">${escapeHtml(
|
||||||
niceDate(thread.post.indexedAt),
|
niceDate(thread.post.indexedAt),
|
||||||
)}</a></blockquote><script async src="${EMBED_SCRIPT}" charset="utf-8"></script>`
|
)}</a></blockquote><script async src="${EMBED_SCRIPT}" charset="utf-8"></script>`
|
||||||
}, [thread])
|
}, [thread, colorMode])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-2 w-full">
|
<div className="flex gap-2 w-full">
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {AppBskyFeedDefs, AtpAgent} from '@atproto/api'
|
|||||||
import {h, render} from 'preact'
|
import {h, render} from 'preact'
|
||||||
|
|
||||||
import logo from '../../assets/logo.svg'
|
import logo from '../../assets/logo.svg'
|
||||||
import {initColorMode} from '../color-mode'
|
import {applyTheme, initSystemColorMode} from '../color-mode'
|
||||||
import {Container} from '../components/container'
|
import {Container} from '../components/container'
|
||||||
import {Link} from '../components/link'
|
import {Link} from '../components/link'
|
||||||
import {Post} from '../components/post'
|
import {Post} from '../components/post'
|
||||||
@@ -22,7 +22,23 @@ if (!uri) {
|
|||||||
throw new Error('No uri in path')
|
throw new Error('No uri in path')
|
||||||
}
|
}
|
||||||
|
|
||||||
initColorMode()
|
const query = new URLSearchParams(window.location.search)
|
||||||
|
|
||||||
|
// theme - default to light mode
|
||||||
|
const colorMode = query.get('colorMode')
|
||||||
|
|
||||||
|
switch (colorMode) {
|
||||||
|
case 'dark':
|
||||||
|
applyTheme('dark')
|
||||||
|
break
|
||||||
|
case 'system':
|
||||||
|
initSystemColorMode()
|
||||||
|
break
|
||||||
|
case 'light':
|
||||||
|
default:
|
||||||
|
applyTheme('light')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
agent
|
agent
|
||||||
.getPostThread({
|
.getPostThread({
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import {Express} from 'express'
|
import {Express} from 'express'
|
||||||
|
|
||||||
import {AppContext} from '../context.js'
|
import {AppContext} from '../context.js'
|
||||||
import {default as create} from './create.js'
|
import {default as createShortLink} from './createShortLink.js'
|
||||||
import {default as health} from './health.js'
|
import {default as health} from './health.js'
|
||||||
import {default as redirect} from './redirect.js'
|
import {default as redirect} from './redirect.js'
|
||||||
|
import {default as shortLink} from './shortLink.js'
|
||||||
import {default as siteAssociation} from './siteAssociation.js'
|
import {default as siteAssociation} from './siteAssociation.js'
|
||||||
|
|
||||||
export * from './util.js'
|
export * from './util.js'
|
||||||
@@ -11,7 +12,8 @@ export * from './util.js'
|
|||||||
export default function (ctx: AppContext, app: Express) {
|
export default function (ctx: AppContext, app: Express) {
|
||||||
app = health(ctx, app) // GET /_health
|
app = health(ctx, app) // GET /_health
|
||||||
app = siteAssociation(ctx, app) // GET /.well-known/apple-app-site-association
|
app = siteAssociation(ctx, app) // GET /.well-known/apple-app-site-association
|
||||||
app = create(ctx, app) // POST /link
|
app = redirect(ctx, app) // GET /redirect
|
||||||
app = redirect(ctx, app) // GET /:linkId (should go last due to permissive matching)
|
app = createShortLink(ctx, app) // POST /link
|
||||||
|
app = shortLink(ctx, app) // GET /:linkId (should go last due to permissive matching)
|
||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,47 +6,40 @@ import {Express} from 'express'
|
|||||||
import {AppContext} from '../context.js'
|
import {AppContext} from '../context.js'
|
||||||
import {handler} from './util.js'
|
import {handler} from './util.js'
|
||||||
|
|
||||||
|
const INTERNAL_IP_REGEX = new RegExp(
|
||||||
|
'(^127.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}$)|(^10.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}$)|(^172.1[6-9]{1}[0-9]{0,1}.[0-9]{1,3}.[0-9]{1,3}$)|(^172.2[0-9]{1}[0-9]{0,1}.[0-9]{1,3}.[0-9]{1,3}$)|(^172.3[0-1]{1}[0-9]{0,1}.[0-9]{1,3}.[0-9]{1,3}$)|(^192.168.[0-9]{1,3}.[0-9]{1,3}$)|^localhost',
|
||||||
|
'i',
|
||||||
|
)
|
||||||
|
|
||||||
export default function (ctx: AppContext, app: Express) {
|
export default function (ctx: AppContext, app: Express) {
|
||||||
return app.get(
|
return app.get(
|
||||||
'/:linkId',
|
'/redirect',
|
||||||
handler(async (req, res) => {
|
handler(async (req, res) => {
|
||||||
const linkId = req.params.linkId
|
let link = req.query.u
|
||||||
const contentType = req.accepts(['html', 'json'])
|
|
||||||
assert(
|
assert(
|
||||||
typeof linkId === 'string',
|
typeof link === 'string',
|
||||||
'express guarantees id parameter is a string',
|
'express guarantees link query parameter is a string',
|
||||||
)
|
)
|
||||||
const found = await ctx.db.db
|
link = decodeURIComponent(link)
|
||||||
.selectFrom('link')
|
|
||||||
.selectAll()
|
let url: URL | undefined
|
||||||
.where('id', '=', linkId)
|
try {
|
||||||
.executeTakeFirst()
|
url = new URL(link)
|
||||||
if (!found) {
|
} catch {}
|
||||||
// potentially broken or mistyped link
|
|
||||||
|
if (
|
||||||
|
!url ||
|
||||||
|
(url.protocol !== 'http:' && url.protocol !== 'https:') || // is a http(s) url
|
||||||
|
(ctx.cfg.service.hostnames.includes(url.hostname.toLowerCase()) &&
|
||||||
|
url.pathname === '/redirect') || // is a redirect loop
|
||||||
|
INTERNAL_IP_REGEX.test(url.hostname) // isn't directing to an internal location
|
||||||
|
) {
|
||||||
res.setHeader('Cache-Control', 'no-store')
|
res.setHeader('Cache-Control', 'no-store')
|
||||||
if (contentType === 'json') {
|
|
||||||
return res
|
|
||||||
.status(404)
|
|
||||||
.json({
|
|
||||||
error: 'NotFound',
|
|
||||||
message: 'Link not found',
|
|
||||||
})
|
|
||||||
.end()
|
|
||||||
}
|
|
||||||
// send the user to the app
|
|
||||||
res.setHeader('Location', `https://${ctx.cfg.service.appHostname}`)
|
res.setHeader('Location', `https://${ctx.cfg.service.appHostname}`)
|
||||||
return res.status(302).end()
|
return res.status(302).end()
|
||||||
}
|
}
|
||||||
// build url from original url in order to preserve query params
|
|
||||||
const url = new URL(
|
|
||||||
req.originalUrl,
|
|
||||||
`https://${ctx.cfg.service.appHostname}`,
|
|
||||||
)
|
|
||||||
url.pathname = found.path
|
|
||||||
res.setHeader('Cache-Control', `max-age=${(7 * DAY) / SECOND}`)
|
res.setHeader('Cache-Control', `max-age=${(7 * DAY) / SECOND}`)
|
||||||
if (contentType === 'json') {
|
|
||||||
return res.json({url: url.href}).end()
|
|
||||||
}
|
|
||||||
res.setHeader('Location', url.href)
|
res.setHeader('Location', url.href)
|
||||||
return res.status(301).end()
|
return res.status(301).end()
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import assert from 'node:assert'
|
||||||
|
|
||||||
|
import {DAY, SECOND} from '@atproto/common'
|
||||||
|
import {Express} from 'express'
|
||||||
|
|
||||||
|
import {AppContext} from '../context.js'
|
||||||
|
import {handler} from './util.js'
|
||||||
|
|
||||||
|
export default function (ctx: AppContext, app: Express) {
|
||||||
|
return app.get(
|
||||||
|
'/:linkId',
|
||||||
|
handler(async (req, res) => {
|
||||||
|
const linkId = req.params.linkId
|
||||||
|
const contentType = req.accepts(['html', 'json'])
|
||||||
|
assert(
|
||||||
|
typeof linkId === 'string',
|
||||||
|
'express guarantees id parameter is a string',
|
||||||
|
)
|
||||||
|
const found = await ctx.db.db
|
||||||
|
.selectFrom('link')
|
||||||
|
.selectAll()
|
||||||
|
.where('id', '=', linkId)
|
||||||
|
.executeTakeFirst()
|
||||||
|
if (!found) {
|
||||||
|
// potentially broken or mistyped link
|
||||||
|
res.setHeader('Cache-Control', 'no-store')
|
||||||
|
if (contentType === 'json') {
|
||||||
|
return res
|
||||||
|
.status(404)
|
||||||
|
.json({
|
||||||
|
error: 'NotFound',
|
||||||
|
message: 'Link not found',
|
||||||
|
})
|
||||||
|
.end()
|
||||||
|
}
|
||||||
|
// send the user to the app
|
||||||
|
res.setHeader('Location', `https://${ctx.cfg.service.appHostname}`)
|
||||||
|
return res.status(302).end()
|
||||||
|
}
|
||||||
|
// build url from original url in order to preserve query params
|
||||||
|
const url = new URL(
|
||||||
|
req.originalUrl,
|
||||||
|
`https://${ctx.cfg.service.appHostname}`,
|
||||||
|
)
|
||||||
|
url.pathname = found.path
|
||||||
|
res.setHeader('Cache-Control', `max-age=${(7 * DAY) / SECOND}`)
|
||||||
|
if (contentType === 'json') {
|
||||||
|
return res.json({url: url.href}).end()
|
||||||
|
}
|
||||||
|
res.setHeader('Location', url.href)
|
||||||
|
return res.status(301).end()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -97,6 +97,7 @@ const hideAvatarLabels = new Set([
|
|||||||
'nudity',
|
'nudity',
|
||||||
'sexual-figurative',
|
'sexual-figurative',
|
||||||
'graphic-media',
|
'graphic-media',
|
||||||
|
'gore',
|
||||||
'self-harm',
|
'self-harm',
|
||||||
'sensitive',
|
'sensitive',
|
||||||
'security',
|
'security',
|
||||||
|
|||||||
@@ -94,6 +94,20 @@ func run(args []string) {
|
|||||||
Value: "",
|
Value: "",
|
||||||
EnvVars: []string{"STATIC_CDN_HOST"},
|
EnvVars: []string{"STATIC_CDN_HOST"},
|
||||||
},
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "bsky-canonical-instance",
|
||||||
|
Usage: "Enable if this is the canonical deployment (bsky.app)",
|
||||||
|
Value: false,
|
||||||
|
Required: false,
|
||||||
|
EnvVars: []string{"BSKY_CANONICAL_INSTANCE"},
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "robots-disallow-all",
|
||||||
|
Usage: "Crawling is allowed by default. Enable this flag to Disallow all",
|
||||||
|
Value: false,
|
||||||
|
Required: false,
|
||||||
|
EnvVars: []string{"ROBOTS_DISALLOW_ALL"},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ func serve(cctx *cli.Context) error {
|
|||||||
corsOrigins := cctx.StringSlice("cors-allowed-origins")
|
corsOrigins := cctx.StringSlice("cors-allowed-origins")
|
||||||
staticCDNHost := cctx.String("static-cdn-host")
|
staticCDNHost := cctx.String("static-cdn-host")
|
||||||
staticCDNHost = strings.TrimSuffix(staticCDNHost, "/")
|
staticCDNHost = strings.TrimSuffix(staticCDNHost, "/")
|
||||||
|
canonicalInstance := cctx.Bool("bsky-canonical-instance")
|
||||||
|
robotsDisallowAll := cctx.Bool("robots-disallow-all")
|
||||||
|
|
||||||
// Echo
|
// Echo
|
||||||
e := echo.New()
|
e := echo.New()
|
||||||
@@ -204,13 +206,23 @@ func serve(cctx *cli.Context) error {
|
|||||||
return http.FS(fsys)
|
return http.FS(fsys)
|
||||||
}())
|
}())
|
||||||
|
|
||||||
e.GET("/robots.txt", echo.WrapHandler(staticHandler))
|
// enable some special endpoints for the "canonical" deployment (bsky.app). not having these enabled should *not* impact regular operation
|
||||||
|
if canonicalInstance {
|
||||||
e.GET("/ips-v4", echo.WrapHandler(staticHandler))
|
e.GET("/ips-v4", echo.WrapHandler(staticHandler))
|
||||||
e.GET("/ips-v6", echo.WrapHandler(staticHandler))
|
e.GET("/ips-v6", echo.WrapHandler(staticHandler))
|
||||||
e.GET("/.well-known/*", echo.WrapHandler(staticHandler))
|
|
||||||
e.GET("/security.txt", func(c echo.Context) error {
|
e.GET("/security.txt", func(c echo.Context) error {
|
||||||
return c.Redirect(http.StatusMovedPermanently, "/.well-known/security.txt")
|
return c.Redirect(http.StatusMovedPermanently, "/.well-known/security.txt")
|
||||||
})
|
})
|
||||||
|
e.GET("/.well-known/*", echo.WrapHandler(staticHandler))
|
||||||
|
}
|
||||||
|
|
||||||
|
// default to permissive, but Disallow all if flag set
|
||||||
|
if robotsDisallowAll {
|
||||||
|
e.File("/robots.txt", "static/robots-disallow-all.txt")
|
||||||
|
} else {
|
||||||
|
e.GET("/robots.txt", echo.WrapHandler(staticHandler))
|
||||||
|
}
|
||||||
|
|
||||||
e.GET("/iframe/youtube.html", echo.WrapHandler(staticHandler))
|
e.GET("/iframe/youtube.html", echo.WrapHandler(staticHandler))
|
||||||
e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", staticHandler)), func(next echo.HandlerFunc) echo.HandlerFunc {
|
e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", staticHandler)), func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
return func(c echo.Context) error {
|
return func(c echo.Context) error {
|
||||||
@@ -283,6 +295,7 @@ func serve(cctx *cli.Context) error {
|
|||||||
e.GET("/profile/:handleOrDID/follows", server.WebGeneric)
|
e.GET("/profile/:handleOrDID/follows", server.WebGeneric)
|
||||||
e.GET("/profile/:handleOrDID/followers", server.WebGeneric)
|
e.GET("/profile/:handleOrDID/followers", server.WebGeneric)
|
||||||
e.GET("/profile/:handleOrDID/known-followers", server.WebGeneric)
|
e.GET("/profile/:handleOrDID/known-followers", server.WebGeneric)
|
||||||
|
e.GET("/profile/:handleOrDID/search", server.WebGeneric)
|
||||||
e.GET("/profile/:handleOrDID/lists/:rkey", server.WebGeneric)
|
e.GET("/profile/:handleOrDID/lists/:rkey", server.WebGeneric)
|
||||||
e.GET("/profile/:handleOrDID/feed/:rkey", server.WebGeneric)
|
e.GET("/profile/:handleOrDID/feed/:rkey", server.WebGeneric)
|
||||||
e.GET("/profile/:handleOrDID/feed/:rkey/liked-by", server.WebGeneric)
|
e.GET("/profile/:handleOrDID/feed/:rkey/liked-by", server.WebGeneric)
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# This is an development or self-hosted instance of the bsky web app, and crawling has been disallowed by the operator team.
|
||||||
|
User-Agent: *
|
||||||
|
Disallow: /
|
||||||
+2
-1
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"project_id": "664276",
|
"project_id": "1",
|
||||||
|
"base_url": "https://bluesky.api.crowdin.com",
|
||||||
"base_path": ".",
|
"base_path": ".",
|
||||||
"preserve_hierarchy": true,
|
"preserve_hierarchy": true,
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
# Internationalization
|
# Internationalization
|
||||||
|
|
||||||
We want the official Bluesky app to be supported in as many languages as possible. If you want to help us translate the app, please open a PR or issue on the [Bluesky app repo on GitHub](https://github.com/bluesky-social/social-app)
|
We want the official Bluesky app to be supported in as many languages as possible. If you want to help us translate the app, please get involved on [Crowdin](https://bluesky.crowdin.com/u/projects/1) or open an issue on the [Bluesky app repo on GitHub](https://github.com/bluesky-social/social-app).
|
||||||
|
|
||||||
## Tools
|
## Tools
|
||||||
|
|
||||||
- We use Lingui to implement translations. You can find the documentation [here](https://lingui.dev/).
|
|
||||||
- We use Crowdin to manage translations.
|
- We use Crowdin to manage translations.
|
||||||
- Bluesky Crowdin: https://crowdin.com/project/bluesky-social
|
- Bluesky Crowdin: https://bluesky.crowdin.com/u/projects/1
|
||||||
- Introduction to Crowdin: https://support.crowdin.com/for-translators/
|
- Introduction to Crowdin: https://support.crowdin.com/for-translators/
|
||||||
|
- We use Lingui to implement translations. You can find the documentation [here](https://lingui.dev/).
|
||||||
|
|
||||||
## Translators
|
## Translators
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ Much of the app is translated by community contributions. (We <3 our translators
|
|||||||
|
|
||||||
### Using Crowdin
|
### Using Crowdin
|
||||||
|
|
||||||
[Crowdin](https://crowdin.com/project/bluesky-social) is our primary tool for managing translations. There are two roles:
|
[Crowdin](https://bluesky.crowdin.com/u/projects/1) is our primary tool for managing translations. There are two roles:
|
||||||
|
|
||||||
- **Proof-readers**. Can create new translations and approve submitted translations.
|
- **Proof-readers**. Can create new translations and approve submitted translations.
|
||||||
- **Translators**. Can create new translations.
|
- **Translators**. Can create new translations.
|
||||||
@@ -32,7 +32,7 @@ Please treat everyone with respect. Proof-readers are given final say on transla
|
|||||||
|
|
||||||
### Adding a new language
|
### Adding a new language
|
||||||
|
|
||||||
Create a new [Crowdin discussion](https://crowdin.com/project/bluesky-social/discussions) or [GitHub issue](https://github.com/bluesky-social/social-app/issues) requesting the new language be added to the project.
|
You can request a new language be added to the project by clicking **Request New Language** on Crowdin or you can create a [GitHub issue](https://github.com/bluesky-social/social-app/issues).
|
||||||
|
|
||||||
Please only request a new language when you are certain you will be able to contribute a substantive portion of translations for the language.
|
Please only request a new language when you are certain you will be able to contribute a substantive portion of translations for the language.
|
||||||
|
|
||||||
@@ -47,7 +47,8 @@ Every night, a GitHub action will run `yarn intl:extract` to update the english
|
|||||||
### Release process
|
### Release process
|
||||||
|
|
||||||
1. Pull main and create a branch.
|
1. Pull main and create a branch.
|
||||||
1. Run `yarn intl:pull` to fetch all translation updates from Crowdin.
|
1. Run `yarn intl:pull` to fetch all translation updates from Crowdin. Commit.
|
||||||
|
1. Run `yarn intl:extract:all` to ensure all `.po` files are synced with the current state of the code. Commit.
|
||||||
1. Create a PR, ensure the translations all look correct, and merge.
|
1. Create a PR, ensure the translations all look correct, and merge.
|
||||||
1. If needed:
|
1. If needed:
|
||||||
1. Merge all approved translation PRs (contributions from outside crowdin).
|
1. Merge all approved translation PRs (contributions from outside crowdin).
|
||||||
|
|||||||
+1
-1
@@ -119,5 +119,5 @@ jest.mock('statsig-react-native-expo', () => ({
|
|||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
jest.mock('../src/lib/bitdrift', () => ({}))
|
jest.mock('../src/logger/bitdrift/lib', () => ({}))
|
||||||
jest.mock('../src/lib/statsig/statsig', () => ({}))
|
jest.mock('../src/lib/statsig/statsig', () => ({}))
|
||||||
|
|||||||
@@ -5,15 +5,18 @@ module.exports = {
|
|||||||
'an',
|
'an',
|
||||||
'ast',
|
'ast',
|
||||||
'ca',
|
'ca',
|
||||||
|
'cy',
|
||||||
'da',
|
'da',
|
||||||
'de',
|
'de',
|
||||||
'el',
|
'el',
|
||||||
'en-GB',
|
'en-GB',
|
||||||
|
'eo',
|
||||||
'es',
|
'es',
|
||||||
'eu',
|
'eu',
|
||||||
'fi',
|
'fi',
|
||||||
'fr',
|
'fr',
|
||||||
'ga',
|
'ga',
|
||||||
|
'gd',
|
||||||
'gl',
|
'gl',
|
||||||
'hi',
|
'hi',
|
||||||
'hu',
|
'hu',
|
||||||
|
|||||||
+28
-11
@@ -31,9 +31,13 @@ class BottomSheetView(
|
|||||||
private lateinit var dialogRootViewGroup: DialogRootViewGroup
|
private lateinit var dialogRootViewGroup: DialogRootViewGroup
|
||||||
private var eventDispatcher: EventDispatcher? = null
|
private var eventDispatcher: EventDispatcher? = null
|
||||||
|
|
||||||
private val screenHeight =
|
private val rawScreenHeight = context.resources.displayMetrics.heightPixels.toFloat()
|
||||||
context.resources.displayMetrics.heightPixels
|
private val safeScreenHeight = (rawScreenHeight - getNavigationBarHeight()).toFloat()
|
||||||
.toFloat()
|
|
||||||
|
private fun getNavigationBarHeight(): Int {
|
||||||
|
val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android")
|
||||||
|
return if (resourceId > 0) resources.getDimensionPixelSize(resourceId) else 0
|
||||||
|
}
|
||||||
|
|
||||||
private val onAttemptDismiss by EventDispatcher()
|
private val onAttemptDismiss by EventDispatcher()
|
||||||
private val onSnapPointChange by EventDispatcher()
|
private val onSnapPointChange by EventDispatcher()
|
||||||
@@ -63,12 +67,12 @@ class BottomSheetView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var maxHeight = this.screenHeight
|
var maxHeight = this.safeScreenHeight
|
||||||
set(value) {
|
set(value) {
|
||||||
val px = dpToPx(value)
|
val px = dpToPx(value)
|
||||||
field =
|
field =
|
||||||
if (px > this.screenHeight) {
|
if (px > this.safeScreenHeight) {
|
||||||
this.screenHeight
|
this.safeScreenHeight
|
||||||
} else {
|
} else {
|
||||||
px
|
px
|
||||||
}
|
}
|
||||||
@@ -153,6 +157,19 @@ class BottomSheetView(
|
|||||||
|
|
||||||
// Presentation
|
// Presentation
|
||||||
|
|
||||||
|
private fun getHalfExpandedRatio(contentHeight: Float): Float {
|
||||||
|
return when {
|
||||||
|
// Full height sheets
|
||||||
|
contentHeight >= safeScreenHeight -> 0.99f
|
||||||
|
// Medium height sheets (>50% but <100%)
|
||||||
|
contentHeight >= safeScreenHeight / 2 ->
|
||||||
|
this.clampRatio(this.getTargetHeight() / safeScreenHeight)
|
||||||
|
// Small height sheets (<50%)
|
||||||
|
else ->
|
||||||
|
this.clampRatio(this.getTargetHeight() / rawScreenHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun present() {
|
private fun present() {
|
||||||
if (this.isOpen || this.isOpening || this.isClosing) return
|
if (this.isOpen || this.isOpening || this.isClosing) return
|
||||||
|
|
||||||
@@ -172,12 +189,12 @@ class BottomSheetView(
|
|||||||
val behavior = BottomSheetBehavior.from(it)
|
val behavior = BottomSheetBehavior.from(it)
|
||||||
behavior.state = BottomSheetBehavior.STATE_HIDDEN
|
behavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||||
behavior.isFitToContents = true
|
behavior.isFitToContents = true
|
||||||
behavior.halfExpandedRatio = this.clampRatio(this.getTargetHeight() / this.screenHeight)
|
behavior.halfExpandedRatio = getHalfExpandedRatio(contentHeight)
|
||||||
behavior.skipCollapsed = true
|
behavior.skipCollapsed = true
|
||||||
behavior.isDraggable = true
|
behavior.isDraggable = true
|
||||||
behavior.isHideable = true
|
behavior.isHideable = true
|
||||||
|
|
||||||
if (contentHeight >= this.screenHeight || this.minHeight >= this.screenHeight) {
|
if (contentHeight >= this.safeScreenHeight || this.minHeight >= this.safeScreenHeight) {
|
||||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||||
this.selectedSnapPoint = 2
|
this.selectedSnapPoint = 2
|
||||||
} else {
|
} else {
|
||||||
@@ -227,11 +244,11 @@ class BottomSheetView(
|
|||||||
bottomSheet?.let {
|
bottomSheet?.let {
|
||||||
val behavior = BottomSheetBehavior.from(it)
|
val behavior = BottomSheetBehavior.from(it)
|
||||||
|
|
||||||
behavior.halfExpandedRatio = this.clampRatio(this.getTargetHeight() / this.screenHeight)
|
behavior.halfExpandedRatio = getHalfExpandedRatio(contentHeight)
|
||||||
|
|
||||||
if (contentHeight > this.screenHeight && behavior.state != BottomSheetBehavior.STATE_EXPANDED) {
|
if (contentHeight > this.safeScreenHeight && behavior.state != BottomSheetBehavior.STATE_EXPANDED) {
|
||||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||||
} else if (contentHeight < this.screenHeight && behavior.state != BottomSheetBehavior.STATE_HALF_EXPANDED) {
|
} else if (contentHeight < this.safeScreenHeight && behavior.state != BottomSheetBehavior.STATE_HALF_EXPANDED) {
|
||||||
behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED
|
behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import {
|
import {
|
||||||
Dimensions,
|
Dimensions,
|
||||||
|
LayoutChangeEvent,
|
||||||
NativeSyntheticEvent,
|
NativeSyntheticEvent,
|
||||||
Platform,
|
Platform,
|
||||||
StyleProp,
|
StyleProp,
|
||||||
View,
|
View,
|
||||||
ViewStyle,
|
ViewStyle,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
|
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||||
import {requireNativeModule, requireNativeViewManager} from 'expo-modules-core'
|
import {requireNativeModule, requireNativeViewManager} from 'expo-modules-core'
|
||||||
|
|
||||||
|
import {isIOS} from '#/platform/detection'
|
||||||
import {BottomSheetState, BottomSheetViewProps} from './BottomSheet.types'
|
import {BottomSheetState, BottomSheetViewProps} from './BottomSheet.types'
|
||||||
import {BottomSheetPortalProvider} from './BottomSheetPortal'
|
import {BottomSheetPortalProvider} from './BottomSheetPortal'
|
||||||
import {Context as PortalContext} from './BottomSheetPortal'
|
import {Context as PortalContext} from './BottomSheetPortal'
|
||||||
@@ -81,12 +84,10 @@ export class BottomSheetNativeComponent extends React.Component<
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const {children, backgroundColor, ...rest} = this.props
|
|
||||||
const cornerRadius = rest.cornerRadius ?? 0
|
|
||||||
|
|
||||||
let extraStyles
|
let extraStyles
|
||||||
if (isIOS15 && this.state.viewHeight) {
|
if (isIOS15 && this.state.viewHeight) {
|
||||||
const {viewHeight} = this.state
|
const {viewHeight} = this.state
|
||||||
|
const cornerRadius = this.props.cornerRadius ?? 0
|
||||||
if (viewHeight < screenHeight / 2) {
|
if (viewHeight < screenHeight / 2) {
|
||||||
extraStyles = {
|
extraStyles = {
|
||||||
height: viewHeight,
|
height: viewHeight,
|
||||||
@@ -99,13 +100,51 @@ export class BottomSheetNativeComponent extends React.Component<
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Portal>
|
<Portal>
|
||||||
|
<BottomSheetNativeComponentInner
|
||||||
|
{...this.props}
|
||||||
|
nativeViewRef={this.ref}
|
||||||
|
onStateChange={this.onStateChange}
|
||||||
|
extraStyles={extraStyles}
|
||||||
|
onLayout={e => {
|
||||||
|
const {height} = e.nativeEvent.layout
|
||||||
|
this.setState({viewHeight: height})
|
||||||
|
this.updateLayout()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function BottomSheetNativeComponentInner({
|
||||||
|
children,
|
||||||
|
backgroundColor,
|
||||||
|
onLayout,
|
||||||
|
onStateChange,
|
||||||
|
nativeViewRef,
|
||||||
|
extraStyles,
|
||||||
|
...rest
|
||||||
|
}: BottomSheetViewProps & {
|
||||||
|
extraStyles?: StyleProp<ViewStyle>
|
||||||
|
onStateChange: (
|
||||||
|
event: NativeSyntheticEvent<{state: BottomSheetState}>,
|
||||||
|
) => void
|
||||||
|
nativeViewRef: React.RefObject<View>
|
||||||
|
onLayout: (event: LayoutChangeEvent) => void
|
||||||
|
}) {
|
||||||
|
const insets = useSafeAreaInsets()
|
||||||
|
const cornerRadius = rest.cornerRadius ?? 0
|
||||||
|
|
||||||
|
const sheetHeight = isIOS ? screenHeight - insets.top : screenHeight
|
||||||
|
|
||||||
|
return (
|
||||||
<NativeView
|
<NativeView
|
||||||
{...rest}
|
{...rest}
|
||||||
onStateChange={this.onStateChange}
|
onStateChange={onStateChange}
|
||||||
ref={this.ref}
|
ref={nativeViewRef}
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
height: screenHeight,
|
height: sheetHeight,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
}}
|
}}
|
||||||
containerBackgroundColor={backgroundColor}>
|
containerBackgroundColor={backgroundColor}>
|
||||||
@@ -121,17 +160,10 @@ export class BottomSheetNativeComponent extends React.Component<
|
|||||||
},
|
},
|
||||||
extraStyles,
|
extraStyles,
|
||||||
]}>
|
]}>
|
||||||
<View
|
<View onLayout={onLayout}>
|
||||||
onLayout={e => {
|
|
||||||
const {height} = e.nativeEvent.layout
|
|
||||||
this.setState({viewHeight: height})
|
|
||||||
this.updateLayout()
|
|
||||||
}}>
|
|
||||||
<BottomSheetPortalProvider>{children}</BottomSheetPortalProvider>
|
<BottomSheetPortalProvider>{children}</BottomSheetPortalProvider>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</NativeView>
|
</NativeView>
|
||||||
</Portal>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
+8
-8
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "bsky.app",
|
"name": "bsky.app",
|
||||||
"version": "1.98.0",
|
"version": "1.99.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
@@ -42,8 +42,9 @@
|
|||||||
"perf:test:measure": "NODE_ENV=test flashlight test --bundleId xyz.blueskyweb.app --testCommand \"yarn perf:test\" --duration 150000 --resultsFilePath .perf/results.json",
|
"perf:test:measure": "NODE_ENV=test flashlight test --bundleId xyz.blueskyweb.app --testCommand \"yarn perf:test\" --duration 150000 --resultsFilePath .perf/results.json",
|
||||||
"perf:test:results": "NODE_ENV=test flashlight report .perf/results.json",
|
"perf:test:results": "NODE_ENV=test flashlight report .perf/results.json",
|
||||||
"perf:measure": "NODE_ENV=test flashlight measure",
|
"perf:measure": "NODE_ENV=test flashlight measure",
|
||||||
"intl:build": "yarn intl:extract && yarn intl:compile",
|
"intl:build": "yarn intl:extract:all && yarn intl:compile",
|
||||||
"intl:extract": "lingui extract --clean --locale en",
|
"intl:extract": "lingui extract --clean --locale en",
|
||||||
|
"intl:extract:all": "lingui extract --clean",
|
||||||
"intl:compile": "lingui compile",
|
"intl:compile": "lingui compile",
|
||||||
"intl:pull": "crowdin download translations --verbose -b main",
|
"intl:pull": "crowdin download translations --verbose -b main",
|
||||||
"intl:push": "crowdin push translations --verbose -b main",
|
"intl:push": "crowdin push translations --verbose -b main",
|
||||||
@@ -56,7 +57,7 @@
|
|||||||
"icons:optimize": "svgo -f ./assets/icons"
|
"icons:optimize": "svgo -f ./assets/icons"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@atproto/api": "^0.13.35",
|
"@atproto/api": "^0.14.7",
|
||||||
"@bitdrift/react-native": "^0.6.8",
|
"@bitdrift/react-native": "^0.6.8",
|
||||||
"@braintree/sanitize-url": "^6.0.2",
|
"@braintree/sanitize-url": "^6.0.2",
|
||||||
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
||||||
@@ -78,10 +79,10 @@
|
|||||||
"@mattermost/react-native-paste-input": "^0.7.1",
|
"@mattermost/react-native-paste-input": "^0.7.1",
|
||||||
"@miblanchard/react-native-slider": "^2.3.1",
|
"@miblanchard/react-native-slider": "^2.3.1",
|
||||||
"@mozzius/expo-dynamic-app-icon": "^1.5.0",
|
"@mozzius/expo-dynamic-app-icon": "^1.5.0",
|
||||||
"@radix-ui/react-dismissable-layer": "^1.1.1",
|
"@radix-ui/react-dismissable-layer": "^1.1.5",
|
||||||
"@radix-ui/react-dropdown-menu": "2.0.1",
|
"@radix-ui/react-dropdown-menu": "2.1.6",
|
||||||
"@radix-ui/react-focus-guards": "^1.1.1",
|
"@radix-ui/react-focus-guards": "^1.1.1",
|
||||||
"@radix-ui/react-focus-scope": "^1.1.0",
|
"@radix-ui/react-focus-scope": "^1.1.2",
|
||||||
"@react-native-async-storage/async-storage": "1.23.1",
|
"@react-native-async-storage/async-storage": "1.23.1",
|
||||||
"@react-native-menu/menu": "^1.1.7",
|
"@react-native-menu/menu": "^1.1.7",
|
||||||
"@react-native-picker/picker": "2.10.2",
|
"@react-native-picker/picker": "2.10.2",
|
||||||
@@ -211,7 +212,7 @@
|
|||||||
"zod": "^3.20.2"
|
"zod": "^3.20.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@atproto/dev-env": "^0.3.67",
|
"@atproto/dev-env": "^0.3.87",
|
||||||
"@babel/core": "^7.26.0",
|
"@babel/core": "^7.26.0",
|
||||||
"@babel/preset-env": "^7.26.0",
|
"@babel/preset-env": "^7.26.0",
|
||||||
"@babel/runtime": "^7.26.0",
|
"@babel/runtime": "^7.26.0",
|
||||||
@@ -270,7 +271,6 @@
|
|||||||
"@expo/image-utils": "0.6.3",
|
"@expo/image-utils": "0.6.3",
|
||||||
"@react-native/babel-preset": "0.76.1",
|
"@react-native/babel-preset": "0.76.1",
|
||||||
"@react-native/normalize-colors": "0.76.1",
|
"@react-native/normalize-colors": "0.76.1",
|
||||||
"@radix-ui/react-focus-scope": "1.1.0",
|
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"**/expo-constants": "17.0.3",
|
"**/expo-constants": "17.0.3",
|
||||||
"**/expo-device": "7.0.1",
|
"**/expo-device": "7.0.1",
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
const {withAppBuildGradle} = require('@expo/config-plugins')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Config Plugin to disable bundle compression in Android build.gradle.
|
||||||
|
* @param {import('@expo/config-plugins').ConfigPlugin} config
|
||||||
|
* @returns {import('@expo/config-plugins').ConfigPlugin}
|
||||||
|
*/
|
||||||
|
module.exports = function withNoBundleCompression(config) {
|
||||||
|
return withAppBuildGradle(config, androidConfig => {
|
||||||
|
let buildGradle = androidConfig.modResults.contents
|
||||||
|
|
||||||
|
const hasAndroidResources = buildGradle.includes('androidResources {')
|
||||||
|
const hasNoCompress = buildGradle.includes('noCompress')
|
||||||
|
|
||||||
|
if (hasAndroidResources) {
|
||||||
|
if (hasNoCompress) {
|
||||||
|
if (
|
||||||
|
buildGradle.includes('noCompress += ["bundle"]') ||
|
||||||
|
buildGradle.includes("noCompress += 'bundle'") ||
|
||||||
|
buildGradle.includes('noCompress += "bundle"')
|
||||||
|
) {
|
||||||
|
return androidConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = buildGradle.split('\n')
|
||||||
|
const modifiedLines = lines.map(line => {
|
||||||
|
if (line.trim().startsWith('noCompress')) {
|
||||||
|
if (line.includes('+=')) {
|
||||||
|
return line.replace(/\]/, ', "bundle"]')
|
||||||
|
} else if (line.includes('=')) {
|
||||||
|
return line.replace('=', '+= ["bundle",') + ']'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return line
|
||||||
|
})
|
||||||
|
androidConfig.modResults.contents = modifiedLines.join('\n')
|
||||||
|
} else {
|
||||||
|
const androidResources = buildGradle.indexOf('androidResources {')
|
||||||
|
if (androidResources === -1) {
|
||||||
|
throw new Error(
|
||||||
|
`Cannot find androidResources { block in build.gradle!`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const insertPosition = buildGradle.indexOf('\n', androidResources) + 1
|
||||||
|
const newContent =
|
||||||
|
buildGradle.slice(0, insertPosition) +
|
||||||
|
' noCompress += ["bundle"]\n' +
|
||||||
|
buildGradle.slice(insertPosition)
|
||||||
|
|
||||||
|
androidConfig.modResults.contents = newContent
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const androidBlock = buildGradle.indexOf('android {')
|
||||||
|
if (androidBlock === -1) {
|
||||||
|
throw new Error(`Cannot find android { block in build.gradle!`)
|
||||||
|
}
|
||||||
|
const insertPosition = buildGradle.indexOf('\n', androidBlock) + 1
|
||||||
|
const newContent =
|
||||||
|
buildGradle.slice(0, insertPosition) +
|
||||||
|
' androidResources {\n' +
|
||||||
|
' noCompress += ["bundle"]\n' +
|
||||||
|
' }\n' +
|
||||||
|
buildGradle.slice(insertPosition)
|
||||||
|
|
||||||
|
androidConfig.modResults.contents = newContent
|
||||||
|
}
|
||||||
|
|
||||||
|
return androidConfig
|
||||||
|
})
|
||||||
|
}
|
||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
import 'react-native-url-polyfill/auto'
|
import 'react-native-url-polyfill/auto'
|
||||||
import '#/lib/sentry' // must be near top
|
import '#/logger/sentry/setup'
|
||||||
import '#/lib/bitdrift' // must be near top
|
import '#/logger/bitdrift/setup'
|
||||||
import '#/view/icons'
|
import '#/view/icons'
|
||||||
|
|
||||||
import React, {useEffect, useState} from 'react'
|
import React, {useEffect, useState} from 'react'
|
||||||
@@ -118,7 +118,7 @@ function InnerApp() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return listenSessionDropped(() => {
|
return listenSessionDropped(() => {
|
||||||
Toast.show(
|
Toast.show(
|
||||||
_(msg`Sorry! Your session expired. Please log in again.`),
|
_(msg`Sorry! Your session expired. Please sign in again.`),
|
||||||
'info',
|
'info',
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
import '#/lib/sentry' // must be near top
|
import '#/logger/sentry/setup' // must be near top
|
||||||
import '#/view/icons'
|
import '#/view/icons'
|
||||||
import './style.css'
|
import './style.css'
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ function InnerApp() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return listenSessionDropped(() => {
|
return listenSessionDropped(() => {
|
||||||
Toast.show(
|
Toast.show(
|
||||||
_(msg`Sorry! Your session expired. Please log in again.`),
|
_(msg`Sorry! Your session expired. Please sign in again.`),
|
||||||
'info',
|
'info',
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
+20
-4
@@ -32,6 +32,7 @@ import {
|
|||||||
import {RouteParams, State} from '#/lib/routes/types'
|
import {RouteParams, State} from '#/lib/routes/types'
|
||||||
import {attachRouteToLogEvents, logEvent} from '#/lib/statsig/statsig'
|
import {attachRouteToLogEvents, logEvent} from '#/lib/statsig/statsig'
|
||||||
import {bskyTitle} from '#/lib/strings/headings'
|
import {bskyTitle} from '#/lib/strings/headings'
|
||||||
|
import {logger} from '#/logger'
|
||||||
import {isNative, isWeb} from '#/platform/detection'
|
import {isNative, isWeb} from '#/platform/detection'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
import {useUnreadNotifications} from '#/state/queries/notifications/unread'
|
import {useUnreadNotifications} from '#/state/queries/notifications/unread'
|
||||||
@@ -68,6 +69,7 @@ import {SharedPreferencesTesterScreen} from '#/screens/E2E/SharedPreferencesTest
|
|||||||
import HashtagScreen from '#/screens/Hashtag'
|
import HashtagScreen from '#/screens/Hashtag'
|
||||||
import {MessagesScreen} from '#/screens/Messages/ChatList'
|
import {MessagesScreen} from '#/screens/Messages/ChatList'
|
||||||
import {MessagesConversationScreen} from '#/screens/Messages/Conversation'
|
import {MessagesConversationScreen} from '#/screens/Messages/Conversation'
|
||||||
|
import {MessagesInboxScreen} from '#/screens/Messages/Inbox'
|
||||||
import {MessagesSettingsScreen} from '#/screens/Messages/Settings'
|
import {MessagesSettingsScreen} from '#/screens/Messages/Settings'
|
||||||
import {ModerationScreen} from '#/screens/Moderation'
|
import {ModerationScreen} from '#/screens/Moderation'
|
||||||
import {Screen as ModerationInteractionSettings} from '#/screens/ModerationInteractionSettings'
|
import {Screen as ModerationInteractionSettings} from '#/screens/ModerationInteractionSettings'
|
||||||
@@ -91,6 +93,7 @@ import {VideoFeed} from '#/screens/VideoFeed'
|
|||||||
import {useTheme} from '#/alf'
|
import {useTheme} from '#/alf'
|
||||||
import {router} from '#/routes'
|
import {router} from '#/routes'
|
||||||
import {Referrer} from '../modules/expo-bluesky-swiss-army'
|
import {Referrer} from '../modules/expo-bluesky-swiss-army'
|
||||||
|
import {ProfileSearchScreen} from './screens/Profile/ProfileSearch'
|
||||||
import {AboutSettingsScreen} from './screens/Settings/AboutSettings'
|
import {AboutSettingsScreen} from './screens/Settings/AboutSettings'
|
||||||
import {AccessibilitySettingsScreen} from './screens/Settings/AccessibilitySettings'
|
import {AccessibilitySettingsScreen} from './screens/Settings/AccessibilitySettings'
|
||||||
import {AccountSettingsScreen} from './screens/Settings/AccountSettings'
|
import {AccountSettingsScreen} from './screens/Settings/AccountSettings'
|
||||||
@@ -207,6 +210,13 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
|
|||||||
getComponent={() => ProfileListScreen}
|
getComponent={() => ProfileListScreen}
|
||||||
options={{title: title(msg`List`), requireAuth: true}}
|
options={{title: title(msg`List`), requireAuth: true}}
|
||||||
/>
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name="ProfileSearch"
|
||||||
|
getComponent={() => ProfileSearchScreen}
|
||||||
|
options={({route}) => ({
|
||||||
|
title: title(msg`Search @${route.params.name}'s posts`),
|
||||||
|
})}
|
||||||
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="PostThread"
|
name="PostThread"
|
||||||
getComponent={() => PostThreadScreen}
|
getComponent={() => PostThreadScreen}
|
||||||
@@ -402,6 +412,11 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
|
|||||||
getComponent={() => MessagesSettingsScreen}
|
getComponent={() => MessagesSettingsScreen}
|
||||||
options={{title: title(msg`Chat settings`), requireAuth: true}}
|
options={{title: title(msg`Chat settings`), requireAuth: true}}
|
||||||
/>
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name="MessagesInbox"
|
||||||
|
getComponent={() => MessagesInboxScreen}
|
||||||
|
options={{title: title(msg`Chat request inbox`), requireAuth: true}}
|
||||||
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="NotificationSettings"
|
name="NotificationSettings"
|
||||||
getComponent={() => NotificationSettingsScreen}
|
getComponent={() => NotificationSettingsScreen}
|
||||||
@@ -721,15 +736,16 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
|
|||||||
linking={LINKING}
|
linking={LINKING}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
onStateChange={() => {
|
onStateChange={() => {
|
||||||
const routeName = getCurrentRouteName()
|
logger.metric('router:navigate', {
|
||||||
if (routeName === 'Notifications') {
|
from: prevLoggedRouteName.current,
|
||||||
logEvent('router:navigate:notifications', {})
|
})
|
||||||
}
|
prevLoggedRouteName.current = getCurrentRouteName()
|
||||||
}}
|
}}
|
||||||
onReady={() => {
|
onReady={() => {
|
||||||
attachRouteToLogEvents(getCurrentRouteName)
|
attachRouteToLogEvents(getCurrentRouteName)
|
||||||
logModuleInitTime()
|
logModuleInitTime()
|
||||||
onReady()
|
onReady()
|
||||||
|
logger.metric('router:navigate', {})
|
||||||
}}>
|
}}>
|
||||||
{children}
|
{children}
|
||||||
</NavigationContainer>
|
</NavigationContainer>
|
||||||
|
|||||||
@@ -31,6 +31,18 @@ export const atoms = {
|
|||||||
right: 0,
|
right: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
},
|
},
|
||||||
|
top_0: {
|
||||||
|
top: 0,
|
||||||
|
},
|
||||||
|
right_0: {
|
||||||
|
right: 0,
|
||||||
|
},
|
||||||
|
bottom_0: {
|
||||||
|
bottom: 0,
|
||||||
|
},
|
||||||
|
left_0: {
|
||||||
|
left: 0,
|
||||||
|
},
|
||||||
z_10: {
|
z_10: {
|
||||||
zIndex: 10,
|
zIndex: 10,
|
||||||
},
|
},
|
||||||
@@ -93,6 +105,9 @@ export const atoms = {
|
|||||||
/*
|
/*
|
||||||
* Border radius
|
* Border radius
|
||||||
*/
|
*/
|
||||||
|
rounded_0: {
|
||||||
|
borderRadius: 0,
|
||||||
|
},
|
||||||
rounded_2xs: {
|
rounded_2xs: {
|
||||||
borderRadius: tokens.borderRadius._2xs,
|
borderRadius: tokens.borderRadius._2xs,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -26,3 +26,18 @@ export function useBreakpoints(): Record<Breakpoint, boolean> & {
|
|||||||
}
|
}
|
||||||
}, [gtPhone, gtMobile, gtTablet])
|
}, [gtPhone, gtMobile, gtTablet])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fine-tuned breakpoints for the shell layout
|
||||||
|
*/
|
||||||
|
export function useLayoutBreakpoints() {
|
||||||
|
const rightNavVisible = useMediaQuery({minWidth: 1075})
|
||||||
|
const centerColumnOffset = useMediaQuery({minWidth: 1075, maxWidth: 1300})
|
||||||
|
const leftNavMinimal = useMediaQuery({maxWidth: 1300})
|
||||||
|
|
||||||
|
return {
|
||||||
|
rightNavVisible,
|
||||||
|
centerColumnOffset,
|
||||||
|
leftNavMinimal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export function AccountList({
|
|||||||
testID="chooseAddAccountBtn"
|
testID="chooseAddAccountBtn"
|
||||||
style={[a.flex_1]}
|
style={[a.flex_1]}
|
||||||
onPress={pendingDid ? undefined : onPressAddAccount}
|
onPress={pendingDid ? undefined : onPressAddAccount}
|
||||||
label={_(msg`Login to account that is not listed`)}>
|
label={_(msg`Sign in to account that is not listed`)}>
|
||||||
{({hovered, pressed}) => (
|
{({hovered, pressed}) => (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React from 'react'
|
|||||||
import {StyleProp, View, ViewStyle} from 'react-native'
|
import {StyleProp, View, ViewStyle} from 'react-native'
|
||||||
|
|
||||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||||
|
import {Button as BaseButton, ButtonProps} from '#/components/Button'
|
||||||
import {CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon} from '#/components/icons/CircleInfo'
|
import {CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon} from '#/components/icons/CircleInfo'
|
||||||
import {Eye_Stroke2_Corner0_Rounded as InfoIcon} from '#/components/icons/Eye'
|
import {Eye_Stroke2_Corner0_Rounded as InfoIcon} from '#/components/icons/Eye'
|
||||||
import {Leaf_Stroke2_Corner0_Rounded as TipIcon} from '#/components/icons/Leaf'
|
import {Leaf_Stroke2_Corner0_Rounded as TipIcon} from '#/components/icons/Leaf'
|
||||||
@@ -49,22 +50,29 @@ export function Text({
|
|||||||
return (
|
return (
|
||||||
<BaseText
|
<BaseText
|
||||||
{...rest}
|
{...rest}
|
||||||
style={[
|
style={[a.flex_1, a.text_sm, a.leading_snug, a.pr_md, style]}>
|
||||||
a.flex_1,
|
|
||||||
a.text_sm,
|
|
||||||
a.leading_snug,
|
|
||||||
{
|
|
||||||
paddingTop: 1,
|
|
||||||
},
|
|
||||||
style,
|
|
||||||
]}>
|
|
||||||
{children}
|
{children}
|
||||||
</BaseText>
|
</BaseText>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function Button({
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: Omit<ButtonProps, 'size' | 'variant' | 'color'>) {
|
||||||
|
return (
|
||||||
|
<BaseButton size="tiny" variant="outline" color="secondary" {...props}>
|
||||||
|
{children}
|
||||||
|
</BaseButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function Row({children}: {children: React.ReactNode}) {
|
export function Row({children}: {children: React.ReactNode}) {
|
||||||
return <View style={[a.flex_row, a.gap_sm]}>{children}</View>
|
return (
|
||||||
|
<View style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
|
||||||
|
{children}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Outer({
|
export function Outer({
|
||||||
|
|||||||
@@ -1,37 +1,37 @@
|
|||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {moderateProfile} from '@atproto/api'
|
import {moderateProfile} from '@atproto/api'
|
||||||
|
|
||||||
|
import {logger} from '#/logger'
|
||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
import {useProfilesQuery} from '#/state/queries/profile'
|
import {useProfilesQuery} from '#/state/queries/profile'
|
||||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
|
import * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
export function AvatarStack({
|
export function AvatarStack({
|
||||||
profiles,
|
profiles,
|
||||||
size = 26,
|
size = 26,
|
||||||
|
numPending,
|
||||||
|
backgroundColor,
|
||||||
}: {
|
}: {
|
||||||
profiles: string[]
|
profiles: bsky.profile.AnyProfileView[]
|
||||||
size?: number
|
size?: number
|
||||||
|
numPending?: number
|
||||||
|
backgroundColor?: string
|
||||||
}) {
|
}) {
|
||||||
const halfSize = size / 2
|
const halfSize = size / 2
|
||||||
const {data, error} = useProfilesQuery({handles: profiles})
|
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const moderationOpts = useModerationOpts()
|
const moderationOpts = useModerationOpts()
|
||||||
|
|
||||||
if (error) {
|
const isPending = (numPending && profiles.length === 0) || !moderationOpts
|
||||||
console.error(error)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const isPending = !data || !moderationOpts
|
|
||||||
|
|
||||||
const items = isPending
|
const items = isPending
|
||||||
? Array.from({length: profiles.length}).map((_, i) => ({
|
? Array.from({length: numPending ?? profiles.length}).map((_, i) => ({
|
||||||
key: i,
|
key: i,
|
||||||
profile: null,
|
profile: null,
|
||||||
moderation: null,
|
moderation: null,
|
||||||
}))
|
}))
|
||||||
: data.profiles.map(item => ({
|
: profiles.map(item => ({
|
||||||
key: item.did,
|
key: item.did,
|
||||||
profile: item,
|
profile: item,
|
||||||
moderation: moderateProfile(item, moderationOpts),
|
moderation: moderateProfile(item, moderationOpts),
|
||||||
@@ -56,7 +56,7 @@ export function AvatarStack({
|
|||||||
height: size,
|
height: size,
|
||||||
left: i * -halfSize,
|
left: i * -halfSize,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: t.atoms.bg.backgroundColor,
|
borderColor: backgroundColor ?? t.atoms.bg.backgroundColor,
|
||||||
borderRadius: 999,
|
borderRadius: 999,
|
||||||
zIndex: 3 - i,
|
zIndex: 3 - i,
|
||||||
},
|
},
|
||||||
@@ -74,3 +74,33 @@ export function AvatarStack({
|
|||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function AvatarStackWithFetch({
|
||||||
|
profiles,
|
||||||
|
size,
|
||||||
|
backgroundColor,
|
||||||
|
}: {
|
||||||
|
profiles: string[]
|
||||||
|
size?: number
|
||||||
|
backgroundColor?: string
|
||||||
|
}) {
|
||||||
|
const {data, error} = useProfilesQuery({handles: profiles})
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
if (error.name !== 'AbortError') {
|
||||||
|
logger.error('Error fetching profiles for AvatarStack', {
|
||||||
|
safeMessage: error,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AvatarStack
|
||||||
|
numPending={profiles.length}
|
||||||
|
profiles={data?.profiles || []}
|
||||||
|
size={size}
|
||||||
|
backgroundColor={backgroundColor}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export const Context = React.createContext<DialogContextProps>({
|
|||||||
nativeSnapPoint: BottomSheetSnapPoint.Hidden,
|
nativeSnapPoint: BottomSheetSnapPoint.Hidden,
|
||||||
disableDrag: false,
|
disableDrag: false,
|
||||||
setDisableDrag: () => {},
|
setDisableDrag: () => {},
|
||||||
|
isWithinDialog: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
export function useDialogContext() {
|
export function useDialogContext() {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import {isAndroid, isIOS} from '#/platform/detection'
|
|||||||
import {useA11y} from '#/state/a11y'
|
import {useA11y} from '#/state/a11y'
|
||||||
import {useDialogStateControlContext} from '#/state/dialogs'
|
import {useDialogStateControlContext} from '#/state/dialogs'
|
||||||
import {List, ListMethods, ListProps} from '#/view/com/util/List'
|
import {List, ListMethods, ListProps} from '#/view/com/util/List'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, tokens, useTheme} from '#/alf'
|
||||||
import {useThemeName} from '#/alf/util/useColorModeTheme'
|
import {useThemeName} from '#/alf/util/useColorModeTheme'
|
||||||
import {Context, useDialogContext} from '#/components/Dialog/context'
|
import {Context, useDialogContext} from '#/components/Dialog/context'
|
||||||
import {
|
import {
|
||||||
@@ -46,7 +46,7 @@ export {useDialogContext, useDialogControl} from '#/components/Dialog/context'
|
|||||||
export * from '#/components/Dialog/shared'
|
export * from '#/components/Dialog/shared'
|
||||||
export * from '#/components/Dialog/types'
|
export * from '#/components/Dialog/types'
|
||||||
export * from '#/components/Dialog/utils'
|
export * from '#/components/Dialog/utils'
|
||||||
// @ts-ignore
|
|
||||||
export const Input = createInput(TextInput)
|
export const Input = createInput(TextInput)
|
||||||
|
|
||||||
export function Outer({
|
export function Outer({
|
||||||
@@ -154,6 +154,7 @@ export function Outer({
|
|||||||
nativeSnapPoint: snapPoint,
|
nativeSnapPoint: snapPoint,
|
||||||
disableDrag,
|
disableDrag,
|
||||||
setDisableDrag,
|
setDisableDrag,
|
||||||
|
isWithinDialog: true,
|
||||||
}),
|
}),
|
||||||
[close, snapPoint, disableDrag, setDisableDrag],
|
[close, snapPoint, disableDrag, setDisableDrag],
|
||||||
)
|
)
|
||||||
@@ -168,7 +169,9 @@ export function Outer({
|
|||||||
onStateChange={onStateChange}
|
onStateChange={onStateChange}
|
||||||
disableDrag={disableDrag}>
|
disableDrag={disableDrag}>
|
||||||
<Context.Provider value={context}>
|
<Context.Provider value={context}>
|
||||||
<View testID={testID}>{children}</View>
|
<View testID={testID} style={[a.relative]}>
|
||||||
|
{children}
|
||||||
|
</View>
|
||||||
</Context.Provider>
|
</Context.Provider>
|
||||||
</BottomSheet>
|
</BottomSheet>
|
||||||
)
|
)
|
||||||
@@ -196,7 +199,7 @@ export function Inner({children, style, header}: DialogInnerProps) {
|
|||||||
|
|
||||||
export const ScrollableInner = React.forwardRef<ScrollView, DialogInnerProps>(
|
export const ScrollableInner = React.forwardRef<ScrollView, DialogInnerProps>(
|
||||||
function ScrollableInner(
|
function ScrollableInner(
|
||||||
{children, style, contentContainerStyle, header, ...props},
|
{children, contentContainerStyle, header, ...props},
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext()
|
const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext()
|
||||||
@@ -216,13 +219,21 @@ export const ScrollableInner = React.forwardRef<ScrollView, DialogInnerProps>(
|
|||||||
[],
|
[],
|
||||||
)
|
)
|
||||||
|
|
||||||
const basePading =
|
let paddingBottom = 0
|
||||||
(isIOS ? 30 : 50) + (isIOS ? keyboardHeight / 4 : keyboardHeight)
|
if (isIOS) {
|
||||||
const fullPaddingBase = insets.bottom + insets.top + basePading
|
paddingBottom += keyboardHeight / 4
|
||||||
const fullPadding = isIOS ? fullPaddingBase : fullPaddingBase + 50
|
if (nativeSnapPoint === BottomSheetSnapPoint.Full) {
|
||||||
|
paddingBottom += insets.bottom + tokens.space.md
|
||||||
const paddingBottom =
|
}
|
||||||
nativeSnapPoint === BottomSheetSnapPoint.Full ? fullPadding : basePading
|
paddingBottom = Math.max(paddingBottom, tokens.space._2xl)
|
||||||
|
} else {
|
||||||
|
paddingBottom += keyboardHeight
|
||||||
|
if (nativeSnapPoint === BottomSheetSnapPoint.Full) {
|
||||||
|
paddingBottom += insets.top
|
||||||
|
}
|
||||||
|
paddingBottom +=
|
||||||
|
Math.max(insets.bottom, tokens.space._5xl) + tokens.space._2xl
|
||||||
|
}
|
||||||
|
|
||||||
const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||||
if (!isAndroid) {
|
if (!isAndroid) {
|
||||||
@@ -238,7 +249,6 @@ export const ScrollableInner = React.forwardRef<ScrollView, DialogInnerProps>(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<KeyboardAwareScrollView
|
<KeyboardAwareScrollView
|
||||||
style={[style]}
|
|
||||||
contentContainerStyle={[
|
contentContainerStyle={[
|
||||||
a.pt_2xl,
|
a.pt_2xl,
|
||||||
a.px_xl,
|
a.px_xl,
|
||||||
@@ -316,7 +326,7 @@ export function Handle() {
|
|||||||
style={[
|
style={[
|
||||||
a.rounded_sm,
|
a.rounded_sm,
|
||||||
{
|
{
|
||||||
top: 10,
|
top: tokens.space._2xl / 2 - 2.5,
|
||||||
width: 35,
|
width: 35,
|
||||||
height: 5,
|
height: 5,
|
||||||
alignSelf: 'center',
|
alignSelf: 'center',
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export function Outer({
|
|||||||
children,
|
children,
|
||||||
control,
|
control,
|
||||||
onClose,
|
onClose,
|
||||||
|
webOptions,
|
||||||
}: React.PropsWithChildren<DialogOuterProps>) {
|
}: React.PropsWithChildren<DialogOuterProps>) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile} = useBreakpoints()
|
||||||
@@ -96,6 +97,7 @@ export function Outer({
|
|||||||
nativeSnapPoint: 0,
|
nativeSnapPoint: 0,
|
||||||
disableDrag: false,
|
disableDrag: false,
|
||||||
setDisableDrag: () => {},
|
setDisableDrag: () => {},
|
||||||
|
isWithinDialog: true,
|
||||||
}),
|
}),
|
||||||
[close],
|
[close],
|
||||||
)
|
)
|
||||||
@@ -115,20 +117,26 @@ export function Outer({
|
|||||||
web(a.fixed),
|
web(a.fixed),
|
||||||
a.inset_0,
|
a.inset_0,
|
||||||
a.z_10,
|
a.z_10,
|
||||||
|
a.px_xl,
|
||||||
|
webOptions?.alignCenter ? a.justify_center : undefined,
|
||||||
a.align_center,
|
a.align_center,
|
||||||
gtMobile ? a.p_lg : a.p_md,
|
{
|
||||||
{overflowY: 'auto'},
|
overflowY: 'auto',
|
||||||
|
paddingVertical: gtMobile ? '10vh' : a.pt_xl.paddingTop,
|
||||||
|
},
|
||||||
]}>
|
]}>
|
||||||
<Backdrop />
|
<Backdrop />
|
||||||
|
{/**
|
||||||
|
* This is needed to prevent centered dialogs from overflowing
|
||||||
|
* above the screen, and provides a "natural" centering so that
|
||||||
|
* stacked dialogs appear relatively aligned.
|
||||||
|
*/}
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
a.w_full,
|
a.w_full,
|
||||||
a.z_20,
|
a.z_20,
|
||||||
a.justify_center,
|
|
||||||
a.align_center,
|
a.align_center,
|
||||||
{
|
web({minHeight: '60vh', position: 'static'}),
|
||||||
minHeight: web('calc(90vh - 36px)') || undefined,
|
|
||||||
},
|
|
||||||
]}>
|
]}>
|
||||||
{children}
|
{children}
|
||||||
</View>
|
</View>
|
||||||
@@ -217,8 +225,8 @@ export const InnerFlatList = React.forwardRef<
|
|||||||
style={[
|
style={[
|
||||||
a.overflow_hidden,
|
a.overflow_hidden,
|
||||||
a.px_0,
|
a.px_0,
|
||||||
// @ts-expect-error web only -sfn
|
// 100 minus 10vh of paddingVertical
|
||||||
{maxHeight: 'calc(-36px + 100vh)'},
|
web({maxHeight: '80vh'}),
|
||||||
webInnerStyle,
|
webInnerStyle,
|
||||||
]}
|
]}
|
||||||
contentContainerStyle={[a.px_0, webInnerContentContainerStyle]}>
|
contentContainerStyle={[a.px_0, webInnerContentContainerStyle]}>
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ export type DialogContextProps = {
|
|||||||
nativeSnapPoint: BottomSheetSnapPoint
|
nativeSnapPoint: BottomSheetSnapPoint
|
||||||
disableDrag: boolean
|
disableDrag: boolean
|
||||||
setDisableDrag: React.Dispatch<React.SetStateAction<boolean>>
|
setDisableDrag: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
|
// in the event that the hook is used outside of a dialog
|
||||||
|
isWithinDialog: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DialogControlOpenOptions = {
|
export type DialogControlOpenOptions = {
|
||||||
@@ -60,11 +62,16 @@ export type DialogOuterProps = {
|
|||||||
control: DialogControlProps
|
control: DialogControlProps
|
||||||
onClose?: () => void
|
onClose?: () => void
|
||||||
nativeOptions?: Omit<BottomSheetViewProps, 'children'>
|
nativeOptions?: Omit<BottomSheetViewProps, 'children'>
|
||||||
webOptions?: {}
|
webOptions?: {
|
||||||
|
alignCenter?: boolean
|
||||||
|
}
|
||||||
testID?: string
|
testID?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type DialogInnerPropsBase<T> = React.PropsWithChildren<ViewStyleProp> & T
|
type DialogInnerPropsBase<T> = React.PropsWithChildren<ViewStyleProp> &
|
||||||
|
T & {
|
||||||
|
testID?: string
|
||||||
|
}
|
||||||
export type DialogInnerProps =
|
export type DialogInnerProps =
|
||||||
| DialogInnerPropsBase<{
|
| DialogInnerPropsBase<{
|
||||||
label?: undefined
|
label?: undefined
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {GestureResponderEvent, View} from 'react-native'
|
import {GestureResponderEvent, View} from 'react-native'
|
||||||
import {
|
import {
|
||||||
AppBskyActorDefs,
|
|
||||||
AppBskyFeedDefs,
|
AppBskyFeedDefs,
|
||||||
AppBskyGraphDefs,
|
AppBskyGraphDefs,
|
||||||
AtUri,
|
AtUri,
|
||||||
@@ -32,6 +31,7 @@ import {Loader} from '#/components/Loader'
|
|||||||
import * as Prompt from '#/components/Prompt'
|
import * as Prompt from '#/components/Prompt'
|
||||||
import {RichText, RichTextProps} from '#/components/RichText'
|
import {RichText, RichTextProps} from '#/components/RichText'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
view: AppBskyFeedDefs.GeneratorView
|
view: AppBskyFeedDefs.GeneratorView
|
||||||
@@ -115,7 +115,7 @@ export function TitleAndByline({
|
|||||||
creator,
|
creator,
|
||||||
}: {
|
}: {
|
||||||
title: string
|
title: string
|
||||||
creator?: AppBskyActorDefs.ProfileViewBasic
|
creator?: bsky.profile.AnyProfileView
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
|
||||||
@@ -269,9 +269,9 @@ function SaveButtonInner({
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
Toast.show(_(msg`Feeds updated!`))
|
Toast.show(_(msg({message: 'Feeds updated!', context: 'toast'})))
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
logger.error(err, {context: `FeedCard: failed to update feeds`, pin})
|
logger.error(err, {message: `FeedCard: failed to update feeds`, pin})
|
||||||
Toast.show(_(msg`Failed to update feeds`), 'xmark')
|
Toast.show(_(msg`Failed to update feeds`), 'xmark')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -307,7 +307,7 @@ function SaveButtonInner({
|
|||||||
|
|
||||||
<Prompt.Basic
|
<Prompt.Basic
|
||||||
control={removePromptControl}
|
control={removePromptControl}
|
||||||
title={_(msg`Remove from my feeds?`)}
|
title={_(msg`Remove from your feeds?`)}
|
||||||
description={_(
|
description={_(
|
||||||
msg`Are you sure you want to remove this from your feeds?`,
|
msg`Are you sure you want to remove this from your feeds?`,
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {ScrollView, View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {AppBskyActorDefs, AppBskyFeedDefs, AtUri} from '@atproto/api'
|
import {ScrollView} from 'react-native-gesture-handler'
|
||||||
|
import {AppBskyFeedDefs, AtUri} from '@atproto/api'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useNavigation} from '@react-navigation/native'
|
import {useNavigation} from '@react-navigation/native'
|
||||||
@@ -26,6 +27,7 @@ import {PersonPlus_Stroke2_Corner0_Rounded as Person} from '#/components/icons/P
|
|||||||
import {InlineLinkText} from '#/components/Link'
|
import {InlineLinkText} from '#/components/Link'
|
||||||
import * as ProfileCard from '#/components/ProfileCard'
|
import * as ProfileCard from '#/components/ProfileCard'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import * as bsky from '#/types/bsky'
|
||||||
import {ProgressGuideList} from './ProgressGuide/List'
|
import {ProgressGuideList} from './ProgressGuide/List'
|
||||||
|
|
||||||
const MOBILE_CARD_WIDTH = 300
|
const MOBILE_CARD_WIDTH = 300
|
||||||
@@ -227,7 +229,7 @@ export function ProfileGrid({
|
|||||||
viewContext = 'feed',
|
viewContext = 'feed',
|
||||||
}: {
|
}: {
|
||||||
isSuggestionsLoading: boolean
|
isSuggestionsLoading: boolean
|
||||||
profiles: AppBskyActorDefs.ProfileViewDetailed[]
|
profiles: bsky.profile.AnyProfileView[]
|
||||||
recId?: number
|
recId?: number
|
||||||
error: Error | null
|
error: Error | null
|
||||||
viewContext: 'profile' | 'feed'
|
viewContext: 'profile' | 'feed'
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {UserAvatar} from '#/view/com/util/UserAvatar'
|
|||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {Link, LinkProps} from '#/components/Link'
|
import {Link, LinkProps} from '#/components/Link'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
const AVI_SIZE = 30
|
const AVI_SIZE = 30
|
||||||
const AVI_SIZE_SMALL = 20
|
const AVI_SIZE_SMALL = 20
|
||||||
@@ -32,11 +33,13 @@ export function KnownFollowers({
|
|||||||
moderationOpts,
|
moderationOpts,
|
||||||
onLinkPress,
|
onLinkPress,
|
||||||
minimal,
|
minimal,
|
||||||
|
showIfEmpty,
|
||||||
}: {
|
}: {
|
||||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
profile: bsky.profile.AnyProfileView
|
||||||
moderationOpts: ModerationOpts
|
moderationOpts: ModerationOpts
|
||||||
onLinkPress?: LinkProps['onPress']
|
onLinkPress?: LinkProps['onPress']
|
||||||
minimal?: boolean
|
minimal?: boolean
|
||||||
|
showIfEmpty?: boolean
|
||||||
}) {
|
}) {
|
||||||
const cache = React.useRef<Map<string, AppBskyActorDefs.KnownFollowers>>(
|
const cache = React.useRef<Map<string, AppBskyActorDefs.KnownFollowers>>(
|
||||||
new Map(),
|
new Map(),
|
||||||
@@ -63,11 +66,12 @@ export function KnownFollowers({
|
|||||||
moderationOpts={moderationOpts}
|
moderationOpts={moderationOpts}
|
||||||
onLinkPress={onLinkPress}
|
onLinkPress={onLinkPress}
|
||||||
minimal={minimal}
|
minimal={minimal}
|
||||||
|
showIfEmpty={showIfEmpty}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return <EmptyFallback show={showIfEmpty} />
|
||||||
}
|
}
|
||||||
|
|
||||||
function KnownFollowersInner({
|
function KnownFollowersInner({
|
||||||
@@ -76,22 +80,19 @@ function KnownFollowersInner({
|
|||||||
cachedKnownFollowers,
|
cachedKnownFollowers,
|
||||||
onLinkPress,
|
onLinkPress,
|
||||||
minimal,
|
minimal,
|
||||||
|
showIfEmpty,
|
||||||
}: {
|
}: {
|
||||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
profile: bsky.profile.AnyProfileView
|
||||||
moderationOpts: ModerationOpts
|
moderationOpts: ModerationOpts
|
||||||
cachedKnownFollowers: AppBskyActorDefs.KnownFollowers
|
cachedKnownFollowers: AppBskyActorDefs.KnownFollowers
|
||||||
onLinkPress?: LinkProps['onPress']
|
onLinkPress?: LinkProps['onPress']
|
||||||
minimal?: boolean
|
minimal?: boolean
|
||||||
|
showIfEmpty?: boolean
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
|
||||||
const textStyle = [
|
const textStyle = [a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]
|
||||||
a.flex_1,
|
|
||||||
a.text_sm,
|
|
||||||
a.leading_snug,
|
|
||||||
t.atoms.text_contrast_medium,
|
|
||||||
]
|
|
||||||
|
|
||||||
const slice = cachedKnownFollowers.followers.slice(0, 3).map(f => {
|
const slice = cachedKnownFollowers.followers.slice(0, 3).map(f => {
|
||||||
const moderation = moderateProfile(f, moderationOpts)
|
const moderation = moderateProfile(f, moderationOpts)
|
||||||
@@ -114,7 +115,7 @@ function KnownFollowersInner({
|
|||||||
* We check above too, but here for clarity and a reminder to _check for
|
* We check above too, but here for clarity and a reminder to _check for
|
||||||
* valid indices_
|
* valid indices_
|
||||||
*/
|
*/
|
||||||
if (slice.length === 0) return null
|
if (slice.length === 0) return <EmptyFallback show={showIfEmpty} />
|
||||||
|
|
||||||
const SIZE = minimal ? AVI_SIZE_SMALL : AVI_SIZE
|
const SIZE = minimal ? AVI_SIZE_SMALL : AVI_SIZE
|
||||||
|
|
||||||
@@ -126,7 +127,6 @@ function KnownFollowersInner({
|
|||||||
onPress={onLinkPress}
|
onPress={onLinkPress}
|
||||||
to={makeProfileLink(profile, 'known-followers')}
|
to={makeProfileLink(profile, 'known-followers')}
|
||||||
style={[
|
style={[
|
||||||
a.flex_1,
|
|
||||||
a.flex_row,
|
a.flex_row,
|
||||||
minimal ? a.gap_sm : a.gap_md,
|
minimal ? a.gap_sm : a.gap_md,
|
||||||
a.align_center,
|
a.align_center,
|
||||||
@@ -242,3 +242,15 @@ function KnownFollowersInner({
|
|||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function EmptyFallback({show}: {show?: boolean}) {
|
||||||
|
const t = useTheme()
|
||||||
|
|
||||||
|
if (!show) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Text style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
|
||||||
|
<Trans>Not followed by anyone you're following</Trans>
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
TextStyleProp,
|
TextStyleProp,
|
||||||
useBreakpoints,
|
useBreakpoints,
|
||||||
useGutters,
|
useGutters,
|
||||||
|
useLayoutBreakpoints,
|
||||||
useTheme,
|
useTheme,
|
||||||
web,
|
web,
|
||||||
} from '#/alf'
|
} from '#/alf'
|
||||||
@@ -23,6 +24,7 @@ import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
|
|||||||
import {
|
import {
|
||||||
BUTTON_VISUAL_ALIGNMENT_OFFSET,
|
BUTTON_VISUAL_ALIGNMENT_OFFSET,
|
||||||
HEADER_SLOT_SIZE,
|
HEADER_SLOT_SIZE,
|
||||||
|
SCROLLBAR_OFFSET,
|
||||||
} from '#/components/Layout/const'
|
} from '#/components/Layout/const'
|
||||||
import {ScrollbarOffsetContext} from '#/components/Layout/context'
|
import {ScrollbarOffsetContext} from '#/components/Layout/context'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
@@ -42,6 +44,7 @@ export function Outer({
|
|||||||
const gutters = useGutters([0, 'base'])
|
const gutters = useGutters([0, 'base'])
|
||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile} = useBreakpoints()
|
||||||
const {isWithinOffsetView} = useContext(ScrollbarOffsetContext)
|
const {isWithinOffsetView} = useContext(ScrollbarOffsetContext)
|
||||||
|
const {centerColumnOffset} = useLayoutBreakpoints()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
@@ -60,7 +63,12 @@ export function Outer({
|
|||||||
}),
|
}),
|
||||||
t.atoms.border_contrast_low,
|
t.atoms.border_contrast_low,
|
||||||
gtMobile && [a.mx_auto, {maxWidth: 600}],
|
gtMobile && [a.mx_auto, {maxWidth: 600}],
|
||||||
!isWithinOffsetView && a.scrollbar_offset,
|
!isWithinOffsetView && {
|
||||||
|
transform: [
|
||||||
|
{translateX: centerColumnOffset ? -150 : 0},
|
||||||
|
{translateX: web(SCROLLBAR_OFFSET) ?? 0},
|
||||||
|
],
|
||||||
|
},
|
||||||
]}>
|
]}>
|
||||||
{children}
|
{children}
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -13,7 +13,15 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
|||||||
|
|
||||||
import {isWeb} from '#/platform/detection'
|
import {isWeb} from '#/platform/detection'
|
||||||
import {useShellLayout} from '#/state/shell/shell-layout'
|
import {useShellLayout} from '#/state/shell/shell-layout'
|
||||||
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
|
import {
|
||||||
|
atoms as a,
|
||||||
|
useBreakpoints,
|
||||||
|
useLayoutBreakpoints,
|
||||||
|
useTheme,
|
||||||
|
web,
|
||||||
|
} from '#/alf'
|
||||||
|
import {useDialogContext} from '#/components/Dialog'
|
||||||
|
import {SCROLLBAR_OFFSET} from '#/components/Layout/const'
|
||||||
import {ScrollbarOffsetContext} from '#/components/Layout/context'
|
import {ScrollbarOffsetContext} from '#/components/Layout/context'
|
||||||
|
|
||||||
export * from '#/components/Layout/const'
|
export * from '#/components/Layout/const'
|
||||||
@@ -47,6 +55,7 @@ export const Screen = React.memo(function Screen({
|
|||||||
export type ContentProps = AnimatedScrollViewProps & {
|
export type ContentProps = AnimatedScrollViewProps & {
|
||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
contentContainerStyle?: StyleProp<ViewStyle>
|
contentContainerStyle?: StyleProp<ViewStyle>
|
||||||
|
ignoreTabletLayoutOffset?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -56,6 +65,7 @@ export const Content = React.memo(function Content({
|
|||||||
children,
|
children,
|
||||||
style,
|
style,
|
||||||
contentContainerStyle,
|
contentContainerStyle,
|
||||||
|
ignoreTabletLayoutOffset,
|
||||||
...props
|
...props
|
||||||
}: ContentProps) {
|
}: ContentProps) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
@@ -84,8 +94,10 @@ export const Content = React.memo(function Content({
|
|||||||
]}
|
]}
|
||||||
{...props}>
|
{...props}>
|
||||||
{isWeb ? (
|
{isWeb ? (
|
||||||
// @ts-ignore web only -esb
|
<Center ignoreTabletLayoutOffset={ignoreTabletLayoutOffset}>
|
||||||
<Center>{children}</Center>
|
{/* @ts-expect-error web only -esb */}
|
||||||
|
{children}
|
||||||
|
</Center>
|
||||||
) : (
|
) : (
|
||||||
children
|
children
|
||||||
)}
|
)}
|
||||||
@@ -138,10 +150,13 @@ export const KeyboardAwareContent = React.memo(function LayoutScrollView({
|
|||||||
export const Center = React.memo(function LayoutContent({
|
export const Center = React.memo(function LayoutContent({
|
||||||
children,
|
children,
|
||||||
style,
|
style,
|
||||||
|
ignoreTabletLayoutOffset,
|
||||||
...props
|
...props
|
||||||
}: ViewProps) {
|
}: ViewProps & {ignoreTabletLayoutOffset?: boolean}) {
|
||||||
const {isWithinOffsetView} = useContext(ScrollbarOffsetContext)
|
const {isWithinOffsetView} = useContext(ScrollbarOffsetContext)
|
||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile} = useBreakpoints()
|
||||||
|
const {centerColumnOffset} = useLayoutBreakpoints()
|
||||||
|
const {isWithinDialog} = useDialogContext()
|
||||||
const ctx = useMemo(() => ({isWithinOffsetView: true}), [])
|
const ctx = useMemo(() => ({isWithinOffsetView: true}), [])
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
@@ -151,8 +166,20 @@ export const Center = React.memo(function LayoutContent({
|
|||||||
gtMobile && {
|
gtMobile && {
|
||||||
maxWidth: 600,
|
maxWidth: 600,
|
||||||
},
|
},
|
||||||
|
!isWithinOffsetView && {
|
||||||
|
transform: [
|
||||||
|
{
|
||||||
|
translateX:
|
||||||
|
centerColumnOffset &&
|
||||||
|
!ignoreTabletLayoutOffset &&
|
||||||
|
!isWithinDialog
|
||||||
|
? -150
|
||||||
|
: 0,
|
||||||
|
},
|
||||||
|
{translateX: web(SCROLLBAR_OFFSET) ?? 0},
|
||||||
|
],
|
||||||
|
},
|
||||||
style,
|
style,
|
||||||
!isWithinOffsetView && a.scrollbar_offset,
|
|
||||||
]}
|
]}
|
||||||
{...props}>
|
{...props}>
|
||||||
<ScrollbarOffsetContext.Provider value={ctx}>
|
<ScrollbarOffsetContext.Provider value={ctx}>
|
||||||
@@ -168,6 +195,7 @@ export const Center = React.memo(function LayoutContent({
|
|||||||
const WebCenterBorders = React.memo(function LayoutContent() {
|
const WebCenterBorders = React.memo(function LayoutContent() {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile} = useBreakpoints()
|
||||||
|
const {centerColumnOffset} = useLayoutBreakpoints()
|
||||||
return gtMobile ? (
|
return gtMobile ? (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
@@ -180,9 +208,8 @@ const WebCenterBorders = React.memo(function LayoutContent() {
|
|||||||
width: 602,
|
width: 602,
|
||||||
left: '50%',
|
left: '50%',
|
||||||
transform: [
|
transform: [
|
||||||
{
|
{translateX: '-50%'},
|
||||||
translateX: '-50%',
|
{translateX: centerColumnOffset ? -150 : 0},
|
||||||
},
|
|
||||||
...a.scrollbar_offset.transform,
|
...a.scrollbar_offset.transform,
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
|||||||
+13
-1
@@ -69,6 +69,11 @@ type BaseLinkProps = Pick<
|
|||||||
* Native-only attribute. If true, will open the share sheet on long press.
|
* Native-only attribute. If true, will open the share sheet on long press.
|
||||||
*/
|
*/
|
||||||
shareOnLongPress?: boolean
|
shareOnLongPress?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the link should be opened through the redirect proxy.
|
||||||
|
*/
|
||||||
|
shouldProxy?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useLink({
|
export function useLink({
|
||||||
@@ -80,9 +85,11 @@ export function useLink({
|
|||||||
onLongPress: outerOnLongPress,
|
onLongPress: outerOnLongPress,
|
||||||
shareOnLongPress,
|
shareOnLongPress,
|
||||||
overridePresentation,
|
overridePresentation,
|
||||||
|
shouldProxy,
|
||||||
}: BaseLinkProps & {
|
}: BaseLinkProps & {
|
||||||
displayText: string
|
displayText: string
|
||||||
overridePresentation?: boolean
|
overridePresentation?: boolean
|
||||||
|
shouldProxy?: boolean
|
||||||
}) {
|
}) {
|
||||||
const navigation = useNavigationDeduped()
|
const navigation = useNavigationDeduped()
|
||||||
const {href} = useLinkProps<AllNavigatorParams>({
|
const {href} = useLinkProps<AllNavigatorParams>({
|
||||||
@@ -118,7 +125,7 @@ export function useLink({
|
|||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
if (isExternal) {
|
if (isExternal) {
|
||||||
openLink(href, overridePresentation)
|
openLink(href, overridePresentation, shouldProxy)
|
||||||
} else {
|
} else {
|
||||||
const shouldOpenInNewTab = shouldClickOpenNewTab(e)
|
const shouldOpenInNewTab = shouldClickOpenNewTab(e)
|
||||||
|
|
||||||
@@ -161,6 +168,7 @@ export function useLink({
|
|||||||
action,
|
action,
|
||||||
navigation,
|
navigation,
|
||||||
overridePresentation,
|
overridePresentation,
|
||||||
|
shouldProxy,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -219,6 +227,7 @@ export function Link({
|
|||||||
onPress: outerOnPress,
|
onPress: outerOnPress,
|
||||||
onLongPress: outerOnLongPress,
|
onLongPress: outerOnLongPress,
|
||||||
download,
|
download,
|
||||||
|
shouldProxy,
|
||||||
...rest
|
...rest
|
||||||
}: LinkProps) {
|
}: LinkProps) {
|
||||||
const {href, isExternal, onPress, onLongPress} = useLink({
|
const {href, isExternal, onPress, onLongPress} = useLink({
|
||||||
@@ -227,6 +236,7 @@ export function Link({
|
|||||||
action,
|
action,
|
||||||
onPress: outerOnPress,
|
onPress: outerOnPress,
|
||||||
onLongPress: outerOnLongPress,
|
onLongPress: outerOnLongPress,
|
||||||
|
shouldProxy: shouldProxy,
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -279,6 +289,7 @@ export function InlineLinkText({
|
|||||||
shareOnLongPress,
|
shareOnLongPress,
|
||||||
disableUnderline,
|
disableUnderline,
|
||||||
overridePresentation,
|
overridePresentation,
|
||||||
|
shouldProxy,
|
||||||
...rest
|
...rest
|
||||||
}: InlineLinkProps) {
|
}: InlineLinkProps) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
@@ -292,6 +303,7 @@ export function InlineLinkText({
|
|||||||
onLongPress: outerOnLongPress,
|
onLongPress: outerOnLongPress,
|
||||||
shareOnLongPress,
|
shareOnLongPress,
|
||||||
overridePresentation,
|
overridePresentation,
|
||||||
|
shouldProxy: shouldProxy,
|
||||||
})
|
})
|
||||||
const {
|
const {
|
||||||
state: hovered,
|
state: hovered,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {
|
import {
|
||||||
AppBskyActorDefs,
|
|
||||||
AppBskyGraphDefs,
|
AppBskyGraphDefs,
|
||||||
AtUri,
|
AtUri,
|
||||||
moderateUserList,
|
moderateUserList,
|
||||||
@@ -26,6 +25,7 @@ import {
|
|||||||
import {Link as InternalLink, LinkProps} from '#/components/Link'
|
import {Link as InternalLink, LinkProps} from '#/components/Link'
|
||||||
import * as Hider from '#/components/moderation/Hider'
|
import * as Hider from '#/components/moderation/Hider'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* This component is based on `FeedCard` and is tightly coupled with that
|
* This component is based on `FeedCard` and is tightly coupled with that
|
||||||
@@ -107,7 +107,7 @@ export function TitleAndByline({
|
|||||||
modUi,
|
modUi,
|
||||||
}: {
|
}: {
|
||||||
title: string
|
title: string
|
||||||
creator?: AppBskyActorDefs.ProfileViewBasic
|
creator?: bsky.profile.AnyProfileView
|
||||||
purpose?: AppBskyGraphDefs.ListView['purpose']
|
purpose?: AppBskyGraphDefs.ListView['purpose']
|
||||||
modUi?: ModerationUI
|
modUi?: ModerationUI
|
||||||
}) {
|
}) {
|
||||||
|
|||||||
@@ -1,19 +1,15 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
|
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
|
||||||
import {Image} from 'expo-image'
|
import {Image} from 'expo-image'
|
||||||
import {
|
import {AppBskyFeedDefs} from '@atproto/api'
|
||||||
AppBskyEmbedExternal,
|
|
||||||
AppBskyEmbedImages,
|
|
||||||
AppBskyEmbedRecordWithMedia,
|
|
||||||
AppBskyEmbedVideo,
|
|
||||||
} from '@atproto/api'
|
|
||||||
import {Trans} from '@lingui/macro'
|
import {Trans} from '@lingui/macro'
|
||||||
|
|
||||||
import {parseTenorGif} from '#/lib/strings/embed-player'
|
import {isTenorGifUri} from '#/lib/strings/embed-player'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
|
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
|
||||||
|
import * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Streamlined MediaPreview component which just handles images, gifs, and videos
|
* Streamlined MediaPreview component which just handles images, gifs, and videos
|
||||||
@@ -22,20 +18,17 @@ export function Embed({
|
|||||||
embed,
|
embed,
|
||||||
style,
|
style,
|
||||||
}: {
|
}: {
|
||||||
embed?:
|
embed: AppBskyFeedDefs.PostView['embed']
|
||||||
| AppBskyEmbedImages.View
|
|
||||||
| AppBskyEmbedRecordWithMedia.View
|
|
||||||
| AppBskyEmbedExternal.View
|
|
||||||
| AppBskyEmbedVideo.View
|
|
||||||
| {[k: string]: unknown}
|
|
||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
}) {
|
}) {
|
||||||
let media = AppBskyEmbedRecordWithMedia.isView(embed) ? embed.media : embed
|
const e = bsky.post.parseEmbed(embed)
|
||||||
|
|
||||||
if (AppBskyEmbedImages.isView(media)) {
|
if (!e) return null
|
||||||
|
|
||||||
|
if (e.type === 'images') {
|
||||||
return (
|
return (
|
||||||
<Outer style={style}>
|
<Outer style={style}>
|
||||||
{media.images.map(image => (
|
{e.view.images.map(image => (
|
||||||
<ImageItem
|
<ImageItem
|
||||||
key={image.thumb}
|
key={image.thumb}
|
||||||
thumbnail={image.thumb}
|
thumbnail={image.thumb}
|
||||||
@@ -44,28 +37,21 @@ export function Embed({
|
|||||||
))}
|
))}
|
||||||
</Outer>
|
</Outer>
|
||||||
)
|
)
|
||||||
} else if (AppBskyEmbedExternal.isView(media) && media.external.thumb) {
|
} else if (e.type === 'link') {
|
||||||
let url: URL | undefined
|
if (!e.view.external.thumb) return null
|
||||||
try {
|
if (!isTenorGifUri(e.view.external.uri)) return null
|
||||||
url = new URL(media.external.uri)
|
|
||||||
} catch {}
|
|
||||||
if (url) {
|
|
||||||
const {success} = parseTenorGif(url)
|
|
||||||
if (success) {
|
|
||||||
return (
|
return (
|
||||||
<Outer style={style}>
|
<Outer style={style}>
|
||||||
<GifItem
|
<GifItem
|
||||||
thumbnail={media.external.thumb}
|
thumbnail={e.view.external.thumb}
|
||||||
alt={media.external.title}
|
alt={e.view.external.title}
|
||||||
/>
|
/>
|
||||||
</Outer>
|
</Outer>
|
||||||
)
|
)
|
||||||
}
|
} else if (e.type === 'video') {
|
||||||
}
|
|
||||||
} else if (AppBskyEmbedVideo.isView(media)) {
|
|
||||||
return (
|
return (
|
||||||
<Outer style={style}>
|
<Outer style={style}>
|
||||||
<VideoItem thumbnail={media.thumbnail} alt={media.alt} />
|
<VideoItem thumbnail={e.view.thumbnail} alt={e.view.alt} />
|
||||||
</Outer>
|
</Outer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ export function Outer({
|
|||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
{/* Re-wrap with context since Dialogs are portal-ed to root */}
|
{/* Re-wrap with context since Dialogs are portal-ed to root */}
|
||||||
<Context.Provider value={context}>
|
<Context.Provider value={context}>
|
||||||
<Dialog.ScrollableInner label={_(msg`Menu`)} style={[a.py_sm]}>
|
<Dialog.ScrollableInner label={_(msg`Menu`)}>
|
||||||
<View style={[a.gap_lg]}>
|
<View style={[a.gap_lg]}>
|
||||||
{children}
|
{children}
|
||||||
{isNative && showCancel && <Cancel />}
|
{isNative && showCancel && <Cancel />}
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ export function Outer({
|
|||||||
collisionPadding={{left: 5, right: 5, bottom: 5}}
|
collisionPadding={{left: 5, right: 5, bottom: 5}}
|
||||||
loop
|
loop
|
||||||
aria-label="Test"
|
aria-label="Test"
|
||||||
className="dropdown-menu-transform-origin">
|
className="dropdown-menu-transform-origin dropdown-menu-constrain-size">
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
a.rounded_sm,
|
a.rounded_sm,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {GestureResponderEvent, View} from 'react-native'
|
import {GestureResponderEvent, View} from 'react-native'
|
||||||
import {
|
import {
|
||||||
AppBskyActorDefs,
|
|
||||||
moderateProfile,
|
moderateProfile,
|
||||||
ModerationOpts,
|
ModerationOpts,
|
||||||
RichText as RichTextApi,
|
RichText as RichTextApi,
|
||||||
@@ -25,13 +24,14 @@ import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus
|
|||||||
import {Link as InternalLink, LinkProps} from '#/components/Link'
|
import {Link as InternalLink, LinkProps} from '#/components/Link'
|
||||||
import {RichText} from '#/components/RichText'
|
import {RichText} from '#/components/RichText'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
export function Default({
|
export function Default({
|
||||||
profile,
|
profile,
|
||||||
moderationOpts,
|
moderationOpts,
|
||||||
logContext = 'ProfileCard',
|
logContext = 'ProfileCard',
|
||||||
}: {
|
}: {
|
||||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
profile: bsky.profile.AnyProfileView
|
||||||
moderationOpts: ModerationOpts
|
moderationOpts: ModerationOpts
|
||||||
logContext?: 'ProfileCard' | 'StarterPackProfilesList'
|
logContext?: 'ProfileCard' | 'StarterPackProfilesList'
|
||||||
}) {
|
}) {
|
||||||
@@ -51,7 +51,7 @@ export function Card({
|
|||||||
moderationOpts,
|
moderationOpts,
|
||||||
logContext = 'ProfileCard',
|
logContext = 'ProfileCard',
|
||||||
}: {
|
}: {
|
||||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
profile: bsky.profile.AnyProfileView
|
||||||
moderationOpts: ModerationOpts
|
moderationOpts: ModerationOpts
|
||||||
logContext?: 'ProfileCard' | 'StarterPackProfilesList'
|
logContext?: 'ProfileCard' | 'StarterPackProfilesList'
|
||||||
}) {
|
}) {
|
||||||
@@ -101,7 +101,7 @@ export function Link({
|
|||||||
style,
|
style,
|
||||||
...rest
|
...rest
|
||||||
}: {
|
}: {
|
||||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
profile: bsky.profile.AnyProfileView
|
||||||
} & Omit<LinkProps, 'to' | 'label'>) {
|
} & Omit<LinkProps, 'to' | 'label'>) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
return (
|
return (
|
||||||
@@ -126,7 +126,7 @@ export function Avatar({
|
|||||||
profile,
|
profile,
|
||||||
moderationOpts,
|
moderationOpts,
|
||||||
}: {
|
}: {
|
||||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
profile: bsky.profile.AnyProfileView
|
||||||
moderationOpts: ModerationOpts
|
moderationOpts: ModerationOpts
|
||||||
}) {
|
}) {
|
||||||
const moderation = moderateProfile(profile, moderationOpts)
|
const moderation = moderateProfile(profile, moderationOpts)
|
||||||
@@ -161,7 +161,7 @@ export function NameAndHandle({
|
|||||||
profile,
|
profile,
|
||||||
moderationOpts,
|
moderationOpts,
|
||||||
}: {
|
}: {
|
||||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
profile: bsky.profile.AnyProfileView
|
||||||
moderationOpts: ModerationOpts
|
moderationOpts: ModerationOpts
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
@@ -224,17 +224,16 @@ export function Description({
|
|||||||
profile: profileUnshadowed,
|
profile: profileUnshadowed,
|
||||||
numberOfLines = 3,
|
numberOfLines = 3,
|
||||||
}: {
|
}: {
|
||||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
profile: bsky.profile.AnyProfileView
|
||||||
numberOfLines?: number
|
numberOfLines?: number
|
||||||
}) {
|
}) {
|
||||||
const profile = useProfileShadow(profileUnshadowed)
|
const profile = useProfileShadow(profileUnshadowed)
|
||||||
const {description} = profile
|
|
||||||
const rt = React.useMemo(() => {
|
const rt = React.useMemo(() => {
|
||||||
if (!description) return
|
if (!('description' in profile)) return
|
||||||
const rt = new RichTextApi({text: description || ''})
|
const rt = new RichTextApi({text: profile.description || ''})
|
||||||
rt.detectFacetsWithoutResolution()
|
rt.detectFacetsWithoutResolution()
|
||||||
return rt
|
return rt
|
||||||
}, [description])
|
}, [profile])
|
||||||
if (!rt) return null
|
if (!rt) return null
|
||||||
if (
|
if (
|
||||||
profile.viewer &&
|
profile.viewer &&
|
||||||
@@ -281,7 +280,7 @@ export function DescriptionPlaceholder({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type FollowButtonProps = {
|
export type FollowButtonProps = {
|
||||||
profile: AppBskyActorDefs.ProfileViewBasic
|
profile: bsky.profile.AnyProfileView
|
||||||
moderationOpts: ModerationOpts
|
moderationOpts: ModerationOpts
|
||||||
logContext: LogEvents['profile:follow']['logContext'] &
|
logContext: LogEvents['profile:follow']['logContext'] &
|
||||||
LogEvents['profile:unfollow']['logContext']
|
LogEvents['profile:unfollow']['logContext']
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export function Outer({
|
|||||||
<Dialog.Outer
|
<Dialog.Outer
|
||||||
control={control}
|
control={control}
|
||||||
testID={testID}
|
testID={testID}
|
||||||
|
webOptions={{alignCenter: true}}
|
||||||
nativeOptions={{preventExpansion: true, ...nativeOptions}}>
|
nativeOptions={{preventExpansion: true, ...nativeOptions}}>
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
<Context.Provider value={context}>
|
<Context.Provider value={context}>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export type RichTextProps = TextStyleProp &
|
|||||||
onLinkPress?: LinkProps['onPress']
|
onLinkPress?: LinkProps['onPress']
|
||||||
interactiveStyle?: TextStyle
|
interactiveStyle?: TextStyle
|
||||||
emojiMultiplier?: number
|
emojiMultiplier?: number
|
||||||
|
shouldProxyLinks?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RichText({
|
export function RichText({
|
||||||
@@ -39,6 +40,7 @@ export function RichText({
|
|||||||
emojiMultiplier = 1.85,
|
emojiMultiplier = 1.85,
|
||||||
onLayout,
|
onLayout,
|
||||||
onTextLayout,
|
onTextLayout,
|
||||||
|
shouldProxyLinks,
|
||||||
}: RichTextProps) {
|
}: RichTextProps) {
|
||||||
const richText = React.useMemo(
|
const richText = React.useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -110,6 +112,7 @@ export function RichText({
|
|||||||
style={interactiveStyles}
|
style={interactiveStyles}
|
||||||
// @ts-ignore TODO
|
// @ts-ignore TODO
|
||||||
dataSet={WORD_WRAP}
|
dataSet={WORD_WRAP}
|
||||||
|
shouldProxy={shouldProxyLinks}
|
||||||
onPress={onLinkPress}>
|
onPress={onLinkPress}>
|
||||||
{segment.text}
|
{segment.text}
|
||||||
</InlineLinkText>
|
</InlineLinkText>
|
||||||
@@ -128,6 +131,7 @@ export function RichText({
|
|||||||
// @ts-ignore TODO
|
// @ts-ignore TODO
|
||||||
dataSet={WORD_WRAP}
|
dataSet={WORD_WRAP}
|
||||||
shareOnLongPress
|
shareOnLongPress
|
||||||
|
shouldProxy={shouldProxyLinks}
|
||||||
onPress={onLinkPress}
|
onPress={onLinkPress}
|
||||||
emoji>
|
emoji>
|
||||||
{toShortUrl(segment.text)}
|
{toShortUrl(segment.text)}
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import React from 'react'
|
import React, {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useImperativeHandle,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
import {
|
import {
|
||||||
findNodeHandle,
|
findNodeHandle,
|
||||||
ListRenderItemInfo,
|
ListRenderItemInfo,
|
||||||
@@ -6,11 +11,10 @@ import {
|
|||||||
View,
|
View,
|
||||||
ViewStyle,
|
ViewStyle,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import {AppBskyGraphDefs, AppBskyGraphGetActorStarterPacks} from '@atproto/api'
|
import {AppBskyGraphDefs} from '@atproto/api'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useNavigation} from '@react-navigation/native'
|
import {useNavigation} from '@react-navigation/native'
|
||||||
import {InfiniteData, UseInfiniteQueryResult} from '@tanstack/react-query'
|
|
||||||
|
|
||||||
import {useGenerateStarterPackMutation} from '#/lib/generate-starterpack'
|
import {useGenerateStarterPackMutation} from '#/lib/generate-starterpack'
|
||||||
import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset'
|
import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset'
|
||||||
@@ -19,28 +23,27 @@ import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
|||||||
import {NavigationProp} from '#/lib/routes/types'
|
import {NavigationProp} from '#/lib/routes/types'
|
||||||
import {parseStarterPackUri} from '#/lib/strings/starter-pack'
|
import {parseStarterPackUri} from '#/lib/strings/starter-pack'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
|
import {useActorStarterPacksQuery} from '#/state/queries/actor-starter-packs'
|
||||||
import {List, ListRef} from '#/view/com/util/List'
|
import {List, ListRef} from '#/view/com/util/List'
|
||||||
import {Text} from '#/view/com/util/text/Text'
|
import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||||
import {atoms as a, ios, useTheme} from '#/alf'
|
import {atoms as a, ios, useTheme} from '#/alf'
|
||||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
import {useDialogControl} from '#/components/Dialog'
|
import {useDialogControl} from '#/components/Dialog'
|
||||||
|
import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog'
|
||||||
|
import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
||||||
import {LinearGradientBackground} from '#/components/LinearGradientBackground'
|
import {LinearGradientBackground} from '#/components/LinearGradientBackground'
|
||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
import * as Prompt from '#/components/Prompt'
|
import * as Prompt from '#/components/Prompt'
|
||||||
import {Default as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
|
import {Default as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
|
||||||
import {VerifyEmailDialog} from '../dialogs/VerifyEmailDialog'
|
import {Text} from '#/components/Typography'
|
||||||
import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '../icons/Plus'
|
|
||||||
|
|
||||||
interface SectionRef {
|
interface SectionRef {
|
||||||
scrollToTop: () => void
|
scrollToTop: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProfileFeedgensProps {
|
interface ProfileFeedgensProps {
|
||||||
starterPacksQuery: UseInfiniteQueryResult<
|
|
||||||
InfiniteData<AppBskyGraphGetActorStarterPacks.OutputSchema, unknown>,
|
|
||||||
Error
|
|
||||||
>
|
|
||||||
scrollElRef: ListRef
|
scrollElRef: ListRef
|
||||||
|
did: string
|
||||||
headerOffset: number
|
headerOffset: number
|
||||||
enabled?: boolean
|
enabled?: boolean
|
||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
@@ -58,8 +61,8 @@ export const ProfileStarterPacks = React.forwardRef<
|
|||||||
ProfileFeedgensProps
|
ProfileFeedgensProps
|
||||||
>(function ProfileFeedgensImpl(
|
>(function ProfileFeedgensImpl(
|
||||||
{
|
{
|
||||||
starterPacksQuery: query,
|
|
||||||
scrollElRef,
|
scrollElRef,
|
||||||
|
did,
|
||||||
headerOffset,
|
headerOffset,
|
||||||
enabled,
|
enabled,
|
||||||
style,
|
style,
|
||||||
@@ -71,17 +74,18 @@ export const ProfileStarterPacks = React.forwardRef<
|
|||||||
) {
|
) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const bottomBarOffset = useBottomBarOffset(100)
|
const bottomBarOffset = useBottomBarOffset(100)
|
||||||
const [isPTRing, setIsPTRing] = React.useState(false)
|
const [isPTRing, setIsPTRing] = useState(false)
|
||||||
const {data, refetch, isFetching, hasNextPage, fetchNextPage} = query
|
const {data, refetch, isFetching, hasNextPage, fetchNextPage} =
|
||||||
|
useActorStarterPacksQuery({did, enabled})
|
||||||
const {isTabletOrDesktop} = useWebMediaQueries()
|
const {isTabletOrDesktop} = useWebMediaQueries()
|
||||||
|
|
||||||
const items = data?.pages.flatMap(page => page.starterPacks)
|
const items = data?.pages.flatMap(page => page.starterPacks)
|
||||||
|
|
||||||
React.useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
scrollToTop: () => {},
|
scrollToTop: () => {},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const onRefresh = React.useCallback(async () => {
|
const onRefresh = useCallback(async () => {
|
||||||
setIsPTRing(true)
|
setIsPTRing(true)
|
||||||
try {
|
try {
|
||||||
await refetch()
|
await refetch()
|
||||||
@@ -91,7 +95,7 @@ export const ProfileStarterPacks = React.forwardRef<
|
|||||||
setIsPTRing(false)
|
setIsPTRing(false)
|
||||||
}, [refetch, setIsPTRing])
|
}, [refetch, setIsPTRing])
|
||||||
|
|
||||||
const onEndReached = React.useCallback(async () => {
|
const onEndReached = useCallback(async () => {
|
||||||
if (isFetching || !hasNextPage) return
|
if (isFetching || !hasNextPage) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -101,7 +105,7 @@ export const ProfileStarterPacks = React.forwardRef<
|
|||||||
}
|
}
|
||||||
}, [isFetching, hasNextPage, fetchNextPage])
|
}, [isFetching, hasNextPage, fetchNextPage])
|
||||||
|
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
if (enabled && scrollElRef.current) {
|
if (enabled && scrollElRef.current) {
|
||||||
const nativeTag = findNodeHandle(scrollElRef.current)
|
const nativeTag = findNodeHandle(scrollElRef.current)
|
||||||
setScrollViewTag(nativeTag)
|
setScrollViewTag(nativeTag)
|
||||||
@@ -140,9 +144,11 @@ export const ProfileStarterPacks = React.forwardRef<
|
|||||||
desktopFixedHeight
|
desktopFixedHeight
|
||||||
onEndReached={onEndReached}
|
onEndReached={onEndReached}
|
||||||
onRefresh={onRefresh}
|
onRefresh={onRefresh}
|
||||||
ListEmptyComponent={Empty}
|
ListEmptyComponent={
|
||||||
|
data ? (isMe ? Empty : undefined) : FeedLoadingPlaceholder
|
||||||
|
}
|
||||||
ListFooterComponent={
|
ListFooterComponent={
|
||||||
items?.length !== 0 && isMe ? CreateAnother : undefined
|
!!data && items?.length !== 0 && isMe ? CreateAnother : undefined
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
@@ -181,7 +187,6 @@ function CreateAnother() {
|
|||||||
|
|
||||||
function Empty() {
|
function Empty() {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const t = useTheme()
|
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
const confirmDialogControl = useDialogControl()
|
const confirmDialogControl = useDialogControl()
|
||||||
const followersDialogControl = useDialogControl()
|
const followersDialogControl = useDialogControl()
|
||||||
@@ -190,7 +195,7 @@ function Empty() {
|
|||||||
const {needsEmailVerification} = useEmail()
|
const {needsEmailVerification} = useEmail()
|
||||||
const verifyEmailControl = useDialogControl()
|
const verifyEmailControl = useDialogControl()
|
||||||
|
|
||||||
const [isGenerating, setIsGenerating] = React.useState(false)
|
const [isGenerating, setIsGenerating] = useState(false)
|
||||||
|
|
||||||
const {mutate: generateStarterPack} = useGenerateStarterPackMutation({
|
const {mutate: generateStarterPack} = useGenerateStarterPackMutation({
|
||||||
onSuccess: ({uri}) => {
|
onSuccess: ({uri}) => {
|
||||||
@@ -227,16 +232,10 @@ function Empty() {
|
|||||||
a.justify_between,
|
a.justify_between,
|
||||||
a.gap_lg,
|
a.gap_lg,
|
||||||
a.shadow_lg,
|
a.shadow_lg,
|
||||||
{marginTop: 1},
|
{marginTop: a.border.borderWidth},
|
||||||
]}>
|
]}>
|
||||||
<View style={[a.gap_xs]}>
|
<View style={[a.gap_xs]}>
|
||||||
<Text
|
<Text style={[a.font_bold, a.text_lg, {color: 'white'}]}>
|
||||||
style={[
|
|
||||||
a.font_bold,
|
|
||||||
a.text_lg,
|
|
||||||
t.atoms.text_contrast_medium,
|
|
||||||
{color: 'white'},
|
|
||||||
]}>
|
|
||||||
<Trans>You haven't created a starter pack yet!</Trans>
|
<Trans>You haven't created a starter pack yet!</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={[a.text_md, {color: 'white'}]}>
|
<Text style={[a.text_md, {color: 'white'}]}>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {useTheme} from '#/alf'
|
|||||||
import {atoms as a} from '#/alf'
|
import {atoms as a} from '#/alf'
|
||||||
import {LinearGradientBackground} from '#/components/LinearGradientBackground'
|
import {LinearGradientBackground} from '#/components/LinearGradientBackground'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
const LazyViewShot = React.lazy(
|
const LazyViewShot = React.lazy(
|
||||||
// @ts-expect-error dynamic import
|
// @ts-expect-error dynamic import
|
||||||
@@ -30,7 +31,12 @@ export const QrCode = React.forwardRef<ViewShot, Props>(function QrCode(
|
|||||||
) {
|
) {
|
||||||
const {record} = starterPack
|
const {record} = starterPack
|
||||||
|
|
||||||
if (!AppBskyGraphStarterpack.isRecord(record)) {
|
if (
|
||||||
|
!bsky.dangerousIsType<AppBskyGraphStarterpack.Record>(
|
||||||
|
record,
|
||||||
|
AppBskyGraphStarterpack.isRecord,
|
||||||
|
)
|
||||||
|
) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import * as Dialog from '#/components/Dialog'
|
|||||||
import {DialogControlProps} from '#/components/Dialog'
|
import {DialogControlProps} from '#/components/Dialog'
|
||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
import {QrCode} from '#/components/StarterPack/QrCode'
|
import {QrCode} from '#/components/StarterPack/QrCode'
|
||||||
|
import * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
export function QrCodeDialog({
|
export function QrCodeDialog({
|
||||||
starterPack,
|
starterPack,
|
||||||
@@ -77,7 +78,12 @@ export function QrCodeDialog({
|
|||||||
} else {
|
} else {
|
||||||
setIsProcessing(true)
|
setIsProcessing(true)
|
||||||
|
|
||||||
if (!AppBskyGraphStarterpack.isRecord(starterPack.record)) {
|
if (
|
||||||
|
!bsky.validate(
|
||||||
|
starterPack.record,
|
||||||
|
AppBskyGraphStarterpack.validateRecord,
|
||||||
|
)
|
||||||
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {Image} from 'expo-image'
|
import {Image} from 'expo-image'
|
||||||
import {AppBskyGraphDefs, AppBskyGraphStarterpack, AtUri} from '@atproto/api'
|
import {AppBskyGraphStarterpack, AtUri} from '@atproto/api'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useQueryClient} from '@tanstack/react-query'
|
import {useQueryClient} from '@tanstack/react-query'
|
||||||
@@ -15,11 +15,12 @@ import {atoms as a, useTheme} from '#/alf'
|
|||||||
import {StarterPack as StarterPackIcon} from '#/components/icons/StarterPack'
|
import {StarterPack as StarterPackIcon} from '#/components/icons/StarterPack'
|
||||||
import {Link as BaseLink, LinkProps as BaseLinkProps} from '#/components/Link'
|
import {Link as BaseLink, LinkProps as BaseLinkProps} from '#/components/Link'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
export function Default({
|
export function Default({
|
||||||
starterPack,
|
starterPack,
|
||||||
}: {
|
}: {
|
||||||
starterPack?: AppBskyGraphDefs.StarterPackViewBasic
|
starterPack?: bsky.starterPack.AnyStarterPackView
|
||||||
}) {
|
}) {
|
||||||
if (!starterPack) return null
|
if (!starterPack) return null
|
||||||
return (
|
return (
|
||||||
@@ -32,7 +33,7 @@ export function Default({
|
|||||||
export function Notification({
|
export function Notification({
|
||||||
starterPack,
|
starterPack,
|
||||||
}: {
|
}: {
|
||||||
starterPack?: AppBskyGraphDefs.StarterPackViewBasic
|
starterPack?: bsky.starterPack.AnyStarterPackView
|
||||||
}) {
|
}) {
|
||||||
if (!starterPack) return null
|
if (!starterPack) return null
|
||||||
return (
|
return (
|
||||||
@@ -47,7 +48,7 @@ export function Card({
|
|||||||
noIcon,
|
noIcon,
|
||||||
noDescription,
|
noDescription,
|
||||||
}: {
|
}: {
|
||||||
starterPack: AppBskyGraphDefs.StarterPackViewBasic
|
starterPack: bsky.starterPack.AnyStarterPackView
|
||||||
noIcon?: boolean
|
noIcon?: boolean
|
||||||
noDescription?: boolean
|
noDescription?: boolean
|
||||||
}) {
|
}) {
|
||||||
@@ -57,7 +58,12 @@ export function Card({
|
|||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
|
|
||||||
if (!AppBskyGraphStarterpack.isRecord(record)) {
|
if (
|
||||||
|
!bsky.dangerousIsType<AppBskyGraphStarterpack.Record>(
|
||||||
|
record,
|
||||||
|
AppBskyGraphStarterpack.isRecord,
|
||||||
|
)
|
||||||
|
) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +106,7 @@ export function Link({
|
|||||||
starterPack,
|
starterPack,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
starterPack: AppBskyGraphDefs.StarterPackViewBasic
|
starterPack: bsky.starterPack.AnyStarterPackView
|
||||||
onPress?: () => void
|
onPress?: () => void
|
||||||
children: BaseLinkProps['children']
|
children: BaseLinkProps['children']
|
||||||
}) {
|
}) {
|
||||||
@@ -139,7 +145,7 @@ export function Link({
|
|||||||
export function Embed({
|
export function Embed({
|
||||||
starterPack,
|
starterPack,
|
||||||
}: {
|
}: {
|
||||||
starterPack: AppBskyGraphDefs.StarterPackViewBasic
|
starterPack: bsky.starterPack.AnyStarterPackView
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const imageUri = getStarterPackOgCard(starterPack)
|
const imageUri = getStarterPackOgCard(starterPack)
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export function WizardEditListDialog({
|
|||||||
state: WizardState
|
state: WizardState
|
||||||
dispatch: (action: WizardAction) => void
|
dispatch: (action: WizardAction) => void
|
||||||
moderationOpts: ModerationOpts
|
moderationOpts: ModerationOpts
|
||||||
profile: AppBskyActorDefs.ProfileViewBasic
|
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||||
}) {
|
}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {Button, ButtonText} from '#/components/Button'
|
|||||||
import * as Toggle from '#/components/forms/Toggle'
|
import * as Toggle from '#/components/forms/Toggle'
|
||||||
import {Checkbox} from '#/components/forms/Toggle'
|
import {Checkbox} from '#/components/forms/Toggle'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
function WizardListCard({
|
function WizardListCard({
|
||||||
type,
|
type,
|
||||||
@@ -123,7 +124,7 @@ export function WizardProfileCard({
|
|||||||
btnType: 'checkbox' | 'remove'
|
btnType: 'checkbox' | 'remove'
|
||||||
state: WizardState
|
state: WizardState
|
||||||
dispatch: (action: WizardAction) => void
|
dispatch: (action: WizardAction) => void
|
||||||
profile: AppBskyActorDefs.ProfileViewBasic
|
profile: bsky.profile.AnyProfileView
|
||||||
moderationOpts: ModerationOpts
|
moderationOpts: ModerationOpts
|
||||||
}) {
|
}) {
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {Link} from '#/components/Link'
|
|||||||
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
||||||
import * as Hider from '#/components/moderation/Hider'
|
import * as Hider from '#/components/moderation/Hider'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
function getBlackColor(t: ReturnType<typeof useTheme>) {
|
function getBlackColor(t: ReturnType<typeof useTheme>) {
|
||||||
return select(t.name, {
|
return select(t.name, {
|
||||||
@@ -78,7 +79,12 @@ export function VideoPostCard({
|
|||||||
if (!AppBskyEmbedVideo.isView(embed)) return null
|
if (!AppBskyEmbedVideo.isView(embed)) return null
|
||||||
|
|
||||||
const author = post.author
|
const author = post.author
|
||||||
const text = AppBskyFeedPost.isRecord(post.record) ? post.record?.text : ''
|
const text = bsky.dangerousIsType<AppBskyFeedPost.Record>(
|
||||||
|
post.record,
|
||||||
|
AppBskyFeedPost.isRecord,
|
||||||
|
)
|
||||||
|
? post.record?.text
|
||||||
|
: ''
|
||||||
const likeCount = post?.likeCount ?? 0
|
const likeCount = post?.likeCount ?? 0
|
||||||
const repostCount = post?.repostCount ?? 0
|
const repostCount = post?.repostCount ?? 0
|
||||||
const {thumbnail} = embed
|
const {thumbnail} = embed
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {Earth_Stroke2_Corner0_Rounded as Earth} from '#/components/icons/Globe'
|
|||||||
import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group'
|
import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group'
|
||||||
import {InlineLinkText} from '#/components/Link'
|
import {InlineLinkText} from '#/components/Link'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import * as bsky from '#/types/bsky'
|
||||||
import {PencilLine_Stroke2_Corner0_Rounded as PencilLine} from './icons/Pencil'
|
import {PencilLine_Stroke2_Corner0_Rounded as PencilLine} from './icons/Pencil'
|
||||||
|
|
||||||
interface WhoCanReplyProps {
|
interface WhoCanReplyProps {
|
||||||
@@ -48,7 +49,10 @@ export function WhoCanReply({post, isThreadAuthor, style}: WhoCanReplyProps) {
|
|||||||
* unexpectedly, we should check to make sure it's for sure the root URI.
|
* unexpectedly, we should check to make sure it's for sure the root URI.
|
||||||
*/
|
*/
|
||||||
const rootUri =
|
const rootUri =
|
||||||
AppBskyFeedPost.isRecord(post.record) && post.record.reply?.root
|
bsky.dangerousIsType<AppBskyFeedPost.Record>(
|
||||||
|
post.record,
|
||||||
|
AppBskyFeedPost.isRecord,
|
||||||
|
) && post.record.reply?.root
|
||||||
? post.record.reply.root.uri
|
? post.record.reply.root.uri
|
||||||
: post.uri
|
: post.uri
|
||||||
const settings = React.useMemo(() => {
|
const settings = React.useMemo(() => {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {msg, Trans} from '@lingui/macro'
|
|||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {cleanError} from '#/lib/strings/errors'
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
|
import {getDateAgo} from '#/lib/strings/time'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {isIOS, isWeb} from '#/platform/detection'
|
import {isIOS, isWeb} from '#/platform/detection'
|
||||||
import {
|
import {
|
||||||
@@ -101,7 +102,7 @@ function BirthdayInner({
|
|||||||
onChangeDate={newDate => setDate(new Date(newDate))}
|
onChangeDate={newDate => setDate(new Date(newDate))}
|
||||||
label={_(msg`Birthday`)}
|
label={_(msg`Birthday`)}
|
||||||
accessibilityHint={_(msg`Enter your birth date`)}
|
accessibilityHint={_(msg`Enter your birth date`)}
|
||||||
maximumDate={new Date()}
|
maximumDate={getDateAgo(13)}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, {memo, useRef, useState} from 'react'
|
import {memo, useEffect, useMemo, useState} from 'react'
|
||||||
import {TextInput, View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {AppBskyActorDefs, AppBskyFeedPost, AtUri} from '@atproto/api'
|
import {AppBskyActorDefs, AppBskyFeedPost, AtUri} from '@atproto/api'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
@@ -8,12 +8,19 @@ import {EMBED_SCRIPT} from '#/lib/constants'
|
|||||||
import {niceDate} from '#/lib/strings/time'
|
import {niceDate} from '#/lib/strings/time'
|
||||||
import {toShareUrl} from '#/lib/strings/url-helpers'
|
import {toShareUrl} from '#/lib/strings/url-helpers'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
import * as TextField from '#/components/forms/TextField'
|
import * as TextField from '#/components/forms/TextField'
|
||||||
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
import * as ToggleButton from '#/components/forms/ToggleButton'
|
||||||
import {CodeBrackets_Stroke2_Corner0_Rounded as CodeBrackets} from '#/components/icons/CodeBrackets'
|
import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
|
||||||
|
import {
|
||||||
|
ChevronBottom_Stroke2_Corner0_Rounded as ChevronBottomIcon,
|
||||||
|
ChevronRight_Stroke2_Corner0_Rounded as ChevronRightIcon,
|
||||||
|
} from '#/components/icons/Chevron'
|
||||||
|
import {CodeBrackets_Stroke2_Corner0_Rounded as CodeBracketsIcon} from '#/components/icons/CodeBrackets'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {Button, ButtonIcon, ButtonText} from '../Button'
|
|
||||||
|
export type ColorModeValues = 'system' | 'light' | 'dark'
|
||||||
|
|
||||||
type EmbedDialogProps = {
|
type EmbedDialogProps = {
|
||||||
control: Dialog.DialogControlProps
|
control: Dialog.DialogControlProps
|
||||||
@@ -44,11 +51,12 @@ function EmbedDialogInner({
|
|||||||
}: Omit<EmbedDialogProps, 'control'>) {
|
}: Omit<EmbedDialogProps, 'control'>) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_, i18n} = useLingui()
|
const {_, i18n} = useLingui()
|
||||||
const ref = useRef<TextInput>(null)
|
|
||||||
const [copied, setCopied] = useState(false)
|
const [copied, setCopied] = useState(false)
|
||||||
|
const [showCustomisation, setShowCustomisation] = useState(false)
|
||||||
|
const [colorMode, setColorMode] = useState<ColorModeValues>('system')
|
||||||
|
|
||||||
// reset copied state after 2 seconds
|
// reset copied state after 2 seconds
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
if (copied) {
|
if (copied) {
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
setCopied(false)
|
setCopied(false)
|
||||||
@@ -57,7 +65,7 @@ function EmbedDialogInner({
|
|||||||
}
|
}
|
||||||
}, [copied])
|
}, [copied])
|
||||||
|
|
||||||
const snippet = React.useMemo(() => {
|
const snippet = useMemo(() => {
|
||||||
function toEmbedUrl(href: string) {
|
function toEmbedUrl(href: string) {
|
||||||
return toShareUrl(href) + '?ref_src=embed'
|
return toShareUrl(href) + '?ref_src=embed'
|
||||||
}
|
}
|
||||||
@@ -75,9 +83,11 @@ function EmbedDialogInner({
|
|||||||
// x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x
|
// x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x
|
||||||
return `<blockquote class="bluesky-embed" data-bluesky-uri="${escapeHtml(
|
return `<blockquote class="bluesky-embed" data-bluesky-uri="${escapeHtml(
|
||||||
postUri,
|
postUri,
|
||||||
)}" data-bluesky-cid="${escapeHtml(postCid)}"><p lang="${escapeHtml(
|
)}" data-bluesky-cid="${escapeHtml(
|
||||||
lang,
|
postCid,
|
||||||
)}">${escapeHtml(record.text)}${
|
)}" data-bluesky-embed-color-mode="${escapeHtml(
|
||||||
|
colorMode,
|
||||||
|
)}"><p lang="${escapeHtml(lang)}">${escapeHtml(record.text)}${
|
||||||
record.embed
|
record.embed
|
||||||
? `<br><br><a href="${escapeHtml(href)}">[image or embed]</a>`
|
? `<br><br><a href="${escapeHtml(href)}">[image or embed]</a>`
|
||||||
: ''
|
: ''
|
||||||
@@ -88,12 +98,13 @@ function EmbedDialogInner({
|
|||||||
)}</a>) <a href="${escapeHtml(href)}">${escapeHtml(
|
)}</a>) <a href="${escapeHtml(href)}">${escapeHtml(
|
||||||
niceDate(i18n, timestamp),
|
niceDate(i18n, timestamp),
|
||||||
)}</a></blockquote><script async src="${EMBED_SCRIPT}" charset="utf-8"></script>`
|
)}</a></blockquote><script async src="${EMBED_SCRIPT}" charset="utf-8"></script>`
|
||||||
}, [i18n, postUri, postCid, record, timestamp, postAuthor])
|
}, [i18n, postUri, postCid, record, timestamp, postAuthor, colorMode])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog.Inner label="Embed post" style={[a.gap_md, {maxWidth: 500}]}>
|
<Dialog.Inner label={_(msg`Embed post`)} style={[{maxWidth: 500}]}>
|
||||||
<View style={[a.gap_sm, a.pb_lg]}>
|
<View style={[a.gap_lg]}>
|
||||||
<Text style={[a.text_2xl, a.font_bold]}>
|
<View style={[a.gap_sm]}>
|
||||||
|
<Text style={[a.text_2xl, a.font_heavy]}>
|
||||||
<Trans>Embed post</Trans>
|
<Trans>Embed post</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
@@ -104,17 +115,73 @@ function EmbedDialogInner({
|
|||||||
</Trans>
|
</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.border,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
a.rounded_sm,
|
||||||
|
a.overflow_hidden,
|
||||||
|
]}>
|
||||||
|
<Button
|
||||||
|
label={
|
||||||
|
showCustomisation
|
||||||
|
? _(msg`Hide customization options`)
|
||||||
|
: _(msg`Show customization options`)
|
||||||
|
}
|
||||||
|
color="secondary"
|
||||||
|
variant="ghost"
|
||||||
|
size="small"
|
||||||
|
shape="default"
|
||||||
|
onPress={() => setShowCustomisation(c => !c)}
|
||||||
|
style={[
|
||||||
|
a.justify_start,
|
||||||
|
showCustomisation && t.atoms.bg_contrast_25,
|
||||||
|
]}>
|
||||||
|
<ButtonIcon
|
||||||
|
icon={showCustomisation ? ChevronBottomIcon : ChevronRightIcon}
|
||||||
|
/>
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Customization options</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{showCustomisation && (
|
||||||
|
<View style={[a.gap_sm, a.p_md]}>
|
||||||
|
<Text style={[t.atoms.text_contrast_medium, a.font_bold]}>
|
||||||
|
<Trans>Color theme</Trans>
|
||||||
|
</Text>
|
||||||
|
<ToggleButton.Group
|
||||||
|
label={_(msg`Color mode`)}
|
||||||
|
values={[colorMode]}
|
||||||
|
onChange={([value]) => setColorMode(value as ColorModeValues)}>
|
||||||
|
<ToggleButton.Button name="system" label={_(msg`System`)}>
|
||||||
|
<ToggleButton.ButtonText>
|
||||||
|
<Trans>System</Trans>
|
||||||
|
</ToggleButton.ButtonText>
|
||||||
|
</ToggleButton.Button>
|
||||||
|
<ToggleButton.Button name="light" label={_(msg`Light`)}>
|
||||||
|
<ToggleButton.ButtonText>
|
||||||
|
<Trans>Light</Trans>
|
||||||
|
</ToggleButton.ButtonText>
|
||||||
|
</ToggleButton.Button>
|
||||||
|
<ToggleButton.Button name="dark" label={_(msg`Dark`)}>
|
||||||
|
<ToggleButton.ButtonText>
|
||||||
|
<Trans>Dark</Trans>
|
||||||
|
</ToggleButton.ButtonText>
|
||||||
|
</ToggleButton.Button>
|
||||||
|
</ToggleButton.Group>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
<View style={[a.flex_row, a.gap_sm]}>
|
<View style={[a.flex_row, a.gap_sm]}>
|
||||||
<View style={[a.flex_1]}>
|
<View style={[a.flex_1]}>
|
||||||
<TextField.Root>
|
<TextField.Root>
|
||||||
<TextField.Icon icon={CodeBrackets} />
|
<TextField.Icon icon={CodeBracketsIcon} />
|
||||||
<TextField.Input
|
<TextField.Input
|
||||||
label={_(msg`Embed HTML code`)}
|
label={_(msg`Embed HTML code`)}
|
||||||
editable={false}
|
editable={false}
|
||||||
selection={{start: 0, end: snippet.length}}
|
selection={{start: 0, end: snippet.length}}
|
||||||
value={snippet}
|
value={snippet}
|
||||||
style={{}}
|
|
||||||
/>
|
/>
|
||||||
</TextField.Root>
|
</TextField.Root>
|
||||||
</View>
|
</View>
|
||||||
@@ -124,14 +191,12 @@ function EmbedDialogInner({
|
|||||||
variant="solid"
|
variant="solid"
|
||||||
size="large"
|
size="large"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
ref.current?.focus()
|
|
||||||
ref.current?.setSelection(0, snippet.length)
|
|
||||||
navigator.clipboard.writeText(snippet)
|
navigator.clipboard.writeText(snippet)
|
||||||
setCopied(true)
|
setCopied(true)
|
||||||
}}>
|
}}>
|
||||||
{copied ? (
|
{copied ? (
|
||||||
<>
|
<>
|
||||||
<ButtonIcon icon={Check} />
|
<ButtonIcon icon={CheckIcon} />
|
||||||
<ButtonText>
|
<ButtonText>
|
||||||
<Trans>Copied!</Trans>
|
<Trans>Copied!</Trans>
|
||||||
</ButtonText>
|
</ButtonText>
|
||||||
@@ -143,6 +208,7 @@ function EmbedDialogInner({
|
|||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
|
</View>
|
||||||
<Dialog.Close />
|
<Dialog.Close />
|
||||||
</Dialog.Inner>
|
</Dialog.Inner>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -318,7 +318,7 @@ function MutedWordsInner() {
|
|||||||
<View style={[a.pt_xs]}>
|
<View style={[a.pt_xs]}>
|
||||||
<Button
|
<Button
|
||||||
disabled={isPending || !field}
|
disabled={isPending || !field}
|
||||||
label={_(msg`Add mute word for configured settings`)}
|
label={_(msg`Add mute word with chosen settings`)}
|
||||||
size="large"
|
size="large"
|
||||||
color="primary"
|
color="primary"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ export function PostInteractionSettingsDialogControlledInner(
|
|||||||
props.control.close()
|
props.control.close()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.error(`Failed to save post interaction settings`, {
|
logger.error(`Failed to save post interaction settings`, {
|
||||||
context: 'PostInteractionSettingsDialogControlledInner',
|
source: 'PostInteractionSettingsDialogControlledInner',
|
||||||
safeMessage: e.message,
|
safeMessage: e.message,
|
||||||
})
|
})
|
||||||
Toast.show(
|
Toast.show(
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import React, {useCallback} from 'react'
|
import React, {useCallback} from 'react'
|
||||||
import {Keyboard, Pressable, View} from 'react-native'
|
import {Keyboard, Pressable, View} from 'react-native'
|
||||||
import {
|
import {ChatBskyConvoDefs, ModerationCause} from '@atproto/api'
|
||||||
AppBskyActorDefs,
|
|
||||||
ChatBskyConvoDefs,
|
|
||||||
ModerationCause,
|
|
||||||
} from '@atproto/api'
|
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useNavigation} from '@react-navigation/native'
|
import {useNavigation} from '@react-navigation/native'
|
||||||
@@ -34,6 +30,7 @@ import {
|
|||||||
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker'
|
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker'
|
||||||
import * as Menu from '#/components/Menu'
|
import * as Menu from '#/components/Menu'
|
||||||
import * as Prompt from '#/components/Prompt'
|
import * as Prompt from '#/components/Prompt'
|
||||||
|
import * as bsky from '#/types/bsky'
|
||||||
import {Bubble_Stroke2_Corner2_Rounded as Bubble} from '../icons/Bubble'
|
import {Bubble_Stroke2_Corner2_Rounded as Bubble} from '../icons/Bubble'
|
||||||
import {ReportDialog} from './ReportDialog'
|
import {ReportDialog} from './ReportDialog'
|
||||||
|
|
||||||
@@ -49,7 +46,7 @@ let ConvoMenu = ({
|
|||||||
style,
|
style,
|
||||||
}: {
|
}: {
|
||||||
convo: ChatBskyConvoDefs.ConvoView
|
convo: ChatBskyConvoDefs.ConvoView
|
||||||
profile: Shadow<AppBskyActorDefs.ProfileViewBasic>
|
profile: Shadow<bsky.profile.AnyProfileView>
|
||||||
control?: Menu.MenuControlProps
|
control?: Menu.MenuControlProps
|
||||||
currentScreen: 'list' | 'conversation'
|
currentScreen: 'list' | 'conversation'
|
||||||
showMarkAsRead?: boolean
|
showMarkAsRead?: boolean
|
||||||
@@ -148,7 +145,7 @@ function MenuContent({
|
|||||||
blockedByListControl,
|
blockedByListControl,
|
||||||
}: {
|
}: {
|
||||||
convo: ChatBskyConvoDefs.ConvoView
|
convo: ChatBskyConvoDefs.ConvoView
|
||||||
profile: Shadow<AppBskyActorDefs.ProfileViewBasic>
|
profile: Shadow<bsky.profile.AnyProfileView>
|
||||||
showMarkAsRead?: boolean
|
showMarkAsRead?: boolean
|
||||||
blockInfo: {
|
blockInfo: {
|
||||||
listBlocks: ModerationCause[]
|
listBlocks: ModerationCause[]
|
||||||
@@ -176,9 +173,9 @@ function MenuContent({
|
|||||||
const {mutate: muteConvo} = useMuteConvo(convoId, {
|
const {mutate: muteConvo} = useMuteConvo(convoId, {
|
||||||
onSuccess: data => {
|
onSuccess: data => {
|
||||||
if (data.convo.muted) {
|
if (data.convo.muted) {
|
||||||
Toast.show(_(msg`Chat muted`))
|
Toast.show(_(msg({message: 'Chat muted', context: 'toast'})))
|
||||||
} else {
|
} else {
|
||||||
Toast.show(_(msg`Chat unmuted`))
|
Toast.show(_(msg({message: 'Chat unmuted', context: 'toast'})))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
|
|||||||
@@ -13,10 +13,12 @@ export function LeaveConvoPrompt({
|
|||||||
control,
|
control,
|
||||||
convoId,
|
convoId,
|
||||||
currentScreen,
|
currentScreen,
|
||||||
|
hasMessages = true,
|
||||||
}: {
|
}: {
|
||||||
control: DialogOuterProps['control']
|
control: DialogOuterProps['control']
|
||||||
convoId: string
|
convoId: string
|
||||||
currentScreen: 'list' | 'conversation'
|
currentScreen: 'list' | 'conversation'
|
||||||
|
hasMessages?: boolean
|
||||||
}) {
|
}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
@@ -39,7 +41,9 @@ export function LeaveConvoPrompt({
|
|||||||
control={control}
|
control={control}
|
||||||
title={_(msg`Leave conversation`)}
|
title={_(msg`Leave conversation`)}
|
||||||
description={_(
|
description={_(
|
||||||
msg`Are you sure you want to leave this conversation? Your messages will be deleted for you, but not for the other participant.`,
|
hasMessages
|
||||||
|
? msg`Are you sure you want to leave this conversation? Your messages will be deleted for you, but not for the other participant.`
|
||||||
|
: msg`Are you sure you want to leave this conversation?`,
|
||||||
)}
|
)}
|
||||||
confirmButtonCta={_(msg`Leave`)}
|
confirmButtonCta={_(msg`Leave`)}
|
||||||
confirmButtonColor="negative"
|
confirmButtonColor="negative"
|
||||||
|
|||||||
@@ -69,7 +69,9 @@ export let MessageMenu = ({
|
|||||||
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
|
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
|
||||||
convo
|
convo
|
||||||
.deleteMessage(message.id)
|
.deleteMessage(message.id)
|
||||||
.then(() => Toast.show(_(msg`Message deleted`)))
|
.then(() =>
|
||||||
|
Toast.show(_(msg({message: 'Message deleted', context: 'toast'}))),
|
||||||
|
)
|
||||||
.catch(() => Toast.show(_(msg`Failed to delete message`)))
|
.catch(() => Toast.show(_(msg`Failed to delete message`)))
|
||||||
}, [_, convo, message.id])
|
}, [_, convo, message.id])
|
||||||
|
|
||||||
|
|||||||
@@ -8,18 +8,20 @@ import {useNavigation} from '@react-navigation/native'
|
|||||||
import {useEmail} from '#/lib/hooks/useEmail'
|
import {useEmail} from '#/lib/hooks/useEmail'
|
||||||
import {NavigationProp} from '#/lib/routes/types'
|
import {NavigationProp} from '#/lib/routes/types'
|
||||||
import {logEvent} from '#/lib/statsig/statsig'
|
import {logEvent} from '#/lib/statsig/statsig'
|
||||||
import {useMaybeConvoForUser} from '#/state/queries/messages/get-convo-for-members'
|
import {useGetConvoAvailabilityQuery} from '#/state/queries/messages/get-convo-availability'
|
||||||
|
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
|
||||||
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {Button, ButtonIcon} from '#/components/Button'
|
import {Button, ButtonIcon} from '#/components/Button'
|
||||||
|
import {useDialogControl} from '#/components/Dialog'
|
||||||
|
import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog'
|
||||||
import {canBeMessaged} from '#/components/dms/util'
|
import {canBeMessaged} from '#/components/dms/util'
|
||||||
import {Message_Stroke2_Corner0_Rounded as Message} from '#/components/icons/Message'
|
import {Message_Stroke2_Corner0_Rounded as Message} from '#/components/icons/Message'
|
||||||
import {useDialogControl} from '../Dialog'
|
|
||||||
import {VerifyEmailDialog} from '../dialogs/VerifyEmailDialog'
|
|
||||||
|
|
||||||
export function MessageProfileButton({
|
export function MessageProfileButton({
|
||||||
profile,
|
profile,
|
||||||
}: {
|
}: {
|
||||||
profile: AppBskyActorDefs.ProfileView
|
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||||
}) {
|
}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
@@ -27,10 +29,19 @@ export function MessageProfileButton({
|
|||||||
const {needsEmailVerification} = useEmail()
|
const {needsEmailVerification} = useEmail()
|
||||||
const verifyEmailControl = useDialogControl()
|
const verifyEmailControl = useDialogControl()
|
||||||
|
|
||||||
const {data: convo, isPending} = useMaybeConvoForUser(profile.did)
|
const {data: convoAvailability} = useGetConvoAvailabilityQuery(profile.did)
|
||||||
|
const {mutate: initiateConvo} = useGetConvoForMembers({
|
||||||
|
onSuccess: ({convo}) => {
|
||||||
|
logEvent('chat:open', {logContext: 'ProfileHeader'})
|
||||||
|
navigation.navigate('MessagesConversation', {conversation: convo.id})
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
Toast.show(_(msg`Failed to create conversation`))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const onPress = React.useCallback(() => {
|
const onPress = React.useCallback(() => {
|
||||||
if (!convo?.id) {
|
if (!convoAvailability?.canChat) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,15 +50,25 @@ export function MessageProfileButton({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (convo && !convo.lastMessage) {
|
if (convoAvailability.convo) {
|
||||||
logEvent('chat:create', {logContext: 'ProfileHeader'})
|
|
||||||
}
|
|
||||||
logEvent('chat:open', {logContext: 'ProfileHeader'})
|
logEvent('chat:open', {logContext: 'ProfileHeader'})
|
||||||
|
navigation.navigate('MessagesConversation', {
|
||||||
|
conversation: convoAvailability.convo.id,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
logEvent('chat:create', {logContext: 'ProfileHeader'})
|
||||||
|
initiateConvo([profile.did])
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
needsEmailVerification,
|
||||||
|
verifyEmailControl,
|
||||||
|
navigation,
|
||||||
|
profile.did,
|
||||||
|
initiateConvo,
|
||||||
|
convoAvailability,
|
||||||
|
])
|
||||||
|
|
||||||
navigation.navigate('MessagesConversation', {conversation: convo.id})
|
if (!convoAvailability) {
|
||||||
}, [needsEmailVerification, verifyEmailControl, convo, navigation])
|
|
||||||
|
|
||||||
if (isPending) {
|
|
||||||
// show pending state based on declaration
|
// show pending state based on declaration
|
||||||
if (canBeMessaged(profile)) {
|
if (canBeMessaged(profile)) {
|
||||||
return (
|
return (
|
||||||
@@ -69,7 +90,7 @@ export function MessageProfileButton({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (convo) {
|
if (convoAvailability.canChat) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {AppBskyActorDefs, ModerationDecision} from '@atproto/api'
|
import {ModerationDecision} from '@atproto/api'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
@@ -14,6 +14,7 @@ import {BlockedByListDialog} from '#/components/dms/BlockedByListDialog'
|
|||||||
import {LeaveConvoPrompt} from '#/components/dms/LeaveConvoPrompt'
|
import {LeaveConvoPrompt} from '#/components/dms/LeaveConvoPrompt'
|
||||||
import {ReportConversationPrompt} from '#/components/dms/ReportConversationPrompt'
|
import {ReportConversationPrompt} from '#/components/dms/ReportConversationPrompt'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
export function MessagesListBlockedFooter({
|
export function MessagesListBlockedFooter({
|
||||||
recipient: initialRecipient,
|
recipient: initialRecipient,
|
||||||
@@ -21,7 +22,7 @@ export function MessagesListBlockedFooter({
|
|||||||
hasMessages,
|
hasMessages,
|
||||||
moderation,
|
moderation,
|
||||||
}: {
|
}: {
|
||||||
recipient: AppBskyActorDefs.ProfileViewBasic
|
recipient: bsky.profile.AnyProfileView
|
||||||
convoId: string
|
convoId: string
|
||||||
hasMessages: boolean
|
hasMessages: boolean
|
||||||
moderation: ModerationDecision
|
moderation: ModerationDecision
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
|||||||
import {isWeb} from '#/platform/detection'
|
import {isWeb} from '#/platform/detection'
|
||||||
import {Shadow} from '#/state/cache/profile-shadow'
|
import {Shadow} from '#/state/cache/profile-shadow'
|
||||||
import {isConvoActive, useConvo} from '#/state/messages/convo'
|
import {isConvoActive, useConvo} from '#/state/messages/convo'
|
||||||
|
import {ConvoItem} from '#/state/messages/convo/types'
|
||||||
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
|
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
|
||||||
import {ConvoMenu} from '#/components/dms/ConvoMenu'
|
import {ConvoMenu} from '#/components/dms/ConvoMenu'
|
||||||
@@ -31,7 +32,7 @@ export let MessagesListHeader = ({
|
|||||||
profile,
|
profile,
|
||||||
moderation,
|
moderation,
|
||||||
}: {
|
}: {
|
||||||
profile?: Shadow<AppBskyActorDefs.ProfileViewBasic>
|
profile?: Shadow<AppBskyActorDefs.ProfileViewDetailed>
|
||||||
moderation?: ModerationDecision
|
moderation?: ModerationDecision
|
||||||
}): React.ReactNode => {
|
}): React.ReactNode => {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
@@ -52,10 +53,10 @@ export let MessagesListHeader = ({
|
|||||||
}, [moderation])
|
}, [moderation])
|
||||||
|
|
||||||
const onPressBack = useCallback(() => {
|
const onPressBack = useCallback(() => {
|
||||||
if (isWeb) {
|
if (navigation.canGoBack()) {
|
||||||
navigation.replace('Messages', {})
|
|
||||||
} else {
|
|
||||||
navigation.goBack()
|
navigation.goBack()
|
||||||
|
} else {
|
||||||
|
navigation.navigate('Messages', {})
|
||||||
}
|
}
|
||||||
}, [navigation])
|
}, [navigation])
|
||||||
|
|
||||||
@@ -138,7 +139,7 @@ function HeaderReady({
|
|||||||
moderation,
|
moderation,
|
||||||
blockInfo,
|
blockInfo,
|
||||||
}: {
|
}: {
|
||||||
profile: Shadow<AppBskyActorDefs.ProfileViewBasic>
|
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>
|
||||||
moderation: ModerationDecision
|
moderation: ModerationDecision
|
||||||
blockInfo: {
|
blockInfo: {
|
||||||
listBlocks: ModerationCause[]
|
listBlocks: ModerationCause[]
|
||||||
@@ -157,8 +158,10 @@ function HeaderReady({
|
|||||||
moderation.ui('displayName'),
|
moderation.ui('displayName'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// @ts-ignore findLast is polyfilled - esb
|
||||||
const latestMessageFromOther = convoState.items.findLast(
|
const latestMessageFromOther = convoState.items.findLast(
|
||||||
item => item.type === 'message' && item.message.sender.did === profile.did,
|
(item: ConvoItem) =>
|
||||||
|
item.type === 'message' && item.message.sender.did === profile.did,
|
||||||
)
|
)
|
||||||
|
|
||||||
const latestReportableMessage =
|
const latestReportableMessage =
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, {memo, useMemo, useState} from 'react'
|
import React, {memo, useMemo, useState} from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {
|
import {
|
||||||
|
$Typed,
|
||||||
AppBskyActorDefs,
|
AppBskyActorDefs,
|
||||||
ChatBskyConvoDefs,
|
ChatBskyConvoDefs,
|
||||||
ComAtprotoModerationCreateReport,
|
ComAtprotoModerationCreateReport,
|
||||||
@@ -154,15 +155,16 @@ function SubmitStep({
|
|||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
if (params.type === 'convoMessage') {
|
if (params.type === 'convoMessage') {
|
||||||
const {convoId, message} = params
|
const {convoId, message} = params
|
||||||
|
const subject: $Typed<ChatBskyConvoDefs.MessageRef> = {
|
||||||
const report = {
|
|
||||||
reasonType: reportOption.reason,
|
|
||||||
subject: {
|
|
||||||
$type: 'chat.bsky.convo.defs#messageRef',
|
$type: 'chat.bsky.convo.defs#messageRef',
|
||||||
messageId: message.id,
|
messageId: message.id,
|
||||||
convoId,
|
convoId,
|
||||||
did: message.sender.did,
|
did: message.sender.did,
|
||||||
} satisfies ChatBskyConvoDefs.MessageRef,
|
}
|
||||||
|
|
||||||
|
const report = {
|
||||||
|
reasonType: reportOption.reason,
|
||||||
|
subject,
|
||||||
reason: details,
|
reason: details,
|
||||||
} satisfies ComAtprotoModerationCreateReport.InputSchema
|
} satisfies ComAtprotoModerationCreateReport.InputSchema
|
||||||
|
|
||||||
@@ -285,7 +287,7 @@ function DoneStep({
|
|||||||
}: {
|
}: {
|
||||||
convoId: string
|
convoId: string
|
||||||
currentScreen: 'list' | 'conversation'
|
currentScreen: 'list' | 'conversation'
|
||||||
profile: AppBskyActorDefs.ProfileViewBasic
|
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||||
}) {
|
}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
@@ -309,6 +311,19 @@ function DoneStep({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
let btnText = _(msg`Done`)
|
||||||
|
let toastMsg: string | undefined
|
||||||
|
if (actions.includes('leave') && actions.includes('block')) {
|
||||||
|
btnText = _(msg`Block and Delete`)
|
||||||
|
toastMsg = _(msg({message: 'Conversation deleted', context: 'toast'}))
|
||||||
|
} else if (actions.includes('leave')) {
|
||||||
|
btnText = _(msg`Delete Conversation`)
|
||||||
|
toastMsg = _(msg({message: 'Conversation deleted', context: 'toast'}))
|
||||||
|
} else if (actions.includes('block')) {
|
||||||
|
btnText = _(msg`Block User`)
|
||||||
|
toastMsg = _(msg({message: 'User blocked', context: 'toast'}))
|
||||||
|
}
|
||||||
|
|
||||||
const onPressPrimaryAction = () => {
|
const onPressPrimaryAction = () => {
|
||||||
control.close(() => {
|
control.close(() => {
|
||||||
if (actions.includes('block')) {
|
if (actions.includes('block')) {
|
||||||
@@ -317,16 +332,10 @@ function DoneStep({
|
|||||||
if (actions.includes('leave')) {
|
if (actions.includes('leave')) {
|
||||||
leaveConvo()
|
leaveConvo()
|
||||||
}
|
}
|
||||||
})
|
if (toastMsg) {
|
||||||
|
Toast.show(toastMsg, 'check')
|
||||||
}
|
}
|
||||||
|
})
|
||||||
let btnText = _(msg`Done`)
|
|
||||||
if (actions.includes('leave') && actions.includes('block')) {
|
|
||||||
btnText = _(msg`Block and Delete`)
|
|
||||||
} else if (actions.includes('leave')) {
|
|
||||||
btnText = _(msg`Delete Conversation`)
|
|
||||||
} else if (actions.includes('block')) {
|
|
||||||
btnText = _(msg`Block User`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import React, {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import {TextInput, View} from 'react-native'
|
import {TextInput, View} from 'react-native'
|
||||||
import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
|
import {moderateProfile, ModerationOpts} from '@atproto/api'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
@@ -28,13 +28,14 @@ import {useInteractionState} from '#/components/hooks/useInteractionState'
|
|||||||
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
|
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
|
||||||
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
type Item =
|
type Item =
|
||||||
| {
|
| {
|
||||||
type: 'profile'
|
type: 'profile'
|
||||||
key: string
|
key: string
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
profile: AppBskyActorDefs.ProfileView
|
profile: bsky.profile.AnyProfileView
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'empty'
|
type: 'empty'
|
||||||
@@ -76,7 +77,10 @@ export function SearchablePeopleList({
|
|||||||
isFetching,
|
isFetching,
|
||||||
} = useActorAutocompleteQuery(searchText, true, 12)
|
} = useActorAutocompleteQuery(searchText, true, 12)
|
||||||
const {data: follows} = useProfileFollowsQuery(currentAccount?.did)
|
const {data: follows} = useProfileFollowsQuery(currentAccount?.did)
|
||||||
const {data: convos} = useListConvosQuery({enabled: showRecentConvos})
|
const {data: convos} = useListConvosQuery({
|
||||||
|
enabled: showRecentConvos,
|
||||||
|
status: 'accepted',
|
||||||
|
})
|
||||||
|
|
||||||
const items = useMemo(() => {
|
const items = useMemo(() => {
|
||||||
let _items: Item[] = []
|
let _items: Item[] = []
|
||||||
@@ -330,7 +334,7 @@ function ProfileCard({
|
|||||||
onPress,
|
onPress,
|
||||||
}: {
|
}: {
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
profile: AppBskyActorDefs.ProfileView
|
profile: bsky.profile.AnyProfileView
|
||||||
moderationOpts: ModerationOpts
|
moderationOpts: ModerationOpts
|
||||||
onPress: (did: string) => void
|
onPress: (did: string) => void
|
||||||
}) {
|
}) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {AppBskyActorDefs} from '@atproto/api'
|
import * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
export function canBeMessaged(profile: AppBskyActorDefs.ProfileView) {
|
export function canBeMessaged(profile: bsky.profile.AnyProfileView) {
|
||||||
switch (profile.associated?.chat?.allowIncoming) {
|
switch (profile.associated?.chat?.allowIncoming) {
|
||||||
case 'none':
|
case 'none':
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from 'react'
|
import {useCallback, useImperativeHandle, useState} from 'react'
|
||||||
|
import {Keyboard} from 'react-native'
|
||||||
import DatePicker from 'react-native-date-picker'
|
import DatePicker from 'react-native-date-picker'
|
||||||
|
|
||||||
import {useTheme} from '#/alf'
|
import {useTheme} from '#/alf'
|
||||||
@@ -12,6 +13,7 @@ export const LabelText = TextField.LabelText
|
|||||||
|
|
||||||
export function DateField({
|
export function DateField({
|
||||||
value,
|
value,
|
||||||
|
inputRef,
|
||||||
onChangeDate,
|
onChangeDate,
|
||||||
label,
|
label,
|
||||||
isInvalid,
|
isInvalid,
|
||||||
@@ -20,9 +22,9 @@ export function DateField({
|
|||||||
maximumDate,
|
maximumDate,
|
||||||
}: DateFieldProps) {
|
}: DateFieldProps) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const [open, setOpen] = React.useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
const onChangeInternal = React.useCallback(
|
const onChangeInternal = useCallback(
|
||||||
(date: Date) => {
|
(date: Date) => {
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
|
|
||||||
@@ -32,11 +34,25 @@ export function DateField({
|
|||||||
[onChangeDate, setOpen],
|
[onChangeDate, setOpen],
|
||||||
)
|
)
|
||||||
|
|
||||||
const onPress = React.useCallback(() => {
|
useImperativeHandle(
|
||||||
|
inputRef,
|
||||||
|
() => ({
|
||||||
|
focus: () => {
|
||||||
|
Keyboard.dismiss()
|
||||||
|
setOpen(true)
|
||||||
|
},
|
||||||
|
blur: () => {
|
||||||
|
setOpen(false)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
|
const onPress = useCallback(() => {
|
||||||
setOpen(true)
|
setOpen(true)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const onCancel = React.useCallback(() => {
|
const onCancel = useCallback(() => {
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {Pressable, View} from 'react-native'
|
import {Pressable, View} from 'react-native'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {android, atoms as a, useTheme, web} from '#/alf'
|
import {atoms as a, native, useTheme, web} from '#/alf'
|
||||||
import * as TextField from '#/components/forms/TextField'
|
import * as TextField from '#/components/forms/TextField'
|
||||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||||
import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays'
|
import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays'
|
||||||
@@ -60,16 +60,16 @@ export function DateFieldButton({
|
|||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
style={[
|
style={[
|
||||||
{
|
{
|
||||||
paddingTop: 12,
|
|
||||||
paddingBottom: 12,
|
|
||||||
paddingLeft: 14,
|
paddingLeft: 14,
|
||||||
paddingRight: 14,
|
paddingRight: 14,
|
||||||
borderColor: 'transparent',
|
borderColor: 'transparent',
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
},
|
},
|
||||||
android({
|
native({
|
||||||
minHeight: 57.5,
|
paddingTop: 10,
|
||||||
|
paddingBottom: 10,
|
||||||
}),
|
}),
|
||||||
|
web(a.py_md),
|
||||||
a.flex_row,
|
a.flex_row,
|
||||||
a.flex_1,
|
a.flex_1,
|
||||||
a.w_full,
|
a.w_full,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import {useCallback, useImperativeHandle} from 'react'
|
||||||
import {Keyboard, View} from 'react-native'
|
import {Keyboard, View} from 'react-native'
|
||||||
import DatePicker from 'react-native-date-picker'
|
import DatePicker from 'react-native-date-picker'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
@@ -25,6 +25,7 @@ export const LabelText = TextField.LabelText
|
|||||||
*/
|
*/
|
||||||
export function DateField({
|
export function DateField({
|
||||||
value,
|
value,
|
||||||
|
inputRef,
|
||||||
onChangeDate,
|
onChangeDate,
|
||||||
testID,
|
testID,
|
||||||
label,
|
label,
|
||||||
@@ -36,7 +37,7 @@ export function DateField({
|
|||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const control = Dialog.useDialogControl()
|
const control = Dialog.useDialogControl()
|
||||||
|
|
||||||
const onChangeInternal = React.useCallback(
|
const onChangeInternal = useCallback(
|
||||||
(date: Date | undefined) => {
|
(date: Date | undefined) => {
|
||||||
if (date) {
|
if (date) {
|
||||||
const formatted = toSimpleDateString(date)
|
const formatted = toSimpleDateString(date)
|
||||||
@@ -46,6 +47,20 @@ export function DateField({
|
|||||||
[onChangeDate],
|
[onChangeDate],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
useImperativeHandle(
|
||||||
|
inputRef,
|
||||||
|
() => ({
|
||||||
|
focus: () => {
|
||||||
|
Keyboard.dismiss()
|
||||||
|
control.open()
|
||||||
|
},
|
||||||
|
blur: () => {
|
||||||
|
control.close()
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[control],
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DateFieldButton
|
<DateFieldButton
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ const Input = TextField.createInput(InputBase as unknown as typeof TextInput)
|
|||||||
|
|
||||||
export function DateField({
|
export function DateField({
|
||||||
value,
|
value,
|
||||||
|
inputRef,
|
||||||
onChangeDate,
|
onChangeDate,
|
||||||
label,
|
label,
|
||||||
isInvalid,
|
isInvalid,
|
||||||
@@ -58,9 +59,9 @@ export function DateField({
|
|||||||
<TextField.Icon icon={CalendarDays} />
|
<TextField.Icon icon={CalendarDays} />
|
||||||
<Input
|
<Input
|
||||||
value={toSimpleDateString(value)}
|
value={toSimpleDateString(value)}
|
||||||
|
inputRef={inputRef as React.Ref<TextInput>}
|
||||||
label={label}
|
label={label}
|
||||||
onChange={handleOnChange}
|
onChange={handleOnChange}
|
||||||
onChangeText={() => {}}
|
|
||||||
testID={testID}
|
testID={testID}
|
||||||
accessibilityHint={accessibilityHint}
|
accessibilityHint={accessibilityHint}
|
||||||
// @ts-expect-error not typed as <input type="date"> even though it is one
|
// @ts-expect-error not typed as <input type="date"> even though it is one
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
|
export type DateFieldRef = {
|
||||||
|
focus: () => void
|
||||||
|
blur: () => void
|
||||||
|
}
|
||||||
export type DateFieldProps = {
|
export type DateFieldProps = {
|
||||||
value: string | Date
|
value: string | Date
|
||||||
onChangeDate: (date: string) => void
|
onChangeDate: (date: string) => void
|
||||||
label: string
|
label: string
|
||||||
|
inputRef?: React.Ref<DateFieldRef>
|
||||||
isInvalid?: boolean
|
isInvalid?: boolean
|
||||||
testID?: string
|
testID?: string
|
||||||
accessibilityHint?: string
|
accessibilityHint?: string
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {Keyboard, View} from 'react-native'
|
import {Keyboard, View} from 'react-native'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {toNiceDomain} from '#/lib/strings/url-helpers'
|
import {toNiceDomain} from '#/lib/strings/url-helpers'
|
||||||
@@ -43,8 +43,8 @@ export function HostingProvider({
|
|||||||
{minimal ? (
|
{minimal ? (
|
||||||
<View style={[a.flex_row, a.align_center, a.flex_wrap]}>
|
<View style={[a.flex_row, a.align_center, a.flex_wrap]}>
|
||||||
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
|
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
|
||||||
|
<Trans>
|
||||||
You are creating an account on{' '}
|
You are creating an account on{' '}
|
||||||
</Text>
|
|
||||||
<Button
|
<Button
|
||||||
label={toNiceDomain(serviceUrl)}
|
label={toNiceDomain(serviceUrl)}
|
||||||
accessibilityHint={_(msg`Changes hosting provider`)}
|
accessibilityHint={_(msg`Changes hosting provider`)}
|
||||||
@@ -52,12 +52,14 @@ export function HostingProvider({
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
size="tiny"
|
size="tiny"
|
||||||
style={[a.px_xs, {marginLeft: tokens.space.xs * -1}]}>
|
style={[a.px_xs, {marginHorizontal: tokens.space.xs * -1}]}>
|
||||||
<ButtonText style={[a.text_sm]}>
|
<ButtonText style={[a.text_sm]}>
|
||||||
{toNiceDomain(serviceUrl)}
|
{toNiceDomain(serviceUrl)}
|
||||||
</ButtonText>
|
</ButtonText>
|
||||||
<ButtonIcon icon={PencilIcon} />
|
<ButtonIcon icon={PencilIcon} />
|
||||||
</Button>
|
</Button>
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
android,
|
android,
|
||||||
applyFonts,
|
applyFonts,
|
||||||
atoms as a,
|
atoms as a,
|
||||||
|
ios,
|
||||||
TextStyleProp,
|
TextStyleProp,
|
||||||
useAlf,
|
useAlf,
|
||||||
useTheme,
|
useTheme,
|
||||||
@@ -189,13 +190,13 @@ export function createInput(Component: typeof TextInput) {
|
|||||||
a.px_xs,
|
a.px_xs,
|
||||||
{
|
{
|
||||||
// paddingVertical doesn't work w/multiline - esb
|
// paddingVertical doesn't work w/multiline - esb
|
||||||
paddingTop: 12,
|
|
||||||
paddingBottom: 13,
|
|
||||||
lineHeight: a.text_md.fontSize * 1.1875,
|
lineHeight: a.text_md.fontSize * 1.1875,
|
||||||
textAlignVertical: rest.multiline ? 'top' : undefined,
|
textAlignVertical: rest.multiline ? 'top' : undefined,
|
||||||
minHeight: rest.multiline ? 80 : undefined,
|
minHeight: rest.multiline ? 80 : undefined,
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
},
|
},
|
||||||
|
ios({paddingTop: 12, paddingBottom: 13}),
|
||||||
|
android(a.py_sm),
|
||||||
// fix for autofill styles covering border
|
// fix for autofill styles covering border
|
||||||
web({
|
web({
|
||||||
paddingTop: 10,
|
paddingTop: 10,
|
||||||
@@ -203,10 +204,6 @@ export function createInput(Component: typeof TextInput) {
|
|||||||
marginTop: 2,
|
marginTop: 2,
|
||||||
marginBottom: 2,
|
marginBottom: 2,
|
||||||
}),
|
}),
|
||||||
android({
|
|
||||||
paddingTop: 8,
|
|
||||||
paddingBottom: 8,
|
|
||||||
}),
|
|
||||||
style,
|
style,
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -355,17 +352,9 @@ export function SuffixText({
|
|||||||
a.pr_sm,
|
a.pr_sm,
|
||||||
a.text_md,
|
a.text_md,
|
||||||
t.atoms.text_contrast_medium,
|
t.atoms.text_contrast_medium,
|
||||||
{
|
a.pointer_events_none,
|
||||||
pointerEvents: 'none',
|
web([{marginTop: -2}, a.leading_snug]),
|
||||||
},
|
(ctx.hovered || ctx.focused) && {color: t.palette.contrast_800},
|
||||||
web({
|
|
||||||
marginTop: -2,
|
|
||||||
}),
|
|
||||||
ctx.hovered || ctx.focused
|
|
||||||
? {
|
|
||||||
color: t.palette.contrast_800,
|
|
||||||
}
|
|
||||||
: {},
|
|
||||||
style,
|
style,
|
||||||
]}>
|
]}>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -11,14 +11,17 @@ import React from 'react'
|
|||||||
import {formatDistance, Locale} from 'date-fns'
|
import {formatDistance, Locale} from 'date-fns'
|
||||||
import {
|
import {
|
||||||
ca,
|
ca,
|
||||||
|
cy,
|
||||||
da,
|
da,
|
||||||
de,
|
de,
|
||||||
el,
|
el,
|
||||||
enGB,
|
enGB,
|
||||||
|
eo,
|
||||||
es,
|
es,
|
||||||
eu,
|
eu,
|
||||||
fi,
|
fi,
|
||||||
fr,
|
fr,
|
||||||
|
gd,
|
||||||
gl,
|
gl,
|
||||||
hi,
|
hi,
|
||||||
hu,
|
hu,
|
||||||
@@ -53,15 +56,18 @@ const locales: Record<AppLanguage, Locale | undefined> = {
|
|||||||
an: undefined,
|
an: undefined,
|
||||||
ast: undefined,
|
ast: undefined,
|
||||||
ca,
|
ca,
|
||||||
|
cy,
|
||||||
da,
|
da,
|
||||||
de,
|
de,
|
||||||
el,
|
el,
|
||||||
['en-GB']: enGB,
|
['en-GB']: enGB,
|
||||||
|
eo,
|
||||||
es,
|
es,
|
||||||
eu,
|
eu,
|
||||||
fi,
|
fi,
|
||||||
fr,
|
fr,
|
||||||
ga: undefined,
|
ga: undefined,
|
||||||
|
gd,
|
||||||
gl,
|
gl,
|
||||||
hi,
|
hi,
|
||||||
hu,
|
hu,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {AppBskyActorDefs} from '@atproto/api'
|
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
@@ -9,12 +8,13 @@ import {Shadow} from '#/state/cache/types'
|
|||||||
import {useProfileFollowMutationQueue} from '#/state/queries/profile'
|
import {useProfileFollowMutationQueue} from '#/state/queries/profile'
|
||||||
import {useRequireAuth} from '#/state/session'
|
import {useRequireAuth} from '#/state/session'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
|
import * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
export function useFollowMethods({
|
export function useFollowMethods({
|
||||||
profile,
|
profile,
|
||||||
logContext,
|
logContext,
|
||||||
}: {
|
}: {
|
||||||
profile: Shadow<AppBskyActorDefs.ProfileViewBasic>
|
profile: Shadow<bsky.profile.AnyProfileView>
|
||||||
logContext: LogEvents['profile:follow']['logContext'] &
|
logContext: LogEvents['profile:follow']['logContext'] &
|
||||||
LogEvents['profile:unfollow']['logContext']
|
LogEvents['profile:unfollow']['logContext']
|
||||||
}) {
|
}) {
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import {createSinglePathSVG} from './TEMPLATE'
|
||||||
|
|
||||||
|
export const CircleX_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm6.293-3.707a1 1 0 0 1 1.414 0L12 10.586l2.293-2.293a1 1 0 1 1 1.414 1.414L13.414 12l2.293 2.293a1 1 0 0 1-1.414 1.414L12 13.414l-2.293 2.293a1 1 0 0 1-1.414-1.414L10.586 12 8.293 9.707a1 1 0 0 1 0-1.414Z',
|
||||||
|
})
|
||||||
@@ -268,6 +268,7 @@ export function LabelerLabelPreference({
|
|||||||
a.rounded_sm,
|
a.rounded_sm,
|
||||||
a.border,
|
a.border,
|
||||||
t.atoms.border_contrast_low,
|
t.atoms.border_contrast_low,
|
||||||
|
a.self_start,
|
||||||
]}>
|
]}>
|
||||||
<Text emoji style={[a.font_bold, t.atoms.text_contrast_low]}>
|
<Text emoji style={[a.font_bold, t.atoms.text_contrast_low]}>
|
||||||
{currentPrefLabel}
|
{currentPrefLabel}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {msg, Trans} from '@lingui/macro'
|
|||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useMutation} from '@tanstack/react-query'
|
import {useMutation} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
|
||||||
import {useLabelSubject} from '#/lib/moderation'
|
import {useLabelSubject} from '#/lib/moderation'
|
||||||
import {useLabelInfo} from '#/lib/moderation/useLabelInfo'
|
import {useLabelInfo} from '#/lib/moderation/useLabelInfo'
|
||||||
import {makeProfileLink} from '#/lib/routes/links'
|
import {makeProfileLink} from '#/lib/routes/links'
|
||||||
@@ -66,7 +67,7 @@ function LabelsOnMeDialogInner(props: LabelsOnMeDialogProps) {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Text style={[a.text_2xl, a.font_bold, a.pb_xs, a.leading_tight]}>
|
<Text style={[a.text_2xl, a.font_heavy, a.pb_xs, a.leading_tight]}>
|
||||||
{isAccount ? (
|
{isAccount ? (
|
||||||
<Trans>Labels on your account</Trans>
|
<Trans>Labels on your account</Trans>
|
||||||
) : (
|
) : (
|
||||||
@@ -122,6 +123,7 @@ function Label({
|
|||||||
const sourceName = labeler
|
const sourceName = labeler
|
||||||
? sanitizeHandle(labeler.creator.handle, '@')
|
? sanitizeHandle(labeler.creator.handle, '@')
|
||||||
: label.src
|
: label.src
|
||||||
|
const timeDiff = useGetTimeAgo({future: true})
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
@@ -163,10 +165,18 @@ function Label({
|
|||||||
<Trans>This label was applied by you.</Trans>
|
<Trans>This label was applied by you.</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
) : (
|
) : (
|
||||||
<View style={{flexDirection: 'row'}}>
|
<View
|
||||||
<Text style={[t.atoms.text_contrast_medium]}>
|
style={[
|
||||||
<Trans>Source: </Trans>{' '}
|
a.flex_row,
|
||||||
</Text>
|
a.justify_between,
|
||||||
|
a.gap_xl,
|
||||||
|
{paddingBottom: 1},
|
||||||
|
]}>
|
||||||
|
<Text
|
||||||
|
style={[a.flex_1, a.leading_snug, t.atoms.text_contrast_medium]}
|
||||||
|
numberOfLines={1}>
|
||||||
|
<Trans>
|
||||||
|
Source:{' '}
|
||||||
<InlineLinkText
|
<InlineLinkText
|
||||||
label={sourceName}
|
label={sourceName}
|
||||||
to={makeProfileLink(
|
to={makeProfileLink(
|
||||||
@@ -175,6 +185,21 @@ function Label({
|
|||||||
onPress={() => control.close()}>
|
onPress={() => control.close()}>
|
||||||
{sourceName}
|
{sourceName}
|
||||||
</InlineLinkText>
|
</InlineLinkText>
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
{label.exp && (
|
||||||
|
<View>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
a.leading_snug,
|
||||||
|
a.text_sm,
|
||||||
|
a.italic,
|
||||||
|
t.atoms.text_contrast_medium,
|
||||||
|
]}>
|
||||||
|
<Trans>Expires in {timeDiff(Date.now(), label.exp)}</Trans>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
@@ -230,7 +255,7 @@ function AppealForm({
|
|||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
control.close()
|
control.close()
|
||||||
Toast.show(_(msg`Appeal submitted`))
|
Toast.show(_(msg({message: 'Appeal submitted', context: 'toast'})))
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ import {ModerationCause} from '@atproto/api'
|
|||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
|
||||||
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
|
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
|
||||||
import {makeProfileLink} from '#/lib/routes/links'
|
import {makeProfileLink} from '#/lib/routes/links'
|
||||||
import {listUriToHref} from '#/lib/strings/url-helpers'
|
import {listUriToHref} from '#/lib/strings/url-helpers'
|
||||||
import {isNative} from '#/platform/detection'
|
import {isNative} from '#/platform/detection'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useGutters, useTheme} from '#/alf'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
import {Divider} from '#/components/Divider'
|
|
||||||
import {InlineLinkText} from '#/components/Link'
|
import {InlineLinkText} from '#/components/Link'
|
||||||
import {AppModerationCause} from '#/components/Pills'
|
import {AppModerationCause} from '#/components/Pills'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
@@ -38,9 +38,11 @@ function ModerationDetailsDialogInner({
|
|||||||
control: Dialog.DialogOuterProps['control']
|
control: Dialog.DialogOuterProps['control']
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
const xGutters = useGutters([0, 'base'])
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const desc = useModerationCauseDescription(modcause)
|
const desc = useModerationCauseDescription(modcause)
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
|
const timeDiff = useGetTimeAgo({future: true})
|
||||||
|
|
||||||
let name
|
let name
|
||||||
let description
|
let description
|
||||||
@@ -128,37 +130,88 @@ function ModerationDetailsDialogInner({
|
|||||||
description = ''
|
description = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sourceName =
|
||||||
|
desc.source || desc.sourceDisplayName || _(msg`an unknown labeler`)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog.ScrollableInner label={_(msg`Moderation details`)}>
|
<Dialog.ScrollableInner
|
||||||
<Text emoji style={[t.atoms.text, a.text_2xl, a.font_bold, a.mb_sm]}>
|
label={_(msg`Moderation details`)}
|
||||||
|
contentContainerStyle={{
|
||||||
|
paddingLeft: 0,
|
||||||
|
paddingRight: 0,
|
||||||
|
paddingBottom: 0,
|
||||||
|
}}>
|
||||||
|
<View style={[xGutters, a.pb_lg]}>
|
||||||
|
<Text emoji style={[t.atoms.text, a.text_2xl, a.font_heavy, a.mb_sm]}>
|
||||||
{name}
|
{name}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={[t.atoms.text, a.text_md, a.leading_snug]}>
|
<Text style={[t.atoms.text, a.text_sm, a.leading_snug]}>
|
||||||
{description}
|
{description}
|
||||||
</Text>
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
{modcause?.type === 'label' && (
|
{modcause?.type === 'label' && (
|
||||||
<View style={[a.pt_lg]}>
|
<View
|
||||||
<Divider />
|
style={[
|
||||||
|
xGutters,
|
||||||
|
a.py_md,
|
||||||
|
a.border_t,
|
||||||
|
!isNative && t.atoms.bg_contrast_25,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
{
|
||||||
|
borderBottomLeftRadius: a.rounded_md.borderRadius,
|
||||||
|
borderBottomRightRadius: a.rounded_md.borderRadius,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
{modcause.source.type === 'user' ? (
|
{modcause.source.type === 'user' ? (
|
||||||
<Text style={[t.atoms.text, a.text_md, a.leading_snug, a.mt_lg]}>
|
<Text style={[t.atoms.text, a.text_md, a.leading_snug]}>
|
||||||
<Trans>This label was applied by the author.</Trans>
|
<Trans>This label was applied by the author.</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Text style={[t.atoms.text, a.text_md, a.leading_snug, a.mt_lg]}>
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_row,
|
||||||
|
a.justify_between,
|
||||||
|
a.gap_xl,
|
||||||
|
{paddingBottom: 1},
|
||||||
|
]}>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
a.flex_1,
|
||||||
|
a.leading_snug,
|
||||||
|
t.atoms.text_contrast_medium,
|
||||||
|
]}
|
||||||
|
numberOfLines={1}>
|
||||||
<Trans>
|
<Trans>
|
||||||
This label was applied by{' '}
|
Source:{' '}
|
||||||
<InlineLinkText
|
<InlineLinkText
|
||||||
label={desc.source || _(msg`an unknown labeler`)}
|
label={sourceName}
|
||||||
to={makeProfileLink({did: modcause.label.src, handle: ''})}
|
to={makeProfileLink({
|
||||||
onPress={() => control.close()}
|
did: modcause.label.src,
|
||||||
style={a.text_md}>
|
handle: '',
|
||||||
{desc.source || _(msg`an unknown labeler`)}
|
})}
|
||||||
|
onPress={() => control.close()}>
|
||||||
|
{sourceName}
|
||||||
</InlineLinkText>
|
</InlineLinkText>
|
||||||
.
|
|
||||||
</Trans>
|
</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
|
{modcause.label.exp && (
|
||||||
|
<View>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
a.leading_snug,
|
||||||
|
a.text_sm,
|
||||||
|
a.italic,
|
||||||
|
t.atoms.text_contrast_medium,
|
||||||
|
]}>
|
||||||
|
<Trans>
|
||||||
|
Expires in {timeDiff(Date.now(), modcause.label.exp)}
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {StyleProp, ViewStyle} from 'react-native'
|
import {StyleProp, ViewStyle} from 'react-native'
|
||||||
import {ModerationCause, ModerationUI} from '@atproto/api'
|
import {ModerationCause, ModerationUI} from '@atproto/api'
|
||||||
|
|
||||||
import {getModerationCauseKey} from '#/lib/moderation'
|
import {getModerationCauseKey, unique} from '#/lib/moderation'
|
||||||
import * as Pills from '#/components/Pills'
|
import * as Pills from '#/components/Pills'
|
||||||
|
|
||||||
export function PostAlerts({
|
export function PostAlerts({
|
||||||
@@ -22,7 +22,7 @@ export function PostAlerts({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Pills.Row size={size} style={[size === 'sm' && {marginLeft: -3}, style]}>
|
<Pills.Row size={size} style={[size === 'sm' && {marginLeft: -3}, style]}>
|
||||||
{modui.alerts.map(cause => (
|
{modui.alerts.filter(unique).map(cause => (
|
||||||
<Pills.Label
|
<Pills.Label
|
||||||
key={getModerationCauseKey(cause)}
|
key={getModerationCauseKey(cause)}
|
||||||
cause={cause}
|
cause={cause}
|
||||||
@@ -30,7 +30,7 @@ export function PostAlerts({
|
|||||||
noBg={size === 'sm'}
|
noBg={size === 'sm'}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{modui.informs.map(cause => (
|
{modui.informs.filter(unique).map(cause => (
|
||||||
<Pills.Label
|
<Pills.Label
|
||||||
key={getModerationCauseKey(cause)}
|
key={getModerationCauseKey(cause)}
|
||||||
cause={cause}
|
cause={cause}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {StyleProp, ViewStyle} from 'react-native'
|
import {StyleProp, ViewStyle} from 'react-native'
|
||||||
import {ModerationDecision} from '@atproto/api'
|
import {ModerationDecision} from '@atproto/api'
|
||||||
|
|
||||||
import {getModerationCauseKey} from '#/lib/moderation'
|
import {getModerationCauseKey, unique} from '#/lib/moderation'
|
||||||
import * as Pills from '#/components/Pills'
|
import * as Pills from '#/components/Pills'
|
||||||
|
|
||||||
export function ProfileHeaderAlerts({
|
export function ProfileHeaderAlerts({
|
||||||
@@ -17,14 +17,14 @@ export function ProfileHeaderAlerts({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Pills.Row size="lg">
|
<Pills.Row size="lg">
|
||||||
{modui.alerts.map(cause => (
|
{modui.alerts.filter(unique).map(cause => (
|
||||||
<Pills.Label
|
<Pills.Label
|
||||||
size="lg"
|
size="lg"
|
||||||
key={getModerationCauseKey(cause)}
|
key={getModerationCauseKey(cause)}
|
||||||
cause={cause}
|
cause={cause}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{modui.informs.map(cause => (
|
{modui.informs.filter(unique).map(cause => (
|
||||||
<Pills.Label
|
<Pills.Label
|
||||||
size="lg"
|
size="lg"
|
||||||
key={getModerationCauseKey(cause)}
|
key={getModerationCauseKey(cause)}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user