Compare commits
130 Commits
env
..
patch-version
| Author | SHA1 | Date | |
|---|---|---|---|
| 4adeb4befc | |||
| d155b718b5 | |||
| 166c56c4cb | |||
| 131578ca3a | |||
| 0014b8b9cd | |||
| f06a78017a | |||
| 27ae829698 | |||
| ada2ac8b91 | |||
| fd8ac1da7d | |||
| beb0fce56c | |||
| ac43b666cb | |||
| 618c4e4a56 | |||
| 153fc17676 | |||
| 8eb1f8f1fc | |||
| da2b6f4797 | |||
| 1c16c6ef0a | |||
| a7db0bac6f | |||
| 29eef6188a | |||
| ba3cff62f1 | |||
| 3be87fbf3b | |||
| 9fde3957e7 | |||
| dae152da85 | |||
| a42efd15d7 | |||
| d0c2003cc7 | |||
| 1cfdd2a7a0 | |||
| 663f16dfc2 | |||
| 947c876fdb | |||
| a62fe855c3 | |||
| c5d9e282e5 | |||
| f5f02aa4b3 | |||
| 2d854091b9 | |||
| 01a51c3275 | |||
| dda0db9963 | |||
| 67c394bc82 | |||
| c995eb2f2f | |||
| 5c14f69566 | |||
| 66c09d9953 | |||
| c82f768374 | |||
| 7222d6fe52 | |||
| 8b6dde17fd | |||
| 07dd57d1ea | |||
| 1c582c59f0 | |||
| 84fcbe398b | |||
| 3be9dde92d | |||
| 96f4f6359a | |||
| 9e59a2eef2 | |||
| 462708e207 | |||
| 36a7a8d8c6 | |||
| 7c36ea1158 | |||
| 9e9ffd5c6e | |||
| 3f963cfed7 | |||
| a2b71e3a4b | |||
| a3d36393a1 | |||
| b996b304c3 | |||
| ec75cbc01f | |||
| b924c53b23 | |||
| c5b831d5d8 | |||
| 3795d85591 | |||
| 7dc8ba8e4e | |||
| 22aee9704c | |||
| 3409a72548 | |||
| 595a894551 | |||
| 0d1373bb71 | |||
| cc8369e868 | |||
| 0437838649 | |||
| 23f0092440 | |||
| 9c5d92088d | |||
| e40656128d | |||
| d76cd88d91 | |||
| 6254cf020a | |||
| 61a14043e5 | |||
| 3fc4c32ac6 | |||
| a383b9ba28 | |||
| bf4d3e89c2 | |||
| 1fd3af3c46 | |||
| 3f879f0315 | |||
| 7c14b54ab2 | |||
| 9c739912a3 | |||
| c4dec63960 | |||
| a7e1913e22 | |||
| a1699da1a9 | |||
| ae9176c9c2 | |||
| 3d954a00e0 | |||
| f9392d4a96 | |||
| bdcddee7a5 | |||
| 5c3576dee7 | |||
| 7a3a1a2bd0 | |||
| e6193d7036 | |||
| cb85768e9c | |||
| ca7116ec3b | |||
| 80e59d31d0 | |||
| ca70aacb8a | |||
| e6bfeb1bab | |||
| 3b4f1e91e3 | |||
| fff625167f | |||
| 5d30111b78 | |||
| 798bf47827 | |||
| b0f7f51b04 | |||
| ea9f2dfbdd | |||
| 1fda7d0553 | |||
| 7ff65df417 | |||
| f8f88878dc | |||
| 8f63cb86b1 | |||
| ed4add7072 | |||
| 48fd397d46 | |||
| f23a621c7e | |||
| da45c42b6f | |||
| 63ba0a436b | |||
| a51fc8e434 | |||
| e2c0f78719 | |||
| 4146f9fe02 | |||
| a28791680f | |||
| d805dec08f | |||
| 326445d3af | |||
| 40d05ac866 | |||
| cb862bffea | |||
| 1e3b7feccf | |||
| ab4be7a9e1 | |||
| a154459b8f | |||
| d793abf8cd | |||
| 11616846ca | |||
| a14704ddfe | |||
| 22a902e0de | |||
| 99051f0ab1 | |||
| f45f9afab7 | |||
| a3f79cc714 | |||
| ae8ddc2c41 | |||
| d56efe250e | |||
| db25f95c33 | |||
| b37199a5a0 |
+5
-20
@@ -1,23 +1,8 @@
|
||||
# The env the app is running in e.g. development, testflight, production
|
||||
EXPO_PUBLIC_ENV=development
|
||||
# This is the commit hash that the current bundle was made from. The user can
|
||||
# see the commit hash in the app's settings along with the other version info.
|
||||
# Useful for debugging/reporting.
|
||||
EXPO_PUBLIC_BUNDLE_IDENTIFIER=
|
||||
# This will always be in the format of YYMMDD, so that it always increases for
|
||||
# each build. This should only be used for Statsig reporting and shouldn't be
|
||||
# used to identify a specific bundle.
|
||||
EXPO_PUBLIC_BUNDLE_DATE=0
|
||||
# The log level for the app's logger transports
|
||||
EXPO_PUBLIC_LOG_LEVEL=info
|
||||
# Currently unused. Enable debug for a subset of the debug logs.
|
||||
EXPO_PUBLIC_LOG_DEBUG=
|
||||
# Copy this to `.env` and `.env.test` files
|
||||
|
||||
#
|
||||
# Bluesky specific
|
||||
#
|
||||
|
||||
# Bitdrift
|
||||
BITDRIFT_API_KEY=
|
||||
# Sentry
|
||||
SENTRY_AUTH_TOKEN=
|
||||
EXPO_PUBLIC_LOG_LEVEL=debug
|
||||
EXPO_PUBLIC_LOG_DEBUG=
|
||||
EXPO_PUBLIC_BUNDLE_IDENTIFIER=
|
||||
EXPO_PUBLIC_BUNDLE_DATE=0
|
||||
|
||||
+2
-15
@@ -32,6 +32,7 @@ module.exports = {
|
||||
'H6',
|
||||
'P',
|
||||
'Admonition',
|
||||
'Admonition.Admonition',
|
||||
],
|
||||
impliedTextProps: [],
|
||||
suggestedTextWrappers: {
|
||||
@@ -80,20 +81,6 @@ module.exports = {
|
||||
],
|
||||
'simple-import-sort/exports': 'error',
|
||||
'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: [
|
||||
'**/__mocks__/*.ts',
|
||||
@@ -105,9 +92,9 @@ module.exports = {
|
||||
'*.lock',
|
||||
'.husky',
|
||||
'patches',
|
||||
'bskyweb',
|
||||
'*.html',
|
||||
'bskyweb',
|
||||
'bskyembed',
|
||||
'src/locale/locales/_build/',
|
||||
'src/locale/locales/**/*.js',
|
||||
],
|
||||
|
||||
@@ -14,7 +14,7 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
name: Build and Submit iOS
|
||||
runs-on: macos-14
|
||||
runs-on: macos-15
|
||||
steps:
|
||||
- name: Check for EXPO_TOKEN
|
||||
run: >
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
|
||||
- uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: '15.3'
|
||||
xcode-version: '16.2'
|
||||
|
||||
- name: ☕️ Setup Cocoapods
|
||||
uses: maxim-lobanov/setup-cocoapods@v1
|
||||
|
||||
@@ -134,7 +134,7 @@ jobs:
|
||||
# GitHub actions are horrible so let's just copy paste this in
|
||||
buildIfNecessaryIOS:
|
||||
name: Build and Submit iOS
|
||||
runs-on: macos-14
|
||||
runs-on: macos-15
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-build-ios
|
||||
cancel-in-progress: true
|
||||
@@ -176,7 +176,7 @@ jobs:
|
||||
|
||||
- uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: '15.3'
|
||||
xcode-version: '16.2'
|
||||
|
||||
- name: ☕️ Setup Cocoapods
|
||||
uses: maxim-lobanov/setup-cocoapods@v1
|
||||
|
||||
@@ -36,3 +36,14 @@ jobs:
|
||||
with:
|
||||
commit_message: Nightly source-language update
|
||||
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
|
||||
nvm-setup: ## Use NVM to install and activate node+yarn
|
||||
nvm install 18
|
||||
nvm use 18
|
||||
nvm install 20
|
||||
nvm use 20
|
||||
npm install --global yarn
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
flows:
|
||||
- "flows/*"
|
||||
- "flows/**"
|
||||
|
||||
@@ -17,7 +17,7 @@ appId: xyz.blueskyweb.app
|
||||
- inputText: "Post with an image"
|
||||
- tapOn:
|
||||
id: "openGalleryBtn"
|
||||
- tapOn: "Content warnings"
|
||||
- tapOn: "Allow Full Access"
|
||||
- tapOn: "Content warnings"
|
||||
- tapOn: "Porn"
|
||||
- tapOn:
|
||||
|
||||
@@ -24,12 +24,10 @@ appId: xyz.blueskyweb.app
|
||||
- inputText: "example@test.com"
|
||||
- tapOn:
|
||||
id: "passwordInput"
|
||||
- inputText: "hunter2"
|
||||
- pressKey: Enter
|
||||
- inputText: "hunter22"
|
||||
- hideKeyboard
|
||||
- tapOn:
|
||||
id: "nextBtn"
|
||||
- tapOn:
|
||||
id: "handleInput"
|
||||
- inputText: "e2e-test"
|
||||
- tapOn:
|
||||
id: "nextBtn"
|
||||
|
||||
@@ -160,7 +160,7 @@ appId: xyz.blueskyweb.app
|
||||
- tapOn:
|
||||
label: "Adds and removes users on curatelists from the profile"
|
||||
id: "bottomBarSearchBtn"
|
||||
- tapOn: "Search"
|
||||
- tapOn: "Search for posts, users, or feeds"
|
||||
- inputText: "bob"
|
||||
- tapOn:
|
||||
id: "searchAutoCompleteResult-bob.test"
|
||||
|
||||
@@ -16,7 +16,7 @@ appId: xyz.blueskyweb.app
|
||||
- tapOn:
|
||||
id: "profileCardButton"
|
||||
- tapOn:
|
||||
id: "profilePager-selector-4"
|
||||
id: "profilePager-selector-5"
|
||||
- tapOn: "alice-favs"
|
||||
- tapOn: "Pin to Home"
|
||||
- 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"
|
||||
- tapOn:
|
||||
id: "bottomBarSearchBtn"
|
||||
- tapOn: "Search"
|
||||
- tapOn: "Search for posts, users, or feeds"
|
||||
- inputText: "b"
|
||||
- tapOn:
|
||||
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
|
||||
- tapOn:
|
||||
id: "bottomBarSearchBtn"
|
||||
- assertVisible: "Search"
|
||||
- tapOn: "Search"
|
||||
- assertVisible: "Search for posts, users, or feeds"
|
||||
- tapOn: "Search for posts, users, or feeds"
|
||||
- inputText: "b"
|
||||
- tapOn:
|
||||
id: "searchAutoCompleteResult-bob.test"
|
||||
|
||||
@@ -75,14 +75,17 @@ module.exports = function (config) {
|
||||
'an',
|
||||
'ast',
|
||||
'ca',
|
||||
'cy',
|
||||
'da',
|
||||
'de',
|
||||
'el',
|
||||
'eo',
|
||||
'es',
|
||||
'eu',
|
||||
'fi',
|
||||
'fr',
|
||||
'ga',
|
||||
'gd',
|
||||
'gl',
|
||||
'hi',
|
||||
'hu',
|
||||
@@ -237,11 +240,13 @@ module.exports = function (config) {
|
||||
},
|
||||
],
|
||||
'./plugins/starterPackAppClipExtension/withStarterPackAppClip.js',
|
||||
'./plugins/withGradleJVMHeapSizeIncrease.js',
|
||||
'./plugins/withAndroidManifestPlugin.js',
|
||||
'./plugins/withAndroidManifestFCMIconPlugin.js',
|
||||
'./plugins/withAndroidStylesAccentColorPlugin.js',
|
||||
'./plugins/withAndroidSplashScreenStatusBarTranslucentPlugin.js',
|
||||
'./plugins/withAndroidNoJitpackPlugin.js',
|
||||
'./plugins/withNoBundleCompression.js',
|
||||
'./plugins/shareExtension/withShareExtensions.js',
|
||||
'./plugins/notificationsExtension/withNotificationsExtension.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="M11.293 4.293a1 1 0 0 1 1.414 0l4 4a1 1 0 0 1-1.414 1.414L12 6.414 8.707 9.707a1 1 0 0 1-1.414-1.414l4-4Zm-4 10a1 1 0 0 1 1.414 0L12 17.586l3.293-3.293a1 1 0 0 1 1.414 1.414l-4 4a1 1 0 0 1-1.414 0l-4-4a1 1 0 0 1 0-1.414Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 363 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12 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')) {
|
||||
searchParams.set('ref_url', encodeURIComponent(ref_url))
|
||||
}
|
||||
searchParams.set('colorMode', embed.dataset.blueskyColorMode || 'system')
|
||||
|
||||
const iframe = document.createElement('iframe')
|
||||
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') {
|
||||
document.documentElement.classList.remove('light', 'dark')
|
||||
document.documentElement.classList.add(theme)
|
||||
}
|
||||
|
||||
export function initColorMode() {
|
||||
export function initSystemColorMode() {
|
||||
applyTheme(
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
? 'dark'
|
||||
|
||||
@@ -9,3 +9,14 @@
|
||||
:root {
|
||||
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'
|
||||
case 'nudity':
|
||||
return 'Non-sexual Nudity'
|
||||
case 'gore':
|
||||
case 'graphic-media':
|
||||
return 'Graphic Media'
|
||||
default:
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
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 {useEffect, useMemo, useRef, useState} from 'preact/hooks'
|
||||
|
||||
import arrowBottom from '../../assets/arrowBottom_stroke2_corner0_rounded.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 {Link} from '../components/link'
|
||||
import {Post} from '../components/post'
|
||||
@@ -22,9 +26,9 @@ export const EMBED_SCRIPT = `${EMBED_SERVICE}/static/embed.js`
|
||||
const root = document.getElementById('app')
|
||||
if (!root) throw new Error('No root element')
|
||||
|
||||
initColorMode()
|
||||
initSystemColorMode()
|
||||
|
||||
const agent = new BskyAgent({
|
||||
const agent = new AtpAgent({
|
||||
service: 'https://public.api.bsky.app',
|
||||
})
|
||||
|
||||
@@ -32,6 +36,7 @@ render(<LandingPage />, root)
|
||||
|
||||
function LandingPage() {
|
||||
const [uri, setUri] = useState('')
|
||||
const [colorMode, setColorMode] = useState<ColorModeValues>('system')
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
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>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
value={uri}
|
||||
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"
|
||||
placeholder={DEFAULT_POST}
|
||||
/>
|
||||
<div className="flex flex-col w-full max-w-[600px] gap-6">
|
||||
<input
|
||||
type="text"
|
||||
value={uri}
|
||||
onInput={e => setUri(e.currentTarget.value)}
|
||||
className="border rounded-lg py-3 px-4 dark:bg-dimmedBg dark:border-slate-500"
|
||||
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" />
|
||||
|
||||
{loading ? (
|
||||
<div className="w-full max-w-[600px]">
|
||||
<div className={`${colorMode} w-full max-w-[600px]`}>
|
||||
<Skeleton />
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full max-w-[600px] gap-8 flex flex-col">
|
||||
{!error && thread && uri && <Snippet thread={thread} />}
|
||||
{!error && thread && <Post thread={thread} key={thread.post.uri} />}
|
||||
{!error && thread && uri && (
|
||||
<Snippet thread={thread} colorMode={colorMode} />
|
||||
)}
|
||||
<div className={colorMode}>
|
||||
{!error && thread && <Post thread={thread} key={thread.post.uri} />}
|
||||
</div>
|
||||
{error && (
|
||||
<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>
|
||||
@@ -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 [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
|
||||
return `<blockquote class="bluesky-embed" data-bluesky-uri="${escapeHtml(
|
||||
thread.post.uri,
|
||||
)}" data-bluesky-cid="${escapeHtml(thread.post.cid)}"><p lang="${escapeHtml(
|
||||
lang,
|
||||
)}">${escapeHtml(record.text)}${
|
||||
)}" data-bluesky-cid="${escapeHtml(
|
||||
thread.post.cid,
|
||||
)}" data-bluesky-embed-color-mode="${escapeHtml(
|
||||
colorMode,
|
||||
)}"><p lang="${escapeHtml(lang)}">${escapeHtml(record.text)}${
|
||||
record.embed
|
||||
? `<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(
|
||||
niceDate(thread.post.indexedAt),
|
||||
)}</a></blockquote><script async src="${EMBED_SCRIPT}" charset="utf-8"></script>`
|
||||
}, [thread])
|
||||
}, [thread, colorMode])
|
||||
|
||||
return (
|
||||
<div className="flex gap-2 w-full">
|
||||
|
||||
@@ -4,7 +4,7 @@ import {AppBskyFeedDefs, AtpAgent} from '@atproto/api'
|
||||
import {h, render} from 'preact'
|
||||
|
||||
import logo from '../../assets/logo.svg'
|
||||
import {initColorMode} from '../color-mode'
|
||||
import {applyTheme, initSystemColorMode} from '../color-mode'
|
||||
import {Container} from '../components/container'
|
||||
import {Link} from '../components/link'
|
||||
import {Post} from '../components/post'
|
||||
@@ -22,7 +22,23 @@ if (!uri) {
|
||||
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
|
||||
.getPostThread({
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import {Express} from 'express'
|
||||
|
||||
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 redirect} from './redirect.js'
|
||||
import {default as shortLink} from './shortLink.js'
|
||||
import {default as siteAssociation} from './siteAssociation.js'
|
||||
|
||||
export * from './util.js'
|
||||
@@ -11,7 +12,8 @@ export * from './util.js'
|
||||
export default function (ctx: AppContext, app: Express) {
|
||||
app = health(ctx, app) // GET /_health
|
||||
app = siteAssociation(ctx, app) // GET /.well-known/apple-app-site-association
|
||||
app = create(ctx, app) // POST /link
|
||||
app = redirect(ctx, app) // GET /:linkId (should go last due to permissive matching)
|
||||
app = redirect(ctx, app) // GET /redirect
|
||||
app = createShortLink(ctx, app) // POST /link
|
||||
app = shortLink(ctx, app) // GET /:linkId (should go last due to permissive matching)
|
||||
return app
|
||||
}
|
||||
|
||||
@@ -6,47 +6,40 @@ import {Express} from 'express'
|
||||
import {AppContext} from '../context.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) {
|
||||
return app.get(
|
||||
'/:linkId',
|
||||
'/redirect',
|
||||
handler(async (req, res) => {
|
||||
const linkId = req.params.linkId
|
||||
const contentType = req.accepts(['html', 'json'])
|
||||
let link = req.query.u
|
||||
assert(
|
||||
typeof linkId === 'string',
|
||||
'express guarantees id parameter is a string',
|
||||
typeof link === 'string',
|
||||
'express guarantees link query parameter is a string',
|
||||
)
|
||||
const found = await ctx.db.db
|
||||
.selectFrom('link')
|
||||
.selectAll()
|
||||
.where('id', '=', linkId)
|
||||
.executeTakeFirst()
|
||||
if (!found) {
|
||||
// potentially broken or mistyped link
|
||||
link = decodeURIComponent(link)
|
||||
|
||||
let url: URL | undefined
|
||||
try {
|
||||
url = new URL(link)
|
||||
} catch {}
|
||||
|
||||
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')
|
||||
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()
|
||||
}),
|
||||
|
||||
@@ -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',
|
||||
'sexual-figurative',
|
||||
'graphic-media',
|
||||
'gore',
|
||||
'self-harm',
|
||||
'sensitive',
|
||||
'security',
|
||||
|
||||
@@ -94,6 +94,20 @@ func run(args []string) {
|
||||
Value: "",
|
||||
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")
|
||||
staticCDNHost := cctx.String("static-cdn-host")
|
||||
staticCDNHost = strings.TrimSuffix(staticCDNHost, "/")
|
||||
canonicalInstance := cctx.Bool("bsky-canonical-instance")
|
||||
robotsDisallowAll := cctx.Bool("robots-disallow-all")
|
||||
|
||||
// Echo
|
||||
e := echo.New()
|
||||
@@ -204,13 +206,23 @@ func serve(cctx *cli.Context) error {
|
||||
return http.FS(fsys)
|
||||
}())
|
||||
|
||||
e.GET("/robots.txt", echo.WrapHandler(staticHandler))
|
||||
e.GET("/ips-v4", 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 {
|
||||
return c.Redirect(http.StatusMovedPermanently, "/.well-known/security.txt")
|
||||
})
|
||||
// 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-v6", echo.WrapHandler(staticHandler))
|
||||
e.GET("/security.txt", func(c echo.Context) error {
|
||||
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("/static/*", echo.WrapHandler(http.StripPrefix("/static/", staticHandler)), func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
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/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/feed/:rkey", 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": ".",
|
||||
"preserve_hierarchy": true,
|
||||
"files": [
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# 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
|
||||
|
||||
- We use Lingui to implement translations. You can find the documentation [here](https://lingui.dev/).
|
||||
- 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/
|
||||
- We use Lingui to implement translations. You can find the documentation [here](https://lingui.dev/).
|
||||
|
||||
## Translators
|
||||
|
||||
@@ -15,7 +15,7 @@ Much of the app is translated by community contributions. (We <3 our translators
|
||||
|
||||
### 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.
|
||||
- **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
|
||||
|
||||
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.
|
||||
|
||||
@@ -47,7 +47,8 @@ Every night, a GitHub action will run `yarn intl:extract` to update the english
|
||||
### Release process
|
||||
|
||||
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. If needed:
|
||||
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', () => ({}))
|
||||
|
||||
@@ -5,15 +5,18 @@ module.exports = {
|
||||
'an',
|
||||
'ast',
|
||||
'ca',
|
||||
'cy',
|
||||
'da',
|
||||
'de',
|
||||
'el',
|
||||
'en-GB',
|
||||
'eo',
|
||||
'es',
|
||||
'eu',
|
||||
'fi',
|
||||
'fr',
|
||||
'ga',
|
||||
'gd',
|
||||
'gl',
|
||||
'hi',
|
||||
'hu',
|
||||
|
||||
+28
-11
@@ -31,9 +31,13 @@ class BottomSheetView(
|
||||
private lateinit var dialogRootViewGroup: DialogRootViewGroup
|
||||
private var eventDispatcher: EventDispatcher? = null
|
||||
|
||||
private val screenHeight =
|
||||
context.resources.displayMetrics.heightPixels
|
||||
.toFloat()
|
||||
private val rawScreenHeight = context.resources.displayMetrics.heightPixels.toFloat()
|
||||
private val safeScreenHeight = (rawScreenHeight - getNavigationBarHeight()).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 onSnapPointChange by EventDispatcher()
|
||||
@@ -63,12 +67,12 @@ class BottomSheetView(
|
||||
}
|
||||
}
|
||||
|
||||
var maxHeight = this.screenHeight
|
||||
var maxHeight = this.safeScreenHeight
|
||||
set(value) {
|
||||
val px = dpToPx(value)
|
||||
field =
|
||||
if (px > this.screenHeight) {
|
||||
this.screenHeight
|
||||
if (px > this.safeScreenHeight) {
|
||||
this.safeScreenHeight
|
||||
} else {
|
||||
px
|
||||
}
|
||||
@@ -153,6 +157,19 @@ class BottomSheetView(
|
||||
|
||||
// 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() {
|
||||
if (this.isOpen || this.isOpening || this.isClosing) return
|
||||
|
||||
@@ -172,12 +189,12 @@ class BottomSheetView(
|
||||
val behavior = BottomSheetBehavior.from(it)
|
||||
behavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
behavior.isFitToContents = true
|
||||
behavior.halfExpandedRatio = this.clampRatio(this.getTargetHeight() / this.screenHeight)
|
||||
behavior.halfExpandedRatio = getHalfExpandedRatio(contentHeight)
|
||||
behavior.skipCollapsed = true
|
||||
behavior.isDraggable = 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
|
||||
this.selectedSnapPoint = 2
|
||||
} else {
|
||||
@@ -227,11 +244,11 @@ class BottomSheetView(
|
||||
bottomSheet?.let {
|
||||
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
|
||||
} 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import * as React from 'react'
|
||||
import {
|
||||
Dimensions,
|
||||
LayoutChangeEvent,
|
||||
NativeSyntheticEvent,
|
||||
Platform,
|
||||
StyleProp,
|
||||
View,
|
||||
ViewStyle,
|
||||
} from 'react-native'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {requireNativeModule, requireNativeViewManager} from 'expo-modules-core'
|
||||
|
||||
import {isIOS} from '#/platform/detection'
|
||||
import {BottomSheetState, BottomSheetViewProps} from './BottomSheet.types'
|
||||
import {BottomSheetPortalProvider} from './BottomSheetPortal'
|
||||
import {Context as PortalContext} from './BottomSheetPortal'
|
||||
@@ -81,12 +84,10 @@ export class BottomSheetNativeComponent extends React.Component<
|
||||
return null
|
||||
}
|
||||
|
||||
const {children, backgroundColor, ...rest} = this.props
|
||||
const cornerRadius = rest.cornerRadius ?? 0
|
||||
|
||||
let extraStyles
|
||||
if (isIOS15 && this.state.viewHeight) {
|
||||
const {viewHeight} = this.state
|
||||
const cornerRadius = this.props.cornerRadius ?? 0
|
||||
if (viewHeight < screenHeight / 2) {
|
||||
extraStyles = {
|
||||
height: viewHeight,
|
||||
@@ -99,39 +100,70 @@ export class BottomSheetNativeComponent extends React.Component<
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<NativeView
|
||||
{...rest}
|
||||
<BottomSheetNativeComponentInner
|
||||
{...this.props}
|
||||
nativeViewRef={this.ref}
|
||||
onStateChange={this.onStateChange}
|
||||
ref={this.ref}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
height: screenHeight,
|
||||
width: '100%',
|
||||
extraStyles={extraStyles}
|
||||
onLayout={e => {
|
||||
const {height} = e.nativeEvent.layout
|
||||
this.setState({viewHeight: height})
|
||||
this.updateLayout()
|
||||
}}
|
||||
containerBackgroundColor={backgroundColor}>
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
flex: 1,
|
||||
backgroundColor,
|
||||
},
|
||||
Platform.OS === 'android' && {
|
||||
borderTopLeftRadius: cornerRadius,
|
||||
borderTopRightRadius: cornerRadius,
|
||||
},
|
||||
extraStyles,
|
||||
]}>
|
||||
<View
|
||||
onLayout={e => {
|
||||
const {height} = e.nativeEvent.layout
|
||||
this.setState({viewHeight: height})
|
||||
this.updateLayout()
|
||||
}}>
|
||||
<BottomSheetPortalProvider>{children}</BottomSheetPortalProvider>
|
||||
</View>
|
||||
</View>
|
||||
</NativeView>
|
||||
/>
|
||||
</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
|
||||
{...rest}
|
||||
onStateChange={onStateChange}
|
||||
ref={nativeViewRef}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
height: sheetHeight,
|
||||
width: '100%',
|
||||
}}
|
||||
containerBackgroundColor={backgroundColor}>
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
flex: 1,
|
||||
backgroundColor,
|
||||
},
|
||||
Platform.OS === 'android' && {
|
||||
borderTopLeftRadius: cornerRadius,
|
||||
borderTopRightRadius: cornerRadius,
|
||||
},
|
||||
extraStyles,
|
||||
]}>
|
||||
<View onLayout={onLayout}>
|
||||
<BottomSheetPortalProvider>{children}</BottomSheetPortalProvider>
|
||||
</View>
|
||||
</View>
|
||||
</NativeView>
|
||||
)
|
||||
}
|
||||
|
||||
+9
-9
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bsky.app",
|
||||
"version": "1.98.0",
|
||||
"version": "1.99.1",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"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:results": "NODE_ENV=test flashlight report .perf/results.json",
|
||||
"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:all": "lingui extract --clean",
|
||||
"intl:compile": "lingui compile",
|
||||
"intl:pull": "crowdin download translations --verbose -b main",
|
||||
"intl:push": "crowdin push translations --verbose -b main",
|
||||
@@ -56,8 +57,8 @@
|
||||
"icons:optimize": "svgo -f ./assets/icons"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "^0.13.35",
|
||||
"@bitdrift/react-native": "^0.6.2",
|
||||
"@atproto/api": "^0.14.7",
|
||||
"@bitdrift/react-native": "^0.6.8",
|
||||
"@braintree/sanitize-url": "^6.0.2",
|
||||
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
@@ -78,10 +79,10 @@
|
||||
"@mattermost/react-native-paste-input": "^0.7.1",
|
||||
"@miblanchard/react-native-slider": "^2.3.1",
|
||||
"@mozzius/expo-dynamic-app-icon": "^1.5.0",
|
||||
"@radix-ui/react-dismissable-layer": "^1.1.1",
|
||||
"@radix-ui/react-dropdown-menu": "2.0.1",
|
||||
"@radix-ui/react-dismissable-layer": "^1.1.5",
|
||||
"@radix-ui/react-dropdown-menu": "2.1.6",
|
||||
"@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-menu/menu": "^1.1.7",
|
||||
"@react-native-picker/picker": "2.10.2",
|
||||
@@ -211,7 +212,7 @@
|
||||
"zod": "^3.20.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@atproto/dev-env": "^0.3.67",
|
||||
"@atproto/dev-env": "^0.3.87",
|
||||
"@babel/core": "^7.26.0",
|
||||
"@babel/preset-env": "^7.26.0",
|
||||
"@babel/runtime": "^7.26.0",
|
||||
@@ -270,7 +271,6 @@
|
||||
"@expo/image-utils": "0.6.3",
|
||||
"@react-native/babel-preset": "0.76.1",
|
||||
"@react-native/normalize-colors": "0.76.1",
|
||||
"@radix-ui/react-focus-scope": "1.1.0",
|
||||
"@types/react": "^18",
|
||||
"**/expo-constants": "17.0.3",
|
||||
"**/expo-device": "7.0.1",
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
const {withGradleProperties} = require('expo/config-plugins')
|
||||
|
||||
function setGradlePropertiesValue(config, key, value) {
|
||||
return withGradleProperties(config, exportedConfig => {
|
||||
const keyIdx = exportedConfig.modResults.findIndex(
|
||||
item => item.type === 'property' && item.key === key,
|
||||
)
|
||||
if (keyIdx >= 0) {
|
||||
exportedConfig.modResults.splice(keyIdx, 1, {
|
||||
type: 'property',
|
||||
key,
|
||||
value,
|
||||
})
|
||||
} else {
|
||||
exportedConfig.modResults.push({
|
||||
type: 'property',
|
||||
key,
|
||||
value,
|
||||
})
|
||||
}
|
||||
|
||||
return exportedConfig
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = function withGradleJVMHeapSizeIncrease(config) {
|
||||
config = setGradlePropertiesValue(
|
||||
config,
|
||||
'org.gradle.jvmargs',
|
||||
'-Xmx4096m -XX:MaxMetaspaceSize=1024m', //Set data of your choice
|
||||
)
|
||||
|
||||
return config
|
||||
}
|
||||
@@ -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
-6
@@ -1,6 +1,6 @@
|
||||
import 'react-native-url-polyfill/auto'
|
||||
import '#/lib/sentry' // must be near top
|
||||
import '#/lib/bitdrift' // must be near top
|
||||
import '#/logger/sentry/setup'
|
||||
import '#/logger/bitdrift/setup'
|
||||
import '#/view/icons'
|
||||
|
||||
import React, {useEffect, useState} from 'react'
|
||||
@@ -69,13 +69,10 @@ import {NuxDialogs} from '#/components/dialogs/nuxs'
|
||||
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
|
||||
import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs'
|
||||
import {Provider as PortalProvider} from '#/components/Portal'
|
||||
import * as env from '#/env'
|
||||
import {Splash} from '#/Splash'
|
||||
import {BottomSheetProvider} from '../modules/bottom-sheet'
|
||||
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
||||
|
||||
logger.info('env', env)
|
||||
|
||||
SplashScreen.preventAutoHideAsync()
|
||||
if (isIOS) {
|
||||
SystemUI.setBackgroundColorAsync('black')
|
||||
@@ -121,7 +118,7 @@ function InnerApp() {
|
||||
useEffect(() => {
|
||||
return listenSessionDropped(() => {
|
||||
Toast.show(
|
||||
_(msg`Sorry! Your session expired. Please log in again.`),
|
||||
_(msg`Sorry! Your session expired. Please sign in again.`),
|
||||
'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 './style.css'
|
||||
|
||||
@@ -94,7 +94,7 @@ function InnerApp() {
|
||||
useEffect(() => {
|
||||
return listenSessionDropped(() => {
|
||||
Toast.show(
|
||||
_(msg`Sorry! Your session expired. Please log in again.`),
|
||||
_(msg`Sorry! Your session expired. Please sign in again.`),
|
||||
'info',
|
||||
)
|
||||
})
|
||||
|
||||
+20
-4
@@ -32,6 +32,7 @@ import {
|
||||
import {RouteParams, State} from '#/lib/routes/types'
|
||||
import {attachRouteToLogEvents, logEvent} from '#/lib/statsig/statsig'
|
||||
import {bskyTitle} from '#/lib/strings/headings'
|
||||
import {logger} from '#/logger'
|
||||
import {isNative, isWeb} from '#/platform/detection'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {useUnreadNotifications} from '#/state/queries/notifications/unread'
|
||||
@@ -68,6 +69,7 @@ import {SharedPreferencesTesterScreen} from '#/screens/E2E/SharedPreferencesTest
|
||||
import HashtagScreen from '#/screens/Hashtag'
|
||||
import {MessagesScreen} from '#/screens/Messages/ChatList'
|
||||
import {MessagesConversationScreen} from '#/screens/Messages/Conversation'
|
||||
import {MessagesInboxScreen} from '#/screens/Messages/Inbox'
|
||||
import {MessagesSettingsScreen} from '#/screens/Messages/Settings'
|
||||
import {ModerationScreen} from '#/screens/Moderation'
|
||||
import {Screen as ModerationInteractionSettings} from '#/screens/ModerationInteractionSettings'
|
||||
@@ -91,6 +93,7 @@ import {VideoFeed} from '#/screens/VideoFeed'
|
||||
import {useTheme} from '#/alf'
|
||||
import {router} from '#/routes'
|
||||
import {Referrer} from '../modules/expo-bluesky-swiss-army'
|
||||
import {ProfileSearchScreen} from './screens/Profile/ProfileSearch'
|
||||
import {AboutSettingsScreen} from './screens/Settings/AboutSettings'
|
||||
import {AccessibilitySettingsScreen} from './screens/Settings/AccessibilitySettings'
|
||||
import {AccountSettingsScreen} from './screens/Settings/AccountSettings'
|
||||
@@ -207,6 +210,13 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
|
||||
getComponent={() => ProfileListScreen}
|
||||
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
|
||||
name="PostThread"
|
||||
getComponent={() => PostThreadScreen}
|
||||
@@ -402,6 +412,11 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
|
||||
getComponent={() => MessagesSettingsScreen}
|
||||
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
|
||||
name="NotificationSettings"
|
||||
getComponent={() => NotificationSettingsScreen}
|
||||
@@ -721,15 +736,16 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
|
||||
linking={LINKING}
|
||||
theme={theme}
|
||||
onStateChange={() => {
|
||||
const routeName = getCurrentRouteName()
|
||||
if (routeName === 'Notifications') {
|
||||
logEvent('router:navigate:notifications', {})
|
||||
}
|
||||
logger.metric('router:navigate', {
|
||||
from: prevLoggedRouteName.current,
|
||||
})
|
||||
prevLoggedRouteName.current = getCurrentRouteName()
|
||||
}}
|
||||
onReady={() => {
|
||||
attachRouteToLogEvents(getCurrentRouteName)
|
||||
logModuleInitTime()
|
||||
onReady()
|
||||
logger.metric('router:navigate', {})
|
||||
}}>
|
||||
{children}
|
||||
</NavigationContainer>
|
||||
|
||||
+28
-1
@@ -31,6 +31,18 @@ export const atoms = {
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
top_0: {
|
||||
top: 0,
|
||||
},
|
||||
right_0: {
|
||||
right: 0,
|
||||
},
|
||||
bottom_0: {
|
||||
bottom: 0,
|
||||
},
|
||||
left_0: {
|
||||
left: 0,
|
||||
},
|
||||
z_10: {
|
||||
zIndex: 10,
|
||||
},
|
||||
@@ -50,9 +62,15 @@ export const atoms = {
|
||||
overflow_hidden: {
|
||||
overflow: 'hidden',
|
||||
},
|
||||
/**
|
||||
* @platform web
|
||||
*/
|
||||
overflow_auto: web({
|
||||
overflow: 'auto',
|
||||
}),
|
||||
|
||||
/*
|
||||
* Width
|
||||
* Width & Height
|
||||
*/
|
||||
w_full: {
|
||||
width: '100%',
|
||||
@@ -63,6 +81,12 @@ export const atoms = {
|
||||
h_full_vh: web({
|
||||
height: '100vh',
|
||||
}),
|
||||
max_w_full: {
|
||||
maxWidth: '100%',
|
||||
},
|
||||
max_h_full: {
|
||||
maxHeight: '100%',
|
||||
},
|
||||
|
||||
/**
|
||||
* Used for the outermost components on screens, to ensure that they can fill
|
||||
@@ -87,6 +111,9 @@ export const atoms = {
|
||||
/*
|
||||
* Border radius
|
||||
*/
|
||||
rounded_0: {
|
||||
borderRadius: 0,
|
||||
},
|
||||
rounded_2xs: {
|
||||
borderRadius: tokens.borderRadius._2xs,
|
||||
},
|
||||
|
||||
@@ -26,3 +26,18 @@ export function useBreakpoints(): Record<Breakpoint, boolean> & {
|
||||
}
|
||||
}, [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"
|
||||
style={[a.flex_1]}
|
||||
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}) => (
|
||||
<View
|
||||
style={[
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react'
|
||||
import {StyleProp, View, ViewStyle} from 'react-native'
|
||||
|
||||
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 {Eye_Stroke2_Corner0_Rounded as InfoIcon} from '#/components/icons/Eye'
|
||||
import {Leaf_Stroke2_Corner0_Rounded as TipIcon} from '#/components/icons/Leaf'
|
||||
@@ -49,22 +50,29 @@ export function Text({
|
||||
return (
|
||||
<BaseText
|
||||
{...rest}
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.text_sm,
|
||||
a.leading_snug,
|
||||
{
|
||||
paddingTop: 1,
|
||||
},
|
||||
style,
|
||||
]}>
|
||||
style={[a.flex_1, a.text_sm, a.leading_snug, a.pr_md, style]}>
|
||||
{children}
|
||||
</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}) {
|
||||
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({
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
import {View} from 'react-native'
|
||||
import {moderateProfile} from '@atproto/api'
|
||||
|
||||
import {logger} from '#/logger'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useProfilesQuery} from '#/state/queries/profile'
|
||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import * as bsky from '#/types/bsky'
|
||||
|
||||
export function AvatarStack({
|
||||
profiles,
|
||||
size = 26,
|
||||
numPending,
|
||||
backgroundColor,
|
||||
}: {
|
||||
profiles: string[]
|
||||
profiles: bsky.profile.AnyProfileView[]
|
||||
size?: number
|
||||
numPending?: number
|
||||
backgroundColor?: string
|
||||
}) {
|
||||
const halfSize = size / 2
|
||||
const {data, error} = useProfilesQuery({handles: profiles})
|
||||
const t = useTheme()
|
||||
const moderationOpts = useModerationOpts()
|
||||
|
||||
if (error) {
|
||||
console.error(error)
|
||||
return null
|
||||
}
|
||||
|
||||
const isPending = !data || !moderationOpts
|
||||
const isPending = (numPending && profiles.length === 0) || !moderationOpts
|
||||
|
||||
const items = isPending
|
||||
? Array.from({length: profiles.length}).map((_, i) => ({
|
||||
? Array.from({length: numPending ?? profiles.length}).map((_, i) => ({
|
||||
key: i,
|
||||
profile: null,
|
||||
moderation: null,
|
||||
}))
|
||||
: data.profiles.map(item => ({
|
||||
: profiles.map(item => ({
|
||||
key: item.did,
|
||||
profile: item,
|
||||
moderation: moderateProfile(item, moderationOpts),
|
||||
@@ -56,7 +56,7 @@ export function AvatarStack({
|
||||
height: size,
|
||||
left: i * -halfSize,
|
||||
borderWidth: 1,
|
||||
borderColor: t.atoms.bg.backgroundColor,
|
||||
borderColor: backgroundColor ?? t.atoms.bg.backgroundColor,
|
||||
borderRadius: 999,
|
||||
zIndex: 3 - i,
|
||||
},
|
||||
@@ -74,3 +74,33 @@ export function AvatarStack({
|
||||
</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,
|
||||
disableDrag: false,
|
||||
setDisableDrag: () => {},
|
||||
isWithinDialog: false,
|
||||
})
|
||||
|
||||
export function useDialogContext() {
|
||||
|
||||
@@ -26,7 +26,7 @@ import {isAndroid, isIOS} from '#/platform/detection'
|
||||
import {useA11y} from '#/state/a11y'
|
||||
import {useDialogStateControlContext} from '#/state/dialogs'
|
||||
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 {Context, useDialogContext} from '#/components/Dialog/context'
|
||||
import {
|
||||
@@ -46,7 +46,7 @@ export {useDialogContext, useDialogControl} from '#/components/Dialog/context'
|
||||
export * from '#/components/Dialog/shared'
|
||||
export * from '#/components/Dialog/types'
|
||||
export * from '#/components/Dialog/utils'
|
||||
// @ts-ignore
|
||||
|
||||
export const Input = createInput(TextInput)
|
||||
|
||||
export function Outer({
|
||||
@@ -154,6 +154,7 @@ export function Outer({
|
||||
nativeSnapPoint: snapPoint,
|
||||
disableDrag,
|
||||
setDisableDrag,
|
||||
isWithinDialog: true,
|
||||
}),
|
||||
[close, snapPoint, disableDrag, setDisableDrag],
|
||||
)
|
||||
@@ -168,7 +169,9 @@ export function Outer({
|
||||
onStateChange={onStateChange}
|
||||
disableDrag={disableDrag}>
|
||||
<Context.Provider value={context}>
|
||||
<View testID={testID}>{children}</View>
|
||||
<View testID={testID} style={[a.relative]}>
|
||||
{children}
|
||||
</View>
|
||||
</Context.Provider>
|
||||
</BottomSheet>
|
||||
)
|
||||
@@ -196,7 +199,7 @@ export function Inner({children, style, header}: DialogInnerProps) {
|
||||
|
||||
export const ScrollableInner = React.forwardRef<ScrollView, DialogInnerProps>(
|
||||
function ScrollableInner(
|
||||
{children, style, contentContainerStyle, header, ...props},
|
||||
{children, contentContainerStyle, header, ...props},
|
||||
ref,
|
||||
) {
|
||||
const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext()
|
||||
@@ -216,13 +219,21 @@ export const ScrollableInner = React.forwardRef<ScrollView, DialogInnerProps>(
|
||||
[],
|
||||
)
|
||||
|
||||
const basePading =
|
||||
(isIOS ? 30 : 50) + (isIOS ? keyboardHeight / 4 : keyboardHeight)
|
||||
const fullPaddingBase = insets.bottom + insets.top + basePading
|
||||
const fullPadding = isIOS ? fullPaddingBase : fullPaddingBase + 50
|
||||
|
||||
const paddingBottom =
|
||||
nativeSnapPoint === BottomSheetSnapPoint.Full ? fullPadding : basePading
|
||||
let paddingBottom = 0
|
||||
if (isIOS) {
|
||||
paddingBottom += keyboardHeight / 4
|
||||
if (nativeSnapPoint === BottomSheetSnapPoint.Full) {
|
||||
paddingBottom += insets.bottom + tokens.space.md
|
||||
}
|
||||
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>) => {
|
||||
if (!isAndroid) {
|
||||
@@ -238,7 +249,6 @@ export const ScrollableInner = React.forwardRef<ScrollView, DialogInnerProps>(
|
||||
|
||||
return (
|
||||
<KeyboardAwareScrollView
|
||||
style={[style]}
|
||||
contentContainerStyle={[
|
||||
a.pt_2xl,
|
||||
a.px_xl,
|
||||
@@ -316,7 +326,7 @@ export function Handle() {
|
||||
style={[
|
||||
a.rounded_sm,
|
||||
{
|
||||
top: 10,
|
||||
top: tokens.space._2xl / 2 - 2.5,
|
||||
width: 35,
|
||||
height: 5,
|
||||
alignSelf: 'center',
|
||||
|
||||
@@ -41,6 +41,7 @@ export function Outer({
|
||||
children,
|
||||
control,
|
||||
onClose,
|
||||
webOptions,
|
||||
}: React.PropsWithChildren<DialogOuterProps>) {
|
||||
const {_} = useLingui()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
@@ -96,6 +97,7 @@ export function Outer({
|
||||
nativeSnapPoint: 0,
|
||||
disableDrag: false,
|
||||
setDisableDrag: () => {},
|
||||
isWithinDialog: true,
|
||||
}),
|
||||
[close],
|
||||
)
|
||||
@@ -115,20 +117,26 @@ export function Outer({
|
||||
web(a.fixed),
|
||||
a.inset_0,
|
||||
a.z_10,
|
||||
a.px_xl,
|
||||
webOptions?.alignCenter ? a.justify_center : undefined,
|
||||
a.align_center,
|
||||
gtMobile ? a.p_lg : a.p_md,
|
||||
{overflowY: 'auto'},
|
||||
{
|
||||
overflowY: 'auto',
|
||||
paddingVertical: gtMobile ? '10vh' : a.pt_xl.paddingTop,
|
||||
},
|
||||
]}>
|
||||
<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
|
||||
style={[
|
||||
a.w_full,
|
||||
a.z_20,
|
||||
a.justify_center,
|
||||
a.align_center,
|
||||
{
|
||||
minHeight: web('calc(90vh - 36px)') || undefined,
|
||||
},
|
||||
web({minHeight: '60vh', position: 'static'}),
|
||||
]}>
|
||||
{children}
|
||||
</View>
|
||||
@@ -217,8 +225,8 @@ export const InnerFlatList = React.forwardRef<
|
||||
style={[
|
||||
a.overflow_hidden,
|
||||
a.px_0,
|
||||
// @ts-expect-error web only -sfn
|
||||
{maxHeight: 'calc(-36px + 100vh)'},
|
||||
// 100 minus 10vh of paddingVertical
|
||||
web({maxHeight: '80vh'}),
|
||||
webInnerStyle,
|
||||
]}
|
||||
contentContainerStyle={[a.px_0, webInnerContentContainerStyle]}>
|
||||
|
||||
@@ -44,6 +44,8 @@ export type DialogContextProps = {
|
||||
nativeSnapPoint: BottomSheetSnapPoint
|
||||
disableDrag: boolean
|
||||
setDisableDrag: React.Dispatch<React.SetStateAction<boolean>>
|
||||
// in the event that the hook is used outside of a dialog
|
||||
isWithinDialog: boolean
|
||||
}
|
||||
|
||||
export type DialogControlOpenOptions = {
|
||||
@@ -60,11 +62,16 @@ export type DialogOuterProps = {
|
||||
control: DialogControlProps
|
||||
onClose?: () => void
|
||||
nativeOptions?: Omit<BottomSheetViewProps, 'children'>
|
||||
webOptions?: {}
|
||||
webOptions?: {
|
||||
alignCenter?: boolean
|
||||
}
|
||||
testID?: string
|
||||
}
|
||||
|
||||
type DialogInnerPropsBase<T> = React.PropsWithChildren<ViewStyleProp> & T
|
||||
type DialogInnerPropsBase<T> = React.PropsWithChildren<ViewStyleProp> &
|
||||
T & {
|
||||
testID?: string
|
||||
}
|
||||
export type DialogInnerProps =
|
||||
| DialogInnerPropsBase<{
|
||||
label?: undefined
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react'
|
||||
import {GestureResponderEvent, View} from 'react-native'
|
||||
import {
|
||||
AppBskyActorDefs,
|
||||
AppBskyFeedDefs,
|
||||
AppBskyGraphDefs,
|
||||
AtUri,
|
||||
@@ -32,6 +31,7 @@ import {Loader} from '#/components/Loader'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {RichText, RichTextProps} from '#/components/RichText'
|
||||
import {Text} from '#/components/Typography'
|
||||
import * as bsky from '#/types/bsky'
|
||||
|
||||
type Props = {
|
||||
view: AppBskyFeedDefs.GeneratorView
|
||||
@@ -115,7 +115,7 @@ export function TitleAndByline({
|
||||
creator,
|
||||
}: {
|
||||
title: string
|
||||
creator?: AppBskyActorDefs.ProfileViewBasic
|
||||
creator?: bsky.profile.AnyProfileView
|
||||
}) {
|
||||
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) {
|
||||
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')
|
||||
}
|
||||
},
|
||||
@@ -307,7 +307,7 @@ function SaveButtonInner({
|
||||
|
||||
<Prompt.Basic
|
||||
control={removePromptControl}
|
||||
title={_(msg`Remove from my feeds?`)}
|
||||
title={_(msg`Remove from your feeds?`)}
|
||||
description={_(
|
||||
msg`Are you sure you want to remove this from your feeds?`,
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react'
|
||||
import {ScrollView, View} from 'react-native'
|
||||
import {AppBskyActorDefs, AppBskyFeedDefs, AtUri} from '@atproto/api'
|
||||
import {View} from 'react-native'
|
||||
import {ScrollView} from 'react-native-gesture-handler'
|
||||
import {AppBskyFeedDefs, AtUri} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
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 * as ProfileCard from '#/components/ProfileCard'
|
||||
import {Text} from '#/components/Typography'
|
||||
import * as bsky from '#/types/bsky'
|
||||
import {ProgressGuideList} from './ProgressGuide/List'
|
||||
|
||||
const MOBILE_CARD_WIDTH = 300
|
||||
@@ -227,7 +229,7 @@ export function ProfileGrid({
|
||||
viewContext = 'feed',
|
||||
}: {
|
||||
isSuggestionsLoading: boolean
|
||||
profiles: AppBskyActorDefs.ProfileViewDetailed[]
|
||||
profiles: bsky.profile.AnyProfileView[]
|
||||
recId?: number
|
||||
error: Error | null
|
||||
viewContext: 'profile' | 'feed'
|
||||
|
||||
@@ -10,6 +10,7 @@ import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Link, LinkProps} from '#/components/Link'
|
||||
import {Text} from '#/components/Typography'
|
||||
import * as bsky from '#/types/bsky'
|
||||
|
||||
const AVI_SIZE = 30
|
||||
const AVI_SIZE_SMALL = 20
|
||||
@@ -32,11 +33,13 @@ export function KnownFollowers({
|
||||
moderationOpts,
|
||||
onLinkPress,
|
||||
minimal,
|
||||
showIfEmpty,
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||
profile: bsky.profile.AnyProfileView
|
||||
moderationOpts: ModerationOpts
|
||||
onLinkPress?: LinkProps['onPress']
|
||||
minimal?: boolean
|
||||
showIfEmpty?: boolean
|
||||
}) {
|
||||
const cache = React.useRef<Map<string, AppBskyActorDefs.KnownFollowers>>(
|
||||
new Map(),
|
||||
@@ -63,11 +66,12 @@ export function KnownFollowers({
|
||||
moderationOpts={moderationOpts}
|
||||
onLinkPress={onLinkPress}
|
||||
minimal={minimal}
|
||||
showIfEmpty={showIfEmpty}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
return <EmptyFallback show={showIfEmpty} />
|
||||
}
|
||||
|
||||
function KnownFollowersInner({
|
||||
@@ -76,22 +80,19 @@ function KnownFollowersInner({
|
||||
cachedKnownFollowers,
|
||||
onLinkPress,
|
||||
minimal,
|
||||
showIfEmpty,
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||
profile: bsky.profile.AnyProfileView
|
||||
moderationOpts: ModerationOpts
|
||||
cachedKnownFollowers: AppBskyActorDefs.KnownFollowers
|
||||
onLinkPress?: LinkProps['onPress']
|
||||
minimal?: boolean
|
||||
showIfEmpty?: boolean
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
|
||||
const textStyle = [
|
||||
a.flex_1,
|
||||
a.text_sm,
|
||||
a.leading_snug,
|
||||
t.atoms.text_contrast_medium,
|
||||
]
|
||||
const textStyle = [a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]
|
||||
|
||||
const slice = cachedKnownFollowers.followers.slice(0, 3).map(f => {
|
||||
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
|
||||
* valid indices_
|
||||
*/
|
||||
if (slice.length === 0) return null
|
||||
if (slice.length === 0) return <EmptyFallback show={showIfEmpty} />
|
||||
|
||||
const SIZE = minimal ? AVI_SIZE_SMALL : AVI_SIZE
|
||||
|
||||
@@ -126,7 +127,7 @@ function KnownFollowersInner({
|
||||
onPress={onLinkPress}
|
||||
to={makeProfileLink(profile, 'known-followers')}
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.max_w_full,
|
||||
a.flex_row,
|
||||
minimal ? a.gap_sm : a.gap_md,
|
||||
a.align_center,
|
||||
@@ -171,6 +172,7 @@ function KnownFollowersInner({
|
||||
|
||||
<Text
|
||||
style={[
|
||||
a.flex_shrink,
|
||||
textStyle,
|
||||
hovered && {
|
||||
textDecorationLine: 'underline',
|
||||
@@ -242,3 +244,15 @@ function KnownFollowersInner({
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {createContext, useCallback, useContext} from 'react'
|
||||
import {GestureResponderEvent, View} from 'react-native'
|
||||
import {GestureResponderEvent, Keyboard, View} from 'react-native'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
TextStyleProp,
|
||||
useBreakpoints,
|
||||
useGutters,
|
||||
useLayoutBreakpoints,
|
||||
useTheme,
|
||||
web,
|
||||
} from '#/alf'
|
||||
@@ -23,6 +24,7 @@ import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
|
||||
import {
|
||||
BUTTON_VISUAL_ALIGNMENT_OFFSET,
|
||||
HEADER_SLOT_SIZE,
|
||||
SCROLLBAR_OFFSET,
|
||||
} from '#/components/Layout/const'
|
||||
import {ScrollbarOffsetContext} from '#/components/Layout/context'
|
||||
import {Text} from '#/components/Typography'
|
||||
@@ -42,6 +44,7 @@ export function Outer({
|
||||
const gutters = useGutters([0, 'base'])
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const {isWithinOffsetView} = useContext(ScrollbarOffsetContext)
|
||||
const {centerColumnOffset} = useLayoutBreakpoints()
|
||||
|
||||
return (
|
||||
<View
|
||||
@@ -60,7 +63,12 @@ export function Outer({
|
||||
}),
|
||||
t.atoms.border_contrast_low,
|
||||
gtMobile && [a.mx_auto, {maxWidth: 600}],
|
||||
!isWithinOffsetView && a.scrollbar_offset,
|
||||
!isWithinOffsetView && {
|
||||
transform: [
|
||||
{translateX: centerColumnOffset ? -150 : 0},
|
||||
{translateX: web(SCROLLBAR_OFFSET) ?? 0},
|
||||
],
|
||||
},
|
||||
]}>
|
||||
{children}
|
||||
</View>
|
||||
@@ -140,6 +148,7 @@ export function MenuButton() {
|
||||
const {gtMobile} = useBreakpoints()
|
||||
|
||||
const onPress = useCallback(() => {
|
||||
Keyboard.dismiss()
|
||||
setDrawerOpen(true)
|
||||
}, [setDrawerOpen])
|
||||
|
||||
|
||||
@@ -13,7 +13,15 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
|
||||
import {isWeb} from '#/platform/detection'
|
||||
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'
|
||||
|
||||
export * from '#/components/Layout/const'
|
||||
@@ -47,6 +55,7 @@ export const Screen = React.memo(function Screen({
|
||||
export type ContentProps = AnimatedScrollViewProps & {
|
||||
style?: StyleProp<ViewStyle>
|
||||
contentContainerStyle?: StyleProp<ViewStyle>
|
||||
ignoreTabletLayoutOffset?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,6 +65,7 @@ export const Content = React.memo(function Content({
|
||||
children,
|
||||
style,
|
||||
contentContainerStyle,
|
||||
ignoreTabletLayoutOffset,
|
||||
...props
|
||||
}: ContentProps) {
|
||||
const t = useTheme()
|
||||
@@ -84,8 +94,10 @@ export const Content = React.memo(function Content({
|
||||
]}
|
||||
{...props}>
|
||||
{isWeb ? (
|
||||
// @ts-ignore web only -esb
|
||||
<Center>{children}</Center>
|
||||
<Center ignoreTabletLayoutOffset={ignoreTabletLayoutOffset}>
|
||||
{/* @ts-expect-error web only -esb */}
|
||||
{children}
|
||||
</Center>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
@@ -138,10 +150,13 @@ export const KeyboardAwareContent = React.memo(function LayoutScrollView({
|
||||
export const Center = React.memo(function LayoutContent({
|
||||
children,
|
||||
style,
|
||||
ignoreTabletLayoutOffset,
|
||||
...props
|
||||
}: ViewProps) {
|
||||
}: ViewProps & {ignoreTabletLayoutOffset?: boolean}) {
|
||||
const {isWithinOffsetView} = useContext(ScrollbarOffsetContext)
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const {centerColumnOffset} = useLayoutBreakpoints()
|
||||
const {isWithinDialog} = useDialogContext()
|
||||
const ctx = useMemo(() => ({isWithinOffsetView: true}), [])
|
||||
return (
|
||||
<View
|
||||
@@ -151,8 +166,20 @@ export const Center = React.memo(function LayoutContent({
|
||||
gtMobile && {
|
||||
maxWidth: 600,
|
||||
},
|
||||
!isWithinOffsetView && {
|
||||
transform: [
|
||||
{
|
||||
translateX:
|
||||
centerColumnOffset &&
|
||||
!ignoreTabletLayoutOffset &&
|
||||
!isWithinDialog
|
||||
? -150
|
||||
: 0,
|
||||
},
|
||||
{translateX: web(SCROLLBAR_OFFSET) ?? 0},
|
||||
],
|
||||
},
|
||||
style,
|
||||
!isWithinOffsetView && a.scrollbar_offset,
|
||||
]}
|
||||
{...props}>
|
||||
<ScrollbarOffsetContext.Provider value={ctx}>
|
||||
@@ -168,6 +195,7 @@ export const Center = React.memo(function LayoutContent({
|
||||
const WebCenterBorders = React.memo(function LayoutContent() {
|
||||
const t = useTheme()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const {centerColumnOffset} = useLayoutBreakpoints()
|
||||
return gtMobile ? (
|
||||
<View
|
||||
style={[
|
||||
@@ -180,9 +208,8 @@ const WebCenterBorders = React.memo(function LayoutContent() {
|
||||
width: 602,
|
||||
left: '50%',
|
||||
transform: [
|
||||
{
|
||||
translateX: '-50%',
|
||||
},
|
||||
{translateX: '-50%'},
|
||||
{translateX: centerColumnOffset ? -150 : 0},
|
||||
...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.
|
||||
*/
|
||||
shareOnLongPress?: boolean
|
||||
|
||||
/**
|
||||
* Whether the link should be opened through the redirect proxy.
|
||||
*/
|
||||
shouldProxy?: boolean
|
||||
}
|
||||
|
||||
export function useLink({
|
||||
@@ -80,9 +85,11 @@ export function useLink({
|
||||
onLongPress: outerOnLongPress,
|
||||
shareOnLongPress,
|
||||
overridePresentation,
|
||||
shouldProxy,
|
||||
}: BaseLinkProps & {
|
||||
displayText: string
|
||||
overridePresentation?: boolean
|
||||
shouldProxy?: boolean
|
||||
}) {
|
||||
const navigation = useNavigationDeduped()
|
||||
const {href} = useLinkProps<AllNavigatorParams>({
|
||||
@@ -118,7 +125,7 @@ export function useLink({
|
||||
})
|
||||
} else {
|
||||
if (isExternal) {
|
||||
openLink(href, overridePresentation)
|
||||
openLink(href, overridePresentation, shouldProxy)
|
||||
} else {
|
||||
const shouldOpenInNewTab = shouldClickOpenNewTab(e)
|
||||
|
||||
@@ -161,6 +168,7 @@ export function useLink({
|
||||
action,
|
||||
navigation,
|
||||
overridePresentation,
|
||||
shouldProxy,
|
||||
],
|
||||
)
|
||||
|
||||
@@ -219,6 +227,7 @@ export function Link({
|
||||
onPress: outerOnPress,
|
||||
onLongPress: outerOnLongPress,
|
||||
download,
|
||||
shouldProxy,
|
||||
...rest
|
||||
}: LinkProps) {
|
||||
const {href, isExternal, onPress, onLongPress} = useLink({
|
||||
@@ -227,6 +236,7 @@ export function Link({
|
||||
action,
|
||||
onPress: outerOnPress,
|
||||
onLongPress: outerOnLongPress,
|
||||
shouldProxy: shouldProxy,
|
||||
})
|
||||
|
||||
return (
|
||||
@@ -279,6 +289,7 @@ export function InlineLinkText({
|
||||
shareOnLongPress,
|
||||
disableUnderline,
|
||||
overridePresentation,
|
||||
shouldProxy,
|
||||
...rest
|
||||
}: InlineLinkProps) {
|
||||
const t = useTheme()
|
||||
@@ -292,6 +303,7 @@ export function InlineLinkText({
|
||||
onLongPress: outerOnLongPress,
|
||||
shareOnLongPress,
|
||||
overridePresentation,
|
||||
shouldProxy: shouldProxy,
|
||||
})
|
||||
const {
|
||||
state: hovered,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {
|
||||
AppBskyActorDefs,
|
||||
AppBskyGraphDefs,
|
||||
AtUri,
|
||||
moderateUserList,
|
||||
@@ -26,7 +25,7 @@ import {
|
||||
import {Link as InternalLink, LinkProps} from '#/components/Link'
|
||||
import * as Hider from '#/components/moderation/Hider'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {ButtonProps} from './Button'
|
||||
import * as bsky from '#/types/bsky'
|
||||
|
||||
/*
|
||||
* This component is based on `FeedCard` and is tightly coupled with that
|
||||
@@ -49,7 +48,7 @@ const MODLIST = 'app.bsky.graph.defs#modlist'
|
||||
type Props = {
|
||||
view: AppBskyGraphDefs.ListView
|
||||
showPinButton?: boolean
|
||||
} & Omit<LinkProps, 'to' | 'label' | 'children'>
|
||||
}
|
||||
|
||||
export function Default(props: Props) {
|
||||
const {view, showPinButton} = props
|
||||
@@ -83,7 +82,7 @@ export function Link({
|
||||
view,
|
||||
children,
|
||||
...props
|
||||
}: Props & Pick<ButtonProps, 'children'>) {
|
||||
}: Props & Omit<LinkProps, 'to' | 'label'>) {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const href = React.useMemo(() => {
|
||||
@@ -108,7 +107,7 @@ export function TitleAndByline({
|
||||
modUi,
|
||||
}: {
|
||||
title: string
|
||||
creator?: AppBskyActorDefs.ProfileViewBasic
|
||||
creator?: bsky.profile.AnyProfileView
|
||||
purpose?: AppBskyGraphDefs.ListView['purpose']
|
||||
modUi?: ModerationUI
|
||||
}) {
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
import React from 'react'
|
||||
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
|
||||
import {Image} from 'expo-image'
|
||||
import {
|
||||
AppBskyEmbedExternal,
|
||||
AppBskyEmbedImages,
|
||||
AppBskyEmbedRecordWithMedia,
|
||||
AppBskyEmbedVideo,
|
||||
} from '@atproto/api'
|
||||
import {AppBskyFeedDefs} from '@atproto/api'
|
||||
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 {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
|
||||
import * as bsky from '#/types/bsky'
|
||||
|
||||
/**
|
||||
* Streamlined MediaPreview component which just handles images, gifs, and videos
|
||||
@@ -22,20 +18,17 @@ export function Embed({
|
||||
embed,
|
||||
style,
|
||||
}: {
|
||||
embed?:
|
||||
| AppBskyEmbedImages.View
|
||||
| AppBskyEmbedRecordWithMedia.View
|
||||
| AppBskyEmbedExternal.View
|
||||
| AppBskyEmbedVideo.View
|
||||
| {[k: string]: unknown}
|
||||
embed: AppBskyFeedDefs.PostView['embed']
|
||||
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 (
|
||||
<Outer style={style}>
|
||||
{media.images.map(image => (
|
||||
{e.view.images.map(image => (
|
||||
<ImageItem
|
||||
key={image.thumb}
|
||||
thumbnail={image.thumb}
|
||||
@@ -44,28 +37,21 @@ export function Embed({
|
||||
))}
|
||||
</Outer>
|
||||
)
|
||||
} else if (AppBskyEmbedExternal.isView(media) && media.external.thumb) {
|
||||
let url: URL | undefined
|
||||
try {
|
||||
url = new URL(media.external.uri)
|
||||
} catch {}
|
||||
if (url) {
|
||||
const {success} = parseTenorGif(url)
|
||||
if (success) {
|
||||
return (
|
||||
<Outer style={style}>
|
||||
<GifItem
|
||||
thumbnail={media.external.thumb}
|
||||
alt={media.external.title}
|
||||
/>
|
||||
</Outer>
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (AppBskyEmbedVideo.isView(media)) {
|
||||
} else if (e.type === 'link') {
|
||||
if (!e.view.external.thumb) return null
|
||||
if (!isTenorGifUri(e.view.external.uri)) return null
|
||||
return (
|
||||
<Outer style={style}>
|
||||
<VideoItem thumbnail={media.thumbnail} alt={media.alt} />
|
||||
<GifItem
|
||||
thumbnail={e.view.external.thumb}
|
||||
alt={e.view.external.title}
|
||||
/>
|
||||
</Outer>
|
||||
)
|
||||
} else if (e.type === 'video') {
|
||||
return (
|
||||
<Outer style={style}>
|
||||
<VideoItem thumbnail={e.view.thumbnail} alt={e.view.alt} />
|
||||
</Outer>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ export function Outer({
|
||||
<Dialog.Handle />
|
||||
{/* Re-wrap with context since Dialogs are portal-ed to root */}
|
||||
<Context.Provider value={context}>
|
||||
<Dialog.ScrollableInner label={_(msg`Menu`)} style={[a.py_sm]}>
|
||||
<Dialog.ScrollableInner label={_(msg`Menu`)}>
|
||||
<View style={[a.gap_lg]}>
|
||||
{children}
|
||||
{isNative && showCancel && <Cancel />}
|
||||
|
||||
@@ -184,9 +184,10 @@ export function Outer({
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content
|
||||
sideOffset={5}
|
||||
collisionPadding={{left: 5, right: 5, bottom: 5}}
|
||||
loop
|
||||
aria-label="Test"
|
||||
className="dropdown-menu-transform-origin">
|
||||
className="dropdown-menu-transform-origin dropdown-menu-constrain-size">
|
||||
<View
|
||||
style={[
|
||||
a.rounded_sm,
|
||||
@@ -195,6 +196,7 @@ export function Outer({
|
||||
t.name === 'light' ? t.atoms.bg : t.atoms.bg_contrast_25,
|
||||
t.atoms.shadow_md,
|
||||
t.atoms.border_contrast_low,
|
||||
a.overflow_auto,
|
||||
!reduceMotionEnabled && a.zoom_fade_in,
|
||||
style,
|
||||
]}>
|
||||
@@ -380,9 +382,8 @@ export function Divider() {
|
||||
style={flatten([
|
||||
a.my_xs,
|
||||
t.atoms.bg_contrast_100,
|
||||
{
|
||||
height: 1,
|
||||
},
|
||||
a.flex_shrink_0,
|
||||
{height: 1},
|
||||
])}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react'
|
||||
import {GestureResponderEvent, View} from 'react-native'
|
||||
import {
|
||||
AppBskyActorDefs,
|
||||
moderateProfile,
|
||||
ModerationOpts,
|
||||
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 {RichText} from '#/components/RichText'
|
||||
import {Text} from '#/components/Typography'
|
||||
import * as bsky from '#/types/bsky'
|
||||
|
||||
export function Default({
|
||||
profile,
|
||||
moderationOpts,
|
||||
logContext = 'ProfileCard',
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||
profile: bsky.profile.AnyProfileView
|
||||
moderationOpts: ModerationOpts
|
||||
logContext?: 'ProfileCard' | 'StarterPackProfilesList'
|
||||
}) {
|
||||
@@ -51,7 +51,7 @@ export function Card({
|
||||
moderationOpts,
|
||||
logContext = 'ProfileCard',
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||
profile: bsky.profile.AnyProfileView
|
||||
moderationOpts: ModerationOpts
|
||||
logContext?: 'ProfileCard' | 'StarterPackProfilesList'
|
||||
}) {
|
||||
@@ -101,7 +101,7 @@ export function Link({
|
||||
style,
|
||||
...rest
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||
profile: bsky.profile.AnyProfileView
|
||||
} & Omit<LinkProps, 'to' | 'label'>) {
|
||||
const {_} = useLingui()
|
||||
return (
|
||||
@@ -126,7 +126,7 @@ export function Avatar({
|
||||
profile,
|
||||
moderationOpts,
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||
profile: bsky.profile.AnyProfileView
|
||||
moderationOpts: ModerationOpts
|
||||
}) {
|
||||
const moderation = moderateProfile(profile, moderationOpts)
|
||||
@@ -161,7 +161,7 @@ export function NameAndHandle({
|
||||
profile,
|
||||
moderationOpts,
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||
profile: bsky.profile.AnyProfileView
|
||||
moderationOpts: ModerationOpts
|
||||
}) {
|
||||
const t = useTheme()
|
||||
@@ -224,17 +224,16 @@ export function Description({
|
||||
profile: profileUnshadowed,
|
||||
numberOfLines = 3,
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||
profile: bsky.profile.AnyProfileView
|
||||
numberOfLines?: number
|
||||
}) {
|
||||
const profile = useProfileShadow(profileUnshadowed)
|
||||
const {description} = profile
|
||||
const rt = React.useMemo(() => {
|
||||
if (!description) return
|
||||
const rt = new RichTextApi({text: description || ''})
|
||||
if (!('description' in profile)) return
|
||||
const rt = new RichTextApi({text: profile.description || ''})
|
||||
rt.detectFacetsWithoutResolution()
|
||||
return rt
|
||||
}, [description])
|
||||
}, [profile])
|
||||
if (!rt) return null
|
||||
if (
|
||||
profile.viewer &&
|
||||
@@ -281,7 +280,7 @@ export function DescriptionPlaceholder({
|
||||
}
|
||||
|
||||
export type FollowButtonProps = {
|
||||
profile: AppBskyActorDefs.ProfileViewBasic
|
||||
profile: bsky.profile.AnyProfileView
|
||||
moderationOpts: ModerationOpts
|
||||
logContext: LogEvents['profile:follow']['logContext'] &
|
||||
LogEvents['profile:unfollow']['logContext']
|
||||
|
||||
@@ -45,6 +45,7 @@ export function Outer({
|
||||
<Dialog.Outer
|
||||
control={control}
|
||||
testID={testID}
|
||||
webOptions={{alignCenter: true}}
|
||||
nativeOptions={{preventExpansion: true, ...nativeOptions}}>
|
||||
<Dialog.Handle />
|
||||
<Context.Provider value={context}>
|
||||
|
||||
@@ -23,6 +23,7 @@ export type RichTextProps = TextStyleProp &
|
||||
onLinkPress?: LinkProps['onPress']
|
||||
interactiveStyle?: TextStyle
|
||||
emojiMultiplier?: number
|
||||
shouldProxyLinks?: boolean
|
||||
}
|
||||
|
||||
export function RichText({
|
||||
@@ -39,6 +40,7 @@ export function RichText({
|
||||
emojiMultiplier = 1.85,
|
||||
onLayout,
|
||||
onTextLayout,
|
||||
shouldProxyLinks,
|
||||
}: RichTextProps) {
|
||||
const richText = React.useMemo(
|
||||
() =>
|
||||
@@ -110,6 +112,7 @@ export function RichText({
|
||||
style={interactiveStyles}
|
||||
// @ts-ignore TODO
|
||||
dataSet={WORD_WRAP}
|
||||
shouldProxy={shouldProxyLinks}
|
||||
onPress={onLinkPress}>
|
||||
{segment.text}
|
||||
</InlineLinkText>
|
||||
@@ -128,6 +131,7 @@ export function RichText({
|
||||
// @ts-ignore TODO
|
||||
dataSet={WORD_WRAP}
|
||||
shareOnLongPress
|
||||
shouldProxy={shouldProxyLinks}
|
||||
onPress={onLinkPress}
|
||||
emoji>
|
||||
{toShortUrl(segment.text)}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import React from 'react'
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {
|
||||
findNodeHandle,
|
||||
ListRenderItemInfo,
|
||||
@@ -6,11 +11,10 @@ import {
|
||||
View,
|
||||
ViewStyle,
|
||||
} from 'react-native'
|
||||
import {AppBskyGraphDefs, AppBskyGraphGetActorStarterPacks} from '@atproto/api'
|
||||
import {AppBskyGraphDefs} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
import {InfiniteData, UseInfiniteQueryResult} from '@tanstack/react-query'
|
||||
|
||||
import {useGenerateStarterPackMutation} from '#/lib/generate-starterpack'
|
||||
import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset'
|
||||
@@ -19,28 +23,27 @@ import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||
import {NavigationProp} from '#/lib/routes/types'
|
||||
import {parseStarterPackUri} from '#/lib/strings/starter-pack'
|
||||
import {logger} from '#/logger'
|
||||
import {useActorStarterPacksQuery} from '#/state/queries/actor-starter-packs'
|
||||
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 {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
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 {Loader} from '#/components/Loader'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {Default as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
|
||||
import {VerifyEmailDialog} from '../dialogs/VerifyEmailDialog'
|
||||
import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '../icons/Plus'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
interface SectionRef {
|
||||
scrollToTop: () => void
|
||||
}
|
||||
|
||||
interface ProfileFeedgensProps {
|
||||
starterPacksQuery: UseInfiniteQueryResult<
|
||||
InfiniteData<AppBskyGraphGetActorStarterPacks.OutputSchema, unknown>,
|
||||
Error
|
||||
>
|
||||
scrollElRef: ListRef
|
||||
did: string
|
||||
headerOffset: number
|
||||
enabled?: boolean
|
||||
style?: StyleProp<ViewStyle>
|
||||
@@ -58,8 +61,8 @@ export const ProfileStarterPacks = React.forwardRef<
|
||||
ProfileFeedgensProps
|
||||
>(function ProfileFeedgensImpl(
|
||||
{
|
||||
starterPacksQuery: query,
|
||||
scrollElRef,
|
||||
did,
|
||||
headerOffset,
|
||||
enabled,
|
||||
style,
|
||||
@@ -71,17 +74,18 @@ export const ProfileStarterPacks = React.forwardRef<
|
||||
) {
|
||||
const t = useTheme()
|
||||
const bottomBarOffset = useBottomBarOffset(100)
|
||||
const [isPTRing, setIsPTRing] = React.useState(false)
|
||||
const {data, refetch, isFetching, hasNextPage, fetchNextPage} = query
|
||||
const [isPTRing, setIsPTRing] = useState(false)
|
||||
const {data, refetch, isFetching, hasNextPage, fetchNextPage} =
|
||||
useActorStarterPacksQuery({did, enabled})
|
||||
const {isTabletOrDesktop} = useWebMediaQueries()
|
||||
|
||||
const items = data?.pages.flatMap(page => page.starterPacks)
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
useImperativeHandle(ref, () => ({
|
||||
scrollToTop: () => {},
|
||||
}))
|
||||
|
||||
const onRefresh = React.useCallback(async () => {
|
||||
const onRefresh = useCallback(async () => {
|
||||
setIsPTRing(true)
|
||||
try {
|
||||
await refetch()
|
||||
@@ -91,7 +95,7 @@ export const ProfileStarterPacks = React.forwardRef<
|
||||
setIsPTRing(false)
|
||||
}, [refetch, setIsPTRing])
|
||||
|
||||
const onEndReached = React.useCallback(async () => {
|
||||
const onEndReached = useCallback(async () => {
|
||||
if (isFetching || !hasNextPage) return
|
||||
|
||||
try {
|
||||
@@ -101,7 +105,7 @@ export const ProfileStarterPacks = React.forwardRef<
|
||||
}
|
||||
}, [isFetching, hasNextPage, fetchNextPage])
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (enabled && scrollElRef.current) {
|
||||
const nativeTag = findNodeHandle(scrollElRef.current)
|
||||
setScrollViewTag(nativeTag)
|
||||
@@ -140,9 +144,11 @@ export const ProfileStarterPacks = React.forwardRef<
|
||||
desktopFixedHeight
|
||||
onEndReached={onEndReached}
|
||||
onRefresh={onRefresh}
|
||||
ListEmptyComponent={Empty}
|
||||
ListEmptyComponent={
|
||||
data ? (isMe ? Empty : undefined) : FeedLoadingPlaceholder
|
||||
}
|
||||
ListFooterComponent={
|
||||
items?.length !== 0 && isMe ? CreateAnother : undefined
|
||||
!!data && items?.length !== 0 && isMe ? CreateAnother : undefined
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
@@ -181,7 +187,6 @@ function CreateAnother() {
|
||||
|
||||
function Empty() {
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const confirmDialogControl = useDialogControl()
|
||||
const followersDialogControl = useDialogControl()
|
||||
@@ -190,7 +195,7 @@ function Empty() {
|
||||
const {needsEmailVerification} = useEmail()
|
||||
const verifyEmailControl = useDialogControl()
|
||||
|
||||
const [isGenerating, setIsGenerating] = React.useState(false)
|
||||
const [isGenerating, setIsGenerating] = useState(false)
|
||||
|
||||
const {mutate: generateStarterPack} = useGenerateStarterPackMutation({
|
||||
onSuccess: ({uri}) => {
|
||||
@@ -227,16 +232,10 @@ function Empty() {
|
||||
a.justify_between,
|
||||
a.gap_lg,
|
||||
a.shadow_lg,
|
||||
{marginTop: 1},
|
||||
{marginTop: a.border.borderWidth},
|
||||
]}>
|
||||
<View style={[a.gap_xs]}>
|
||||
<Text
|
||||
style={[
|
||||
a.font_bold,
|
||||
a.text_lg,
|
||||
t.atoms.text_contrast_medium,
|
||||
{color: 'white'},
|
||||
]}>
|
||||
<Text style={[a.font_bold, a.text_lg, {color: 'white'}]}>
|
||||
<Trans>You haven't created a starter pack yet!</Trans>
|
||||
</Text>
|
||||
<Text style={[a.text_md, {color: 'white'}]}>
|
||||
|
||||
@@ -13,6 +13,7 @@ import {useTheme} from '#/alf'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {LinearGradientBackground} from '#/components/LinearGradientBackground'
|
||||
import {Text} from '#/components/Typography'
|
||||
import * as bsky from '#/types/bsky'
|
||||
|
||||
const LazyViewShot = React.lazy(
|
||||
// @ts-expect-error dynamic import
|
||||
@@ -30,7 +31,12 @@ export const QrCode = React.forwardRef<ViewShot, Props>(function QrCode(
|
||||
) {
|
||||
const {record} = starterPack
|
||||
|
||||
if (!AppBskyGraphStarterpack.isRecord(record)) {
|
||||
if (
|
||||
!bsky.dangerousIsType<AppBskyGraphStarterpack.Record>(
|
||||
record,
|
||||
AppBskyGraphStarterpack.isRecord,
|
||||
)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import * as Dialog from '#/components/Dialog'
|
||||
import {DialogControlProps} from '#/components/Dialog'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {QrCode} from '#/components/StarterPack/QrCode'
|
||||
import * as bsky from '#/types/bsky'
|
||||
|
||||
export function QrCodeDialog({
|
||||
starterPack,
|
||||
@@ -77,7 +78,12 @@ export function QrCodeDialog({
|
||||
} else {
|
||||
setIsProcessing(true)
|
||||
|
||||
if (!AppBskyGraphStarterpack.isRecord(starterPack.record)) {
|
||||
if (
|
||||
!bsky.validate(
|
||||
starterPack.record,
|
||||
AppBskyGraphStarterpack.validateRecord,
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {Image} from 'expo-image'
|
||||
import {AppBskyGraphDefs, AppBskyGraphStarterpack, AtUri} from '@atproto/api'
|
||||
import {AppBskyGraphStarterpack, AtUri} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
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 {Link as BaseLink, LinkProps as BaseLinkProps} from '#/components/Link'
|
||||
import {Text} from '#/components/Typography'
|
||||
import * as bsky from '#/types/bsky'
|
||||
|
||||
export function Default({
|
||||
starterPack,
|
||||
}: {
|
||||
starterPack?: AppBskyGraphDefs.StarterPackViewBasic
|
||||
starterPack?: bsky.starterPack.AnyStarterPackView
|
||||
}) {
|
||||
if (!starterPack) return null
|
||||
return (
|
||||
@@ -32,7 +33,7 @@ export function Default({
|
||||
export function Notification({
|
||||
starterPack,
|
||||
}: {
|
||||
starterPack?: AppBskyGraphDefs.StarterPackViewBasic
|
||||
starterPack?: bsky.starterPack.AnyStarterPackView
|
||||
}) {
|
||||
if (!starterPack) return null
|
||||
return (
|
||||
@@ -47,7 +48,7 @@ export function Card({
|
||||
noIcon,
|
||||
noDescription,
|
||||
}: {
|
||||
starterPack: AppBskyGraphDefs.StarterPackViewBasic
|
||||
starterPack: bsky.starterPack.AnyStarterPackView
|
||||
noIcon?: boolean
|
||||
noDescription?: boolean
|
||||
}) {
|
||||
@@ -57,7 +58,12 @@ export function Card({
|
||||
const t = useTheme()
|
||||
const {currentAccount} = useSession()
|
||||
|
||||
if (!AppBskyGraphStarterpack.isRecord(record)) {
|
||||
if (
|
||||
!bsky.dangerousIsType<AppBskyGraphStarterpack.Record>(
|
||||
record,
|
||||
AppBskyGraphStarterpack.isRecord,
|
||||
)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -100,7 +106,7 @@ export function Link({
|
||||
starterPack,
|
||||
children,
|
||||
}: {
|
||||
starterPack: AppBskyGraphDefs.StarterPackViewBasic
|
||||
starterPack: bsky.starterPack.AnyStarterPackView
|
||||
onPress?: () => void
|
||||
children: BaseLinkProps['children']
|
||||
}) {
|
||||
@@ -139,7 +145,7 @@ export function Link({
|
||||
export function Embed({
|
||||
starterPack,
|
||||
}: {
|
||||
starterPack: AppBskyGraphDefs.StarterPackViewBasic
|
||||
starterPack: bsky.starterPack.AnyStarterPackView
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const imageUri = getStarterPackOgCard(starterPack)
|
||||
|
||||
@@ -38,7 +38,7 @@ export function WizardEditListDialog({
|
||||
state: WizardState
|
||||
dispatch: (action: WizardAction) => void
|
||||
moderationOpts: ModerationOpts
|
||||
profile: AppBskyActorDefs.ProfileViewBasic
|
||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
|
||||
@@ -22,6 +22,7 @@ import {Button, ButtonText} from '#/components/Button'
|
||||
import * as Toggle from '#/components/forms/Toggle'
|
||||
import {Checkbox} from '#/components/forms/Toggle'
|
||||
import {Text} from '#/components/Typography'
|
||||
import * as bsky from '#/types/bsky'
|
||||
|
||||
function WizardListCard({
|
||||
type,
|
||||
@@ -123,7 +124,7 @@ export function WizardProfileCard({
|
||||
btnType: 'checkbox' | 'remove'
|
||||
state: WizardState
|
||||
dispatch: (action: WizardAction) => void
|
||||
profile: AppBskyActorDefs.ProfileViewBasic
|
||||
profile: bsky.profile.AnyProfileView
|
||||
moderationOpts: ModerationOpts
|
||||
}) {
|
||||
const {currentAccount} = useSession()
|
||||
|
||||
@@ -27,6 +27,7 @@ import {Link} from '#/components/Link'
|
||||
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
||||
import * as Hider from '#/components/moderation/Hider'
|
||||
import {Text} from '#/components/Typography'
|
||||
import * as bsky from '#/types/bsky'
|
||||
|
||||
function getBlackColor(t: ReturnType<typeof useTheme>) {
|
||||
return select(t.name, {
|
||||
@@ -78,7 +79,12 @@ export function VideoPostCard({
|
||||
if (!AppBskyEmbedVideo.isView(embed)) return null
|
||||
|
||||
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 repostCount = post?.repostCount ?? 0
|
||||
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 {InlineLinkText} from '#/components/Link'
|
||||
import {Text} from '#/components/Typography'
|
||||
import * as bsky from '#/types/bsky'
|
||||
import {PencilLine_Stroke2_Corner0_Rounded as PencilLine} from './icons/Pencil'
|
||||
|
||||
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.
|
||||
*/
|
||||
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.uri
|
||||
const settings = React.useMemo(() => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {getDateAgo} from '#/lib/strings/time'
|
||||
import {logger} from '#/logger'
|
||||
import {isIOS, isWeb} from '#/platform/detection'
|
||||
import {
|
||||
@@ -101,7 +102,7 @@ function BirthdayInner({
|
||||
onChangeDate={newDate => setDate(new Date(newDate))}
|
||||
label={_(msg`Birthday`)}
|
||||
accessibilityHint={_(msg`Enter your birth date`)}
|
||||
maximumDate={new Date()}
|
||||
maximumDate={getDateAgo(13)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, {memo, useRef, useState} from 'react'
|
||||
import {TextInput, View} from 'react-native'
|
||||
import {memo, useEffect, useMemo, useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {AppBskyActorDefs, AppBskyFeedPost, AtUri} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
@@ -8,12 +8,19 @@ import {EMBED_SCRIPT} from '#/lib/constants'
|
||||
import {niceDate} from '#/lib/strings/time'
|
||||
import {toShareUrl} from '#/lib/strings/url-helpers'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import * as TextField from '#/components/forms/TextField'
|
||||
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
||||
import {CodeBrackets_Stroke2_Corner0_Rounded as CodeBrackets} from '#/components/icons/CodeBrackets'
|
||||
import * as ToggleButton from '#/components/forms/ToggleButton'
|
||||
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 {Button, ButtonIcon, ButtonText} from '../Button'
|
||||
|
||||
export type ColorModeValues = 'system' | 'light' | 'dark'
|
||||
|
||||
type EmbedDialogProps = {
|
||||
control: Dialog.DialogControlProps
|
||||
@@ -44,11 +51,12 @@ function EmbedDialogInner({
|
||||
}: Omit<EmbedDialogProps, 'control'>) {
|
||||
const t = useTheme()
|
||||
const {_, i18n} = useLingui()
|
||||
const ref = useRef<TextInput>(null)
|
||||
const [copied, setCopied] = useState(false)
|
||||
const [showCustomisation, setShowCustomisation] = useState(false)
|
||||
const [colorMode, setColorMode] = useState<ColorModeValues>('system')
|
||||
|
||||
// reset copied state after 2 seconds
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (copied) {
|
||||
const timeout = setTimeout(() => {
|
||||
setCopied(false)
|
||||
@@ -57,7 +65,7 @@ function EmbedDialogInner({
|
||||
}
|
||||
}, [copied])
|
||||
|
||||
const snippet = React.useMemo(() => {
|
||||
const snippet = useMemo(() => {
|
||||
function toEmbedUrl(href: string) {
|
||||
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
|
||||
return `<blockquote class="bluesky-embed" data-bluesky-uri="${escapeHtml(
|
||||
postUri,
|
||||
)}" data-bluesky-cid="${escapeHtml(postCid)}"><p lang="${escapeHtml(
|
||||
lang,
|
||||
)}">${escapeHtml(record.text)}${
|
||||
)}" data-bluesky-cid="${escapeHtml(
|
||||
postCid,
|
||||
)}" data-bluesky-embed-color-mode="${escapeHtml(
|
||||
colorMode,
|
||||
)}"><p lang="${escapeHtml(lang)}">${escapeHtml(record.text)}${
|
||||
record.embed
|
||||
? `<br><br><a href="${escapeHtml(href)}">[image or embed]</a>`
|
||||
: ''
|
||||
@@ -88,60 +98,116 @@ function EmbedDialogInner({
|
||||
)}</a>) <a href="${escapeHtml(href)}">${escapeHtml(
|
||||
niceDate(i18n, timestamp),
|
||||
)}</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 (
|
||||
<Dialog.Inner label="Embed post" style={[a.gap_md, {maxWidth: 500}]}>
|
||||
<View style={[a.gap_sm, a.pb_lg]}>
|
||||
<Text style={[a.text_2xl, a.font_bold]}>
|
||||
<Trans>Embed post</Trans>
|
||||
</Text>
|
||||
<Text
|
||||
style={[a.text_md, t.atoms.text_contrast_medium, a.leading_normal]}>
|
||||
<Trans>
|
||||
Embed this post in your website. Simply copy the following snippet
|
||||
and paste it into the HTML code of your website.
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={[a.flex_row, a.gap_sm]}>
|
||||
<View style={[a.flex_1]}>
|
||||
<TextField.Root>
|
||||
<TextField.Icon icon={CodeBrackets} />
|
||||
<TextField.Input
|
||||
label={_(msg`Embed HTML code`)}
|
||||
editable={false}
|
||||
selection={{start: 0, end: snippet.length}}
|
||||
value={snippet}
|
||||
style={{}}
|
||||
/>
|
||||
</TextField.Root>
|
||||
<Dialog.Inner label={_(msg`Embed post`)} style={[{maxWidth: 500}]}>
|
||||
<View style={[a.gap_lg]}>
|
||||
<View style={[a.gap_sm]}>
|
||||
<Text style={[a.text_2xl, a.font_heavy]}>
|
||||
<Trans>Embed post</Trans>
|
||||
</Text>
|
||||
<Text
|
||||
style={[a.text_md, t.atoms.text_contrast_medium, a.leading_normal]}>
|
||||
<Trans>
|
||||
Embed this post in your website. Simply copy the following snippet
|
||||
and paste it into the HTML code of your website.
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
<Button
|
||||
label={_(msg`Copy code`)}
|
||||
color="primary"
|
||||
variant="solid"
|
||||
size="large"
|
||||
onPress={() => {
|
||||
ref.current?.focus()
|
||||
ref.current?.setSelection(0, snippet.length)
|
||||
navigator.clipboard.writeText(snippet)
|
||||
setCopied(true)
|
||||
}}>
|
||||
{copied ? (
|
||||
<>
|
||||
<ButtonIcon icon={Check} />
|
||||
<ButtonText>
|
||||
<Trans>Copied!</Trans>
|
||||
</ButtonText>
|
||||
</>
|
||||
) : (
|
||||
<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>Copy code</Trans>
|
||||
<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>
|
||||
)}
|
||||
</Button>
|
||||
</View>
|
||||
<View style={[a.flex_row, a.gap_sm]}>
|
||||
<View style={[a.flex_1]}>
|
||||
<TextField.Root>
|
||||
<TextField.Icon icon={CodeBracketsIcon} />
|
||||
<TextField.Input
|
||||
label={_(msg`Embed HTML code`)}
|
||||
editable={false}
|
||||
selection={{start: 0, end: snippet.length}}
|
||||
value={snippet}
|
||||
/>
|
||||
</TextField.Root>
|
||||
</View>
|
||||
<Button
|
||||
label={_(msg`Copy code`)}
|
||||
color="primary"
|
||||
variant="solid"
|
||||
size="large"
|
||||
onPress={() => {
|
||||
navigator.clipboard.writeText(snippet)
|
||||
setCopied(true)
|
||||
}}>
|
||||
{copied ? (
|
||||
<>
|
||||
<ButtonIcon icon={CheckIcon} />
|
||||
<ButtonText>
|
||||
<Trans>Copied!</Trans>
|
||||
</ButtonText>
|
||||
</>
|
||||
) : (
|
||||
<ButtonText>
|
||||
<Trans>Copy code</Trans>
|
||||
</ButtonText>
|
||||
)}
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
<Dialog.Close />
|
||||
</Dialog.Inner>
|
||||
|
||||
@@ -318,7 +318,7 @@ function MutedWordsInner() {
|
||||
<View style={[a.pt_xs]}>
|
||||
<Button
|
||||
disabled={isPending || !field}
|
||||
label={_(msg`Add mute word for configured settings`)}
|
||||
label={_(msg`Add mute word with chosen settings`)}
|
||||
size="large"
|
||||
color="primary"
|
||||
variant="solid"
|
||||
|
||||
@@ -81,8 +81,9 @@ export function PostInteractionSettingsControlledDialog({
|
||||
<Trans>
|
||||
You can set default interaction settings in{' '}
|
||||
<Text style={[a.font_bold, t.atoms.text_contrast_medium]}>
|
||||
Settings → Moderation → Interaction settings.
|
||||
Settings → Moderation → Interaction settings
|
||||
</Text>
|
||||
.
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
@@ -208,7 +209,7 @@ export function PostInteractionSettingsDialogControlledInner(
|
||||
props.control.close()
|
||||
} catch (e: any) {
|
||||
logger.error(`Failed to save post interaction settings`, {
|
||||
context: 'PostInteractionSettingsDialogControlledInner',
|
||||
source: 'PostInteractionSettingsDialogControlledInner',
|
||||
safeMessage: e.message,
|
||||
})
|
||||
Toast.show(
|
||||
|
||||
@@ -43,7 +43,7 @@ function SigninDialogInner({}: {control: Dialog.DialogOuterProps['control']}) {
|
||||
|
||||
return (
|
||||
<Dialog.ScrollableInner
|
||||
label={_(msg`Sign into Bluesky or create a new account`)}
|
||||
label={_(msg`Sign in to Bluesky or create a new account`)}
|
||||
style={[gtMobile ? {width: 'auto', maxWidth: 420} : a.w_full]}>
|
||||
<View style={[!isNative && a.p_2xl]}>
|
||||
<View
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import React, {useCallback} from 'react'
|
||||
import {Keyboard, Pressable, View} from 'react-native'
|
||||
import {
|
||||
AppBskyActorDefs,
|
||||
ChatBskyConvoDefs,
|
||||
ModerationCause,
|
||||
} from '@atproto/api'
|
||||
import {ChatBskyConvoDefs, ModerationCause} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
@@ -34,6 +30,7 @@ import {
|
||||
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker'
|
||||
import * as Menu from '#/components/Menu'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import * as bsky from '#/types/bsky'
|
||||
import {Bubble_Stroke2_Corner2_Rounded as Bubble} from '../icons/Bubble'
|
||||
import {ReportDialog} from './ReportDialog'
|
||||
|
||||
@@ -49,7 +46,7 @@ let ConvoMenu = ({
|
||||
style,
|
||||
}: {
|
||||
convo: ChatBskyConvoDefs.ConvoView
|
||||
profile: Shadow<AppBskyActorDefs.ProfileViewBasic>
|
||||
profile: Shadow<bsky.profile.AnyProfileView>
|
||||
control?: Menu.MenuControlProps
|
||||
currentScreen: 'list' | 'conversation'
|
||||
showMarkAsRead?: boolean
|
||||
@@ -148,7 +145,7 @@ function MenuContent({
|
||||
blockedByListControl,
|
||||
}: {
|
||||
convo: ChatBskyConvoDefs.ConvoView
|
||||
profile: Shadow<AppBskyActorDefs.ProfileViewBasic>
|
||||
profile: Shadow<bsky.profile.AnyProfileView>
|
||||
showMarkAsRead?: boolean
|
||||
blockInfo: {
|
||||
listBlocks: ModerationCause[]
|
||||
@@ -176,9 +173,9 @@ function MenuContent({
|
||||
const {mutate: muteConvo} = useMuteConvo(convoId, {
|
||||
onSuccess: data => {
|
||||
if (data.convo.muted) {
|
||||
Toast.show(_(msg`Chat muted`))
|
||||
Toast.show(_(msg({message: 'Chat muted', context: 'toast'})))
|
||||
} else {
|
||||
Toast.show(_(msg`Chat unmuted`))
|
||||
Toast.show(_(msg({message: 'Chat unmuted', context: 'toast'})))
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
|
||||
@@ -13,10 +13,12 @@ export function LeaveConvoPrompt({
|
||||
control,
|
||||
convoId,
|
||||
currentScreen,
|
||||
hasMessages = true,
|
||||
}: {
|
||||
control: DialogOuterProps['control']
|
||||
convoId: string
|
||||
currentScreen: 'list' | 'conversation'
|
||||
hasMessages?: boolean
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
@@ -39,7 +41,9 @@ export function LeaveConvoPrompt({
|
||||
control={control}
|
||||
title={_(msg`Leave conversation`)}
|
||||
description={_(
|
||||
msg`Are you sure you want to leave this conversation? Your messages will be deleted for you, but not for the other participant.`,
|
||||
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`)}
|
||||
confirmButtonColor="negative"
|
||||
|
||||
@@ -69,7 +69,9 @@ export let MessageMenu = ({
|
||||
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
|
||||
convo
|
||||
.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`)))
|
||||
}, [_, convo, message.id])
|
||||
|
||||
|
||||
@@ -8,18 +8,20 @@ import {useNavigation} from '@react-navigation/native'
|
||||
import {useEmail} from '#/lib/hooks/useEmail'
|
||||
import {NavigationProp} from '#/lib/routes/types'
|
||||
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 {Button, ButtonIcon} from '#/components/Button'
|
||||
import {useDialogControl} from '#/components/Dialog'
|
||||
import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog'
|
||||
import {canBeMessaged} from '#/components/dms/util'
|
||||
import {Message_Stroke2_Corner0_Rounded as Message} from '#/components/icons/Message'
|
||||
import {useDialogControl} from '../Dialog'
|
||||
import {VerifyEmailDialog} from '../dialogs/VerifyEmailDialog'
|
||||
|
||||
export function MessageProfileButton({
|
||||
profile,
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileView
|
||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
@@ -27,10 +29,19 @@ export function MessageProfileButton({
|
||||
const {needsEmailVerification} = useEmail()
|
||||
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(() => {
|
||||
if (!convo?.id) {
|
||||
if (!convoAvailability?.canChat) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -39,15 +50,25 @@ export function MessageProfileButton({
|
||||
return
|
||||
}
|
||||
|
||||
if (convo && !convo.lastMessage) {
|
||||
if (convoAvailability.convo) {
|
||||
logEvent('chat:open', {logContext: 'ProfileHeader'})
|
||||
navigation.navigate('MessagesConversation', {
|
||||
conversation: convoAvailability.convo.id,
|
||||
})
|
||||
} else {
|
||||
logEvent('chat:create', {logContext: 'ProfileHeader'})
|
||||
initiateConvo([profile.did])
|
||||
}
|
||||
logEvent('chat:open', {logContext: 'ProfileHeader'})
|
||||
}, [
|
||||
needsEmailVerification,
|
||||
verifyEmailControl,
|
||||
navigation,
|
||||
profile.did,
|
||||
initiateConvo,
|
||||
convoAvailability,
|
||||
])
|
||||
|
||||
navigation.navigate('MessagesConversation', {conversation: convo.id})
|
||||
}, [needsEmailVerification, verifyEmailControl, convo, navigation])
|
||||
|
||||
if (isPending) {
|
||||
if (!convoAvailability) {
|
||||
// show pending state based on declaration
|
||||
if (canBeMessaged(profile)) {
|
||||
return (
|
||||
@@ -69,7 +90,7 @@ export function MessageProfileButton({
|
||||
}
|
||||
}
|
||||
|
||||
if (convo) {
|
||||
if (convoAvailability.canChat) {
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {AppBskyActorDefs, ModerationDecision} from '@atproto/api'
|
||||
import {ModerationDecision} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
@@ -14,6 +14,7 @@ import {BlockedByListDialog} from '#/components/dms/BlockedByListDialog'
|
||||
import {LeaveConvoPrompt} from '#/components/dms/LeaveConvoPrompt'
|
||||
import {ReportConversationPrompt} from '#/components/dms/ReportConversationPrompt'
|
||||
import {Text} from '#/components/Typography'
|
||||
import * as bsky from '#/types/bsky'
|
||||
|
||||
export function MessagesListBlockedFooter({
|
||||
recipient: initialRecipient,
|
||||
@@ -21,7 +22,7 @@ export function MessagesListBlockedFooter({
|
||||
hasMessages,
|
||||
moderation,
|
||||
}: {
|
||||
recipient: AppBskyActorDefs.ProfileViewBasic
|
||||
recipient: bsky.profile.AnyProfileView
|
||||
convoId: string
|
||||
hasMessages: boolean
|
||||
moderation: ModerationDecision
|
||||
|
||||
@@ -17,6 +17,7 @@ import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {Shadow} from '#/state/cache/profile-shadow'
|
||||
import {isConvoActive, useConvo} from '#/state/messages/convo'
|
||||
import {ConvoItem} from '#/state/messages/convo/types'
|
||||
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
|
||||
import {ConvoMenu} from '#/components/dms/ConvoMenu'
|
||||
@@ -31,7 +32,7 @@ export let MessagesListHeader = ({
|
||||
profile,
|
||||
moderation,
|
||||
}: {
|
||||
profile?: Shadow<AppBskyActorDefs.ProfileViewBasic>
|
||||
profile?: Shadow<AppBskyActorDefs.ProfileViewDetailed>
|
||||
moderation?: ModerationDecision
|
||||
}): React.ReactNode => {
|
||||
const t = useTheme()
|
||||
@@ -52,10 +53,10 @@ export let MessagesListHeader = ({
|
||||
}, [moderation])
|
||||
|
||||
const onPressBack = useCallback(() => {
|
||||
if (isWeb) {
|
||||
navigation.replace('Messages', {})
|
||||
} else {
|
||||
if (navigation.canGoBack()) {
|
||||
navigation.goBack()
|
||||
} else {
|
||||
navigation.navigate('Messages', {})
|
||||
}
|
||||
}, [navigation])
|
||||
|
||||
@@ -138,7 +139,7 @@ function HeaderReady({
|
||||
moderation,
|
||||
blockInfo,
|
||||
}: {
|
||||
profile: Shadow<AppBskyActorDefs.ProfileViewBasic>
|
||||
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>
|
||||
moderation: ModerationDecision
|
||||
blockInfo: {
|
||||
listBlocks: ModerationCause[]
|
||||
@@ -157,8 +158,10 @@ function HeaderReady({
|
||||
moderation.ui('displayName'),
|
||||
)
|
||||
|
||||
// @ts-ignore findLast is polyfilled - esb
|
||||
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 =
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, {memo, useMemo, useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {
|
||||
$Typed,
|
||||
AppBskyActorDefs,
|
||||
ChatBskyConvoDefs,
|
||||
ComAtprotoModerationCreateReport,
|
||||
@@ -154,15 +155,16 @@ function SubmitStep({
|
||||
mutationFn: async () => {
|
||||
if (params.type === 'convoMessage') {
|
||||
const {convoId, message} = params
|
||||
const subject: $Typed<ChatBskyConvoDefs.MessageRef> = {
|
||||
$type: 'chat.bsky.convo.defs#messageRef',
|
||||
messageId: message.id,
|
||||
convoId,
|
||||
did: message.sender.did,
|
||||
}
|
||||
|
||||
const report = {
|
||||
reasonType: reportOption.reason,
|
||||
subject: {
|
||||
$type: 'chat.bsky.convo.defs#messageRef',
|
||||
messageId: message.id,
|
||||
convoId,
|
||||
did: message.sender.did,
|
||||
} satisfies ChatBskyConvoDefs.MessageRef,
|
||||
subject,
|
||||
reason: details,
|
||||
} satisfies ComAtprotoModerationCreateReport.InputSchema
|
||||
|
||||
@@ -285,7 +287,7 @@ function DoneStep({
|
||||
}: {
|
||||
convoId: string
|
||||
currentScreen: 'list' | 'conversation'
|
||||
profile: AppBskyActorDefs.ProfileViewBasic
|
||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
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 = () => {
|
||||
control.close(() => {
|
||||
if (actions.includes('block')) {
|
||||
@@ -317,18 +332,12 @@ function DoneStep({
|
||||
if (actions.includes('leave')) {
|
||||
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 (
|
||||
<View style={a.gap_2xl}>
|
||||
<View style={[a.justify_center, gtMobile ? a.gap_sm : a.gap_xs]}>
|
||||
|
||||
@@ -6,7 +6,7 @@ import React, {
|
||||
useState,
|
||||
} from 'react'
|
||||
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 {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 {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||
import {Text} from '#/components/Typography'
|
||||
import * as bsky from '#/types/bsky'
|
||||
|
||||
type Item =
|
||||
| {
|
||||
type: 'profile'
|
||||
key: string
|
||||
enabled: boolean
|
||||
profile: AppBskyActorDefs.ProfileView
|
||||
profile: bsky.profile.AnyProfileView
|
||||
}
|
||||
| {
|
||||
type: 'empty'
|
||||
@@ -76,7 +77,10 @@ export function SearchablePeopleList({
|
||||
isFetching,
|
||||
} = useActorAutocompleteQuery(searchText, true, 12)
|
||||
const {data: follows} = useProfileFollowsQuery(currentAccount?.did)
|
||||
const {data: convos} = useListConvosQuery({enabled: showRecentConvos})
|
||||
const {data: convos} = useListConvosQuery({
|
||||
enabled: showRecentConvos,
|
||||
status: 'accepted',
|
||||
})
|
||||
|
||||
const items = useMemo(() => {
|
||||
let _items: Item[] = []
|
||||
@@ -330,7 +334,7 @@ function ProfileCard({
|
||||
onPress,
|
||||
}: {
|
||||
enabled: boolean
|
||||
profile: AppBskyActorDefs.ProfileView
|
||||
profile: bsky.profile.AnyProfileView
|
||||
moderationOpts: ModerationOpts
|
||||
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) {
|
||||
case 'none':
|
||||
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 {useTheme} from '#/alf'
|
||||
@@ -12,6 +13,7 @@ export const LabelText = TextField.LabelText
|
||||
|
||||
export function DateField({
|
||||
value,
|
||||
inputRef,
|
||||
onChangeDate,
|
||||
label,
|
||||
isInvalid,
|
||||
@@ -20,9 +22,9 @@ export function DateField({
|
||||
maximumDate,
|
||||
}: DateFieldProps) {
|
||||
const t = useTheme()
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const onChangeInternal = React.useCallback(
|
||||
const onChangeInternal = useCallback(
|
||||
(date: Date) => {
|
||||
setOpen(false)
|
||||
|
||||
@@ -32,11 +34,25 @@ export function DateField({
|
||||
[onChangeDate, setOpen],
|
||||
)
|
||||
|
||||
const onPress = React.useCallback(() => {
|
||||
useImperativeHandle(
|
||||
inputRef,
|
||||
() => ({
|
||||
focus: () => {
|
||||
Keyboard.dismiss()
|
||||
setOpen(true)
|
||||
},
|
||||
blur: () => {
|
||||
setOpen(false)
|
||||
},
|
||||
}),
|
||||
[],
|
||||
)
|
||||
|
||||
const onPress = useCallback(() => {
|
||||
setOpen(true)
|
||||
}, [])
|
||||
|
||||
const onCancel = React.useCallback(() => {
|
||||
const onCancel = useCallback(() => {
|
||||
setOpen(false)
|
||||
}, [])
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {Pressable, View} from 'react-native'
|
||||
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 {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||
import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays'
|
||||
@@ -60,16 +60,16 @@ export function DateFieldButton({
|
||||
onBlur={onBlur}
|
||||
style={[
|
||||
{
|
||||
paddingTop: 12,
|
||||
paddingBottom: 12,
|
||||
paddingLeft: 14,
|
||||
paddingRight: 14,
|
||||
borderColor: 'transparent',
|
||||
borderWidth: 2,
|
||||
},
|
||||
android({
|
||||
minHeight: 57.5,
|
||||
native({
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
}),
|
||||
web(a.py_md),
|
||||
a.flex_row,
|
||||
a.flex_1,
|
||||
a.w_full,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import {useCallback, useImperativeHandle} from 'react'
|
||||
import {Keyboard, View} from 'react-native'
|
||||
import DatePicker from 'react-native-date-picker'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
@@ -25,6 +25,7 @@ export const LabelText = TextField.LabelText
|
||||
*/
|
||||
export function DateField({
|
||||
value,
|
||||
inputRef,
|
||||
onChangeDate,
|
||||
testID,
|
||||
label,
|
||||
@@ -36,7 +37,7 @@ export function DateField({
|
||||
const t = useTheme()
|
||||
const control = Dialog.useDialogControl()
|
||||
|
||||
const onChangeInternal = React.useCallback(
|
||||
const onChangeInternal = useCallback(
|
||||
(date: Date | undefined) => {
|
||||
if (date) {
|
||||
const formatted = toSimpleDateString(date)
|
||||
@@ -46,6 +47,20 @@ export function DateField({
|
||||
[onChangeDate],
|
||||
)
|
||||
|
||||
useImperativeHandle(
|
||||
inputRef,
|
||||
() => ({
|
||||
focus: () => {
|
||||
Keyboard.dismiss()
|
||||
control.open()
|
||||
},
|
||||
blur: () => {
|
||||
control.close()
|
||||
},
|
||||
}),
|
||||
[control],
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<DateFieldButton
|
||||
|
||||
@@ -34,6 +34,7 @@ const Input = TextField.createInput(InputBase as unknown as typeof TextInput)
|
||||
|
||||
export function DateField({
|
||||
value,
|
||||
inputRef,
|
||||
onChangeDate,
|
||||
label,
|
||||
isInvalid,
|
||||
@@ -58,9 +59,9 @@ export function DateField({
|
||||
<TextField.Icon icon={CalendarDays} />
|
||||
<Input
|
||||
value={toSimpleDateString(value)}
|
||||
inputRef={inputRef as React.Ref<TextInput>}
|
||||
label={label}
|
||||
onChange={handleOnChange}
|
||||
onChangeText={() => {}}
|
||||
testID={testID}
|
||||
accessibilityHint={accessibilityHint}
|
||||
// @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 = {
|
||||
value: string | Date
|
||||
onChangeDate: (date: string) => void
|
||||
label: string
|
||||
inputRef?: React.Ref<DateFieldRef>
|
||||
isInvalid?: boolean
|
||||
testID?: string
|
||||
accessibilityHint?: string
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import {Keyboard, View} from 'react-native'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {toNiceDomain} from '#/lib/strings/url-helpers'
|
||||
@@ -41,9 +41,9 @@ export function HostingProvider({
|
||||
onSelect={onSelectServiceUrl}
|
||||
/>
|
||||
{minimal ? (
|
||||
<View style={[a.flex_row, a.align_center, a.flex_wrap]}>
|
||||
<View style={[a.flex_row, a.align_center, a.flex_wrap, a.gap_xs]}>
|
||||
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
|
||||
You are creating an account on{' '}
|
||||
<Trans>You are creating an account on</Trans>
|
||||
</Text>
|
||||
<Button
|
||||
label={toNiceDomain(serviceUrl)}
|
||||
@@ -52,7 +52,11 @@ export function HostingProvider({
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="tiny"
|
||||
style={[a.px_xs, {marginLeft: tokens.space.xs * -1}]}>
|
||||
style={[
|
||||
a.px_xs,
|
||||
{marginHorizontal: tokens.space.xs * -1},
|
||||
{paddingVertical: 0},
|
||||
]}>
|
||||
<ButtonText style={[a.text_sm]}>
|
||||
{toNiceDomain(serviceUrl)}
|
||||
</ButtonText>
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
android,
|
||||
applyFonts,
|
||||
atoms as a,
|
||||
ios,
|
||||
TextStyleProp,
|
||||
useAlf,
|
||||
useTheme,
|
||||
@@ -189,13 +190,13 @@ export function createInput(Component: typeof TextInput) {
|
||||
a.px_xs,
|
||||
{
|
||||
// paddingVertical doesn't work w/multiline - esb
|
||||
paddingTop: 12,
|
||||
paddingBottom: 13,
|
||||
lineHeight: a.text_md.fontSize * 1.1875,
|
||||
textAlignVertical: rest.multiline ? 'top' : undefined,
|
||||
minHeight: rest.multiline ? 80 : undefined,
|
||||
minWidth: 0,
|
||||
},
|
||||
ios({paddingTop: 12, paddingBottom: 13}),
|
||||
android(a.py_sm),
|
||||
// fix for autofill styles covering border
|
||||
web({
|
||||
paddingTop: 10,
|
||||
@@ -203,10 +204,6 @@ export function createInput(Component: typeof TextInput) {
|
||||
marginTop: 2,
|
||||
marginBottom: 2,
|
||||
}),
|
||||
android({
|
||||
paddingTop: 8,
|
||||
paddingBottom: 8,
|
||||
}),
|
||||
style,
|
||||
])
|
||||
|
||||
@@ -226,6 +223,7 @@ export function createInput(Component: typeof TextInput) {
|
||||
<>
|
||||
<Component
|
||||
accessibilityHint={undefined}
|
||||
hitSlop={HITSLOP_20}
|
||||
{...rest}
|
||||
accessibilityLabel={label}
|
||||
ref={refs}
|
||||
@@ -242,7 +240,6 @@ export function createInput(Component: typeof TextInput) {
|
||||
placeholder={placeholder || label}
|
||||
placeholderTextColor={t.palette.contrast_500}
|
||||
keyboardAppearance={t.name === 'light' ? 'light' : 'dark'}
|
||||
hitSlop={HITSLOP_20}
|
||||
style={flattened}
|
||||
/>
|
||||
|
||||
@@ -355,17 +352,9 @@ export function SuffixText({
|
||||
a.pr_sm,
|
||||
a.text_md,
|
||||
t.atoms.text_contrast_medium,
|
||||
{
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
web({
|
||||
marginTop: -2,
|
||||
}),
|
||||
ctx.hovered || ctx.focused
|
||||
? {
|
||||
color: t.palette.contrast_800,
|
||||
}
|
||||
: {},
|
||||
a.pointer_events_none,
|
||||
web([{marginTop: -2}, a.leading_snug]),
|
||||
(ctx.hovered || ctx.focused) && {color: t.palette.contrast_800},
|
||||
style,
|
||||
]}>
|
||||
{children}
|
||||
|
||||
@@ -11,14 +11,17 @@ import React from 'react'
|
||||
import {formatDistance, Locale} from 'date-fns'
|
||||
import {
|
||||
ca,
|
||||
cy,
|
||||
da,
|
||||
de,
|
||||
el,
|
||||
enGB,
|
||||
eo,
|
||||
es,
|
||||
eu,
|
||||
fi,
|
||||
fr,
|
||||
gd,
|
||||
gl,
|
||||
hi,
|
||||
hu,
|
||||
@@ -53,15 +56,18 @@ const locales: Record<AppLanguage, Locale | undefined> = {
|
||||
an: undefined,
|
||||
ast: undefined,
|
||||
ca,
|
||||
cy,
|
||||
da,
|
||||
de,
|
||||
el,
|
||||
['en-GB']: enGB,
|
||||
eo,
|
||||
es,
|
||||
eu,
|
||||
fi,
|
||||
fr,
|
||||
ga: undefined,
|
||||
gd,
|
||||
gl,
|
||||
hi,
|
||||
hu,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from 'react'
|
||||
import {AppBskyActorDefs} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
@@ -9,12 +8,13 @@ import {Shadow} from '#/state/cache/types'
|
||||
import {useProfileFollowMutationQueue} from '#/state/queries/profile'
|
||||
import {useRequireAuth} from '#/state/session'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import * as bsky from '#/types/bsky'
|
||||
|
||||
export function useFollowMethods({
|
||||
profile,
|
||||
logContext,
|
||||
}: {
|
||||
profile: Shadow<AppBskyActorDefs.ProfileViewBasic>
|
||||
profile: Shadow<bsky.profile.AnyProfileView>
|
||||
logContext: LogEvents['profile:follow']['logContext'] &
|
||||
LogEvents['profile:unfollow']['logContext']
|
||||
}) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user