Compare commits

..

26 Commits

Author SHA1 Message Date
Eric Bailey f056b81e32 Better proxy handling 2025-02-03 10:24:40 -06:00
Eric Bailey 0fc2668b5a Rename, update var in dev 2025-01-22 17:50:54 -06:00
Eric Bailey 9533aea792 Clean up 2025-01-22 17:45:12 -06:00
Eric Bailey 6fb52fabcc Ok maybe fix env 2025-01-22 17:00:19 -06:00
Eric Bailey 52bb9b04f1 Retain context in command 2025-01-22 15:56:21 -06:00
Eric Bailey 5911aebdf2 Cleanup dockerfile 2025-01-22 15:50:55 -06:00
Eric Bailey 868a991c55 Fix env 2025-01-22 15:08:35 -06:00
Eric Bailey eaddf13f05 Fix env 2025-01-22 15:07:57 -06:00
Eric Bailey 100d879713 Clean up env vars 2025-01-22 14:53:43 -06:00
Eric Bailey b308f09d4f Forgot to add proxy did 2025-01-22 14:38:57 -06:00
Eric Bailey 87ae4533d4 Add serve command 2025-01-22 13:59:20 -06:00
Eric Bailey d5ea9e0011 Ope fix path 2025-01-22 13:15:00 -06:00
Eric Bailey 386d04b098 Output env, fix dir 2025-01-22 12:45:29 -06:00
Eric Bailey 298f398a75 Add smallworld dockerfile 2025-01-22 11:37:16 -06:00
Eric Bailey 4f2f96c994 WIP rework env file 2025-01-22 11:15:26 -06:00
Eric Bailey e4af97f80d Update .env.example 2025-01-22 11:15:26 -06:00
rafael aa9f622f04 rename APPVIEW_PROXY -> APPVIEW_DID 2025-01-22 11:15:26 -06:00
Eric Bailey cad07f5dc2 Rm trending passthrough in dev 2025-01-22 11:15:26 -06:00
Eric Bailey 686b055d03 Add back locale compilation 2025-01-22 11:15:26 -06:00
Eric Bailey d376969a0d Configure proxy 2025-01-22 11:15:26 -06:00
Eric Bailey cea35fafd5 Add env to Dockerfile.dev 2025-01-22 11:15:26 -06:00
Eric Bailey 19fc4535b2 Use new env var 2025-01-22 11:15:26 -06:00
Eric Bailey 2eb19981ba Remove lingui postinstall for now 2025-01-22 11:15:26 -06:00
Eric Bailey 7aa97e244c Add Dockerfile.dev 2025-01-22 11:15:26 -06:00
Eric Bailey 7afefccdd0 Test commit 2025-01-22 11:15:26 -06:00
Eric Bailey f421821b06 Override service 2025-01-22 11:15:26 -06:00
434 changed files with 88302 additions and 155234 deletions
+25 -6
View File
@@ -1,8 +1,27 @@
# Copy this to `.env` and `.env.test` files
BITDRIFT_API_KEY=
SENTRY_AUTH_TOKEN=
EXPO_PUBLIC_LOG_LEVEL=debug
EXPO_PUBLIC_LOG_DEBUG=
# 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
# Optional app view proxy DID
EXPO_PUBLIC_APPVIEW_PROXY_DID=
# 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=
#
# Bluesky specific
#
# Chat service DID
EXPO_PUBLIC_CHAT_DID=
# Bitdrift
BITDRIFT_API_KEY=
# Sentry (deprecated)
SENTRY_AUTH_TOKEN=
+14 -2
View File
@@ -32,7 +32,6 @@ module.exports = {
'H6',
'P',
'Admonition',
'Admonition.Admonition',
],
impliedTextProps: [],
suggestedTextWrappers: {
@@ -81,6 +80,19 @@ 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.',
},
],
},
],
},
ignorePatterns: [
'**/__mocks__/*.ts',
@@ -92,9 +104,9 @@ module.exports = {
'*.lock',
'.husky',
'patches',
'bskyweb',
'*.html',
'bskyweb',
'bskyembed',
'src/locale/locales/_build/',
'src/locale/locales/**/*.js',
],
+2 -2
View File
@@ -14,7 +14,7 @@ on:
jobs:
build:
name: Build and Submit iOS
runs-on: macos-15
runs-on: macos-14
steps:
- name: Check for EXPO_TOKEN
run: >
@@ -52,7 +52,7 @@ jobs:
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '16.2'
xcode-version: '15.3'
- 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-15
runs-on: macos-14
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: '16.2'
xcode-version: '15.3'
- name: ☕️ Setup Cocoapods
uses: maxim-lobanov/setup-cocoapods@v1
@@ -1,49 +0,0 @@
name: Nightly Update Source Languages
on:
schedule:
- cron: '0 2 * * *' # run at 2 AM UTC
workflow_dispatch:
jobs:
extract-languages:
name: Nightly Update Source Languages
runs-on: ubuntu-latest
permissions:
# Give the default GITHUB_TOKEN write permission to commit and push the
# added or changed files to the repository.
contents: write
steps:
- name: Check out Git repository
uses: actions/checkout@v3
with:
ssh-key: ${{secrets.GH_ACTION_DEPLOY_KEY}}
- name: Install node
uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
- name: Yarn install
uses: Wandalen/wretry.action@master
with:
command: yarn --frozen-lockfile
attempt_limit: 3
attempt_delay: 2000
- name: Extract language strings
run: yarn intl:extract
- name: Create commit
uses: stefanzweifel/git-auto-commit-action@v5
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 }}
+92
View File
@@ -0,0 +1,92 @@
FROM golang:1.23-bullseye AS build-env
WORKDIR /usr/src/social-app
ENV DEBIAN_FRONTEND=noninteractive
#
# Environment: node
#
ENV NODE_VERSION=20
ENV NVM_DIR=/usr/share/nvm
#
# Environment: golang
#
ENV GODEBUG="netdns=go"
ENV GOOS="linux"
ENV GOARCH="amd64"
ENV CGO_ENABLED=1
ENV GOEXPERIMENT="loopvar"
#
# Evironment: app
#
# Optional app view proxy DID
ARG EXPO_PUBLIC_APPVIEW_PROXY_DID
# 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.
ARG EXPO_PUBLIC_BUNDLE_IDENTIFIER
ENV EXPO_PUBLIC_BUNDLE_IDENTIFIER=${EXPO_PUBLIC_BUNDLE_IDENTIFIER:-self-hosted-bluesky}
# The log level for the app's logger transports
ARG EXPO_PUBLIC_LOG_LEVEL
ENV EXPO_PUBLIC_LOG_LEVEL=${EXPO_PUBLIC_LOG_LEVEL:-info}
#
# Copy everything into the container.
#
COPY . .
#
# Install NVM
# Install Node
# Install Yarn
# Install dependencies
#
RUN mkdir --parents $NVM_DIR && \
wget \
--output-document=/tmp/nvm-install.sh \
https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh && \
bash /tmp/nvm-install.sh && \
\. "$NVM_DIR/nvm.sh" && \
nvm install $NODE_VERSION && \
nvm use $NODE_VERSION && \
npm install --global yarn && \
yarn && \
yarn intl:build && \
EXPO_PUBLIC_ENV=production \
EXPO_PUBLIC_BUNDLE_IDENTIFIER=$EXPO_PUBLIC_BUNDLE_IDENTIFIER \
EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H") \
EXPO_PUBLIC_APPVIEW_PROXY_DID=$EXPO_PUBLIC_APPVIEW_PROXY_DID \
EXPO_PUBLIC_LOG_LEVEL=$EXPO_PUBLIC_LOG_LEVEL \
yarn build-web
#
# Build the Go webserver binary
#
RUN cd bskyweb/ && \
go mod download && \
go mod verify && \
go build \
-v \
-trimpath \
-tags timetzdata \
-o /bskyweb \
./cmd/bskyweb
#
# Build the final image.
#
FROM debian:bullseye-slim
ENV GODEBUG=netdns=go
ENV TZ=Etc/UTC
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install --yes \
dumb-init \
ca-certificates
ENTRYPOINT ["dumb-init", "--"]
WORKDIR /bskyweb
COPY --from=build-env /bskyweb /usr/bin/bskyweb
CMD ["/usr/bin/bskyweb", "serve"]
LABEL org.opencontainers.image.source=https://github.com/bluesky-social/social-app
LABEL org.opencontainers.image.description="Self-hosted Bluesky web app"
LABEL org.opencontainers.image.licenses=MIT
+19
View File
@@ -0,0 +1,19 @@
FROM node:22.13-alpine3.20
WORKDIR /app
COPY package.json /app/package.json
COPY yarn.lock /app/yarn.lock
COPY /patches /app/patches
COPY /lingui.config.js /app/lingui.config.js
COPY /src/locale/locales /app/src/locale/locales
RUN pwd
RUN cd /app
RUN yarn
COPY . /app
ENV EXPO_PUBLIC_APPVIEW_PROXY_DID=${EXPO_PUBLIC_APPVIEW_PROXY_DID}
CMD ["yarn", "web"]
+2 -2
View File
@@ -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 20
nvm use 20
nvm install 18
nvm use 18
npm install --global yarn
+1 -1
View File
@@ -1,2 +1,2 @@
flows:
- "flows/**"
- "flows/*"
+1 -1
View File
@@ -17,7 +17,7 @@ appId: xyz.blueskyweb.app
- inputText: "Post with an image"
- tapOn:
id: "openGalleryBtn"
- tapOn: "Allow Full Access"
- tapOn: "Content warnings"
- tapOn: "Content warnings"
- tapOn: "Porn"
- tapOn:
+4 -2
View File
@@ -24,10 +24,12 @@ appId: xyz.blueskyweb.app
- inputText: "example@test.com"
- tapOn:
id: "passwordInput"
- inputText: "hunter22"
- hideKeyboard
- inputText: "hunter2"
- pressKey: Enter
- tapOn:
id: "nextBtn"
- tapOn:
id: "handleInput"
- inputText: "e2e-test"
- tapOn:
id: "nextBtn"
+1 -1
View File
@@ -160,7 +160,7 @@ appId: xyz.blueskyweb.app
- tapOn:
label: "Adds and removes users on curatelists from the profile"
id: "bottomBarSearchBtn"
- tapOn: "Search for posts, users, or feeds"
- tapOn: "Search"
- inputText: "bob"
- tapOn:
id: "searchAutoCompleteResult-bob.test"
+1 -1
View File
@@ -16,7 +16,7 @@ appId: xyz.blueskyweb.app
- tapOn:
id: "profileCardButton"
- tapOn:
id: "profilePager-selector-5"
id: "profilePager-selector-4"
- tapOn: "alice-favs"
- tapOn: "Pin to Home"
- tapOn:
+1 -1
View File
@@ -21,7 +21,7 @@ appId: xyz.blueskyweb.app
label: "Feeds button disappears after pinning a feed"
id: "bottomBarProfileBtn"
- tapOn:
id: "profilePager-selector-5"
id: "profilePager-selector-4"
- tapOn: "alice-favs"
- tapOn: "Pin to Home"
- tapOn:
-20
View File
@@ -1,20 +0,0 @@
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%"
+1 -1
View File
@@ -15,7 +15,7 @@ appId: xyz.blueskyweb.app
id: "bottomBarSearchBtn"
- tapOn:
id: "bottomBarSearchBtn"
- tapOn: "Search for posts, users, or feeds"
- tapOn: "Search"
- inputText: "b"
- tapOn:
id: "searchAutoCompleteResult-bob.test"
@@ -1,24 +0,0 @@
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"
@@ -1,24 +0,0 @@
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"
@@ -1,35 +0,0 @@
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"
@@ -1,30 +0,0 @@
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"
+2 -2
View File
@@ -12,8 +12,8 @@ appId: xyz.blueskyweb.app
# Navigate to another user profile via autocomplete
- tapOn:
id: "bottomBarSearchBtn"
- assertVisible: "Search for posts, users, or feeds"
- tapOn: "Search for posts, users, or feeds"
- assertVisible: "Search"
- tapOn: "Search"
- inputText: "b"
- tapOn:
id: "searchAutoCompleteResult-bob.test"
-42
View File
@@ -1,42 +0,0 @@
import {IsValidHandle, validateServiceHandle} from '#/lib/strings/handles'
describe('handle validation', () => {
const valid = [
['ali', 'bsky.social'],
['alice', 'bsky.social'],
['a-lice', 'bsky.social'],
['a-----lice', 'bsky.social'],
['123', 'bsky.social'],
['123456789012345678', 'bsky.social'],
['alice', 'custom-pds.com'],
['alice', 'my-custom-pds-with-long-name.social'],
['123456789012345678', 'my-custom-pds-with-long-name.social'],
]
it.each(valid)(`should be valid: %s.%s`, (handle, service) => {
const result = validateServiceHandle(handle, service)
expect(result.overall).toEqual(true)
})
const invalid = [
['al', 'bsky.social', 'frontLength'],
['-alice', 'bsky.social', 'hyphenStartOrEnd'],
['alice-', 'bsky.social', 'hyphenStartOrEnd'],
['%%%', 'bsky.social', 'handleChars'],
['1234567890123456789', 'bsky.social', 'frontLength'],
[
'1234567890123456789',
'my-custom-pds-with-long-name.social',
'frontLength',
],
['al', 'my-custom-pds-with-long-name.social', 'frontLength'],
['a'.repeat(300), 'toolong.com', 'totalLength'],
] satisfies [string, string, keyof IsValidHandle][]
it.each(invalid)(
`should be invalid: %s.%s due to %s`,
(handle, service, expectedError) => {
const result = validateServiceHandle(handle, service)
expect(result.overall).toEqual(false)
expect(result[expectedError]).toEqual(false)
},
)
})
-9
View File
@@ -75,21 +75,15 @@ module.exports = function (config) {
'an',
'ast',
'ca',
'cy',
'da',
'de',
'el',
'eo',
'es',
'eu',
'fi',
'fr',
'ga',
'gd',
'gl',
'hi',
'hu',
'ia',
'id',
'it',
'ja',
@@ -101,7 +95,6 @@ module.exports = function (config) {
'pt-BR',
'ro',
'ru',
'sv',
'th',
'tr',
'uk',
@@ -240,13 +233,11 @@ 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',
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="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>

Before

Width:  |  Height:  |  Size: 363 B

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

Before

Width:  |  Height:  |  Size: 454 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M7 2a1 1 0 0 1 1 1v2a1 1 0 0 1-2 0V3a1 1 0 0 1 1-1Zm4 0a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V3a1 1 0 0 1 1-1Zm4 0a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V3a1 1 0 0 1 1-1ZM4 9a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2h.5a3.5 3.5 0 1 1 0 7H18v4a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V9Zm12 0H6v11h10V9Zm2 5h.5a1.5 1.5 0 0 0 0-3H18v3Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 437 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="M6.5 4.999Zm0 0a.1.1 0 0 1-.016.024.2.2 0 0 1-.152.043q.086.014.168.032V7.5a1 1 0 0 1-.33.742c-.543.49-.917 1.176-1.23 2.036a1 1 0 0 1-.94.657H3V14h1a1 1 0 0 1 .866.5c.436.754.879 1.195 1.637 1.636A1 1 0 0 1 7 17v2h2a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2h2v-2.537a1 1 0 0 1 .332-.744A4.98 4.98 0 0 0 19 11.999 5 5 0 0 0 14 7H9.474a1 1 0 0 1-.893-.55v-.001l-.001-.002-.005-.008L9.474 6l-.9.438h.001v.002l.002.001.001.003.002.003v.003a2.6 2.6 0 0 0-.443-.538c-.319-.298-.839-.648-1.637-.814V5Zm2.082 1.452ZM9.5 4.447c.211.196.379.387.508.553H14a7 7 0 0 1 6.72 8.96 1.003 1.003 0 0 0 1.146-1.46 1 1 0 1 1 1.731-1 3 3 0 0 1-3.714 4.286A7 7 0 0 1 19 16.888V19a2 2 0 0 1-2 2h-2a2 2 0 0 1-2-2h-2a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-1.45A5.8 5.8 0 0 1 3.448 16H3a2 2 0 0 1-2-2v-3.065a2 2 0 0 1 2-2h.324c.286-.649.657-1.291 1.176-1.852V5c0-1.047.89-2.12 2.161-1.907 1.33.223 2.248.805 2.839 1.354ZM8.25 12a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -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 4c0-1.012.894-2.187 2.237-1.846a5.002 5.002 0 0 1 3.218 7.119 4 4 0 0 1 2.345 4.976A3.501 3.501 0 0 1 18.5 21h-13a3.5 3.5 0 0 1-1.3-6.75 4 4 0 0 1 2.052-4.848A4 4 0 0 1 10 4h2.001Zm-4.187 7.009A2 2 0 0 0 6 13c0 .366.098.706.271 1H12a1 1 0 1 1 0 2H5.5a1.5 1.5 0 0 0 0 3h13a1.5 1.5 0 1 0 0-3H17a1 1 0 1 1 0-2h.729A1.96 1.96 0 0 0 18 13a2 2 0 0 0-2-2h-2a1 1 0 1 1 0-2h1.236a3.002 3.002 0 0 0-1.243-4.832A2 2 0 0 1 12 6h-2a2 2 0 0 0-1.728 3.009H9a1 1 0 0 1 0 2H7.813Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 610 B

-1
View File
@@ -68,7 +68,6 @@ 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 -7
View File
@@ -1,15 +1,9 @@
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 initSystemColorMode() {
export function initColorMode() {
applyTheme(
window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
-11
View File
@@ -9,14 +9,3 @@
: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>");
}
}
-1
View File
@@ -13,7 +13,6 @@ export function labelsToInfo(
return 'Adult Content'
case 'nudity':
return 'Non-sexual Nudity'
case 'gore':
case 'graphic-media':
return 'Graphic Media'
default:
+19 -58
View File
@@ -1,16 +1,12 @@
import '../index.css'
import {AppBskyFeedDefs, AppBskyFeedPost, AtpAgent, AtUri} from '@atproto/api'
import {AppBskyFeedDefs, AppBskyFeedPost, AtUri, BskyAgent} 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 {
assertColorModeValues,
ColorModeValues,
initSystemColorMode,
} from '../color-mode'
import {initColorMode} from '../color-mode'
import {Container} from '../components/container'
import {Link} from '../components/link'
import {Post} from '../components/post'
@@ -26,9 +22,9 @@ export const EMBED_SCRIPT = `${EMBED_SERVICE}/static/embed.js`
const root = document.getElementById('app')
if (!root) throw new Error('No root element')
initSystemColorMode()
initColorMode()
const agent = new AtpAgent({
const agent = new BskyAgent({
service: 'https://public.api.bsky.app',
})
@@ -36,7 +32,6 @@ 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>(
@@ -125,50 +120,24 @@ function LandingPage() {
<h1 className="text-4xl font-bold text-center">Embed a Bluesky Post</h1>
<div className="flex flex-col w-full max-w-[600px] gap-6">
<input
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>
<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}
/>
<img src={arrowBottom} className="w-6 dark:invert" />
{loading ? (
<div className={`${colorMode} w-full max-w-[600px]`}>
<div className="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} colorMode={colorMode} />
)}
<div className={colorMode}>
{!error && thread && <Post thread={thread} key={thread.post.uri} />}
</div>
{!error && thread && uri && <Snippet thread={thread} />}
{!error && thread && <Post thread={thread} key={thread.post.uri} />}
{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>
@@ -199,13 +168,7 @@ function Skeleton() {
)
}
function Snippet({
thread,
colorMode,
}: {
thread: AppBskyFeedDefs.ThreadViewPost
colorMode: ColorModeValues
}) {
function Snippet({thread}: {thread: AppBskyFeedDefs.ThreadViewPost}) {
const ref = useRef<HTMLInputElement>(null)
const [copied, setCopied] = useState(false)
@@ -241,11 +204,9 @@ function Snippet({
// 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,
)}" data-bluesky-embed-color-mode="${escapeHtml(
colorMode,
)}"><p lang="${escapeHtml(lang)}">${escapeHtml(record.text)}${
)}" data-bluesky-cid="${escapeHtml(thread.post.cid)}"><p lang="${escapeHtml(
lang,
)}">${escapeHtml(record.text)}${
record.embed
? `<br><br><a href="${escapeHtml(href)}">[image or embed]</a>`
: ''
@@ -256,7 +217,7 @@ function Snippet({
)}</a>) <a href="${escapeHtml(href)}">${escapeHtml(
niceDate(thread.post.indexedAt),
)}</a></blockquote><script async src="${EMBED_SCRIPT}" charset="utf-8"></script>`
}, [thread, colorMode])
}, [thread])
return (
<div className="flex gap-2 w-full">
+2 -18
View File
@@ -4,7 +4,7 @@ import {AppBskyFeedDefs, AtpAgent} from '@atproto/api'
import {h, render} from 'preact'
import logo from '../../assets/logo.svg'
import {applyTheme, initSystemColorMode} from '../color-mode'
import {initColorMode} from '../color-mode'
import {Container} from '../components/container'
import {Link} from '../components/link'
import {Post} from '../components/post'
@@ -22,23 +22,7 @@ if (!uri) {
throw new Error('No uri in path')
}
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
}
initColorMode()
agent
.getPostThread({
+3 -5
View File
@@ -1,10 +1,9 @@
import {Express} from 'express'
import {AppContext} from '../context.js'
import {default as createShortLink} from './createShortLink.js'
import {default as create} from './create.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'
@@ -12,8 +11,7 @@ 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 = redirect(ctx, app) // GET /redirect
app = createShortLink(ctx, app) // POST /link
app = shortLink(ctx, app) // GET /:linkId (should go last due to permissive matching)
app = create(ctx, app) // POST /link
app = redirect(ctx, app) // GET /:linkId (should go last due to permissive matching)
return app
}
+31 -24
View File
@@ -6,40 +6,47 @@ 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(
'/redirect',
'/:linkId',
handler(async (req, res) => {
let link = req.query.u
const linkId = req.params.linkId
const contentType = req.accepts(['html', 'json'])
assert(
typeof link === 'string',
'express guarantees link query parameter is a string',
typeof linkId === 'string',
'express guarantees id parameter is a string',
)
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
) {
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()
}),
-54
View File
@@ -1,54 +0,0 @@
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()
}),
)
}
-1
View File
@@ -97,7 +97,6 @@ const hideAvatarLabels = new Set([
'nudity',
'sexual-figurative',
'graphic-media',
'gore',
'self-harm',
'sensitive',
'security',
-14
View File
@@ -94,20 +94,6 @@ 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"},
},
},
},
}
+7 -20
View File
@@ -63,8 +63,6 @@ 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()
@@ -206,23 +204,13 @@ func serve(cctx *cli.Context) error {
return http.FS(fsys)
}())
// 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("/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")
})
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 {
@@ -295,7 +283,6 @@ 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)
-3
View File
@@ -1,3 +0,0 @@
# 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: /
+1
View File
@@ -8,6 +8,7 @@
Preconnect to essential domains
-->
<link rel="preconnect" href="https://bsky.social">
<link rel="preconnect" href="https://bsky.network">
<title>{%- block head_title -%}Bluesky{%- endblock -%}</title>
<!-- Hello Humans! API docs at https://atproto.com -->
-21
View File
@@ -1,21 +0,0 @@
{
"project_id": "1",
"base_url": "https://bluesky.api.crowdin.com",
"base_path": ".",
"preserve_hierarchy": true,
"files": [
{
"source": "/src/locale/locales/en/messages.po",
"translation": "/src/locale/locales/%two_letters_code%/messages.po",
"languages_mapping": {
"two_letters_code": {
"pt-BR": "pt-BR",
"en-GB": "en-GB",
"zh-CN": "zh-CN",
"zh-TW": "zh-TW",
"zh-HK": "zh-HK"
}
}
}
]
}
+8 -59
View File
@@ -1,67 +1,11 @@
# 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 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).
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)
## Tools
- We use Crowdin to manage translations.
- 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
Much of the app is translated by community contributions. (We <3 our translators!) If you want to participate in the translation of the app, read this section.
### Using Crowdin
[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.
All translations must be approved by proof-readers before they are accepted into the app.
### Using other platforms
You may contribute PRs separately from Crowdin, however we strongly recommend using Crowdin to avoid conflicts.
### Code of conduct on Crowdin
Please treat everyone with respect. Proof-readers are given final say on translations. Translators who frequently come into conflict with other translators, or who contribute noticably incorrect translations, will have their membership to the Crowdin project revoked.
### Adding a new language
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.
## Maintainers
Install the [Crowdin CLI](https://crowdin.github.io/crowdin-cli/). You will need to [configure your API token](https://crowdin.github.io/crowdin-cli/configuration) to access the project.
### English source-file sync with Crowdin
Every night, a GitHub action will run `yarn intl:extract` to update the english `messages.po` file. This will be automatically synced with Crowdin. Crowdin should notify all subscribed users of new translations.
### Release process
1. Pull main and create a branch.
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. Run `yarn intl:push` to sync Crowdin with the state of the repo.
### Testing the translations in Crowdin
You can run `yarn intl:pull` to pull the currently-approved translations from Crowdin.
## Developers
We are using Lingui to manage translations. You can find the documentation [here](https://lingui.dev/).
### Adding new strings
When adding a new string, do it as follows:
```jsx
// Before
@@ -117,7 +61,6 @@ So the workflow is as follows:
6. Enjoy translated app!
### Common pitfalls
These pitfalls are memoization pitfalls that will cause the components to not re-render when the locale is changed -- causing stale translations to be shown.
```jsx
@@ -168,3 +111,9 @@ export function Welcome() {
return <div>{welcome}</div>;
}
```
### Credits
Please check each individual `messages.po` file for the credits of the translators. We are very grateful for their help!
If you would like to translate the Bluesky app into your language, please open a PR or issue on this repo.
+1 -1
View File
@@ -119,5 +119,5 @@ jest.mock('statsig-react-native-expo', () => ({
},
}))
jest.mock('../src/logger/bitdrift/lib', () => ({}))
jest.mock('../src/lib/bitdrift', () => ({}))
jest.mock('../src/lib/statsig/statsig', () => ({}))
-7
View File
@@ -5,22 +5,16 @@ module.exports = {
'an',
'ast',
'ca',
'cy',
'da',
'de',
'el',
'en-GB',
'eo',
'es',
'eu',
'fi',
'fr',
'ga',
'gd',
'gl',
'hi',
'hu',
'ia',
'id',
'it',
'ja',
@@ -32,7 +26,6 @@ module.exports = {
'pt-BR',
'ro',
'ru',
'sv',
'th',
'tr',
'uk',
@@ -31,13 +31,9 @@ class BottomSheetView(
private lateinit var dialogRootViewGroup: DialogRootViewGroup
private var eventDispatcher: EventDispatcher? = null
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 screenHeight =
context.resources.displayMetrics.heightPixels
.toFloat()
private val onAttemptDismiss by EventDispatcher()
private val onSnapPointChange by EventDispatcher()
@@ -67,12 +63,12 @@ class BottomSheetView(
}
}
var maxHeight = this.safeScreenHeight
var maxHeight = this.screenHeight
set(value) {
val px = dpToPx(value)
field =
if (px > this.safeScreenHeight) {
this.safeScreenHeight
if (px > this.screenHeight) {
this.screenHeight
} else {
px
}
@@ -157,19 +153,6 @@ 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
@@ -189,12 +172,12 @@ class BottomSheetView(
val behavior = BottomSheetBehavior.from(it)
behavior.state = BottomSheetBehavior.STATE_HIDDEN
behavior.isFitToContents = true
behavior.halfExpandedRatio = getHalfExpandedRatio(contentHeight)
behavior.halfExpandedRatio = this.clampRatio(this.getTargetHeight() / this.screenHeight)
behavior.skipCollapsed = true
behavior.isDraggable = true
behavior.isHideable = true
if (contentHeight >= this.safeScreenHeight || this.minHeight >= this.safeScreenHeight) {
if (contentHeight >= this.screenHeight || this.minHeight >= this.screenHeight) {
behavior.state = BottomSheetBehavior.STATE_EXPANDED
this.selectedSnapPoint = 2
} else {
@@ -244,11 +227,11 @@ class BottomSheetView(
bottomSheet?.let {
val behavior = BottomSheetBehavior.from(it)
behavior.halfExpandedRatio = getHalfExpandedRatio(contentHeight)
behavior.halfExpandedRatio = this.clampRatio(this.getTargetHeight() / this.screenHeight)
if (contentHeight > this.safeScreenHeight && behavior.state != BottomSheetBehavior.STATE_EXPANDED) {
if (contentHeight > this.screenHeight && behavior.state != BottomSheetBehavior.STATE_EXPANDED) {
behavior.state = BottomSheetBehavior.STATE_EXPANDED
} else if (contentHeight < this.safeScreenHeight && behavior.state != BottomSheetBehavior.STATE_HALF_EXPANDED) {
} else if (contentHeight < this.screenHeight && behavior.state != BottomSheetBehavior.STATE_HALF_EXPANDED) {
behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED
}
}
@@ -1,17 +1,14 @@
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'
@@ -84,10 +81,12 @@ 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,
@@ -100,70 +99,39 @@ export class BottomSheetNativeComponent extends React.Component<
return (
<Portal>
<BottomSheetNativeComponentInner
{...this.props}
nativeViewRef={this.ref}
<NativeView
{...rest}
onStateChange={this.onStateChange}
extraStyles={extraStyles}
onLayout={e => {
const {height} = e.nativeEvent.layout
this.setState({viewHeight: height})
this.updateLayout()
ref={this.ref}
style={{
position: 'absolute',
height: screenHeight,
width: '100%',
}}
/>
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>
)
}
+13 -15
View File
@@ -1,11 +1,11 @@
{
"name": "bsky.app",
"version": "1.99.1",
"version": "1.97.0",
"private": true,
"engines": {
"node": ">=20"
},
"packageManager": "yarn@1.22.22",
"packageManager": "yarn@1.22.19",
"scripts": {
"prepare": "is-ci || husky install",
"postinstall": "patch-package && yarn intl:compile",
@@ -42,12 +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:all && yarn intl:compile",
"intl:extract": "lingui extract --clean --locale en",
"intl:extract:all": "lingui extract --clean",
"intl:build": "yarn intl:extract && yarn intl:compile",
"intl:extract": "lingui extract",
"intl:compile": "lingui compile",
"intl:pull": "crowdin download translations --verbose -b main",
"intl:push": "crowdin push translations --verbose -b main",
"nuke": "rm -rf ./node_modules && rm -rf ./ios && rm -rf ./android",
"update-extensions": "bash scripts/updateExtensions.sh",
"export": "npx expo export",
@@ -57,8 +54,8 @@
"icons:optimize": "svgo -f ./assets/icons"
},
"dependencies": {
"@atproto/api": "^0.14.7",
"@bitdrift/react-native": "^0.6.8",
"@atproto/api": "^0.13.28",
"@bitdrift/react-native": "^0.6.2",
"@braintree/sanitize-url": "^6.0.2",
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
"@emoji-mart/react": "^1.1.1",
@@ -79,10 +76,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.5",
"@radix-ui/react-dropdown-menu": "2.1.6",
"@radix-ui/react-dismissable-layer": "^1.1.1",
"@radix-ui/react-dropdown-menu": "2.0.1",
"@radix-ui/react-focus-guards": "^1.1.1",
"@radix-ui/react-focus-scope": "^1.1.2",
"@radix-ui/react-focus-scope": "^1.1.0",
"@react-native-async-storage/async-storage": "1.23.1",
"@react-native-menu/menu": "^1.1.7",
"@react-native-picker/picker": "2.10.2",
@@ -124,7 +121,7 @@
"eventemitter3": "^5.0.1",
"expo": "~52.0.19",
"expo-application": "^6.0.1",
"expo-blur": "^14.0.3",
"expo-blur": "^14.0.1",
"expo-build-properties": "^0.13.1",
"expo-camera": "~16.0.7",
"expo-clipboard": "^7.0.0",
@@ -173,7 +170,7 @@
"react-dom": "^18.2.0",
"react-image-crop": "^11.0.7",
"react-keyed-flatten-children": "^3.0.0",
"react-native": "0.76.6",
"react-native": "0.76.3",
"react-native-compressor": "1.10.3",
"react-native-date-picker": "^5.0.7",
"react-native-drawer-layout": "^4.1.1",
@@ -212,7 +209,7 @@
"zod": "^3.20.2"
},
"devDependencies": {
"@atproto/dev-env": "^0.3.87",
"@atproto/dev-env": "^0.3.67",
"@babel/core": "^7.26.0",
"@babel/preset-env": "^7.26.0",
"@babel/runtime": "^7.26.0",
@@ -271,6 +268,7 @@
"@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",
-34
View File
@@ -1,34 +0,0 @@
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
}
-70
View File
@@ -1,70 +0,0 @@
const {withAppBuildGradle} = require('@expo/config-plugins')
/**
* A Config Plugin to disable bundle compression in Android build.gradle.
* @param {import('@expo/config-plugins').ConfigPlugin} config
* @returns {import('@expo/config-plugins').ConfigPlugin}
*/
module.exports = function withNoBundleCompression(config) {
return withAppBuildGradle(config, androidConfig => {
let buildGradle = androidConfig.modResults.contents
const hasAndroidResources = buildGradle.includes('androidResources {')
const hasNoCompress = buildGradle.includes('noCompress')
if (hasAndroidResources) {
if (hasNoCompress) {
if (
buildGradle.includes('noCompress += ["bundle"]') ||
buildGradle.includes("noCompress += 'bundle'") ||
buildGradle.includes('noCompress += "bundle"')
) {
return androidConfig
}
const lines = buildGradle.split('\n')
const modifiedLines = lines.map(line => {
if (line.trim().startsWith('noCompress')) {
if (line.includes('+=')) {
return line.replace(/\]/, ', "bundle"]')
} else if (line.includes('=')) {
return line.replace('=', '+= ["bundle",') + ']'
}
}
return line
})
androidConfig.modResults.contents = modifiedLines.join('\n')
} else {
const androidResources = buildGradle.indexOf('androidResources {')
if (androidResources === -1) {
throw new Error(
`Cannot find androidResources { block in build.gradle!`,
)
}
const insertPosition = buildGradle.indexOf('\n', androidResources) + 1
const newContent =
buildGradle.slice(0, insertPosition) +
' noCompress += ["bundle"]\n' +
buildGradle.slice(insertPosition)
androidConfig.modResults.contents = newContent
}
} else {
const androidBlock = buildGradle.indexOf('android {')
if (androidBlock === -1) {
throw new Error(`Cannot find android { block in build.gradle!`)
}
const insertPosition = buildGradle.indexOf('\n', androidBlock) + 1
const newContent =
buildGradle.slice(0, insertPosition) +
' androidResources {\n' +
' noCompress += ["bundle"]\n' +
' }\n' +
buildGradle.slice(insertPosition)
androidConfig.modResults.contents = newContent
}
return androidConfig
})
}
+3 -3
View File
@@ -1,6 +1,6 @@
import 'react-native-url-polyfill/auto'
import '#/logger/sentry/setup'
import '#/logger/bitdrift/setup'
import '#/lib/sentry' // must be near top
import '#/lib/bitdrift' // must be near top
import '#/view/icons'
import React, {useEffect, useState} from 'react'
@@ -118,7 +118,7 @@ function InnerApp() {
useEffect(() => {
return listenSessionDropped(() => {
Toast.show(
_(msg`Sorry! Your session expired. Please sign in again.`),
_(msg`Sorry! Your session expired. Please log in again.`),
'info',
)
})
+2 -2
View File
@@ -1,4 +1,4 @@
import '#/logger/sentry/setup' // must be near top
import '#/lib/sentry' // 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 sign in again.`),
_(msg`Sorry! Your session expired. Please log in again.`),
'info',
)
})
+4 -29
View File
@@ -32,7 +32,6 @@ 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'
@@ -69,10 +68,8 @@ 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'
import {PostLikedByScreen} from '#/screens/Post/PostLikedBy'
import {PostQuotesScreen} from '#/screens/Post/PostQuotes'
import {PostRepostedByScreen} from '#/screens/Post/PostRepostedBy'
@@ -93,7 +90,6 @@ 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'
@@ -159,14 +155,6 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
getComponent={() => ModerationBlockedAccounts}
options={{title: title(msg`Blocked Accounts`), requireAuth: true}}
/>
<Stack.Screen
name="ModerationInteractionSettings"
getComponent={() => ModerationInteractionSettings}
options={{
title: title(msg`Post Interaction Settings`),
requireAuth: true,
}}
/>
<Stack.Screen
name="Settings"
getComponent={() => SettingsScreen}
@@ -210,13 +198,6 @@ 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}
@@ -412,11 +393,6 @@ 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}
@@ -736,16 +712,15 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
linking={LINKING}
theme={theme}
onStateChange={() => {
logger.metric('router:navigate', {
from: prevLoggedRouteName.current,
})
prevLoggedRouteName.current = getCurrentRouteName()
const routeName = getCurrentRouteName()
if (routeName === 'Notifications') {
logEvent('router:navigate:notifications', {})
}
}}
onReady={() => {
attachRouteToLogEvents(getCurrentRouteName)
logModuleInitTime()
onReady()
logger.metric('router:navigate', {})
}}>
{children}
</NavigationContainer>
+3 -57
View File
@@ -31,18 +31,6 @@ 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,
},
@@ -62,15 +50,9 @@ export const atoms = {
overflow_hidden: {
overflow: 'hidden',
},
/**
* @platform web
*/
overflow_auto: web({
overflow: 'auto',
}),
/*
* Width & Height
* Width
*/
w_full: {
width: '100%',
@@ -81,17 +63,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
* the screen and extend beyond.
*/
// @ts-ignore - web only minHeight string
util_screen_outer: [
web({
minHeight: '100vh',
@@ -99,7 +76,7 @@ export const atoms = {
native({
height: '100%',
}),
] as StyleProp<ViewStyle>,
] as ViewStyle,
/*
* Theme-independent bg colors
@@ -111,9 +88,6 @@ export const atoms = {
/*
* Border radius
*/
rounded_0: {
borderRadius: 0,
},
rounded_2xs: {
borderRadius: tokens.borderRadius._2xs,
},
@@ -991,34 +965,6 @@ export const atoms = {
transitionDelay: '50ms',
}),
/*
* Animaations
*/
fade_in: web({
animation: 'fadeIn ease-out 0.15s',
}),
fade_out: web({
animation: 'fadeOut ease-out 0.15s',
}),
zoom_in: web({
animation: 'zoomIn ease-out 0.1s',
}),
zoom_out: web({
animation: 'zoomOut ease-out 0.1s',
}),
slide_in_left: web({
// exponential easing function
animation: 'slideInLeft cubic-bezier(0.16, 1, 0.3, 1) 0.5s',
}),
slide_out_left: web({
animation: 'slideOutLeft ease-in 0.15s',
animationFillMode: 'forwards',
}),
// special composite animation for dialogs
zoom_fade_in: web({
animation: 'zoomIn ease-out 0.1s, fadeIn ease-out 0.1s',
}),
/**
* {@link Layout.SCROLLBAR_OFFSET}
*/
-15
View File
@@ -26,18 +26,3 @@ 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,
}
}
+1 -1
View File
@@ -62,7 +62,7 @@ export function AccountList({
testID="chooseAddAccountBtn"
style={[a.flex_1]}
onPress={pendingDid ? undefined : onPressAddAccount}
label={_(msg`Sign in to account that is not listed`)}>
label={_(msg`Login to account that is not listed`)}>
{({hovered, pressed}) => (
<View
style={[
+10 -18
View File
@@ -2,7 +2,6 @@ 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'
@@ -50,29 +49,22 @@ export function Text({
return (
<BaseText
{...rest}
style={[a.flex_1, a.text_sm, a.leading_snug, a.pr_md, style]}>
style={[
a.flex_1,
a.text_sm,
a.leading_snug,
{
paddingTop: 1,
},
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_1, a.flex_row, a.align_center, a.gap_sm]}>
{children}
</View>
)
return <View style={[a.flex_row, a.gap_sm]}>{children}</View>
}
export function Outer({
+11 -41
View File
@@ -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: bsky.profile.AnyProfileView[]
profiles: string[]
size?: number
numPending?: number
backgroundColor?: string
}) {
const halfSize = size / 2
const {data, error} = useProfilesQuery({handles: profiles})
const t = useTheme()
const moderationOpts = useModerationOpts()
const isPending = (numPending && profiles.length === 0) || !moderationOpts
if (error) {
console.error(error)
return null
}
const isPending = !data || !moderationOpts
const items = isPending
? Array.from({length: numPending ?? profiles.length}).map((_, i) => ({
? Array.from({length: profiles.length}).map((_, i) => ({
key: i,
profile: null,
moderation: null,
}))
: profiles.map(item => ({
: data.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: backgroundColor ?? t.atoms.bg.backgroundColor,
borderColor: t.atoms.bg.backgroundColor,
borderRadius: 999,
zIndex: 3 - i,
},
@@ -74,33 +74,3 @@ 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}
/>
)
}
-1
View File
@@ -14,7 +14,6 @@ export const Context = React.createContext<DialogContextProps>({
nativeSnapPoint: BottomSheetSnapPoint.Hidden,
disableDrag: false,
setDisableDrag: () => {},
isWithinDialog: false,
})
export function useDialogContext() {
+13 -23
View File
@@ -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, tokens, useTheme} from '#/alf'
import {atoms as a, 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,7 +154,6 @@ export function Outer({
nativeSnapPoint: snapPoint,
disableDrag,
setDisableDrag,
isWithinDialog: true,
}),
[close, snapPoint, disableDrag, setDisableDrag],
)
@@ -169,9 +168,7 @@ export function Outer({
onStateChange={onStateChange}
disableDrag={disableDrag}>
<Context.Provider value={context}>
<View testID={testID} style={[a.relative]}>
{children}
</View>
<View testID={testID}>{children}</View>
</Context.Provider>
</BottomSheet>
)
@@ -199,7 +196,7 @@ export function Inner({children, style, header}: DialogInnerProps) {
export const ScrollableInner = React.forwardRef<ScrollView, DialogInnerProps>(
function ScrollableInner(
{children, contentContainerStyle, header, ...props},
{children, style, contentContainerStyle, header, ...props},
ref,
) {
const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext()
@@ -219,21 +216,13 @@ export const ScrollableInner = React.forwardRef<ScrollView, DialogInnerProps>(
[],
)
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 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
const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
if (!isAndroid) {
@@ -249,6 +238,7 @@ export const ScrollableInner = React.forwardRef<ScrollView, DialogInnerProps>(
return (
<KeyboardAwareScrollView
style={[style]}
contentContainerStyle={[
a.pt_2xl,
a.px_xl,
@@ -326,7 +316,7 @@ export function Handle() {
style={[
a.rounded_sm,
{
top: tokens.space._2xl / 2 - 2.5,
top: 10,
width: 35,
height: 5,
alignSelf: 'center',
+21 -25
View File
@@ -15,7 +15,6 @@ import {FocusScope} from '@radix-ui/react-focus-scope'
import {RemoveScrollBar} from 'react-remove-scroll-bar'
import {logger} from '#/logger'
import {useA11y} from '#/state/a11y'
import {useDialogStateControlContext} from '#/state/dialogs'
import {atoms as a, flatten, useBreakpoints, useTheme, web} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
@@ -41,7 +40,6 @@ export function Outer({
children,
control,
onClose,
webOptions,
}: React.PropsWithChildren<DialogOuterProps>) {
const {_} = useLingui()
const {gtMobile} = useBreakpoints()
@@ -97,7 +95,6 @@ export function Outer({
nativeSnapPoint: 0,
disableDrag: false,
setDisableDrag: () => {},
isWithinDialog: true,
}),
[close],
)
@@ -117,26 +114,20 @@ export function Outer({
web(a.fixed),
a.inset_0,
a.z_10,
a.px_xl,
webOptions?.alignCenter ? a.justify_center : undefined,
a.align_center,
{
overflowY: 'auto',
paddingVertical: gtMobile ? '10vh' : a.pt_xl.paddingTop,
},
gtMobile ? a.p_lg : a.p_md,
{overflowY: 'auto'},
]}>
<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,
web({minHeight: '60vh', position: 'static'}),
{
minHeight: web('calc(90vh - 36px)') || undefined,
},
]}>
{children}
</View>
@@ -161,7 +152,6 @@ export function Inner({
const t = useTheme()
const {close} = React.useContext(Context)
const {gtMobile} = useBreakpoints()
const {reduceMotionEnabled} = useA11y()
useFocusGuards()
return (
<FocusScope loop asChild trapped>
@@ -171,7 +161,7 @@ export function Inner({
aria-label={label}
aria-labelledby={accessibilityLabelledBy}
aria-describedby={accessibilityDescribedBy}
// @ts-expect-error web only -prf
// @ts-ignore web only -prf
onClick={stopPropagation}
onStartShouldSetResponder={_ => true}
onTouchEnd={stopPropagation}
@@ -187,9 +177,10 @@ export function Inner({
shadowColor: t.palette.black,
shadowOpacity: t.name === 'light' ? 0.1 : 0.4,
shadowRadius: 30,
// @ts-ignore web only
animation: 'fadeIn ease-out 0.1s',
},
!reduceMotionEnabled && a.zoom_fade_in,
style,
flatten(style),
])}>
<DismissableLayer
onInteractOutside={preventDefault}
@@ -225,8 +216,8 @@ export const InnerFlatList = React.forwardRef<
style={[
a.overflow_hidden,
a.px_0,
// 100 minus 10vh of paddingVertical
web({maxHeight: '80vh'}),
// @ts-ignore web only -sfn
{maxHeight: 'calc(-36px + 100vh)'},
webInnerStyle,
]}
contentContainerStyle={[a.px_0, webInnerContentContainerStyle]}>
@@ -271,15 +262,20 @@ export function Handle() {
function Backdrop() {
const t = useTheme()
const {reduceMotionEnabled} = useA11y()
return (
<View style={{opacity: 0.8}}>
<View
style={{
opacity: 0.8,
}}>
<View
style={[
a.fixed,
a.inset_0,
{backgroundColor: t.palette.black},
!reduceMotionEnabled && a.fade_in,
{
backgroundColor: t.palette.black,
// @ts-ignore web only
animation: 'fadeIn ease-out 0.15s',
},
]}
/>
</View>
+2 -9
View File
@@ -44,8 +44,6 @@ 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 = {
@@ -62,16 +60,11 @@ export type DialogOuterProps = {
control: DialogControlProps
onClose?: () => void
nativeOptions?: Omit<BottomSheetViewProps, 'children'>
webOptions?: {
alignCenter?: boolean
}
webOptions?: {}
testID?: string
}
type DialogInnerPropsBase<T> = React.PropsWithChildren<ViewStyleProp> &
T & {
testID?: string
}
type DialogInnerPropsBase<T> = React.PropsWithChildren<ViewStyleProp> & T
export type DialogInnerProps =
| DialogInnerPropsBase<{
label?: undefined
+5 -5
View File
@@ -1,6 +1,7 @@
import React from 'react'
import {GestureResponderEvent, View} from 'react-native'
import {
AppBskyActorDefs,
AppBskyFeedDefs,
AppBskyGraphDefs,
AtUri,
@@ -31,7 +32,6 @@ 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?: bsky.profile.AnyProfileView
creator?: AppBskyActorDefs.ProfileViewBasic
}) {
const t = useTheme()
@@ -269,9 +269,9 @@ function SaveButtonInner({
},
])
}
Toast.show(_(msg({message: 'Feeds updated!', context: 'toast'})))
Toast.show(_(msg`Feeds updated!`))
} catch (err: any) {
logger.error(err, {message: `FeedCard: failed to update feeds`, pin})
logger.error(err, {context: `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 your feeds?`)}
title={_(msg`Remove from my feeds?`)}
description={_(
msg`Are you sure you want to remove this from your feeds?`,
)}
+5 -28
View File
@@ -1,7 +1,6 @@
import React from 'react'
import {View} from 'react-native'
import {ScrollView} from 'react-native-gesture-handler'
import {AppBskyFeedDefs, AtUri} from '@atproto/api'
import {ScrollView, View} from 'react-native'
import {AppBskyActorDefs, AppBskyFeedDefs, AtUri} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
@@ -27,7 +26,6 @@ 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
@@ -198,7 +196,6 @@ export function SuggestedFollowsProfile({did}: {did: string}) {
<ProfileGrid
isSuggestionsLoading={isSuggestionsLoading}
profiles={data?.suggestions ?? []}
recId={data?.recId}
error={error}
viewContext="profile"
/>
@@ -225,12 +222,10 @@ export function ProfileGrid({
isSuggestionsLoading,
error,
profiles,
recId,
viewContext = 'feed',
}: {
isSuggestionsLoading: boolean
profiles: bsky.profile.AnyProfileView[]
recId?: number
profiles: AppBskyActorDefs.ProfileViewDetailed[]
error: Error | null
viewContext: 'profile' | 'feed'
}) {
@@ -254,19 +249,12 @@ export function ProfileGrid({
))
) : error || !profiles.length ? null : (
<>
{profiles.slice(0, maxLength).map((profile, index) => (
{profiles.slice(0, maxLength).map(profile => (
<ProfileCard.Link
key={profile.did}
profile={profile}
onPress={() => {
logEvent('suggestedUser:press', {
logContext:
viewContext === 'feed'
? 'InterstitialDiscover'
: 'InterstitialProfile',
recId,
position: index,
})
logEvent('feed:interstitial:profileCard:press', {})
}}
style={[
a.flex_1,
@@ -294,17 +282,6 @@ export function ProfileGrid({
logContext="FeedInterstitial"
shape="round"
colorInverted
onFollow={() => {
logEvent('suggestedUser:follow', {
logContext:
viewContext === 'feed'
? 'InterstitialDiscover'
: 'InterstitialProfile',
location: 'Card',
recId,
position: index,
})
}}
/>
</ProfileCard.Header>
<ProfileCard.Description profile={profile} numberOfLines={2} />
+11 -25
View File
@@ -10,7 +10,6 @@ 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
@@ -33,13 +32,11 @@ export function KnownFollowers({
moderationOpts,
onLinkPress,
minimal,
showIfEmpty,
}: {
profile: bsky.profile.AnyProfileView
profile: AppBskyActorDefs.ProfileViewDetailed
moderationOpts: ModerationOpts
onLinkPress?: LinkProps['onPress']
minimal?: boolean
showIfEmpty?: boolean
}) {
const cache = React.useRef<Map<string, AppBskyActorDefs.KnownFollowers>>(
new Map(),
@@ -66,12 +63,11 @@ export function KnownFollowers({
moderationOpts={moderationOpts}
onLinkPress={onLinkPress}
minimal={minimal}
showIfEmpty={showIfEmpty}
/>
)
}
return <EmptyFallback show={showIfEmpty} />
return null
}
function KnownFollowersInner({
@@ -80,19 +76,22 @@ function KnownFollowersInner({
cachedKnownFollowers,
onLinkPress,
minimal,
showIfEmpty,
}: {
profile: bsky.profile.AnyProfileView
profile: AppBskyActorDefs.ProfileViewDetailed
moderationOpts: ModerationOpts
cachedKnownFollowers: AppBskyActorDefs.KnownFollowers
onLinkPress?: LinkProps['onPress']
minimal?: boolean
showIfEmpty?: boolean
}) {
const t = useTheme()
const {_} = useLingui()
const textStyle = [a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]
const textStyle = [
a.flex_1,
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)
@@ -115,7 +114,7 @@ function KnownFollowersInner({
* We check above too, but here for clarity and a reminder to _check for
* valid indices_
*/
if (slice.length === 0) return <EmptyFallback show={showIfEmpty} />
if (slice.length === 0) return null
const SIZE = minimal ? AVI_SIZE_SMALL : AVI_SIZE
@@ -127,7 +126,7 @@ function KnownFollowersInner({
onPress={onLinkPress}
to={makeProfileLink(profile, 'known-followers')}
style={[
a.max_w_full,
a.flex_1,
a.flex_row,
minimal ? a.gap_sm : a.gap_md,
a.align_center,
@@ -172,7 +171,6 @@ function KnownFollowersInner({
<Text
style={[
a.flex_shrink,
textStyle,
hovered && {
textDecorationLine: 'underline',
@@ -244,15 +242,3 @@ 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>
)
}
+2 -11
View File
@@ -1,5 +1,5 @@
import {createContext, useCallback, useContext} from 'react'
import {GestureResponderEvent, Keyboard, View} from 'react-native'
import {GestureResponderEvent, View} from 'react-native'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
@@ -14,7 +14,6 @@ import {
TextStyleProp,
useBreakpoints,
useGutters,
useLayoutBreakpoints,
useTheme,
web,
} from '#/alf'
@@ -24,7 +23,6 @@ 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'
@@ -44,7 +42,6 @@ export function Outer({
const gutters = useGutters([0, 'base'])
const {gtMobile} = useBreakpoints()
const {isWithinOffsetView} = useContext(ScrollbarOffsetContext)
const {centerColumnOffset} = useLayoutBreakpoints()
return (
<View
@@ -63,12 +60,7 @@ export function Outer({
}),
t.atoms.border_contrast_low,
gtMobile && [a.mx_auto, {maxWidth: 600}],
!isWithinOffsetView && {
transform: [
{translateX: centerColumnOffset ? -150 : 0},
{translateX: web(SCROLLBAR_OFFSET) ?? 0},
],
},
!isWithinOffsetView && a.scrollbar_offset,
]}>
{children}
</View>
@@ -148,7 +140,6 @@ export function MenuButton() {
const {gtMobile} = useBreakpoints()
const onPress = useCallback(() => {
Keyboard.dismiss()
setDrawerOpen(true)
}, [setDrawerOpen])
+8 -35
View File
@@ -13,15 +13,7 @@ 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,
useLayoutBreakpoints,
useTheme,
web,
} from '#/alf'
import {useDialogContext} from '#/components/Dialog'
import {SCROLLBAR_OFFSET} from '#/components/Layout/const'
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
import {ScrollbarOffsetContext} from '#/components/Layout/context'
export * from '#/components/Layout/const'
@@ -55,7 +47,6 @@ export const Screen = React.memo(function Screen({
export type ContentProps = AnimatedScrollViewProps & {
style?: StyleProp<ViewStyle>
contentContainerStyle?: StyleProp<ViewStyle>
ignoreTabletLayoutOffset?: boolean
}
/**
@@ -65,7 +56,6 @@ export const Content = React.memo(function Content({
children,
style,
contentContainerStyle,
ignoreTabletLayoutOffset,
...props
}: ContentProps) {
const t = useTheme()
@@ -94,10 +84,8 @@ export const Content = React.memo(function Content({
]}
{...props}>
{isWeb ? (
<Center ignoreTabletLayoutOffset={ignoreTabletLayoutOffset}>
{/* @ts-expect-error web only -esb */}
{children}
</Center>
// @ts-ignore web only -esb
<Center>{children}</Center>
) : (
children
)}
@@ -150,13 +138,10 @@ export const KeyboardAwareContent = React.memo(function LayoutScrollView({
export const Center = React.memo(function LayoutContent({
children,
style,
ignoreTabletLayoutOffset,
...props
}: ViewProps & {ignoreTabletLayoutOffset?: boolean}) {
}: ViewProps) {
const {isWithinOffsetView} = useContext(ScrollbarOffsetContext)
const {gtMobile} = useBreakpoints()
const {centerColumnOffset} = useLayoutBreakpoints()
const {isWithinDialog} = useDialogContext()
const ctx = useMemo(() => ({isWithinOffsetView: true}), [])
return (
<View
@@ -166,20 +151,8 @@ 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}>
@@ -195,7 +168,6 @@ 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={[
@@ -208,8 +180,9 @@ const WebCenterBorders = React.memo(function LayoutContent() {
width: 602,
left: '50%',
transform: [
{translateX: '-50%'},
{translateX: centerColumnOffset ? -150 : 0},
{
translateX: '-50%',
},
...a.scrollbar_offset.transform,
],
}),
+7 -25
View File
@@ -69,11 +69,6 @@ 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({
@@ -84,12 +79,8 @@ export function useLink({
onPress: outerOnPress,
onLongPress: outerOnLongPress,
shareOnLongPress,
overridePresentation,
shouldProxy,
}: BaseLinkProps & {
displayText: string
overridePresentation?: boolean
shouldProxy?: boolean
}) {
const navigation = useNavigationDeduped()
const {href} = useLinkProps<AllNavigatorParams>({
@@ -125,7 +116,7 @@ export function useLink({
})
} else {
if (isExternal) {
openLink(href, overridePresentation, shouldProxy)
openLink(href)
} else {
const shouldOpenInNewTab = shouldClickOpenNewTab(e)
@@ -167,8 +158,6 @@ export function useLink({
closeModal,
action,
navigation,
overridePresentation,
shouldProxy,
],
)
@@ -227,7 +216,6 @@ export function Link({
onPress: outerOnPress,
onLongPress: outerOnLongPress,
download,
shouldProxy,
...rest
}: LinkProps) {
const {href, isExternal, onPress, onLongPress} = useLink({
@@ -236,7 +224,6 @@ export function Link({
action,
onPress: outerOnPress,
onLongPress: outerOnLongPress,
shouldProxy: shouldProxy,
})
return (
@@ -267,13 +254,12 @@ export function Link({
export type InlineLinkProps = React.PropsWithChildren<
BaseLinkProps &
TextStyleProp &
Pick<TextProps, 'selectable' | 'numberOfLines'> &
Pick<ButtonProps, 'label' | 'accessibilityHint'> & {
disableUnderline?: boolean
title?: TextProps['title']
overridePresentation?: boolean
}
>
Pick<TextProps, 'selectable' | 'numberOfLines'>
> &
Pick<ButtonProps, 'label' | 'accessibilityHint'> & {
disableUnderline?: boolean
title?: TextProps['title']
}
export function InlineLinkText({
children,
@@ -288,8 +274,6 @@ export function InlineLinkText({
label,
shareOnLongPress,
disableUnderline,
overridePresentation,
shouldProxy,
...rest
}: InlineLinkProps) {
const t = useTheme()
@@ -302,8 +286,6 @@ export function InlineLinkText({
onPress: outerOnPress,
onLongPress: outerOnLongPress,
shareOnLongPress,
overridePresentation,
shouldProxy: shouldProxy,
})
const {
state: hovered,
+2 -2
View File
@@ -1,6 +1,7 @@
import React from 'react'
import {View} from 'react-native'
import {
AppBskyActorDefs,
AppBskyGraphDefs,
AtUri,
moderateUserList,
@@ -25,7 +26,6 @@ import {
import {Link as InternalLink, LinkProps} from '#/components/Link'
import * as Hider from '#/components/moderation/Hider'
import {Text} from '#/components/Typography'
import * as bsky from '#/types/bsky'
/*
* This component is based on `FeedCard` and is tightly coupled with that
@@ -107,7 +107,7 @@ export function TitleAndByline({
modUi,
}: {
title: string
creator?: bsky.profile.AnyProfileView
creator?: AppBskyActorDefs.ProfileViewBasic
purpose?: AppBskyGraphDefs.ListView['purpose']
modUi?: ModerationUI
}) {
+36 -22
View File
@@ -1,15 +1,19 @@
import React from 'react'
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import {Image} from 'expo-image'
import {AppBskyFeedDefs} from '@atproto/api'
import {
AppBskyEmbedExternal,
AppBskyEmbedImages,
AppBskyEmbedRecordWithMedia,
AppBskyEmbedVideo,
} from '@atproto/api'
import {Trans} from '@lingui/macro'
import {isTenorGifUri} from '#/lib/strings/embed-player'
import {parseTenorGif} 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
@@ -18,17 +22,20 @@ export function Embed({
embed,
style,
}: {
embed: AppBskyFeedDefs.PostView['embed']
embed?:
| AppBskyEmbedImages.View
| AppBskyEmbedRecordWithMedia.View
| AppBskyEmbedExternal.View
| AppBskyEmbedVideo.View
| {[k: string]: unknown}
style?: StyleProp<ViewStyle>
}) {
const e = bsky.post.parseEmbed(embed)
let media = AppBskyEmbedRecordWithMedia.isView(embed) ? embed.media : embed
if (!e) return null
if (e.type === 'images') {
if (AppBskyEmbedImages.isView(media)) {
return (
<Outer style={style}>
{e.view.images.map(image => (
{media.images.map(image => (
<ImageItem
key={image.thumb}
thumbnail={image.thumb}
@@ -37,21 +44,28 @@ export function Embed({
))}
</Outer>
)
} else if (e.type === 'link') {
if (!e.view.external.thumb) return null
if (!isTenorGifUri(e.view.external.uri)) return null
} 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)) {
return (
<Outer style={style}>
<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} />
<VideoItem thumbnail={media.thumbnail} alt={media.alt} />
</Outer>
)
}
+1 -1
View File
@@ -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`)}>
<Dialog.ScrollableInner label={_(msg`Menu`)} style={[a.py_sm]}>
<View style={[a.gap_lg]}>
{children}
{isNative && showCancel && <Cancel />}
+4 -12
View File
@@ -4,7 +4,6 @@ import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import {useA11y} from '#/state/a11y'
import {atoms as a, flatten, useTheme, web} from '#/alf'
import * as Dialog from '#/components/Dialog'
import {useInteractionState} from '#/components/hooks/useInteractionState'
@@ -178,16 +177,10 @@ export function Outer({
style?: StyleProp<ViewStyle>
}>) {
const t = useTheme()
const {reduceMotionEnabled} = useA11y()
return (
<DropdownMenu.Portal>
<DropdownMenu.Content
sideOffset={5}
collisionPadding={{left: 5, right: 5, bottom: 5}}
loop
aria-label="Test"
className="dropdown-menu-transform-origin dropdown-menu-constrain-size">
<DropdownMenu.Content sideOffset={5} loop aria-label="Test">
<View
style={[
a.rounded_sm,
@@ -196,8 +189,6 @@ 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,
]}>
{children}
@@ -382,8 +373,9 @@ export function Divider() {
style={flatten([
a.my_xs,
t.atoms.bg_contrast_100,
a.flex_shrink_0,
{height: 1},
{
height: 1,
},
])}
/>
)
+12 -14
View File
@@ -1,6 +1,7 @@
import React from 'react'
import {GestureResponderEvent, View} from 'react-native'
import {
AppBskyActorDefs,
moderateProfile,
ModerationOpts,
RichText as RichTextApi,
@@ -24,14 +25,13 @@ 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: bsky.profile.AnyProfileView
profile: AppBskyActorDefs.ProfileViewDetailed
moderationOpts: ModerationOpts
logContext?: 'ProfileCard' | 'StarterPackProfilesList'
}) {
@@ -51,7 +51,7 @@ export function Card({
moderationOpts,
logContext = 'ProfileCard',
}: {
profile: bsky.profile.AnyProfileView
profile: AppBskyActorDefs.ProfileViewDetailed
moderationOpts: ModerationOpts
logContext?: 'ProfileCard' | 'StarterPackProfilesList'
}) {
@@ -101,7 +101,7 @@ export function Link({
style,
...rest
}: {
profile: bsky.profile.AnyProfileView
profile: AppBskyActorDefs.ProfileViewDetailed
} & Omit<LinkProps, 'to' | 'label'>) {
const {_} = useLingui()
return (
@@ -126,7 +126,7 @@ export function Avatar({
profile,
moderationOpts,
}: {
profile: bsky.profile.AnyProfileView
profile: AppBskyActorDefs.ProfileViewDetailed
moderationOpts: ModerationOpts
}) {
const moderation = moderateProfile(profile, moderationOpts)
@@ -161,7 +161,7 @@ export function NameAndHandle({
profile,
moderationOpts,
}: {
profile: bsky.profile.AnyProfileView
profile: AppBskyActorDefs.ProfileViewDetailed
moderationOpts: ModerationOpts
}) {
const t = useTheme()
@@ -224,16 +224,17 @@ export function Description({
profile: profileUnshadowed,
numberOfLines = 3,
}: {
profile: bsky.profile.AnyProfileView
profile: AppBskyActorDefs.ProfileViewDetailed
numberOfLines?: number
}) {
const profile = useProfileShadow(profileUnshadowed)
const {description} = profile
const rt = React.useMemo(() => {
if (!('description' in profile)) return
const rt = new RichTextApi({text: profile.description || ''})
if (!description) return
const rt = new RichTextApi({text: description || ''})
rt.detectFacetsWithoutResolution()
return rt
}, [profile])
}, [description])
if (!rt) return null
if (
profile.viewer &&
@@ -280,12 +281,11 @@ export function DescriptionPlaceholder({
}
export type FollowButtonProps = {
profile: bsky.profile.AnyProfileView
profile: AppBskyActorDefs.ProfileViewBasic
moderationOpts: ModerationOpts
logContext: LogEvents['profile:follow']['logContext'] &
LogEvents['profile:unfollow']['logContext']
colorInverted?: boolean
onFollow?: () => void
} & Partial<ButtonProps>
export function FollowButton(props: FollowButtonProps) {
@@ -299,7 +299,6 @@ export function FollowButtonInner({
moderationOpts,
logContext,
onPress: onPressProp,
onFollow,
colorInverted,
...rest
}: FollowButtonProps) {
@@ -326,7 +325,6 @@ export function FollowButtonInner({
),
)
onPressProp?.(e)
onFollow?.()
} catch (err: any) {
if (err?.name !== 'AbortError') {
Toast.show(_(msg`An issue occurred, please try again.`), 'xmark')
@@ -762,7 +762,7 @@ function SearchInput({
autoComplete="off"
autoCapitalize="none"
accessibilityLabel={_(msg`Search profiles`)}
accessibilityHint={_(msg`Searches for profiles`)}
accessibilityHint={_(msg`Search profiles`)}
/>
</View>
)
-1
View File
@@ -45,7 +45,6 @@ export function Outer({
<Dialog.Outer
control={control}
testID={testID}
webOptions={{alignCenter: true}}
nativeOptions={{preventExpansion: true, ...nativeOptions}}>
<Dialog.Handle />
<Context.Provider value={context}>
+1 -2
View File
@@ -16,7 +16,6 @@ import * as Dialog from '#/components/Dialog'
import * as Toggle from '#/components/forms/Toggle'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
import {PaperPlane_Stroke2_Corner0_Rounded as SendIcon} from '#/components/icons/PaperPlane'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
import {ReportDialogProps} from './types'
@@ -224,7 +223,7 @@ export function SubmitView({
<ButtonText>
<Trans>Send report</Trans>
</ButtonText>
<ButtonIcon icon={submitting ? Loader : SendIcon} />
{submitting && <ButtonIcon icon={Loader} />}
</Button>
</View>
{/* Maybe fix this later -h */}
-4
View File
@@ -23,7 +23,6 @@ export type RichTextProps = TextStyleProp &
onLinkPress?: LinkProps['onPress']
interactiveStyle?: TextStyle
emojiMultiplier?: number
shouldProxyLinks?: boolean
}
export function RichText({
@@ -40,7 +39,6 @@ export function RichText({
emojiMultiplier = 1.85,
onLayout,
onTextLayout,
shouldProxyLinks,
}: RichTextProps) {
const richText = React.useMemo(
() =>
@@ -112,7 +110,6 @@ export function RichText({
style={interactiveStyles}
// @ts-ignore TODO
dataSet={WORD_WRAP}
shouldProxy={shouldProxyLinks}
onPress={onLinkPress}>
{segment.text}
</InlineLinkText>
@@ -131,7 +128,6 @@ export function RichText({
// @ts-ignore TODO
dataSet={WORD_WRAP}
shareOnLongPress
shouldProxy={shouldProxyLinks}
onPress={onLinkPress}
emoji>
{toShortUrl(segment.text)}
@@ -1,9 +1,4 @@
import React, {
useCallback,
useEffect,
useImperativeHandle,
useState,
} from 'react'
import React from 'react'
import {
findNodeHandle,
ListRenderItemInfo,
@@ -11,10 +6,11 @@ import {
View,
ViewStyle,
} from 'react-native'
import {AppBskyGraphDefs} from '@atproto/api'
import {AppBskyGraphDefs, AppBskyGraphGetActorStarterPacks} 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'
@@ -23,27 +19,28 @@ 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 {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {Text} from '#/view/com/util/text/Text'
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 {Text} from '#/components/Typography'
import {VerifyEmailDialog} from '../dialogs/VerifyEmailDialog'
import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '../icons/Plus'
interface SectionRef {
scrollToTop: () => void
}
interface ProfileFeedgensProps {
starterPacksQuery: UseInfiniteQueryResult<
InfiniteData<AppBskyGraphGetActorStarterPacks.OutputSchema, unknown>,
Error
>
scrollElRef: ListRef
did: string
headerOffset: number
enabled?: boolean
style?: StyleProp<ViewStyle>
@@ -61,8 +58,8 @@ export const ProfileStarterPacks = React.forwardRef<
ProfileFeedgensProps
>(function ProfileFeedgensImpl(
{
starterPacksQuery: query,
scrollElRef,
did,
headerOffset,
enabled,
style,
@@ -74,18 +71,17 @@ export const ProfileStarterPacks = React.forwardRef<
) {
const t = useTheme()
const bottomBarOffset = useBottomBarOffset(100)
const [isPTRing, setIsPTRing] = useState(false)
const {data, refetch, isFetching, hasNextPage, fetchNextPage} =
useActorStarterPacksQuery({did, enabled})
const [isPTRing, setIsPTRing] = React.useState(false)
const {data, refetch, isFetching, hasNextPage, fetchNextPage} = query
const {isTabletOrDesktop} = useWebMediaQueries()
const items = data?.pages.flatMap(page => page.starterPacks)
useImperativeHandle(ref, () => ({
React.useImperativeHandle(ref, () => ({
scrollToTop: () => {},
}))
const onRefresh = useCallback(async () => {
const onRefresh = React.useCallback(async () => {
setIsPTRing(true)
try {
await refetch()
@@ -95,7 +91,7 @@ export const ProfileStarterPacks = React.forwardRef<
setIsPTRing(false)
}, [refetch, setIsPTRing])
const onEndReached = useCallback(async () => {
const onEndReached = React.useCallback(async () => {
if (isFetching || !hasNextPage) return
try {
@@ -105,7 +101,7 @@ export const ProfileStarterPacks = React.forwardRef<
}
}, [isFetching, hasNextPage, fetchNextPage])
useEffect(() => {
React.useEffect(() => {
if (enabled && scrollElRef.current) {
const nativeTag = findNodeHandle(scrollElRef.current)
setScrollViewTag(nativeTag)
@@ -144,11 +140,9 @@ export const ProfileStarterPacks = React.forwardRef<
desktopFixedHeight
onEndReached={onEndReached}
onRefresh={onRefresh}
ListEmptyComponent={
data ? (isMe ? Empty : undefined) : FeedLoadingPlaceholder
}
ListEmptyComponent={Empty}
ListFooterComponent={
!!data && items?.length !== 0 && isMe ? CreateAnother : undefined
items?.length !== 0 && isMe ? CreateAnother : undefined
}
/>
</View>
@@ -187,6 +181,7 @@ function CreateAnother() {
function Empty() {
const {_} = useLingui()
const t = useTheme()
const navigation = useNavigation<NavigationProp>()
const confirmDialogControl = useDialogControl()
const followersDialogControl = useDialogControl()
@@ -195,7 +190,7 @@ function Empty() {
const {needsEmailVerification} = useEmail()
const verifyEmailControl = useDialogControl()
const [isGenerating, setIsGenerating] = useState(false)
const [isGenerating, setIsGenerating] = React.useState(false)
const {mutate: generateStarterPack} = useGenerateStarterPackMutation({
onSuccess: ({uri}) => {
@@ -232,10 +227,16 @@ function Empty() {
a.justify_between,
a.gap_lg,
a.shadow_lg,
{marginTop: a.border.borderWidth},
{marginTop: 1},
]}>
<View style={[a.gap_xs]}>
<Text style={[a.font_bold, a.text_lg, {color: 'white'}]}>
<Text
style={[
a.font_bold,
a.text_lg,
t.atoms.text_contrast_medium,
{color: 'white'},
]}>
<Trans>You haven't created a starter pack yet!</Trans>
</Text>
<Text style={[a.text_md, {color: 'white'}]}>
+1 -7
View File
@@ -13,7 +13,6 @@ 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
@@ -31,12 +30,7 @@ export const QrCode = React.forwardRef<ViewShot, Props>(function QrCode(
) {
const {record} = starterPack
if (
!bsky.dangerousIsType<AppBskyGraphStarterpack.Record>(
record,
AppBskyGraphStarterpack.isRecord,
)
) {
if (!AppBskyGraphStarterpack.isRecord(record)) {
return null
}
+1 -7
View File
@@ -18,7 +18,6 @@ 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,
@@ -78,12 +77,7 @@ export function QrCodeDialog({
} else {
setIsProcessing(true)
if (
!bsky.validate(
starterPack.record,
AppBskyGraphStarterpack.validateRecord,
)
) {
if (!AppBskyGraphStarterpack.isRecord(starterPack.record)) {
return
}
+9 -15
View File
@@ -1,7 +1,7 @@
import React from 'react'
import {View} from 'react-native'
import {Image} from 'expo-image'
import {AppBskyGraphStarterpack, AtUri} from '@atproto/api'
import {AppBskyGraphDefs, AppBskyGraphStarterpack, AtUri} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
@@ -12,15 +12,14 @@ import {precacheResolvedUri} from '#/state/queries/resolve-uri'
import {precacheStarterPack} from '#/state/queries/starter-packs'
import {useSession} from '#/state/session'
import {atoms as a, useTheme} from '#/alf'
import {StarterPack as StarterPackIcon} from '#/components/icons/StarterPack'
import {StarterPack} 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?: bsky.starterPack.AnyStarterPackView
starterPack?: AppBskyGraphDefs.StarterPackViewBasic
}) {
if (!starterPack) return null
return (
@@ -33,7 +32,7 @@ export function Default({
export function Notification({
starterPack,
}: {
starterPack?: bsky.starterPack.AnyStarterPackView
starterPack?: AppBskyGraphDefs.StarterPackViewBasic
}) {
if (!starterPack) return null
return (
@@ -48,7 +47,7 @@ export function Card({
noIcon,
noDescription,
}: {
starterPack: bsky.starterPack.AnyStarterPackView
starterPack: AppBskyGraphDefs.StarterPackViewBasic
noIcon?: boolean
noDescription?: boolean
}) {
@@ -58,19 +57,14 @@ export function Card({
const t = useTheme()
const {currentAccount} = useSession()
if (
!bsky.dangerousIsType<AppBskyGraphStarterpack.Record>(
record,
AppBskyGraphStarterpack.isRecord,
)
) {
if (!AppBskyGraphStarterpack.isRecord(record)) {
return null
}
return (
<View style={[a.w_full, a.gap_md]}>
<View style={[a.flex_row, a.gap_sm, a.w_full]}>
{!noIcon ? <StarterPackIcon width={40} gradient="sky" /> : null}
{!noIcon ? <StarterPack width={40} gradient="sky" /> : null}
<View style={[a.flex_1]}>
<Text
emoji
@@ -106,7 +100,7 @@ export function Link({
starterPack,
children,
}: {
starterPack: bsky.starterPack.AnyStarterPackView
starterPack: AppBskyGraphDefs.StarterPackViewBasic
onPress?: () => void
children: BaseLinkProps['children']
}) {
@@ -145,7 +139,7 @@ export function Link({
export function Embed({
starterPack,
}: {
starterPack: bsky.starterPack.AnyStarterPackView
starterPack: AppBskyGraphDefs.StarterPackViewBasic
}) {
const t = useTheme()
const imageUri = getStarterPackOgCard(starterPack)
@@ -38,7 +38,7 @@ export function WizardEditListDialog({
state: WizardState
dispatch: (action: WizardAction) => void
moderationOpts: ModerationOpts
profile: AppBskyActorDefs.ProfileViewDetailed
profile: AppBskyActorDefs.ProfileViewBasic
}) {
const {_} = useLingui()
const t = useTheme()
@@ -22,7 +22,6 @@ 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,
@@ -124,7 +123,7 @@ export function WizardProfileCard({
btnType: 'checkbox' | 'remove'
state: WizardState
dispatch: (action: WizardAction) => void
profile: bsky.profile.AnyProfileView
profile: AppBskyActorDefs.ProfileViewBasic
moderationOpts: ModerationOpts
}) {
const {currentAccount} = useSession()
+40 -50
View File
@@ -12,7 +12,6 @@ import {PressableScale} from '#/lib/custom-animations/PressableScale'
// import {UserAvatar} from '#/view/com/util/UserAvatar'
import type {TrendingTopic} from '#/state/queries/trending/useTrendingTopics'
import {atoms as a, native, useTheme, ViewStyleProp} from '#/alf'
import {StarterPack as StarterPackIcon} from '#/components/icons/StarterPack'
import {Link as InternalLink, LinkProps} from '#/components/Link'
import {Text} from '#/components/Typography'
@@ -25,8 +24,9 @@ export function TrendingTopic({
const topic = useTopic(raw)
const isSmall = size === 'small'
const hasIcon = topic.type === 'starter-pack' && !isSmall
const iconSize = 20
// const hasAvi = topic.type === 'feed' || topic.type === 'profile'
// const aviSize = isSmall ? 16 : 20
// const iconSize = isSmall ? 16 : 20
return (
<View
@@ -45,47 +45,45 @@ export function TrendingTopic({
},
]
: [a.py_sm, a.px_md],
hasIcon && {gap: 6},
style,
/*
{
padding: 6,
gap: hasAvi ? 4 : 2,
},
a.pr_md,
*/
]}>
{hasIcon && topic.type === 'starter-pack' && (
<StarterPackIcon
gradient="sky"
width={iconSize}
style={{marginLeft: -3, marginVertical: -1}}
/>
)}
{/*
<View
style={[
a.align_center,
a.justify_center,
a.rounded_full,
a.overflow_hidden,
{
width: iconSize,
height: iconSize,
},
]}>
{topic.type === 'tag' ? (
<Hashtag width={iconSize} />
) : topic.type === 'topic' ? (
<Quote width={iconSize - 2} />
) : topic.type === 'feed' ? (
<UserAvatar
type="user"
size={aviSize}
avatar=""
/>
) : (
<UserAvatar
type="user"
size={aviSize}
avatar=""
/>
)}
</View>
<View
style={[
a.align_center,
a.justify_center,
a.rounded_full,
a.overflow_hidden,
{
width: aviSize,
height: aviSize,
},
]}>
{topic.type === 'tag' ? (
<Hashtag width={iconSize} />
) : topic.type === 'topic' ? (
<Quote width={iconSize - 2} />
) : topic.type === 'feed' ? (
<UserAvatar
type="user"
size={aviSize}
avatar=""
/>
) : (
<UserAvatar
type="user"
size={aviSize}
avatar=""
/>
)}
</View>
*/}
<Text
@@ -153,7 +151,7 @@ export function TrendingTopicLink({
type ParsedTrendingTopic =
| {
type: 'topic' | 'tag' | 'starter-pack' | 'unknown'
type: 'topic' | 'tag' | 'unknown'
label: string
displayName: string
url: string
@@ -189,14 +187,6 @@ export function useTopic(raw: TrendingTopic): ParsedTrendingTopic {
uri: undefined,
url: link,
}
} else if (link.startsWith('/starter-pack')) {
return {
type: 'starter-pack',
label: _(msg`Browse starter pack ${displayName}`),
displayName,
uri: undefined,
url: link,
}
}
/*
+2 -8
View File
@@ -27,7 +27,6 @@ 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, {
@@ -79,12 +78,7 @@ export function VideoPostCard({
if (!AppBskyEmbedVideo.isView(embed)) return null
const author = post.author
const text = bsky.dangerousIsType<AppBskyFeedPost.Record>(
post.record,
AppBskyFeedPost.isRecord,
)
? post.record?.text
: ''
const text = AppBskyFeedPost.isRecord(post.record) ? post.record?.text : ''
const likeCount = post?.likeCount ?? 0
const repostCount = post?.repostCount ?? 0
const {thumbnail} = embed
@@ -118,7 +112,7 @@ export function VideoPostCard({
return (
<Link
accessibilityHint={_(msg`Views video in immersive mode`)}
accessibilityHint={_(msg`Tap to view video in immersive mode.`)}
label={_(msg`Video from ${author.handle}: ${text}`)}
to={{
screen: 'VideoFeed',
+2 -24
View File
@@ -29,7 +29,6 @@ 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 {
@@ -49,10 +48,7 @@ export function WhoCanReply({post, isThreadAuthor, style}: WhoCanReplyProps) {
* unexpectedly, we should check to make sure it's for sure the root URI.
*/
const rootUri =
bsky.dangerousIsType<AppBskyFeedPost.Record>(
post.record,
AppBskyFeedPost.isRecord,
) && post.record.reply?.root
AppBskyFeedPost.isRecord(post.record) && post.record.reply?.root
? post.record.reply.root.uri
: post.uri
const settings = React.useMemo(() => {
@@ -213,12 +209,7 @@ function Rules({
a.flex_wrap,
t.atoms.text_contrast_medium,
]}>
{settings.length === 0 ? (
<Trans>
This post has an unknown type of threadgate on it. Your app may be
out of date.
</Trans>
) : settings[0].type === 'everybody' ? (
{settings[0].type === 'everybody' ? (
<Trans>Everybody can reply to this post.</Trans>
) : settings[0].type === 'nobody' ? (
<Trans>Replies to this post are disabled.</Trans>
@@ -262,19 +253,6 @@ function Rule({
if (rule.type === 'mention') {
return <Trans>mentioned users</Trans>
}
if (rule.type === 'followers') {
return (
<Trans>
users following{' '}
<InlineLinkText
label={`@${post.author.handle}`}
to={makeProfileLink(post.author)}
style={[a.text_sm, a.leading_snug]}>
@{post.author.handle}
</InlineLinkText>
</Trans>
)
}
if (rule.type === 'following') {
return (
<Trans>
+10 -7
View File
@@ -4,7 +4,6 @@ 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 {
@@ -13,12 +12,12 @@ import {
usePreferencesSetBirthDateMutation,
} from '#/state/queries/preferences'
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
import {DateInput} from '#/view/com/util/forms/DateInput'
import {atoms as a, useTheme} from '#/alf'
import * as Dialog from '#/components/Dialog'
import {DateField} from '#/components/forms/DateField'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
import {Button, ButtonIcon, ButtonText} from '../Button'
import {Text} from '../Typography'
export function BirthDateSettingsDialog({
control,
@@ -96,13 +95,17 @@ function BirthdayInner({
return (
<View style={a.gap_lg} testID="birthDateSettingsDialog">
<View style={isIOS && [a.w_full, a.align_center]}>
<DateField
<DateInput
handleAsUTC
testID="birthdayInput"
value={date}
onChangeDate={newDate => setDate(new Date(newDate))}
label={_(msg`Birthday`)}
onChange={setDate}
buttonType="default-light"
buttonStyle={[a.rounded_sm]}
buttonLabelType="lg"
accessibilityLabel={_(msg`Birthday`)}
accessibilityHint={_(msg`Enter your birth date`)}
maximumDate={getDateAgo(13)}
accessibilityLabelledBy="birthDate"
/>
</View>
+59 -125
View File
@@ -1,5 +1,5 @@
import {memo, useEffect, useMemo, useState} from 'react'
import {View} from 'react-native'
import React, {memo, useRef, useState} from 'react'
import {TextInput, View} from 'react-native'
import {AppBskyActorDefs, AppBskyFeedPost, AtUri} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -8,19 +8,12 @@ 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 * 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 {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {CodeBrackets_Stroke2_Corner0_Rounded as CodeBrackets} from '#/components/icons/CodeBrackets'
import {Text} from '#/components/Typography'
export type ColorModeValues = 'system' | 'light' | 'dark'
import {Button, ButtonIcon, ButtonText} from '../Button'
type EmbedDialogProps = {
control: Dialog.DialogControlProps
@@ -51,12 +44,11 @@ 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
useEffect(() => {
React.useEffect(() => {
if (copied) {
const timeout = setTimeout(() => {
setCopied(false)
@@ -65,7 +57,7 @@ function EmbedDialogInner({
}
}, [copied])
const snippet = useMemo(() => {
const snippet = React.useMemo(() => {
function toEmbedUrl(href: string) {
return toShareUrl(href) + '?ref_src=embed'
}
@@ -83,11 +75,9 @@ 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,
)}" data-bluesky-embed-color-mode="${escapeHtml(
colorMode,
)}"><p lang="${escapeHtml(lang)}">${escapeHtml(record.text)}${
)}" data-bluesky-cid="${escapeHtml(postCid)}"><p lang="${escapeHtml(
lang,
)}">${escapeHtml(record.text)}${
record.embed
? `<br><br><a href="${escapeHtml(href)}">[image or embed]</a>`
: ''
@@ -98,116 +88,60 @@ 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, colorMode])
}, [i18n, postUri, postCid, record, timestamp, postAuthor])
return (
<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>
<View
style={[
a.border,
t.atoms.border_contrast_low,
a.rounded_sm,
a.overflow_hidden,
]}>
<Button
label={
showCustomisation
? _(msg`Hide customization options`)
: _(msg`Show customization options`)
}
color="secondary"
variant="ghost"
size="small"
shape="default"
onPress={() => setShowCustomisation(c => !c)}
style={[
a.justify_start,
showCustomisation && t.atoms.bg_contrast_25,
]}>
<ButtonIcon
icon={showCustomisation ? ChevronBottomIcon : ChevronRightIcon}
/>
<ButtonText>
<Trans>Customization options</Trans>
</ButtonText>
</Button>
<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>
{showCustomisation && (
<View style={[a.gap_sm, a.p_md]}>
<Text style={[t.atoms.text_contrast_medium, a.font_bold]}>
<Trans>Color theme</Trans>
</Text>
<ToggleButton.Group
label={_(msg`Color mode`)}
values={[colorMode]}
onChange={([value]) => setColorMode(value as ColorModeValues)}>
<ToggleButton.Button name="system" label={_(msg`System`)}>
<ToggleButton.ButtonText>
<Trans>System</Trans>
</ToggleButton.ButtonText>
</ToggleButton.Button>
<ToggleButton.Button name="light" label={_(msg`Light`)}>
<ToggleButton.ButtonText>
<Trans>Light</Trans>
</ToggleButton.ButtonText>
</ToggleButton.Button>
<ToggleButton.Button name="dark" label={_(msg`Dark`)}>
<ToggleButton.ButtonText>
<Trans>Dark</Trans>
</ToggleButton.ButtonText>
</ToggleButton.Button>
</ToggleButton.Group>
</View>
)}
<View 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>
</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>
</>
) : (
<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>Copy code</Trans>
<Trans>Copied!</Trans>
</ButtonText>
)}
</Button>
</View>
</>
) : (
<ButtonText>
<Trans>Copy code</Trans>
</ButtonText>
)}
</Button>
</View>
<Dialog.Close />
</Dialog.Inner>
+1 -1
View File
@@ -318,7 +318,7 @@ function MutedWordsInner() {
<View style={[a.pt_xs]}>
<Button
disabled={isPending || !field}
label={_(msg`Add mute word with chosen settings`)}
label={_(msg`Add mute word for configured settings`)}
size="large"
color="primary"
variant="solid"
@@ -40,7 +40,6 @@ import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
export type PostInteractionSettingsFormProps = {
canSave?: boolean
onSave: () => void
isSaving?: boolean
@@ -59,54 +58,20 @@ export function PostInteractionSettingsControlledDialog({
}: PostInteractionSettingsFormProps & {
control: Dialog.DialogControlProps
}) {
const t = useTheme()
const {_} = useLingui()
return (
<Dialog.Outer control={control}>
<Dialog.Handle />
<Dialog.ScrollableInner
label={_(msg`Edit post interaction settings`)}
style={[{maxWidth: 500}, a.w_full]}>
<View style={[a.gap_md]}>
<Header />
<PostInteractionSettingsForm {...rest} />
<Text
style={[
a.pt_sm,
a.text_sm,
a.leading_snug,
t.atoms.text_contrast_medium,
]}>
<Trans>
You can set default interaction settings in{' '}
<Text style={[a.font_bold, t.atoms.text_contrast_medium]}>
Settings &rarr; Moderation &rarr; Interaction settings
</Text>
.
</Trans>
</Text>
</View>
<PostInteractionSettingsForm {...rest} />
<Dialog.Close />
</Dialog.ScrollableInner>
</Dialog.Outer>
)
}
export function Header() {
return (
<View style={[a.gap_md, a.pb_sm]}>
<Text style={[a.text_2xl, a.font_bold]}>
<Trans>Post interaction settings</Trans>
</Text>
<Text style={[a.text_md, a.pb_xs]}>
<Trans>Customize who can interact with this post.</Trans>
</Text>
<Divider />
</View>
)
}
export type PostInteractionSettingsDialogProps = {
control: Dialog.DialogControlProps
/**
@@ -209,7 +174,7 @@ export function PostInteractionSettingsDialogControlledInner(
props.control.close()
} catch (e: any) {
logger.error(`Failed to save post interaction settings`, {
source: 'PostInteractionSettingsDialogControlledInner',
context: 'PostInteractionSettingsDialogControlledInner',
safeMessage: e.message,
})
Toast.show(
@@ -238,31 +203,26 @@ export function PostInteractionSettingsDialogControlledInner(
<Dialog.ScrollableInner
label={_(msg`Edit post interaction settings`)}
style={[{maxWidth: 500}, a.w_full]}>
<View style={[a.gap_md]}>
<Header />
{isLoading ? (
<View style={[a.flex_1, a.py_4xl, a.align_center, a.justify_center]}>
<Loader size="xl" />
</View>
) : (
<PostInteractionSettingsForm
replySettingsDisabled={!isThreadgateOwnedByViewer}
isSaving={isSaving}
onSave={onSave}
postgate={postgateValue}
onChangePostgate={setEditedPostgate}
threadgateAllowUISettings={allowUIValue}
onChangeThreadgateAllowUISettings={setEditedAllowUISettings}
/>
)}
</View>
{isLoading ? (
<View style={[a.flex_1, a.py_4xl, a.align_center, a.justify_center]}>
<Loader size="xl" />
</View>
) : (
<PostInteractionSettingsForm
replySettingsDisabled={!isThreadgateOwnedByViewer}
isSaving={isSaving}
onSave={onSave}
postgate={postgateValue}
onChangePostgate={setEditedPostgate}
threadgateAllowUISettings={allowUIValue}
onChangeThreadgateAllowUISettings={setEditedAllowUISettings}
/>
)}
</Dialog.ScrollableInner>
)
}
export function PostInteractionSettingsForm({
canSave = true,
onSave,
isSaving,
postgate,
@@ -323,7 +283,17 @@ export function PostInteractionSettingsForm({
return (
<View>
<View style={[a.flex_1, a.gap_md]}>
<Text style={[a.text_2xl, a.font_bold]}>
<Trans>Post interaction settings</Trans>
</Text>
<View style={[a.gap_lg]}>
<Text style={[a.text_md]}>
<Trans>Customize who can interact with this post.</Trans>
</Text>
<Divider />
<View style={[a.gap_sm]}>
<Text style={[a.font_bold, a.text_lg]}>
<Trans>Quote settings</Trans>
@@ -430,7 +400,7 @@ export function PostInteractionSettingsForm({
disabled={replySettingsDisabled}
/>
<Selectable
label={_(msg`Users you follow`)}
label={_(msg`Followed users`)}
isSelected={
!!threadgateAllowUISettings.find(
v => v.type === 'following',
@@ -439,16 +409,6 @@ export function PostInteractionSettingsForm({
onPress={() => onPressAudience({type: 'following'})}
disabled={replySettingsDisabled}
/>
<Selectable
label={_(msg`Your followers`)}
isSelected={
!!threadgateAllowUISettings.find(
v => v.type === 'followers',
)
}
onPress={() => onPressAudience({type: 'followers'})}
disabled={replySettingsDisabled}
/>
{lists && lists.length > 0
? lists.map(list => (
<Selectable
@@ -475,7 +435,6 @@ export function PostInteractionSettingsForm({
</View>
<Button
disabled={!canSave || isSaving}
label={_(msg`Save`)}
onPress={onSave}
color="primary"
+1 -1
View File
@@ -43,7 +43,7 @@ function SigninDialogInner({}: {control: Dialog.DialogOuterProps['control']}) {
return (
<Dialog.ScrollableInner
label={_(msg`Sign in to Bluesky or create a new account`)}
label={_(msg`Sign into Bluesky or create a new account`)}
style={[gtMobile ? {width: 'auto', maxWidth: 420} : a.w_full]}>
<View style={[!isNative && a.p_2xl]}>
<View
+145 -173
View File
@@ -1,6 +1,10 @@
import React, {useCallback} from 'react'
import {Keyboard, Pressable, View} from 'react-native'
import {ChatBskyConvoDefs, ModerationCause} from '@atproto/api'
import {
AppBskyActorDefs,
ChatBskyConvoDefs,
ModerationCause,
} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
@@ -30,12 +34,11 @@ 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'
let ConvoMenu = ({
convo,
convo: initialConvo,
profile,
control,
currentScreen,
@@ -46,7 +49,7 @@ let ConvoMenu = ({
style,
}: {
convo: ChatBskyConvoDefs.ConvoView
profile: Shadow<bsky.profile.AnyProfileView>
profile: Shadow<AppBskyActorDefs.ProfileViewBasic>
control?: Menu.MenuControlProps
currentScreen: 'list' | 'conversation'
showMarkAsRead?: boolean
@@ -58,124 +61,30 @@ let ConvoMenu = ({
latestReportableMessage?: ChatBskyConvoDefs.MessageView
style?: ViewStyleProp['style']
}): React.ReactNode => {
const navigation = useNavigation<NavigationProp>()
const {_} = useLingui()
const t = useTheme()
const leaveConvoControl = Prompt.usePromptControl()
const reportControl = Prompt.usePromptControl()
const blockedByListControl = Prompt.usePromptControl()
const {listBlocks} = blockInfo
return (
<>
<Menu.Root control={control}>
{!hideTrigger && (
<View style={[style]}>
<Menu.Trigger label={_(msg`Chat settings`)}>
{({props, state}) => (
<Pressable
{...props}
onPress={() => {
Keyboard.dismiss()
props.onPress()
}}
style={[
a.p_sm,
a.rounded_full,
(state.hovered || state.pressed) && t.atoms.bg_contrast_25,
// make sure pfp is in the middle
{marginLeft: -10},
]}>
<DotsHorizontal size="md" style={t.atoms.text} />
</Pressable>
)}
</Menu.Trigger>
</View>
)}
<Menu.Outer>
<MenuContent
profile={profile}
showMarkAsRead={showMarkAsRead}
blockInfo={blockInfo}
convo={convo}
leaveConvoControl={leaveConvoControl}
reportControl={reportControl}
blockedByListControl={blockedByListControl}
/>
</Menu.Outer>
</Menu.Root>
<LeaveConvoPrompt
control={leaveConvoControl}
convoId={convo.id}
currentScreen={currentScreen}
/>
{latestReportableMessage ? (
<ReportDialog
currentScreen={currentScreen}
params={{
type: 'convoMessage',
convoId: convo.id,
message: latestReportableMessage,
}}
control={reportControl}
/>
) : (
<ReportConversationPrompt control={reportControl} />
)}
<BlockedByListDialog
control={blockedByListControl}
listBlocks={listBlocks}
/>
</>
)
}
ConvoMenu = React.memo(ConvoMenu)
function MenuContent({
convo: initialConvo,
profile,
showMarkAsRead,
blockInfo,
leaveConvoControl,
reportControl,
blockedByListControl,
}: {
convo: ChatBskyConvoDefs.ConvoView
profile: Shadow<bsky.profile.AnyProfileView>
showMarkAsRead?: boolean
blockInfo: {
listBlocks: ModerationCause[]
userBlock?: ModerationCause
}
leaveConvoControl: Prompt.PromptControlProps
reportControl: Prompt.PromptControlProps
blockedByListControl: Prompt.PromptControlProps
}) {
const navigation = useNavigation<NavigationProp>()
const {_} = useLingui()
const {mutate: markAsRead} = useMarkAsReadMutation()
const {listBlocks, userBlock} = blockInfo
const isBlocking = userBlock || !!listBlocks.length
const isDeletedAccount = profile.handle === 'missing.invalid'
const convoId = initialConvo.id
const {data: convo} = useConvoQuery(initialConvo)
const onNavigateToProfile = useCallback(() => {
navigation.navigate('Profile', {name: profile.did})
}, [navigation, profile.did])
const {mutate: muteConvo} = useMuteConvo(convoId, {
const {mutate: muteConvo} = useMuteConvo(convo?.id, {
onSuccess: data => {
if (data.convo.muted) {
Toast.show(_(msg({message: 'Chat muted', context: 'toast'})))
Toast.show(_(msg`Chat muted`))
} else {
Toast.show(_(msg({message: 'Chat unmuted', context: 'toast'})))
Toast.show(_(msg`Chat unmuted`))
}
},
onError: () => {
@@ -198,81 +107,144 @@ function MenuContent({
}
}, [userBlock, listBlocks, blockedByListControl, queueBlock, queueUnblock])
return isDeletedAccount ? (
<Menu.Item
label={_(msg`Leave conversation`)}
onPress={() => leaveConvoControl.open()}>
<Menu.ItemText>
<Trans>Leave conversation</Trans>
</Menu.ItemText>
<Menu.ItemIcon icon={ArrowBoxLeft} />
</Menu.Item>
) : (
return (
<>
<Menu.Group>
{showMarkAsRead && (
<Menu.Item
label={_(msg`Mark as read`)}
onPress={() => markAsRead({convoId})}>
<Menu.ItemText>
<Trans>Mark as read</Trans>
</Menu.ItemText>
<Menu.ItemIcon icon={Bubble} />
</Menu.Item>
<Menu.Root control={control}>
{!hideTrigger && (
<View style={[style]}>
<Menu.Trigger label={_(msg`Chat settings`)}>
{({props, state}) => (
<Pressable
{...props}
onPress={() => {
Keyboard.dismiss()
props.onPress()
}}
style={[
a.p_sm,
a.rounded_full,
(state.hovered || state.pressed) && t.atoms.bg_contrast_25,
// make sure pfp is in the middle
{marginLeft: -10},
]}>
<DotsHorizontal size="md" style={t.atoms.text} />
</Pressable>
)}
</Menu.Trigger>
</View>
)}
<Menu.Item
label={_(msg`Go to user's profile`)}
onPress={onNavigateToProfile}>
<Menu.ItemText>
<Trans>Go to profile</Trans>
</Menu.ItemText>
<Menu.ItemIcon icon={Person} />
</Menu.Item>
<Menu.Item
label={_(msg`Mute conversation`)}
onPress={() => muteConvo({mute: !convo?.muted})}>
<Menu.ItemText>
{convo?.muted ? (
<Trans>Unmute conversation</Trans>
) : (
<Trans>Mute conversation</Trans>
)}
</Menu.ItemText>
<Menu.ItemIcon icon={convo?.muted ? Unmute : Mute} />
</Menu.Item>
</Menu.Group>
<Menu.Divider />
<Menu.Group>
<Menu.Item
label={isBlocking ? _(msg`Unblock account`) : _(msg`Block account`)}
onPress={toggleBlock}>
<Menu.ItemText>
{isBlocking ? _(msg`Unblock account`) : _(msg`Block account`)}
</Menu.ItemText>
<Menu.ItemIcon icon={isBlocking ? PersonCheck : PersonX} />
</Menu.Item>
<Menu.Item
label={_(msg`Report conversation`)}
onPress={() => reportControl.open()}>
<Menu.ItemText>
<Trans>Report conversation</Trans>
</Menu.ItemText>
<Menu.ItemIcon icon={Flag} />
</Menu.Item>
</Menu.Group>
<Menu.Divider />
<Menu.Group>
<Menu.Item
label={_(msg`Leave conversation`)}
onPress={() => leaveConvoControl.open()}>
<Menu.ItemText>
<Trans>Leave conversation</Trans>
</Menu.ItemText>
<Menu.ItemIcon icon={ArrowBoxLeft} />
</Menu.Item>
</Menu.Group>
{isDeletedAccount ? (
<Menu.Outer>
<Menu.Item
label={_(msg`Leave conversation`)}
onPress={() => leaveConvoControl.open()}>
<Menu.ItemText>
<Trans>Leave conversation</Trans>
</Menu.ItemText>
<Menu.ItemIcon icon={ArrowBoxLeft} />
</Menu.Item>
</Menu.Outer>
) : (
<Menu.Outer>
<Menu.Group>
{showMarkAsRead && (
<Menu.Item
label={_(msg`Mark as read`)}
onPress={() =>
markAsRead({
convoId: convo?.id,
})
}>
<Menu.ItemText>
<Trans>Mark as read</Trans>
</Menu.ItemText>
<Menu.ItemIcon icon={Bubble} />
</Menu.Item>
)}
<Menu.Item
label={_(msg`Go to user's profile`)}
onPress={onNavigateToProfile}>
<Menu.ItemText>
<Trans>Go to profile</Trans>
</Menu.ItemText>
<Menu.ItemIcon icon={Person} />
</Menu.Item>
<Menu.Item
label={_(msg`Mute conversation`)}
onPress={() => muteConvo({mute: !convo?.muted})}>
<Menu.ItemText>
{convo?.muted ? (
<Trans>Unmute conversation</Trans>
) : (
<Trans>Mute conversation</Trans>
)}
</Menu.ItemText>
<Menu.ItemIcon icon={convo?.muted ? Unmute : Mute} />
</Menu.Item>
</Menu.Group>
<Menu.Divider />
<Menu.Group>
<Menu.Item
label={
isBlocking ? _(msg`Unblock account`) : _(msg`Block account`)
}
onPress={toggleBlock}>
<Menu.ItemText>
{isBlocking ? _(msg`Unblock account`) : _(msg`Block account`)}
</Menu.ItemText>
<Menu.ItemIcon icon={isBlocking ? PersonCheck : PersonX} />
</Menu.Item>
<Menu.Item
label={_(msg`Report conversation`)}
onPress={() => reportControl.open()}>
<Menu.ItemText>
<Trans>Report conversation</Trans>
</Menu.ItemText>
<Menu.ItemIcon icon={Flag} />
</Menu.Item>
</Menu.Group>
<Menu.Divider />
<Menu.Group>
<Menu.Item
label={_(msg`Leave conversation`)}
onPress={() => leaveConvoControl.open()}>
<Menu.ItemText>
<Trans>Leave conversation</Trans>
</Menu.ItemText>
<Menu.ItemIcon icon={ArrowBoxLeft} />
</Menu.Item>
</Menu.Group>
</Menu.Outer>
)}
</Menu.Root>
<LeaveConvoPrompt
control={leaveConvoControl}
convoId={convo.id}
currentScreen={currentScreen}
/>
{latestReportableMessage ? (
<ReportDialog
params={{
type: 'convoMessage',
convoId: convo.id,
message: latestReportableMessage,
}}
control={reportControl}
/>
) : (
<ReportConversationPrompt control={reportControl} />
)}
<BlockedByListDialog
control={blockedByListControl}
listBlocks={listBlocks}
/>
</>
)
}
ConvoMenu = React.memo(ConvoMenu)
export {ConvoMenu}
+10 -9
View File
@@ -1,6 +1,6 @@
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {StackActions, useNavigation} from '@react-navigation/native'
import {useNavigation} from '@react-navigation/native'
import {NavigationProp} from '#/lib/routes/types'
import {isNative} from '#/platform/detection'
@@ -13,21 +13,24 @@ export function LeaveConvoPrompt({
control,
convoId,
currentScreen,
hasMessages = true,
}: {
control: DialogOuterProps['control']
convoId: string
currentScreen: 'list' | 'conversation'
hasMessages?: boolean
}) {
const {_} = useLingui()
const navigation = useNavigation<NavigationProp>()
const {mutate: leaveConvo} = useLeaveConvo(convoId, {
onMutate: () => {
onSuccess: () => {
if (currentScreen === 'conversation') {
navigation.dispatch(
StackActions.replace('Messages', isNative ? {animation: 'pop'} : {}),
navigation.replace(
'Messages',
isNative
? {
animation: 'pop',
}
: {},
)
}
},
@@ -41,9 +44,7 @@ export function LeaveConvoPrompt({
control={control}
title={_(msg`Leave conversation`)}
description={_(
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?`,
msg`Are you sure you want to leave this conversation? Your messages will be deleted for you, but not for the other participant.`,
)}
confirmButtonCta={_(msg`Leave`)}
confirmButtonColor="negative"
+1 -4
View File
@@ -69,9 +69,7 @@ export let MessageMenu = ({
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
convo
.deleteMessage(message.id)
.then(() =>
Toast.show(_(msg({message: 'Message deleted', context: 'toast'}))),
)
.then(() => Toast.show(_(msg`Message deleted`)))
.catch(() => Toast.show(_(msg`Failed to delete message`)))
}, [_, convo, message.id])
@@ -140,7 +138,6 @@ export let MessageMenu = ({
</Menu.Root>
<ReportDialog
currentScreen="conversation"
params={{type: 'convoMessage', convoId: convo.convo.id, message}}
control={reportControl}
/>
+13 -34
View File
@@ -8,20 +8,18 @@ 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 {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 {useMaybeConvoForUser} from '#/state/queries/messages/get-convo-for-members'
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.ProfileViewDetailed
profile: AppBskyActorDefs.ProfileView
}) {
const {_} = useLingui()
const t = useTheme()
@@ -29,19 +27,10 @@ export function MessageProfileButton({
const {needsEmailVerification} = useEmail()
const verifyEmailControl = useDialogControl()
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 {data: convo, isPending} = useMaybeConvoForUser(profile.did)
const onPress = React.useCallback(() => {
if (!convoAvailability?.canChat) {
if (!convo?.id) {
return
}
@@ -50,25 +39,15 @@ export function MessageProfileButton({
return
}
if (convoAvailability.convo) {
logEvent('chat:open', {logContext: 'ProfileHeader'})
navigation.navigate('MessagesConversation', {
conversation: convoAvailability.convo.id,
})
} else {
if (convo && !convo.lastMessage) {
logEvent('chat:create', {logContext: 'ProfileHeader'})
initiateConvo([profile.did])
}
}, [
needsEmailVerification,
verifyEmailControl,
navigation,
profile.did,
initiateConvo,
convoAvailability,
])
logEvent('chat:open', {logContext: 'ProfileHeader'})
if (!convoAvailability) {
navigation.navigate('MessagesConversation', {conversation: convo.id})
}, [needsEmailVerification, verifyEmailControl, convo, navigation])
if (isPending) {
// show pending state based on declaration
if (canBeMessaged(profile)) {
return (
@@ -90,7 +69,7 @@ export function MessageProfileButton({
}
}
if (convoAvailability.canChat) {
if (convo) {
return (
<>
<Button
@@ -1,6 +1,6 @@
import React from 'react'
import {View} from 'react-native'
import {ModerationDecision} from '@atproto/api'
import {AppBskyActorDefs, ModerationCause} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -14,18 +14,20 @@ 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,
convoId,
hasMessages,
moderation,
blockInfo,
}: {
recipient: bsky.profile.AnyProfileView
recipient: AppBskyActorDefs.ProfileViewBasic
convoId: string
hasMessages: boolean
moderation: ModerationDecision
blockInfo: {
listBlocks: ModerationCause[]
userBlock: ModerationCause | undefined
}
}) {
const t = useTheme()
const {gtMobile} = useBreakpoints()
@@ -37,17 +39,7 @@ export function MessagesListBlockedFooter({
const reportControl = useDialogControl()
const blockedByListControl = useDialogControl()
const {listBlocks, userBlock} = React.useMemo(() => {
const modui = moderation.ui('profileView')
const blocks = modui.alerts.filter(alert => alert.type === 'blocking')
const listBlocks = blocks.filter(alert => alert.source.type === 'list')
const userBlock = blocks.find(alert => alert.source.type === 'user')
return {
listBlocks,
userBlock,
}
}, [moderation])
const {listBlocks, userBlock} = blockInfo
const isBlocking = !!userBlock || !!listBlocks.length
const onUnblockPress = React.useCallback(() => {

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