diff --git a/.github/workflows/bundle-deploy-eas-update.yml b/.github/workflows/bundle-deploy-eas-update.yml index 983494dd18..5f1f0a4e28 100644 --- a/.github/workflows/bundle-deploy-eas-update.yml +++ b/.github/workflows/bundle-deploy-eas-update.yml @@ -71,18 +71,18 @@ jobs: profile: ${{ inputs.channel || 'testflight' }} previous-commit-tag: ${{ inputs.runtimeVersion }} - - name: Lint check - run: pnpm lint - - - name: Prettier check - run: pnpm prettier --check . - - name: 🔤 Compile translations run: pnpm intl:build 2>&1 | tee i18n.log - name: Check for i18n compilation errors run: if grep -q "invalid syntax" "i18n.log"; then echo "\n\nFound compilation errors!\n\n" && exit 1; else echo "\n\nNo compilation errors!\n\n"; fi + - name: Lint check + run: pnpm lint + + - name: Prettier check + run: pnpm prettier --check . + - name: Type check run: pnpm typecheck diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 93729874b6..aa6c92765d 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -21,7 +21,7 @@ jobs: strategy: fail-fast: false matrix: - job: [lint, prettier] + job: [lint, prettier, typecheck] steps: - name: Check out Git repository uses: actions/checkout@v5 @@ -62,6 +62,8 @@ jobs: command: pnpm install --frozen-lockfile attempt_limit: 3 attempt_delay: 2000 + - name: Check & compile i18n + run: pnpm intl:build - name: Lint checks run: pnpm ${{ matrix.job }} # Aggregates the matrix results into a single stable check name so branch @@ -80,28 +82,6 @@ jobs: run: | echo "linting result: $RESULT" test "$RESULT" = "success" - typechecking: - name: Run typecheck - runs-on: ubuntu-latest - steps: - - name: Check out Git repository - uses: actions/checkout@v5 - - uses: pnpm/action-setup@v6 - - name: Install node - uses: actions/setup-node@v6 - with: - node-version-file: package.json - cache: pnpm - - name: pnpm install - uses: Wandalen/wretry.action@master - with: - command: pnpm install --frozen-lockfile - attempt_limit: 3 - attempt_delay: 2000 - - name: Check & compile i18n - run: pnpm intl:build - - name: Type check - run: pnpm typecheck testing: name: Run tests runs-on: ubuntu-latest diff --git a/.github/workflows/pull-request-comment.yml b/.github/workflows/pull-request-comment.yml index a497a5625d..8ede462392 100644 --- a/.github/workflows/pull-request-comment.yml +++ b/.github/workflows/pull-request-comment.yml @@ -128,15 +128,15 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Lint check - run: pnpm lint - - name: 🔤 Compile translations run: pnpm intl:build 2>&1 | tee i18n.log - name: Check for i18n compilation errors run: if grep -q "invalid syntax" "i18n.log"; then echo "\n\nFound compilation errors!\n\n" && exit 1; else echo "\n\nNo compilation errors!\n\n"; fi + - name: Lint check + run: pnpm lint + - name: Type check run: pnpm typecheck diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 5b7eef4c94..f2e677dc79 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -1,12 +1,72 @@ { + "src/Navigation.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/Splash.android.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/Splash.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/ageAssurance/components/RedirectOverlay.tsx": { + "@typescript-eslint/require-await": { + "count": 1 + } + }, + "src/ageAssurance/util.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, "src/alf/util/flatten.ts": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 9 + } + }, + "src/alf/util/systemUI.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + } + }, + "src/alf/util/useGutters.ts": { + "react-hooks/immutability": { + "count": 5 } }, "src/analytics/PassiveAnalytics.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "react-hooks/purity": { + "count": 1 + } + }, + "src/analytics/features/index.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/require-await": { + "count": 1 + } + }, + "src/analytics/identifiers/session.test.ts": { + "@typescript-eslint/no-unsafe-call": { + "count": 9 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 9 } }, "src/analytics/metadata.ts": { @@ -17,16 +77,84 @@ "src/analytics/metrics/client.test.ts": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 8 + }, + "@typescript-eslint/require-await": { + "count": 5 } }, "src/analytics/metrics/client.ts": { "@typescript-eslint/no-explicit-any": { "count": 5 + }, + "@typescript-eslint/no-floating-promises": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + } + }, + "src/analytics/utils.ts": { + "react-hooks/immutability": { + "count": 1 + } + }, + "src/components/AppLanguageDropdown.tsx": { + "@typescript-eslint/no-unsafe-enum-comparison": { + "count": 1 + } + }, + "src/components/Autocomplete/Autocomplete.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/components/Autocomplete/useAutocomplete/index.ts": { + "react-hooks/immutability": { + "count": 1 + } + }, + "src/components/BotAccountAlert.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/components/Button.tsx": { + "react-hooks/immutability": { + "count": 1 } }, "src/components/Composer/index.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-redundant-type-constituents": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 7 + }, + "react-hooks/immutability": { + "count": 1 + }, + "react-hooks/refs": { + "count": 1 + } + }, + "src/components/DebugFieldDisplay.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 } }, "src/components/Dialog/index.tsx": { @@ -37,6 +165,18 @@ "src/components/Dialog/index.web.tsx": { "@typescript-eslint/no-explicit-any": { "count": 4 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 3 + }, + "@typescript-eslint/require-await": { + "count": 1 } }, "src/components/DraggableList/index.web.tsx": { @@ -44,6 +184,11 @@ "count": 2 } }, + "src/components/EmojiPicker/preload.web.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + } + }, "src/components/FeedCard.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 @@ -52,16 +197,53 @@ "src/components/FocusScope/index.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + }, + "react-hooks/refs": { + "count": 1 } }, "src/components/InternationalPhoneCodeSelect.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + } + }, + "src/components/KnownFollowers.tsx": { + "react-hooks/refs": { + "count": 6 + } + }, + "src/components/Lightbox/Lightbox.web.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/components/Lightbox/pager/ImageItem/ImageItem.android.tsx": { + "react-hooks/immutability": { + "count": 1 + } + }, + "src/components/Lightbox/pager/ImageItem/ImageItem.ios.tsx": { + "react-hooks/refs": { + "count": 1 + } + }, + "src/components/LikedByList.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 2 } }, "src/components/Lists.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 } }, "src/components/Menu/types.ts": { @@ -74,34 +256,129 @@ "count": 1 } }, + "src/components/Post/Embed/ExternalEmbed/ExternalGif.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 3 + } + }, + "src/components/Post/Embed/ExternalEmbed/index.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, "src/components/Post/Embed/ImageEmbed.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/components/Post/Embed/StandardSiteEmbed/index.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 3 + } + }, + "src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-enum-comparison": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + }, + "react-hooks/immutability": { + "count": 1 } }, "src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/utils.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 3 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 3 } }, "src/components/PostControls/BookmarkButton.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-misused-promises": { + "count": 2 + } + }, + "src/components/PostControls/DiscoverDebug.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/components/PostControls/ShareMenu/ShareMenuItems.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 3 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/components/PostControls/ShareMenu/ShareMenuItems.web.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 3 + } + }, + "src/components/PostControls/ShareMenu/index.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 } }, "src/components/ProfileHoverCard/index.web.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-floating-promises": { + "count": 3 + }, + "@typescript-eslint/require-await": { + "count": 1 + }, + "react-hooks/immutability": { + "count": 1 + }, + "react-hooks/refs": { + "count": 2 + } + }, + "src/components/ProgressGuide/FollowDialog.tsx": { + "react-hooks/refs": { + "count": 2 + } + }, + "src/components/RichTextTag.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 2 } }, "src/components/Select/index.tsx": { "@typescript-eslint/no-explicit-any": { "count": 4 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 } }, "src/components/Select/index.web.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 } }, "src/components/Select/types.ts": { @@ -109,9 +386,44 @@ "count": 3 } }, + "src/components/SendErrorReportDialog.tsx": { + "@typescript-eslint/require-await": { + "count": 1 + } + }, + "src/components/StarterPack/Main/ProfilesList.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, "src/components/StarterPack/ProfileStarterPacks.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 2 + } + }, + "src/components/StarterPack/QrCodeDialog.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 5 + }, + "@typescript-eslint/no-misused-promises": { + "count": 2 + }, + "@typescript-eslint/require-await": { + "count": 4 + } + }, + "src/components/StarterPack/ShareDialog.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 2 + }, + "@typescript-eslint/require-await": { + "count": 1 } }, "src/components/StarterPack/Wizard/WizardEditListDialog.tsx": { @@ -119,9 +431,109 @@ "count": 1 } }, + "src/components/Tooltip/index.tsx": { + "@typescript-eslint/no-base-to-string": { + "count": 1 + } + }, + "src/components/Tooltip/index.web.tsx": { + "@typescript-eslint/no-base-to-string": { + "count": 1 + } + }, + "src/components/WhoCanReply.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "react-hooks/refs": { + "count": 1 + } + }, + "src/components/ageAssurance/AgeAssuranceErrors.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/components/ageAssurance/AgeAssuranceInitDialog.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 2 + }, + "react-hooks/purity": { + "count": 1 + } + }, + "src/components/ageAssurance/AgeAssuranceRedirectDialog.tsx": { + "@typescript-eslint/require-await": { + "count": 1 + } + }, + "src/components/contacts/components/OTPInput.tsx": { + "react-hooks/refs": { + "count": 1 + } + }, + "src/components/contacts/screens/GetContacts.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/components/contacts/screens/VerifyNumber.tsx": { + "@typescript-eslint/require-await": { + "count": 1 + }, + "react-hooks/purity": { + "count": 1 + } + }, + "src/components/contacts/screens/ViewMatches.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, "src/components/dialogs/DeviceLocationRequestDialog.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 + } + }, + "src/components/dialogs/EmailDialog/components/ResendEmailText.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/components/dialogs/EmailDialog/data/useAccountEmailState.ts": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/components/dialogs/EmailDialog/screens/Manage2FA/Disable.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 3 + } + }, + "src/components/dialogs/EmailDialog/screens/Manage2FA/Enable.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/components/dialogs/EmailDialog/screens/Update.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 3 + } + }, + "src/components/dialogs/EmailDialog/screens/Verify.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 3 + } + }, + "src/components/dialogs/InAppBrowserConsent.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 2 } }, "src/components/dialogs/LanguageSelectDialog.tsx": { @@ -129,27 +541,105 @@ "count": 1 } }, + "src/components/dialogs/LinkWarning.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + } + }, "src/components/dialogs/MutedWords.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-floating-promises": { + "count": 2 + }, + "@typescript-eslint/no-misused-promises": { + "count": 3 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 + }, + "@typescript-eslint/require-await": { + "count": 1 } }, "src/components/dialogs/PostInteractionSettingsDialog.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 + }, "react-hooks/preserve-manual-memoization": { "count": 1 } }, + "src/components/dialogs/ServerInput.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/components/dialogs/StarterPackDialog.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/components/dialogs/SwitchAccount.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/components/dialogs/lists/CreateListFromStarterPackDialog.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + } + }, "src/components/dialogs/lists/CreateOrEditListDialog.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/components/dialogs/nuxs/FindContactsAnnouncement.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/components/dialogs/nuxs/index.tsx": { + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + }, + "react-hooks/immutability": { + "count": 1 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/components/dms/MessageItem.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 } }, "src/components/forms/DateField/index.web.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 + } + }, + "src/components/forms/SearchInput.tsx": { + "react-hooks/refs": { + "count": 1 } }, "src/components/forms/SegmentedControl.tsx": { @@ -157,6 +647,14 @@ "count": 1 } }, + "src/components/forms/TextField.tsx": { + "@typescript-eslint/no-unsafe-member-access": { + "count": 3 + }, + "react-hooks/refs": { + "count": 1 + } + }, "src/components/forms/ToggleButton.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 @@ -165,6 +663,37 @@ "src/components/hooks/useFollowMethods.ts": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 + } + }, + "src/components/hooks/useFullscreen.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/components/hooks/useLandingEntry.native.ts": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/components/hooks/useLandingEntry.ts": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/components/hooks/useRefreshOnFocus.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/components/hooks/useRichText.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 1 } }, "src/components/images/AutoSizedImage.tsx": { @@ -175,6 +704,15 @@ "src/components/images/Gallery/index.tsx": { "@typescript-eslint/no-explicit-any": { "count": 5 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 4 + }, + "react-hooks/refs": { + "count": 1 } }, "src/components/images/Gallery/useKeyboardHandlers.ts": { @@ -197,6 +735,51 @@ "count": 2 } }, + "src/components/intents/VerifyEmailIntentDialog.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/components/interstitials/TrendingVideos.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/components/moderation/ContentHider.tsx": { + "react-hooks/immutability": { + "count": 1 + } + }, + "src/components/moderation/LabelsOnMeDialog.tsx": { + "react-hooks/purity": { + "count": 1 + } + }, + "src/components/moderation/ModerationDetailsDialog.tsx": { + "react-hooks/purity": { + "count": 1 + } + }, + "src/components/verification/VerificationCreatePrompt.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/components/verification/VerificationRemovePrompt.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/features/liveEvents/preferences.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/features/liveNow/components/LiveStatusDialog.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, "src/features/liveNow/index.tsx": { "@typescript-eslint/no-explicit-any": { "count": 3 @@ -205,6 +788,12 @@ "src/geolocation/service.ts": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 } }, "src/lib/ScrollContext.tsx": { @@ -212,9 +801,22 @@ "count": 3 } }, + "src/lib/api/feed/demo.ts": { + "@typescript-eslint/require-await": { + "count": 2 + } + }, + "src/lib/api/feed/posts.ts": { + "@typescript-eslint/require-await": { + "count": 1 + } + }, "src/lib/api/index.ts": { "@typescript-eslint/no-explicit-any": { "count": 5 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 3 } }, "src/lib/async/retry.ts": { @@ -232,9 +834,20 @@ "count": 1 } }, + "src/lib/custom-animations/CountWheel.web.tsx": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, "src/lib/functions.ts": { "@typescript-eslint/no-explicit-any": { "count": 6 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 25 } }, "src/lib/getUserDisplayName.ts": { @@ -242,19 +855,80 @@ "count": 1 } }, + "src/lib/haptics.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, "src/lib/hooks/useAccountSwitcher.ts": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + } + }, + "src/lib/hooks/useAnimatedValue.ts": { + "react-hooks/refs": { + "count": 1 + } + }, + "src/lib/hooks/useDraggableScrollView.ts": { + "react-hooks/refs": { + "count": 1 + } + }, + "src/lib/hooks/useIntentHandler.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 2 } }, "src/lib/hooks/useNonReactiveCallback.ts": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 1 + } + }, + "src/lib/hooks/useNotificationHandler.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 16 + }, + "@typescript-eslint/require-await": { + "count": 1 } }, "src/lib/hooks/useOTAUpdates.ts": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 4 + } + }, + "src/lib/hooks/useOpenLink.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + }, + "@typescript-eslint/require-await": { + "count": 1 + } + }, + "src/lib/hooks/usePermissions.ts": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-enum-comparison": { + "count": 5 + } + }, + "src/lib/hooks/usePermissions.web.ts": { + "@typescript-eslint/require-await": { + "count": 3 } }, "src/lib/hooks/useRequireEmailVerification.tsx": { @@ -262,14 +936,33 @@ "count": 2 } }, + "src/lib/hooks/useTLDs.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/lib/hooks/useTabFocusEffect.ts": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, "src/lib/hooks/useToggleMutationQueue.ts": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 2 } }, "src/lib/hooks/useWebScrollRestoration.ts": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "react-hooks/immutability": { + "count": 1 } }, "src/lib/international-telephone-codes.ts": { @@ -277,9 +970,30 @@ "count": 1 } }, + "src/lib/link-meta/link-meta.ts": { + "@typescript-eslint/no-unsafe-member-access": { + "count": 9 + } + }, "src/lib/media/manip.ts": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-floating-promises": { + "count": 5 + } + }, + "src/lib/media/manip.web.ts": { + "@typescript-eslint/prefer-promise-reject-errors": { + "count": 1 + }, + "@typescript-eslint/require-await": { + "count": 3 + } + }, + "src/lib/media/picker.web.tsx": { + "@typescript-eslint/require-await": { + "count": 2 } }, "src/lib/media/save-image.ios.ts": { @@ -297,9 +1011,34 @@ "count": 1 } }, + "src/lib/notifications/notifications.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 5 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-enum-comparison": { + "count": 2 + }, + "@typescript-eslint/require-await": { + "count": 1 + } + }, "src/lib/react-query.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + }, + "react-hooks/refs": { + "count": 1 + } + }, + "src/lib/routes/links.ts": { + "@typescript-eslint/no-redundant-type-constituents": { + "count": 1 } }, "src/lib/routes/router.ts": { @@ -312,82 +1051,298 @@ "count": 1 } }, + "src/lib/sharing.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, "src/lib/strings/errors.ts": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 14 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 14 + } + }, + "src/lib/translation/index.tsx": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/lib/useGetEmojis/getEmojis.ts": { + "@typescript-eslint/require-await": { + "count": 1 + } + }, + "src/logger/transports/sentry.ts": { + "@typescript-eslint/no-unsafe-enum-comparison": { + "count": 3 + } + }, + "src/logger/types.ts": { + "@typescript-eslint/no-redundant-type-constituents": { + "count": 1 } }, "src/screens/Bookmarks/index.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 3 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 } }, "src/screens/Deactivated.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + } + }, + "src/screens/E2E/SharedPreferencesTesterScreen.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 6 + }, + "@typescript-eslint/require-await": { + "count": 6 + } + }, + "src/screens/Feeds/NoFollowingFeed.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/screens/Feeds/NoSavedFeedsOfAnyType.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/screens/Hashtag.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/screens/Home/NoFeedsPinned.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/screens/List/ListHiddenScreen.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 3 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/screens/Log.tsx": { + "@typescript-eslint/no-unsafe-enum-comparison": { + "count": 2 } }, "src/screens/Login/ChooseAccountForm.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 } }, "src/screens/Login/ForgotPasswordForm.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 } }, "src/screens/Login/LoginForm.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 3 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 4 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 4 + }, + "react-hooks/refs": { + "count": 1 } }, "src/screens/Login/SetNewPasswordForm.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + } + }, + "src/screens/Login/index.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "react-hooks/purity": { + "count": 1 + }, + "react-hooks/refs": { + "count": 1 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/screens/Messages/ChatList.tsx": { + "react-hooks/immutability": { + "count": 1 } }, "src/screens/Moderation/index.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 } }, "src/screens/ModerationInteractionSettings/index.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + } + }, + "src/screens/Onboarding/StepFinished/ValuePropositionPager.tsx": { + "react-hooks/refs": { + "count": 4 } }, "src/screens/Onboarding/StepFinished/index.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 } }, "src/screens/Onboarding/StepInterests/index.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/require-await": { + "count": 1 + } + }, + "src/screens/Onboarding/StepProfile/index.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/screens/Onboarding/StepSuggestedStarterpacks/StarterPackCard.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/screens/Onboarding/StepSuggestedStarterpacks/index.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 } }, "src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 4 } }, "src/screens/PostThread/index.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 3 + }, "react-hooks/preserve-manual-memoization": { "count": 1 + }, + "react-hooks/refs": { + "count": 4 } }, "src/screens/Profile/Header/EditProfileDialog.tsx": { "@typescript-eslint/no-explicit-any": { "count": 3 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 } }, "src/screens/Profile/Header/ProfileHeaderLabeler.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 3 } }, "src/screens/Profile/Header/Shell.tsx": { @@ -395,29 +1350,164 @@ "count": 1 } }, + "src/screens/Profile/KnownFollowers.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 2 + } + }, "src/screens/Profile/Sections/Feed.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-floating-promises": { + "count": 1 } }, "src/screens/Profile/components/GermButton.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + } + }, + "src/screens/Profile/components/ProfileFeedHeader.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 5 + } + }, + "src/screens/ProfileList/FeedSection.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/screens/ProfileList/components/Header.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 3 + } + }, + "src/screens/ProfileList/components/MoreOptionsMenu.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 5 + } + }, + "src/screens/ProfileList/components/SubscribeMenu.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 2 + } + }, + "src/screens/ProfileList/index.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/screens/SavedFeeds.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 2 + } + }, + "src/screens/Search/Shell.tsx": { + "react-hooks/refs": { + "count": 4 + } + }, + "src/screens/Search/modules/ExploreSuggestedAccounts.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/screens/Search/modules/ExploreTrendingTopics.tsx": { + "react-hooks/purity": { + "count": 2 + } + }, + "src/screens/Search/modules/ExploreTrendingVideos.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/screens/Settings/AboutSettings.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/screens/Settings/AppPasswords.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/screens/Settings/AutomationLabelSettings.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 2 } }, "src/screens/Settings/FindContactsSettings.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-floating-promises": { + "count": 3 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/require-await": { + "count": 1 + } + }, + "src/screens/Settings/InterestsSettings.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/require-await": { + "count": 1 + } + }, + "src/screens/Settings/components/ChangeHandleDialog.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 4 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 } }, "src/screens/Settings/components/ChangePasswordDialog.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-misused-promises": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 + } + }, + "src/screens/Settings/components/CopyButton.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 } }, "src/screens/Settings/components/DeactivateAccountDialog.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 } }, "src/screens/Settings/components/DeleteAccountDialog.tsx": { @@ -425,14 +1515,114 @@ "count": 2 } }, + "src/screens/Settings/components/DisableEmail2FADialog.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 4 + } + }, + "src/screens/Settings/components/OTAInfo.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/screens/Signup/StepCaptcha/CaptchaWebView.tsx": { + "react-hooks/purity": { + "count": 1 + } + }, + "src/screens/Signup/StepHandle/index.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, "src/screens/Signup/StepInfo/Policies.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 } }, + "src/screens/Signup/StepInfo/index.tsx": { + "react-hooks/refs": { + "count": 3 + } + }, "src/screens/SignupQueued.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/screens/StarterPack/StarterPackLandingScreen.tsx": { + "@typescript-eslint/no-unsafe-call": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + } + }, + "src/screens/StarterPack/StarterPackScreen.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 2 + }, + "@typescript-eslint/require-await": { + "count": 1 + } + }, + "src/screens/StarterPack/Wizard/StepFeeds.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/screens/StarterPack/Wizard/StepProfiles.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/screens/StarterPack/Wizard/index.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + }, + "@typescript-eslint/require-await": { + "count": 1 + } + }, + "src/screens/Topic.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/state/a11y.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/cache/profile-shadow.ts": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/state/cache/thread-mutes.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 2 } }, "src/state/cache/types.ts": { @@ -443,31 +1633,213 @@ "src/state/feed-feedback.tsx": { "@typescript-eslint/no-explicit-any": { "count": 3 + }, + "react-hooks/refs": { + "count": 2 + } + }, + "src/state/gallery.ts": { + "@typescript-eslint/prefer-promise-reject-errors": { + "count": 1 } }, "src/state/messages/events/agent.ts": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-floating-promises": { + "count": 4 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 + } + }, + "src/state/messages/message-drafts.tsx": { + "react-hooks/refs": { + "count": 1 } }, "src/state/persisted/index.ts": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 } }, "src/state/persisted/index.web.ts": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 4 } }, "src/state/persisted/util.ts": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + } + }, + "src/state/preferences/alt-text-required.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/preferences/autoplay.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/preferences/disable-haptics.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/preferences/external-embeds-prefs.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/preferences/hidden-posts.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/preferences/in-app-browser.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/preferences/kawaii.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/state/preferences/languages.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/preferences/large-alt-badge.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/preferences/subtitles.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/preferences/trending.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/preferences/used-starter-packs.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/queries/activity-subscriptions.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/queries/actor-autocomplete.ts": { + "react-hooks/immutability": { + "count": 2 + } + }, + "src/state/queries/app-passwords.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + } + }, + "src/state/queries/bookmarks/useBookmarkMutation.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + } + }, + "src/state/queries/bookmarks/useBookmarksQuery.ts": { + "@typescript-eslint/require-await": { + "count": 2 + } + }, + "src/state/queries/explore-feed-previews.tsx": { + "react-hooks/refs": { + "count": 4 + } + }, + "src/state/queries/feed.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/queries/handle.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/queries/list-memberships.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + } + }, + "src/state/queries/list.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 7 + } + }, + "src/state/queries/messages/accept-conversation.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + } + }, + "src/state/queries/messages/update-all-read.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 3 + } + }, + "src/state/queries/my-lists.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 2 } }, "src/state/queries/notifications/feed.ts": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-floating-promises": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 + } + }, + "src/state/queries/notifications/unread.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 4 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 4 + }, + "react-hooks/refs": { + "count": 1 } }, "src/state/queries/nuxs/definitions.ts": { @@ -478,109 +1850,386 @@ "src/state/queries/pinned-post.ts": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-floating-promises": { + "count": 2 } }, "src/state/queries/post-feed.ts": { "@typescript-eslint/no-explicit-any": { "count": 3 + }, + "@typescript-eslint/no-floating-promises": { + "count": 3 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 3 } }, "src/state/queries/postgate/index.ts": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 3 + }, + "@typescript-eslint/require-await": { + "count": 2 + } + }, + "src/state/queries/preferences/index.ts": { + "react-hooks/immutability": { + "count": 1 + } + }, + "src/state/queries/preferences/useThreadPreferences.ts": { + "react-hooks/refs": { + "count": 2 } }, "src/state/queries/search-posts.ts": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 + } + }, + "src/state/queries/starter-packs.ts": { + "@typescript-eslint/require-await": { + "count": 1 + }, + "react-hooks/immutability": { + "count": 1 + } + }, + "src/state/queries/suggested-follows.ts": { + "@typescript-eslint/no-unused-vars": { + "count": 1 } }, "src/state/queries/threadgate/index.ts": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-floating-promises": { + "count": 3 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 + }, + "@typescript-eslint/require-await": { + "count": 3 } }, "src/state/queries/util.ts": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 } }, "src/state/session/agent.ts": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/require-await": { + "count": 1 + } + }, + "src/state/shell/color-mode.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + } + }, + "src/state/shell/composer/index.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/shell/onboarding.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 5 } }, "src/state/shell/progress-guide.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-floating-promises": { + "count": 4 + } + }, + "src/state/shell/reminders.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/shell/tick-every-minute.tsx": { + "react-hooks/purity": { + "count": 1 + } + }, + "src/storage/archive/index.ts": { + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 } }, "src/storage/index.ts": { "@typescript-eslint/no-explicit-any": { "count": 8 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + } + }, + "src/view/com/auth/SplashScreen.web.tsx": { + "react-hooks/set-state-in-effect": { + "count": 1 } }, "src/view/com/composer/Composer.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 }, + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 4 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 + }, + "react-hooks/immutability": { + "count": 2 + }, "react-hooks/preserve-manual-memoization": { "count": 3 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/view/com/composer/SelectMediaButton.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/view/com/composer/drafts/DraftsButton.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 } }, "src/view/com/composer/drafts/state/queries.ts": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-floating-promises": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 + } + }, + "src/view/com/composer/drafts/state/storage.ts": { + "@typescript-eslint/require-await": { + "count": 3 + } + }, + "src/view/com/composer/photos/EditImageDialog.web.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/view/com/composer/photos/Gallery.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 } }, "src/view/com/composer/photos/OpenCameraBtn.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/view/com/composer/select-language/SuggestedLanguage.tsx": { + "react-hooks/refs": { + "count": 1 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/view/com/composer/text-input/TextInput.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/view/com/composer/text-input/TextInput.web.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "react-hooks/refs": { + "count": 1 + } + }, + "src/view/com/composer/text-input/web/Autocomplete.tsx": { + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/view/com/composer/videos/pickVideo.web.ts": { + "@typescript-eslint/no-misused-promises": { + "count": 1 } }, "src/view/com/feeds/ComposerPrompt.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-floating-promises": { + "count": 2 + } + }, + "src/view/com/feeds/FeedPage.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 2 } }, "src/view/com/feeds/ProfileFeedgens.tsx": { "@typescript-eslint/no-explicit-any": { "count": 3 + }, + "@typescript-eslint/no-floating-promises": { + "count": 2 + }, + "@typescript-eslint/no-misused-promises": { + "count": 3 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 } }, "src/view/com/lists/ListMembers.tsx": { "@typescript-eslint/no-explicit-any": { "count": 3 + }, + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 } }, "src/view/com/lists/MyLists.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-misused-promises": { + "count": 3 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 6 } }, "src/view/com/lists/ProfileLists.tsx": { "@typescript-eslint/no-explicit-any": { "count": 3 + }, + "@typescript-eslint/no-floating-promises": { + "count": 2 + }, + "@typescript-eslint/no-misused-promises": { + "count": 3 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 + } + }, + "src/view/com/modals/Modal.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/require-await": { + "count": 1 + }, + "react-hooks/refs": { + "count": 1 } }, "src/view/com/notifications/NotificationFeed.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 6 + }, + "react-hooks/set-state-in-effect": { + "count": 1 } }, "src/view/com/notifications/NotificationFeedItem.tsx": { "@typescript-eslint/no-explicit-any": { "count": 3 + }, + "@typescript-eslint/no-misused-promises": { + "count": 3 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 } }, "src/view/com/pager/Pager.tsx": { "@typescript-eslint/no-explicit-any": { "count": 4 + }, + "react-hooks/refs": { + "count": 1 } }, "src/view/com/pager/Pager.web.tsx": { + "react-hooks/immutability": { + "count": 1 + }, "react-hooks/preserve-manual-memoization": { "count": 1 + }, + "react-hooks/refs": { + "count": 1 } }, "src/view/com/pager/PagerWithHeader.tsx": { @@ -593,14 +2242,46 @@ "count": 2 } }, + "src/view/com/pager/TabBar.tsx": { + "react-hooks/immutability": { + "count": 1 + } + }, "src/view/com/pager/TabBar.web.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 4 + } + }, + "src/view/com/post-thread/PostLikedBy.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 2 + } + }, + "src/view/com/post-thread/PostQuotes.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + } + }, + "src/view/com/post-thread/PostRepostedBy.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 2 } }, "src/view/com/posts/FeedShutdownMsg.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-misused-promises": { + "count": 2 } }, "src/view/com/posts/PostFeed.tsx": { @@ -611,16 +2292,47 @@ "src/view/com/posts/PostFeedErrorMessage.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 8 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 9 + } + }, + "src/view/com/profile/ProfileFollowers.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 2 + } + }, + "src/view/com/profile/ProfileFollows.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 2 } }, "src/view/com/profile/ProfileMenu.tsx": { "@typescript-eslint/no-explicit-any": { "count": 6 + }, + "@typescript-eslint/no-floating-promises": { + "count": 4 + }, + "@typescript-eslint/no-misused-promises": { + "count": 3 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 6 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 12 } }, "src/view/com/testing/TestCtrls.e2e.tsx": { "@typescript-eslint/no-explicit-any": { - "count": 2 + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 9 } }, "src/view/com/util/EmptyState.tsx": { @@ -631,16 +2343,36 @@ "src/view/com/util/ErrorBoundary.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 } }, "src/view/com/util/EventStopper.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 } }, "src/view/com/util/Link.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-misused-promises": { + "count": 2 + } + }, + "src/view/com/util/LoadingPlaceholder.tsx": { + "react-hooks/purity": { + "count": 1 } }, "src/view/com/util/MainScrollProvider.tsx": { @@ -648,9 +2380,22 @@ "count": 2 } }, + "src/view/com/util/UserAvatar.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 3 + } + }, + "src/view/com/util/UserBanner.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 3 + } + }, "src/view/com/util/ViewSelector.tsx": { "@typescript-eslint/no-explicit-any": { "count": 6 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 } }, "src/view/com/util/Views.tsx": { @@ -658,14 +2403,101 @@ "count": 1 } }, + "src/view/com/util/Views.web.tsx": { + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + } + }, + "src/view/com/util/forms/Button.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, "src/view/screens/Debug.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 4 + } + }, + "src/view/screens/Feeds.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 3 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + } + }, + "src/view/screens/Home.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/view/screens/ModerationBlockedAccounts.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 3 + } + }, + "src/view/screens/ModerationMutedAccounts.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 3 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + } + }, + "src/view/screens/Notifications.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 3 + } + }, + "src/view/screens/Storybook/ListContained.tsx": { + "@typescript-eslint/no-unsafe-call": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 + } + }, + "src/view/screens/Storybook/Storybook.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/view/shell/Drawer.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 2 } }, "src/view/shell/createNativeStackNavigatorWithAuth.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 1 + }, + "react-hooks/refs": { + "count": 13 + } + }, + "src/view/shell/desktop/Search.tsx": { + "@typescript-eslint/no-unsafe-call": { + "count": 3 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 3 + }, + "react-hooks/refs": { + "count": 3 + } + }, + "src/view/shell/index.web.tsx": { + "react-hooks/set-state-in-effect": { + "count": 1 } } } \ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs index 50ab8e5da4..c8addf66ae 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -134,11 +134,10 @@ export default defineConfig( 'react-native/no-inline-styles': 'off', ...reactNativeA11y.configs.all.rules, 'react-compiler/react-compiler': 'warn', - // TODO: Fix these and set to error - 'react-hooks/set-state-in-effect': 'warn', - 'react-hooks/purity': 'warn', - 'react-hooks/refs': 'warn', - 'react-hooks/immutability': 'warn', + 'react-hooks/set-state-in-effect': 'error', + 'react-hooks/purity': 'error', + 'react-hooks/refs': 'error', + 'react-hooks/immutability': 'error', /** * Import sorting @@ -235,9 +234,8 @@ export default defineConfig( }, ], /** - * Maintain previous behavior - these are stricter in typescript-eslint - * v8 `warn` ones are probably worth fixing. `off` ones are a bit too - * nit-picky + * Maintain previous behavior via eslint-suppressions.json - these are + * stricter in typescript-eslint v8. `off` ones are a bit too nit-picky. */ '@typescript-eslint/no-explicit-any': 'error', '@typescript-eslint/ban-ts-comment': 'off', @@ -247,18 +245,18 @@ export default defineConfig( '@typescript-eslint/unbound-method': 'off', '@typescript-eslint/no-unsafe-argument': 'off', '@typescript-eslint/no-unsafe-return': 'off', - '@typescript-eslint/no-unsafe-member-access': 'warn', - '@typescript-eslint/no-unsafe-call': 'warn', - '@typescript-eslint/no-floating-promises': 'warn', - '@typescript-eslint/no-misused-promises': 'warn', - '@typescript-eslint/require-await': 'warn', - '@typescript-eslint/no-unsafe-enum-comparison': 'warn', - '@typescript-eslint/no-unnecessary-type-assertion': 'warn', - '@typescript-eslint/no-redundant-type-constituents': 'warn', - '@typescript-eslint/no-duplicate-type-constituents': 'warn', - '@typescript-eslint/no-base-to-string': 'warn', - '@typescript-eslint/prefer-promise-reject-errors': 'warn', - '@typescript-eslint/await-thenable': 'warn', + '@typescript-eslint/no-unsafe-member-access': 'error', + '@typescript-eslint/no-unsafe-call': 'error', + '@typescript-eslint/no-floating-promises': 'error', + '@typescript-eslint/no-misused-promises': 'error', + '@typescript-eslint/require-await': 'error', + '@typescript-eslint/no-unsafe-enum-comparison': 'error', + '@typescript-eslint/no-unnecessary-type-assertion': 'error', + '@typescript-eslint/no-redundant-type-constituents': 'error', + '@typescript-eslint/no-duplicate-type-constituents': 'error', + '@typescript-eslint/no-base-to-string': 'error', + '@typescript-eslint/prefer-promise-reject-errors': 'error', + '@typescript-eslint/await-thenable': 'error', 'no-restricted-imports': [ 'error', diff --git a/package.json b/package.json index b8bd26e241..c0670da958 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "prettier": "prettier --check ." }, "dependencies": { - "@atproto/api": "0.20.8", + "@atproto/api": "0.20.9", "@atproto/syntax": "0.6.1", "@bitdrift/react-native": "^0.6.8", "@braintree/sanitize-url": "^6.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b3d0003107..564e39f8f0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -242,8 +242,8 @@ importers: .: dependencies: '@atproto/api': - specifier: 0.20.8 - version: 0.20.8 + specifier: 0.20.9 + version: 0.20.9 '@atproto/syntax': specifier: 0.6.1 version: 0.6.1 @@ -877,8 +877,8 @@ packages: graphql: optional: true - '@atproto/api@0.20.8': - resolution: {integrity: sha512-rTkA6kOmA2axSrg6VgpdXpsCFWpofnHBOn6pKg69Ju5MpIHqk4haQMgjBcVh1G3kUxzwgSAr7SYrPS3dFe5Etg==} + '@atproto/api@0.20.9': + resolution: {integrity: sha512-Yuw7Ewn+yMJZ8GskbuvI3lKPW65rsXic1xjFA2Dpq6H8WjVYs6xNZ31bkwtTYDDwjKIZcJmAVbAVgdfjo4T9iw==} engines: {node: '>=22'} '@atproto/common-web@0.5.0': @@ -9493,7 +9493,7 @@ snapshots: '@0no-co/graphql.web@1.2.0': {} - '@atproto/api@0.20.8': + '@atproto/api@0.20.9': dependencies: '@atproto/common-web': 0.5.0 '@atproto/lexicon': 0.7.1 diff --git a/src/components/MediaPreview.tsx b/src/components/MediaPreview.tsx index 996e3b18e8..de07a03b23 100644 --- a/src/components/MediaPreview.tsx +++ b/src/components/MediaPreview.tsx @@ -1,6 +1,10 @@ import {type StyleProp, StyleSheet, View, type ViewStyle} from 'react-native' import {Image} from 'expo-image' -import {type AppBskyEmbedImages, type AppBskyFeedDefs} from '@atproto/api' +import { + AppBskyEmbedGallery, + type AppBskyEmbedImages, + type AppBskyFeedDefs, +} from '@atproto/api' import {Trans, useLingui} from '@lingui/react/macro' import {shareImageModal} from '#/lib/media/manip' @@ -52,23 +56,26 @@ export function Embed({ // a 10-image gallery doesn't blow out the row width. return ( - {e.view.items.slice(0, 4).map(item => { - const image: AppBskyEmbedImages.ViewImage = { - thumb: item.thumbnail, - fullsize: item.fullsize, - alt: item.alt, - aspectRatio: item.aspectRatio, - } - return peekable ? ( - - ) : ( - - ) - })} + {e.view.items + .filter(AppBskyEmbedGallery.isViewImage) + .slice(0, 4) + .map(item => { + const image: AppBskyEmbedImages.ViewImage = { + thumb: item.thumbnail, + fullsize: item.fullsize, + alt: item.alt, + aspectRatio: item.aspectRatio, + } + return peekable ? ( + + ) : ( + + ) + })} ) } else if (e.type === 'link') { diff --git a/src/components/Post/Embed/ImageEmbed.tsx b/src/components/Post/Embed/ImageEmbed.tsx index 5b51c29f5b..fca9cec5c8 100644 --- a/src/components/Post/Embed/ImageEmbed.tsx +++ b/src/components/Post/Embed/ImageEmbed.tsx @@ -2,7 +2,7 @@ import {useRef} from 'react' import {InteractionManager, View} from 'react-native' import {type AnimatedRef} from 'react-native-reanimated' import {Image} from 'expo-image' -import {type AppBskyEmbedImages} from '@atproto/api' +import {AppBskyEmbedGallery, type AppBskyEmbedImages} from '@atproto/api' import {atoms as a, tokens} from '#/alf' import {AutoSizedImage} from '#/components/images/AutoSizedImage' @@ -28,7 +28,7 @@ export function ImageEmbed({ const {openLightbox} = useLightboxControls() const images: AppBskyEmbedImages.ViewImage[] = embed.type === 'gallery' - ? embed.view.items.map(item => ({ + ? embed.view.items.filter(AppBskyEmbedGallery.isViewImage).map(item => ({ thumb: item.thumbnail, fullsize: item.fullsize, alt: item.alt, @@ -101,8 +101,7 @@ export function ImageEmbed({ crop={ rest.viewContext === PostEmbedViewContext.ThreadHighlighted ? 'none' - : rest.viewContext === - PostEmbedViewContext.FeedEmbedRecordWithMedia + : rest.isWithinQuote ? 'square' : 'constrained' } @@ -117,10 +116,7 @@ export function ImageEmbed({ onPress(0, [containerRef], [dims]) } onPressIn={() => onPressIn(0)} - hideBadge={ - rest.viewContext === - PostEmbedViewContext.FeedEmbedRecordWithMedia - } + hideBadge={rest.isWithinQuote} /> @@ -135,6 +131,7 @@ export function ImageEmbed({ onPress={onPress} onPressIn={onPressIn} viewContext={rest.viewContext} + isWithinQuote={rest.isWithinQuote} /> ) diff --git a/src/components/Prompt.tsx b/src/components/Prompt.tsx index 916a7ec2d9..5fdf18e2a1 100644 --- a/src/components/Prompt.tsx +++ b/src/components/Prompt.tsx @@ -224,6 +224,7 @@ export function Basic({ cancelButtonCta, confirmButtonCta, onConfirm, + onClose, confirmButtonColor, showCancel = true, }: React.PropsWithChildren<{ @@ -240,11 +241,12 @@ export function Basic({ * should NOT close the dialog as a side effect of this method. */ onConfirm: (e: GestureResponderEvent) => void + onClose?: () => void confirmButtonColor?: ButtonColor showCancel?: boolean }>) { return ( - + {title} {description && {description}} diff --git a/src/components/Toast/Toast.tsx b/src/components/Toast/Toast.tsx index ac5bc4889a..3e11cf051e 100644 --- a/src/components/Toast/Toast.tsx +++ b/src/components/Toast/Toast.tsx @@ -8,8 +8,10 @@ import { type UninheritableButtonProps, } from '#/components/Button' import {CircleCheck_Stroke2_Corner0_Rounded as CircleCheck} from '#/components/icons/CircleCheck' -import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' -import {CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon} from '#/components/icons/CircleInfo' +import { + CircleInfo_Stroke2_Corner0_Rounded as CircleInfo, + CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon, +} from '#/components/icons/CircleInfo' import {type Props as SVGIconProps} from '#/components/icons/common' import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' import {dismiss} from '#/components/Toast/sonner' diff --git a/src/components/dms/AfterReportDialog.tsx b/src/components/dms/AfterReportDialog.tsx index 3df194fc8b..060ea1fcfb 100644 --- a/src/components/dms/AfterReportDialog.tsx +++ b/src/components/dms/AfterReportDialog.tsx @@ -33,14 +33,19 @@ export const AfterReportDialog = memo(function BlockOrDeleteDialogInner({ control, params, currentScreen, + onClose, }: { control: Dialog.DialogControlProps params: ReportDialogParams currentScreen: 'list' | 'conversation' + onClose?: () => void }): React.ReactNode { const {t: l} = useLingui() return ( - + { - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) - convo - .deleteMessage(message.id) - .then(() => Toast.show(l({message: 'Message deleted', context: 'toast'}))) - .catch(() => Toast.show(l`Failed to delete message`)) - }, [l, convo, message.id]) - const onEmojiSelect = useCallback( (emoji: string) => { if ( @@ -128,104 +112,72 @@ export let MessageContextMenu = ({ const sender = senderProfile return ( - <> - - {IS_NATIVE && reactionsAvailable && ( - - - - )} - - - {children} - - - + {IS_NATIVE && reactionsAvailable && ( + - {message.text.length > 0 && ( - <> - - - {l`Translate`} - - - - - {l`Copy message text`} - - - - )} + + + )} + + + {children} + + + + {message.text.length > 0 && ( + <> + + + {l`Translate`} + + + + + {l`Copy message text`} + + + + )} + openDeleteMessage(message)}> + + {l`Delete for me`} + + {!isFromSelf && ( deleteControl.open()}> - - {l`Delete for me`} + testID="messageDropdownReportBtn" + label={l`Report message`} + onPress={() => openReportMessage(message, senderProfile)}> + + {l`Report`} - {!isFromSelf && ( - reportControl.open()}> - - {l`Report`} - - )} - - - { - if (sender) { - unstableCacheProfileView(queryClient, sender) - } - blockOrDeleteControl.open() - }} - /> - - - + )} + + ) } MessageContextMenu = memo(MessageContextMenu) diff --git a/src/components/dms/MessageItem.tsx b/src/components/dms/MessageItem.tsx index 6f1819ada2..ac6acb5891 100644 --- a/src/components/dms/MessageItem.tsx +++ b/src/components/dms/MessageItem.tsx @@ -40,8 +40,8 @@ import {useSession} from '#/state/session' import {atoms as a, native, platform, useTheme} from '#/alf' import {isOnlyEmoji} from '#/alf/typography' import {Button} from '#/components/Button' -import {useDialogControl} from '#/components/Dialog' import {ActionsWrapper} from '#/components/dms/ActionsWrapper' +import {useMessageDialogs} from '#/components/dms/MessageOverlays' import {InlineLinkText, Link} from '#/components/Link' import * as ProfileCard from '#/components/ProfileCard' import * as Prompt from '#/components/Prompt' @@ -49,7 +49,7 @@ import {RichText} from '#/components/RichText' import {Text} from '#/components/Typography' import {DateDivider} from './DateDivider' import {MessageItemEmbed} from './MessageItemEmbed' -import {ReactionsDialog} from './ReactionsDialog' +import {groupReactions} from './ReactionsDialog' import {CLUSTERED_MESSAGE_THRESHOLD_MS, MESSAGE_GAP_THRESHOLD_MS} from './util' const AVATAR_SIZE = 28 @@ -118,7 +118,7 @@ let MessageItem = ({ const {message} = item const profile = useMaybeProfileShadow(relatedProfiles.get(message.sender.did)) - const reactionsControl = useDialogControl() + const {openReactions} = useMessageDialogs() const isPending = item.type === 'pending-message' @@ -243,34 +243,10 @@ let MessageItem = ({ ) - const groupedReactions = useMemo(() => { - const reactions = message.reactions ?? [] - const grouped = new Map< - string, - { - key: string - value: string - senders: ChatBskyConvoDefs.ReactionViewSender[] - count: number - } - >() - for (const reaction of reactions) { - if (!reaction) continue - const existing = grouped.get(reaction.value) - if (existing) { - existing.senders.push(reaction.sender) - existing.count++ - } else { - grouped.set(reaction.value, { - key: reaction.value, - value: reaction.value, - senders: [reaction.sender], - count: 1, - }) - } - } - return Array.from(grouped.values()) - }, [message.reactions]) + const groupedReactions = useMemo( + () => groupReactions(message.reactions), + [message.reactions], + ) const reactions = useMemo(() => message.reactions ?? [], [message.reactions]) @@ -336,7 +312,7 @@ let MessageItem = ({ transform: [{translateY: -8}], }, ]} - onPress={isGroupChat ? reactionsControl.open : undefined}> + onPress={isGroupChat ? () => openReactions(message) : undefined}> {groupedReactions.map(group => ( ) : null} - ) diff --git a/src/components/dms/MessageOverlays.tsx b/src/components/dms/MessageOverlays.tsx new file mode 100644 index 0000000000..41228f2899 --- /dev/null +++ b/src/components/dms/MessageOverlays.tsx @@ -0,0 +1,175 @@ +import { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from 'react' +import {LayoutAnimation} from 'react-native' +import {type ChatBskyConvoDefs} from '@atproto/api' +import {useLingui} from '@lingui/react/macro' +import {useQueryClient} from '@tanstack/react-query' + +import {useConvoActive} from '#/state/messages/convo' +import {unstableCacheProfileView} from '#/state/queries/unstable-profile-cache' +import {useDialogControl} from '#/components/Dialog' +import {AfterReportDialog} from '#/components/dms/AfterReportDialog' +import {ReactionsDialog} from '#/components/dms/ReactionsDialog' +import {ReportDialog} from '#/components/moderation/ReportDialog' +import * as Prompt from '#/components/Prompt' +import {usePromptControl} from '#/components/Prompt' +import * as Toast from '#/components/Toast' +import type * as bsky from '#/types/bsky' + +type MessageDialogsContextType = { + openDeleteMessage: (message: ChatBskyConvoDefs.MessageView) => void + openReportMessage: ( + message: ChatBskyConvoDefs.MessageView, + senderProfile: bsky.profile.AnyProfileView | undefined, + ) => void + openReactions: (message: ChatBskyConvoDefs.MessageView) => void +} + +const Context = createContext(null) + +export function useMessageDialogs() { + const ctx = useContext(Context) + if (!ctx) { + throw new Error('useMessageDialogs must be used within a MessageOverlays') + } + return ctx +} + +export function MessageOverlays({children}: {children: React.ReactNode}) { + const {t: l} = useLingui() + const queryClient = useQueryClient() + const convo = useConvoActive() + + const deleteControl = usePromptControl() + const reportControl = usePromptControl() + const afterReportControl = usePromptControl() + const reactionsControl = useDialogControl() + + const [deleteTarget, setDeleteTarget] = + useState(null) + const [reportTarget, setReportTarget] = useState<{ + message: ChatBskyConvoDefs.MessageView + senderProfile: bsky.profile.AnyProfileView | undefined + } | null>(null) + const [afterReportTarget, setAfterReportTarget] = + useState(null) + const [reactionsTarget, setReactionsTarget] = + useState(null) + + const openDeleteMessage = useCallback( + (message: ChatBskyConvoDefs.MessageView) => { + setDeleteTarget(message) + deleteControl.open() + }, + [deleteControl], + ) + + const openReportMessage = useCallback( + ( + message: ChatBskyConvoDefs.MessageView, + senderProfile: bsky.profile.AnyProfileView | undefined, + ) => { + setReportTarget({message, senderProfile}) + reportControl.open() + }, + [reportControl], + ) + + const openReactions = useCallback( + (message: ChatBskyConvoDefs.MessageView) => { + setReactionsTarget(message) + }, + [], + ) + + // These dialogs are conditionally mounted, so we can't open them in the same + // tick that we set their targets - the control refs aren't attached yet. Open + // in an effect after the dialog has mounted. + useEffect(() => { + if (reactionsTarget) { + reactionsControl.open() + } + }, [reactionsTarget, reactionsControl]) + + useEffect(() => { + if (afterReportTarget) { + afterReportControl.open() + } + }, [afterReportTarget, afterReportControl]) + + const onConfirmDelete = useCallback(() => { + if (!deleteTarget) return + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) + convo + .deleteMessage(deleteTarget.id) + .then(() => Toast.show(l({message: 'Message deleted', context: 'toast'}))) + .catch(() => Toast.show(l`Failed to delete message`)) + }, [l, convo, deleteTarget]) + + const onAfterReportSubmit = useCallback(() => { + if (!reportTarget) return + if (reportTarget.senderProfile) { + unstableCacheProfileView(queryClient, reportTarget.senderProfile) + } + setAfterReportTarget(reportTarget.message) + }, [queryClient, reportTarget]) + + const ctx = useMemo( + () => ({openDeleteMessage, openReportMessage, openReactions}), + [openDeleteMessage, openReportMessage, openReactions], + ) + + const reportSubject = reportTarget + ? ({ + view: 'message', + convoId: convo.convo.view.id, + message: reportTarget.message, + } as const) + : undefined + + return ( + + {children} + setReportTarget(null)} + /> + {afterReportTarget && ( + setAfterReportTarget(null)} + /> + )} + {reactionsTarget && ( + setReactionsTarget(null)} + /> + )} + setDeleteTarget(null)} + /> + + ) +} diff --git a/src/components/dms/ReactionsDialog.tsx b/src/components/dms/ReactionsDialog.tsx index 576ef4cead..8119cc1b7c 100644 --- a/src/components/dms/ReactionsDialog.tsx +++ b/src/components/dms/ReactionsDialog.tsx @@ -1,4 +1,4 @@ -import {useRef, useState} from 'react' +import {useMemo, useRef, useState} from 'react' import { LayoutAnimation, Pressable, @@ -37,14 +37,12 @@ export function ReactionsDialog({ control, relatedProfiles, message, - reactions, - groupedReactions, + onClose, }: { control: Dialog.DialogControlProps relatedProfiles: Map message: ChatBskyConvoDefs.MessageView - reactions?: ChatBskyConvoDefs.ReactionView[] - groupedReactions?: Reaction[] + onClose?: () => void }) { const {t: l} = useLingui() @@ -54,6 +52,9 @@ export function ReactionsDialog({ const [selected, setSelected] = useState('all') + const reactions = message.reactions + const groupedReactions = useMemo(() => groupReactions(reactions), [reactions]) + const filteredReactions = reactions?.filter( r => selected === 'all' || r.value === selected, ) @@ -78,7 +79,10 @@ export function ReactionsDialog({ return ( setSelected('all')} + onClose={() => { + setSelected('all') + onClose?.() + }} nativeOptions={{ preventExpansion: true, minHeight: screenHeight / 2, @@ -388,3 +392,25 @@ function ReactionTab({ ) } + +export function groupReactions( + reactions: ChatBskyConvoDefs.ReactionView[] | undefined, +): Reaction[] { + const grouped = new Map() + for (const reaction of reactions ?? []) { + if (!reaction) continue + const existing = grouped.get(reaction.value) + if (existing) { + existing.senders.push(reaction.sender) + existing.count++ + } else { + grouped.set(reaction.value, { + key: reaction.value, + value: reaction.value, + senders: [reaction.sender], + count: 1, + }) + } + } + return Array.from(grouped.values()) +} diff --git a/src/components/images/Gallery/index.tsx b/src/components/images/Gallery/index.tsx index f4ffcd7159..fe2b63e606 100644 --- a/src/components/images/Gallery/index.tsx +++ b/src/components/images/Gallery/index.tsx @@ -54,6 +54,7 @@ interface GalleryProps { ) => void onPressIn?: (index: number) => void viewContext?: PostEmbedViewContext + isWithinQuote?: boolean } const Context = createContext<{ @@ -97,6 +98,7 @@ export function Gallery({ onPress, onPressIn, viewContext, + isWithinQuote, }: GalleryProps) { const {t: l} = useLingui() const ax = useAnalytics() @@ -104,14 +106,21 @@ export function Gallery({ const largeAltBadge = useLargeAltBadgeEnabled() const bps = useBreakpoints() const window = useWindowDimensions() - const isWithinQuote = - viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia const isWithinChat = viewContext === PostEmbedViewContext.ChatMessage const hideBadges = isWithinQuote const contentHeight = useMemo(() => { if (isWithinChat) { return 120 } + if (isWithinQuote) { + if (bps.gtMobile) { + return 220 + } else if (bps.gtPhone) { + return 190 + } else { + return 150 + } + } if (bps.gtMobile) { return 300 } else if (bps.gtPhone) { @@ -119,7 +128,7 @@ export function Gallery({ } else { return 200 } - }, [bps, isWithinChat]) + }, [bps, isWithinChat, isWithinQuote]) /* * Container overflow styles @@ -220,7 +229,7 @@ export function Gallery({ crop={ viewContext === PostEmbedViewContext.ThreadHighlighted ? 'none' - : viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia + : isWithinQuote ? 'square' : 'constrained' } @@ -229,9 +238,7 @@ export function Gallery({ onPress?.(index, [containerRef], [dims]) } onPressIn={() => onPressIn?.(index)} - hideBadge={ - viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia - } + hideBadge={isWithinQuote} /> ))} diff --git a/src/components/images/Gallery/maybeApplyGalleryOffsetStyles.ts b/src/components/images/Gallery/maybeApplyGalleryOffsetStyles.ts index 6b0d8067bb..1749daf7ea 100644 --- a/src/components/images/Gallery/maybeApplyGalleryOffsetStyles.ts +++ b/src/components/images/Gallery/maybeApplyGalleryOffsetStyles.ts @@ -1,4 +1,5 @@ import { + AppBskyEmbedGallery, AppBskyEmbedImages, AppBskyEmbedRecordWithMedia, type AppBskyFeedDefs, @@ -7,7 +8,6 @@ import { type ModerationUI, } from '@atproto/api' -import {AppBskyEmbedGallery} from '#/lib/api/gallery-embed-shim' import {unique} from '#/lib/moderation' import {type AppModerationCause} from '#/components/Pills' import {Features, features} from '#/analytics/features' diff --git a/src/components/moderation/ReportDialog/index.tsx b/src/components/moderation/ReportDialog/index.tsx index c24c6feae0..d1d3aa5584 100644 --- a/src/components/moderation/ReportDialog/index.tsx +++ b/src/components/moderation/ReportDialog/index.tsx @@ -75,9 +75,11 @@ export function ReportDialog( () => (props.subject ? parseReportSubject(props.subject) : undefined), [props.subject], ) + const propsOnClose = props.onClose const onClose = useCallback(() => { ax.metric('reportDialog:close', {}) - }, [ax]) + propsOnClose?.() + }, [ax, propsOnClose]) return ( diff --git a/src/components/moderation/ReportDialog/types.ts b/src/components/moderation/ReportDialog/types.ts index 89089d138b..57ed3cd279 100644 --- a/src/components/moderation/ReportDialog/types.ts +++ b/src/components/moderation/ReportDialog/types.ts @@ -88,4 +88,8 @@ export type ReportDialogProps = { * Called if the report was successfully submitted. */ onAfterSubmit?: () => void + /** + * Called after the dialog finishes closing. + */ + onClose?: () => void } diff --git a/src/lib/api/gallery-embed-shim.impl.ts b/src/lib/api/gallery-embed-shim.impl.ts deleted file mode 100644 index 3788eb9e7b..0000000000 --- a/src/lib/api/gallery-embed-shim.impl.ts +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Implementation backing `AppBskyEmbedGallery`. See gallery-embed-shim.ts. - */ -import {type AppBskyEmbedDefs, type BlobRef} from '@atproto/api' - -export interface Main { - $type?: 'app.bsky.embed.gallery' - items: Image[] -} - -export interface Image { - $type?: 'app.bsky.embed.gallery#image' - image: BlobRef - alt: string - aspectRatio: AppBskyEmbedDefs.AspectRatio -} - -export interface View { - $type?: 'app.bsky.embed.gallery#view' - items: ViewImage[] -} - -export interface ViewImage { - $type?: 'app.bsky.embed.gallery#viewImage' - thumbnail: string - fullsize: string - alt: string - aspectRatio: AppBskyEmbedDefs.AspectRatio -} - -export function isMain( - v: V, -): v is V & Main & {$type: 'app.bsky.embed.gallery'} { - return ( - typeof v === 'object' && - v !== null && - (v as {$type?: string}).$type === 'app.bsky.embed.gallery' - ) -} - -export function isImage( - v: V, -): v is V & Image & {$type: 'app.bsky.embed.gallery#image'} { - return ( - typeof v === 'object' && - v !== null && - (v as {$type?: string}).$type === 'app.bsky.embed.gallery#image' - ) -} - -export function isView( - v: V, -): v is V & View & {$type: 'app.bsky.embed.gallery#view'} { - return ( - typeof v === 'object' && - v !== null && - (v as {$type?: string}).$type === 'app.bsky.embed.gallery#view' - ) -} - -export function isViewImage( - v: V, -): v is V & ViewImage & {$type: 'app.bsky.embed.gallery#viewImage'} { - return ( - typeof v === 'object' && - v !== null && - (v as {$type?: string}).$type === 'app.bsky.embed.gallery#viewImage' - ) -} diff --git a/src/lib/api/gallery-embed-shim.ts b/src/lib/api/gallery-embed-shim.ts deleted file mode 100644 index b65a098985..0000000000 --- a/src/lib/api/gallery-embed-shim.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Local shim for `app.bsky.embed.gallery` until @atproto/api ships the - * generated types. Mirrors the shape from atproto PR #4827: - * https://github.com/bluesky-social/atproto/pull/4827 - * - * Once the lexicon ships and we bump @atproto/api, delete this file and - * replace `import {AppBskyEmbedGallery} from '#/lib/api/gallery-embed-shim'` - * with `import {AppBskyEmbedGallery} from '@atproto/api'`. - */ -import * as gallery from './gallery-embed-shim.impl' - -export {gallery as AppBskyEmbedGallery} diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index 283f552ec6..4e0a8d47c9 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -1,6 +1,7 @@ import { type $Typed, type AppBskyEmbedExternal, + type AppBskyEmbedGallery, type AppBskyEmbedImages, type AppBskyEmbedRecord, type AppBskyEmbedRecordWithMedia, @@ -21,7 +22,6 @@ import {sha256} from 'js-sha256' import {CID} from 'multiformats/cid' import * as Hasher from 'multiformats/hashes/hasher' -import {type AppBskyEmbedGallery} from '#/lib/api/gallery-embed-shim' import {isNetworkError} from '#/lib/strings/errors' import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip' import {logger} from '#/logger' @@ -347,14 +347,14 @@ async function resolveMedia( count: imagesDraft.length, }) onStateChange?.(t`Uploading images...`) - const items: AppBskyEmbedGallery.Image[] = await Promise.all( + const items: $Typed[] = await Promise.all( imagesDraft.map(async (image, i) => { logger.debug(`Compressing gallery image #${i}`) const {path, width, height, mime} = await compressImage(image) logger.debug(`Uploading gallery image #${i}`) const res = await uploadBlob(agent, path, mime) return { - $type: 'app.bsky.embed.gallery#image', + $type: 'app.bsky.embed.gallery#image' as const, image: res.data.blob, alt: image.alt, aspectRatio: {width, height}, diff --git a/src/lib/hooks/useTimeAgo.ts b/src/lib/hooks/useTimeAgo.ts index 67398ec547..ffc808a91f 100644 --- a/src/lib/hooks/useTimeAgo.ts +++ b/src/lib/hooks/useTimeAgo.ts @@ -58,12 +58,12 @@ export function dateDiff( if (diffSeconds < NOW) { diff = { value: 0, - unit: 'now' as DateDiff['unit'], + unit: 'now', } } else if (diffSeconds < MINUTE) { diff = { value: diffSeconds, - unit: 'second' as DateDiff['unit'], + unit: 'second', } } else if (diffSeconds < HOUR) { const value = @@ -72,7 +72,7 @@ export function dateDiff( : Math.floor(diffSeconds / MINUTE) diff = { value, - unit: 'minute' as DateDiff['unit'], + unit: 'minute', } } else if (diffSeconds < DAY) { const value = @@ -81,7 +81,7 @@ export function dateDiff( : Math.floor(diffSeconds / HOUR) diff = { value, - unit: 'hour' as DateDiff['unit'], + unit: 'hour', } } else if (diffSeconds < MONTH_30) { const value = @@ -90,7 +90,7 @@ export function dateDiff( : Math.floor(diffSeconds / DAY) diff = { value, - unit: 'day' as DateDiff['unit'], + unit: 'day', } } else { const value = @@ -99,7 +99,7 @@ export function dateDiff( : Math.floor(diffSeconds / MONTH_30) diff = { value, - unit: 'month' as DateDiff['unit'], + unit: 'month', } } diff --git a/src/lib/routes/links.ts b/src/lib/routes/links.ts index 908db7cba7..c87ccb5a39 100644 --- a/src/lib/routes/links.ts +++ b/src/lib/routes/links.ts @@ -19,8 +19,8 @@ export function makeProfileLink( export function makeCustomFeedLink( did: string, rkey: string, - segment?: string | undefined, - feedCacheKey?: 'discover' | 'explore' | undefined, + segment?: string, + feedCacheKey?: 'discover' | 'explore', ) { return ( [`/profile`, did, 'feed', rkey, ...(segment ? [segment] : [])].join('/') + diff --git a/src/screens/Messages/components/MessagesList.tsx b/src/screens/Messages/components/MessagesList.tsx index 99b725267e..fbebdc0745 100644 --- a/src/screens/Messages/components/MessagesList.tsx +++ b/src/screens/Messages/components/MessagesList.tsx @@ -56,6 +56,7 @@ import {MessageListError} from '#/screens/Messages/components/MessageListError' import {atoms as a, platform, tokens, useTheme, web} from '#/alf' import {DateDivider} from '#/components/dms/DateDivider' import {MessageItem} from '#/components/dms/MessageItem' +import {MessageOverlays} from '#/components/dms/MessageOverlays' import {NewMessagesPill} from '#/components/dms/NewMessagesPill' import {SystemMessageGroup} from '#/components/dms/SystemMessageGroup' import {SystemMessageItem} from '#/components/dms/SystemMessageItem' @@ -498,127 +499,133 @@ export function MessagesList({ return ( - - {/* Custom scroll provider so that we can use the `onScroll` event in our custom List implementation */} - - - - - {convoState.hasAllHistory ? ( - convoState.convo?.kind === 'group' ? ( - - ) : ( - - ) - ) : null} - - } - // native only (prop is not supported on web) - renderScrollComponent={renderScrollComponent} - contentContainerStyle={{ - paddingBottom: platform({ - // ios is slightly larger as the input has no top padding - ios: tokens.space.lg, - android: tokens.space.md, - web: 0, // web uses ListFooterComponent instead for scroll reasons - }), - }} - ListFooterComponent={ - - } - style={[ - web({ - scrollbarWidth: 'thin', - scrollbarColor: `${t.palette.contrast_100} transparent`, - scrollbarGutter: 'stable', - }), - ]} - pointerEvents={!hasScrolled ? 'none' : 'auto'} - contentInset={{top: transparentHeaderHeight}} - scrollIndicatorInsets={{top: transparentHeaderHeight}} - /> - - - - {footer ?? ( - - {({loading}) => - ax.features.enabled(ax.features.DmsNewMessageComposerEnable) ? ( - - void onSendMessage(message) - } - hasEmbed={!!embedUri} - setEmbed={setEmbed} - loading={loading}> - + + {/* Custom scroll provider so that we can use the `onScroll` event in our custom List implementation */} + + + + + {convoState.hasAllHistory ? ( + convoState.convo?.kind === 'group' ? ( + + ) : ( + + ) + ) : null} + + } + // native only (prop is not supported on web) + renderScrollComponent={renderScrollComponent} + contentContainerStyle={{ + paddingBottom: platform({ + // ios is slightly larger as the input has no top padding + ios: tokens.space.lg, + android: tokens.space.md, + web: 0, // web uses ListFooterComponent instead for scroll reasons + }), + }} + ListFooterComponent={ + + } + style={[ + web({ + scrollbarWidth: 'thin', + scrollbarColor: `${t.palette.contrast_100} transparent`, + scrollbarGutter: 'stable', + }), + ]} + pointerEvents={!hasScrolled ? 'none' : 'auto'} + contentInset={{top: transparentHeaderHeight}} + scrollIndicatorInsets={{top: transparentHeaderHeight}} + /> + + + + {footer ?? ( + + {({loading}) => + ax.features.enabled( + ax.features.DmsNewMessageComposerEnable, + ) ? ( + + void onSendMessage(message) + } + hasEmbed={!!embedUri} setEmbed={setEmbed} - /> - - ) : ( - - + + + ) : ( + - - ) - } - - )} - - + loading={loading}> + + + ) + } + + )} + + - {newMessagesPill.show && } + {newMessagesPill.show && ( + + )} + ) } diff --git a/src/screens/Messages/components/RequestListItem.tsx b/src/screens/Messages/components/RequestListItem.tsx index 64dbebb164..79eab480ae 100644 --- a/src/screens/Messages/components/RequestListItem.tsx +++ b/src/screens/Messages/components/RequestListItem.tsx @@ -28,6 +28,9 @@ export function RequestListItem({ const isDeletedAccount = !convo.primaryMember || convo.primaryMember.handle === 'missing.invalid' + const canAcceptRequest = + convo.kind === 'direct' || convo.details.lockStatus === 'unlocked' + return ( @@ -65,7 +68,9 @@ export function RequestListItem({ ]}> {convo.primaryMember && !isDeletedAccount ? ( <> - + {canAcceptRequest ? ( + + ) : null} { - if (vals[0]) onChange(vals[0] as DynamicAppIcon.IconName) + if (vals[0]) onChange(vals[0]) }}> {children} diff --git a/src/screens/VideoFeed/components/Scrubber.tsx b/src/screens/VideoFeed/components/Scrubber.tsx index 85bd4fddfd..acaf02453c 100644 --- a/src/screens/VideoFeed/components/Scrubber.tsx +++ b/src/screens/VideoFeed/components/Scrubber.tsx @@ -23,8 +23,7 @@ import { import {useEventListener} from 'expo' import {type VideoPlayer} from 'expo-video' -import {tokens} from '#/alf' -import {atoms as a} from '#/alf' +import {atoms as a, tokens} from '#/alf' import {formatTime} from '#/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/utils' import {Text} from '#/components/Typography' diff --git a/src/state/queries/notifications/feed.ts b/src/state/queries/notifications/feed.ts index 57d83cb5bb..ca885107d8 100644 --- a/src/state/queries/notifications/feed.ts +++ b/src/state/queries/notifications/feed.ts @@ -298,7 +298,7 @@ export function* findAllPostsInQueryData( if (AppBskyFeedDefs.isPostView(item.subject)) { const quotedPost = getEmbeddedPost(item.subject?.embed) if (quotedPost && didOrHandleUriMatches(atUri, quotedPost)) { - yield embedViewRecordToPostView(quotedPost!) + yield embedViewRecordToPostView(quotedPost) } } } diff --git a/src/state/queries/nuxs/util.ts b/src/state/queries/nuxs/util.ts index ef323b422e..dea35a9166 100644 --- a/src/state/queries/nuxs/util.ts +++ b/src/state/queries/nuxs/util.ts @@ -34,7 +34,7 @@ export function parseAppNux(nux: AppBskyActorDefs.Nux): AppNux | undefined { export function serializeAppNux(nux: AppNux): AppBskyActorDefs.Nux { const {data, ...rest} = nux - const schema = NuxSchemas[nux.id as Nux] + const schema = NuxSchemas[nux.id] const result: AppBskyActorDefs.Nux = { ...rest, diff --git a/src/state/queries/suggested-follows.ts b/src/state/queries/suggested-follows.ts index c1ec4a12a7..7197eb2980 100644 --- a/src/state/queries/suggested-follows.ts +++ b/src/state/queries/suggested-follows.ts @@ -85,7 +85,7 @@ export function useSuggestedFollowsByActorWithDismiss({ const profiles = useMemo(() => { return (data?.suggestions ?? []).map(profile => ({ - actor: profile as bsky.profile.AnyProfileView, + actor: profile, recId: data?.recId, })) }, [data?.suggestions, data?.recId]) diff --git a/src/state/queries/usePostThread/traversal.ts b/src/state/queries/usePostThread/traversal.ts index 889c208c35..81abd958fb 100644 --- a/src/state/queries/usePostThread/traversal.ts +++ b/src/state/queries/usePostThread/traversal.ts @@ -196,7 +196,7 @@ export function sortAndAnnotateThreadItems( * `repliesSeenCounter` later on, since `repliesSeenCounter` * is 1-indexed and `replyIndex` is 0-indexed. */ - childMetadata!.replyIndex = + childMetadata.replyIndex = childParentMetadata.repliesSeenCounter } diff --git a/src/types/bsky/post.ts b/src/types/bsky/post.ts index 4020c1632d..43621ef63b 100644 --- a/src/types/bsky/post.ts +++ b/src/types/bsky/post.ts @@ -1,6 +1,7 @@ import { type $Typed, AppBskyEmbedExternal, + AppBskyEmbedGallery, AppBskyEmbedImages, AppBskyEmbedRecord, AppBskyEmbedRecordWithMedia, @@ -10,8 +11,6 @@ import { AppBskyLabelerDefs, } from '@atproto/api' -import {AppBskyEmbedGallery} from '#/lib/api/gallery-embed-shim' - export type Embed = | { type: 'post' diff --git a/src/view/com/composer/ComposerReplyTo.tsx b/src/view/com/composer/ComposerReplyTo.tsx index 8a889d7f9c..131337f955 100644 --- a/src/view/com/composer/ComposerReplyTo.tsx +++ b/src/view/com/composer/ComposerReplyTo.tsx @@ -2,6 +2,7 @@ import {useCallback, useMemo, useState} from 'react' import {LayoutAnimation, Pressable, View} from 'react-native' import {Image} from 'expo-image' import { + AppBskyEmbedGallery, AppBskyEmbedImages, AppBskyEmbedRecord, AppBskyEmbedRecordWithMedia, @@ -10,7 +11,6 @@ import { import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' -import {AppBskyEmbedGallery} from '#/lib/api/gallery-embed-shim' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' import {type ComposerOptsPostRef} from '#/state/shell/composer' @@ -134,16 +134,19 @@ export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) { } function galleryItemsToImages( - items: AppBskyEmbedGallery.ViewImage[], + items: AppBskyEmbedGallery.View['items'], ): AppBskyEmbedImages.ViewImage[] { // The reply-to thumbnail only renders up to 4 tiles; slicing here keeps // the existing layout switch valid for galleries up to 10 items. - return items.slice(0, 4).map(item => ({ - thumb: item.thumbnail, - fullsize: item.fullsize, - alt: item.alt, - aspectRatio: item.aspectRatio, - })) + return items + .filter(AppBskyEmbedGallery.isViewImage) + .slice(0, 4) + .map(item => ({ + thumb: item.thumbnail, + fullsize: item.fullsize, + alt: item.alt, + aspectRatio: item.aspectRatio, + })) } function ComposerReplyToImages({ diff --git a/src/view/com/composer/drafts/state/api.ts b/src/view/com/composer/drafts/state/api.ts index cca5f2b855..75475fc142 100644 --- a/src/view/com/composer/drafts/state/api.ts +++ b/src/view/com/composer/drafts/state/api.ts @@ -1,16 +1,9 @@ /** * Type converters for Draft API - convert between ComposerState and server Draft types. */ -import {type AppBskyDraftDefs, AtUri, RichText} from '@atproto/api' +import {AppBskyDraftDefs, AtUri, RichText} from '@atproto/api' import {nanoid} from 'nanoid/non-secure' -// Shim: AppBskyDraftDefs.DraftPost gains an `embedGallery` field in atproto -// PR #4827. Until @atproto/api ships those types, we widen the shape locally. -// Delete this once the lexicon publishes. -type DraftPostWithGallery = AppBskyDraftDefs.DraftPost & { - embedGallery?: AppBskyDraftDefs.DraftEmbedImage[] -} - import {resolveLink} from '#/lib/api/resolve' import {getDeviceName} from '#/lib/deviceName' import {getImageDim} from '#/lib/media/manip' @@ -123,10 +116,15 @@ async function postDraftToServerPost( localRefPaths, ) } else if (post.embed.media.type === 'gallery') { - ;(draftPost as DraftPostWithGallery).embedGallery = serializeImages( - post.embed.media.images, - localRefPaths, - ) + draftPost.embedGallery = { + $type: 'app.bsky.draft.defs#draftEmbedGallery', + items: serializeImages(post.embed.media.images, localRefPaths).map( + img => ({ + $type: 'app.bsky.draft.defs#draftEmbedImage' as const, + ...img, + }), + ), + } } else if (post.embed.media.type === 'video') { const video = await serializeVideo(post.embed.media.video, localRefPaths) if (video) { @@ -326,11 +324,11 @@ async function restoreDraftImages( height, mime: 'image/jpeg', }, - } as ComposerImage + } satisfies ComposerImage }) return (await Promise.all(imagePromises)).filter( - (img): img is ComposerImage => img !== null, + (img): img is NonNullable => img !== null, ) } @@ -380,18 +378,18 @@ export function draftViewToSummary({ } // Process gallery - const summaryEmbedGallery = (post as DraftPostWithGallery).embedGallery - if (summaryEmbedGallery) { - for (const img of summaryEmbedGallery) { + if (post.embedGallery) { + for (const item of post.embedGallery.items) { + if (!AppBskyDraftDefs.isDraftEmbedImage(item)) continue meta.mediaCount++ meta.hasMedia = true - const exists = storage.mediaExists(img.localRef.path) + const exists = storage.mediaExists(item.localRef.path) if (!exists) { meta.hasMissingMedia = true } images.push({ - localPath: img.localRef.path, - altText: img.alt || '', + localPath: item.localRef.path, + altText: item.alt || '', exists, }) } @@ -523,9 +521,11 @@ export async function draftToComposerPosts( } // Restore gallery - const embedGallery = (post as DraftPostWithGallery).embedGallery - if (embedGallery && embedGallery.length > 0) { - const images = await restoreDraftImages(embedGallery, loadedMedia) + if (post.embedGallery && post.embedGallery.items.length > 0) { + const galleryImages = post.embedGallery.items.filter( + AppBskyDraftDefs.isDraftEmbedImage, + ) + const images = await restoreDraftImages(galleryImages, loadedMedia) if (images.length > 0) { embed.media = {type: 'gallery', images} } @@ -561,7 +561,7 @@ export async function draftToComposerPosts( tinygif: mediaObject, preview: mediaObject, }, - } as Gif, + }, alt: gifData.alt, } break @@ -680,10 +680,10 @@ export function extractLocalRefs(draft: AppBskyDraftDefs.Draft): Set { refs.add(img.localRef.path) } } - const embedGallery = (post as DraftPostWithGallery).embedGallery - if (embedGallery) { - for (const img of embedGallery) { - refs.add(img.localRef.path) + if (post.embedGallery) { + for (const item of post.embedGallery.items) { + if (!AppBskyDraftDefs.isDraftEmbedImage(item)) continue + refs.add(item.localRef.path) } } if (post.embedVideos) { diff --git a/src/view/com/composer/drafts/state/queries.ts b/src/view/com/composer/drafts/state/queries.ts index c0d896da61..07dd67dd88 100644 --- a/src/view/com/composer/drafts/state/queries.ts +++ b/src/view/com/composer/drafts/state/queries.ts @@ -1,10 +1,4 @@ -import {AppBskyDraftCreateDraft, type AppBskyDraftDefs} from '@atproto/api' - -// Shim: AppBskyDraftDefs.DraftPost gains `embedGallery` in atproto PR #4827. -// Delete once @atproto/api publishes the new lexicon. -type DraftPostWithGallery = AppBskyDraftDefs.DraftPost & { - embedGallery?: AppBskyDraftDefs.DraftEmbedImage[] -} +import {AppBskyDraftCreateDraft, AppBskyDraftDefs} from '@atproto/api' import { useInfiniteQuery, useMutation, @@ -81,15 +75,15 @@ export async function loadDraftMedia(draft: AppBskyDraftDefs.Draft): Promise<{ } } // Load gallery - const embedGallery = (post as DraftPostWithGallery).embedGallery - if (embedGallery) { - for (const img of embedGallery) { + if (post.embedGallery) { + for (const item of post.embedGallery.items) { + if (!AppBskyDraftDefs.isDraftEmbedImage(item)) continue try { - const url = await storage.loadMediaFromLocal(img.localRef.path) - loadedMedia.set(img.localRef.path, url) + const url = await storage.loadMediaFromLocal(item.localRef.path) + loadedMedia.set(item.localRef.path, url) } catch (e) { logger.error('Failed to load draft gallery image', { - path: img.localRef.path, + path: item.localRef.path, safeMessage: e instanceof Error ? e.message : String(e), }) } @@ -247,10 +241,10 @@ export function useDeleteDraftMutation() { await storage.deleteMediaFromLocal(img.localRef.path) } } - const embedGallery = (post as DraftPostWithGallery).embedGallery - if (embedGallery) { - for (const img of embedGallery) { - await storage.deleteMediaFromLocal(img.localRef.path) + if (post.embedGallery) { + for (const item of post.embedGallery.items) { + if (!AppBskyDraftDefs.isDraftEmbedImage(item)) continue + await storage.deleteMediaFromLocal(item.localRef.path) } } if (post.embedVideos) { diff --git a/src/view/com/testing/TestCtrls.e2e.tsx b/src/view/com/testing/TestCtrls.e2e.tsx index b01651d7b1..2110c9540e 100644 --- a/src/view/com/testing/TestCtrls.e2e.tsx +++ b/src/view/com/testing/TestCtrls.e2e.tsx @@ -55,7 +55,7 @@ export function TestCtrls() { accessibilityLabel="Text input field" accessibilityHint="Enter proxy header" testID="e2eProxyHeaderInput" - onChangeText={val => setProxyHeader(val as any)} + onChangeText={val => setProxyHeader(val)} autoComplete="off" autoCorrect={false} autoCapitalize="none" diff --git a/src/view/com/util/PressableWithHover.tsx b/src/view/com/util/PressableWithHover.tsx index f4006a5268..a4a2d194fd 100644 --- a/src/view/com/util/PressableWithHover.tsx +++ b/src/view/com/util/PressableWithHover.tsx @@ -3,9 +3,9 @@ import { Pressable, type PressableProps, type StyleProp, + type View, type ViewStyle, } from 'react-native' -import {type View} from 'react-native' import {addStyle} from '#/lib/styles' import {useInteractionState} from '#/components/hooks/useInteractionState' diff --git a/src/view/shell/BlockDrawerGesture.tsx b/src/view/shell/BlockDrawerGesture.tsx index bae9a8db82..93ef18cfae 100644 --- a/src/view/shell/BlockDrawerGesture.tsx +++ b/src/view/shell/BlockDrawerGesture.tsx @@ -4,6 +4,8 @@ import {Gesture, GestureDetector} from 'react-native-gesture-handler' export function BlockDrawerGesture({children}: {children: React.ReactNode}) { const drawerGesture = useContext(DrawerGestureContext) ?? Gesture.Native() // noop for web - const scrollGesture = Gesture.Native().blocksExternalGesture(drawerGesture) + let scrollGesture = Gesture.Native() + .shouldCancelWhenOutside(false) // for some reason defaults to true on Android + .blocksExternalGesture(drawerGesture) return {children} } diff --git a/src/view/shell/createNativeStackNavigatorWithAuth.tsx b/src/view/shell/createNativeStackNavigatorWithAuth.tsx index 4191f94176..a23ed693ec 100644 --- a/src/view/shell/createNativeStackNavigatorWithAuth.tsx +++ b/src/view/shell/createNativeStackNavigatorWithAuth.tsx @@ -171,7 +171,7 @@ function NativeStackNavigator({ } // Evicted screens get a lightweight placeholder instead of their full tree - finalDescriptors = {} as typeof descriptors + finalDescriptors = {} for (const key in descriptors) { if (mountSet.has(key)) { finalDescriptors[key] = descriptors[key] diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx index 827ab092c4..9b215d9f13 100644 --- a/src/view/shell/index.tsx +++ b/src/view/shell/index.tsx @@ -174,8 +174,15 @@ function DrawerLayout({children}: {children: React.ReactNode}) { // so fail the drawer gesture immediately. .failOffsetX(-1) // Don't rush declaring that a movement to the right - // is a drawer swipe. It could be a vertical scroll. - .activeOffsetX(5) + // is a drawer swipe. It could be a vertical scroll, or a + // slow horizontal carousel swipe. On Android a child + // `blocksExternalGesture` only holds the drawer off once the + // native scroll has activated, which on a slow swipe doesn't + // happen until movement crosses the native touch slop + // (~8-16px). Activating the drawer below that lets a slow + // carousel swipe pop the drawer open (APP-2119), so require + // more travel before claiming on Android. + .activeOffsetX(IS_ANDROID ? 20 : 5) ) } } else {