Compare commits
78 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3f75cd14f2 | |||
| 11eac7cbcd | |||
| 6eb489bd6c | |||
| 71221c9a91 | |||
| d4d5d6d8b9 | |||
| de68915bc0 | |||
| ea00663e7f | |||
| 12e57378e8 | |||
| 24606dbcfc | |||
| 6f31121e51 | |||
| 1abc96314b | |||
| 7cc897d096 | |||
| 6ed30213c8 | |||
| d7e8429753 | |||
| 6086bc5d5c | |||
| 199821d670 | |||
| 11dff765db | |||
| 73639aa571 | |||
| f3b6d85913 | |||
| 239d3cc535 | |||
| d49d43f6eb | |||
| a482dae936 | |||
| 25d44d8939 | |||
| e7801a0b76 | |||
| 7861cd153d | |||
| be53a168ba | |||
| b56d0c419b | |||
| 03adb4260f | |||
| 27926f093c | |||
| fe5a623b44 | |||
| fc944841ce | |||
| 66ca3e34a4 | |||
| 4788106f32 | |||
| 6414bda252 | |||
| 591fd262b3 | |||
| ede10368d5 | |||
| 1127e50248 | |||
| e4485da4ff | |||
| 6c7fd96528 | |||
| 6a1c0ff346 | |||
| ad18439148 | |||
| 7280e44ea2 | |||
| 7e6b23bd86 | |||
| d2bc22e9be | |||
| 0b50e9f109 | |||
| 0f6a99c8a5 | |||
| ea9986f127 | |||
| 5222a6e1c3 | |||
| b7f505ab00 | |||
| 34f2cc2322 | |||
| 8a30131686 | |||
| 69421d1f33 | |||
| 995808e816 | |||
| 508fa2aced | |||
| d528ae791f | |||
| b15903e0cb | |||
| 9bea450803 | |||
| fb5d56422a | |||
| ad38d905e4 | |||
| 2b34721876 | |||
| c928d6b14a | |||
| 0dd71fb087 | |||
| 54b8eacba1 | |||
| b7b47d30bf | |||
| 3561fc8112 | |||
| fe31cf4508 | |||
| a9efd94983 | |||
| 6cd6a3447d | |||
| ec8c36e614 | |||
| 84cae44cd2 | |||
| 5bf1141b55 | |||
| e1ee85622d | |||
| b1dc5179b6 | |||
| 96d77841e0 | |||
| b38edc52b8 | |||
| d68fc78e93 | |||
| 028ed2f8e2 | |||
| 67049b98c9 |
@@ -50,8 +50,8 @@ jobs:
|
||||
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
distribution: "temurin"
|
||||
java-version: "17"
|
||||
|
||||
- name: ⚙️ Install dependencies
|
||||
run: yarn install
|
||||
@@ -96,7 +96,7 @@ jobs:
|
||||
id: timestamp
|
||||
uses: nanzm/get-time-action@master
|
||||
with:
|
||||
format: 'MM-DD-HH-mm-ss'
|
||||
format: "MM-DD-HH-mm-ss"
|
||||
|
||||
- name: 🚀 Upload Production Artifact
|
||||
id: upload-artifact-production
|
||||
@@ -146,6 +146,11 @@ jobs:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
|
||||
- name: 🧹 Clear Metro cache
|
||||
if: ${{ inputs.profile == 'production' }}
|
||||
# https://github.com/expo/eas-cli/issues/2959#issuecomment-2749791326
|
||||
run: rm -rf ${TMPDIR:-/tmp}/metro-cache
|
||||
|
||||
- name: 🏗️ Build Production APK
|
||||
if: ${{ inputs.profile == 'production' }}
|
||||
run: >
|
||||
@@ -182,11 +187,11 @@ jobs:
|
||||
- name: ⬇️ Restore Cache
|
||||
id: get-base-commit
|
||||
uses: actions/cache@v4
|
||||
if: ${{ inputs.profile == 'testflight' }}
|
||||
if: ${{ inputs.profile == 'testflight-android' }}
|
||||
with:
|
||||
path: most-recent-testflight-commit.txt
|
||||
key: most-recent-testflight-commit
|
||||
|
||||
- name: ✏️ Write commit hash to cache
|
||||
if: ${{ inputs.profile == 'testflight' }}
|
||||
if: ${{ inputs.profile == 'testflight-android' }}
|
||||
run: echo ${{ github.sha }} > most-recent-testflight-commit.txt
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
build:
|
||||
if: github.repository == 'bluesky-social/social-app'
|
||||
name: Build and Submit iOS
|
||||
runs-on: macos-15
|
||||
runs-on: macos-26
|
||||
steps:
|
||||
- name: Check for EXPO_TOKEN
|
||||
run: >
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
|
||||
- uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: "16.4"
|
||||
xcode-version: "26.0"
|
||||
|
||||
- name: ☕️ Setup Cocoapods
|
||||
uses: maxim-lobanov/setup-cocoapods@v1
|
||||
@@ -98,10 +98,29 @@ jobs:
|
||||
yarn use-build-number-with-bump
|
||||
eas build -p ios
|
||||
--profile ${{ inputs.profile || 'testflight' }}
|
||||
--local --output build.ipa --non-interactive
|
||||
--local --output build.tar.gz --non-interactive
|
||||
|
||||
- name: 📂 Extract build artifact
|
||||
run: |
|
||||
if [ -f "build.tar.gz" ]; then
|
||||
echo "Extracting build.tar.gz..."
|
||||
mkdir ios-build
|
||||
tar -xzf build.tar.gz -C ios-build
|
||||
echo "Extraction completed successfully"
|
||||
else
|
||||
echo "Archive file not found!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: 🚀 Deploy
|
||||
run: eas submit -p ios --non-interactive --path build.ipa
|
||||
run: eas submit -p ios --non-interactive --path ios-build/ios/build/Bluesky.ipa
|
||||
|
||||
- name: 🪲 Upload dSYM to Sentry
|
||||
run: >
|
||||
SENTRY_ORG=blueskyweb
|
||||
SENTRY_PROJECT=app
|
||||
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
yarn sentry-cli debug-files upload ios-build/ios/build/Bluesky.app.dSYM.zip --include-sources
|
||||
|
||||
- name: 📚 Get version from package.json
|
||||
id: get-build-info
|
||||
|
||||
@@ -150,7 +150,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-26
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-build-ios
|
||||
cancel-in-progress: true
|
||||
@@ -192,7 +192,7 @@ jobs:
|
||||
|
||||
- uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: "16.2"
|
||||
xcode-version: "26.0"
|
||||
|
||||
- name: ☕️ Setup Cocoapods
|
||||
uses: maxim-lobanov/setup-cocoapods@v1
|
||||
|
||||
@@ -3,7 +3,7 @@ appId: xyz.blueskyweb.app
|
||||
- runScript:
|
||||
file: ../setupServer.js
|
||||
env:
|
||||
SERVER_PATH: ""
|
||||
SERVER_PATH: ""
|
||||
- runFlow:
|
||||
file: ../setupApp.yml
|
||||
- tapOn:
|
||||
@@ -25,7 +25,10 @@ appId: xyz.blueskyweb.app
|
||||
- tapOn:
|
||||
id: "passwordInput"
|
||||
- inputText: "hunter22"
|
||||
- hideKeyboard
|
||||
- tapOn:
|
||||
# hideKeyboard on ios is borked
|
||||
# https://docs.maestro.dev/troubleshooting/known-issues#hidekeyboard-command-is-flaky
|
||||
text: "Your account"
|
||||
- tapOn:
|
||||
id: "nextBtn"
|
||||
- tapOn:
|
||||
@@ -34,7 +37,7 @@ appId: xyz.blueskyweb.app
|
||||
- inputText: "e2e-test"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: "handleAvailableCheck"
|
||||
id: "handleAvailableCheck"
|
||||
- tapOn:
|
||||
id: "nextBtn"
|
||||
- assertVisible: "Give your profile a face"
|
||||
|
||||
@@ -17,63 +17,52 @@ appId: xyz.blueskyweb.app
|
||||
id: "e2eGotoLists"
|
||||
- tapOn:
|
||||
id: "newUserListBtn"
|
||||
- assertVisible:
|
||||
id: "createOrEditListModal"
|
||||
- waitForAnimationToEnd
|
||||
- assertVisible: "Create user list"
|
||||
- tapOn:
|
||||
id: "editNameInput"
|
||||
id: "editListNameInput"
|
||||
- inputText: "Good Ppl"
|
||||
- tapOn:
|
||||
id: "editDescriptionInput"
|
||||
id: "editListDescriptionInput"
|
||||
- inputText: "They good"
|
||||
- tapOn: "Save"
|
||||
- tapOn: "Save"
|
||||
- assertNotVisible:
|
||||
id: "createOrEditListModal"
|
||||
- assertNotVisible: "Create user list"
|
||||
- tapOn: "People"
|
||||
- assertVisible: "Good Ppl"
|
||||
- assertVisible: "They good"
|
||||
|
||||
- tapOn:
|
||||
label: "Edit display name and description via the edit curatelist modal"
|
||||
point: "90%,9%"
|
||||
id: "moreOptionsBtn"
|
||||
- tapOn: "Edit list details"
|
||||
- assertVisible:
|
||||
id: "createOrEditListModal"
|
||||
- assertVisible: "Edit user list"
|
||||
- tapOn:
|
||||
id: "editNameInput"
|
||||
id: "editListNameInput"
|
||||
- eraseText
|
||||
- inputText: "Bad Ppl"
|
||||
- hideKeyboard
|
||||
- tapOn:
|
||||
id: "editDescriptionInput"
|
||||
id: "editListDescriptionInput"
|
||||
- eraseText
|
||||
- inputText: "They bad"
|
||||
- tapOn: "Save"
|
||||
- tapOn: "Save"
|
||||
- assertNotVisible:
|
||||
id: "createOrEditListModal"
|
||||
- assertNotVisible: "Edit user list"
|
||||
- assertVisible: Bad Ppl
|
||||
- assertVisible: They bad
|
||||
|
||||
- tapOn:
|
||||
label: "Remove description via the edit curatelist modal"
|
||||
point: "90%,9%"
|
||||
id: "moreOptionsBtn"
|
||||
- tapOn: "Edit list details"
|
||||
- assertVisible:
|
||||
id: "createOrEditListModal"
|
||||
- assertVisible: "Edit user list"
|
||||
- tapOn:
|
||||
id: "editDescriptionInput"
|
||||
id: "editListDescriptionInput"
|
||||
- eraseText
|
||||
- tapOn: "Save"
|
||||
- tapOn: "Save"
|
||||
- assertNotVisible:
|
||||
id: "createOrEditListModal"
|
||||
- assertNotVisible: "Edit user list"
|
||||
- assertNotVisible:
|
||||
id: "listDescription"
|
||||
|
||||
- tapOn:
|
||||
label: "Delete the curatelist"
|
||||
point: "90%,9%"
|
||||
id: "moreOptionsBtn"
|
||||
- tapOn: "Delete List"
|
||||
- tapOn:
|
||||
id: "confirmBtn"
|
||||
@@ -82,18 +71,15 @@ appId: xyz.blueskyweb.app
|
||||
id: "newUserListBtn"
|
||||
- tapOn:
|
||||
id: "newUserListBtn"
|
||||
- assertVisible:
|
||||
id: "createOrEditListModal"
|
||||
- assertVisible: "Create user list"
|
||||
- tapOn:
|
||||
id: "editNameInput"
|
||||
id: "editListNameInput"
|
||||
- inputText: "Good Ppl"
|
||||
- tapOn:
|
||||
id: "editDescriptionInput"
|
||||
id: "editListDescriptionInput"
|
||||
- inputText: "They good"
|
||||
- tapOn: "Save"
|
||||
- tapOn: "Save"
|
||||
- assertNotVisible:
|
||||
id: "createOrEditListModal"
|
||||
- assertNotVisible: "Create user list"
|
||||
- tapOn: "People"
|
||||
- assertVisible: "Good Ppl"
|
||||
- assertVisible: "They good"
|
||||
|
||||
@@ -19,21 +19,17 @@ appId: xyz.blueskyweb.app
|
||||
id: "moderationlistsBtn"
|
||||
- tapOn: "New list"
|
||||
- tapOn:
|
||||
id: "editNameInput"
|
||||
id: "editListNameInput"
|
||||
- inputText: "Muted Users"
|
||||
- tapOn:
|
||||
id: "editDescriptionInput"
|
||||
id: "editListDescriptionInput"
|
||||
- inputText: "Shhh"
|
||||
- tapOn: "Save"
|
||||
- tapOn: "Save"
|
||||
|
||||
# view modlist
|
||||
- assertVisible: "Muted Users"
|
||||
- assertVisible: "Shhh"
|
||||
|
||||
# DOES NOT WORK - THE BUTTON IS NOT ACCESSIBLE
|
||||
# IGNORING FOR NOW, FIX THE COMPONENTS IN THE NEXT RELEASE
|
||||
# BECAUSE THIS IS A LEGIT A11Y PROBLEM -sfn
|
||||
- tapOn: "Subscribe to this list"
|
||||
- tapOn: "Mute accounts"
|
||||
- tapOn: "Mute list"
|
||||
|
||||
@@ -30,7 +30,14 @@ appId: xyz.blueskyweb.app
|
||||
- assertVisible: "What are your interests?"
|
||||
- tapOn:
|
||||
id: "onboardingContinue"
|
||||
- assertVisible: "You're ready to go!"
|
||||
- assertVisible: "Suggested for you"
|
||||
# mock server doesn't have suggestions api
|
||||
- tapOn: "Skip to next step"
|
||||
- swipe:
|
||||
direction: "LEFT"
|
||||
- swipe:
|
||||
direction: "LEFT"
|
||||
- assertVisible: "Complete onboarding and start using your account"
|
||||
- tapOn:
|
||||
id: "onboardingFinish"
|
||||
- tapOn:
|
||||
|
||||
@@ -16,7 +16,8 @@ appId: xyz.blueskyweb.app
|
||||
id: "e2eStartOnboarding"
|
||||
- tapOn: "Select an avatar"
|
||||
- waitForAnimationToEnd
|
||||
- assertVisible: "Search your library…"
|
||||
- assertVisible: "Photos"
|
||||
- assertVisible: "Collections"
|
||||
- tapOn:
|
||||
point: "50%,22%"
|
||||
- waitForAnimationToEnd
|
||||
@@ -27,7 +28,14 @@ appId: xyz.blueskyweb.app
|
||||
- assertVisible: "What are your interests?"
|
||||
- tapOn:
|
||||
id: "onboardingContinue"
|
||||
- assertVisible: "You're ready to go!"
|
||||
- assertVisible: "Suggested for you"
|
||||
# mock server doesn't have suggestions api
|
||||
- tapOn: "Skip to next step"
|
||||
- swipe:
|
||||
direction: "LEFT"
|
||||
- swipe:
|
||||
direction: "LEFT"
|
||||
- assertVisible: "Complete onboarding and start using your account"
|
||||
- tapOn:
|
||||
id: "onboardingFinish"
|
||||
- tapOn:
|
||||
|
||||
@@ -31,6 +31,8 @@ appId: xyz.blueskyweb.app
|
||||
id: "viewHeaderHomeFeedPrefsBtn"
|
||||
- tapOn:
|
||||
id: "replyBtn"
|
||||
- tapOn:
|
||||
id: "replyBtn"
|
||||
- inputText: "Reply 1"
|
||||
- tapOn:
|
||||
id: "composerPublishBtn"
|
||||
@@ -69,6 +71,8 @@ appId: xyz.blueskyweb.app
|
||||
id: "profilePager-selector-1"
|
||||
- tapOn:
|
||||
id: "replyBtn"
|
||||
- tapOn:
|
||||
id: "replyBtn"
|
||||
- inputText: "Reply 2"
|
||||
- tapOn:
|
||||
id: "composerPublishBtn"
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
appId: xyz.blueskyweb.app
|
||||
---
|
||||
- launchApp:
|
||||
appId: "xyz.blueskyweb.app"
|
||||
clearState: true
|
||||
appId: "xyz.blueskyweb.app"
|
||||
clearState: true
|
||||
- waitForAnimationToEnd
|
||||
- tapOn: "http://localhost:8081"
|
||||
- waitForAnimationToEnd
|
||||
- extendedWaitUntil:
|
||||
visible: "Continue"
|
||||
- swipe:
|
||||
from: "Bluesky"
|
||||
direction: DOWN
|
||||
from: "Bluesky"
|
||||
direction: DOWN
|
||||
duration: 100
|
||||
- tapOn:
|
||||
id: e2eProxyHeaderInput
|
||||
id: e2eProxyHeaderInput
|
||||
- inputText: ${output.result}
|
||||
- pressKey: Enter
|
||||
- hideKeyboard
|
||||
|
||||
@@ -40,7 +40,7 @@ module.exports = function (_config) {
|
||||
runtimeVersion: {
|
||||
policy: 'appVersion',
|
||||
},
|
||||
icon: './assets/app-icons/ios_icon_default_light.png',
|
||||
icon: './assets/app-icons/ios_icon_default_next.png',
|
||||
userInterfaceStyle: 'automatic',
|
||||
primaryColor: '#1083fe',
|
||||
newArchEnabled: false,
|
||||
@@ -112,6 +112,34 @@ module.exports = function (_config) {
|
||||
'com.apple.security.application-groups': 'group.app.bsky',
|
||||
},
|
||||
privacyManifests: {
|
||||
NSPrivacyCollectedDataTypes: [
|
||||
{
|
||||
NSPrivacyCollectedDataType: 'NSPrivacyCollectedDataTypeCrashData',
|
||||
NSPrivacyCollectedDataTypeLinked: false,
|
||||
NSPrivacyCollectedDataTypeTracking: false,
|
||||
NSPrivacyCollectedDataTypePurposes: [
|
||||
'NSPrivacyCollectedDataTypePurposeAppFunctionality',
|
||||
],
|
||||
},
|
||||
{
|
||||
NSPrivacyCollectedDataType:
|
||||
'NSPrivacyCollectedDataTypePerformanceData',
|
||||
NSPrivacyCollectedDataTypeLinked: false,
|
||||
NSPrivacyCollectedDataTypeTracking: false,
|
||||
NSPrivacyCollectedDataTypePurposes: [
|
||||
'NSPrivacyCollectedDataTypePurposeAppFunctionality',
|
||||
],
|
||||
},
|
||||
{
|
||||
NSPrivacyCollectedDataType:
|
||||
'NSPrivacyCollectedDataTypeOtherDiagnosticData',
|
||||
NSPrivacyCollectedDataTypeLinked: false,
|
||||
NSPrivacyCollectedDataTypeTracking: false,
|
||||
NSPrivacyCollectedDataTypePurposes: [
|
||||
'NSPrivacyCollectedDataTypePurposeAppFunctionality',
|
||||
],
|
||||
},
|
||||
],
|
||||
NSPrivacyAccessedAPITypes: [
|
||||
{
|
||||
NSPrivacyAccessedAPIType:
|
||||
@@ -143,7 +171,7 @@ module.exports = function (_config) {
|
||||
barStyle: 'light-content',
|
||||
},
|
||||
android: {
|
||||
icon: './assets/app-icons/android_icon_default_light.png',
|
||||
icon: './assets/app-icons/android_icon_default_next.png',
|
||||
adaptiveIcon: {
|
||||
foregroundImage: './assets/icon-android-foreground.png',
|
||||
monochromeImage: './assets/icon-android-foreground.png',
|
||||
@@ -305,8 +333,8 @@ module.exports = function (_config) {
|
||||
prerendered: true,
|
||||
},
|
||||
next: {
|
||||
ios: './assets/app-icons/icon_default_next.png',
|
||||
android: './assets/app-icons/icon_default_next.png',
|
||||
ios: './assets/app-icons/ios_icon_default_next.png',
|
||||
android: './assets/app-icons/android_icon_default_next.png',
|
||||
prerendered: true,
|
||||
},
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 369 KiB |
|
Before Width: | Height: | Size: 486 KiB |
|
After Width: | Height: | Size: 426 KiB |
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 125 KiB |
@@ -330,7 +330,7 @@ function ExternalEmbed({
|
||||
{content.external.thumb && (
|
||||
<img
|
||||
src={content.external.thumb}
|
||||
className="aspect-[1.91/1] object-cover"
|
||||
className="aspect-[1200/630] object-cover"
|
||||
/>
|
||||
)}
|
||||
<div className="py-3 px-4">
|
||||
@@ -435,7 +435,7 @@ function StarterPackEmbed({
|
||||
<Link
|
||||
href={starterPackHref}
|
||||
className="w-full rounded-xl overflow-hidden border dark:border-slate-600 flex flex-col items-stretch">
|
||||
<img src={imageUri} className="aspect-[1.91/1] object-cover" />
|
||||
<img src={imageUri} className="aspect-[1200/630] object-cover" />
|
||||
<div className="py-3 px-4">
|
||||
<div className="flex space-x-2 items-center">
|
||||
<img src={starterPackIcon} className="w-10 h-10" />
|
||||
|
||||
@@ -15,7 +15,7 @@ import {redirectLogger} from '../logger.js'
|
||||
|
||||
const SAFELINK_MIN_FETCH_INTERVAL = 1_000
|
||||
const SAFELINK_MAX_FETCH_INTERVAL = 10_000
|
||||
const SCHEME_REGEX = /^[a-zA-Z][a-zA-Z0-9+.-]*:/
|
||||
const SCHEME_REGEX = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//
|
||||
|
||||
export class SafelinkClient {
|
||||
private domainCache: LRUCache<string, SafelinkRule | 'ok'>
|
||||
|
||||
@@ -62,7 +62,12 @@
|
||||
"@type": "DiscussionForumPosting",
|
||||
"author": {
|
||||
"@type": "Person",
|
||||
{%- if postView.Author.DisplayName %}
|
||||
"name": "{{ postView.Author.DisplayName }}",
|
||||
"alternateName": "@{{ postView.Author.Handle }}",
|
||||
{% else %}
|
||||
"name": "@{{ postView.Author.Handle }}",
|
||||
{% endif -%}
|
||||
"url": "https://bsky.app/profile/{{ postView.Author.Handle }}"
|
||||
},
|
||||
{%- if postText %}
|
||||
|
||||
@@ -59,8 +59,12 @@
|
||||
"dateCreated": "{{ profileView.CreatedAt }}",
|
||||
"mainEntity": {
|
||||
"@type": "Person",
|
||||
{%- if profileView.DisplayName %}
|
||||
"name": "{{ profileView.DisplayName }}",
|
||||
"alternateName": "@{{ profileView.Handle }}",
|
||||
{% else %}
|
||||
"name": "@{{ profileView.Handle }}",
|
||||
{% endif -%}
|
||||
"identifier": "{{ profileView.Did }}",
|
||||
"description": "{{ profileView.Description }}",
|
||||
"image": "{{ profileView.Avatar }}",
|
||||
|
||||
@@ -75,7 +75,7 @@ After you do `yarn ios` and `yarn android` once, you can later just run `yarn we
|
||||
|
||||
- Start in various console tabs:
|
||||
- `yarn e2e:mock-server`
|
||||
- `yarn e2e:metro`
|
||||
- `yarn e2e:start`
|
||||
- Run once: `yarn e2e:build`
|
||||
- Each test run: `yarn e2e:run`
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ values.
|
||||
### Running Maestro tests
|
||||
|
||||
- In one tab, run `yarn e2e:mock-server`
|
||||
- In a second tab, run `yarn e2e:metro`
|
||||
- In a second tab, run `yarn e2e:build`
|
||||
- In a third tab, run `yarn e2e:run`
|
||||
|
||||
## Using Flashlight for Performance Testing
|
||||
|
||||
@@ -36,7 +36,8 @@
|
||||
"extends": "base",
|
||||
"ios": {
|
||||
"resourceClass": "large",
|
||||
"autoIncrement": true
|
||||
"autoIncrement": true,
|
||||
"buildArtifactPaths": ["ios/build/*"]
|
||||
},
|
||||
"android": {
|
||||
"autoIncrement": true
|
||||
@@ -50,7 +51,8 @@
|
||||
"extends": "base",
|
||||
"distribution": "internal",
|
||||
"ios": {
|
||||
"autoIncrement": false
|
||||
"autoIncrement": false,
|
||||
"buildArtifactPaths": ["ios/build/*"]
|
||||
},
|
||||
"android": {
|
||||
"autoIncrement": false
|
||||
@@ -63,7 +65,8 @@
|
||||
"testflight": {
|
||||
"extends": "base",
|
||||
"ios": {
|
||||
"autoIncrement": true
|
||||
"autoIncrement": true,
|
||||
"buildArtifactPaths": ["ios/build/*"]
|
||||
},
|
||||
"android": {
|
||||
"autoIncrement": true
|
||||
@@ -77,7 +80,8 @@
|
||||
"extends": "base",
|
||||
"distribution": "internal",
|
||||
"ios": {
|
||||
"autoIncrement": true
|
||||
"autoIncrement": true,
|
||||
"buildArtifactPaths": ["ios/build/*"]
|
||||
},
|
||||
"android": {
|
||||
"autoIncrement": true
|
||||
|
||||
@@ -46,8 +46,9 @@
|
||||
"lint-native:fix": "swiftlint --fix ./modules && ktlint --format ./modules",
|
||||
"typecheck": "tsc --project ./tsconfig.check.json",
|
||||
"e2e:mock-server": "NODE_ENV=development ./jest/dev-infra/with-test-redis-and-db.sh ts-node --project tsconfig.e2e.json __e2e__/mock-server.ts",
|
||||
"e2e:metro": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios",
|
||||
"e2e:metro-android": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:android",
|
||||
"e2e:build": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios",
|
||||
"e2e:build-android": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:android",
|
||||
"e2e:start": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo start",
|
||||
"e2e:run": "maestro test __e2e__",
|
||||
"perf:test": "NODE_ENV=test maestro test",
|
||||
"perf:test:run": "NODE_ENV=test maestro test __e2e__/perf-test.yml",
|
||||
@@ -71,10 +72,11 @@
|
||||
"icons:optimize": "svgo -f ./assets/icons"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "^0.17.0",
|
||||
"@atproto/api": "^0.17.1",
|
||||
"@bitdrift/react-native": "^0.6.8",
|
||||
"@braintree/sanitize-url": "^6.0.2",
|
||||
"@bsky.app/alf": "^0.1.2",
|
||||
"@bsky.app/alf": "^0.1.5",
|
||||
"@bsky.app/react-native-mmkv": "2.12.5",
|
||||
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
"@expo/html-elements": "^0.12.5",
|
||||
@@ -145,6 +147,7 @@
|
||||
"expo-image-manipulator": "~14.0.7",
|
||||
"expo-image-picker": "~17.0.8",
|
||||
"expo-intent-launcher": "~13.0.7",
|
||||
"expo-keep-awake": "^15.0.7",
|
||||
"expo-linear-gradient": "~15.0.7",
|
||||
"expo-linking": "~8.0.8",
|
||||
"expo-localization": "~17.0.7",
|
||||
@@ -192,7 +195,6 @@
|
||||
"react-native-gesture-handler": "~2.28.0",
|
||||
"react-native-get-random-values": "~1.11.0",
|
||||
"react-native-keyboard-controller": "1.18.5",
|
||||
"@bsky.app/react-native-mmkv": "2.12.5",
|
||||
"react-native-pager-view": "6.8.0",
|
||||
"react-native-progress": "bluesky-social/react-native-progress",
|
||||
"react-native-qrcode-styled": "^0.3.3",
|
||||
@@ -219,7 +221,7 @@
|
||||
"zod": "^3.20.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@atproto/dev-env": "^0.3.172",
|
||||
"@atproto/dev-env": "^0.3.181",
|
||||
"@babel/core": "^7.26.0",
|
||||
"@babel/preset-env": "^7.26.0",
|
||||
"@babel/runtime": "^7.26.0",
|
||||
@@ -271,6 +273,7 @@
|
||||
"react-refresh": "^0.14.0",
|
||||
"svgo": "^3.3.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"ts-plugin-sort-import-suggestions": "^1.0.4",
|
||||
"typescript": "^5.9.2",
|
||||
"webpack-bundle-analyzer": "^4.10.1"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
diff --git a/node_modules/react-native-uitextview/ios/RNUITextViewShadow.swift b/node_modules/react-native-uitextview/ios/RNUITextViewShadow.swift
|
||||
index c34ba71..13d576a 100644
|
||||
--- a/node_modules/react-native-uitextview/ios/RNUITextViewShadow.swift
|
||||
+++ b/node_modules/react-native-uitextview/ios/RNUITextViewShadow.swift
|
||||
@@ -159,13 +159,23 @@ class RNUITextViewShadow: RCTShadowView {
|
||||
let maxSize = CGSize(width: CGFloat(maxWidth), height: CGFloat(MAXFLOAT))
|
||||
let textSize = self.attributedText.boundingRect(with: maxSize, options: .usesLineFragmentOrigin, context: nil)
|
||||
|
||||
- var totalLines = self.lineHeight == 0.0 ? 0 : Int(ceil(textSize.height / self.lineHeight))
|
||||
-
|
||||
- if self.numberOfLines != 0, totalLines > self.numberOfLines {
|
||||
- totalLines = self.numberOfLines
|
||||
+ var finalHeight: CGFloat
|
||||
+
|
||||
+ if self.numberOfLines != 0 && self.lineHeight != 0.0 {
|
||||
+ // numberOfLines is set with custom line height - need to calculate lines and snap to lineHeight multiples
|
||||
+ // NOTE: this calculation can be inaccurate with fractional font sizes
|
||||
+ var totalLines = Int(ceil(textSize.height / self.lineHeight))
|
||||
+ if totalLines > self.numberOfLines {
|
||||
+ totalLines = self.numberOfLines
|
||||
+ }
|
||||
+ finalHeight = CGFloat(totalLines) * self.lineHeight
|
||||
+ } else {
|
||||
+ // Either no numberOfLines limit, or no custom lineHeight - use actual text height
|
||||
+ // (numberOfLines without custom lineHeight is handled by the UITextView's textContainer.maximumNumberOfLines)
|
||||
+ finalHeight = textSize.height
|
||||
}
|
||||
|
||||
- self.frameSize = CGSize(width: CGFloat(maxWidth), height: CGFloat(CGFloat(totalLines) * self.lineHeight))
|
||||
+ self.frameSize = CGSize(width: CGFloat(maxWidth), height: finalHeight)
|
||||
return YGSize(width: Float(self.frameSize.width), height: Float(self.frameSize.height))
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {type StyleProp, type ViewStyle} from 'react-native'
|
||||
import {atoms as baseAtoms} from '@bsky.app/alf'
|
||||
|
||||
import {CARD_ASPECT_RATIO} from '#/lib/constants'
|
||||
import {native, platform, web} from '#/alf/util/platform'
|
||||
import * as Layout from '#/components/Layout'
|
||||
|
||||
@@ -31,6 +32,16 @@ export const atoms = {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
|
||||
/**
|
||||
* Aspect ratios
|
||||
*/
|
||||
aspect_square: {
|
||||
aspectRatio: 1,
|
||||
},
|
||||
aspect_card: {
|
||||
aspectRatio: CARD_ASPECT_RATIO,
|
||||
},
|
||||
|
||||
/*
|
||||
* Transition
|
||||
*/
|
||||
@@ -67,7 +78,7 @@ export const atoms = {
|
||||
}),
|
||||
|
||||
/*
|
||||
* Animaations
|
||||
* Animations
|
||||
*/
|
||||
fade_in: web({
|
||||
animation: 'fadeIn ease-out 0.15s',
|
||||
|
||||
@@ -172,7 +172,7 @@ function AccountItem({
|
||||
</View>
|
||||
|
||||
{isCurrentAccount ? (
|
||||
<Check size="sm" style={[{color: t.palette.positive_600}]} />
|
||||
<Check size="sm" style={[{color: t.palette.positive_500}]} />
|
||||
) : (
|
||||
<Chevron size="sm" style={[t.atoms.text]} />
|
||||
)}
|
||||
|
||||
@@ -7,13 +7,14 @@ import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfoIcon} from '#/components
|
||||
import {CircleX_Stroke2_Corner0_Rounded as CircleXIcon} from '#/components/icons/CircleX'
|
||||
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
|
||||
import {Text as BaseText, type TextProps} from '#/components/Typography'
|
||||
import {EmojiSad_Stroke2_Corner0_Rounded as EmojiSadIcon} from './icons/Emoji'
|
||||
|
||||
export const colors = {
|
||||
warning: '#FFC404',
|
||||
}
|
||||
|
||||
type Context = {
|
||||
type: 'info' | 'tip' | 'warning' | 'error'
|
||||
type: 'info' | 'tip' | 'warning' | 'error' | 'apology'
|
||||
}
|
||||
|
||||
const Context = createContext<Context>({
|
||||
@@ -29,12 +30,14 @@ export function Icon() {
|
||||
tip: CircleInfoIcon,
|
||||
warning: WarningIcon,
|
||||
error: CircleXIcon,
|
||||
apology: EmojiSadIcon,
|
||||
}[type]
|
||||
const fill = {
|
||||
info: t.atoms.text_contrast_medium.color,
|
||||
tip: t.palette.primary_500,
|
||||
warning: colors.warning,
|
||||
error: t.palette.negative_500,
|
||||
apology: t.atoms.text_contrast_medium.color,
|
||||
}[type]
|
||||
return <Icon fill={fill} size="md" />
|
||||
}
|
||||
@@ -87,7 +90,7 @@ export function Row({
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
return (
|
||||
<View style={[a.flex_1, a.flex_row, a.align_start, a.gap_sm, style]}>
|
||||
<View style={[a.w_full, a.flex_row, a.align_start, a.gap_sm, style]}>
|
||||
{children}
|
||||
</View>
|
||||
)
|
||||
@@ -109,6 +112,7 @@ export function Outer({
|
||||
tip: t.palette.primary_500,
|
||||
warning: colors.warning,
|
||||
error: t.palette.negative_500,
|
||||
apology: t.atoms.border_contrast_high.borderColor,
|
||||
}[type]
|
||||
return (
|
||||
<Context.Provider value={{type}}>
|
||||
|
||||
@@ -62,7 +62,7 @@ export function HeaderText({
|
||||
style?: StyleProp<TextStyle>
|
||||
}) {
|
||||
return (
|
||||
<Text style={[a.text_lg, a.text_center, a.font_bold, style]}>
|
||||
<Text style={[a.text_lg, a.text_center, a.font_semi_bold, style]}>
|
||||
{children}
|
||||
</Text>
|
||||
)
|
||||
|
||||
@@ -390,7 +390,12 @@ export function ProfileGrid({
|
||||
{!isProfileHeaderContext && (
|
||||
<InlineLinkText
|
||||
label={_(msg`See more suggested profiles on the Explore page`)}
|
||||
to="/search">
|
||||
to="/search"
|
||||
onPress={() => {
|
||||
logger.metric('suggestedUser:seeMore', {
|
||||
logContext: isFeedContext ? 'Explore' : 'Profile',
|
||||
})
|
||||
}}>
|
||||
<Trans>See more</Trans>
|
||||
</InlineLinkText>
|
||||
)}
|
||||
@@ -429,6 +434,11 @@ function SeeMoreSuggestedProfilesCard() {
|
||||
to="/search"
|
||||
color="primary"
|
||||
label={_(msg`Browse more accounts on the Explore page`)}
|
||||
onPress={() => {
|
||||
logger.metric('suggestedUser:seeMore', {
|
||||
logContext: 'Explore',
|
||||
})
|
||||
}}
|
||||
style={[
|
||||
a.flex_col,
|
||||
a.align_center,
|
||||
|
||||
@@ -258,7 +258,7 @@ export function InterestTabs({
|
||||
t.atoms.border_contrast_low,
|
||||
t.atoms.bg,
|
||||
a.h_full,
|
||||
{aspectRatio: 1},
|
||||
a.aspect_square,
|
||||
a.rounded_full,
|
||||
]}>
|
||||
<ButtonIcon icon={ArrowLeft} />
|
||||
@@ -292,7 +292,7 @@ export function InterestTabs({
|
||||
t.atoms.border_contrast_low,
|
||||
t.atoms.bg,
|
||||
a.h_full,
|
||||
{aspectRatio: 1},
|
||||
a.aspect_square,
|
||||
a.rounded_full,
|
||||
]}>
|
||||
<ButtonIcon icon={ArrowRight} />
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import {useId} from 'react'
|
||||
import {useKeepAwake} from 'expo-keep-awake'
|
||||
import {useIsFocused} from '@react-navigation/native'
|
||||
|
||||
/**
|
||||
* Stops the screen from sleeping. Only applies to the current screen.
|
||||
*
|
||||
* Note: Expo keeps the screen permanently awake when in dev mode, so
|
||||
* you'll only see this do anything when in production.
|
||||
*
|
||||
* @platform ios, android
|
||||
*/
|
||||
export function KeepAwake({enabled = true}) {
|
||||
const isFocused = useIsFocused()
|
||||
if (enabled && isFocused) {
|
||||
return <KeepAwakeInner />
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function KeepAwakeInner() {
|
||||
const id = useId()
|
||||
// if you don't pass an explicit ID, any `useKeepAwake` hook unmounting disables them all.
|
||||
// very strange behaviour, but easily fixed by passing a unique ID -sfn
|
||||
useKeepAwake(id)
|
||||
return null
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export function KeepAwake() {
|
||||
return null
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
import {View, type ViewStyle} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {type Gate} from '#/lib/statsig/gates'
|
||||
import {useGate} from '#/lib/statsig/statsig'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useSession} from '#/state/session'
|
||||
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
||||
import {Logo} from '#/view/icons/Logo'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
interface LoggedOutCTAProps {
|
||||
style?: ViewStyle
|
||||
gateName: Gate
|
||||
}
|
||||
|
||||
export function LoggedOutCTA({style, gateName}: LoggedOutCTAProps) {
|
||||
const {hasSession} = useSession()
|
||||
const {requestSwitchToAccount} = useLoggedOutViewControls()
|
||||
const gate = useGate()
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
|
||||
// Only show for logged-out users on web
|
||||
if (hasSession || !isWeb) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Check gate at the last possible moment to avoid counting users as exposed when they won't see the element
|
||||
if (!gate(gateName)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[a.pb_md, style]}>
|
||||
<View
|
||||
style={[
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.justify_between,
|
||||
a.px_lg,
|
||||
a.py_md,
|
||||
a.rounded_md,
|
||||
a.mb_xs,
|
||||
t.atoms.bg_contrast_25,
|
||||
]}>
|
||||
<View style={[a.flex_row, a.align_center, a.flex_1, a.pr_md]}>
|
||||
<Logo width={30} style={[a.mr_md]} />
|
||||
<View style={[a.flex_1]}>
|
||||
<Text style={[a.text_lg, a.font_semi_bold, a.leading_snug]}>
|
||||
<Trans>Join Bluesky</Trans>
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
a.text_md,
|
||||
a.font_medium,
|
||||
a.leading_snug,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}>
|
||||
<Trans>The open social network.</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Button
|
||||
onPress={() => {
|
||||
requestSwitchToAccount({requestedAccount: 'new'})
|
||||
}}
|
||||
label={_(msg`Create account`)}
|
||||
size="small"
|
||||
variant="solid"
|
||||
color="primary">
|
||||
<ButtonText>
|
||||
<Trans>Create account</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -87,7 +87,7 @@ export function ImageItem({
|
||||
}) {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<View style={[a.relative, a.flex_1, {aspectRatio: 1, maxWidth: 100}]}>
|
||||
<View style={[a.relative, a.flex_1, a.aspect_square, {maxWidth: 100}]}>
|
||||
<Image
|
||||
key={thumbnail}
|
||||
source={{uri: thumbnail}}
|
||||
@@ -131,7 +131,8 @@ export function VideoItem({
|
||||
style={[
|
||||
{backgroundColor: 'black'},
|
||||
a.flex_1,
|
||||
{aspectRatio: 1, maxWidth: 100},
|
||||
a.aspect_square,
|
||||
{maxWidth: 100},
|
||||
a.justify_center,
|
||||
a.align_center,
|
||||
]}>
|
||||
|
||||
@@ -97,9 +97,7 @@ export const ExternalEmbed = ({
|
||||
]}>
|
||||
{imageUri && !embedPlayerParams ? (
|
||||
<Image
|
||||
style={{
|
||||
aspectRatio: 1.91,
|
||||
}}
|
||||
style={[a.aspect_card]}
|
||||
source={{uri: imageUri}}
|
||||
accessibilityIgnoresInvertColors
|
||||
/>
|
||||
|
||||
@@ -17,7 +17,7 @@ export function FeedEmbed({
|
||||
return (
|
||||
<FeedCard.Link
|
||||
view={embed.view}
|
||||
style={[a.border, t.atoms.border_contrast_medium, a.p_md, a.rounded_sm]}>
|
||||
style={[a.border, t.atoms.border_contrast_low, a.p_md, a.rounded_sm]}>
|
||||
<FeedCard.Outer>
|
||||
<FeedCard.Header>
|
||||
<FeedCard.Avatar src={embed.view.avatar} />
|
||||
|
||||
@@ -17,7 +17,7 @@ export function ListEmbed({
|
||||
return (
|
||||
<ListCard.Default
|
||||
view={embed.view}
|
||||
style={[a.border, t.atoms.border_contrast_medium, a.p_md, a.rounded_sm]}
|
||||
style={[a.border, t.atoms.border_contrast_low, a.p_md, a.rounded_sm]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import {ContentHider} from '#/components/moderation/ContentHider'
|
||||
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
||||
import {RichText} from '#/components/RichText'
|
||||
import {Embed as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
|
||||
import {SubtleWebHover} from '#/components/SubtleWebHover'
|
||||
import {SubtleHover} from '#/components/SubtleHover'
|
||||
import * as bsky from '#/types/bsky'
|
||||
import {
|
||||
type Embed as TEmbed,
|
||||
@@ -284,7 +284,7 @@ export function QuoteEmbed({
|
||||
childContainerStyle={[a.pt_sm]}>
|
||||
{({active}) => (
|
||||
<>
|
||||
{!active && <SubtleWebHover hover={hover} style={[a.rounded_md]} />}
|
||||
{!active && <SubtleHover hover={hover} style={[a.rounded_md]} />}
|
||||
<Link
|
||||
style={[!active && a.p_md]}
|
||||
hoverStyle={{borderColor: pal.colors.borderLinkHover}}
|
||||
|
||||
@@ -57,7 +57,9 @@ export function PostRepliedTo({
|
||||
size="xs"
|
||||
style={[t.atoms.text_contrast_medium, {top: -1}]}
|
||||
/>
|
||||
<Text style={textStyle}>{label}</Text>
|
||||
<Text style={[a.flex_1, textStyle]} numberOfLines={1}>
|
||||
{label}
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import {t} from '@lingui/macro'
|
||||
import {DISCOVER_DEBUG_DIDS} from '#/lib/constants'
|
||||
import {useGate} from '#/lib/statsig/statsig'
|
||||
import {useSession} from '#/state/session'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {IS_INTERNAL} from '#/env'
|
||||
|
||||
@@ -16,7 +16,6 @@ export function DiscoverDebug({
|
||||
feedContext: string | undefined
|
||||
}) {
|
||||
const {currentAccount} = useSession()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const gate = useGate()
|
||||
const isDiscoverDebugUser =
|
||||
IS_INTERNAL ||
|
||||
@@ -30,15 +29,11 @@ export function DiscoverDebug({
|
||||
<Pressable
|
||||
accessible={false}
|
||||
hitSlop={10}
|
||||
style={[
|
||||
a.absolute,
|
||||
{zIndex: 1000, maxWidth: 65, bottom: -4},
|
||||
gtMobile ? a.right_0 : a.left_0,
|
||||
]}
|
||||
style={[a.absolute, {zIndex: 1000, maxWidth: 65, bottom: -4}, a.left_0]}
|
||||
onPress={e => {
|
||||
e.stopPropagation()
|
||||
Clipboard.setStringAsync(feedContext)
|
||||
Toast.show(t`Copied to clipboard`, 'clipboard-check')
|
||||
Toast.show(t`Copied to clipboard`)
|
||||
}}>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
|
||||
@@ -46,7 +46,12 @@ import {
|
||||
useProfileBlockMutationQueue,
|
||||
useProfileMuteMutationQueue,
|
||||
} from '#/state/queries/profile'
|
||||
import {useToggleReplyVisibilityMutation} from '#/state/queries/threadgate'
|
||||
import {
|
||||
InvalidInteractionSettingsError,
|
||||
MAX_HIDDEN_REPLIES,
|
||||
MaxHiddenRepliesError,
|
||||
useToggleReplyVisibilityMutation,
|
||||
} from '#/state/queries/threadgate'
|
||||
import {useRequireAuth, useSession} from '#/state/session'
|
||||
import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
@@ -253,6 +258,7 @@ let PostMenuItems = ({
|
||||
|
||||
const onHidePost = () => {
|
||||
hidePost({uri: postUri})
|
||||
logEvent('thread:click:hideReplyForMe', {})
|
||||
}
|
||||
|
||||
const hideInPWI = !!postAuthor.labels?.find(
|
||||
@@ -333,16 +339,42 @@ let PostMenuItems = ({
|
||||
replyUri: postUri,
|
||||
action,
|
||||
})
|
||||
|
||||
// Log metric only when hiding (not when showing)
|
||||
if (isHide) {
|
||||
logEvent('thread:click:hideReplyForEveryone', {})
|
||||
}
|
||||
|
||||
Toast.show(
|
||||
isHide
|
||||
? _(msg`Reply was successfully hidden`)
|
||||
: _(msg({message: 'Reply visibility updated', context: 'toast'})),
|
||||
)
|
||||
} catch (e: any) {
|
||||
Toast.show(
|
||||
_(msg({message: 'Updating reply visibility failed', context: 'toast'})),
|
||||
)
|
||||
logger.error(`Failed to ${action} reply`, {safeMessage: e.message})
|
||||
if (e instanceof MaxHiddenRepliesError) {
|
||||
Toast.show(
|
||||
_(
|
||||
msg({
|
||||
message: `You can hide a maximum of ${MAX_HIDDEN_REPLIES} replies.`,
|
||||
context: 'toast',
|
||||
}),
|
||||
),
|
||||
)
|
||||
} else if (e instanceof InvalidInteractionSettingsError) {
|
||||
Toast.show(
|
||||
_(msg({message: 'Invalid interaction settings.', context: 'toast'})),
|
||||
)
|
||||
} else {
|
||||
Toast.show(
|
||||
_(
|
||||
msg({
|
||||
message: 'Updating reply visibility failed',
|
||||
context: 'toast',
|
||||
}),
|
||||
),
|
||||
)
|
||||
logger.error(`Failed to ${action} reply`, {safeMessage: e.message})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ let RepostButton = ({
|
||||
<PostControlButton
|
||||
testID="repostBtn"
|
||||
active={isReposted}
|
||||
activeColor={t.palette.positive_600}
|
||||
activeColor={t.palette.positive_500}
|
||||
big={big}
|
||||
onPress={onPress}
|
||||
onLongPress={onLongPress}
|
||||
|
||||
@@ -47,7 +47,7 @@ export const RepostButton = ({
|
||||
<PostControlButton
|
||||
testID="repostBtn"
|
||||
active={isReposted}
|
||||
activeColor={t.palette.positive_600}
|
||||
activeColor={t.palette.positive_500}
|
||||
label={props.accessibilityLabel}
|
||||
big={big}
|
||||
{...props}>
|
||||
@@ -100,7 +100,7 @@ export const RepostButton = ({
|
||||
<PostControlButton
|
||||
onPress={() => requireAuth(() => {})}
|
||||
active={isReposted}
|
||||
activeColor={t.palette.positive_600}
|
||||
activeColor={t.palette.positive_500}
|
||||
label={_(msg`Repost or quote post`)}
|
||||
big={big}>
|
||||
<PostControlButtonIcon icon={Repost} />
|
||||
|
||||
@@ -136,7 +136,9 @@ let ShareMenuItems = ({
|
||||
{hideInPWI && (
|
||||
<Menu.Group>
|
||||
<Menu.ContainerItem>
|
||||
<Admonition type="warning" style={[a.flex_1, a.border_0, a.p_0]}>
|
||||
<Admonition
|
||||
type="warning"
|
||||
style={[a.flex_1, a.border_0, a.p_0, a.bg_transparent]}>
|
||||
<Trans>This post is only visible to logged-in users.</Trans>
|
||||
</Admonition>
|
||||
</Menu.ContainerItem>
|
||||
|
||||
@@ -210,7 +210,7 @@ let PostControls = ({
|
||||
a.flex_1,
|
||||
a.align_start,
|
||||
{marginLeft: big ? -2 : -6},
|
||||
replyDisabled ? {opacity: 0.5} : undefined,
|
||||
replyDisabled ? {opacity: 0.6} : undefined,
|
||||
]}>
|
||||
<PostControlButton
|
||||
testID="replyBtn"
|
||||
|
||||
@@ -4,6 +4,7 @@ import {type ModerationOpts} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {popularInterests, useInterestsDisplayNames} from '#/lib/interests'
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
@@ -13,10 +14,6 @@ import {useGetSuggestedUsersQuery} from '#/state/queries/trending/useGetSuggeste
|
||||
import {useSession} from '#/state/session'
|
||||
import {type Follow10ProgressGuide} from '#/state/shell/progress-guide'
|
||||
import {type ListMethods} from '#/view/com/util/List'
|
||||
import {
|
||||
popularInterests,
|
||||
useInterestsDisplayNames,
|
||||
} from '#/screens/Onboarding/state'
|
||||
import {
|
||||
atoms as a,
|
||||
native,
|
||||
|
||||
@@ -32,7 +32,7 @@ export function Text({blend, style}: TextStyleProp & SkeletonProps) {
|
||||
<View
|
||||
style={[
|
||||
a.rounded_md,
|
||||
t.atoms.bg_contrast_25,
|
||||
t.atoms.bg_contrast_50,
|
||||
{
|
||||
height: lineHeight * 0.7,
|
||||
opacity: blend ? 0.6 : 1,
|
||||
@@ -57,7 +57,7 @@ export function Circle({
|
||||
a.justify_center,
|
||||
a.align_center,
|
||||
a.rounded_full,
|
||||
t.atoms.bg_contrast_25,
|
||||
t.atoms.bg_contrast_50,
|
||||
{
|
||||
width: size,
|
||||
height: size,
|
||||
@@ -80,7 +80,7 @@ export function Pill({
|
||||
<View
|
||||
style={[
|
||||
a.rounded_full,
|
||||
t.atoms.bg_contrast_25,
|
||||
t.atoms.bg_contrast_50,
|
||||
{
|
||||
width: size * 1.618,
|
||||
height: size,
|
||||
|
||||
@@ -91,8 +91,8 @@ function ShareDialogInner({
|
||||
source={{uri: imageUrl}}
|
||||
style={[
|
||||
a.rounded_sm,
|
||||
a.aspect_card,
|
||||
{
|
||||
aspectRatio: 1200 / 630,
|
||||
transform: [{scale: gtMobile ? 0.85 : 1}],
|
||||
marginTop: gtMobile ? -20 : 0,
|
||||
},
|
||||
|
||||
@@ -191,7 +191,7 @@ export function Embed({
|
||||
<Link starterPack={starterPack}>
|
||||
<Image
|
||||
source={imageUri}
|
||||
style={[a.w_full, {aspectRatio: 1.91}]}
|
||||
style={[a.w_full, a.aspect_card]}
|
||||
accessibilityIgnoresInvertColors={true}
|
||||
/>
|
||||
<View style={[a.px_sm, a.py_md]}>
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import {View} from 'react-native'
|
||||
|
||||
import {isTouchDevice} from '#/lib/browser'
|
||||
import {isNative, isWeb} from '#/platform/detection'
|
||||
import {atoms as a, useTheme, type ViewStyleProp} from '#/alf'
|
||||
|
||||
export function SubtleHover({style, hover}: ViewStyleProp & {hover: boolean}) {
|
||||
export function SubtleHover({
|
||||
style,
|
||||
hover,
|
||||
web = true,
|
||||
native = false,
|
||||
}: ViewStyleProp & {hover: boolean; web?: boolean; native?: boolean}) {
|
||||
const t = useTheme()
|
||||
|
||||
let opacity: number
|
||||
@@ -18,17 +25,25 @@ export function SubtleHover({style, hover}: ViewStyleProp & {hover: boolean}) {
|
||||
break
|
||||
}
|
||||
|
||||
return (
|
||||
const el = (
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.inset_0,
|
||||
a.pointer_events_none,
|
||||
a.transition_opacity,
|
||||
t.atoms.bg_contrast_25,
|
||||
t.atoms.bg_contrast_50,
|
||||
style,
|
||||
{opacity: hover ? opacity : 0},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
|
||||
if (isWeb && web) {
|
||||
return isTouchDevice ? null : el
|
||||
} else if (isNative && native) {
|
||||
return el
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import {type ViewStyleProp} from '#/alf'
|
||||
|
||||
export function SubtleWebHover({}: ViewStyleProp & {hover: boolean}) {
|
||||
return null
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import {StyleSheet, View} from 'react-native'
|
||||
|
||||
import {isTouchDevice} from '#/lib/browser'
|
||||
import {useTheme, type ViewStyleProp} from '#/alf'
|
||||
|
||||
export function SubtleWebHover({
|
||||
style,
|
||||
hover,
|
||||
}: ViewStyleProp & {hover: boolean}) {
|
||||
const t = useTheme()
|
||||
if (isTouchDevice) {
|
||||
return null
|
||||
}
|
||||
let opacity = 0.5
|
||||
switch (t.name) {
|
||||
case 'dark':
|
||||
opacity = 0.4
|
||||
break
|
||||
case 'dim':
|
||||
opacity = 0.45
|
||||
break
|
||||
case 'light':
|
||||
opacity = 0.5
|
||||
break
|
||||
}
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
t.atoms.bg_contrast_25,
|
||||
styles.container,
|
||||
{opacity: hover ? opacity : 0},
|
||||
style,
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
top: 0,
|
||||
pointerEvents: 'none',
|
||||
// @ts-ignore web only
|
||||
transition: '0.15s ease-in-out opacity',
|
||||
},
|
||||
})
|
||||
@@ -159,7 +159,7 @@ export function Inner({}: {optimisticState?: AgeAssuranceRedirectDialogState}) {
|
||||
a.pt_lg,
|
||||
a.pb_md,
|
||||
]}>
|
||||
<SuccessIcon size="sm" fill={t.palette.positive_600} />
|
||||
<SuccessIcon size="sm" fill={t.palette.positive_500} />
|
||||
<Text style={[a.text_xl, a.font_bold]}>
|
||||
<Trans>Success</Trans>
|
||||
</Text>
|
||||
|
||||
@@ -281,7 +281,7 @@ export function Update(_props: ScreenProps<ScreenID.Update>) {
|
||||
<Divider />
|
||||
<View style={[a.gap_sm]}>
|
||||
<View style={[a.flex_row, a.gap_sm, a.align_center]}>
|
||||
<Check fill={t.palette.positive_600} size="xs" />
|
||||
<Check fill={t.palette.positive_500} size="xs" />
|
||||
<Text style={[a.text_md, a.font_bold]}>
|
||||
<Trans>Success!</Trans>
|
||||
</Text>
|
||||
|
||||
@@ -176,7 +176,7 @@ export function Verify({config, showScreen}: ScreenProps<ScreenID.Verify>) {
|
||||
<View style={[a.gap_sm]}>
|
||||
<Text style={[a.text_xl, a.font_bold]}>
|
||||
<Span style={{top: 1}}>
|
||||
<Check size="sm" fill={t.palette.positive_600} />
|
||||
<Check size="sm" fill={t.palette.positive_500} />
|
||||
</Span>
|
||||
{' '}
|
||||
<Trans>Email verification complete!</Trans>
|
||||
@@ -202,7 +202,7 @@ export function Verify({config, showScreen}: ScreenProps<ScreenID.Verify>) {
|
||||
state.mutationStatus === 'success' ? (
|
||||
<>
|
||||
<Span style={{top: 1}}>
|
||||
<Check size="sm" fill={t.palette.positive_600} />
|
||||
<Check size="sm" fill={t.palette.positive_500} />
|
||||
</Span>
|
||||
{' '}
|
||||
<Trans>Email sent!</Trans>
|
||||
|
||||
@@ -300,7 +300,8 @@ export function GifPreview({
|
||||
a.flex_1,
|
||||
a.mb_sm,
|
||||
a.rounded_sm,
|
||||
{aspectRatio: 1, opacity: pressed ? 0.8 : 1},
|
||||
a.aspect_square,
|
||||
{opacity: pressed ? 0.8 : 1},
|
||||
t.atoms.bg_contrast_25,
|
||||
]}
|
||||
source={{
|
||||
|
||||
@@ -13,6 +13,7 @@ import isEqual from 'lodash.isequal'
|
||||
import {logger} from '#/logger'
|
||||
import {STALE} from '#/state/queries'
|
||||
import {useMyListsQuery} from '#/state/queries/my-lists'
|
||||
import {useGetPost} from '#/state/queries/post'
|
||||
import {
|
||||
createPostgateQueryKey,
|
||||
getPostgateRecord,
|
||||
@@ -25,12 +26,15 @@ import {
|
||||
} from '#/state/queries/postgate/util'
|
||||
import {
|
||||
createThreadgateViewQueryKey,
|
||||
getThreadgateView,
|
||||
type ThreadgateAllowUISetting,
|
||||
threadgateViewToAllowUISetting,
|
||||
useSetThreadgateAllowMutation,
|
||||
useThreadgateViewQuery,
|
||||
} from '#/state/queries/threadgate'
|
||||
import {
|
||||
PostThreadContextProvider,
|
||||
usePostThreadContext,
|
||||
} from '#/state/queries/usePostThread'
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
@@ -133,10 +137,13 @@ export type PostInteractionSettingsDialogProps = {
|
||||
export function PostInteractionSettingsDialog(
|
||||
props: PostInteractionSettingsDialogProps,
|
||||
) {
|
||||
const postThreadContext = usePostThreadContext()
|
||||
return (
|
||||
<Dialog.Outer control={props.control}>
|
||||
<Dialog.Handle />
|
||||
<PostInteractionSettingsDialogControlledInner {...props} />
|
||||
<PostThreadContextProvider context={postThreadContext}>
|
||||
<PostInteractionSettingsDialogControlledInner {...props} />
|
||||
</PostThreadContextProvider>
|
||||
</Dialog.Outer>
|
||||
)
|
||||
}
|
||||
@@ -558,6 +565,7 @@ export function usePrefetchPostInteractionSettings({
|
||||
}) {
|
||||
const queryClient = useQueryClient()
|
||||
const agent = useAgent()
|
||||
const getPost = useGetPost()
|
||||
|
||||
return React.useCallback(async () => {
|
||||
try {
|
||||
@@ -570,7 +578,10 @@ export function usePrefetchPostInteractionSettings({
|
||||
}),
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: createThreadgateViewQueryKey(rootPostUri),
|
||||
queryFn: () => getThreadgateView({agent, postUri: rootPostUri}),
|
||||
queryFn: async () => {
|
||||
const post = await getPost({uri: rootPostUri})
|
||||
return post.threadgate ?? null
|
||||
},
|
||||
staleTime: STALE.SECONDS.THIRTY,
|
||||
}),
|
||||
])
|
||||
@@ -579,5 +590,5 @@ export function usePrefetchPostInteractionSettings({
|
||||
safeMessage: e.message,
|
||||
})
|
||||
}
|
||||
}, [queryClient, agent, postUri, rootPostUri])
|
||||
}, [queryClient, agent, postUri, rootPostUri, getPost])
|
||||
}
|
||||
|
||||
@@ -349,6 +349,14 @@ function DialogInner({
|
||||
? _(msg`Create user list`)
|
||||
: _(msg`Create moderation list`)
|
||||
|
||||
const displayNamePlaceholder = isCurateList
|
||||
? _(msg`e.g. Great Posters`)
|
||||
: _(msg`e.g. Spammers`)
|
||||
|
||||
const descriptionPlaceholder = isCurateList
|
||||
? _(msg`e.g. The posters who never miss.`)
|
||||
: _(msg`e.g. Users that repeatedly reply with ads.`)
|
||||
|
||||
return (
|
||||
<Dialog.ScrollableInner
|
||||
label={title}
|
||||
@@ -389,7 +397,7 @@ function DialogInner({
|
||||
defaultValue={displayName}
|
||||
onChangeText={onChangeDisplayName}
|
||||
label={_(msg`Name`)}
|
||||
placeholder={_(msg`e.g. Great Posters`)}
|
||||
placeholder={displayNamePlaceholder}
|
||||
testID="editListNameInput"
|
||||
/>
|
||||
</TextField.Root>
|
||||
@@ -426,8 +434,8 @@ function DialogInner({
|
||||
onChangeText={onChangeDescription}
|
||||
multiline
|
||||
label={_(msg`Description`)}
|
||||
placeholder={_(msg`e.g. The posters that never miss.`)}
|
||||
testID="editProfileDescriptionInput"
|
||||
placeholder={descriptionPlaceholder}
|
||||
testID="editListDescriptionInput"
|
||||
/>
|
||||
</TextField.Root>
|
||||
{descriptionTooLong && (
|
||||
|
||||
@@ -56,10 +56,7 @@ function DialogInner({profile}: {profile: bsky.profile.AnyProfileView}) {
|
||||
|
||||
const date = new Date()
|
||||
date.setMinutes(date.getMinutes() + offset)
|
||||
return i18n
|
||||
.date(date, {hour: 'numeric', minute: '2-digit', hour12: true})
|
||||
.toLocaleUpperCase()
|
||||
.replace(' ', '')
|
||||
return i18n.date(date, {hour: 'numeric', minute: '2-digit', hour12: true})
|
||||
},
|
||||
[tick, i18n],
|
||||
)
|
||||
|
||||
@@ -102,7 +102,7 @@ export function LiveStatus({
|
||||
style={[
|
||||
t.atoms.bg_contrast_25,
|
||||
a.w_full,
|
||||
{aspectRatio: 1.91},
|
||||
a.aspect_card,
|
||||
android([
|
||||
a.overflow_hidden,
|
||||
{
|
||||
|
||||
@@ -85,7 +85,11 @@ function ContentHiderActive({
|
||||
blur.type !== 'label' ||
|
||||
(blur.type === 'label' && blur.source.type !== 'user')
|
||||
) {
|
||||
return desc.name
|
||||
if (desc.isSubjectAccount) {
|
||||
return _(msg`${desc.name} (Account)`)
|
||||
} else {
|
||||
return desc.name
|
||||
}
|
||||
}
|
||||
|
||||
let hasAdultContentLabel = false
|
||||
@@ -127,6 +131,7 @@ function ContentHiderActive({
|
||||
modui?.blurs,
|
||||
blur,
|
||||
desc.name,
|
||||
desc.isSubjectAccount,
|
||||
labelDefs,
|
||||
i18n.locale,
|
||||
globalLabelStrings,
|
||||
|
||||
@@ -34,6 +34,7 @@ interface Props extends ComponentProps<typeof Link> {
|
||||
modui: ModerationUI
|
||||
profile: AppBskyActorDefs.ProfileViewBasic
|
||||
interpretFilterAsBlur?: boolean
|
||||
hiderStyle?: StyleProp<ViewStyle>
|
||||
}
|
||||
|
||||
export function PostHider({
|
||||
@@ -42,6 +43,7 @@ export function PostHider({
|
||||
disabled,
|
||||
modui,
|
||||
style,
|
||||
hiderStyle,
|
||||
children,
|
||||
iconSize,
|
||||
iconStyles,
|
||||
@@ -100,6 +102,7 @@ export function PostHider({
|
||||
},
|
||||
override ? {paddingBottom: 0} : undefined,
|
||||
t.atoms.bg,
|
||||
hiderStyle,
|
||||
]}>
|
||||
<ModerationDetailsDialog control={control} modcause={blur} />
|
||||
<Pressable
|
||||
|
||||
@@ -21,12 +21,12 @@ export const ENV: string = process.env.EXPO_PUBLIC_ENV
|
||||
export const IS_TESTFLIGHT = ENV === 'testflight'
|
||||
|
||||
/**
|
||||
* Indicates whether the app is __DEV__
|
||||
* Indicates whether the app is `__DEV__`
|
||||
*/
|
||||
export const IS_DEV = __DEV__
|
||||
|
||||
/**
|
||||
* Indicates whether the app is __DEV__ or TestFlight
|
||||
* Indicates whether the app is `__DEV__` or TestFlight
|
||||
*/
|
||||
export const IS_INTERNAL = IS_DEV || IS_TESTFLIGHT
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ export const EMBED_SERVICE = 'https://embed.bsky.app'
|
||||
export const EMBED_SCRIPT = `${EMBED_SERVICE}/static/embed.js`
|
||||
export const BSKY_DOWNLOAD_URL = 'https://bsky.app/download'
|
||||
export const STARTER_PACK_MAX_SIZE = 150
|
||||
export const CARD_ASPECT_RATIO = 1200 / 630
|
||||
|
||||
// HACK
|
||||
// Yes, this is exactly what it looks like. It's a hard-coded constant
|
||||
|
||||
@@ -78,6 +78,7 @@ const NETWORK_ERRORS = [
|
||||
'Network request failed',
|
||||
'Failed to fetch',
|
||||
'Load failed',
|
||||
'Upstream service unreachable',
|
||||
]
|
||||
|
||||
export function isNetworkError(e: unknown) {
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import {useMemo} from 'react'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
export const interests = [
|
||||
'animals',
|
||||
'art',
|
||||
'books',
|
||||
'comedy',
|
||||
'comics',
|
||||
'culture',
|
||||
'dev',
|
||||
'education',
|
||||
'finance',
|
||||
'food',
|
||||
'gaming',
|
||||
'journalism',
|
||||
'movies',
|
||||
'music',
|
||||
'nature',
|
||||
'news',
|
||||
'pets',
|
||||
'photography',
|
||||
'politics',
|
||||
'science',
|
||||
'sports',
|
||||
'tech',
|
||||
'tv',
|
||||
'writers',
|
||||
] as const
|
||||
export type Interest = (typeof interests)[number]
|
||||
|
||||
// most popular selected interests
|
||||
export const popularInterests = [
|
||||
'art',
|
||||
'gaming',
|
||||
'sports',
|
||||
'comics',
|
||||
'music',
|
||||
'politics',
|
||||
'photography',
|
||||
'science',
|
||||
'news',
|
||||
] satisfies Interest[]
|
||||
|
||||
export function useInterestsDisplayNames() {
|
||||
const {_} = useLingui()
|
||||
|
||||
return useMemo<Record<string, string>>(() => {
|
||||
return {
|
||||
// Keep this alphabetized
|
||||
animals: _(msg`Animals`),
|
||||
art: _(msg`Art`),
|
||||
books: _(msg`Books`),
|
||||
comedy: _(msg`Comedy`),
|
||||
comics: _(msg`Comics`),
|
||||
culture: _(msg`Culture`),
|
||||
dev: _(msg`Software Dev`),
|
||||
education: _(msg`Education`),
|
||||
finance: _(msg`Finance`),
|
||||
food: _(msg`Food`),
|
||||
gaming: _(msg`Video Games`),
|
||||
journalism: _(msg`Journalism`),
|
||||
movies: _(msg`Movies`),
|
||||
music: _(msg`Music`),
|
||||
nature: _(msg`Nature`),
|
||||
news: _(msg`News`),
|
||||
pets: _(msg`Pets`),
|
||||
photography: _(msg`Photography`),
|
||||
politics: _(msg`Politics`),
|
||||
science: _(msg`Science`),
|
||||
sports: _(msg`Sports`),
|
||||
tech: _(msg`Tech`),
|
||||
tv: _(msg`TV`),
|
||||
writers: _(msg`Writers`),
|
||||
} satisfies Record<Interest, string>
|
||||
}, [_])
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import {type ModerationUI} from '@atproto/api'
|
||||
|
||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
@@ -5,9 +7,10 @@ import type * as bsky from '#/types/bsky'
|
||||
export function createSanitizedDisplayName(
|
||||
profile: bsky.profile.AnyProfileView,
|
||||
noAt = false,
|
||||
moderation?: ModerationUI,
|
||||
) {
|
||||
if (profile.displayName != null && profile.displayName !== '') {
|
||||
return sanitizeDisplayName(profile.displayName)
|
||||
return sanitizeDisplayName(profile.displayName, moderation)
|
||||
} else {
|
||||
return sanitizeHandle(profile.handle, noAt ? '' : '@')
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ export interface ModerationCauseDescription {
|
||||
sourceType?: ModerationCauseSource['type']
|
||||
sourceAvi?: string
|
||||
sourceDid?: string
|
||||
isSubjectAccount?: boolean
|
||||
}
|
||||
|
||||
export function useModerationCauseDescription(
|
||||
@@ -162,6 +163,7 @@ export function useModerationCauseDescription(
|
||||
sourceType: cause.source.type,
|
||||
sourceAvi: labeler?.creator.avatar,
|
||||
sourceDid: cause.label.src,
|
||||
isSubjectAccount: cause.label.uri.startsWith('did:'),
|
||||
}
|
||||
}
|
||||
// should never happen
|
||||
|
||||
@@ -7,6 +7,7 @@ import debounce from 'lodash.debounce'
|
||||
|
||||
import {PUBLIC_APPVIEW_DID, PUBLIC_STAGING_APPVIEW_DID} from '#/lib/constants'
|
||||
import {logger as notyLogger} from '#/lib/notifications/util'
|
||||
import {isNetworkError} from '#/lib/strings/errors'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {useAgeAssuranceContext} from '#/state/ageAssurance'
|
||||
import {type SessionAccount, useAgent, useSession} from '#/state/session'
|
||||
@@ -47,7 +48,9 @@ async function _registerPushToken({
|
||||
|
||||
notyLogger.debug(`registerPushToken: success`)
|
||||
} catch (error) {
|
||||
notyLogger.error(`registerPushToken: failed`, {safeMessage: error})
|
||||
if (!isNetworkError(error)) {
|
||||
notyLogger.error(`registerPushToken: failed`, {safeMessage: error})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ export type CommonNavigatorParams = {
|
||||
InterestsSettings: undefined
|
||||
AboutSettings: undefined
|
||||
AppIconSettings: undefined
|
||||
Search: {q?: string}
|
||||
Search: {q?: string; tab?: 'user' | 'profile' | 'feed'}
|
||||
Hashtag: {tag: string; author?: string}
|
||||
Topic: {topic: string}
|
||||
MessagesConversation: {conversation: string; embed?: string; accept?: true}
|
||||
@@ -102,7 +102,7 @@ export type HomeTabNavigatorParams = CommonNavigatorParams & {
|
||||
}
|
||||
|
||||
export type SearchTabNavigatorParams = CommonNavigatorParams & {
|
||||
Search: {q?: string}
|
||||
Search: {q?: string; tab?: 'user' | 'profile' | 'feed'}
|
||||
}
|
||||
|
||||
export type NotificationsTabNavigatorParams = CommonNavigatorParams & {
|
||||
@@ -119,7 +119,7 @@ export type MessagesTabNavigatorParams = CommonNavigatorParams & {
|
||||
|
||||
export type FlatNavigatorParams = CommonNavigatorParams & {
|
||||
Home: undefined
|
||||
Search: {q?: string}
|
||||
Search: {q?: string; tab?: 'user' | 'profile' | 'feed'}
|
||||
Feeds: undefined
|
||||
Notifications: undefined
|
||||
Messages: {pushToConversation?: string; animation?: 'push' | 'pop'}
|
||||
@@ -129,7 +129,7 @@ export type AllNavigatorParams = CommonNavigatorParams & {
|
||||
HomeTab: undefined
|
||||
Home: undefined
|
||||
SearchTab: undefined
|
||||
Search: {q?: string}
|
||||
Search: {q?: string; tab?: 'user' | 'profile' | 'feed'}
|
||||
Feeds: undefined
|
||||
NotificationsTab: undefined
|
||||
Notifications: undefined
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
export type Gate =
|
||||
// Keep this alphabetic please.
|
||||
| 'alt_share_icon'
|
||||
| 'cta_above_post_heading'
|
||||
| 'cta_above_post_replies'
|
||||
| 'debug_show_feedcontext'
|
||||
| 'debug_subscriptions'
|
||||
| 'disable_onboarding_policy_update_notice'
|
||||
| 'explore_show_suggested_feeds'
|
||||
| 'feed_reply_button_open_thread'
|
||||
| 'old_postonboarding'
|
||||
| 'onboarding_add_video_feed'
|
||||
| 'onboarding_suggested_accounts'
|
||||
| 'onboarding_value_prop'
|
||||
| 'onboarding_suggested_starterpacks'
|
||||
| 'post_follow_profile_suggested_accounts'
|
||||
| 'remove_show_latest_button'
|
||||
| 'test_gate_1'
|
||||
|
||||
@@ -31,6 +31,7 @@ const NETWORK_ERRORS = [
|
||||
'Network request failed',
|
||||
'Failed to fetch',
|
||||
'Load failed',
|
||||
'Upstream service unreachable',
|
||||
]
|
||||
|
||||
export function isNetworkError(e: unknown) {
|
||||
|
||||