chore: Release v0.0.1-alpha.19 (#758)
This commit is contained in:
commit
62adeb7481
|
|
@ -4,3 +4,4 @@ VITE_IMGPROXY_URL=http://localhost:2873
|
|||
VITE_SENTRY_DSN=
|
||||
VITE_BUILD_TYPE=production
|
||||
VITE_POSTHOG_KEY=
|
||||
VITE_INBOXES_EMAIL=@follow.re
|
||||
|
|
|
|||
|
|
@ -3,3 +3,6 @@ contact_links:
|
|||
- name: 💬 Follow's Discord Server
|
||||
url: https://discord.gg/tUDVZjEr
|
||||
about: Want to discuss / chat with the community? Here you go!
|
||||
- name: Discuss an issue
|
||||
url: https://github.com/RSSNext/Follow/discussions
|
||||
about: For general questions, ideas, or non-bug related discussions, please use GitHub Discussions.
|
||||
|
|
|
|||
|
|
@ -15,10 +15,6 @@ jobs:
|
|||
ref: ${{ github.head_ref }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
|
||||
- name: AutoCorrect
|
||||
if: runner.os == 'Linux'
|
||||
uses: huacnlee/autocorrect-action@main
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
|
|
|
|||
|
|
@ -51,10 +51,6 @@ jobs:
|
|||
fetch-depth: 1
|
||||
lfs: true
|
||||
|
||||
- name: AutoCorrect
|
||||
if: runner.os == 'Linux'
|
||||
uses: huacnlee/autocorrect-action@main
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
|
|
|
|||
|
|
@ -24,10 +24,6 @@ jobs:
|
|||
- name: Checkout LFS objects
|
||||
run: git lfs checkout
|
||||
|
||||
- name: AutoCorrect
|
||||
if: runner.os == 'Linux'
|
||||
uses: huacnlee/autocorrect-action@main
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
|
|
|
|||
|
|
@ -13,22 +13,7 @@ env:
|
|||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
|
||||
jobs:
|
||||
check-for-changes:
|
||||
runs-on: ubuntu-latest
|
||||
# outputs:
|
||||
# should_run: ${{ steps.check.outputs.should_run }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
# - name: Check for code changes
|
||||
# id: check
|
||||
# run: |
|
||||
# git diff --quiet @{1.day.ago} HEAD -- ':!.github' || echo "should_run=true" >> $GITHUB_OUTPUT
|
||||
|
||||
nightly-release:
|
||||
# needs: check-for-changes
|
||||
# if: needs.check-for-changes.outputs.should_run == 'true'
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
|
|
@ -107,25 +92,20 @@ jobs:
|
|||
shell: bash
|
||||
run: |
|
||||
# Get the current version from package.json
|
||||
if [ -f package.json ]; then
|
||||
CURRENT_VERSION=$(node -p "require('./package.json').version")
|
||||
# Remove any existing prerelease identifier (e.g., -alpha.1)
|
||||
BASE_VERSION=$(echo $CURRENT_VERSION | sed -E 's/(-[a-zA-Z]+\.[0-9]+)$//')
|
||||
# Generate the nightly version
|
||||
NIGHTLY_DATE=$(date +'%Y%m%d')
|
||||
NIGHTLY_VERSION="${BASE_VERSION}-nightly.${NIGHTLY_DATE}"
|
||||
echo "NIGHTLY_VERSION=$NIGHTLY_VERSION" >> $GITHUB_ENV
|
||||
# Update version in package.json
|
||||
if [[ "$RUNNER_OS" == "Windows" ]]; then
|
||||
sed -i "s/\"version\": \".*\"/\"version\": \"$NIGHTLY_VERSION\"/" package.json
|
||||
else
|
||||
sed -i '' "s/\"version\": \".*\"/\"version\": \"$NIGHTLY_VERSION\"/" package.json
|
||||
fi
|
||||
echo "Updated version to $NIGHTLY_VERSION"
|
||||
CURRENT_VERSION=$(node -p "require('./package.json').version")
|
||||
# Remove any existing prerelease identifier (e.g., -alpha.1)
|
||||
BASE_VERSION=$(echo $CURRENT_VERSION | sed -E 's/(-[a-zA-Z]+\.[0-9]+)$//')
|
||||
# Generate the nightly version
|
||||
NIGHTLY_DATE=$(date +'%Y%m%d')
|
||||
NIGHTLY_VERSION="${BASE_VERSION}-nightly.${NIGHTLY_DATE}"
|
||||
echo "NIGHTLY_VERSION=$NIGHTLY_VERSION" >> $GITHUB_ENV
|
||||
# Update version in package.json
|
||||
if [[ "$RUNNER_OS" == "Windows" ]]; then
|
||||
sed -i "s/\"version\": \".*\"/\"version\": \"$NIGHTLY_VERSION\"/" package.json
|
||||
else
|
||||
echo "package.json not found"
|
||||
exit 1
|
||||
sed -i '' "s/\"version\": \".*\"/\"version\": \"$NIGHTLY_VERSION\"/" package.json
|
||||
fi
|
||||
echo "Updated version to $NIGHTLY_VERSION"
|
||||
|
||||
- name: Build
|
||||
if: matrix.os != 'macos-latest'
|
||||
|
|
@ -171,6 +151,7 @@ jobs:
|
|||
name: Nightly ${{ env.NIGHTLY_VERSION }}
|
||||
draft: false
|
||||
prerelease: true
|
||||
tag_name: nightly-${{ env.NIGHTLY_VERSION }}
|
||||
files: |
|
||||
out/make/**/*.dmg
|
||||
out/make/**/*.zip
|
||||
|
|
@ -179,7 +160,7 @@ jobs:
|
|||
out/make/**/*.yml
|
||||
body: |
|
||||
This is an automated nightly release for testing purposes.
|
||||
Version: ${{ env.NIGHTLY_VERSION }}
|
||||
Version: 0.0.0-nightly.${{ env.NIGHTLY_VERSION }}
|
||||
|
||||
**Warning:** This build may be unstable and is not recommended for production use.
|
||||
env:
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@
|
|||
"severity": "off"
|
||||
}
|
||||
],
|
||||
"cSpell.words": ["rsshub", "Русский"],
|
||||
"cSpell.words": ["Hydable", "rsshub", "Русский"],
|
||||
"editor.foldingImportsByDefault": true,
|
||||
"commentTranslate.hover.enabled": false,
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
|
|
|
|||
70
CHANGELOG.md
70
CHANGELOG.md
|
|
@ -1,6 +1,6 @@
|
|||
# CHANGELOG
|
||||
|
||||
## [0.0.1-alpha.18](https://github.com/RSSNext/follow/compare/v0.0.1-alpha.3...v0.0.1-alpha.18) (2024-09-27)
|
||||
## [0.0.1-alpha.19](https://github.com/RSSNext/follow/compare/v0.0.1-alpha.3...v0.0.1-alpha.19) (2024-10-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
|
@ -20,10 +20,12 @@
|
|||
* add download app fab ([0f8309b](https://github.com/RSSNext/follow/commit/0f8309b2031d53a17eb74ed36f2a0f504c54c431))
|
||||
* add en lang fixed[#641](https://github.com/RSSNext/follow/issues/641) ([23d6988](https://github.com/RSSNext/follow/commit/23d6988a63ce6ecdce562fb632fe85823082d38b))
|
||||
* add environment in error report issue template ([232c24d](https://github.com/RSSNext/follow/commit/232c24de3dbc135f4064c17680fbe6e429c92697))
|
||||
* add feed from url, close [#686](https://github.com/RSSNext/follow/issues/686) ([fd30789](https://github.com/RSSNext/follow/commit/fd30789d86a6980aa4b1c4ea45c81c7b7deb3e15))
|
||||
* add feed should validate feed id first ([767710d](https://github.com/RSSNext/follow/commit/767710deda41af1842179ef03f9b6b1b79208f9e))
|
||||
* add feed temp store use nonce ([6e36232](https://github.com/RSSNext/follow/commit/6e36232b41f1b6d877caf74e1ad7be50da716381))
|
||||
* add feed when site url not match ([1a2190c](https://github.com/RSSNext/follow/commit/1a2190c1aa834a33932788e65269a1128fd3a7e2))
|
||||
* add hover bg color when context menu opened ([6ceb263](https://github.com/RSSNext/follow/commit/6ceb263b777ccc9681cd086a28e019ce9bda3a43))
|
||||
* add i18n in preview-media ([#675](https://github.com/RSSNext/follow/issues/675)) ([935c87d](https://github.com/RSSNext/follow/commit/935c87de285ecabb44a92bc3536135e3c2c0e14f))
|
||||
* add language selector loading lock map ([2d14c1e](https://github.com/RSSNext/follow/commit/2d14c1e5b5ad086b3556dd9a8862a6ce00368c4d))
|
||||
* add link ([d9168c0](https://github.com/RSSNext/follow/commit/d9168c01b3f2a16fbfeadb722173e819dd3feb80))
|
||||
* add lock when login button click ([539f3bc](https://github.com/RSSNext/follow/commit/539f3bc599f4df9784662ca2bf972a94edf556c4))
|
||||
|
|
@ -32,6 +34,7 @@
|
|||
* add nonce id for temp feed ([8f81d99](https://github.com/RSSNext/follow/commit/8f81d99f1dd48e60f9861d69c840c77fa70bde93))
|
||||
* add page error boundary ([86d366d](https://github.com/RSSNext/follow/commit/86d366db76add0ad07eb921290b9444b211e2c4f))
|
||||
* add rounded class to video fallback message ([#665](https://github.com/RSSNext/follow/issues/665)) ([86d7b8c](https://github.com/RSSNext/follow/commit/86d7b8cc1293d4ec7cca79155a50a5f7ea01dd75))
|
||||
* add safe inset-top for Windows, fixed [#576](https://github.com/RSSNext/follow/issues/576) ([3352e8c](https://github.com/RSSNext/follow/commit/3352e8c292e810bb9a333e2a3c5e23adbae2a57a))
|
||||
* add show readability tip condition ([208e404](https://github.com/RSSNext/follow/commit/208e4040cea5d3485acf5750e7120fb5f715e30f))
|
||||
* add some polyfills for old browser, fixed [#236](https://github.com/RSSNext/follow/issues/236) ([b1fbfbd](https://github.com/RSSNext/follow/commit/b1fbfbd7e38a6a10430f7ed2d4b0644c2d5bbf22))
|
||||
* add tooltip for resize panel ([cdec351](https://github.com/RSSNext/follow/commit/cdec3510352cc7281e75ef1957e32bc6e712cf04))
|
||||
|
|
@ -77,6 +80,7 @@
|
|||
* ci and tootip portal ([3729917](https://github.com/RSSNext/follow/commit/37299173c8fcaf88d44b1a54baf7ddc03aa20ca6))
|
||||
* ci env `NODE_OPTIONS` max-old-space-size ([b4f9b1b](https://github.com/RSSNext/follow/commit/b4f9b1b8ea326b1613ca291ed5bbcd932c5e837a))
|
||||
* **ci:** fetch all depth ([a504a12](https://github.com/RSSNext/follow/commit/a504a127bca93a6675b3ff02bcea0b1eca6e9707))
|
||||
* **ci:** nightly build ([a4ce7b1](https://github.com/RSSNext/follow/commit/a4ce7b16f476b8d72a0b1761709ec1ceba591242))
|
||||
* **ci:** remove check diff ([32f3a9e](https://github.com/RSSNext/follow/commit/32f3a9edba28ec2368730c6038ff6d3f8d55b404))
|
||||
* clean local async data ([ae26dd7](https://github.com/RSSNext/follow/commit/ae26dd7763fe32990086e76fa896ec09dec170cd))
|
||||
* cls when star in gird item ([#636](https://github.com/RSSNext/follow/issues/636)) ([b993ae0](https://github.com/RSSNext/follow/commit/b993ae050e282c22e40f649141677f7905ede601))
|
||||
|
|
@ -103,19 +107,24 @@
|
|||
* discover form overflow scrollbar ([1145e92](https://github.com/RSSNext/follow/commit/1145e92ae7125873d69f2689d86418b004d93319))
|
||||
* discover form should preview twice when has optional value ([f0f8185](https://github.com/RSSNext/follow/commit/f0f8185d92e447943c14a4ceeeceb7211498bc28))
|
||||
* discover page title i18n ([b92d317](https://github.com/RSSNext/follow/commit/b92d317e9c1dbdd674a6b122a02f3f6a3826682f))
|
||||
* discover recommendation card link button style ([1ac465c](https://github.com/RSSNext/follow/commit/1ac465cbb96bda4a189dea086caf3bbe77aa64ee))
|
||||
* **discover:** update follow status after add feed, closes [#269](https://github.com/RSSNext/follow/issues/269), closes ([32a55ec](https://github.com/RSSNext/follow/commit/32a55ece6440f80f837adc6d558029ea5fbd6982))
|
||||
* display white block ([#633](https://github.com/RSSNext/follow/issues/633)) ([d9bb749](https://github.com/RSSNext/follow/commit/d9bb74937b5b2f528707e80fc2a76a89df10a1a8))
|
||||
* don't auto focus in user profile modal ([f1ce5df](https://github.com/RSSNext/follow/commit/f1ce5df581c0a91967d3d093b6735e2fdd3ea906))
|
||||
* don't retry when 404 ([8bd9bd0](https://github.com/RSSNext/follow/commit/8bd9bd0a8bf92764d8dd13a0728e3ab821b3484b))
|
||||
* dont handle unread when filter applied ([acfb35a](https://github.com/RSSNext/follow/commit/acfb35abd0edb85989549849d0176c4a65e5f9fa))
|
||||
* downgarde vitest ([dee089e](https://github.com/RSSNext/follow/commit/dee089e1c75bf9031663b682aff9cee33b60db76))
|
||||
* downgrade vitest ([2f15900](https://github.com/RSSNext/follow/commit/2f15900b087f1a0a511f82e559eae617dc2faf32))
|
||||
* draggable panel dragging bg color ([8de7078](https://github.com/RSSNext/follow/commit/8de7078d10b733456c480117ba8ed8d2166e20b9))
|
||||
* drawer edge shadow style ([cbb8649](https://github.com/RSSNext/follow/commit/cbb8649fde99e7a800153ad35aa041aaf44e6dcf))
|
||||
* drawer top edge anchor ([248369a](https://github.com/RSSNext/follow/commit/248369afe6d085885fd76a4ecf6a3f9b692b14cf))
|
||||
* duplicated separator ([6e1ee50](https://github.com/RSSNext/follow/commit/6e1ee50536ded4b65f76cdb35bdf98c4657a1a74))
|
||||
* dynamic load i18n resource in electron prod ([2328648](https://github.com/RSSNext/follow/commit/232864852a934d571a2b47af7f4d02c787cc8250))
|
||||
* eagle icon ([021dfab](https://github.com/RSSNext/follow/commit/021dfab5eeb888f155982cc1cd3636b2b4ca52a3))
|
||||
* electron context menu i18n ([1a1d8ab](https://github.com/RSSNext/follow/commit/1a1d8ab5c1ff4917d3b3368059eff0b4cdf57520))
|
||||
* electron exit webview fullscreen sidebar color ([bc7914f](https://github.com/RSSNext/follow/commit/bc7914f803939897d817080972e5307cf58f8caf))
|
||||
* electron external window size option ([399d5d7](https://github.com/RSSNext/follow/commit/399d5d7a2cda5c7a7f9ce91ae9b9125f9ece7c54))
|
||||
* electron menu support checkbox ([7881f23](https://github.com/RSSNext/follow/commit/7881f236ad7f69725a25daa01deeb81aa9dc76ab))
|
||||
* electron search ([63bea18](https://github.com/RSSNext/follow/commit/63bea180f545e2f64cdc7e379f807333fee0ded6))
|
||||
* electron title bar drag-region ([29ab323](https://github.com/RSSNext/follow/commit/29ab32344fafda613b0c9d49b587786eaa40b00a))
|
||||
* electron window frame and `backgroundMaterial` ([e18ec75](https://github.com/RSSNext/follow/commit/e18ec754a05231fb82a7cbdb80632d79a6232872))
|
||||
|
|
@ -127,10 +136,13 @@
|
|||
* entries hasNext ([4cb5678](https://github.com/RSSNext/follow/commit/4cb56784acfb46516a4320f0088c294eafb32fcf))
|
||||
* entry bar action ([ba23dcf](https://github.com/RSSNext/follow/commit/ba23dcf46ff7569562d0a786ab85d46093b8abe8))
|
||||
* entry column padding top and filter unread/read in tweet view ([d181771](https://github.com/RSSNext/follow/commit/d181771a7bf314cdb30f2fdd76294c5cded524ab))
|
||||
* entry column title line height ([b615c17](https://github.com/RSSNext/follow/commit/b615c171d551f27932a7a42a814388bd8e07cf84))
|
||||
* entry column title top overflow ([4478034](https://github.com/RSSNext/follow/commit/447803488c1ed01769c134f3113b0a3926e07446))
|
||||
* entry item overlay flash ([687e039](https://github.com/RSSNext/follow/commit/687e039ca2ad43eed2b4b48a29df96595dcdbc0f))
|
||||
* entry preview modal content ([8492c2a](https://github.com/RSSNext/follow/commit/8492c2afb3af79726b9448359a63b432b04a3518))
|
||||
* entry read history more not showing on desktop ([0c6494b](https://github.com/RSSNext/follow/commit/0c6494b5682078fa1da1ea2e6f5d81aa436a5155))
|
||||
* entry view tracker params ([0c05a8b](https://github.com/RSSNext/follow/commit/0c05a8b88c1c385b7809f1ea9e3355f0d11e69dd))
|
||||
* env example ([cdb8dd5](https://github.com/RSSNext/follow/commit/cdb8dd5cf31ec2d43b18a50cdd3259302620867e))
|
||||
* **error:** filter user is empty ([83b4031](https://github.com/RSSNext/follow/commit/83b4031405d2a6e979a32fbb18095f22d19d6a70))
|
||||
* **eslint:** json sort key ([5a81dd6](https://github.com/RSSNext/follow/commit/5a81dd6786b7c0e4a533f3cb4e50d7900d005875))
|
||||
* exit full screen before hiding window ([#341](https://github.com/RSSNext/follow/issues/341)) ([bd5b08f](https://github.com/RSSNext/follow/commit/bd5b08f314ddaddb9b079b3d10b6928d28962df5))
|
||||
|
|
@ -151,6 +163,7 @@
|
|||
* feed list can switch view, fix [#278](https://github.com/RSSNext/follow/issues/278) ([76a33b6](https://github.com/RSSNext/follow/commit/76a33b6f905bac61f6390b01b13c6a43b94f535f))
|
||||
* feed list overflow ([4941cf7](https://github.com/RSSNext/follow/commit/4941cf7b93a751f585ab5032974fe6eb191f7968))
|
||||
* **feed-icon:** re-render loop ([b6f78bb](https://github.com/RSSNext/follow/commit/b6f78bbbf89b804fc4a0ad89b524f0a24673c19a))
|
||||
* fetch inbox entry ([ecb2d33](https://github.com/RSSNext/follow/commit/ecb2d3397993ba019019cbf82c6e969dcf7851ed))
|
||||
* fill relative url in entry title link, fix [#372](https://github.com/RSSNext/follow/issues/372) ([7c3aab6](https://github.com/RSSNext/follow/commit/7c3aab6f042a710aaa99d8fb7a774fce89af2b08))
|
||||
* fix the fixed misalignment when image is empty and name is also empty ([#179](https://github.com/RSSNext/follow/issues/179)) ([f6b62c8](https://github.com/RSSNext/follow/commit/f6b62c8ce0908772c394d653857197c9302e332b))
|
||||
* fixed text overlap issues caused by style ([#527](https://github.com/RSSNext/follow/issues/527)) ([103bd6a](https://github.com/RSSNext/follow/commit/103bd6a414beb9a808a9ad661348ff416626bc18))
|
||||
|
|
@ -162,6 +175,7 @@
|
|||
* generate-i18n scripti ([fd361e8](https://github.com/RSSNext/follow/commit/fd361e88864c57c6f76793519a2e57a650aa4a2b))
|
||||
* gird item text and icon align center ([eabd59c](https://github.com/RSSNext/follow/commit/eabd59cf1feb774759279832005fcd0e26aa7bb6))
|
||||
* gird mode skeleton ([0a7f648](https://github.com/RSSNext/follow/commit/0a7f6488c176ee059f7c87f3c51e0234c85c1d4c))
|
||||
* grid mode for images and video content page, shadow not fully displayed when the mouse hovers over ([#727](https://github.com/RSSNext/follow/issues/727)) ([361b09e](https://github.com/RSSNext/follow/commit/361b09eb23c86f377cc82d7e523781a7d4507689))
|
||||
* group header extra action button for wider mode ([213b4a5](https://github.com/RSSNext/follow/commit/213b4a5864c78716648b2319f5d6f0dfcb531e3a))
|
||||
* grouped list data item render ([da486bc](https://github.com/RSSNext/follow/commit/da486bce3eeafc84aeb3e560ff368c41771688cf))
|
||||
* handle empty title in entry translation ([#362](https://github.com/RSSNext/follow/issues/362)) ([a2457a7](https://github.com/RSSNext/follow/commit/a2457a7789aff5928714cbe55e3e7863bb51719b))
|
||||
|
|
@ -174,7 +188,14 @@
|
|||
* hono.ts ([fd03caa](https://github.com/RSSNext/follow/commit/fd03caac22f9e035734d369351134ed63da93439))
|
||||
* i18n dispatcher ([c038c79](https://github.com/RSSNext/follow/commit/c038c791af14418ee641af08e689a619b99a1735))
|
||||
* i18n persist ([18e0353](https://github.com/RSSNext/follow/commit/18e035394b8d1a77f8454f47e0049359e69d750a))
|
||||
* **i18n-selector:** hover cls ([60f1af2](https://github.com/RSSNext/follow/commit/60f1af2409300e64ad351621abf71ed326188af8))
|
||||
* **i18n/en:** enhance English grammar ([#698](https://github.com/RSSNext/follow/issues/698)) ([4478517](https://github.com/RSSNext/follow/commit/4478517566aced3e55928229433a4cc83dd0c3ae))
|
||||
* **i18n/zh:** improve translation text in `wallet.transaction` table ([#706](https://github.com/RSSNext/follow/issues/706)) ([67e305c](https://github.com/RSSNext/follow/commit/67e305ccc6b33ca6e81ce68af9f9522219f9a067))
|
||||
* **i18n:** add de to langs ([#683](https://github.com/RSSNext/follow/issues/683)) ([018b5b1](https://github.com/RSSNext/follow/commit/018b5b1b1b3ced06543189512f5bb2df39f946af))
|
||||
* **i18n:** add native french support ([#670](https://github.com/RSSNext/follow/issues/670)) ([b214c0d](https://github.com/RSSNext/follow/commit/b214c0db01edafced973ed9075ff6be3959c5f61))
|
||||
* **i18n:** complete the Chinese (Taiwan) and Chinese (Hong Kong) json files. ([#720](https://github.com/RSSNext/follow/issues/720)) ([ad5adc3](https://github.com/RSSNext/follow/commit/ad5adc358933370bdff9b17f732d7a0d924fe10d))
|
||||
* **i18n:** dup key ([d90964e](https://github.com/RSSNext/follow/commit/d90964ea9e32d5d7b162d6d7525df213ac732e12))
|
||||
* **i18n:** duplicated keys ([234cdd3](https://github.com/RSSNext/follow/commit/234cdd3c71bb2e1fb0548734f31b10093372bc90))
|
||||
* **i18n:** fix json nested key fixes [#605](https://github.com/RSSNext/follow/issues/605) ([34b9a20](https://github.com/RSSNext/follow/commit/34b9a20acf41f62c2710699cc6190fdc95c7d749))
|
||||
* **i18n:** give star ([#502](https://github.com/RSSNext/follow/issues/502)) ([9ead475](https://github.com/RSSNext/follow/commit/9ead4754d31fee3a1c14a3ef9210a621c2dd039f))
|
||||
* **i18n:** hk language resource ([43c89e0](https://github.com/RSSNext/follow/commit/43c89e007d50620fbf298bbf571db0c3cf43e028))
|
||||
|
|
@ -183,6 +204,7 @@
|
|||
* **i18n:** relative time add `ago` postfix ([9fe48c5](https://github.com/RSSNext/follow/commit/9fe48c57133e0360a74aa46c06f01393cc044136))
|
||||
* icon button transition ([596538b](https://github.com/RSSNext/follow/commit/596538ba10b6b70d12ccc4efdd9f206d9ba11817))
|
||||
* icon fallback line height ([02cd98d](https://github.com/RSSNext/follow/commit/02cd98d9cc9c09006a91277a5b976d753054738d))
|
||||
* image blurhash `aspectRatio` ([9239483](https://github.com/RSSNext/follow/commit/9239483081e6e4adfda4934695c03df394e873fa))
|
||||
* image url replacement ([8e75a8c](https://github.com/RSSNext/follow/commit/8e75a8cc6931b354e3206ea22917842c991d4c45))
|
||||
* **image:** error fallback ([18fdaa2](https://github.com/RSSNext/follow/commit/18fdaa20610fba875054c3d298ecb07e099ad584))
|
||||
* import circular and copywrite i18n ([725f9f5](https://github.com/RSSNext/follow/commit/725f9f5509c3b34079923fad47096b108568ccd6))
|
||||
|
|
@ -212,6 +234,7 @@
|
|||
* loadFile options hash ([b41fa66](https://github.com/RSSNext/follow/commit/b41fa665fa334f0037de294d8e0585a5cf94c649))
|
||||
* loading circle clip path ([ba396be](https://github.com/RSSNext/follow/commit/ba396beaa78eae3c1b250fd846cc8434c2abc07f))
|
||||
* loading style ([61ee2cc](https://github.com/RSSNext/follow/commit/61ee2ccb3e30d3ab82de0bfe6aca35df38794dca))
|
||||
* lockfile ([8e571e7](https://github.com/RSSNext/follow/commit/8e571e7fa77fdfc5c0e992bda53cea1c5eb0fb76))
|
||||
* login button transition ([0f566e7](https://github.com/RSSNext/follow/commit/0f566e757bdc15fc72c4cd3681384d7b4242b55a))
|
||||
* login page style ([094a668](https://github.com/RSSNext/follow/commit/094a6684b62de69748f20011f5bd0f68833c30e1))
|
||||
* **login-button:** add overflow ([650aeeb](https://github.com/RSSNext/follow/commit/650aeeb099a18341a6fd25b006e77818f040fa93))
|
||||
|
|
@ -221,6 +244,8 @@
|
|||
* markdown link populate relative link ([bad5869](https://github.com/RSSNext/follow/commit/bad5869f3f974b96fa5513b99574f60fb8aba963))
|
||||
* masonry layout cls ([9428368](https://github.com/RSSNext/follow/commit/9428368cfc4a00afb1c72f9ca3a4267b311c5b2b))
|
||||
* **masonry:** in view mark read and scroll out mark read ([1c5a1ef](https://github.com/RSSNext/follow/commit/1c5a1ef9a58d7953e0966d2fe8d8fe4543463b84))
|
||||
* media fallback src overflow ([3dd705f](https://github.com/RSSNext/follow/commit/3dd705f3a427c53d000c4934513c97d6132399d0))
|
||||
* media preview image fixed size ([14c7028](https://github.com/RSSNext/follow/commit/14c7028347b22eb1cf768d5c3a4c20ffe9497b93))
|
||||
* **media-preview:** solve image cls ([e0bcb71](https://github.com/RSSNext/follow/commit/e0bcb718aee57e7eeef31c3ffcdf5564c823c845))
|
||||
* **media:** if no src then return null ([183009a](https://github.com/RSSNext/follow/commit/183009adee6d5674923a72ae0f5a10b85d4c7ba6))
|
||||
* missing feed caused by parsing domain error ([1385876](https://github.com/RSSNext/follow/commit/13858762b4fed592ab7e5040ae6a59a9f3872747))
|
||||
|
|
@ -228,6 +253,8 @@
|
|||
* modal max height, fixed [#230](https://github.com/RSSNext/follow/issues/230) ([fbf4727](https://github.com/RSSNext/follow/commit/fbf4727608ada80ba0f42349f2dace0543e22f9e))
|
||||
* modal overlay zindex, fixed [#271](https://github.com/RSSNext/follow/issues/271) ([7a5000f](https://github.com/RSSNext/follow/commit/7a5000f20503ade15033822d9ad1c11303424728))
|
||||
* modal prevent dimiss when mouse select text ([d99dfbc](https://github.com/RSSNext/follow/commit/d99dfbc79a2d6adb19eb7086fbc3aff8e860fb7b))
|
||||
* modal title overflow tooltip ([5146dd2](https://github.com/RSSNext/follow/commit/5146dd23487f98080bf246f50101e6d13ed79aea))
|
||||
* **modal:** limit setting modal drag range ([#732](https://github.com/RSSNext/follow/issues/732)) ([34f462f](https://github.com/RSSNext/follow/commit/34f462f2877ed979a59644e666ce27ba14dadc5b))
|
||||
* modify the Tooltip content of the list dates title ([#663](https://github.com/RSSNext/follow/issues/663)) ([f99ba5d](https://github.com/RSSNext/follow/commit/f99ba5de916fc17904f6716b4d7eb4ccc295f79c))
|
||||
* more highlighted player thumb ([a17ea00](https://github.com/RSSNext/follow/commit/a17ea00c23604312b09283ef89946140b61b2be9))
|
||||
* new invitation button zindex ([0d79392](https://github.com/RSSNext/follow/commit/0d79392cde4c282cbc3faf69fd697b3726638279))
|
||||
|
|
@ -247,6 +274,8 @@
|
|||
* picture entry preview in Grid view ([#658](https://github.com/RSSNext/follow/issues/658)) ([634978f](https://github.com/RSSNext/follow/commit/634978f427865f646088f13a315207cde63d532c))
|
||||
* player holder size ([78cb08c](https://github.com/RSSNext/follow/commit/78cb08c7dc98aeab9cb5575296470106dbeb4e84))
|
||||
* player marquee mask ([aa9ca62](https://github.com/RSSNext/follow/commit/aa9ca624a8d0304838bc05c330a8165a41430d3e))
|
||||
* populate image src ([426a1d4](https://github.com/RSSNext/follow/commit/426a1d48ddf3131019be80889d00dc394d6ba36f))
|
||||
* populated image src in markdown ([66cca8a](https://github.com/RSSNext/follow/commit/66cca8ad7a8541cc9aa65884a822f10c2dd19be6))
|
||||
* position of swiper navigation buttons ([#541](https://github.com/RSSNext/follow/issues/541)) ([0d760e7](https://github.com/RSSNext/follow/commit/0d760e7bf9e4e74f823c715237c300773865650a))
|
||||
* posthog capture app info ([8cf7ae2](https://github.com/RSSNext/follow/commit/8cf7ae26f47d6cbc58322978d915c699d16f95a3))
|
||||
* posthog tracker ([f4790da](https://github.com/RSSNext/follow/commit/f4790da8b1f27937b47b27d127497742684e9cc1))
|
||||
|
|
@ -263,6 +292,7 @@
|
|||
* prose max widht ([2bd5b45](https://github.com/RSSNext/follow/commit/2bd5b45e6bb4cc127dd07aa073d3b83c0a8bee82))
|
||||
* provide a default ctx value for modal ([38ab5bf](https://github.com/RSSNext/follow/commit/38ab5bffafad32f97ac11a743a35be0a6012403d))
|
||||
* proxy instagram picture ([ae05f1d](https://github.com/RSSNext/follow/commit/ae05f1da2b6d214aa615ad36189e7336f69e1fa1))
|
||||
* proxy sp1.piokok.com and video preview url ([0338876](https://github.com/RSSNext/follow/commit/033887693bc805d3217a4251447bb1e7da00db9a))
|
||||
* radix auto focus issue ([b3d05b4](https://github.com/RSSNext/follow/commit/b3d05b42e9b0afd8b4e828bdf57eb564a53e613f))
|
||||
* read entry history more button position ([61ca6b4](https://github.com/RSSNext/follow/commit/61ca6b452b61cd0770e15e908102c258e29b1c13))
|
||||
* read history always show ([#413](https://github.com/RSSNext/follow/issues/413)) ([c3675b2](https://github.com/RSSNext/follow/commit/c3675b25886ddd33f232f0c146208cb8f42c0b36))
|
||||
|
|
@ -276,6 +306,7 @@
|
|||
* regarding the style issue of selecting subscription types using the Tab and arrow keys ([#525](https://github.com/RSSNext/follow/issues/525)) ([cea7f4a](https://github.com/RSSNext/follow/commit/cea7f4a95ab7336c85613cc551c48639e20b2b9d))
|
||||
* remove `src` ([9aa16b6](https://github.com/RSSNext/follow/commit/9aa16b62d84f40c90334396d219ee9ab2a371750))
|
||||
* remove button leading ([84835c5](https://github.com/RSSNext/follow/commit/84835c5ed9df6ac1370b532980929049e8c0d245))
|
||||
* remove clamp limit, close [#688](https://github.com/RSSNext/follow/issues/688) ([553ba7d](https://github.com/RSSNext/follow/commit/553ba7d61f48d94b7dd2fa89538ec8bd68956d1b))
|
||||
* remove comment ([5343ab2](https://github.com/RSSNext/follow/commit/5343ab2dbe3549a740509e150ebb27cad8b770a4))
|
||||
* remove debug `zh_CN` args ([d650b47](https://github.com/RSSNext/follow/commit/d650b473b966471105c06841876183bc79fad714))
|
||||
* remove duplicated en and tweak lang sort ([#547](https://github.com/RSSNext/follow/issues/547)) ([2bd3d0b](https://github.com/RSSNext/follow/commit/2bd3d0b4c8c68014771375d97d75e491bfd28385))
|
||||
|
|
@ -285,8 +316,11 @@
|
|||
* rename category input composition and style ([1ab3770](https://github.com/RSSNext/follow/commit/1ab3770f0279eb043f7a37a1b5251e5ead0ef0b6))
|
||||
* reorganize package.json ([b2c48a4](https://github.com/RSSNext/follow/commit/b2c48a49e3789cb3dfd9e9170f5371fc6b25c4b1))
|
||||
* require title for list form ([#575](https://github.com/RSSNext/follow/issues/575)) ([b5aeada](https://github.com/RSSNext/follow/commit/b5aeada3cbe1f9f1b1a856c5a3a766a886e29d0d))
|
||||
* reset feed search, close [#701](https://github.com/RSSNext/follow/issues/701) ([a060986](https://github.com/RSSNext/follow/commit/a060986fa3107f8f5e050b430787014dd9bbdc10))
|
||||
* reset is archived state on reset ([b701dd6](https://github.com/RSSNext/follow/commit/b701dd6e1badeaf0d0470bcff28abc3fb4057893))
|
||||
* reset state ([b0a8831](https://github.com/RSSNext/follow/commit/b0a88312f52b2832d3b449aef15f5835b1cf3afc))
|
||||
* resizeable panel ([e2664c2](https://github.com/RSSNext/follow/commit/e2664c2dda663343fa8f53536d6b625daaba6103))
|
||||
* resolve content blur after modal dragged ([#746](https://github.com/RSSNext/follow/issues/746)) ([a4cbec4](https://github.com/RSSNext/follow/commit/a4cbec45e2af245a3d44648d8b59946aca67ddd1))
|
||||
* Respect app light/dark mode over OS preference [#228](https://github.com/RSSNext/follow/issues/228) ([ecef3e9](https://github.com/RSSNext/follow/commit/ecef3e961a79ac9c99bf9a419fa7573433148da0))
|
||||
* rewrite ([3a8cbf2](https://github.com/RSSNext/follow/commit/3a8cbf243903f08392dd1d3eab723b31ff86143a))
|
||||
* round left only in electron ([2d10811](https://github.com/RSSNext/follow/commit/2d10811273d2053b889b2688189b4212b6e9d6a1))
|
||||
|
|
@ -308,6 +342,7 @@
|
|||
* shadow dom style injection in prod ([#270](https://github.com/RSSNext/follow/issues/270)) ([c05d95d](https://github.com/RSSNext/follow/commit/c05d95d495393f1957de609c1ba4de5e707b5a81))
|
||||
* shiki block style ([75908d4](https://github.com/RSSNext/follow/commit/75908d4a6f43847539accd4bad876983902bd5d8))
|
||||
* shortcut modal overlay and id ([c210dc9](https://github.com/RSSNext/follow/commit/c210dc900eda2b30e05a9a5434f06810c1a6cd14))
|
||||
* ShortcutMode cannot be closed using shortcuts keys ([#708](https://github.com/RSSNext/follow/issues/708)) ([12f3965](https://github.com/RSSNext/follow/commit/12f3965f6ac093d0b88eb5e25f47651649dc6ddd))
|
||||
* shortcuts kbd shake ([#433](https://github.com/RSSNext/follow/issues/433)) ([7e8f462](https://github.com/RSSNext/follow/commit/7e8f462424e6b2b98b316c9814610034db93f8c9))
|
||||
* should dismiss when outside (quick new panel) ([675ac84](https://github.com/RSSNext/follow/commit/675ac845d4b9d7c6a71b7891440d2860ca34f0a4))
|
||||
* should use markdown to render link in rsshub parameter desc ([#505](https://github.com/RSSNext/follow/issues/505)) ([2f64787](https://github.com/RSSNext/follow/commit/2f64787e4bc1bc21bf1c1741ed0179f80c66549a))
|
||||
|
|
@ -323,6 +358,7 @@
|
|||
* social media typography ([5d27d29](https://github.com/RSSNext/follow/commit/5d27d29e4bd2791248d6a907a889a4907f0e0d20))
|
||||
* sort by preferred title, fix [#390](https://github.com/RSSNext/follow/issues/390) ([cc25c67](https://github.com/RSSNext/follow/commit/cc25c6785e98f7a200904bdac2b36ff03cf358dd))
|
||||
* sort shortcut ([1f8e4a3](https://github.com/RSSNext/follow/commit/1f8e4a3b966891787f05d9b6d0ec0fe327260e49))
|
||||
* source content loading status ([6d76504](https://github.com/RSSNext/follow/commit/6d765044fb30df639e5324f65e7e30e6dcb2e772))
|
||||
* split entry history api and accurate count ([8f3acd6](https://github.com/RSSNext/follow/commit/8f3acd648eab7f44f61c1685b996f02cfe8cc443))
|
||||
* stable mark all feed list, fixed [#245](https://github.com/RSSNext/follow/issues/245) ([c9d098f](https://github.com/RSSNext/follow/commit/c9d098f4e9bc5d3e3ca5bd026f1947adf6dbcca7))
|
||||
* stable shadow dom key ([06cafb5](https://github.com/RSSNext/follow/commit/06cafb50d8490a29856c27d9344b76090e9f81af))
|
||||
|
|
@ -334,8 +370,11 @@
|
|||
* **style:** add `thumbnail` for video preview ([#632](https://github.com/RSSNext/follow/issues/632)) ([afecb71](https://github.com/RSSNext/follow/commit/afecb71b4657c297613b8ba9be61de61f131ad9f))
|
||||
* **style:** button align center and ([38a5cb6](https://github.com/RSSNext/follow/commit/38a5cb682dc0c3082664a2192686ad16f68ecec3))
|
||||
* **styles:** add gap for received button ([f1b86b3](https://github.com/RSSNext/follow/commit/f1b86b3068eaebee1d123a7c1b5cedb4b4ebef4f))
|
||||
* **styles:** align the styles of Skeleton and Item ([#749](https://github.com/RSSNext/follow/issues/749)) ([ebceaf6](https://github.com/RSSNext/follow/commit/ebceaf6be4f702bab22749a4305bf52174200dfc))
|
||||
* **styles:** user subscription modal list item style ([a954eb9](https://github.com/RSSNext/follow/commit/a954eb93bc74fc6730244e15067888f88743d067))
|
||||
* **style:** video play button align center ([465f1a6](https://github.com/RSSNext/follow/commit/465f1a6acf6a1d96a28e724f071f8a68a327a56d))
|
||||
* subcription status & category empty, input value null ([#263](https://github.com/RSSNext/follow/issues/263)) ([93e2ccf](https://github.com/RSSNext/follow/commit/93e2ccfd5831f8a7f43932acd2976785e3adfbff))
|
||||
* subscription inbox data ([2b496c4](https://github.com/RSSNext/follow/commit/2b496c4e49f223a9d00381983149e827bdb695fd))
|
||||
* subview layout ([36a34de](https://github.com/RSSNext/follow/commit/36a34de26caf2ff00847827635eedd3771faadc0))
|
||||
* supports Windows ([#189](https://github.com/RSSNext/follow/issues/189)) ([486a328](https://github.com/RSSNext/follow/commit/486a328d357317361ef24222fab09eb47e26d07d))
|
||||
* swap show all and unread only copy, fixed [#307](https://github.com/RSSNext/follow/issues/307) ([2b366d1](https://github.com/RSSNext/follow/commit/2b366d1e28e78649e1a7ed6d221d704bcbe4c520))
|
||||
|
|
@ -351,10 +390,12 @@
|
|||
* toc not responsive in dev mode ([ee1894c](https://github.com/RSSNext/follow/commit/ee1894c2d0c420ecf0e63a6dd6bafda57a25a6e3))
|
||||
* toc range calcation ([2ebcc58](https://github.com/RSSNext/follow/commit/2ebcc58414802cffc84fc25ad30bb9fe272ea773))
|
||||
* toc scroll logic ([261d12e](https://github.com/RSSNext/follow/commit/261d12e5600028a5bff3b2c1451271ee962403ed))
|
||||
* tooltip content of open image and improve i18n support ([#689](https://github.com/RSSNext/follow/issues/689)) ([440de7b](https://github.com/RSSNext/follow/commit/440de7b737f9abcadc68ab1099fd28a8e6710699))
|
||||
* tooltip in dark mode shadow ([55266ce](https://github.com/RSSNext/follow/commit/55266ce2cceea4e348c33989398f8f9bb92644a7))
|
||||
* tooltip style in dark mode ([a61c79e](https://github.com/RSSNext/follow/commit/a61c79ee7d4bd73fdf35fa5565c711297a29c7a0))
|
||||
* transcation table text overflow ([a026c6e](https://github.com/RSSNext/follow/commit/a026c6e34c0eeac1ee6acec1c8181c009ca0ab36))
|
||||
* translation markdown tooltip wrapper style ([639bace](https://github.com/RSSNext/follow/commit/639bace313311db6319fb1fe168b68af7aa1e8e0))
|
||||
* truncate long title in dialog title ([#729](https://github.com/RSSNext/follow/issues/729)) ([8d25987](https://github.com/RSSNext/follow/commit/8d259878c30a987940d7e982c63b37f100008635))
|
||||
* try fix vercel config ([968dea9](https://github.com/RSSNext/follow/commit/968dea91f474a76298091de6a23f3ba7a11fb22f))
|
||||
* try to fix linux build ([89eda0f](https://github.com/RSSNext/follow/commit/89eda0f2148b5cc4d45d95f462c6a721022d0bc8))
|
||||
* try to fix some sentry error ([0b31c1e](https://github.com/RSSNext/follow/commit/0b31c1e998212900dda0044acda8a3d4d5df1189))
|
||||
|
|
@ -366,6 +407,7 @@
|
|||
* type error ([c7f11f9](https://github.com/RSSNext/follow/commit/c7f11f99f28bf918aaf8c03b74145b2f9b126a72))
|
||||
* type error ([5cd41ad](https://github.com/RSSNext/follow/commit/5cd41ad10ccbd241083d6f951509eb0bafe6e25f))
|
||||
* type error ([60f8347](https://github.com/RSSNext/follow/commit/60f8347eec118176681a9ad7fcab7f45e383fc1b))
|
||||
* types ([f0767e2](https://github.com/RSSNext/follow/commit/f0767e2ad93d29db9a82b283f1163e340167be68))
|
||||
* types ([831f0f8](https://github.com/RSSNext/follow/commit/831f0f892ac32bdb2963267e94c3130805924b59))
|
||||
* types ([a116dd4](https://github.com/RSSNext/follow/commit/a116dd4018f90886239f7583cbc82b511ff2a902))
|
||||
* types ([3c5b224](https://github.com/RSSNext/follow/commit/3c5b224904e8db1c8ba6b78cfb87d09cd81de98c))
|
||||
|
|
@ -374,6 +416,7 @@
|
|||
* types ([d9458a1](https://github.com/RSSNext/follow/commit/d9458a1850b3e83e9bbb9374d4fc3b6884a24837))
|
||||
* typing ([a386fd6](https://github.com/RSSNext/follow/commit/a386fd6e97fb98c42b3eedf156c5b167ff67bd34))
|
||||
* typo ([deb96c5](https://github.com/RSSNext/follow/commit/deb96c538258c939c60762c723d58955899031cf))
|
||||
* **ui:** entry title line height ([#608](https://github.com/RSSNext/follow/issues/608)) ([5309491](https://github.com/RSSNext/follow/commit/53094912981ad8cfc79120621725bab1708250f8))
|
||||
* **ui:** social media gap if no media ([1435632](https://github.com/RSSNext/follow/commit/14356325e42d87c43f33b975fbb1d963cfa67251))
|
||||
* **ui:** social media unread dot position ([e760676](https://github.com/RSSNext/follow/commit/e760676827297f60a4d00a51f4be98579c93c9f7))
|
||||
* undo kbd color in dark mode ([2a106ae](https://github.com/RSSNext/follow/commit/2a106ae632e0641d58d0d45b88f61a1c5c6eafa5))
|
||||
|
|
@ -394,6 +437,7 @@
|
|||
* use feed enabled condition ([9726d43](https://github.com/RSSNext/follow/commit/9726d4396673e2be02c60ce049ed07c5bf978380))
|
||||
* use insertedAt as cursor for list ([#580](https://github.com/RSSNext/follow/issues/580)) ([e420451](https://github.com/RSSNext/follow/commit/e420451afc4eba6308ecf72ff710f390ee2b0b8b))
|
||||
* use official embed player in bilibili video when use web ([#219](https://github.com/RSSNext/follow/issues/219)) ([9177a34](https://github.com/RSSNext/follow/commit/9177a349e087cdc60b22e2338146caaefd377694))
|
||||
* useFeedHeaderTitle ([edbdc86](https://github.com/RSSNext/follow/commit/edbdc8626161f9bcc7c93fc372e8a2e247d13a50))
|
||||
* user avatar use profile data if userId passed, fixed [#550](https://github.com/RSSNext/follow/issues/550) ([176afa7](https://github.com/RSSNext/follow/commit/176afa72ba1e0801ac383982f826cc19b215b242))
|
||||
* user button color in dark mode ([04e3c38](https://github.com/RSSNext/follow/commit/04e3c386c5699e6b0907de2a98701668fc7f278b))
|
||||
* user modal list padding ([992ea15](https://github.com/RSSNext/follow/commit/992ea1594af02e3815adae2fe99de4f205543239))
|
||||
|
|
@ -401,6 +445,7 @@
|
|||
* userActions in feed store ([8da4fb7](https://github.com/RSSNext/follow/commit/8da4fb71d9cc14832bc985cf8c1bd6f04dbe4e4c))
|
||||
* video preview ([e38820c](https://github.com/RSSNext/follow/commit/e38820c1aa2294f45e05d331bf4bce0e4d8efb44))
|
||||
* view icon color in dark mode ([991bff7](https://github.com/RSSNext/follow/commit/991bff7660e93b872f1398c89c0fe4316f4327d9))
|
||||
* view source content in picture view ([16938f6](https://github.com/RSSNext/follow/commit/16938f60786f8496152361405c0fcff4635f7d59))
|
||||
* virtuosoitem height zero issue ([3c5c7ba](https://github.com/RSSNext/follow/commit/3c5c7babc9b3f83ef89ef1007f25d7b3ccb8f6a1))
|
||||
* wait language load then switch to target language ([bbe4ea6](https://github.com/RSSNext/follow/commit/bbe4ea66e75a8911e9ef39764d02d22251ba743e))
|
||||
* wallet table latyout ([6c60dd2](https://github.com/RSSNext/follow/commit/6c60dd265245d08d622c59d035d2124285ff68f1))
|
||||
|
|
@ -413,6 +458,7 @@
|
|||
* windows multi-display ([7490cd1](https://github.com/RSSNext/follow/commit/7490cd12d0e42a7f41931acd9d77bf64be39a26d))
|
||||
* **windows:** dont remove locale ([7305eba](https://github.com/RSSNext/follow/commit/7305eba3fee218330818d96ca155f833aa99d631))
|
||||
* **windows:** skip remove locle ([5df82d0](https://github.com/RSSNext/follow/commit/5df82d0c0f98bebb6e78a694a6ceda3644eb36ef))
|
||||
* wrong opening status of newly added feed ([#726](https://github.com/RSSNext/follow/issues/726)) ([2520ddb](https://github.com/RSSNext/follow/commit/2520ddb0f4f1923b729ae707a703713f1a158f06))
|
||||
* wrong text wrap ([#316](https://github.com/RSSNext/follow/issues/316)) ([0cee7ef](https://github.com/RSSNext/follow/commit/0cee7eff25ed1058b108f1c7a9c643ea17b479c0))
|
||||
* wtf, cursor deleted my code ([1852d1e](https://github.com/RSSNext/follow/commit/1852d1ee812567c473ff26d316b93d1ccf8e8a88))
|
||||
* zero tipUsers ([7f994d7](https://github.com/RSSNext/follow/commit/7f994d7a21080468cb07908b4b8fc694ebfeba37))
|
||||
|
|
@ -441,6 +487,7 @@
|
|||
* add more words i18n ([#516](https://github.com/RSSNext/follow/issues/516)) ([e2b8ec8](https://github.com/RSSNext/follow/commit/e2b8ec88624ad1e8e7f6136a624fc284ae3fb77b))
|
||||
* add object-cover to feed icon, add zh-HK and zh-TW errors lang ([#508](https://github.com/RSSNext/follow/issues/508)) ([9351a8f](https://github.com/RSSNext/follow/commit/9351a8fe735977ae02320370f9aa6a83854d7adf))
|
||||
* add og image, fixed [#242](https://github.com/RSSNext/follow/issues/242) ([5d4e958](https://github.com/RSSNext/follow/commit/5d4e9586569e4afe212138d2001ed9007ce9950b))
|
||||
* add overflow tooltip for feed title ([a89ad8b](https://github.com/RSSNext/follow/commit/a89ad8be84384c1851587d02f6b21ce64ad24bdb))
|
||||
* add player download src ([fc50751](https://github.com/RSSNext/follow/commit/fc5075139f2f82a66c703349a719927c35b11b09))
|
||||
* add preview image bottom indicator ([bbc1591](https://github.com/RSSNext/follow/commit/bbc15913e8a1afd148c4c70a8056c64d46587791))
|
||||
* add readability action rules ([5e8fb17](https://github.com/RSSNext/follow/commit/5e8fb17f4a849abd755d92d32d3e5e1a76a2d2ab))
|
||||
|
|
@ -476,6 +523,7 @@
|
|||
* context menu of adding feeds to lists ([afb79a2](https://github.com/RSSNext/follow/commit/afb79a27fba87d43e62ea06c3a64267ac3d7f4e4))
|
||||
* context sub menu ([2d18c53](https://github.com/RSSNext/follow/commit/2d18c53de0cb8c99336d0e915941b1fdee36df98))
|
||||
* copy logo svg ([512699c](https://github.com/RSSNext/follow/commit/512699c07080f3064f984f23a21e28356ef9fcc0))
|
||||
* **corner-player:** support now playing ([#717](https://github.com/RSSNext/follow/issues/717)) ([553f71b](https://github.com/RSSNext/follow/commit/553f71b1babb7a483fe8ddef741e281f1eb77730))
|
||||
* custom feed title ([#300](https://github.com/RSSNext/follow/issues/300)) ([501e2f4](https://github.com/RSSNext/follow/commit/501e2f44c1d3eb9a1a316549faedb2e1288f7fbe))
|
||||
* date item in entry column ([#199](https://github.com/RSSNext/follow/issues/199)) ([9d5a811](https://github.com/RSSNext/follow/commit/9d5a81112e873f5b422356fa0b3fcf526abb84e5))
|
||||
* dayjs locale ([6ed0a3b](https://github.com/RSSNext/follow/commit/6ed0a3bf3a937a95985c98bb24c4bc23f6b776de))
|
||||
|
|
@ -526,7 +574,11 @@
|
|||
* **i18n:** add Spanish language support ([#400](https://github.com/RSSNext/follow/issues/400)) ([6b7823d](https://github.com/RSSNext/follow/commit/6b7823d9b314509f209516b1d7412446ff4ac1a3))
|
||||
* **i18n:** add zh-CN i18n for external page ([#399](https://github.com/RSSNext/follow/issues/399)) ([49a5c50](https://github.com/RSSNext/follow/commit/49a5c50236d51c3c728bb51cd19f34915291e59a))
|
||||
* **i18n:** added Turkish language ([#544](https://github.com/RSSNext/follow/issues/544)) ([3751363](https://github.com/RSSNext/follow/commit/3751363d45af3096ccefb81bf60ac72040fce505))
|
||||
* **i18n:** Chinese: Update `errors` ([#687](https://github.com/RSSNext/follow/issues/687)) ([368d232](https://github.com/RSSNext/follow/commit/368d23250817bb4033168d8736f8e0ed5cd9475f))
|
||||
* **i18n:** Chinese: Update translation ([#679](https://github.com/RSSNext/follow/issues/679)) ([36593cc](https://github.com/RSSNext/follow/commit/36593cc05a1b94545266e65008ef4a52f80cfbfe))
|
||||
* **i18n:** Chinese: webhook action setting ([#705](https://github.com/RSSNext/follow/issues/705)) ([1d63489](https://github.com/RSSNext/follow/commit/1d6348919f850f28c007a0e589deb7a29b56ec25))
|
||||
* **i18n:** complete Traditional Chinese translation for native file ([#506](https://github.com/RSSNext/follow/issues/506)) ([2059329](https://github.com/RSSNext/follow/commit/2059329691b375a73cd9cb4f2389b68bbb544e41))
|
||||
* **i18n:** conditional display of 'browser' or 'new tab' ([#692](https://github.com/RSSNext/follow/issues/692)) ([213a4bc](https://github.com/RSSNext/follow/commit/213a4bce2c22eba336981e57c3a2ebd321de63da))
|
||||
* **i18n:** fill out and optimized partial Chinese translations ([#431](https://github.com/RSSNext/follow/issues/431)) ([5f87190](https://github.com/RSSNext/follow/commit/5f87190f018fea7f34c3ccbfad081fd8b855f5b8))
|
||||
* **i18n:** improve zh-CN ([#410](https://github.com/RSSNext/follow/issues/410)) ([0b0304e](https://github.com/RSSNext/follow/commit/0b0304eaa1dfa8396e78612a9a6a50653ce9669e))
|
||||
* **i18n:** Korean and fallback language name ([#582](https://github.com/RSSNext/follow/issues/582)) ([2ae12c9](https://github.com/RSSNext/follow/commit/2ae12c9d2b976c8165414cc74c7494b2cfd7ea7c))
|
||||
|
|
@ -535,9 +587,14 @@
|
|||
* **i18n:** Update Chinese Translation ([#572](https://github.com/RSSNext/follow/issues/572)) ([d24139b](https://github.com/RSSNext/follow/commit/d24139b82233ea04674c8b93ddee323d2c84a8ef))
|
||||
* **i18n:** zh-CN: context menu of adding feeds to lists ([#661](https://github.com/RSSNext/follow/issues/661)) ([c3b2fa9](https://github.com/RSSNext/follow/commit/c3b2fa9274ef1e199e66d07c94dc6674f1de6045))
|
||||
* ignore feed errors within 9 hours ([b71926e](https://github.com/RSSNext/follow/commit/b71926ec286b77dd1b9fc25df459176063b194da))
|
||||
* **image:** blurhash for entry media preview, and other adjustment ([b688848](https://github.com/RSSNext/follow/commit/b688848dacc25f3106c90681b3044a69571a54d5))
|
||||
* impl cmd+b ([ea8a832](https://github.com/RSSNext/follow/commit/ea8a83271e9be37c277540eb605651d7166d2ea4))
|
||||
* impl masonry in view mark read and scroll to mark read ([4263658](https://github.com/RSSNext/follow/commit/4263658e3240850ca74fcaa666bf8145553f8874))
|
||||
* improve shiki code block renderer and show language ([590d9cb](https://github.com/RSSNext/follow/commit/590d9cbb94a63f843d00bf54848bba40157d2452))
|
||||
* inbox ([#742](https://github.com/RSSNext/follow/issues/742)) ([2fba746](https://github.com/RSSNext/follow/commit/2fba746447c1c10f2d58ac9eb908cd0d6a5fb364))
|
||||
* inbox list api ([6da197c](https://github.com/RSSNext/follow/commit/6da197c76bc4d08c80277fc8801a68bfbd413248))
|
||||
* inbox set read and unread ([6da8bce](https://github.com/RSSNext/follow/commit/6da8bce04cb0da99efbad473879350131f993c16))
|
||||
* inbox unread data updating ([61d9a48](https://github.com/RSSNext/follow/commit/61d9a48310d726205271d1d01026511fb4ccb276))
|
||||
* integration settings page ([f9f1938](https://github.com/RSSNext/follow/commit/f9f19386b28e853eb03b3ca5bae629d4f6542cb3))
|
||||
* invitation limitation message ([fa37a0c](https://github.com/RSSNext/follow/commit/fa37a0ca2c8235fb5fd47f96c26de7fdcd762ad8))
|
||||
* **invitation:** add signout button ([3c0f4e7](https://github.com/RSSNext/follow/commit/3c0f4e779c68a933d57e232d62774fca1c0b86ed))
|
||||
|
|
@ -563,9 +620,11 @@
|
|||
* mark read doesn't require manual confirmation if hotkey called, fixed [#293](https://github.com/RSSNext/follow/issues/293) ([6ce06be](https://github.com/RSSNext/follow/commit/6ce06be58d9c5017c77b774cc53fa6076167d563))
|
||||
* mark read for cateogry in ctx menu ([da79f1c](https://github.com/RSSNext/follow/commit/da79f1c2783fa65f164aaf4942ebfa2156b4bf26))
|
||||
* markdown heading render ([f3ac1ac](https://github.com/RSSNext/follow/commit/f3ac1ac3c6e2c36203288921f0ea8c3635eec539))
|
||||
* masonry item blur hash and html render also ([6f94ec5](https://github.com/RSSNext/follow/commit/6f94ec5027813b0d3804b1b0ab057d484036c8f2))
|
||||
* modal resize and draggable to absolute position ([aad5eba](https://github.com/RSSNext/follow/commit/aad5eba2edb66039d123e9e60e484b55d9a037df))
|
||||
* more excluded path params ([3d16aa9](https://github.com/RSSNext/follow/commit/3d16aa9f8b3b369a146537f19ab9a2fe230e9553))
|
||||
* more i18n text ([6f8acce](https://github.com/RSSNext/follow/commit/6f8acced7bc51d2bb22325506e1c9bc97ee963f7))
|
||||
* native menu add `hide` prop ([8c6753d](https://github.com/RSSNext/follow/commit/8c6753d4a66d1e7ad09efcbbca24c8cf51ed8222))
|
||||
* navigator bar ([56750fd](https://github.com/RSSNext/follow/commit/56750fd1722eaca7416e5c8617fb501fcf055cac))
|
||||
* new design for mark read in date item ([e5b0583](https://github.com/RSSNext/follow/commit/e5b0583d38e41d81663bec1ee16e0c24cb385d7a))
|
||||
* new platform icons ([7513130](https://github.com/RSSNext/follow/commit/7513130c17c501f3e2d7c29fc13e2315edb4dc8c))
|
||||
|
|
@ -577,6 +636,7 @@
|
|||
* optimize 404 page ([f51b1e2](https://github.com/RSSNext/follow/commit/f51b1e2de9e8d8c3bdea9db79476c17cd7247a4b))
|
||||
* optimize compact picture entry preview title ([5e6d73a](https://github.com/RSSNext/follow/commit/5e6d73a728164dcb0603e03fbcb9fb1825c5c2ef))
|
||||
* optimize daily report and report modal for social media ([252962c](https://github.com/RSSNext/follow/commit/252962c188e29293f4b6baca2e99568a064f79f4))
|
||||
* optimize no showUnreadCount dot display ([9698fa6](https://github.com/RSSNext/follow/commit/9698fa69d110781d26b542e9e81a51dc7b16805e))
|
||||
* optimize the style of the profile to maximize the presentation of the content ([96769fb](https://github.com/RSSNext/follow/commit/96769fbe49eb68ea09f1307ac27d0c0d904d0abf))
|
||||
* parse newsletter embedded `<style/>` ([#233](https://github.com/RSSNext/follow/issues/233)) ([d63a9d8](https://github.com/RSSNext/follow/commit/d63a9d820b41fdb99aa646956e13aa91835f1e3d))
|
||||
* path parser v8 ([5be2e9a](https://github.com/RSSNext/follow/commit/5be2e9aad4309ba5d63451460d674e233981f6e3))
|
||||
|
|
@ -592,6 +652,7 @@
|
|||
* reduce motion use fade-in and fade-out transition ([78053c1](https://github.com/RSSNext/follow/commit/78053c17fd91ccd7568fe57af3767cd6636929e2))
|
||||
* refresh cursor style when split panel can't drag ([c9f70c0](https://github.com/RSSNext/follow/commit/c9f70c09891eaf5dec89c305a5382e705c8c7eb3))
|
||||
* refresh unread data in entries refresh action ([31dfbd8](https://github.com/RSSNext/follow/commit/31dfbd8de56bafa6b26188a1329d9d81fca36ed6))
|
||||
* Remember the open state of the category, Ensure that each View'… ([#709](https://github.com/RSSNext/follow/issues/709)) ([cb2078d](https://github.com/RSSNext/follow/commit/cb2078da70ac93add77aee9da83d5687c4b8b9c8))
|
||||
* remove tooltip border ([9e770c9](https://github.com/RSSNext/follow/commit/9e770c96a18beeb290adb9d189dd6afff9dbb817))
|
||||
* render social media with full text ([1531d2b](https://github.com/RSSNext/follow/commit/1531d2b70916b3aff7293db411f472cb013cc4e7))
|
||||
* resizeable setting panel and adjust action tab ([5fd9b23](https://github.com/RSSNext/follow/commit/5fd9b23f03bccd6d3bc427ee0e5c31bfb69e3c42))
|
||||
|
|
@ -608,6 +669,7 @@
|
|||
* show media fallback for video item ([3446a81](https://github.com/RSSNext/follow/commit/3446a81ea28d9e18e5dfa18306758cda64637520))
|
||||
* show reasons for profile update failure ([#484](https://github.com/RSSNext/follow/issues/484)) ([b8b94ac](https://github.com/RSSNext/follow/commit/b8b94ac6c4e97088144f20cb8abd8f1d8080f8fd))
|
||||
* smaller and fixed unread dot ([72aa3a8](https://github.com/RSSNext/follow/commit/72aa3a8cae968893b7ca3b0f8abf11c50c739c74))
|
||||
* smaller lists image ([31591d6](https://github.com/RSSNext/follow/commit/31591d68d595c9b44bf2ec49b7e3083acd612bb3))
|
||||
* some optimize ([5f61579](https://github.com/RSSNext/follow/commit/5f6157972c7ce2bf45b6e7b4026d9e727795c9a3))
|
||||
* sort list by insertedat ([36fbbdd](https://github.com/RSSNext/follow/commit/36fbbddc6d8880f13b99fb38042692894383ecfa))
|
||||
* sort the query results by the number of followers in asc order ([#379](https://github.com/RSSNext/follow/issues/379)) ([304cb7f](https://github.com/RSSNext/follow/commit/304cb7f59f8622cb076e5bf2b240b1a3f6c2614c))
|
||||
|
|
@ -631,6 +693,7 @@
|
|||
* update discord link ([a45071b](https://github.com/RSSNext/follow/commit/a45071b75da3fa343d52d3ddd6626693b73ea35d))
|
||||
* update hono.ts ([66174a0](https://github.com/RSSNext/follow/commit/66174a028d0e709e0bd38cd7862bddeb1bdd19b4))
|
||||
* update hono.ts ([c9e65dc](https://github.com/RSSNext/follow/commit/c9e65dcdde649963aafaad3c0b83165bf86df5dd))
|
||||
* update icons ([e474bbf](https://github.com/RSSNext/follow/commit/e474bbf45d99da1aca68d8fcc1110a8f3ccb45fa))
|
||||
* update invitation dialog text ([b062ea2](https://github.com/RSSNext/follow/commit/b062ea2353093ec67e1309652bc0d996b87b177a))
|
||||
* update invitations ([84c7b46](https://github.com/RSSNext/follow/commit/84c7b46155c064eab1dbd030add4a00dee412469))
|
||||
* update readwise logo ([c85460c](https://github.com/RSSNext/follow/commit/c85460c5bd13a3dadda1d4592b1ce6e434f4a8c6))
|
||||
|
|
@ -639,10 +702,14 @@
|
|||
* use claim check api ([03764da](https://github.com/RSSNext/follow/commit/03764dac0ec500068376372ce4d85b1bc412bcd4))
|
||||
* use dotlottie and add confetti ([2385d15](https://github.com/RSSNext/follow/commit/2385d1530c62d7adada3923832d8f57a13268b6e))
|
||||
* use user.handle default to share profile url ([36d51d9](https://github.com/RSSNext/follow/commit/36d51d96f89a0182fd59cd48ff175ea5e8009f2e))
|
||||
* **user-profile:** smoother height switching ([#674](https://github.com/RSSNext/follow/issues/674)) ([e6459a5](https://github.com/RSSNext/follow/commit/e6459a5b63235755ca0e8acfa59db2d56ec40713))
|
||||
* **ux:** rotating icon when reset rotate back ([13105ee](https://github.com/RSSNext/follow/commit/13105ee4177edb75d31b43d2bd1161516dcc8b8a))
|
||||
* video player ([#225](https://github.com/RSSNext/follow/issues/225)) ([bb7d312](https://github.com/RSSNext/follow/commit/bb7d3127a9fe4543af12edfc49ce745e6ee7099e))
|
||||
* video player i18n ([a498228](https://github.com/RSSNext/follow/commit/a4982289f8b4a7967e9ebea837109bd5e342b663))
|
||||
* view source content in app ([#655](https://github.com/RSSNext/follow/issues/655)) ([8ba5822](https://github.com/RSSNext/follow/commit/8ba5822a29a76a8e9817967a6af2bde0cb846460))
|
||||
* warn when go to external untrusted link ([06be9b5](https://github.com/RSSNext/follow/commit/06be9b58f7c054ef166ea7d5759944280de9a201))
|
||||
* webhook action setting ([0016a07](https://github.com/RSSNext/follow/commit/0016a0742b7ecd107152e09cb629d0d2ea637764))
|
||||
* wide mode ([346db5a](https://github.com/RSSNext/follow/commit/346db5ad480966d669edf4d4e1517fba866de93a))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
|
@ -653,6 +720,7 @@
|
|||
* persist tts voices ([9ea05b9](https://github.com/RSSNext/follow/commit/9ea05b9e517ccb67a4fc49cf85a255d15557a010))
|
||||
* prefetch for entry list ([5c7a27d](https://github.com/RSSNext/follow/commit/5c7a27de7dedfaf1812f26823207331543dfcead))
|
||||
* reduce re-render when ctx menu open ([96ff9f6](https://github.com/RSSNext/follow/commit/96ff9f660c0a469daafbb5f5cad47370bcd3a8f0))
|
||||
* reduce vite alias length ([8bc4771](https://github.com/RSSNext/follow/commit/8bc477100b8e5d3532d09875eda32004762eacca))
|
||||
* shadow dom css cache ([c1e3a02](https://github.com/RSSNext/follow/commit/c1e3a025f7ef8ff570399a44a95797c2c8261e42))
|
||||
* speed up user profile modal data loading ([53a5bae](https://github.com/RSSNext/follow/commit/53a5bae316b406ff5c250c3e7772ce4071891bee))
|
||||
|
||||
|
|
|
|||
|
|
@ -42,17 +42,16 @@
|
|||
"msedge-tts": "1.3.4",
|
||||
"nanoid": "5.0.7",
|
||||
"ofetch": "1.4.0",
|
||||
"posthog-js": "1.163.0",
|
||||
"posthog-js": "1.165.0",
|
||||
"posthog-node": "4.2.0",
|
||||
"semver": "7.6.3",
|
||||
"vscode-languagedetection": "npm:@vscode/vscode-languagedetection@^1.0.22"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash-es": "4.17.12",
|
||||
"@types/node": "^22.5.5",
|
||||
"@types/node": "^22.7.4",
|
||||
"electron": "32.1.2",
|
||||
"electron-devtools-installer": "3.2.0",
|
||||
"hono": "4.6.2",
|
||||
"typescript": "^5.6.2"
|
||||
"hono": "4.6.3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,10 +24,11 @@ export const handleUrlRouting = (url: string) => {
|
|||
mainWindow.focus()
|
||||
const caller = callWindowExpose(mainWindow)
|
||||
|
||||
const id = searchParams.get("id")
|
||||
const id = searchParams.get("id") ?? undefined
|
||||
const isList = searchParams.get("type") === "list"
|
||||
if (!id) return
|
||||
caller.follow(id, { isList })
|
||||
const urlParam = searchParams.get("url") ?? undefined
|
||||
if (!id && !urlParam) return
|
||||
caller.follow({ isList, id, url: urlParam })
|
||||
return
|
||||
}
|
||||
default: {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export interface ActionMenuItem {
|
|||
shortcut?: string
|
||||
icon?: string
|
||||
submenu?: MenuItem[]
|
||||
checked?: boolean
|
||||
}
|
||||
export const menuRoute = {
|
||||
showContextMenu: t.procedure
|
||||
|
|
@ -28,10 +29,13 @@ export const menuRoute = {
|
|||
type: "separator" as const,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
label: item.label,
|
||||
enabled: item.enabled ?? true,
|
||||
accelerator: item.shortcut?.replace("Meta", "CmdOrCtrl"),
|
||||
checked: typeof item.checked === "boolean" ? item.checked : undefined,
|
||||
type: typeof item.checked === "boolean" ? "checkbox" : undefined,
|
||||
click() {
|
||||
context.sender.send(
|
||||
"menu-click",
|
||||
|
|
|
|||
|
|
@ -16,11 +16,10 @@
|
|||
"@headlessui/react": "2.1.8",
|
||||
"@hono/auth-js": "1.0.10",
|
||||
"@hookform/resolvers": "3.9.0",
|
||||
"@iconify/tools": "4.0.6",
|
||||
"@lottiefiles/dotlottie-react": "0.8.12",
|
||||
"@iconify/tools": "4.0.7",
|
||||
"@lottiefiles/dotlottie-react": "0.9.0",
|
||||
"@microflash/remark-callout-directives": "4.3.1",
|
||||
"@mozilla/readability": "^0.5.0",
|
||||
"@radix-ui/react-alert-dialog": "1.1.1",
|
||||
"@radix-ui/react-avatar": "1.1.0",
|
||||
"@radix-ui/react-checkbox": "1.1.1",
|
||||
"@radix-ui/react-context-menu": "2.2.1",
|
||||
|
|
@ -38,13 +37,12 @@
|
|||
"@radix-ui/react-tabs": "1.1.0",
|
||||
"@radix-ui/react-toast": "1.2.1",
|
||||
"@radix-ui/react-tooltip": "1.1.2",
|
||||
"@sentry/react": "8.30.0",
|
||||
"@sentry/vite-plugin": "2.22.4",
|
||||
"@shikijs/transformers": "1.18.0",
|
||||
"@sentry/react": "8.32.0",
|
||||
"@shikijs/transformers": "1.20.0",
|
||||
"@t3-oss/env-core": "^0.11.1",
|
||||
"@tanstack/query-sync-storage-persister": "5.56.2",
|
||||
"@tanstack/react-query": "5.56.2",
|
||||
"@tanstack/react-query-devtools": "5.56.2",
|
||||
"@tanstack/react-query-devtools": "5.58.0",
|
||||
"@tanstack/react-query-persist-client": "5.56.2",
|
||||
"@use-gesture/react": "10.3.1",
|
||||
"@yornaath/batshit": "0.10.1",
|
||||
|
|
@ -54,10 +52,10 @@
|
|||
"cmdk": "1.0.0",
|
||||
"dayjs": "1.11.13",
|
||||
"dexie": "4.0.8",
|
||||
"dnum": "^2.13.1",
|
||||
"dnum": "^2.14.0",
|
||||
"electron-log": "5.2.0",
|
||||
"foxact": "0.2.38",
|
||||
"framer-motion": "11.5.6",
|
||||
"framer-motion": "11.9.0",
|
||||
"franc-min": "6.2.0",
|
||||
"fuse.js": "7.0.0",
|
||||
"hast-util-to-jsx-runtime": "2.3.0",
|
||||
|
|
@ -68,14 +66,13 @@
|
|||
"immer": "10.1.1",
|
||||
"jotai": "2.10.0",
|
||||
"lethargy": "1.0.9",
|
||||
"linkedom": "^0.18.5",
|
||||
"lodash-es": "4.17.21",
|
||||
"masonic": "4.0.1",
|
||||
"nanoid": "5.0.7",
|
||||
"ofetch": "1.4.0",
|
||||
"path-to-regexp": "8.1.0",
|
||||
"posthog-js": "1.163.0",
|
||||
"re-resizable": "6.9.18",
|
||||
"path-to-regexp": "8.2.0",
|
||||
"posthog-js": "1.165.0",
|
||||
"re-resizable": "6.10.0",
|
||||
"react-blurhash": "^0.3.0",
|
||||
"react-error-boundary": "4.0.13",
|
||||
"react-fast-marquee": "1.6.5",
|
||||
|
|
@ -89,19 +86,19 @@
|
|||
"react-shadow": "20.5.0",
|
||||
"react-virtuoso": "4.10.4",
|
||||
"rehype-infer-description-meta": "2.0.0",
|
||||
"rehype-parse": "9.0.0",
|
||||
"rehype-parse": "9.0.1",
|
||||
"rehype-sanitize": "6.0.0",
|
||||
"rehype-stringify": "10.0.0",
|
||||
"rehype-stringify": "10.0.1",
|
||||
"remark-directive": "3.0.0",
|
||||
"remark-gfm": "4.0.0",
|
||||
"remark-gh-alerts": "0.0.3",
|
||||
"remark-parse": "11.0.0",
|
||||
"remark-rehype": "11.1.1",
|
||||
"shiki": "1.18.0",
|
||||
"shiki": "1.20.0",
|
||||
"sonner": "^1.5.0",
|
||||
"swiper": "11.1.14",
|
||||
"tailwind-merge": "2.5.2",
|
||||
"tldts": "6.1.47",
|
||||
"tldts": "6.1.48",
|
||||
"unified": "11.0.5",
|
||||
"use-context-selector": "2.0.0",
|
||||
"usehooks-ts": "3.1.0",
|
||||
|
|
@ -113,12 +110,12 @@
|
|||
"@babel/generator": "7.25.6",
|
||||
"@hono/node-server": "1.13.1",
|
||||
"@types/lodash-es": "4.17.12",
|
||||
"@types/node": "^22.5.5",
|
||||
"@types/react": "^18.3.8",
|
||||
"@types/node": "^22.7.4",
|
||||
"@types/react": "^18.3.10",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"fake-indexeddb": "6.0.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"typescript": "^5.6.2"
|
||||
"vitest": "2.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ export const createDefaultSettings = (): UISettings => ({
|
|||
// View
|
||||
pictureViewMasonry: true,
|
||||
pictureViewFilterNoImage: false,
|
||||
wideMode: false,
|
||||
|
||||
// TTS
|
||||
voice: "en-US-AndrewMultilingualNeural",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
import { atom } from "jotai"
|
||||
import { useCallback } from "react"
|
||||
|
||||
import { useModalStack } from "~/components/ui/modal"
|
||||
import { createAtomHooks } from "~/lib/jotai"
|
||||
import { SourceContentView } from "~/modules/entry-content/components/SourceContentView"
|
||||
|
||||
export const [, , useShowSourceContent, , getShowSourceContent, setShowSourceContent] =
|
||||
createAtomHooks(atom<boolean>(false))
|
||||
|
||||
export const toggleShowSourceContent = () => setShowSourceContent(!getShowSourceContent())
|
||||
export const resetShowSourceContent = () => setShowSourceContent(false)
|
||||
|
||||
export const useSourceContentModal = () => {
|
||||
const { present } = useModalStack()
|
||||
|
||||
return useCallback(
|
||||
({ title, src }: { title?: string; src: string }) => {
|
||||
present({
|
||||
id: src,
|
||||
title,
|
||||
content: () => <SourceContentView src={src} />,
|
||||
resizeable: true,
|
||||
clickOutsideToDismiss: true,
|
||||
max: true,
|
||||
})
|
||||
},
|
||||
[present],
|
||||
)
|
||||
}
|
||||
|
|
@ -196,6 +196,10 @@ export function FeedIcon({
|
|||
)
|
||||
break
|
||||
}
|
||||
case feed?.type === "inbox": {
|
||||
ImageElement = <i className="i-mgc-inbox-cute-fi mr-2 shrink-0" style={sizeStyle} />
|
||||
break
|
||||
}
|
||||
case !!feed?.title && !!feed.title[0]: {
|
||||
ImageElement = fallbackIcon
|
||||
break
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { WEB_URL } from "@follow/shared/constants"
|
||||
import { env } from "@follow/shared/env"
|
||||
|
||||
import { FeedIcon } from "~/components/feed-icon"
|
||||
import { cn } from "~/lib/utils"
|
||||
|
|
@ -16,15 +17,35 @@ export function FollowSummary({
|
|||
docs?: string
|
||||
className?: string
|
||||
}) {
|
||||
const isList = feed.type === "list"
|
||||
let siteLink
|
||||
let feedLink
|
||||
let feedTitle = feed.title
|
||||
let feedText
|
||||
switch (feed.type) {
|
||||
case "feed": {
|
||||
siteLink = feed.siteUrl || void 0
|
||||
feedLink = feed.url || docs
|
||||
feedText = feed.url || docs
|
||||
break
|
||||
}
|
||||
case "list": {
|
||||
siteLink = `${WEB_URL}/list/${feed.id}`
|
||||
feedLink = siteLink
|
||||
feedText = `/list/${feed.id}`
|
||||
break
|
||||
}
|
||||
case "inbox": {
|
||||
siteLink = void 0
|
||||
feedLink = siteLink
|
||||
feedText = `${feed.id}${env.VITE_INBOXES_EMAIL}`
|
||||
feedTitle = feed.title || `${feed.id}'s Inbox`
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("flex select-text flex-col gap-2 text-sm", className)}>
|
||||
<a
|
||||
href={!isList ? feed.siteUrl || void 0 : `${WEB_URL}/list/${feed.id}`}
|
||||
target="_blank"
|
||||
className="flex items-center"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<a href={siteLink} target="_blank" className="flex items-center" rel="noreferrer">
|
||||
<FeedIcon
|
||||
feed={feed}
|
||||
fallbackUrl={docs}
|
||||
|
|
@ -33,7 +54,7 @@ export function FollowSummary({
|
|||
/>
|
||||
<div className="truncate text-base font-semibold leading-tight">
|
||||
<div className="flex items-center">
|
||||
{feed.title}
|
||||
{feedTitle}
|
||||
<FeedCertification className="center" feed={feed} />
|
||||
</div>
|
||||
<EllipsisHorizontalTextWithTooltip className="truncate text-xs font-normal text-zinc-500">
|
||||
|
|
@ -43,15 +64,8 @@ export function FollowSummary({
|
|||
</a>
|
||||
<div className="flex items-center gap-1 truncate text-zinc-500">
|
||||
<i className="i-mgc-right-cute-re shrink-0" />
|
||||
<a
|
||||
href={!isList ? feed.url || docs : `${WEB_URL}/list/${feed.id}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="truncate"
|
||||
>
|
||||
<EllipsisHorizontalTextWithTooltip>
|
||||
{`${!isList ? feed.url || docs : `list:${feed.id}`}`}
|
||||
</EllipsisHorizontalTextWithTooltip>
|
||||
<a href={feedLink} target="_blank" rel="noreferrer" className="truncate">
|
||||
<EllipsisHorizontalTextWithTooltip>{feedText}</EllipsisHorizontalTextWithTooltip>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -106,8 +106,8 @@ const ContextMenuCheckboxItem = React.forwardRef<
|
|||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex items-center justify-center">
|
||||
<ContextMenuPrimitive.ItemIndicator>
|
||||
<i className="i-mgc-check-filled size-4" />
|
||||
<ContextMenuPrimitive.ItemIndicator asChild>
|
||||
<i className="i-mgc-check-filled size-3" />
|
||||
</ContextMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -18,11 +18,11 @@ export const Input = forwardRef<
|
|||
ref={ref}
|
||||
className={cn(
|
||||
"min-w-0 flex-auto appearance-none rounded-lg text-sm",
|
||||
"bg-theme-background px-3 py-[calc(theme(spacing.2)-1px)] placeholder:text-zinc-400 dark:bg-zinc-700/[0.15]",
|
||||
"bg-theme-background px-3 py-[calc(theme(spacing.2)-1px)]",
|
||||
"ring-accent/20 duration-200 focus:border-accent/80 focus:outline-none focus:ring-2",
|
||||
"focus:!bg-accent/5",
|
||||
"border border-border",
|
||||
"dark:text-zinc-200 dark:placeholder:text-zinc-500",
|
||||
"placeholder:text-theme-placeholder-text dark:bg-zinc-700/[0.15] dark:text-zinc-200",
|
||||
"hover:border-accent/60",
|
||||
props.type === "password" ? "font-mono placeholder:font-sans" : "font-sans",
|
||||
"w-full",
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ export const TextArea = forwardRef<
|
|||
"border-transparent",
|
||||
isFocus && "border-accent/80 ring-2",
|
||||
|
||||
"dark:text-zinc-200 dark:placeholder:text-zinc-500",
|
||||
"placeholder:text-theme-placeholder-text dark:text-zinc-200",
|
||||
wrapperClassName,
|
||||
)}
|
||||
onMouseMove={handleMouseMove}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import { createElement, Fragment, memo, useEffect, useMemo, useState } from "react"
|
||||
|
||||
import type { MediaInfo } from "~/lib/parse-html"
|
||||
import { parseHtml } from "~/lib/parse-html"
|
||||
import type { RemarkOptions } from "~/lib/parse-markdown"
|
||||
import { parseMarkdown } from "~/lib/parse-markdown"
|
||||
import { cn } from "~/lib/utils"
|
||||
import { useWrappedElementSize } from "~/providers/wrapped-element-provider"
|
||||
|
||||
import type { MediaInfoRecord } from "../media"
|
||||
import { MediaContainerWidthProvider, MediaInfoRecordProvider } from "../media"
|
||||
import { MarkdownRenderContainerRefContext } from "./context"
|
||||
|
||||
export const Markdown: Component<
|
||||
|
|
@ -43,7 +45,7 @@ const HTMLImpl = <A extends keyof JSX.IntrinsicElements = "div">(
|
|||
|
||||
accessory?: React.ReactNode
|
||||
noMedia?: boolean
|
||||
mediaInfo?: MediaInfo
|
||||
mediaInfo?: MediaInfoRecord
|
||||
} & JSX.IntrinsicElements[A] &
|
||||
Partial<{
|
||||
renderInlineStyle: boolean
|
||||
|
|
@ -53,7 +55,6 @@ const HTMLImpl = <A extends keyof JSX.IntrinsicElements = "div">(
|
|||
const [remarkOptions, setRemarkOptions] = useState({
|
||||
renderInlineStyle,
|
||||
noMedia,
|
||||
mediaInfo,
|
||||
})
|
||||
const [shouldForceReMountKey, setShouldForceReMountKey] = useState(0)
|
||||
|
||||
|
|
@ -79,10 +80,16 @@ const HTMLImpl = <A extends keyof JSX.IntrinsicElements = "div">(
|
|||
[children, remarkOptions],
|
||||
)
|
||||
|
||||
const { w: containerWidth } = useWrappedElementSize()
|
||||
|
||||
if (!markdownElement) return null
|
||||
return (
|
||||
<MarkdownRenderContainerRefContext.Provider value={refElement}>
|
||||
{createElement(as, { ...rest, ref: setRefElement }, markdownElement)}
|
||||
<MediaContainerWidthProvider width={containerWidth}>
|
||||
<MediaInfoRecordProvider mediaInfo={mediaInfo}>
|
||||
{createElement(as, { ...rest, ref: setRefElement }, markdownElement)}
|
||||
</MediaInfoRecordProvider>
|
||||
</MediaContainerWidthProvider>
|
||||
{accessory && <Fragment key={shouldForceReMountKey}>{accessory}</Fragment>}
|
||||
</MarkdownRenderContainerRefContext.Provider>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import { cn } from "~/lib/utils"
|
||||
import { useEntryContentContext } from "~/modules/entry-content/hooks"
|
||||
import { useWrappedElementSize } from "~/providers/wrapped-element-provider"
|
||||
import { useEntry } from "~/store/entry"
|
||||
|
||||
import { Media } from "../../media"
|
||||
import { usePopulatedFullUrl } from "../utils"
|
||||
|
||||
export const MarkdownBlockImage = (
|
||||
props: React.ImgHTMLAttributes<HTMLImageElement> & {
|
||||
|
|
@ -12,18 +15,28 @@ export const MarkdownBlockImage = (
|
|||
},
|
||||
) => {
|
||||
const size = useWrappedElementSize()
|
||||
const { feedId } = useEntryContentContext()
|
||||
|
||||
const src = usePopulatedFullUrl(feedId, props.src)
|
||||
|
||||
const { entryId } = useEntryContentContext()
|
||||
const media = useEntry(entryId, (entry) => entry?.entries.media?.find((m) => m.url === props.src))
|
||||
|
||||
return (
|
||||
<Media
|
||||
type="photo"
|
||||
{...props}
|
||||
src={src}
|
||||
height={media?.height || props.height}
|
||||
width={media?.width || props.width}
|
||||
blurhash={media?.blurhash}
|
||||
mediaContainerClassName={cn(
|
||||
"rounded",
|
||||
size.w < Number.parseInt(props.width as string) && "w-full",
|
||||
)}
|
||||
showFallback
|
||||
popper
|
||||
className="flex justify-center"
|
||||
className="my-8 flex justify-center"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
import { cn } from "~/lib/utils"
|
||||
import { useEntryContentContext } from "~/modules/entry-content/hooks"
|
||||
|
||||
import { Media } from "../../media"
|
||||
import { usePopulatedFullUrl } from "../utils"
|
||||
|
||||
export const MarkdownInlineImage = (
|
||||
props: React.ImgHTMLAttributes<HTMLImageElement> & {
|
||||
proxy?: {
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
},
|
||||
) => {
|
||||
const { feedId } = useEntryContentContext()
|
||||
const populatedUrl = usePopulatedFullUrl(feedId, props.src)
|
||||
return (
|
||||
<Media
|
||||
type="photo"
|
||||
{...props}
|
||||
src={populatedUrl}
|
||||
mediaContainerClassName={cn("inline max-w-full rounded-md")}
|
||||
popper
|
||||
className="inline"
|
||||
showFallback
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,35 +1,14 @@
|
|||
import { useMemo } from "react"
|
||||
|
||||
import { FeedViewType } from "~/lib/enum"
|
||||
import { useEntryContentContext } from "~/modules/entry-content/hooks"
|
||||
import { useFeedByIdSelector } from "~/store/feed"
|
||||
|
||||
import type { LinkProps } from "../../link"
|
||||
import { Tooltip, TooltipContent, TooltipPortal, TooltipTrigger } from "../../tooltip"
|
||||
import { ensureAndRenderTimeStamp } from "../utils"
|
||||
import { ensureAndRenderTimeStamp, usePopulatedFullUrl } from "../utils"
|
||||
|
||||
const safeUrl = (url: string, baseUrl: string) => {
|
||||
try {
|
||||
return new URL(url, baseUrl).href
|
||||
} catch {
|
||||
return url
|
||||
}
|
||||
}
|
||||
export const MarkdownLink = (props: LinkProps) => {
|
||||
const { view, feedId } = useEntryContentContext()
|
||||
|
||||
const feedSiteUrl = useFeedByIdSelector(feedId, (feed) =>
|
||||
"siteUrl" in feed ? feed.siteUrl : undefined,
|
||||
)
|
||||
|
||||
const populatedFullHref = useMemo(() => {
|
||||
const { href } = props
|
||||
if (!href) return "#"
|
||||
|
||||
if (href.startsWith("http")) return href
|
||||
if (href.startsWith("/") && feedSiteUrl) return safeUrl(href, feedSiteUrl)
|
||||
return href
|
||||
}, [feedSiteUrl, props])
|
||||
const populatedFullHref = usePopulatedFullUrl(feedId, props.href)
|
||||
|
||||
const parseTimeStamp = view === FeedViewType.Audios
|
||||
if (parseTimeStamp) {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
import { useMemo } from "react"
|
||||
|
||||
import { useFeedByIdSelector } from "~/store/feed"
|
||||
|
||||
import { TimeStamp } from "../components/TimeStamp"
|
||||
|
||||
export const ensureAndRenderTimeStamp = (children: string) => {
|
||||
|
|
@ -35,3 +39,26 @@ export function timeStringToSeconds(time: string): number | null {
|
|||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const safeUrl = (url: string, baseUrl: string) => {
|
||||
try {
|
||||
return new URL(url, baseUrl).href
|
||||
} catch {
|
||||
return url
|
||||
}
|
||||
}
|
||||
export const usePopulatedFullUrl = (feedId: string, relativeUrl?: string) => {
|
||||
const feedSiteUrl = useFeedByIdSelector(feedId, (feed) =>
|
||||
"siteUrl" in feed ? feed.siteUrl : undefined,
|
||||
)
|
||||
|
||||
const populatedFullHref = useMemo(() => {
|
||||
if (!relativeUrl) return void 0
|
||||
|
||||
if (relativeUrl.startsWith("http")) return relativeUrl
|
||||
if (relativeUrl.startsWith("/") && feedSiteUrl) return safeUrl(relativeUrl, feedSiteUrl)
|
||||
return relativeUrl
|
||||
}, [feedSiteUrl, relativeUrl])
|
||||
|
||||
return populatedFullHref
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useForceUpdate } from "framer-motion"
|
||||
import type { FC, ImgHTMLAttributes, VideoHTMLAttributes } from "react"
|
||||
import { memo, useMemo, useState } from "react"
|
||||
import { createContext, memo, useContext, useMemo, useState } from "react"
|
||||
import { Blurhash, BlurhashCanvas } from "react-blurhash"
|
||||
import { useEventCallback } from "usehooks-ts"
|
||||
|
||||
import { nextFrame } from "~/lib/dom"
|
||||
|
|
@ -18,6 +19,8 @@ type BaseProps = {
|
|||
mediaContainerClassName?: string
|
||||
showFallback?: boolean
|
||||
thumbnail?: boolean
|
||||
|
||||
blurhash?: string
|
||||
}
|
||||
export type MediaProps = BaseProps &
|
||||
(
|
||||
|
|
@ -49,7 +52,15 @@ const MediaImpl: FC<MediaProps> = ({
|
|||
thumbnail,
|
||||
...props
|
||||
}) => {
|
||||
const { src, style, type, previewImageUrl, showFallback, ...rest } = props
|
||||
const { src, style, type, previewImageUrl, showFallback, blurhash, height, width, ...rest } =
|
||||
props
|
||||
|
||||
const ctxMediaInfo = useContext(MediaInfoRecordContext)
|
||||
const ctxHeight = ctxMediaInfo[src!]?.height
|
||||
const ctxWidth = ctxMediaInfo[src!]?.width
|
||||
|
||||
const finalHeight = height || ctxHeight
|
||||
const finalWidth = width || ctxWidth
|
||||
|
||||
const [imgSrc, setImgSrc] = useState(() =>
|
||||
proxy && src && !failedList.has(src)
|
||||
|
|
@ -61,6 +72,18 @@ const MediaImpl: FC<MediaProps> = ({
|
|||
: src,
|
||||
)
|
||||
|
||||
const previewImageSrc = useMemo(
|
||||
() =>
|
||||
proxy && previewImageUrl
|
||||
? getImageProxyUrl({
|
||||
url: previewImageUrl,
|
||||
width: proxy.width,
|
||||
height: proxy.height,
|
||||
})
|
||||
: previewImageUrl,
|
||||
[previewImageUrl, proxy],
|
||||
)
|
||||
|
||||
const [mediaLoadState, setMediaLoadState] = useState<"loading" | "loaded" | "error">("loading")
|
||||
const errorHandle: React.ReactEventHandler<HTMLImageElement> = useEventCallback((e) => {
|
||||
if (imgSrc !== props.src) {
|
||||
|
|
@ -77,12 +100,17 @@ const MediaImpl: FC<MediaProps> = ({
|
|||
const previewMedia = usePreviewMedia()
|
||||
const handleClick = useEventCallback((e: React.MouseEvent) => {
|
||||
if (popper && src) {
|
||||
const width = Number.parseInt(props.width as string)
|
||||
const height = Number.parseInt(props.height as string)
|
||||
previewMedia(
|
||||
[
|
||||
{
|
||||
url: src,
|
||||
type,
|
||||
fallbackUrl: imgSrc,
|
||||
blurhash: props.blurhash,
|
||||
width: width || undefined,
|
||||
height: height || undefined,
|
||||
},
|
||||
],
|
||||
0,
|
||||
|
|
@ -103,19 +131,24 @@ const MediaImpl: FC<MediaProps> = ({
|
|||
}
|
||||
})
|
||||
|
||||
const containerWidth = useMediaContainerWidth()
|
||||
|
||||
const InnerContent = useMemo(() => {
|
||||
switch (type) {
|
||||
case "photo": {
|
||||
return (
|
||||
<img
|
||||
height={finalHeight}
|
||||
width={finalWidth}
|
||||
{...(rest as ImgHTMLAttributes<HTMLImageElement>)}
|
||||
onError={errorHandle}
|
||||
className={cn(
|
||||
!(props.width || props.height) && "size-full",
|
||||
"cursor-card bg-gray-200 object-cover duration-200 dark:bg-neutral-800",
|
||||
"size-full object-contain",
|
||||
// "bg-gray-200 dark:bg-neutral-800",
|
||||
popper && "cursor-zoom-in",
|
||||
"duration-200",
|
||||
mediaLoadState === "loaded" ? "opacity-100" : "opacity-0",
|
||||
|
||||
"!my-0",
|
||||
mediaContainerClassName,
|
||||
)}
|
||||
src={imgSrc}
|
||||
|
|
@ -129,13 +162,13 @@ const MediaImpl: FC<MediaProps> = ({
|
|||
<span
|
||||
className={cn(
|
||||
"center",
|
||||
!(props.width || props.height) && "size-full",
|
||||
!(finalWidth || finalHeight) && "size-full",
|
||||
"relative cursor-card bg-stone-100 object-cover",
|
||||
mediaContainerClassName,
|
||||
)}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<VideoPreview src={src!} previewImageUrl={previewImageUrl} thumbnail={thumbnail} />
|
||||
<VideoPreview src={src!} previewImageUrl={previewImageSrc} thumbnail={thumbnail} />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
|
@ -149,11 +182,11 @@ const MediaImpl: FC<MediaProps> = ({
|
|||
handleOnLoad,
|
||||
imgSrc,
|
||||
mediaContainerClassName,
|
||||
finalHeight,
|
||||
finalWidth,
|
||||
mediaLoadState,
|
||||
popper,
|
||||
previewImageUrl,
|
||||
props.height,
|
||||
props.width,
|
||||
previewImageSrc,
|
||||
rest,
|
||||
src,
|
||||
thumbnail,
|
||||
|
|
@ -174,10 +207,25 @@ const MediaImpl: FC<MediaProps> = ({
|
|||
)
|
||||
} else {
|
||||
return (
|
||||
<div
|
||||
className={cn("rounded bg-zinc-100 dark:bg-neutral-900", className)}
|
||||
style={props.style}
|
||||
/>
|
||||
<div className={cn("relative rounded", className)} style={props.style}>
|
||||
<span
|
||||
className={cn(
|
||||
"relative inline-block max-w-full bg-theme-placeholder-image",
|
||||
mediaContainerClassName,
|
||||
)}
|
||||
style={{
|
||||
aspectRatio:
|
||||
props.height && props.width ? `${props.width} / ${props.height}` : undefined,
|
||||
width: props.width ? `${props.width}px` : "100%",
|
||||
}}
|
||||
>
|
||||
{props.blurhash && (
|
||||
<span className="absolute inset-0 overflow-hidden rounded">
|
||||
<BlurhashCanvas hash={props.blurhash} className="size-full" />
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -185,10 +233,29 @@ const MediaImpl: FC<MediaProps> = ({
|
|||
return (
|
||||
<span
|
||||
data-state={type !== "video" ? mediaLoadState : undefined}
|
||||
className={cn("block overflow-hidden rounded", className)}
|
||||
className={cn("relative block overflow-hidden rounded", className)}
|
||||
style={style}
|
||||
>
|
||||
{InnerContent}
|
||||
{!!props.width && !!props.height && !!containerWidth ? (
|
||||
<AspectRatio
|
||||
width={Number.parseInt(props.width as string)}
|
||||
height={Number.parseInt(props.height as string)}
|
||||
containerWidth={containerWidth}
|
||||
>
|
||||
<div className="absolute inset-0 flex items-center justify-center overflow-hidden rounded">
|
||||
{blurhash ? (
|
||||
<Blurhash hash={blurhash} width="100%" height="100%" />
|
||||
) : (
|
||||
<div className="size-full bg-border" />
|
||||
)}
|
||||
</div>
|
||||
<div className="absolute inset-0 flex items-center justify-center overflow-hidden rounded">
|
||||
{InnerContent}
|
||||
</div>
|
||||
</AspectRatio>
|
||||
) : (
|
||||
InnerContent
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
|
@ -201,7 +268,7 @@ const FallbackMedia: FC<MediaProps> = ({ type, mediaContainerClassName, classNam
|
|||
className={cn(
|
||||
!(props.width || props.height) && "size-full",
|
||||
"center rounded bg-zinc-100 dark:bg-neutral-900",
|
||||
"not-prose !flex max-h-full flex-col space-y-1 p-4",
|
||||
"not-prose !flex max-h-full flex-col space-y-1 p-4 @container",
|
||||
|
||||
mediaContainerClassName,
|
||||
)}
|
||||
|
|
@ -210,19 +277,56 @@ const FallbackMedia: FC<MediaProps> = ({ type, mediaContainerClassName, classNam
|
|||
width: props.width ? `${props.width}px` : "100%",
|
||||
}}
|
||||
>
|
||||
<i className="i-mgc-close-cute-re text-xl text-red-500" />
|
||||
<p>Media loaded failed</p>
|
||||
<div className="space-x-1 break-all px-4 text-sm">
|
||||
Go to{" "}
|
||||
<a href={props.src} target="_blank" rel="noreferrer" className="follow-link--underline">
|
||||
{props.src}
|
||||
</a>
|
||||
<i className="i-mgc-external-link-cute-re translate-y-px" />
|
||||
<div className="hidden @sm:hidden @md:contents">
|
||||
<i className="i-mgc-close-cute-re text-xl text-red-500" />
|
||||
<p>Media loaded failed</p>
|
||||
<div className="space-x-1 break-all px-4 text-sm">
|
||||
Go to{" "}
|
||||
<a href={props.src} target="_blank" rel="noreferrer" className="follow-link--underline">
|
||||
original media url
|
||||
</a>
|
||||
<i className="i-mgc-external-link-cute-re translate-y-0.5" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const AspectRatio = ({
|
||||
width,
|
||||
height,
|
||||
containerWidth,
|
||||
children,
|
||||
style,
|
||||
...props
|
||||
}: {
|
||||
width: number
|
||||
height: number
|
||||
containerWidth?: number
|
||||
children: React.ReactNode
|
||||
style?: React.CSSProperties
|
||||
[key: string]: any
|
||||
}) => {
|
||||
const scaleFactor = containerWidth && width ? Math.min(1, containerWidth / width) : 1
|
||||
|
||||
const scaledWidth = width ? width * scaleFactor : undefined
|
||||
const scaledHeight = height ? height * scaleFactor : undefined
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
width: scaledWidth ? `${scaledWidth}px` : "100%",
|
||||
height: scaledHeight ? `${scaledHeight}px` : "auto",
|
||||
...style,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const VideoPreview: FC<{
|
||||
src: string
|
||||
previewImageUrl?: string
|
||||
|
|
@ -275,3 +379,40 @@ const VideoPreview: FC<{
|
|||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const MediaContainerWidthContext = createContext<number>(0)
|
||||
export const MediaContainerWidthProvider = ({
|
||||
children,
|
||||
width,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
width: number
|
||||
}) => {
|
||||
return (
|
||||
<MediaContainerWidthContext.Provider value={width}>
|
||||
{children}
|
||||
</MediaContainerWidthContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const useMediaContainerWidth = () => {
|
||||
return useContext(MediaContainerWidthContext)
|
||||
}
|
||||
|
||||
export type MediaInfoRecord = Record<string, { width?: number; height?: number }>
|
||||
const MediaInfoRecordContext = createContext<MediaInfoRecord>({})
|
||||
|
||||
const noop = {} as const
|
||||
export const MediaInfoRecordProvider = ({
|
||||
children,
|
||||
mediaInfo,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
mediaInfo?: MediaInfoRecord
|
||||
}) => {
|
||||
return (
|
||||
<MediaInfoRecordContext.Provider value={mediaInfo || noop}>
|
||||
{children}
|
||||
</MediaInfoRecordContext.Provider>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@ export function SwipeMedia({
|
|||
previewImageUrl={med.preview_image_url}
|
||||
loading="lazy"
|
||||
proxy={proxySize}
|
||||
blurhash={med.blurhash}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onPreview?.(uniqMedia, i)
|
||||
|
|
@ -131,6 +132,9 @@ export function SwipeMedia({
|
|||
loading="lazy"
|
||||
proxy={proxySize}
|
||||
showFallback={true}
|
||||
height={uniqMedia[0].height}
|
||||
width={uniqMedia[0].width}
|
||||
blurhash={uniqMedia[0].blurhash}
|
||||
/>
|
||||
) : (
|
||||
<div className="relative flex aspect-video w-full items-center overflow-hidden rounded-t-2xl border-b">
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import type { MediaModel } from "@follow/shared/hono"
|
|||
import type { FC } from "react"
|
||||
import { Fragment, useCallback, useEffect, useRef, useState } from "react"
|
||||
import { Blurhash } from "react-blurhash"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Keyboard, Mousewheel } from "swiper/modules"
|
||||
import type { SwiperRef } from "swiper/react"
|
||||
import { Swiper, SwiperSlide } from "swiper/react"
|
||||
|
|
@ -17,6 +18,7 @@ import { EntryContent } from "~/modules/entry-content"
|
|||
import { ActionButton, MotionButtonBase } from "../button"
|
||||
import { microReboundPreset } from "../constants/spring"
|
||||
import { useCurrentModal } from "../modal"
|
||||
import { RootPortal } from "../portal"
|
||||
import { VideoPlayer } from "./VideoPlayer"
|
||||
|
||||
const Wrapper: Component<{
|
||||
|
|
@ -25,6 +27,7 @@ const Wrapper: Component<{
|
|||
entryId?: string
|
||||
}> = ({ children, src, showActions, entryId }) => {
|
||||
const { dismiss } = useCurrentModal()
|
||||
const { t } = useTranslation(["shortcuts", "external"])
|
||||
|
||||
return (
|
||||
<div className="center relative size-full px-20 pb-8 pt-10" onClick={dismiss}>
|
||||
|
|
@ -42,33 +45,35 @@ const Wrapper: Component<{
|
|||
)}
|
||||
>
|
||||
{children}
|
||||
<div
|
||||
className="absolute bottom-4 right-4 z-[99] flex gap-3 text-white/70 [&_button]:hover:text-white"
|
||||
onClick={stopPropagation}
|
||||
>
|
||||
{showActions && (
|
||||
<Fragment>
|
||||
{!!window.electron && (
|
||||
<RootPortal>
|
||||
<div
|
||||
className="fixed bottom-4 right-4 z-[99] flex gap-3 text-white/70 [&_button]:hover:text-white"
|
||||
onClick={stopPropagation}
|
||||
>
|
||||
{showActions && (
|
||||
<Fragment>
|
||||
{!!window.electron && (
|
||||
<ActionButton
|
||||
tooltip={t("external:header.download")}
|
||||
onClick={() => {
|
||||
tipcClient?.download(src)
|
||||
}}
|
||||
>
|
||||
<i className="i-mgc-download-2-cute-re" />
|
||||
</ActionButton>
|
||||
)}
|
||||
<ActionButton
|
||||
tooltip="Download"
|
||||
tooltip={t(COPY_MAP.OpenInBrowser())}
|
||||
onClick={() => {
|
||||
tipcClient?.download(src)
|
||||
window.open(src)
|
||||
}}
|
||||
>
|
||||
<i className="i-mgc-download-2-cute-re" />
|
||||
<i className="i-mgc-external-link-cute-re" />
|
||||
</ActionButton>
|
||||
)}
|
||||
<ActionButton
|
||||
tooltip={COPY_MAP.OpenInBrowser()}
|
||||
onClick={() => {
|
||||
window.open(src)
|
||||
}}
|
||||
>
|
||||
<i className="i-mgc-external-link-cute-re" />
|
||||
</ActionButton>
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
</RootPortal>
|
||||
</div>
|
||||
{entryId && (
|
||||
<div
|
||||
|
|
@ -226,6 +231,9 @@ export const PreviewMediaContent: FC<{
|
|||
alt="cover"
|
||||
src={med.url}
|
||||
loading="lazy"
|
||||
height={med.height}
|
||||
width={med.width}
|
||||
blurhash={med.blurhash}
|
||||
/>
|
||||
)}
|
||||
</SwiperSlide>
|
||||
|
|
@ -283,8 +291,9 @@ const FallbackableImage: FC<
|
|||
}, [currentSrc, currentState, fallbackUrl, src])
|
||||
|
||||
return (
|
||||
<div className={cn("flex size-full flex-col", containerClassName)}>
|
||||
<div className={cn("center flex size-full flex-col", containerClassName)}>
|
||||
{isLoading && !isAllError && (
|
||||
// FIXME: optimize this if image load, the placeholder background will flash
|
||||
<div className="center absolute inset-0 size-full">
|
||||
{blurhash ? (
|
||||
<div style={{ aspectRatio: `${props.width} / ${props.height}` }} className="w-full">
|
||||
|
|
@ -297,6 +306,7 @@ const FallbackableImage: FC<
|
|||
)}
|
||||
{!isAllError && (
|
||||
<img
|
||||
data-blurhash={blurhash}
|
||||
src={currentSrc}
|
||||
onLoad={() => setIsLoading(false)}
|
||||
onError={handleError}
|
||||
|
|
@ -307,6 +317,11 @@ const FallbackableImage: FC<
|
|||
blurhash && !isLoading ? "duration-500 ease-in-out animate-in fade-in-0" : "",
|
||||
props.className,
|
||||
)}
|
||||
style={{
|
||||
maxHeight: `min(100%, ${Number.parseInt(props.height as string)}px)`,
|
||||
maxWidth: `min(100%, ${Number.parseInt(props.width as string)}px)`,
|
||||
...props.style,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{isAllError && (
|
||||
|
|
@ -342,7 +357,7 @@ const FallbackableImage: FC<
|
|||
)}
|
||||
|
||||
{currentState === "fallback" && (
|
||||
<div className="mt-4 text-center text-xs">
|
||||
<div className="mt-4 text-center text-xs text-white/60">
|
||||
<span>
|
||||
This image is preview in low quality, because the original image is not available.
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -64,6 +64,12 @@ export const useModalStack = (options?: ModalStackOptions) => {
|
|||
}
|
||||
}
|
||||
const actions = {
|
||||
getTopModalStack() {
|
||||
return jotaiStore.get(modalStackAtom)[0]
|
||||
},
|
||||
getModalStackById(id: string) {
|
||||
return jotaiStore.get(modalStackAtom).find((item) => item.id === id)
|
||||
},
|
||||
dismiss(id: string) {
|
||||
jotaiStore.set(modalStackAtom, (p) => p.filter((item) => item.id !== id))
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import * as Dialog from "@radix-ui/react-dialog"
|
||||
import type { BoundingBox } from "framer-motion"
|
||||
import { useAnimationControls, useDragControls } from "framer-motion"
|
||||
import { produce } from "immer"
|
||||
import { useSetAtom } from "jotai"
|
||||
|
|
@ -23,13 +24,14 @@ import { AppErrorBoundary } from "~/components/common/AppErrorBoundary"
|
|||
import { SafeFragment } from "~/components/common/Fragment"
|
||||
import { m } from "~/components/common/Motion"
|
||||
import { ErrorComponentType } from "~/components/errors/enum"
|
||||
import { isElectronBuild } from "~/constants"
|
||||
import { ElECTRON_CUSTOM_TITLEBAR_HEIGHT, isElectronBuild } from "~/constants"
|
||||
import { useSwitchHotKeyScope } from "~/hooks/common"
|
||||
import { nextFrame, stopPropagation } from "~/lib/dom"
|
||||
import { cn } from "~/lib/utils"
|
||||
import { cn, getOS } from "~/lib/utils"
|
||||
|
||||
import { Divider } from "../../divider"
|
||||
import { RootPortalProvider } from "../../portal/provider"
|
||||
import { EllipsisHorizontalTextWithTooltip } from "../../typography"
|
||||
import { modalStackAtom } from "./atom"
|
||||
import { MODAL_STACK_Z_INDEX, modalMontionConfig } from "./constants"
|
||||
import type { CurrentModalContentProps, ModalActionsInternal } from "./context"
|
||||
|
|
@ -250,6 +252,17 @@ export const ModalInternal = memo(
|
|||
},
|
||||
[autoFocus],
|
||||
)
|
||||
|
||||
const measureDragConstraints = useCallback((constraints: BoundingBox) => {
|
||||
if (getOS() === "Windows") {
|
||||
return {
|
||||
...constraints,
|
||||
top: constraints.top + ElECTRON_CUSTOM_TITLEBAR_HEIGHT,
|
||||
}
|
||||
}
|
||||
return constraints
|
||||
}, [])
|
||||
|
||||
useImperativeHandle(ref, () => modalElementRef.current!)
|
||||
if (CustomModalComponent) {
|
||||
return (
|
||||
|
|
@ -339,6 +352,7 @@ export const ModalInternal = memo(
|
|||
dragListener={false}
|
||||
dragMomentum={false}
|
||||
dragConstraints={edgeElementRef}
|
||||
onMeasureDragConstraints={measureDragConstraints}
|
||||
whileDrag={{
|
||||
cursor: "grabbing",
|
||||
}}
|
||||
|
|
@ -352,17 +366,15 @@ export const ModalInternal = memo(
|
|||
className="flex grow flex-col"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex items-center",
|
||||
"max-h-[70vh] min-w-[300px] max-w-[90vw] lg:max-h-[calc(100vh-20rem)] lg:max-w-[70vw]",
|
||||
)}
|
||||
className={"relative flex items-center"}
|
||||
onPointerDownCapture={handleDrag}
|
||||
onPointerDown={handleResizeEnable}
|
||||
>
|
||||
<Dialog.Title className="flex shrink-0 grow items-center gap-2 px-4 py-1 text-lg font-semibold">
|
||||
<Dialog.Title className="flex w-0 max-w-full grow items-center gap-2 px-4 py-1 text-lg font-semibold">
|
||||
{icon && <span className="size-4">{icon}</span>}
|
||||
|
||||
<span>{title}</span>
|
||||
<EllipsisHorizontalTextWithTooltip className="truncate">
|
||||
<span>{title}</span>
|
||||
</EllipsisHorizontalTextWithTooltip>
|
||||
</Dialog.Title>
|
||||
{canClose && (
|
||||
<Dialog.DialogClose className="center p-2" tabIndex={1} onClick={close}>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ export const I18N_LOCALE_KEY = getStorageNS("I18N_LOCALE")
|
|||
export const ROUTE_FEED_PENDING = "all"
|
||||
export const ROUTE_ENTRY_PENDING = "pending"
|
||||
export const ROUTE_FEED_IN_FOLDER = "folder-"
|
||||
export const ROUTE_FEED_IN_LIST = "list-"
|
||||
export const ROUTE_FEED_IN_INBOX = "inbox-"
|
||||
|
||||
export const DAILY_CLAIM_AMOUNT = "20"
|
||||
export const INVITATION_PRICE = "100"
|
||||
|
|
|
|||
|
|
@ -2,10 +2,20 @@ import type { Transaction } from "dexie"
|
|||
import Dexie from "dexie"
|
||||
|
||||
import { LOCAL_DB_NAME } from "./constants"
|
||||
import { dbSchemaV1, dbSchemaV2, dbSchemaV3, dbSchemaV4, dbSchemaV5 } from "./db_schema"
|
||||
import {
|
||||
dbSchemaV1,
|
||||
dbSchemaV2,
|
||||
dbSchemaV3,
|
||||
dbSchemaV4,
|
||||
dbSchemaV5,
|
||||
dbSchemaV6,
|
||||
dbSchemaV7,
|
||||
} from "./db_schema"
|
||||
import type { DB_Cleaner } from "./schemas/cleaner"
|
||||
import type { DB_Entry, DB_EntryRelated } from "./schemas/entry"
|
||||
import type { DB_Feed, DB_FeedUnread } from "./schemas/feed"
|
||||
import type { DB_Inbox } from "./schemas/inbox"
|
||||
import type { DB_List } from "./schemas/list"
|
||||
import type { DB_Subscription } from "./schemas/subscription"
|
||||
|
||||
export interface LocalDBSchemaMap {
|
||||
|
|
@ -15,6 +25,8 @@ export interface LocalDBSchemaMap {
|
|||
entryRelated: DB_EntryRelated
|
||||
feedUnreads: DB_FeedUnread
|
||||
cleaner: DB_Cleaner
|
||||
lists: DB_List
|
||||
inboxes: DB_Inbox
|
||||
}
|
||||
|
||||
// Define a local DB
|
||||
|
|
@ -24,6 +36,8 @@ export class BrowserDB extends Dexie {
|
|||
public subscriptions: BrowserDBTable<"subscriptions">
|
||||
public entryRelated: BrowserDBTable<"entryRelated">
|
||||
public feedUnreads: BrowserDBTable<"feedUnreads">
|
||||
public lists: BrowserDBTable<"lists">
|
||||
public inboxes: BrowserDBTable<"inboxes">
|
||||
public cleaner: BrowserDBTable<"cleaner">
|
||||
|
||||
constructor() {
|
||||
|
|
@ -33,6 +47,8 @@ export class BrowserDB extends Dexie {
|
|||
this.version(3).stores(dbSchemaV3)
|
||||
this.version(4).stores(dbSchemaV4)
|
||||
this.version(5).stores(dbSchemaV5)
|
||||
this.version(6).stores(dbSchemaV6)
|
||||
this.version(7).stores(dbSchemaV7)
|
||||
|
||||
this.entries = this.table("entries")
|
||||
this.feeds = this.table("feeds")
|
||||
|
|
@ -40,6 +56,8 @@ export class BrowserDB extends Dexie {
|
|||
this.entryRelated = this.table("entryRelated")
|
||||
this.feedUnreads = this.table("feedUnreads")
|
||||
this.cleaner = this.table("cleaner")
|
||||
this.lists = this.table("lists")
|
||||
this.inboxes = this.table("inboxes")
|
||||
}
|
||||
|
||||
async upgradeToV2(trans: Transaction) {
|
||||
|
|
|
|||
|
|
@ -28,3 +28,13 @@ export const dbSchemaV5 = {
|
|||
...dbSchemaV4,
|
||||
cleaner: "&refId, visitedAt",
|
||||
}
|
||||
|
||||
export const dbSchemaV6 = {
|
||||
...dbSchemaV5,
|
||||
lists: "&id, title",
|
||||
}
|
||||
|
||||
export const dbSchemaV7 = {
|
||||
...dbSchemaV6,
|
||||
inboxes: "&id",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import type { FeedOrListRespModel } from "~/models"
|
||||
import type { FeedModel } from "~/models"
|
||||
|
||||
export type DB_FeedUnread = {
|
||||
id: string
|
||||
count: number
|
||||
}
|
||||
|
||||
export type DB_Feed = FeedOrListRespModel & { id: string }
|
||||
export type DB_Feed = FeedModel & { id: string }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
export type DB_Inbox = {
|
||||
id: string
|
||||
title: string
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
export type DB_List = {
|
||||
id: string
|
||||
title: string
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
description: string
|
||||
fee: number
|
||||
image: string
|
||||
ownerUserId: string
|
||||
timelineUpdatedAt: string
|
||||
feedIds: string[]
|
||||
}
|
||||
|
|
@ -13,6 +13,12 @@ import {
|
|||
setReadabilityStatus,
|
||||
} from "~/atoms/readability"
|
||||
import { useIntegrationSettingKey } from "~/atoms/settings/integration"
|
||||
import {
|
||||
setShowSourceContent,
|
||||
toggleShowSourceContent,
|
||||
useShowSourceContent,
|
||||
useSourceContentModal,
|
||||
} from "~/atoms/source-content"
|
||||
import { whoami } from "~/atoms/user"
|
||||
import { mountLottie } from "~/components/ui/lottie-container"
|
||||
import {
|
||||
|
|
@ -23,6 +29,7 @@ import {
|
|||
import { shortcuts } from "~/constants/shortcuts"
|
||||
import { tipcClient } from "~/lib/client"
|
||||
import { nextFrame } from "~/lib/dom"
|
||||
import { FeedViewType } from "~/lib/enum"
|
||||
import { getOS } from "~/lib/utils"
|
||||
import StarAnimationUri from "~/lottie/star.lottie?url"
|
||||
import type { CombinedEntryModel } from "~/models"
|
||||
|
|
@ -31,6 +38,8 @@ import type { FlatEntryModel } from "~/store/entry"
|
|||
import { entryActions } from "~/store/entry"
|
||||
import { useFeedById } from "~/store/feed"
|
||||
|
||||
import { navigateEntry } from "./useNavigateEntry"
|
||||
|
||||
const absoluteStarAnimationUri = new URL(StarAnimationUri, import.meta.url).href
|
||||
|
||||
export const useEntryReadabilityToggle = ({ id, url }: { id: string; url: string }) =>
|
||||
|
|
@ -97,12 +106,22 @@ export const useUnCollect = (entry: Nullable<CombinedEntryModel>) => {
|
|||
export const useRead = () =>
|
||||
useMutation({
|
||||
mutationFn: async (entry: Nullable<CombinedEntryModel>) =>
|
||||
entry && entryActions.markRead(entry.feeds.id, entry.entries.id, true),
|
||||
entry &&
|
||||
entryActions.markRead({
|
||||
feedId: entry.feeds.id,
|
||||
entryId: entry.entries.id,
|
||||
read: true,
|
||||
}),
|
||||
})
|
||||
export const useUnread = () =>
|
||||
useMutation({
|
||||
mutationFn: async (entry: Nullable<CombinedEntryModel>) =>
|
||||
entry && entryActions.markRead(entry.feeds.id, entry.entries.id, false),
|
||||
entry &&
|
||||
entryActions.markRead({
|
||||
feedId: entry.feeds.id,
|
||||
entryId: entry.entries.id,
|
||||
read: false,
|
||||
}),
|
||||
})
|
||||
|
||||
export const useEntryActions = ({
|
||||
|
|
@ -148,6 +167,9 @@ export const useEntryActions = ({
|
|||
entryId: populatedEntry?.entries.id ?? undefined,
|
||||
})
|
||||
|
||||
const showSourceContent = useShowSourceContent()
|
||||
const showSourceContentModal = useSourceContentModal()
|
||||
|
||||
const collect = useCollect(populatedEntry)
|
||||
const uncollect = useUnCollect(populatedEntry)
|
||||
const read = useRead()
|
||||
|
|
@ -339,15 +361,46 @@ export const useEntryActions = ({
|
|||
},
|
||||
{
|
||||
key: "openInBrowser",
|
||||
name: t("entry_actions.open_in_browser"),
|
||||
name: t("entry_actions.open_in_browser", {
|
||||
which: t(window.electron ? "words.browser" : "words.newTab"),
|
||||
}),
|
||||
shortcut: shortcuts.entry.openInBrowser.key,
|
||||
className: "i-mgc-world-2-cute-re",
|
||||
hide: !populatedEntry.entries.url,
|
||||
hide: type === "toolbar" || !populatedEntry.entries.url,
|
||||
onClick: () => {
|
||||
if (!populatedEntry.entries.url) return
|
||||
window.open(populatedEntry.entries.url, "_blank")
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "viewSourceContent",
|
||||
name: t("entry_actions.view_source_content"),
|
||||
// shortcut: shortcuts.entry.openInBrowser.key,
|
||||
className: !showSourceContent ? "i-mgc-world-2-cute-re" : tw`i-mgc-world-2-cute-fi`,
|
||||
hide: !populatedEntry.entries.url,
|
||||
active: showSourceContent,
|
||||
onClick: () => {
|
||||
if (!populatedEntry.entries.url) return
|
||||
const viewPreviewInModal = [
|
||||
FeedViewType.SocialMedia,
|
||||
FeedViewType.Videos,
|
||||
FeedViewType.Pictures,
|
||||
].includes(view!)
|
||||
if (viewPreviewInModal) {
|
||||
showSourceContentModal({
|
||||
title: populatedEntry.entries.title ?? undefined,
|
||||
src: populatedEntry.entries.url,
|
||||
})
|
||||
return
|
||||
}
|
||||
if (type === "toolbar") {
|
||||
toggleShowSourceContent()
|
||||
return
|
||||
}
|
||||
navigateEntry({ entryId: populatedEntry.entries.id })
|
||||
setShowSourceContent(true)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: t("entry_actions.share"),
|
||||
key: "share",
|
||||
|
|
@ -404,9 +457,12 @@ export const useEntryActions = ({
|
|||
instapaperPassword,
|
||||
instapaperUsername,
|
||||
feed?.ownerUserId,
|
||||
type,
|
||||
showSourceContent,
|
||||
openTipModal,
|
||||
collect,
|
||||
uncollect,
|
||||
showSourceContentModal,
|
||||
read,
|
||||
unread,
|
||||
])
|
||||
|
|
|
|||
|
|
@ -1,17 +1,26 @@
|
|||
import { WEB_URL } from "@follow/shared/constants"
|
||||
import { env } from "@follow/shared/env"
|
||||
import { useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { whoami } from "~/atoms/user"
|
||||
import { useModalStack } from "~/components/ui/modal"
|
||||
import type { NativeMenuItem } from "~/lib/native-menu"
|
||||
import type { FeedViewType } from "~/lib/enum"
|
||||
import type { NativeMenuItem, NullableNativeMenuItem } from "~/lib/native-menu"
|
||||
import { useFeedClaimModal } from "~/modules/claim"
|
||||
import { FeedForm } from "~/modules/discover/feed-form"
|
||||
import { Queries } from "~/queries"
|
||||
import { getFeedById, useAddFeedToFeedList, useFeedById } from "~/store/feed"
|
||||
import { InboxForm } from "~/modules/discover/inbox-form"
|
||||
import { ListForm } from "~/modules/discover/list-form"
|
||||
import {
|
||||
getFeedById,
|
||||
useAddFeedToFeedList,
|
||||
useFeedById,
|
||||
useRemoveFeedFromFeedList,
|
||||
} from "~/store/feed"
|
||||
import { useInboxById } from "~/store/inbox"
|
||||
import { useListById, useListByView } from "~/store/list"
|
||||
import { subscriptionActions, useSubscriptionByFeedId } from "~/store/subscription"
|
||||
|
||||
import { useAuthQuery } from "../common"
|
||||
import { useNavigateEntry } from "./useNavigateEntry"
|
||||
import { getRouteParams } from "./useRouteParams"
|
||||
import { useDeleteSubscription } from "./useSubscriptionActions"
|
||||
|
|
@ -38,25 +47,23 @@ export const useFeedActions = ({
|
|||
const navigateEntry = useNavigateEntry()
|
||||
const isEntryList = type === "entryList"
|
||||
|
||||
const listList = useAuthQuery(Queries.lists.list())
|
||||
const addMutation = useAddFeedToFeedList()
|
||||
const { mutateAsync: addFeedToListMutation } = useAddFeedToFeedList()
|
||||
const { mutateAsync: removeFeedFromListMutation } = useRemoveFeedFromFeedList()
|
||||
|
||||
const listByView = useListByView(view!)
|
||||
|
||||
const items = useMemo(() => {
|
||||
if (!feed) return []
|
||||
const isList = feed?.type === "list"
|
||||
|
||||
const items: NativeMenuItem[] = [
|
||||
...(!isList
|
||||
? [
|
||||
{
|
||||
type: "text" as const,
|
||||
label: t("sidebar.feed_actions.mark_all_as_read"),
|
||||
shortcut: "Meta+Shift+A",
|
||||
disabled: isEntryList,
|
||||
click: () => subscriptionActions.markReadByFeedIds({ feedIds: [feedId] }),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(!feed.ownerUserId && !!feed.id && !isList
|
||||
{
|
||||
type: "text" as const,
|
||||
label: t("sidebar.feed_actions.mark_all_as_read"),
|
||||
shortcut: "Meta+Shift+A",
|
||||
disabled: isEntryList,
|
||||
click: () => subscriptionActions.markReadByFeedIds({ feedIds: [feedId] }),
|
||||
},
|
||||
...(!feed.ownerUserId && !!feed.id && !false
|
||||
? [
|
||||
{
|
||||
type: "text" as const,
|
||||
|
|
@ -74,11 +81,7 @@ export const useFeedActions = ({
|
|||
? [
|
||||
{
|
||||
type: "text" as const,
|
||||
label: t(
|
||||
isList
|
||||
? "sidebar.feed_actions.list_owned_by_you"
|
||||
: "sidebar.feed_actions.feed_owned_by_you",
|
||||
),
|
||||
label: t("sidebar.feed_actions.feed_owned_by_you"),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
|
|
@ -86,41 +89,44 @@ export const useFeedActions = ({
|
|||
type: "separator" as const,
|
||||
disabled: isEntryList,
|
||||
},
|
||||
...(!isList
|
||||
? [
|
||||
{
|
||||
type: "text" as const,
|
||||
label: t("sidebar.feed_column.context_menu.add_feeds_to_list"),
|
||||
enabled: !!listList.data?.length,
|
||||
submenu: listList.data?.map((list) => ({
|
||||
label: list.title || "",
|
||||
type: "text" as const,
|
||||
click() {
|
||||
return addMutation.mutate({
|
||||
feedId,
|
||||
listId: list.id,
|
||||
})
|
||||
},
|
||||
})),
|
||||
{
|
||||
type: "text" as const,
|
||||
label: t("sidebar.feed_column.context_menu.add_feeds_to_list"),
|
||||
enabled: !!listByView?.length,
|
||||
submenu: listByView?.map((list) => {
|
||||
const isIncluded = list.feedIds.includes(feedId)
|
||||
return {
|
||||
label: list.title || "",
|
||||
type: "text" as const,
|
||||
checked: isIncluded,
|
||||
click() {
|
||||
if (!isIncluded) {
|
||||
addFeedToListMutation({
|
||||
feedId,
|
||||
listId: list.id,
|
||||
})
|
||||
} else {
|
||||
removeFeedFromListMutation({
|
||||
feedId,
|
||||
listId: list.id,
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "separator" as const,
|
||||
disabled: isEntryList,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
}
|
||||
}),
|
||||
},
|
||||
{
|
||||
type: "separator" as const,
|
||||
disabled: isEntryList,
|
||||
},
|
||||
{
|
||||
type: "text" as const,
|
||||
label: isEntryList ? t("sidebar.feed_actions.edit_feed") : t("sidebar.feed_actions.edit"),
|
||||
shortcut: "E",
|
||||
click: () => {
|
||||
present({
|
||||
title: isList
|
||||
? t("sidebar.feed_actions.edit_list")
|
||||
: t("sidebar.feed_actions.edit_feed"),
|
||||
content: ({ dismiss }) => (
|
||||
<FeedForm asWidget id={feedId} onSuccess={dismiss} isList={isList} />
|
||||
),
|
||||
title: t("sidebar.feed_actions.edit_feed"),
|
||||
content: ({ dismiss }) => <FeedForm asWidget id={feedId} onSuccess={dismiss} />,
|
||||
})
|
||||
},
|
||||
},
|
||||
|
|
@ -134,11 +140,7 @@ export const useFeedActions = ({
|
|||
},
|
||||
{
|
||||
type: "text" as const,
|
||||
label: t(
|
||||
isList
|
||||
? "sidebar.feed_actions.navigate_to_list"
|
||||
: "sidebar.feed_actions.navigate_to_feed",
|
||||
),
|
||||
label: t("sidebar.feed_actions.navigate_to_feed"),
|
||||
shortcut: "Meta+G",
|
||||
disabled: !isEntryList || getRouteParams().feedId === feedId,
|
||||
click: () => {
|
||||
|
|
@ -151,58 +153,46 @@ export const useFeedActions = ({
|
|||
},
|
||||
{
|
||||
type: "text" as const,
|
||||
label: t(
|
||||
isList
|
||||
? "sidebar.feed_actions.open_list_in_browser"
|
||||
: "sidebar.feed_actions.open_feed_in_browser",
|
||||
),
|
||||
label: t("sidebar.feed_actions.open_feed_in_browser", {
|
||||
which: t(window.electron ? "words.browser" : "words.newTab"),
|
||||
}),
|
||||
disabled: isEntryList,
|
||||
shortcut: "O",
|
||||
click: () =>
|
||||
window.open(
|
||||
isList
|
||||
? `${WEB_URL}/list/${feedId}?view=${view}`
|
||||
: `${WEB_URL}/feed/${feedId}?view=${view}`,
|
||||
"_blank",
|
||||
),
|
||||
click: () => window.open(`${WEB_URL}/feed/${feedId}?view=${view}`, "_blank"),
|
||||
},
|
||||
{
|
||||
type: "text" as const,
|
||||
label: t("sidebar.feed_actions.open_site_in_browser", {
|
||||
which: t(window.electron ? "words.browser" : "words.newTab"),
|
||||
}),
|
||||
shortcut: "Meta+O",
|
||||
disabled: isEntryList,
|
||||
click: () => {
|
||||
const feed = getFeedById(feedId)
|
||||
if (feed) {
|
||||
"siteUrl" in feed && feed.siteUrl && window.open(feed.siteUrl, "_blank")
|
||||
}
|
||||
},
|
||||
},
|
||||
...(!isList
|
||||
? [
|
||||
{
|
||||
type: "text" as const,
|
||||
label: t("sidebar.feed_actions.open_site_in_browser"),
|
||||
shortcut: "Meta+O",
|
||||
disabled: isEntryList,
|
||||
click: () => {
|
||||
const feed = getFeedById(feedId)
|
||||
if (feed) {
|
||||
"siteUrl" in feed && feed.siteUrl && window.open(feed.siteUrl, "_blank")
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
type: "separator",
|
||||
disabled: isEntryList,
|
||||
},
|
||||
{
|
||||
type: "text" as const,
|
||||
label: t(
|
||||
isList ? "sidebar.feed_actions.copy_list_url" : "sidebar.feed_actions.copy_feed_url",
|
||||
),
|
||||
label: t("sidebar.feed_actions.copy_feed_url"),
|
||||
disabled: isEntryList,
|
||||
shortcut: "Meta+C",
|
||||
click: () => {
|
||||
const url = isList ? `${WEB_URL}/list/${feedId}?view=${view}` : feed.url
|
||||
// @ts-expect-error
|
||||
const { url } = feed || {}
|
||||
if (!url) return
|
||||
navigator.clipboard.writeText(url)
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "text" as const,
|
||||
label: t(
|
||||
isList ? "sidebar.feed_actions.copy_list_id" : "sidebar.feed_actions.copy_feed_id",
|
||||
),
|
||||
label: t("sidebar.feed_actions.copy_feed_id"),
|
||||
shortcut: "Meta+Shift+C",
|
||||
disabled: isEntryList,
|
||||
click: () => {
|
||||
|
|
@ -213,17 +203,155 @@ export const useFeedActions = ({
|
|||
|
||||
return items
|
||||
}, [
|
||||
t,
|
||||
claimFeed,
|
||||
deleteSubscription,
|
||||
feed,
|
||||
feedId,
|
||||
t,
|
||||
isEntryList,
|
||||
navigateEntry,
|
||||
listByView,
|
||||
feedId,
|
||||
claimFeed,
|
||||
addFeedToListMutation,
|
||||
removeFeedFromListMutation,
|
||||
present,
|
||||
deleteSubscription,
|
||||
subscription,
|
||||
navigateEntry,
|
||||
view,
|
||||
])
|
||||
|
||||
return { items }
|
||||
}
|
||||
|
||||
export const useListActions = ({ listId, view }: { listId: string; view: FeedViewType }) => {
|
||||
const { t } = useTranslation()
|
||||
const list = useListById(listId)
|
||||
const subscription = useSubscriptionByFeedId(listId)
|
||||
|
||||
const { present } = useModalStack()
|
||||
const { mutateAsync: deleteSubscription } = useDeleteSubscription({})
|
||||
|
||||
const navigateEntry = useNavigateEntry()
|
||||
|
||||
const items = useMemo(() => {
|
||||
if (!list) return []
|
||||
|
||||
const items: NullableNativeMenuItem[] = [
|
||||
list.ownerUserId === whoami()?.id && {
|
||||
type: "text" as const,
|
||||
label: t("sidebar.feed_actions.list_owned_by_you"),
|
||||
},
|
||||
{
|
||||
type: "separator" as const,
|
||||
hide: list.ownerUserId !== whoami()?.id,
|
||||
},
|
||||
|
||||
{
|
||||
type: "text" as const,
|
||||
label: t("sidebar.feed_actions.edit"),
|
||||
shortcut: "E",
|
||||
click: () => {
|
||||
present({
|
||||
title: t("sidebar.feed_actions.edit_list"),
|
||||
content: ({ dismiss }) => <ListForm asWidget id={listId} onSuccess={dismiss} />,
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "text" as const,
|
||||
label: t("sidebar.feed_actions.unfollow"),
|
||||
shortcut: "Meta+Backspace",
|
||||
click: () => deleteSubscription(subscription),
|
||||
},
|
||||
{
|
||||
type: "text" as const,
|
||||
label: t("sidebar.feed_actions.navigate_to_list"),
|
||||
shortcut: "Meta+G",
|
||||
disabled: getRouteParams().feedId === listId,
|
||||
click: () => {
|
||||
navigateEntry({ listId })
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "separator" as const,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
type: "text" as const,
|
||||
label: t("sidebar.feed_actions.open_list_in_browser", {
|
||||
which: t(window.electron ? "words.browser" : "words.newTab"),
|
||||
}),
|
||||
disabled: false,
|
||||
shortcut: "O",
|
||||
click: () => window.open(`${WEB_URL}/list/${listId}?view=${view}`, "_blank"),
|
||||
},
|
||||
|
||||
{
|
||||
type: "separator",
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
type: "text" as const,
|
||||
label: t("sidebar.feed_actions.copy_list_url"),
|
||||
disabled: false,
|
||||
shortcut: "Meta+C",
|
||||
click: () => {
|
||||
const url = `${WEB_URL}/list/${listId}?view=${view}`
|
||||
navigator.clipboard.writeText(url)
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "text" as const,
|
||||
label: t("sidebar.feed_actions.copy_list_id"),
|
||||
shortcut: "Meta+Shift+C",
|
||||
disabled: false,
|
||||
click: () => {
|
||||
navigator.clipboard.writeText(listId)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
return items
|
||||
}, [list, t, present, deleteSubscription, subscription, navigateEntry, listId, view])
|
||||
|
||||
return { items }
|
||||
}
|
||||
|
||||
export const useInboxActions = ({ inboxId }: { inboxId: string }) => {
|
||||
const { t } = useTranslation()
|
||||
const inbox = useInboxById(inboxId)
|
||||
const { present } = useModalStack()
|
||||
|
||||
const items = useMemo(() => {
|
||||
if (!inbox) return []
|
||||
|
||||
const items: NativeMenuItem[] = [
|
||||
{
|
||||
type: "text" as const,
|
||||
label: t("sidebar.feed_actions.edit"),
|
||||
shortcut: "E",
|
||||
click: () => {
|
||||
present({
|
||||
title: t("sidebar.feed_actions.edit_inbox"),
|
||||
content: ({ dismiss }) => <InboxForm asWidget id={inboxId} onSuccess={dismiss} />,
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "separator" as const,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
type: "text" as const,
|
||||
label: t("sidebar.feed_actions.copy_email_address"),
|
||||
shortcut: "Meta+Shift+C",
|
||||
disabled: false,
|
||||
click: () => {
|
||||
navigator.clipboard.writeText(`${inboxId}${env.VITE_INBOXES_EMAIL}`)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
return items
|
||||
}, [inbox, t, inboxId, present])
|
||||
|
||||
return { items }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useCallback } from "react"
|
|||
import { useWhoami } from "~/atoms/user"
|
||||
import { useModalStack } from "~/components/ui/modal"
|
||||
import { FeedForm } from "~/modules/discover/feed-form"
|
||||
import { ListForm } from "~/modules/discover/list-form"
|
||||
|
||||
import { useI18n } from "../common"
|
||||
|
||||
|
|
@ -25,14 +26,12 @@ export const usePresentFeedFormModal = () => {
|
|||
const isList = !!("listId" in params)
|
||||
present({
|
||||
title: isList ? t("sidebar.feed_actions.edit_list") : t("sidebar.feed_actions.edit_feed"),
|
||||
content: ({ dismiss }) => (
|
||||
<FeedForm
|
||||
asWidget
|
||||
id={isList ? params.listId : params.feedId}
|
||||
isList={isList}
|
||||
onSuccess={dismiss}
|
||||
/>
|
||||
),
|
||||
content: ({ dismiss }) =>
|
||||
isList ? (
|
||||
<ListForm asWidget id={params.listId} onSuccess={dismiss} />
|
||||
) : (
|
||||
<FeedForm asWidget id={params.feedId} onSuccess={dismiss} />
|
||||
),
|
||||
})
|
||||
} else {
|
||||
window.location.href = `${DEEPLINK_SCHEME}add?id=${"feedId" in params ? params.feedId : params.listId}`
|
||||
|
|
|
|||
|
|
@ -2,7 +2,14 @@ import { isUndefined } from "lodash-es"
|
|||
|
||||
import { getReadonlyRoute, getStableRouterNavigate } from "~/atoms/route"
|
||||
import { setSidebarActiveView } from "~/atoms/sidebar"
|
||||
import { ROUTE_ENTRY_PENDING, ROUTE_FEED_IN_FOLDER, ROUTE_FEED_PENDING } from "~/constants"
|
||||
import { resetShowSourceContent } from "~/atoms/source-content"
|
||||
import {
|
||||
ROUTE_ENTRY_PENDING,
|
||||
ROUTE_FEED_IN_FOLDER,
|
||||
ROUTE_FEED_IN_INBOX,
|
||||
ROUTE_FEED_IN_LIST,
|
||||
ROUTE_FEED_PENDING,
|
||||
} from "~/constants"
|
||||
import { FeedViewType } from "~/lib/enum"
|
||||
|
||||
type NavigateEntryOptions = Partial<{
|
||||
|
|
@ -10,6 +17,8 @@ type NavigateEntryOptions = Partial<{
|
|||
entryId: string | null
|
||||
view: FeedViewType
|
||||
folderName: string
|
||||
inboxId: string
|
||||
listId: string
|
||||
}>
|
||||
/**
|
||||
* @description a hook to navigate to `feedId`, `entryId`, add search for `view`, `level`
|
||||
|
|
@ -18,7 +27,7 @@ type NavigateEntryOptions = Partial<{
|
|||
export const useNavigateEntry = () => navigateEntry
|
||||
|
||||
export const navigateEntry = (options: NavigateEntryOptions) => {
|
||||
const { entryId, feedId, view, folderName } = options || {}
|
||||
const { entryId, feedId, view, folderName, inboxId, listId } = options || {}
|
||||
const { params, searchParams } = getReadonlyRoute()
|
||||
let finalFeedId = feedId || params.feedId || ROUTE_FEED_PENDING
|
||||
|
||||
|
|
@ -30,6 +39,14 @@ export const navigateEntry = (options: NavigateEntryOptions) => {
|
|||
finalFeedId = `${ROUTE_FEED_IN_FOLDER}${folderName}`
|
||||
}
|
||||
|
||||
if (inboxId) {
|
||||
finalFeedId = `${ROUTE_FEED_IN_INBOX}${inboxId}`
|
||||
}
|
||||
|
||||
if (listId) {
|
||||
finalFeedId = `${ROUTE_FEED_IN_LIST}${listId}`
|
||||
}
|
||||
|
||||
finalFeedId = encodeURIComponent(finalFeedId)
|
||||
const nextSearchParams = new URLSearchParams(searchParams)
|
||||
|
||||
|
|
@ -37,6 +54,7 @@ export const navigateEntry = (options: NavigateEntryOptions) => {
|
|||
nextSearchParams.set("view", view.toString())
|
||||
setSidebarActiveView(view)
|
||||
}
|
||||
resetShowSourceContent()
|
||||
|
||||
const finalView = nextSearchParams.get("view")
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import {
|
|||
FEED_COLLECTION_LIST,
|
||||
ROUTE_ENTRY_PENDING,
|
||||
ROUTE_FEED_IN_FOLDER,
|
||||
ROUTE_FEED_IN_INBOX,
|
||||
ROUTE_FEED_IN_LIST,
|
||||
ROUTE_FEED_PENDING,
|
||||
} from "~/constants"
|
||||
import { FeedViewType } from "~/lib/enum"
|
||||
|
|
@ -42,8 +44,9 @@ export interface BizRouteParams {
|
|||
isCollection: boolean
|
||||
isAllFeeds: boolean
|
||||
isPendingEntry: boolean
|
||||
|
||||
folderName?: string
|
||||
inboxId?: string
|
||||
listId?: string
|
||||
}
|
||||
|
||||
const parseRouteParams = (params: Params<any>, search: URLSearchParams): BizRouteParams => {
|
||||
|
|
@ -64,6 +67,12 @@ const parseRouteParams = (params: Params<any>, search: URLSearchParams): BizRout
|
|||
folderName: params.feedId?.startsWith(ROUTE_FEED_IN_FOLDER)
|
||||
? params.feedId.slice(ROUTE_FEED_IN_FOLDER.length)
|
||||
: undefined,
|
||||
inboxId: params.feedId?.startsWith(ROUTE_FEED_IN_INBOX)
|
||||
? params.feedId.slice(ROUTE_FEED_IN_INBOX.length)
|
||||
: undefined,
|
||||
listId: params.feedId?.startsWith(ROUTE_FEED_IN_LIST)
|
||||
? params.feedId.slice(ROUTE_FEED_IN_LIST.length)
|
||||
: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,20 +3,10 @@ import { initializeDefaultIntegrationSettings } from "~/atoms/settings/integrati
|
|||
import { initializeDefaultUISettings } from "~/atoms/settings/ui"
|
||||
import { appLog } from "~/lib/log"
|
||||
import { sleep } from "~/lib/utils"
|
||||
import {
|
||||
EntryRelatedKey,
|
||||
EntryRelatedService,
|
||||
EntryService,
|
||||
FeedService,
|
||||
FeedUnreadService,
|
||||
SubscriptionService,
|
||||
} from "~/services"
|
||||
import type { FlatEntryModel } from "~/store/entry"
|
||||
|
||||
import { entryActions, useEntryStore } from "../store/entry/store"
|
||||
import { feedActions, useFeedStore } from "../store/feed"
|
||||
import { subscriptionActions } from "../store/subscription"
|
||||
import { feedUnreadActions } from "../store/unread"
|
||||
import { EntryService, FeedService, FeedUnreadService, SubscriptionService } from "~/services"
|
||||
import { InboxService } from "~/services/inbox"
|
||||
import type { Hydable } from "~/services/interface"
|
||||
import { ListService } from "~/services/list"
|
||||
|
||||
export const setHydrated = (v: boolean) => {
|
||||
window.__dbIsReady = v
|
||||
|
|
@ -25,7 +15,16 @@ export const setHydrated = (v: boolean) => {
|
|||
export const hydrateDatabaseToStore = async () => {
|
||||
async function hydrate() {
|
||||
const now = Date.now()
|
||||
await Promise.all([hydrateFeed(), hydrateSubscription(), hydrateFeedUnread(), hydrateEntry()])
|
||||
|
||||
const hydrates: Hydable[] = [
|
||||
FeedService,
|
||||
SubscriptionService,
|
||||
FeedUnreadService,
|
||||
EntryService,
|
||||
ListService,
|
||||
InboxService,
|
||||
]
|
||||
await Promise.all(hydrates.map((h) => h.hydrate()))
|
||||
|
||||
window.__dbIsReady = true
|
||||
const costTime = Date.now() - now
|
||||
|
|
@ -40,60 +39,6 @@ export const hydrateDatabaseToStore = async () => {
|
|||
})
|
||||
}
|
||||
|
||||
async function hydrateFeed() {
|
||||
const feeds = await FeedService.findAll()
|
||||
feedActions.upsertMany(feeds)
|
||||
return useFeedStore.getState().feeds
|
||||
}
|
||||
|
||||
async function hydrateFeedUnread() {
|
||||
const unread = await FeedUnreadService.getAll()
|
||||
|
||||
return feedUnreadActions.hydrate(unread)
|
||||
}
|
||||
async function hydrateEntry() {
|
||||
const [entries, entryRelated, feedEntries, collections] = await Promise.all([
|
||||
EntryService.findAll(),
|
||||
|
||||
EntryRelatedService.findAll(EntryRelatedKey.READ),
|
||||
EntryRelatedService.findAll(EntryRelatedKey.FEED_ID),
|
||||
EntryRelatedService.findAll(EntryRelatedKey.COLLECTION),
|
||||
])
|
||||
|
||||
const storeValue = [] as FlatEntryModel[]
|
||||
for (const entry of entries) {
|
||||
const entryRelatedFeedId = entry.feedId || feedEntries[entry.id]
|
||||
if (!entryRelatedFeedId) {
|
||||
logHydrateError(`Entry ${entry.id} has no related feed id`)
|
||||
continue
|
||||
}
|
||||
|
||||
storeValue.push({
|
||||
entries: entry,
|
||||
feedId: entryRelatedFeedId,
|
||||
read: entryRelated[entry.id] || false,
|
||||
collections: collections[entry.id] as {
|
||||
createdAt: string
|
||||
},
|
||||
})
|
||||
}
|
||||
entryActions.hydrate(storeValue)
|
||||
useEntryStore.setState({
|
||||
starIds: new Set(Object.keys(collections)),
|
||||
})
|
||||
}
|
||||
|
||||
async function hydrateSubscription() {
|
||||
const subscriptions = await SubscriptionService.findAll()
|
||||
|
||||
subscriptionActions.upsertMany(subscriptions)
|
||||
}
|
||||
|
||||
const logHydrateError = (message: string) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.debug(`Hydrate error: ${message}, maybe local database data is dirty.`)
|
||||
}
|
||||
|
||||
export const hydrateSettings = () => {
|
||||
initializeDefaultUISettings()
|
||||
initializeDefaultGeneralSettings()
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { get } from "lodash-es"
|
|||
import { tipcClient } from "./client"
|
||||
import { getOS } from "./utils"
|
||||
|
||||
export type NativeMenuItem =
|
||||
export type NativeMenuItem = (
|
||||
| {
|
||||
type: "text"
|
||||
label: string
|
||||
|
|
@ -14,8 +14,12 @@ export type NativeMenuItem =
|
|||
shortcut?: string
|
||||
disabled?: boolean
|
||||
submenu?: NativeMenuItem[]
|
||||
checked?: boolean
|
||||
}
|
||||
| { type: "separator"; disabled?: boolean }
|
||||
) & { hide?: boolean }
|
||||
|
||||
export type NullableNativeMenuItem = NativeMenuItem | null | undefined | false | ""
|
||||
|
||||
function sortShortcutsString(shortcut: string) {
|
||||
const order = ["Shift", "Ctrl", "Alt", "Meta"]
|
||||
|
|
@ -31,7 +35,7 @@ function sortShortcutsString(shortcut: string) {
|
|||
return [...sortedModifiers, ...otherKeys].join("+")
|
||||
}
|
||||
export const showNativeMenu = async (
|
||||
items: Array<Nullable<NativeMenuItem | false>>,
|
||||
items: Array<NullableNativeMenuItem>,
|
||||
e?: MouseEvent | React.MouseEvent,
|
||||
) => {
|
||||
const nextItems = (items.filter(Boolean) as NativeMenuItem[]).map((item) => {
|
||||
|
|
@ -41,6 +45,10 @@ export const showNativeMenu = async (
|
|||
shortcut: item.shortcut ? sortShortcutsString(item.shortcut) : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
if (item.hide) {
|
||||
return []
|
||||
}
|
||||
return item
|
||||
}) as NativeMenuItem[]
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import { ShikiHighLighter } from "~/components/ui/code-highlighter"
|
|||
import { MarkdownBlockImage, MarkdownLink, MarkdownP } from "~/components/ui/markdown/renderers"
|
||||
import { BlockError } from "~/components/ui/markdown/renderers/BlockErrorBoundary"
|
||||
import { createHeadingRenderer } from "~/components/ui/markdown/renderers/Heading"
|
||||
import { MarkdownInlineImage } from "~/components/ui/markdown/renderers/InlineImage"
|
||||
import { Media } from "~/components/ui/media"
|
||||
|
||||
function markInlineImage(node?: Element) {
|
||||
|
|
@ -31,14 +32,11 @@ function markInlineImage(node?: Element) {
|
|||
}
|
||||
}
|
||||
|
||||
export type MediaInfo = Record<string, { width?: number; height?: number }>
|
||||
|
||||
export const parseHtml = (
|
||||
content: string,
|
||||
options?: Partial<{
|
||||
renderInlineStyle: boolean
|
||||
noMedia?: boolean
|
||||
mediaInfo?: MediaInfo
|
||||
}>,
|
||||
) => {
|
||||
const file = new VFile(content)
|
||||
|
|
@ -99,13 +97,7 @@ export const parseHtml = (
|
|||
markInlineImage(node)
|
||||
return createElement(MarkdownLink, { ...props } as any)
|
||||
},
|
||||
img: ({ ...props }) => {
|
||||
return createElement(Img, {
|
||||
...props,
|
||||
width: props.src ? options?.mediaInfo?.[props.src]?.width : props.width,
|
||||
height: props.src ? options?.mediaInfo?.[props.src]?.height : props.height,
|
||||
})
|
||||
},
|
||||
img: Img,
|
||||
|
||||
h1: createHeadingRenderer(1),
|
||||
h2: createHeadingRenderer(2),
|
||||
|
|
@ -216,19 +208,11 @@ const Img: Components["img"] = ({ node, ...props }) => {
|
|||
...props,
|
||||
proxy: { height: 0, width: 700 },
|
||||
}
|
||||
if (node?.properties.inline) {
|
||||
return createElement(Media, {
|
||||
type: "photo",
|
||||
...nextProps,
|
||||
|
||||
mediaContainerClassName: tw`max-w-full inline rounded-md`,
|
||||
popper: true,
|
||||
className: tw`inline`,
|
||||
showFallback: true,
|
||||
})
|
||||
}
|
||||
|
||||
return createElement(MarkdownBlockImage, nextProps)
|
||||
return createElement(
|
||||
node?.properties.inline ? MarkdownInlineImage : MarkdownBlockImage,
|
||||
nextProps,
|
||||
)
|
||||
}
|
||||
|
||||
function extractCodeFromHtml(htmlString: string) {
|
||||
|
|
|
|||
|
|
@ -18,20 +18,38 @@ export function clamp(value, min, max) {
|
|||
return Math.max(Math.min(max, value), min)
|
||||
}
|
||||
|
||||
export function getEntriesParams({ id, view }: { id?: number | string; view?: number }) {
|
||||
export function getEntriesParams({
|
||||
feedId,
|
||||
inboxId,
|
||||
listId,
|
||||
view,
|
||||
}: {
|
||||
feedId?: number | string
|
||||
inboxId?: number | string
|
||||
listId?: number | string
|
||||
view?: number
|
||||
}) {
|
||||
const params: {
|
||||
feedId?: string
|
||||
feedIdList?: string[]
|
||||
isCollection?: boolean
|
||||
withContent?: boolean
|
||||
inboxId?: string
|
||||
listId?: string
|
||||
} = {}
|
||||
if (id === FEED_COLLECTION_LIST) {
|
||||
params.isCollection = true
|
||||
} else if (id && id !== ROUTE_FEED_PENDING) {
|
||||
if (id.toString().includes(",")) {
|
||||
params.feedIdList = `${id}`.split(",")
|
||||
} else {
|
||||
params.feedId = `${id}`
|
||||
if (inboxId) {
|
||||
params.inboxId = `${inboxId}`
|
||||
} else if (listId) {
|
||||
params.listId = `${listId}`
|
||||
} else if (feedId) {
|
||||
if (feedId === FEED_COLLECTION_LIST) {
|
||||
params.isCollection = true
|
||||
} else if (feedId !== ROUTE_FEED_PENDING) {
|
||||
if (feedId.toString().includes(",")) {
|
||||
params.feedIdList = `${feedId}`.split(",")
|
||||
} else {
|
||||
params.feedId = `${feedId}`
|
||||
}
|
||||
}
|
||||
}
|
||||
if (view === FeedViewType.SocialMedia) {
|
||||
|
|
|
|||
|
|
@ -25,10 +25,13 @@ export type TransactionModel = ExtractBizResponse<
|
|||
|
||||
export type FeedModel = ExtractBizResponse<typeof apiClient.feeds.$get>["data"]["feed"]
|
||||
|
||||
type ListModelPoplutedFeeds = ExtractBizResponse<typeof apiClient.lists.$get>["data"]["list"]
|
||||
export type ListModelPoplutedFeeds = ExtractBizResponse<typeof apiClient.lists.$get>["data"]["list"]
|
||||
export type ListModel = Omit<ListModelPoplutedFeeds, "feeds">
|
||||
export type FeedOrListRespModel = FeedModel | ListModelPoplutedFeeds
|
||||
export type FeedOrListModel = FeedModel | ListModel
|
||||
|
||||
export type InboxModel = ExtractBizResponse<typeof apiClient.inboxes.$get>["data"]
|
||||
|
||||
export type FeedOrListRespModel = FeedModel | ListModelPoplutedFeeds | InboxModel
|
||||
export type FeedOrListModel = FeedModel | ListModel | InboxModel
|
||||
|
||||
export type EntryResponse = Exclude<
|
||||
Extract<ExtractBizResponse<typeof apiClient.entries.$get>, { code: 0 }>["data"],
|
||||
|
|
@ -36,7 +39,8 @@ export type EntryResponse = Exclude<
|
|||
>
|
||||
|
||||
export type EntriesResponse = Array<
|
||||
Exclude<Awaited<ReturnType<typeof apiClient.entries.$post>>["data"], undefined>
|
||||
| Exclude<Awaited<ReturnType<typeof apiClient.entries.$post>>["data"], undefined>
|
||||
| Exclude<Awaited<ReturnType<typeof apiClient.entries.inbox.$post>>["data"], undefined>
|
||||
>[number]
|
||||
|
||||
export type CombinedEntryModel = EntriesResponse[number] & {
|
||||
|
|
@ -84,3 +88,27 @@ export type MediaModel = Exclude<
|
|||
ExtractBizResponse<typeof apiClient.entries.$get>["data"],
|
||||
undefined
|
||||
>["entries"]["media"]
|
||||
|
||||
export type ActionsInput = {
|
||||
name: string
|
||||
condition: {
|
||||
field?: ActionFeedField
|
||||
operator?: ActionOperation
|
||||
value?: string
|
||||
}[]
|
||||
result: {
|
||||
translation?: string
|
||||
summary?: boolean
|
||||
readability?: boolean
|
||||
rewriteRules?: {
|
||||
from: string
|
||||
to: string
|
||||
}[]
|
||||
blockRules?: {
|
||||
field?: ActionEntryField
|
||||
operator?: ActionOperation
|
||||
value?: string | number
|
||||
}[]
|
||||
webhooks?: string[]
|
||||
}
|
||||
}[]
|
||||
|
|
|
|||
|
|
@ -184,6 +184,7 @@ export const DailyReportModalContent: Component<DailyReportContentProps> = ({
|
|||
}) => {
|
||||
const content = useQueryData({ endDate, startDate, view })
|
||||
const RelatedEntryLink = useState(() => createRelatedEntryLink("toast"))[0]
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className="center flex-col">
|
||||
{content.isLoading ? (
|
||||
|
|
@ -203,7 +204,7 @@ export const DailyReportModalContent: Component<DailyReportContentProps> = ({
|
|||
) : (
|
||||
<div className="center pointer-events-none absolute inset-0 translate-y-6 flex-col gap-4 opacity-80">
|
||||
<EmptyIcon />
|
||||
<p>No AI news found for this period.</p>
|
||||
<p>{t("ai_daily.no_found")}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -65,11 +65,12 @@ export const useParseDailyDate = (day: DayOf) => {
|
|||
|
||||
export const useAIDailyReportModal = () => {
|
||||
const { present } = useModalStack()
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useCallback(() => {
|
||||
present({
|
||||
content: () => <FeedDailyModalContent />,
|
||||
title: "AI Daily Report",
|
||||
title: t("ai_daily.header"),
|
||||
resizeable: true,
|
||||
clickOutsideToDismiss: true,
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import { FeedViewType } from "~/lib/enum"
|
|||
import { getFetchErrorMessage, toastFetchError } from "~/lib/error-parser"
|
||||
import { getNewIssueUrl } from "~/lib/issues"
|
||||
import { cn } from "~/lib/utils"
|
||||
import type { FeedModel } from "~/models"
|
||||
import { feed as feedQuery, useFeed } from "~/queries/feed"
|
||||
import { subscription as subscriptionQuery } from "~/queries/subscriptions"
|
||||
import { useFeedByIdOrUrl } from "~/store/feed"
|
||||
|
|
@ -47,18 +48,15 @@ const formSchema = z.object({
|
|||
})
|
||||
|
||||
const defaultValue = { view: FeedViewType.Articles.toString() } as z.infer<typeof formSchema>
|
||||
|
||||
export const FeedForm: Component<{
|
||||
url?: string
|
||||
id?: string
|
||||
isList?: boolean
|
||||
|
||||
defaultValues?: z.infer<typeof formSchema>
|
||||
|
||||
asWidget?: boolean
|
||||
|
||||
onSuccess?: () => void
|
||||
}> = ({ id: _id, defaultValues = defaultValue, url, asWidget, onSuccess, isList }) => {
|
||||
const queryParams = { id: _id, url, isList }
|
||||
}> = ({ id: _id, defaultValues = defaultValue, url, asWidget, onSuccess }) => {
|
||||
const queryParams = { id: _id, url }
|
||||
|
||||
const feedQuery = useFeed(queryParams)
|
||||
|
||||
|
|
@ -66,7 +64,7 @@ export const FeedForm: Component<{
|
|||
const feed = useFeedByIdOrUrl({
|
||||
id,
|
||||
url,
|
||||
})
|
||||
}) as FeedModel
|
||||
|
||||
const hasSub = useSubscriptionByFeedId(feed?.id || "")
|
||||
const isSubscribed = !!feedQuery.data?.subscription || hasSub
|
||||
|
|
@ -96,6 +94,7 @@ export const FeedForm: Component<{
|
|||
asWidget,
|
||||
onSuccess,
|
||||
subscriptionData: feedQuery.data?.subscription,
|
||||
feed,
|
||||
}}
|
||||
/>
|
||||
) : feedQuery.isLoading ? (
|
||||
|
|
@ -162,13 +161,12 @@ export const FeedForm: Component<{
|
|||
const FeedInnerForm = ({
|
||||
defaultValues,
|
||||
id,
|
||||
url,
|
||||
asWidget,
|
||||
onSuccess,
|
||||
subscriptionData,
|
||||
feed,
|
||||
}: {
|
||||
defaultValues?: z.infer<typeof formSchema>
|
||||
url?: string
|
||||
id?: string
|
||||
asWidget?: boolean
|
||||
onSuccess?: () => void
|
||||
|
|
@ -178,21 +176,15 @@ const FeedInnerForm = ({
|
|||
isPrivate?: boolean
|
||||
title?: string | null
|
||||
}
|
||||
feed: FeedModel
|
||||
}) => {
|
||||
const subscription = useSubscriptionByFeedId(id || "") || subscriptionData
|
||||
const isSubscribed = !!subscription
|
||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||
const feed = useFeedByIdOrUrl({ id, url })!
|
||||
const isList = feed?.type === "list"
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: isList
|
||||
? {
|
||||
...defaultValues,
|
||||
view: feed.view.toString(),
|
||||
}
|
||||
: defaultValues,
|
||||
defaultValues,
|
||||
})
|
||||
|
||||
const { setClickOutSideToDismiss, dismiss } = useCurrentModal()
|
||||
|
|
@ -213,12 +205,12 @@ const FeedInnerForm = ({
|
|||
const followMutation = useMutation({
|
||||
mutationFn: async (values: z.infer<typeof formSchema>) => {
|
||||
const body = {
|
||||
...(isList ? { listId: feed.id } : { url: feed.url }),
|
||||
url: feed.url,
|
||||
view: Number.parseInt(values.view),
|
||||
category: values.category,
|
||||
isPrivate: values.isPrivate,
|
||||
title: values.title,
|
||||
...(isSubscribed && !isList && { feedId: feed.id }),
|
||||
feedId: feed.id,
|
||||
}
|
||||
const $method = isSubscribed ? apiClient.subscriptions.$patch : apiClient.subscriptions.$post
|
||||
|
||||
|
|
@ -289,11 +281,7 @@ const FeedInnerForm = ({
|
|||
<FormItem>
|
||||
<FormLabel>{t("feed_form.view")}</FormLabel>
|
||||
|
||||
<ViewSelectorRadioGroup
|
||||
{...form.register("view")}
|
||||
disabled={isList}
|
||||
className={cn(isList && "opacity-60")}
|
||||
/>
|
||||
<ViewSelectorRadioGroup {...form.register("view")} />
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
|
|
@ -314,36 +302,34 @@ const FeedInnerForm = ({
|
|||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{!isList && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="category"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="category"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div>
|
||||
<FormLabel>{t("feed_form.category")}</FormLabel>
|
||||
<FormDescription>{t("feed_form.category_description")}</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<div>
|
||||
<FormLabel>{t("feed_form.category")}</FormLabel>
|
||||
<FormDescription>{t("feed_form.category_description")}</FormDescription>
|
||||
<Autocomplete
|
||||
maxHeight={window.innerHeight < 600 ? 120 : 240}
|
||||
portal
|
||||
suggestions={suggestions}
|
||||
{...(field as any)}
|
||||
onSuggestionSelected={(suggestion) => {
|
||||
if (suggestion) {
|
||||
field.onChange(suggestion.value)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<FormControl>
|
||||
<div>
|
||||
<Autocomplete
|
||||
maxHeight={window.innerHeight < 600 ? 120 : 240}
|
||||
portal
|
||||
suggestions={suggestions}
|
||||
{...(field as any)}
|
||||
onSuggestionSelected={(suggestion) => {
|
||||
if (suggestion) {
|
||||
field.onChange(suggestion.value)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="isPrivate"
|
||||
|
|
@ -365,18 +351,6 @@ const FeedInnerForm = ({
|
|||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{isList && !!feed.fee && !isSubscribed && (
|
||||
<div>
|
||||
<FormLabel className="flex items-center gap-1">
|
||||
{t("feed_form.fee")}{" "}
|
||||
<div className="ml-2 flex scale-[0.85] items-center gap-1">
|
||||
{feed.fee}
|
||||
<i className="i-mgc-power size-4 text-accent" />
|
||||
</div>
|
||||
</FormLabel>
|
||||
<FormDescription className="mt-0.5">{t("feed_form.fee_description")}</FormDescription>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-1 items-end justify-end gap-4">
|
||||
{isSubscribed && (
|
||||
<Button
|
||||
|
|
@ -391,13 +365,7 @@ const FeedInnerForm = ({
|
|||
</Button>
|
||||
)}
|
||||
<Button ref={buttonRef} type="submit" isLoading={followMutation.isPending}>
|
||||
{isSubscribed
|
||||
? t("feed_form.update")
|
||||
: isList && feed.fee
|
||||
? t("feed_form.follow_with_fee", {
|
||||
fee: feed.fee,
|
||||
})
|
||||
: t("feed_form.follow")}
|
||||
{isSubscribed ? t("feed_form.update") : t("feed_form.follow")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import type { FeedViewType } from "~/lib/enum"
|
|||
|
||||
import { FollowSummary } from "../../components/feed-summary"
|
||||
import { FeedForm } from "./feed-form"
|
||||
import { ListForm } from "./list-form"
|
||||
|
||||
const formSchema = z.object({
|
||||
keyword: z.string().min(1),
|
||||
|
|
@ -63,7 +64,7 @@ const info: Record<
|
|||
}
|
||||
|
||||
type DiscoverSearchData = Awaited<ReturnType<typeof apiClient.discover.$post>>["data"]
|
||||
export function DiscoverForm({ type }: { type: string }) {
|
||||
export function DiscoverForm({ type = "search" }: { type?: string }) {
|
||||
const { prefix, default: defaultValue } = info[type]
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
|
|
@ -320,21 +321,33 @@ const SearchCard: FC<{
|
|||
onClick={() => {
|
||||
present({
|
||||
title: "Add Feed",
|
||||
content: ({ dismiss }) => (
|
||||
<FeedForm
|
||||
asWidget
|
||||
url={item.feed?.url}
|
||||
id={item.feed?.id || item.list?.id}
|
||||
isList={!!item.list?.id}
|
||||
defaultValues={{
|
||||
view: getSidebarActiveView().toString(),
|
||||
}}
|
||||
onSuccess={() => {
|
||||
onSuccess(item)
|
||||
dismiss()
|
||||
}}
|
||||
/>
|
||||
),
|
||||
content: ({ dismiss }) =>
|
||||
item.list?.id ? (
|
||||
<ListForm
|
||||
asWidget
|
||||
id={item.list?.id}
|
||||
defaultValues={{
|
||||
view: getSidebarActiveView().toString(),
|
||||
}}
|
||||
onSuccess={() => {
|
||||
onSuccess(item)
|
||||
dismiss()
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<FeedForm
|
||||
asWidget
|
||||
url={item.feed?.url}
|
||||
id={item.feed?.id}
|
||||
defaultValues={{
|
||||
view: getSidebarActiveView().toString(),
|
||||
}}
|
||||
onSuccess={() => {
|
||||
onSuccess(item)
|
||||
dismiss()
|
||||
}}
|
||||
/>
|
||||
),
|
||||
})
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,202 @@
|
|||
import { env } from "@follow/shared/env"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useMutation } from "@tanstack/react-query"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { toast } from "sonner"
|
||||
import { z } from "zod"
|
||||
|
||||
import { FollowSummary } from "~/components/feed-summary"
|
||||
import { Logo } from "~/components/icons/logo"
|
||||
import { Button } from "~/components/ui/button"
|
||||
import { Card, CardHeader } from "~/components/ui/card"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "~/components/ui/form"
|
||||
import { Input } from "~/components/ui/input"
|
||||
import { apiClient } from "~/lib/api-fetch"
|
||||
import { FeedViewType } from "~/lib/enum"
|
||||
import { cn } from "~/lib/utils"
|
||||
import type { InboxModel } from "~/models"
|
||||
import { useInbox } from "~/queries/inboxes"
|
||||
import { useInboxById } from "~/store/inbox"
|
||||
import { subscriptionActions } from "~/store/subscription"
|
||||
|
||||
export const InboxForm: Component<{
|
||||
id?: string
|
||||
asWidget?: boolean
|
||||
onSuccess?: () => void
|
||||
}> = ({ id: _id, asWidget, onSuccess }) => {
|
||||
const queryParams = { id: _id }
|
||||
|
||||
const feedQuery = useInbox(queryParams)
|
||||
|
||||
const id = feedQuery.data?.id || _id
|
||||
const inbox = useInboxById(id)
|
||||
|
||||
const isSubscribed = true
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-full flex-col",
|
||||
asWidget ? "min-h-[210px] w-[550px] max-w-full" : "px-[18px] pb-[18px] pt-12",
|
||||
)}
|
||||
>
|
||||
{!asWidget && (
|
||||
<div className="mb-4 mt-2 flex items-center gap-2 text-[22px] font-bold">
|
||||
<Logo className="size-8" />
|
||||
{isSubscribed ? t("feed_form.update_follow") : t("feed_form.add_follow")}
|
||||
</div>
|
||||
)}
|
||||
<InboxInnerForm
|
||||
{...{
|
||||
onSuccess,
|
||||
inbox,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const inboxHandleSchema = z
|
||||
.string()
|
||||
.min(3)
|
||||
.max(32)
|
||||
.regex(/^[a-z0-9_-]+$/)
|
||||
|
||||
const formSchema = z.object({
|
||||
handle: inboxHandleSchema,
|
||||
title: z.string(),
|
||||
})
|
||||
|
||||
const InboxInnerForm = ({
|
||||
onSuccess,
|
||||
inbox,
|
||||
}: {
|
||||
onSuccess?: () => void
|
||||
inbox?: Nullable<InboxModel>
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
handle: inbox?.id,
|
||||
title: inbox?.title || "",
|
||||
},
|
||||
})
|
||||
|
||||
const mutationCreate = useMutation({
|
||||
mutationFn: async ({ handle, title }: { handle: string; title: string }) => {
|
||||
await apiClient.inboxes.$post({
|
||||
json: {
|
||||
handle,
|
||||
title,
|
||||
},
|
||||
})
|
||||
onSuccess?.()
|
||||
},
|
||||
onSuccess: (_) => {
|
||||
subscriptionActions.fetchByView(FeedViewType.Articles)
|
||||
toast.success(t("discover.inbox_create_success"))
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(t("discover.inbox_create_error"))
|
||||
},
|
||||
})
|
||||
|
||||
const mutationChange = useMutation({
|
||||
mutationFn: async ({ handle, title }: { handle: string; title: string }) => {
|
||||
await apiClient.inboxes.$put({
|
||||
json: {
|
||||
handle,
|
||||
title,
|
||||
},
|
||||
})
|
||||
onSuccess?.()
|
||||
},
|
||||
onSuccess: () => {
|
||||
subscriptionActions.fetchByView(FeedViewType.Articles)
|
||||
toast.success(t("discover.inbox_update_success"))
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(t("discover.inbox_update_error"))
|
||||
},
|
||||
})
|
||||
|
||||
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
if (inbox) {
|
||||
mutationChange.mutate({ handle: values.handle, title: values.title })
|
||||
} else {
|
||||
mutationCreate.mutate({ handle: values.handle, title: values.title })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 flex-col gap-y-4">
|
||||
{inbox && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<FollowSummary feed={inbox} />
|
||||
</CardHeader>
|
||||
</Card>
|
||||
)}
|
||||
<>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className={cn("space-y-4")}
|
||||
data-testid="discover-form"
|
||||
>
|
||||
{!inbox && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="handle"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("discover.inbox_handle")}</FormLabel>
|
||||
<FormControl>
|
||||
<div className={cn("flex w-64 items-center gap-2")}>
|
||||
<Input autoFocus {...field} />
|
||||
<span className="text-zinc-500">{env.VITE_INBOXES_EMAIL}</span>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("discover.inbox_title")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className={cn("center flex justify-end gap-4")}
|
||||
data-testid="discover-form-actions"
|
||||
>
|
||||
<Button type="submit" isLoading={mutationCreate.isPending}>
|
||||
{t(inbox ? "discover.inbox_update" : "discover.inbox_create")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
import { env } from "@follow/shared/env"
|
||||
import { useMutation } from "@tanstack/react-query"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { toast } from "sonner"
|
||||
|
||||
import { Button } from "~/components/ui/button"
|
||||
import { CopyButton } from "~/components/ui/code-highlighter"
|
||||
import { useModalStack } from "~/components/ui/modal"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "~/components/ui/table"
|
||||
import { apiClient } from "~/lib/api-fetch"
|
||||
import { FeedViewType } from "~/lib/enum"
|
||||
import { useInboxList } from "~/queries/inboxes"
|
||||
import { subscriptionActions } from "~/store/subscription"
|
||||
|
||||
import { InboxForm } from "./inbox-form"
|
||||
|
||||
export function DiscoverInboxList() {
|
||||
const { t } = useTranslation()
|
||||
const inboxes = useInboxList()
|
||||
|
||||
const { present } = useModalStack()
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-4 flex items-center gap-2 text-sm text-zinc-500">
|
||||
<span>{t("discover.inbox.description")}</span>
|
||||
<a
|
||||
href="https://github.com/RSSNext/Follow/wiki/Inbox#webhooks"
|
||||
className="text-zinc-600 underline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t("discover.inbox.webhooks_docs")}
|
||||
</a>
|
||||
</div>
|
||||
<Table className="mb-8 w-[600px]">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="pl-0 pr-6">{t("discover.inbox.handle")}</TableHead>
|
||||
<TableHead className="pl-0 pr-6">{t("discover.inbox.email")}</TableHead>
|
||||
<TableHead className="pl-0 pr-6">{t("discover.inbox.title")}</TableHead>
|
||||
<TableHead className="pl-0 pr-6">{t("discover.inbox.secret")}</TableHead>
|
||||
<TableHead className="px-0">{t("discover.inbox.actions")}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{inboxes.data?.map((inbox) => (
|
||||
<TableRow key={inbox.id}>
|
||||
<TableCell size="sm">{inbox.id}</TableCell>
|
||||
<TableCell size="sm">
|
||||
<div className="group relative flex w-fit items-center gap-2">
|
||||
<span className="shrink-0">
|
||||
{inbox.id}
|
||||
{env.VITE_INBOXES_EMAIL}
|
||||
</span>
|
||||
<CopyButton
|
||||
value={`${inbox.id}${env.VITE_INBOXES_EMAIL}`}
|
||||
className="absolute -right-6 p-1 opacity-0 group-hover:opacity-100 [&_i]:size-3"
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell size="sm">{inbox.title}</TableCell>
|
||||
<TableCell size="sm">
|
||||
<div className="group relative flex w-fit items-center gap-2 font-mono">
|
||||
<span className="shrink-0">****</span>
|
||||
<CopyButton
|
||||
value={inbox.secret}
|
||||
className="absolute -right-6 p-1 opacity-0 group-hover:opacity-100 [&_i]:size-3"
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell size="sm">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
present({
|
||||
title: t("discover.inbox_destroy_confirm"),
|
||||
content: ({ dismiss }) => (
|
||||
<ConfirmDestroyModalContent
|
||||
id={inbox.id}
|
||||
onSuccess={() => {
|
||||
inboxes.refetch()
|
||||
dismiss()
|
||||
}}
|
||||
/>
|
||||
),
|
||||
})
|
||||
}
|
||||
>
|
||||
<i className="i-mgc-delete-2-cute-re" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
present({
|
||||
title: t("sidebar.feed_actions.edit_inbox"),
|
||||
content: ({ dismiss }) => (
|
||||
<InboxForm asWidget id={inbox.id} onSuccess={dismiss} />
|
||||
),
|
||||
})
|
||||
}}
|
||||
>
|
||||
<i className="i-mgc-edit-cute-re" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className="center flex">
|
||||
<Button
|
||||
className="flex items-center gap-2"
|
||||
onClick={() =>
|
||||
present({
|
||||
title: t("sidebar.feed_actions.new_inbox"),
|
||||
content: ({ dismiss }) => (
|
||||
<InboxForm
|
||||
asWidget
|
||||
onSuccess={() => {
|
||||
inboxes.refetch()
|
||||
dismiss()
|
||||
}}
|
||||
/>
|
||||
),
|
||||
})
|
||||
}
|
||||
>
|
||||
<i className="i-mgc-add-cute-re" />
|
||||
{t("discover.inbox_create")}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const ConfirmDestroyModalContent = ({ id, onSuccess }: { id: string; onSuccess: () => void }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const mutationDestroy = useMutation({
|
||||
mutationFn: async (id: string) => {
|
||||
await apiClient.inboxes.$delete({
|
||||
json: {
|
||||
handle: id,
|
||||
},
|
||||
})
|
||||
},
|
||||
onSuccess: () => {
|
||||
subscriptionActions.fetchByView(FeedViewType.Articles)
|
||||
toast.success(t("discover.inbox_destroy_success"))
|
||||
onSuccess()
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(t("discover.inbox_destroy_error"))
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="w-[512px]">
|
||||
<div className="mb-4 text-red-600">{t("discover.inbox_destroy_warning")}</div>
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={() => mutationDestroy.mutate(id)}>{t("words.confirm")}</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,356 @@
|
|||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useMutation } from "@tanstack/react-query"
|
||||
import { useEffect, useRef } from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { toast } from "sonner"
|
||||
import { z } from "zod"
|
||||
|
||||
import { FollowSummary } from "~/components/feed-summary"
|
||||
import { Logo } from "~/components/icons/logo"
|
||||
import { Button } from "~/components/ui/button"
|
||||
import { Card, CardHeader } from "~/components/ui/card"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "~/components/ui/form"
|
||||
import { Input } from "~/components/ui/input"
|
||||
import { LoadingCircle } from "~/components/ui/loading"
|
||||
import { useCurrentModal } from "~/components/ui/modal"
|
||||
import { Switch } from "~/components/ui/switch"
|
||||
import { useI18n } from "~/hooks/common"
|
||||
import { apiClient } from "~/lib/api-fetch"
|
||||
import { tipcClient } from "~/lib/client"
|
||||
import { FeedViewType } from "~/lib/enum"
|
||||
import { getFetchErrorMessage, toastFetchError } from "~/lib/error-parser"
|
||||
import { getNewIssueUrl } from "~/lib/issues"
|
||||
import { cn } from "~/lib/utils"
|
||||
import type { ListModel } from "~/models"
|
||||
import { lists as listsQuery, useList } from "~/queries/lists"
|
||||
import { subscription as subscriptionQuery } from "~/queries/subscriptions"
|
||||
import { useListById } from "~/store/list"
|
||||
import { useSubscriptionByFeedId } from "~/store/subscription"
|
||||
import { feedUnreadActions } from "~/store/unread"
|
||||
|
||||
import { ViewSelectorRadioGroup } from "../shared/ViewSelectorRadioGroup"
|
||||
|
||||
const formSchema = z.object({
|
||||
view: z.string(),
|
||||
category: z.string().nullable().optional(),
|
||||
isPrivate: z.boolean().optional(),
|
||||
title: z.string().optional(),
|
||||
})
|
||||
|
||||
const defaultValue = { view: FeedViewType.Articles.toString() } as z.infer<typeof formSchema>
|
||||
|
||||
export const ListForm: Component<{
|
||||
id?: string
|
||||
|
||||
defaultValues?: z.infer<typeof formSchema>
|
||||
|
||||
asWidget?: boolean
|
||||
|
||||
onSuccess?: () => void
|
||||
}> = ({ id: _id, defaultValues = defaultValue, asWidget, onSuccess }) => {
|
||||
const queryParams = { id: _id }
|
||||
|
||||
const feedQuery = useList(queryParams)
|
||||
|
||||
const id = feedQuery.data?.list.id || _id
|
||||
const list = useListById(id)
|
||||
|
||||
const hasSub = useSubscriptionByFeedId(list?.id || "")
|
||||
const isSubscribed = !!feedQuery.data?.subscription || hasSub
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-full flex-col",
|
||||
asWidget ? "min-h-[420px] w-[550px] max-w-full" : "px-[18px] pb-[18px] pt-12",
|
||||
)}
|
||||
>
|
||||
{!asWidget && (
|
||||
<div className="mb-4 mt-2 flex items-center gap-2 text-[22px] font-bold">
|
||||
<Logo className="size-8" />
|
||||
{isSubscribed ? t("feed_form.update_follow") : t("feed_form.add_follow")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{list ? (
|
||||
<ListInnerForm
|
||||
{...{
|
||||
defaultValues,
|
||||
id,
|
||||
asWidget,
|
||||
onSuccess,
|
||||
subscriptionData: feedQuery.data?.subscription,
|
||||
list,
|
||||
}}
|
||||
/>
|
||||
) : feedQuery.isLoading ? (
|
||||
<div className="flex flex-1 items-center justify-center">
|
||||
<LoadingCircle size="large" />
|
||||
</div>
|
||||
) : feedQuery.error ? (
|
||||
<div className="center grow flex-col gap-3">
|
||||
<i className="i-mgc-close-cute-re size-7 text-red-500" />
|
||||
<p>{t("feed_form.error_fetching_feed")}</p>
|
||||
<p className="cursor-text select-text break-all px-8 text-center">
|
||||
{getFetchErrorMessage(feedQuery.error)}
|
||||
</p>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
feedQuery.refetch()
|
||||
}}
|
||||
>
|
||||
{t("feed_form.retry")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
window.open(
|
||||
getNewIssueUrl({
|
||||
body: [
|
||||
"### Info:",
|
||||
"",
|
||||
"List ID:",
|
||||
"```",
|
||||
id,
|
||||
"```",
|
||||
"",
|
||||
"Error:",
|
||||
"```",
|
||||
getFetchErrorMessage(feedQuery.error),
|
||||
"```",
|
||||
].join("\n"),
|
||||
title: `Error in fetching list: ${id}`,
|
||||
}),
|
||||
"_blank",
|
||||
)
|
||||
}}
|
||||
>
|
||||
{t("feed_form.feedback")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="center h-full grow flex-col">
|
||||
<i className="i-mgc-question-cute-re mb-6 size-12 text-zinc-500" />
|
||||
<p>{t("feed_form.feed_not_found")}</p>
|
||||
<p>{id}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ListInnerForm = ({
|
||||
defaultValues,
|
||||
id,
|
||||
asWidget,
|
||||
onSuccess,
|
||||
subscriptionData,
|
||||
list,
|
||||
}: {
|
||||
defaultValues?: z.infer<typeof formSchema>
|
||||
id?: string
|
||||
asWidget?: boolean
|
||||
onSuccess?: () => void
|
||||
subscriptionData?: {
|
||||
view?: number
|
||||
category?: string | null
|
||||
isPrivate?: boolean
|
||||
title?: string | null
|
||||
}
|
||||
list: ListModel
|
||||
}) => {
|
||||
const subscription = useSubscriptionByFeedId(id || "") || subscriptionData
|
||||
const isSubscribed = !!subscription
|
||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
...defaultValues,
|
||||
view: list.view.toString(),
|
||||
},
|
||||
})
|
||||
|
||||
const { setClickOutSideToDismiss, dismiss } = useCurrentModal()
|
||||
|
||||
useEffect(() => {
|
||||
setClickOutSideToDismiss(!form.formState.isDirty)
|
||||
}, [form.formState.isDirty])
|
||||
|
||||
useEffect(() => {
|
||||
if (subscription) {
|
||||
form.setValue("view", `${subscription?.view}`)
|
||||
form.setValue("isPrivate", subscription?.isPrivate || false)
|
||||
form.setValue("title", subscription?.title || "")
|
||||
}
|
||||
}, [subscription])
|
||||
|
||||
const followMutation = useMutation({
|
||||
mutationFn: async (values: z.infer<typeof formSchema>) => {
|
||||
const body = {
|
||||
listId: list.id,
|
||||
view: Number.parseInt(values.view),
|
||||
category: values.category,
|
||||
isPrivate: values.isPrivate,
|
||||
title: values.title,
|
||||
}
|
||||
const $method = isSubscribed ? apiClient.subscriptions.$patch : apiClient.subscriptions.$post
|
||||
|
||||
return $method({
|
||||
json: body,
|
||||
})
|
||||
},
|
||||
onSuccess: (_, variables) => {
|
||||
if (isSubscribed && variables.view !== `${subscription?.view}`) {
|
||||
subscriptionQuery.byView(subscription?.view).invalidate()
|
||||
tipcClient?.invalidateQuery(subscriptionQuery.byView(subscription?.view).key)
|
||||
feedUnreadActions.fetchUnreadByView(subscription?.view)
|
||||
}
|
||||
subscriptionQuery.byView(Number.parseInt(variables.view)).invalidate()
|
||||
tipcClient?.invalidateQuery(subscriptionQuery.byView(Number.parseInt(variables.view)).key)
|
||||
feedUnreadActions.fetchUnreadByView(Number.parseInt(variables.view))
|
||||
|
||||
const listId = list.id
|
||||
if (listId) {
|
||||
listsQuery.byId({ id: listId }).invalidate()
|
||||
tipcClient?.invalidateQuery(listsQuery.byId({ id: listId }).key)
|
||||
}
|
||||
toast(isSubscribed ? t("feed_form.updated") : t("feed_form.followed"), {
|
||||
duration: 1000,
|
||||
})
|
||||
|
||||
if (!asWidget && !isSubscribed) {
|
||||
window.close()
|
||||
}
|
||||
|
||||
onSuccess?.()
|
||||
},
|
||||
async onError(err) {
|
||||
toastFetchError(err)
|
||||
},
|
||||
})
|
||||
|
||||
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
followMutation.mutate(values)
|
||||
}
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 flex-col gap-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<FollowSummary feed={list} />
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-1 flex-col gap-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="view"
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("feed_form.view")}</FormLabel>
|
||||
|
||||
<ViewSelectorRadioGroup
|
||||
{...form.register("view")}
|
||||
disabled={true}
|
||||
className="opacity-60"
|
||||
/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div>
|
||||
<FormLabel>{t("feed_form.title")}</FormLabel>
|
||||
<FormDescription>{t("feed_form.title_description")}</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="isPrivate"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<FormLabel>{t("feed_form.private_follow")}</FormLabel>
|
||||
<FormDescription>{t("feed_form.private_follow_description")}</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
className="shrink-0"
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{!!list.fee && !isSubscribed && (
|
||||
<div>
|
||||
<FormLabel className="flex items-center gap-1">
|
||||
{t("feed_form.fee")}{" "}
|
||||
<div className="ml-2 flex scale-[0.85] items-center gap-1">
|
||||
{list.fee}
|
||||
<i className="i-mgc-power size-4 text-accent" />
|
||||
</div>
|
||||
</FormLabel>
|
||||
<FormDescription className="mt-0.5">{t("feed_form.fee_description")}</FormDescription>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-1 items-end justify-end gap-4">
|
||||
{isSubscribed && (
|
||||
<Button
|
||||
type="button"
|
||||
ref={buttonRef}
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
dismiss()
|
||||
}}
|
||||
>
|
||||
{t.common("cancel")}
|
||||
</Button>
|
||||
)}
|
||||
<Button ref={buttonRef} type="submit" isLoading={followMutation.isPending}>
|
||||
{isSubscribed
|
||||
? t("feed_form.update")
|
||||
: list.fee
|
||||
? t("feed_form.follow_with_fee", {
|
||||
fee: list.fee,
|
||||
})
|
||||
: t("feed_form.follow")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -25,11 +25,20 @@ export const RecommendationCard: FC<RecommendationCardProps> = memo(({ data, rou
|
|||
<CardContent className="p-5 pt-0">
|
||||
<ul className="space-y-1 text-sm text-muted-foreground">
|
||||
{Object.keys(data.routes).map((route) => (
|
||||
<li key={route} className="duration-200 hover:text-theme-foreground-hover">
|
||||
<li
|
||||
key={route}
|
||||
className="hover:text-theme-foreground-hover"
|
||||
onClick={(e) => {
|
||||
;(e.target as HTMLElement).querySelector("button")?.click()
|
||||
}}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded p-0.5 duration-200 hover:bg-muted hover:px-1.5"
|
||||
onClick={() => {
|
||||
present({
|
||||
id: `recommendation-content-${route}`,
|
||||
content: () => (
|
||||
<RecommendationContent routePrefix={routePrefix} route={data.routes[route]} />
|
||||
),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
import { FeedViewType } from "~/lib/enum"
|
||||
|
||||
import { ArticleItem, ArticleItemSkeleton } from "./article-item"
|
||||
import { AudioItem, AudioItemSkeleton } from "./audio-item"
|
||||
import { NotificationItem, NotificationItemSkeleton } from "./notification-item"
|
||||
import { PictureItem, PictureItemSkeleton } from "./picture-item"
|
||||
import { SocialMediaItem, SocialMediaItemSkeleton } from "./social-media-item"
|
||||
import { VideoItem, VideoItemSkeleton } from "./video-item"
|
||||
|
||||
const ItemMap = {
|
||||
[FeedViewType.Articles]: ArticleItem,
|
||||
[FeedViewType.SocialMedia]: SocialMediaItem,
|
||||
[FeedViewType.Pictures]: PictureItem,
|
||||
[FeedViewType.Videos]: VideoItem,
|
||||
[FeedViewType.Audios]: AudioItem,
|
||||
[FeedViewType.Notifications]: NotificationItem,
|
||||
}
|
||||
export const getItemComponentByView = (view: FeedViewType) => {
|
||||
return ItemMap[view] || ArticleItem
|
||||
}
|
||||
|
||||
const SkeletonItemMap = {
|
||||
[FeedViewType.Articles]: ArticleItemSkeleton,
|
||||
[FeedViewType.SocialMedia]: SocialMediaItemSkeleton,
|
||||
[FeedViewType.Pictures]: PictureItemSkeleton,
|
||||
[FeedViewType.Videos]: VideoItemSkeleton,
|
||||
[FeedViewType.Audios]: AudioItemSkeleton,
|
||||
[FeedViewType.Notifications]: NotificationItemSkeleton,
|
||||
}
|
||||
|
||||
export const getSkeletonItemComponentByView = (view: FeedViewType) => {
|
||||
return SkeletonItemMap[view]
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import { useEventCallback } from "usehooks-ts"
|
|||
|
||||
import { useGeneralSettingKey } from "~/atoms/settings/general"
|
||||
import { Masonry } from "~/components/ui/Masonry"
|
||||
import { MediaContainerWidthProvider } from "~/components/ui/media"
|
||||
import { useScrollViewElement } from "~/components/ui/scroll-area/hooks"
|
||||
import { Skeleton } from "~/components/ui/skeleton"
|
||||
import { useRefValue } from "~/hooks/common"
|
||||
|
|
@ -235,16 +236,18 @@ export const PictureMasonry: FC<MasonryProps> = (props) => {
|
|||
<MasonryItemsAspectRatioContext.Provider value={masonryItemsRadio}>
|
||||
<MasonryItemsAspectRatioSetterContext.Provider value={setMasonryItemsRadio}>
|
||||
<MasonryIntersectionContext.Provider value={intersectionObserver}>
|
||||
<Masonry
|
||||
items={items}
|
||||
columnGutter={gutter}
|
||||
columnWidth={currentItemWidth}
|
||||
columnCount={currentColumn}
|
||||
overscanBy={2}
|
||||
render={render}
|
||||
onRender={handleRender}
|
||||
itemKey={itemKey}
|
||||
/>
|
||||
<MediaContainerWidthProvider width={currentItemWidth}>
|
||||
<Masonry
|
||||
items={items}
|
||||
columnGutter={gutter}
|
||||
columnWidth={currentItemWidth}
|
||||
columnCount={currentColumn}
|
||||
overscanBy={2}
|
||||
render={render}
|
||||
onRender={handleRender}
|
||||
itemKey={itemKey}
|
||||
/>
|
||||
</MediaContainerWidthProvider>
|
||||
</MasonryIntersectionContext.Provider>
|
||||
</MasonryItemsAspectRatioSetterContext.Provider>
|
||||
</MasonryItemsAspectRatioContext.Provider>
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ export const SocialMediaItem: EntryListItemFC = ({ entryId, entryPreview, transl
|
|||
>
|
||||
<FeedIcon fallback feed={feed} entry={entry.entries} size={32} className="mt-1" />
|
||||
<div ref={ref} className="ml-2 min-w-0 flex-1">
|
||||
<div className={cn("-mt-0.5 flex-1 text-sm", content && "line-clamp-[10]")}>
|
||||
<div className="-mt-0.5 flex-1 text-sm">
|
||||
<div className="w-[calc(100%-10rem)] space-x-1 leading-6">
|
||||
<span className="inline-flex items-center gap-1 text-base font-semibold">
|
||||
<span>{entry.entries.author || feed.title}</span>
|
||||
|
|
@ -137,7 +137,8 @@ export const SocialMediaItem: EntryListItemFC = ({ entryId, entryPreview, transl
|
|||
src={media.url}
|
||||
type={media.type}
|
||||
previewImageUrl={media.preview_image_url}
|
||||
className="size-28 shrink-0 data-[state=loading]:!bg-zinc-200 dark:data-[state=loading]:!bg-neutral-700"
|
||||
blurhash={media.blurhash}
|
||||
className="size-28 shrink-0 data-[state=loading]:!bg-theme-placeholder-image"
|
||||
loading="lazy"
|
||||
proxy={proxySize}
|
||||
onClick={(e) => {
|
||||
|
|
@ -146,6 +147,7 @@ export const SocialMediaItem: EntryListItemFC = ({ entryId, entryPreview, transl
|
|||
mediaList.map((m) => ({
|
||||
url: m.url,
|
||||
type: m.type,
|
||||
blurhash: m.blurhash,
|
||||
fallbackUrl:
|
||||
m.preview_image_url ?? getImageProxyUrl({ url: m.url, ...proxySize }),
|
||||
})),
|
||||
|
|
@ -204,9 +206,9 @@ const ActionBar = ({ entryId }: { entryId: string }) => {
|
|||
}
|
||||
|
||||
export const SocialMediaItemSkeleton = (
|
||||
<div className="relative m-auto w-[75ch] rounded-md bg-theme-background text-zinc-700 transition-colors dark:text-neutral-400">
|
||||
<div className="relative m-auto w-[645px] rounded-md bg-theme-background text-zinc-700 transition-colors dark:text-neutral-400">
|
||||
<div className="relative">
|
||||
<div className="group relative flex py-4 pl-3 pr-2">
|
||||
<div className="group relative flex px-8 py-6">
|
||||
<Skeleton className="mr-2 size-9" />
|
||||
<div className="ml-2 min-w-0 flex-1">
|
||||
<div className="-mt-0.5 line-clamp-5 flex-1 text-sm">
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { useRouteParams, useRouteParamsSelector } from "~/hooks/biz/useRoutePara
|
|||
import { useAuthQuery } from "~/hooks/common"
|
||||
import { entries, useEntries } from "~/queries/entries"
|
||||
import { entryActions, useEntryIdsByFeedIdOrView } from "~/store/entry"
|
||||
import { isListSubscription, useFolderFeedsByFeedId } from "~/store/subscription"
|
||||
import { useFolderFeedsByFeedId } from "~/store/subscription"
|
||||
import { feedUnreadActions } from "~/store/unread"
|
||||
|
||||
export const useEntryMarkReadHandler = (entriesIds: string[]) => {
|
||||
|
|
@ -59,8 +59,7 @@ export const useEntriesByView = ({
|
|||
const routeParams = useRouteParams()
|
||||
const unreadOnly = useGeneralSettingKey("unreadOnly")
|
||||
|
||||
const { feedId, view, isAllFeeds, isCollection } = routeParams
|
||||
const isList = isListSubscription(feedId)
|
||||
const { feedId, view, isAllFeeds, isCollection, listId, inboxId } = routeParams
|
||||
|
||||
const folderIds = useFolderFeedsByFeedId({
|
||||
feedId,
|
||||
|
|
@ -68,11 +67,12 @@ export const useEntriesByView = ({
|
|||
})
|
||||
|
||||
const entriesOptions = {
|
||||
id: folderIds?.join(",") || feedId,
|
||||
feedId: folderIds?.join(",") || feedId,
|
||||
inboxId,
|
||||
listId,
|
||||
view,
|
||||
...(unreadOnly === true && { read: false }),
|
||||
isArchived,
|
||||
isList,
|
||||
}
|
||||
const query = useEntries(entriesOptions)
|
||||
|
||||
|
|
@ -169,10 +169,10 @@ export const useEntriesByView = ({
|
|||
() =>
|
||||
isCollection
|
||||
? sortEntriesIdByStarAt(mergedEntries[view])
|
||||
: isList
|
||||
: listId
|
||||
? sortEntriesIdByEntryInsertedAt(mergedEntries[view])
|
||||
: sortEntriesIdByEntryPublishedAt(mergedEntries[view]),
|
||||
[isCollection, isList, mergedEntries, view],
|
||||
[isCollection, listId, mergedEntries, view],
|
||||
)
|
||||
|
||||
const groupByDate = useGeneralSettingKey("groupByDate")
|
||||
|
|
@ -192,7 +192,7 @@ export const useEntriesByView = ({
|
|||
continue
|
||||
}
|
||||
const date = new Date(
|
||||
isList ? entry.entries.insertedAt : entry.entries.publishedAt,
|
||||
listId ? entry.entries.insertedAt : entry.entries.publishedAt,
|
||||
).toDateString()
|
||||
if (date !== lastDate) {
|
||||
counts.push(1)
|
||||
|
|
@ -235,7 +235,7 @@ export function batchMarkRead(ids: string[]) {
|
|||
|
||||
if (batchLikeIds.length > 0) {
|
||||
for (const [feedId, id] of batchLikeIds) {
|
||||
entryActions.markRead(feedId, id, true)
|
||||
entryActions.markRead({ feedId, entryId: id, read: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ export interface MarkAllFilter {
|
|||
}
|
||||
export const useMarkAllByRoute = (filter?: MarkAllFilter) => {
|
||||
const routerParams = useRouteParams()
|
||||
const { feedId, view } = routerParams
|
||||
const { feedId, view, inboxId } = routerParams
|
||||
const folderIds = useFolderFeedsByFeedId({
|
||||
feedId,
|
||||
view,
|
||||
|
|
@ -21,6 +21,12 @@ export const useMarkAllByRoute = (filter?: MarkAllFilter) => {
|
|||
|
||||
if (typeof routerParams.feedId === "number" || routerParams.isAllFeeds) {
|
||||
subscriptionActions.markReadByView(view, filter)
|
||||
} else if (inboxId) {
|
||||
subscriptionActions.markReadByFeedIds({
|
||||
inboxId,
|
||||
view,
|
||||
filter,
|
||||
})
|
||||
} else if (folderIds) {
|
||||
subscriptionActions.markReadByFeedIds({
|
||||
feedIds: folderIds,
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ function EntryColumnImpl() {
|
|||
const feedId = activeEntry?.feedId
|
||||
if (!feedId) return
|
||||
|
||||
entryActions.markRead(feedId, activeEntryId, true)
|
||||
entryActions.markRead({ feedId, entryId: activeEntryId, read: true })
|
||||
}, [activeEntry?.feedId, activeEntryId, isCollection, isPendingEntry])
|
||||
|
||||
const isInteracted = useRef(false)
|
||||
|
|
@ -99,7 +99,7 @@ function EntryColumnImpl() {
|
|||
!isCollection &&
|
||||
routeFeedId !== ROUTE_FEED_PENDING &&
|
||||
entries.totalCount < 40 &&
|
||||
feed?.type !== "list"
|
||||
feed?.type === "feed"
|
||||
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
const virtuosoOptions = {
|
||||
|
|
@ -178,9 +178,10 @@ function EntryColumnImpl() {
|
|||
}
|
||||
data-total-count={virtuosoOptions.totalCount}
|
||||
>
|
||||
{virtuosoOptions.totalCount === 0 && !entries.isLoading && !entries.error && (
|
||||
<AddFeedHelper />
|
||||
)}
|
||||
{virtuosoOptions.totalCount === 0 &&
|
||||
!entries.isLoading &&
|
||||
!entries.error &&
|
||||
feed?.type === "feed" && <AddFeedHelper />}
|
||||
|
||||
<EntryListHeader
|
||||
refetch={entries.refetch}
|
||||
|
|
|
|||
|
|
@ -4,19 +4,15 @@ import { memo } from "react"
|
|||
import { LoadingCircle } from "~/components/ui/loading"
|
||||
import { views } from "~/constants"
|
||||
import { useAuthQuery } from "~/hooks/common"
|
||||
import { FeedViewType } from "~/lib/enum"
|
||||
import type { FeedViewType } from "~/lib/enum"
|
||||
import { FeedViewType as FeedViewTypeEnum } from "~/lib/enum"
|
||||
import { cn } from "~/lib/utils"
|
||||
import { Queries } from "~/queries"
|
||||
import type { FlatEntryModel } from "~/store/entry"
|
||||
import { useEntry } from "~/store/entry/hooks"
|
||||
|
||||
import { ReactVirtuosoItemPlaceholder } from "../../components/ui/placeholder"
|
||||
import { ArticleItem, ArticleItemSkeleton } from "./Items/article-item"
|
||||
import { AudioItem, AudioItemSkeleton } from "./Items/audio-item"
|
||||
import { NotificationItem, NotificationItemSkeleton } from "./Items/notification-item"
|
||||
import { PictureItem, PictureItemSkeleton } from "./Items/picture-item"
|
||||
import { SocialMediaItem, SocialMediaItemSkeleton } from "./Items/social-media-item"
|
||||
import { VideoItem, VideoItemSkeleton } from "./Items/video-item"
|
||||
import { getItemComponentByView, getSkeletonItemComponentByView } from "./Items"
|
||||
import { EntryItemWrapper } from "./layouts/EntryItemWrapper"
|
||||
import { girdClassNames } from "./styles"
|
||||
import type { EntryListItemFC } from "./types"
|
||||
|
|
@ -42,40 +38,18 @@ function EntryItemImpl({ entry, view }: { entry: FlatEntryModel; view?: number }
|
|||
},
|
||||
)
|
||||
|
||||
let Item: EntryListItemFC
|
||||
|
||||
switch (view) {
|
||||
case FeedViewType.Articles: {
|
||||
Item = ArticleItem
|
||||
break
|
||||
}
|
||||
case FeedViewType.SocialMedia: {
|
||||
Item = SocialMediaItem
|
||||
break
|
||||
}
|
||||
case FeedViewType.Pictures: {
|
||||
Item = PictureItem
|
||||
break
|
||||
}
|
||||
case FeedViewType.Videos: {
|
||||
Item = VideoItem
|
||||
break
|
||||
}
|
||||
case FeedViewType.Audios: {
|
||||
Item = AudioItem
|
||||
break
|
||||
}
|
||||
case FeedViewType.Notifications: {
|
||||
Item = NotificationItem
|
||||
break
|
||||
}
|
||||
default: {
|
||||
Item = ArticleItem
|
||||
}
|
||||
}
|
||||
const Item: EntryListItemFC = getItemComponentByView(view as FeedViewType)
|
||||
const overlayItemClassName =
|
||||
view === FeedViewTypeEnum.Pictures || view === FeedViewTypeEnum.Videos ? "top-0" : ""
|
||||
|
||||
return (
|
||||
<EntryItemWrapper itemClassName={Item.wrapperClassName} entry={entry} view={view} overlay>
|
||||
<EntryItemWrapper
|
||||
itemClassName={Item.wrapperClassName}
|
||||
entry={entry}
|
||||
view={view}
|
||||
overlay
|
||||
overlayItemClassName={overlayItemClassName}
|
||||
>
|
||||
<Item entryId={entry.entries.id} translation={translation.data} />
|
||||
</EntryItemWrapper>
|
||||
)
|
||||
|
|
@ -87,15 +61,6 @@ export const EntryItem: FC<EntryItemProps> = memo(({ entryId, view }) => {
|
|||
return <EntryItemImpl entry={entry} view={view} />
|
||||
})
|
||||
|
||||
const SkeletonItemMap = {
|
||||
[FeedViewType.Articles]: ArticleItemSkeleton,
|
||||
[FeedViewType.SocialMedia]: SocialMediaItemSkeleton,
|
||||
[FeedViewType.Pictures]: PictureItemSkeleton,
|
||||
[FeedViewType.Videos]: VideoItemSkeleton,
|
||||
[FeedViewType.Audios]: AudioItemSkeleton,
|
||||
[FeedViewType.Notifications]: NotificationItemSkeleton,
|
||||
}
|
||||
|
||||
const LoadingCircleFallback = (
|
||||
<div className="center mt-2">
|
||||
<LoadingCircle size="medium" />
|
||||
|
|
@ -106,7 +71,7 @@ export const EntryItemSkeleton: FC<{
|
|||
view: FeedViewType
|
||||
count?: number
|
||||
}> = memo(({ view, count }) => {
|
||||
const SkeletonItem = SkeletonItemMap[view]
|
||||
const SkeletonItem = getSkeletonItemComponentByView(view)
|
||||
if (count === 1) {
|
||||
return SkeletonItem
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,10 +21,11 @@ export const EntryItemWrapper: FC<
|
|||
entry: FlatEntryModel
|
||||
view?: number
|
||||
itemClassName?: string
|
||||
overlayItemClassName?: string
|
||||
overlay?: boolean
|
||||
style?: React.CSSProperties
|
||||
} & PropsWithChildren
|
||||
> = ({ entry, view, overlay, children, itemClassName, style }) => {
|
||||
> = ({ entry, view, overlay, children, itemClassName, overlayItemClassName, style }) => {
|
||||
const { items } = useEntryActions({
|
||||
view,
|
||||
entry,
|
||||
|
|
@ -50,7 +51,7 @@ export const EntryItemWrapper: FC<
|
|||
if (!document.hasFocus()) return
|
||||
if (asRead) return
|
||||
|
||||
entryActions.markRead(entry.feedId, entry.entries.id, true)
|
||||
entryActions.markRead({ feedId: entry.feedId, entryId: entry.entries.id, read: true })
|
||||
},
|
||||
233,
|
||||
{
|
||||
|
|
@ -127,7 +128,10 @@ export const EntryItemWrapper: FC<
|
|||
onContextMenu={handleContextMenu}
|
||||
>
|
||||
{overlay ? (
|
||||
<ListItemHoverOverlay isActive={isActive || isContextMenuOpen}>
|
||||
<ListItemHoverOverlay
|
||||
isActive={isActive || isContextMenuOpen}
|
||||
className={overlayItemClassName}
|
||||
>
|
||||
{children}
|
||||
</ListItemHoverOverlay>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ export const EntryListHeader: FC<{
|
|||
|
||||
const unreadOnly = useGeneralSettingKey("unreadOnly")
|
||||
|
||||
const { feedId, entryId, view } = routerParams
|
||||
const { feedId, entryId, view, listId } = routerParams
|
||||
|
||||
const headerTitle = useFeedHeaderTitle()
|
||||
const os = getOS()
|
||||
|
|
@ -45,12 +45,12 @@ export const EntryListHeader: FC<{
|
|||
|
||||
const titleInfo = !!headerTitle && (
|
||||
<div className={!titleAtBottom ? "min-w-0 translate-y-1" : void 0}>
|
||||
<div className="min-w-0 break-all text-lg font-bold leading-none">
|
||||
<div className="h-6 min-w-0 break-all text-lg font-bold leading-tight">
|
||||
<EllipsisHorizontalTextWithTooltip className="inline-block !w-auto max-w-full">
|
||||
{headerTitle}
|
||||
<span className="relative -top-px">{headerTitle}</span>
|
||||
</EllipsisHorizontalTextWithTooltip>
|
||||
</div>
|
||||
<div className="text-xs font-medium text-zinc-400">
|
||||
<div className="text-xs font-medium leading-none text-zinc-400">
|
||||
{totalCount || 0} {t("quantifier.piece", { ns: "common" })}
|
||||
{unreadOnly && !isInCollectionList ? t("words.unread") : ""}
|
||||
{t("space", { ns: "common" })}
|
||||
|
|
@ -58,13 +58,13 @@ export const EntryListHeader: FC<{
|
|||
</div>
|
||||
</div>
|
||||
)
|
||||
const { mutateAsync: refreshFeed, isPending } = useRefreshFeedMutation(routerParams.feedId)
|
||||
const { mutateAsync: refreshFeed, isPending } = useRefreshFeedMutation(feedId)
|
||||
|
||||
const user = useWhoami()
|
||||
const isOnline = useIsOnline()
|
||||
|
||||
const feed = useFeedById(routerParams.feedId)
|
||||
const isList = feed?.type === "list"
|
||||
const feed = useFeedById(feedId)
|
||||
const isList = !!listId
|
||||
|
||||
const titleStyleBasedView = ["pl-12", "pl-7", "pl-7", "pl-7", "px-5", "pl-12"]
|
||||
|
||||
|
|
@ -98,6 +98,7 @@ export const EntryListHeader: FC<{
|
|||
)}
|
||||
|
||||
<AppendTaildingDivider>
|
||||
{!views[view].wideMode && <WideModeButton />}
|
||||
{view === FeedViewType.SocialMedia && <DailyReportButton />}
|
||||
{view === FeedViewType.Pictures && <SwitchToMasonryButton />}
|
||||
{view === FeedViewType.Pictures && <FilterNoImageButton />}
|
||||
|
|
@ -231,6 +232,39 @@ const SwitchToMasonryButton = () => {
|
|||
)
|
||||
}
|
||||
|
||||
const WideModeButton = () => {
|
||||
const isWideMode = useUISettingKey("wideMode")
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<ImpressionView
|
||||
event="Switch to Wide Mode"
|
||||
properties={{
|
||||
wideMode: isWideMode ? 1 : 0,
|
||||
}}
|
||||
>
|
||||
<ActionButton
|
||||
onClick={() => {
|
||||
setUISetting("wideMode", !isWideMode)
|
||||
window.posthog?.capture("Switch to Wide Mode", {
|
||||
wideMode: !isWideMode ? 1 : 0,
|
||||
click: 1,
|
||||
})
|
||||
}}
|
||||
tooltip={
|
||||
!isWideMode
|
||||
? t("entry_list_header.switch_to_widemode")
|
||||
: t("entry_list_header.switch_to_normalmode")
|
||||
}
|
||||
>
|
||||
<i
|
||||
className={cn(isWideMode ? "i-mgc-align-justify-cute-re" : "i-mgc-align-left-cute-re")}
|
||||
/>
|
||||
</ActionButton>
|
||||
</ImpressionView>
|
||||
)
|
||||
}
|
||||
|
||||
const AppendTaildingDivider = ({ children }: { children: React.ReactNode }) => (
|
||||
<>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,14 @@ import { env } from "@follow/shared/env"
|
|||
import { useDebounceCallback } from "usehooks-ts"
|
||||
|
||||
import { AudioPlayer, useAudioPlayerAtomSelector } from "~/atoms/player"
|
||||
import { useUISettingKey } from "~/atoms/settings/ui"
|
||||
import { FeedIcon } from "~/components/feed-icon"
|
||||
import { FollowIcon } from "~/components/icons/follow"
|
||||
import { Button } from "~/components/ui/button"
|
||||
import { RelativeTime } from "~/components/ui/datetime"
|
||||
import { Media } from "~/components/ui/media"
|
||||
import { useModalStack } from "~/components/ui/modal"
|
||||
import { EllipsisHorizontalTextWithTooltip } from "~/components/ui/typography"
|
||||
import { FEED_COLLECTION_LIST } from "~/constants"
|
||||
import { useAsRead } from "~/hooks/biz/useAsRead"
|
||||
import { useRouteParamsSelector } from "~/hooks/biz/useRouteParams"
|
||||
|
|
@ -45,7 +47,9 @@ export function ListItem({
|
|||
|
||||
const handlePrefetchEntry = useDebounceCallback(
|
||||
() => {
|
||||
Queries.entries.byId(entryId).prefetch()
|
||||
feed?.type === "inbox"
|
||||
? Queries.entries.byInboxId(entryId).prefetch()
|
||||
: Queries.entries.byId(entryId).prefetch()
|
||||
},
|
||||
300,
|
||||
{ leading: false },
|
||||
|
|
@ -58,19 +62,23 @@ export function ListItem({
|
|||
const isFollowed = !!useSubscriptionStore((state) => feedId && state.data[feedId])
|
||||
const { present } = useModalStack()
|
||||
|
||||
const settingWideMode = useUISettingKey("wideMode")
|
||||
|
||||
// NOTE: prevent 0 height element, react virtuoso will not stop render any more
|
||||
if (!entry || !feed) return <ReactVirtuosoItemPlaceholder />
|
||||
|
||||
const displayTime = inInCollection ? entry.collections?.createdAt : entry.entries.publishedAt
|
||||
const envIsSafari = isSafari()
|
||||
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={handlePrefetchEntry}
|
||||
onMouseLeave={handlePrefetchEntry.cancel}
|
||||
className={cn(
|
||||
"group relative flex cursor-menu py-4 pl-3 pr-2",
|
||||
"group relative flex cursor-menu pl-3 pr-2",
|
||||
!asRead &&
|
||||
"before:absolute before:-left-0.5 before:top-[1.4375rem] before:block before:size-2 before:rounded-full before:bg-accent",
|
||||
settingWideMode ? "py-3" : "py-4",
|
||||
)}
|
||||
>
|
||||
{!withAudio && <FeedIcon feed={feed} fallback entry={entry.entries} />}
|
||||
|
|
@ -79,7 +87,7 @@ export function ListItem({
|
|||
"-mt-0.5 flex-1 text-sm leading-tight",
|
||||
|
||||
// FIXME: Safari bug, not support line-clamp cross elements
|
||||
!envIsSafari && "line-clamp-4",
|
||||
!envIsSafari && (settingWideMode ? "line-clamp-2" : "line-clamp-4"),
|
||||
)}
|
||||
>
|
||||
<div
|
||||
|
|
@ -89,7 +97,9 @@ export function ListItem({
|
|||
entry.collections && "text-zinc-600 dark:text-zinc-500",
|
||||
)}
|
||||
>
|
||||
<span className="truncate">{getPreferredTitle(feed)}</span>
|
||||
<EllipsisHorizontalTextWithTooltip className="truncate">
|
||||
{getPreferredTitle(feed)}
|
||||
</EllipsisHorizontalTextWithTooltip>
|
||||
<span>·</span>
|
||||
<span className="shrink-0">{!!displayTime && <RelativeTime date={displayTime} />}</span>
|
||||
</div>
|
||||
|
|
@ -172,7 +182,7 @@ export function ListItem({
|
|||
fallback={false}
|
||||
feed={feed}
|
||||
entry={entry.entries}
|
||||
size={80}
|
||||
size={settingWideMode ? 65 : 80}
|
||||
className="m-0 rounded"
|
||||
useMedia
|
||||
/>
|
||||
|
|
@ -186,12 +196,19 @@ export function ListItem({
|
|||
src={entry.entries.media[0].url}
|
||||
type={entry.entries.media[0].type}
|
||||
previewImageUrl={entry.entries.media[0].preview_image_url}
|
||||
className="ml-2 size-20 shrink-0"
|
||||
className={cn(
|
||||
"center ml-2 flex shrink-0 rounded",
|
||||
settingWideMode ? "size-12" : "size-20",
|
||||
)}
|
||||
mediaContainerClassName={"w-auto h-auto rounded"}
|
||||
loading="lazy"
|
||||
proxy={{
|
||||
width: 160,
|
||||
height: 160,
|
||||
}}
|
||||
height={entry.entries.media[0].height}
|
||||
width={entry.entries.media[0].width}
|
||||
blurhash={entry.entries.media[0].blurhash}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ import { memo } from "react"
|
|||
|
||||
import { useWhoami } from "~/atoms/user"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "~/components/ui/avatar"
|
||||
import { ScrollArea } from "~/components/ui/scroll-area"
|
||||
import { Tooltip, TooltipContent, TooltipPortal, TooltipTrigger } from "~/components/ui/tooltip"
|
||||
import { EllipsisHorizontalTextWithTooltip } from "~/components/ui/typography"
|
||||
import { useAuthQuery } from "~/hooks/common"
|
||||
import { useAppLayoutGridContainerWidth } from "~/providers/app-grid-layout-container-provider"
|
||||
import { Queries } from "~/queries"
|
||||
|
|
@ -55,9 +57,9 @@ export const EntryReadHistory: Component<{ entryId: string }> = ({ entryId }) =>
|
|||
right: `${LIMIT * 8}px`,
|
||||
zIndex: 11,
|
||||
}}
|
||||
className="no-drag-region relative flex size-7 items-center justify-center rounded-full border border-border bg-muted ring ring-background"
|
||||
className="no-drag-region relative flex size-7 items-center justify-center rounded-full border border-border bg-muted ring-2 ring-background"
|
||||
>
|
||||
<span className="text-[10px] font-medium text-muted-foreground">
|
||||
<span className="text-[10px] font-medium tabular-nums text-muted-foreground">
|
||||
+{Math.min(totalCount - LIMIT, 99)}
|
||||
</span>
|
||||
</button>
|
||||
|
|
@ -69,21 +71,28 @@ export const EntryReadHistory: Component<{ entryId: string }> = ({ entryId }) =>
|
|||
sideOffset={12}
|
||||
align="start"
|
||||
side="right"
|
||||
asChild
|
||||
className={clsx(
|
||||
"z-10 flex max-h-[300px] flex-col overflow-y-auto rounded-md border bg-background drop-shadow",
|
||||
// Animation, fade up
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=open]:slide-in-from-bottom-3",
|
||||
"z-10 rounded-md border bg-background drop-shadow",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
"data-[state=closed]:slide-out-to-left-5 data-[state=open]:slide-in-from-left-5",
|
||||
"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
|
||||
"transition-all duration-200 ease-in-out",
|
||||
)}
|
||||
>
|
||||
<ul>
|
||||
{entryHistory.userIds
|
||||
.filter((id) => id !== me?.id)
|
||||
.slice(LIMIT)
|
||||
.map((userId) => (
|
||||
<EntryUserRow userId={userId} key={userId} />
|
||||
))}
|
||||
</ul>
|
||||
<ScrollArea.ScrollArea
|
||||
rootClassName="max-h-[300px] max-w-[20ch] flex flex-col"
|
||||
flex
|
||||
>
|
||||
<ul>
|
||||
{entryHistory.userIds
|
||||
.filter((id) => id !== me?.id)
|
||||
.slice(LIMIT)
|
||||
.map((userId) => (
|
||||
<EntryUserRow userId={userId} key={userId} />
|
||||
))}
|
||||
</ul>
|
||||
</ScrollArea.ScrollArea>
|
||||
</HoverCardContent>
|
||||
</HoverCardPortal>
|
||||
)}
|
||||
|
|
@ -99,21 +108,22 @@ const EntryUserRow: Component<{ userId: string }> = memo(({ userId }) => {
|
|||
if (!user) return null
|
||||
|
||||
return (
|
||||
<li className="relative flex min-w-0 max-w-[50ch] rounded-md p-1 hover:bg-muted">
|
||||
<button
|
||||
type="button"
|
||||
className="flex min-w-0 items-center gap-2 truncate"
|
||||
onClick={() => {
|
||||
presentUserProfile(userId)
|
||||
}}
|
||||
>
|
||||
<Avatar className="aspect-square size-7 overflow-hidden rounded-full border border-border ring-1 ring-background">
|
||||
<AvatarImage src={user?.image || undefined} />
|
||||
<AvatarFallback>{user.name?.slice(0, 2)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<li
|
||||
onClick={() => presentUserProfile(userId)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="relative flex min-w-0 max-w-[50ch] shrink-0 cursor-button items-center gap-2 truncate rounded-md p-1 px-2 hover:bg-muted"
|
||||
>
|
||||
<Avatar className="block aspect-square size-7 overflow-hidden rounded-full border border-border ring-1 ring-background">
|
||||
<AvatarImage src={user?.image || undefined} />
|
||||
<AvatarFallback>{user.name?.slice(0, 2)}</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
{user.name && <p className="truncate pr-8 text-xs font-medium">{user.name}</p>}
|
||||
</button>
|
||||
{user.name && (
|
||||
<EllipsisHorizontalTextWithTooltip className="pr-8 text-xs font-medium">
|
||||
<span>{user.name}</span>
|
||||
</EllipsisHorizontalTextWithTooltip>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
})
|
||||
|
|
@ -144,7 +154,7 @@ const EntryUser: Component<{
|
|||
}}
|
||||
>
|
||||
<Avatar className="aspect-square size-7 border border-border ring-1 ring-background">
|
||||
<AvatarImage src={user?.image || undefined} />
|
||||
<AvatarImage src={user?.image || undefined} className="bg-theme-placeholder-image" />
|
||||
<AvatarFallback>{user.name?.slice(0, 2)}</AvatarFallback>
|
||||
</Avatar>
|
||||
</m.button>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
import { AnimatePresence } from "framer-motion"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
|
||||
import { useShowSourceContent } from "~/atoms/source-content"
|
||||
import { m } from "~/components/common/Motion"
|
||||
import { softSpringPreset } from "~/components/ui/constants/spring"
|
||||
|
||||
import { EntryContentLoading } from "../loading"
|
||||
|
||||
const ViewTag = window.electron ? "webview" : "iframe"
|
||||
const variants = {
|
||||
hidden: { x: "100%" },
|
||||
visible: { x: 0 },
|
||||
exit: { x: "100%" },
|
||||
}
|
||||
|
||||
const Banner = () => {
|
||||
return (
|
||||
<div className="z-50 w-full bg-yellow-600 p-3 text-white">
|
||||
<div className="text-center">
|
||||
<p>Some websites can't be displayed here. Download desktop app to view it.</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const SourceContentView = ({ src }: { src: string }) => {
|
||||
const showSourceContent = useShowSourceContent()
|
||||
const [loading, setLoading] = useState(true)
|
||||
const webviewRef = useRef<HTMLIFrameElement | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const abortController = new AbortController()
|
||||
const webview = webviewRef.current
|
||||
if (!webview) return
|
||||
const handleDidStopLoading = () => setLoading(false)
|
||||
|
||||
// See https://www.electronjs.org/docs/latest/api/webview-tag#example
|
||||
webview.addEventListener("did-stop-loading", handleDidStopLoading, {
|
||||
signal: abortController.signal,
|
||||
})
|
||||
|
||||
return () => {
|
||||
abortController.abort()
|
||||
}
|
||||
}, [src, showSourceContent])
|
||||
|
||||
return (
|
||||
<>
|
||||
{!window.electron && <Banner />}
|
||||
<div className="relative flex size-full flex-col">
|
||||
{loading && (
|
||||
<div className="center mt-16 min-w-0">
|
||||
<EntryContentLoading icon={src} />
|
||||
</div>
|
||||
)}
|
||||
<m.div
|
||||
className="size-full"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: loading ? 0 : 1 }}
|
||||
transition={softSpringPreset}
|
||||
>
|
||||
<ViewTag
|
||||
ref={webviewRef}
|
||||
className="size-full"
|
||||
src={src}
|
||||
sandbox="allow-scripts allow-same-origin"
|
||||
// For iframe
|
||||
onLoad={() => setLoading(false)}
|
||||
/>
|
||||
</m.div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const SourceContentPanel = ({ src }: { src: string | null }) => {
|
||||
const showSourceContent = useShowSourceContent()
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{showSourceContent && src && (
|
||||
<m.div
|
||||
className="absolute left-0 top-0 z-[1] size-full bg-theme-background"
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="exit"
|
||||
variants={variants}
|
||||
transition={softSpringPreset}
|
||||
>
|
||||
<SourceContentView src={src} />
|
||||
</m.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
|
|
@ -186,8 +186,8 @@ const ElectronAdditionActions = window.electron
|
|||
name: t("entry_content.header.readability"),
|
||||
className: cn(
|
||||
isInReadability(entryReadabilityStatus)
|
||||
? `i-mgc-sparkles-2-filled`
|
||||
: `i-mgc-sparkles-2-cute-re`,
|
||||
? `i-mgc-docment-cute-fi`
|
||||
: `i-mgc-docment-cute-re`,
|
||||
entryReadabilityStatus === ReadabilityStatus.WAITING ? `animate-pulse` : "",
|
||||
),
|
||||
key: "readability",
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ import { stopPropagation } from "~/lib/dom"
|
|||
import { FeedViewType } from "~/lib/enum"
|
||||
import { getNewIssueUrl } from "~/lib/issues"
|
||||
import { cn } from "~/lib/utils"
|
||||
import type { ActiveEntryId, FeedModel } from "~/models"
|
||||
import type { ActiveEntryId, FeedModel, InboxModel } from "~/models"
|
||||
import {
|
||||
useIsSoFWrappedElement,
|
||||
useWrappedElement,
|
||||
|
|
@ -45,6 +45,7 @@ import { EntryPlaceholderDaily } from "../ai/ai-daily/EntryPlaceholderDaily"
|
|||
import { setEntryContentScrollToTop, setEntryTitleMeta } from "./atoms"
|
||||
import { EntryPlaceholderLogo } from "./components/EntryPlaceholderLogo"
|
||||
import { EntryTitle } from "./components/EntryTitle"
|
||||
import { SourceContentPanel } from "./components/SourceContentView"
|
||||
import { SupportCreator } from "./components/SupportCreator"
|
||||
import { EntryHeader } from "./header"
|
||||
import { EntryContentLoading } from "./loading"
|
||||
|
|
@ -88,15 +89,19 @@ export const EntryContentRender: Component<{
|
|||
}> = ({ entryId, noMedia, className, compact }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { error, data, isPending } = useAuthQuery(Queries.entries.byId(entryId), {
|
||||
staleTime: 300_000,
|
||||
})
|
||||
|
||||
const entry = useEntry(entryId)
|
||||
useTitle(entry?.entries.title)
|
||||
|
||||
const feed = useFeedById(entry?.feedId) as FeedModel
|
||||
const feed = useFeedById(entry?.feedId) as FeedModel | InboxModel
|
||||
const readerRenderInlineStyle = useUISettingKey("readerRenderInlineStyle")
|
||||
|
||||
const { error, data, isPending } = useAuthQuery(
|
||||
feed?.type === "inbox" ? Queries.entries.byInboxId(entryId) : Queries.entries.byId(entryId),
|
||||
{
|
||||
staleTime: 300_000,
|
||||
},
|
||||
)
|
||||
|
||||
const summary = useAuthQuery(
|
||||
Queries.ai.summary({
|
||||
entryId,
|
||||
|
|
@ -144,21 +149,25 @@ export const EntryContentRender: Component<{
|
|||
: undefined,
|
||||
[readerFontFamily],
|
||||
)
|
||||
const mediaInfo = useMemo(
|
||||
() =>
|
||||
Object.fromEntries(
|
||||
(entry?.entries.media ?? data?.entries.media)
|
||||
?.filter((m) => m.type === "photo")
|
||||
.map((cur) => [
|
||||
cur.url,
|
||||
{
|
||||
width: cur.width,
|
||||
height: cur.height,
|
||||
},
|
||||
]) ?? [],
|
||||
),
|
||||
[entry?.entries.media, data?.entries.media],
|
||||
)
|
||||
|
||||
if (!entry) return null
|
||||
|
||||
const content = entry?.entries.content ?? data?.entries.content
|
||||
const mediaInfo = Object.fromEntries(
|
||||
(entry.entries.media ?? data?.entries.media)
|
||||
?.filter((m) => m.type === "photo")
|
||||
.map((cur) => [
|
||||
cur.url,
|
||||
{
|
||||
width: cur.width,
|
||||
height: cur.height,
|
||||
},
|
||||
]) ?? [],
|
||||
)
|
||||
|
||||
return (
|
||||
<EntryContentProvider
|
||||
|
|
@ -174,88 +183,93 @@ export const EntryContentRender: Component<{
|
|||
compact={compact}
|
||||
/>
|
||||
|
||||
<ScrollArea.ScrollArea
|
||||
mask={false}
|
||||
rootClassName={cn("h-0 min-w-0 grow overflow-y-auto @container", className)}
|
||||
scrollbarClassName="mr-[1.5px]"
|
||||
viewportClassName="p-5"
|
||||
ref={scrollerRef}
|
||||
>
|
||||
<div
|
||||
style={stableRenderStyle}
|
||||
className="duration-200 ease-in-out animate-in fade-in slide-in-from-bottom-24 f-motion-reduce:fade-in-0 f-motion-reduce:slide-in-from-bottom-0"
|
||||
key={entry.entries.id}
|
||||
<div className="relative flex size-full flex-col overflow-hidden">
|
||||
<ScrollArea.ScrollArea
|
||||
mask={false}
|
||||
rootClassName={cn("h-0 min-w-0 grow overflow-y-auto @container", className)}
|
||||
scrollbarClassName="mr-[1.5px]"
|
||||
viewportClassName="p-5"
|
||||
ref={scrollerRef}
|
||||
>
|
||||
<article
|
||||
data-testid="entry-render"
|
||||
onContextMenu={stopPropagation}
|
||||
className="relative m-auto min-w-0 max-w-[550px] @3xl:max-w-[70ch]"
|
||||
<div
|
||||
style={stableRenderStyle}
|
||||
className="duration-200 ease-in-out animate-in fade-in slide-in-from-bottom-24 f-motion-reduce:fade-in-0 f-motion-reduce:slide-in-from-bottom-0"
|
||||
key={entry.entries.id}
|
||||
>
|
||||
<EntryTitle entryId={entryId} compact={compact} />
|
||||
<article
|
||||
data-testid="entry-render"
|
||||
onContextMenu={stopPropagation}
|
||||
className="relative m-auto min-w-0 max-w-[550px] @3xl:max-w-[70ch]"
|
||||
>
|
||||
<EntryTitle entryId={entryId} compact={compact} />
|
||||
|
||||
<WrappedElementProvider boundingDetection>
|
||||
<div className="mx-auto mb-32 mt-8 max-w-full cursor-auto select-text break-all text-[0.94rem]">
|
||||
<TitleMetaHandler entryId={entry.entries.id} />
|
||||
{(summary.isLoading || summary.data) && (
|
||||
<div className="my-8 space-y-1 rounded-lg border px-4 py-3">
|
||||
<div className="flex items-center gap-2 font-medium text-zinc-800 dark:text-neutral-400">
|
||||
<i className="i-mgc-magic-2-cute-re align-middle" />
|
||||
<span>{t("entry_content.ai_summary")}</span>
|
||||
<WrappedElementProvider boundingDetection>
|
||||
<div className="mx-auto mb-32 mt-8 max-w-full cursor-auto select-text break-all text-[0.94rem]">
|
||||
<TitleMetaHandler entryId={entry.entries.id} />
|
||||
{(summary.isLoading || summary.data) && (
|
||||
<div className="my-8 space-y-1 rounded-lg border px-4 py-3">
|
||||
<div className="flex items-center gap-2 font-medium text-zinc-800 dark:text-neutral-400">
|
||||
<i className="i-mgc-magic-2-cute-re align-middle" />
|
||||
<span>{t("entry_content.ai_summary")}</span>
|
||||
</div>
|
||||
<AutoResizeHeight spring className="text-sm leading-relaxed">
|
||||
{summary.isLoading ? SummaryLoadingSkeleton : summary.data}
|
||||
</AutoResizeHeight>
|
||||
</div>
|
||||
<AutoResizeHeight spring className="text-sm leading-relaxed">
|
||||
{summary.isLoading ? SummaryLoadingSkeleton : summary.data}
|
||||
</AutoResizeHeight>
|
||||
</div>
|
||||
)}
|
||||
<ErrorBoundary fallback={RenderError}>
|
||||
{!isInReadabilityMode ? (
|
||||
<ShadowDOM>
|
||||
<HTML
|
||||
mediaInfo={mediaInfo}
|
||||
noMedia={noMedia}
|
||||
accessory={contentAccessories}
|
||||
as="article"
|
||||
className="prose !max-w-full dark:prose-invert prose-h1:text-[1.6em] prose-h1:font-bold"
|
||||
style={stableRenderStyle}
|
||||
renderInlineStyle={readerRenderInlineStyle}
|
||||
>
|
||||
{content}
|
||||
</HTML>
|
||||
</ShadowDOM>
|
||||
) : (
|
||||
<ReadabilityContent entryId={entryId} />
|
||||
)}
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</WrappedElementProvider>
|
||||
<ErrorBoundary fallback={RenderError}>
|
||||
{!isInReadabilityMode ? (
|
||||
<ShadowDOM>
|
||||
<HTML
|
||||
mediaInfo={mediaInfo}
|
||||
noMedia={noMedia}
|
||||
accessory={contentAccessories}
|
||||
as="article"
|
||||
className="prose !max-w-full dark:prose-invert prose-h1:text-[1.6em] prose-h1:font-bold"
|
||||
style={stableRenderStyle}
|
||||
renderInlineStyle={readerRenderInlineStyle}
|
||||
>
|
||||
{content}
|
||||
</HTML>
|
||||
</ShadowDOM>
|
||||
) : (
|
||||
<ReadabilityContent entryId={entryId} />
|
||||
)}
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</WrappedElementProvider>
|
||||
|
||||
{entry.settings?.readability && (
|
||||
<ReadabilityAutoToggle id={entry.entries.id} url={entry.entries.url ?? ""} />
|
||||
)}
|
||||
{entry.settings?.readability && (
|
||||
<ReadabilityAutoToggleEffect id={entry.entries.id} url={entry.entries.url ?? ""} />
|
||||
)}
|
||||
|
||||
{!content && (
|
||||
<div className="center mt-16 min-w-0">
|
||||
{isPending ? (
|
||||
<EntryContentLoading icon={feed?.siteUrl!} />
|
||||
) : error ? (
|
||||
<div className="center flex min-w-0 flex-col gap-2">
|
||||
<i className="i-mgc-close-cute-re text-3xl text-red-500" />
|
||||
<span className="font-sans text-sm">Network Error</span>
|
||||
{!content && (
|
||||
<div className="center mt-16 min-w-0">
|
||||
{isPending ? (
|
||||
<EntryContentLoading
|
||||
icon={feed?.type === "inbox" ? undefined : feed?.siteUrl!}
|
||||
/>
|
||||
) : error ? (
|
||||
<div className="center flex min-w-0 flex-col gap-2">
|
||||
<i className="i-mgc-close-cute-re text-3xl text-red-500" />
|
||||
<span className="font-sans text-sm">Network Error</span>
|
||||
|
||||
<pre className="mt-6 w-full overflow-auto whitespace-pre-wrap break-all">
|
||||
{error.message}
|
||||
</pre>
|
||||
</div>
|
||||
) : (
|
||||
<NoContent id={entry.entries.id} url={entry.entries.url ?? ""} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<pre className="mt-6 w-full overflow-auto whitespace-pre-wrap break-all">
|
||||
{error.message}
|
||||
</pre>
|
||||
</div>
|
||||
) : (
|
||||
<NoContent id={entry.entries.id} url={entry.entries.url ?? ""} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{feed?.ownerUserId && <SupportCreator entryId={entryId} />}
|
||||
</article>
|
||||
</div>
|
||||
</ScrollArea.ScrollArea>
|
||||
{feed?.ownerUserId && <SupportCreator entryId={entryId} />}
|
||||
</article>
|
||||
</div>
|
||||
</ScrollArea.ScrollArea>
|
||||
<SourceContentPanel src={entry.entries.url} />
|
||||
</div>
|
||||
</EntryContentProvider>
|
||||
)
|
||||
}
|
||||
|
|
@ -310,7 +324,7 @@ const ReadabilityContent = ({ entryId }: { entryId: string }) => {
|
|||
</p>
|
||||
) : (
|
||||
<div className="center mt-16 flex flex-col gap-2">
|
||||
<LoadingWithIcon size="large" icon={<i className="i-mgc-sparkles-2-cute-re" />} />
|
||||
<LoadingWithIcon size="large" icon={<i className="i-mgc-docment-cute-re" />} />
|
||||
<span className="text-sm">{t("entry_content.fetching_content")}</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -346,13 +360,13 @@ const NoContent: FC<{
|
|||
<span>{t("entry_content.web_app_notice")}</span>
|
||||
</div>
|
||||
)}
|
||||
{url && window.electron && <ReadabilityAutoToggle url={url} id={id} />}
|
||||
{url && window.electron && <ReadabilityAutoToggleEffect url={url} id={id} />}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ReadabilityAutoToggle = ({ url, id }: { url: string; id: string }) => {
|
||||
const ReadabilityAutoToggleEffect = ({ url, id }: { url: string; id: string }) => {
|
||||
const toggle = useEntryReadabilityToggle({
|
||||
id,
|
||||
url,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { getUrlIcon } from "~/lib/utils"
|
|||
|
||||
export const EntryContentLoading = (props: { icon?: string }) => {
|
||||
if (!props.icon) {
|
||||
return <LoadingWithIcon size="large" icon={<i className="i-mgc-sparkles-2-cute-re" />} />
|
||||
return <LoadingWithIcon size="large" icon={<i className="i-mgc-docment-cute-re" />} />
|
||||
}
|
||||
return (
|
||||
<div className="center mb-14 flex flex-col gap-4">
|
||||
|
|
|
|||
|
|
@ -10,13 +10,13 @@ import { LoadingCircle } from "~/components/ui/loading"
|
|||
import { ROUTE_FEED_IN_FOLDER, views } from "~/constants"
|
||||
import { useNavigateEntry } from "~/hooks/biz/useNavigateEntry"
|
||||
import { getRouteParams, useRouteParamsSelector } from "~/hooks/biz/useRouteParams"
|
||||
import { useAnyPointDown, useAuthQuery, useInputComposition } from "~/hooks/common"
|
||||
import { useAnyPointDown, useInputComposition } from "~/hooks/common"
|
||||
import { stopPropagation } from "~/lib/dom"
|
||||
import type { FeedViewType } from "~/lib/enum"
|
||||
import { showNativeMenu } from "~/lib/native-menu"
|
||||
import { cn, sortByAlphabet } from "~/lib/utils"
|
||||
import { Queries } from "~/queries"
|
||||
import { getPreferredTitle, useAddFeedToFeedList, useFeedStore } from "~/store/feed"
|
||||
import { useListByView } from "~/store/list"
|
||||
import { subscriptionActions, useSubscriptionByFeedId } from "~/store/subscription"
|
||||
import { useFeedUnreadStore } from "~/store/unread"
|
||||
|
||||
|
|
@ -30,16 +30,10 @@ type FeedId = string
|
|||
interface FeedCategoryProps {
|
||||
data: FeedId[]
|
||||
view?: number
|
||||
expansion: boolean
|
||||
showUnreadCount?: boolean
|
||||
categoryOpenStateData: Record<string, boolean>
|
||||
}
|
||||
|
||||
function FeedCategoryImpl({
|
||||
data: ids,
|
||||
view,
|
||||
expansion,
|
||||
showUnreadCount = true,
|
||||
}: FeedCategoryProps) {
|
||||
function FeedCategoryImpl({ data: ids, view, categoryOpenStateData }: FeedCategoryProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const sortByUnreadFeedList = useFeedUnreadStore((state) =>
|
||||
|
|
@ -52,14 +46,18 @@ function FeedCategoryImpl({
|
|||
const folderName = subscription?.category || subscription.defaultCategory
|
||||
|
||||
const showCollapse = sortByUnreadFeedList.length > 1 || subscription?.category
|
||||
const [open, setOpen] = useState(!showCollapse)
|
||||
|
||||
const [open, setOpen] = useState(() => {
|
||||
if (!showCollapse) return true
|
||||
if (folderName && typeof categoryOpenStateData[folderName] === "boolean") {
|
||||
return categoryOpenStateData[folderName]
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
const shouldOpen = useRouteParamsSelector(
|
||||
(s) => typeof s.feedId === "string" && ids.includes(s.feedId),
|
||||
)
|
||||
|
||||
const itemsRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldOpen) {
|
||||
setOpen(true)
|
||||
|
|
@ -73,17 +71,30 @@ function FeedCategoryImpl({
|
|||
})
|
||||
}
|
||||
}, [shouldOpen])
|
||||
const expansion = folderName ? categoryOpenStateData[folderName] : true
|
||||
|
||||
useEffect(() => {
|
||||
if (showCollapse) {
|
||||
setOpen(expansion)
|
||||
}
|
||||
}, [expansion])
|
||||
|
||||
const itemsRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const toggleCategoryOpenState = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation()
|
||||
if (!isCategoryEditing) {
|
||||
setCategoryActive()
|
||||
}
|
||||
if (view !== undefined && folderName) {
|
||||
subscriptionActions.toggleCategoryOpenState(view, folderName)
|
||||
}
|
||||
}
|
||||
|
||||
const setCategoryActive = () => {
|
||||
if (view !== undefined) {
|
||||
navigate({
|
||||
entryId: null,
|
||||
// TODO joint feedId is too long, need to be optimized
|
||||
folderName,
|
||||
view,
|
||||
})
|
||||
|
|
@ -116,9 +127,10 @@ function FeedCategoryImpl({
|
|||
})
|
||||
const isCategoryIsWaiting = isChangePending
|
||||
|
||||
const listList = useAuthQuery(Queries.lists.list())
|
||||
const addMutation = useAddFeedToFeedList()
|
||||
|
||||
const listList = useListByView(view!)
|
||||
|
||||
return (
|
||||
<div tabIndex={-1} onClick={stopPropagation}>
|
||||
{!!showCollapse && (
|
||||
|
|
@ -150,8 +162,8 @@ function FeedCategoryImpl({
|
|||
{
|
||||
type: "text",
|
||||
label: t("sidebar.feed_column.context_menu.add_feeds_to_list"),
|
||||
enabled: !!listList.data?.length,
|
||||
submenu: listList.data?.map((list) => ({
|
||||
enabled: !!listList?.length,
|
||||
submenu: listList?.map((list) => ({
|
||||
label: list.title || "",
|
||||
type: "text",
|
||||
click() {
|
||||
|
|
@ -171,7 +183,7 @@ function FeedCategoryImpl({
|
|||
.filter((v) => v.view !== view)
|
||||
.map((v) => ({
|
||||
label: t(v.name),
|
||||
type: "text",
|
||||
type: "text" as const,
|
||||
shortcut: (v.view + 1).toString(),
|
||||
icon: v.icon,
|
||||
click() {
|
||||
|
|
@ -206,11 +218,7 @@ function FeedCategoryImpl({
|
|||
<div className="flex w-full min-w-0 items-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
if (isCategoryEditing) return
|
||||
e.stopPropagation()
|
||||
setOpen(!open)
|
||||
}}
|
||||
onClick={toggleCategoryOpenState}
|
||||
data-state={open ? "open" : "close"}
|
||||
className={cn(
|
||||
"flex h-8 items-center [&_.i-mgc-right-cute-fi]:data-[state=open]:rotate-90",
|
||||
|
|
@ -241,14 +249,7 @@ function FeedCategoryImpl({
|
|||
/>
|
||||
) : (
|
||||
<Fragment>
|
||||
<span
|
||||
className={cn(
|
||||
"grow truncate",
|
||||
!showUnreadCount && (unread ? "font-bold" : "font-medium opacity-70"),
|
||||
)}
|
||||
>
|
||||
{folderName}
|
||||
</span>
|
||||
<span className="grow truncate">{folderName}</span>
|
||||
|
||||
<UnreadNumber unread={unread} className="ml-2" />
|
||||
</Fragment>
|
||||
|
|
@ -280,7 +281,6 @@ function FeedCategoryImpl({
|
|||
ids={ids}
|
||||
showCollapse={showCollapse as boolean}
|
||||
view={view as FeedViewType}
|
||||
showUnreadCount={showUnreadCount}
|
||||
/>
|
||||
</m.div>
|
||||
)}
|
||||
|
|
@ -368,7 +368,6 @@ const RenameCategoryForm: FC<{
|
|||
|
||||
type SortListProps = {
|
||||
ids: string[]
|
||||
showUnreadCount?: boolean
|
||||
view: FeedViewType
|
||||
showCollapse: boolean
|
||||
}
|
||||
|
|
@ -389,7 +388,7 @@ const SortedFeedItems = (props: SortListProps) => {
|
|||
}
|
||||
|
||||
const SortByAlphabeticalList = (props: SortListProps) => {
|
||||
const { ids, showUnreadCount, showCollapse, view } = props
|
||||
const { ids, showCollapse, view } = props
|
||||
const isDesc = useFeedListSortSelector((s) => s.order === "desc")
|
||||
const sortedFeedList = useFeedStore((state) => {
|
||||
const res = ids.sort((a, b) => {
|
||||
|
|
@ -407,7 +406,6 @@ const SortByAlphabeticalList = (props: SortListProps) => {
|
|||
<Fragment>
|
||||
{sortedFeedList.map((feedId) => (
|
||||
<FeedItem
|
||||
showUnreadCount={showUnreadCount}
|
||||
key={feedId}
|
||||
feedId={feedId}
|
||||
view={view}
|
||||
|
|
@ -417,7 +415,7 @@ const SortByAlphabeticalList = (props: SortListProps) => {
|
|||
</Fragment>
|
||||
)
|
||||
}
|
||||
const SortByUnreadList = ({ ids, showUnreadCount, showCollapse, view }: SortListProps) => {
|
||||
const SortByUnreadList = ({ ids, showCollapse, view }: SortListProps) => {
|
||||
const isDesc = useFeedListSortSelector((s) => s.order === "desc")
|
||||
const sortByUnreadFeedList = useFeedUnreadStore((state) => {
|
||||
const res = ids.sort((a, b) => (state.data[b] || 0) - (state.data[a] || 0))
|
||||
|
|
@ -428,7 +426,6 @@ const SortByUnreadList = ({ ids, showUnreadCount, showCollapse, view }: SortList
|
|||
<Fragment>
|
||||
{sortByUnreadFeedList.map((feedId) => (
|
||||
<FeedItem
|
||||
showUnreadCount={showUnreadCount}
|
||||
key={feedId}
|
||||
feedId={feedId}
|
||||
view={view}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,12 @@ const handleClickPlay = () => {
|
|||
AudioPlayer.togglePlayAndPause()
|
||||
}
|
||||
|
||||
const setNowPlaying = (metadata: MediaMetadataInit) => {
|
||||
if ("mediaSession" in navigator) {
|
||||
navigator.mediaSession.metadata = new MediaMetadata(metadata)
|
||||
}
|
||||
}
|
||||
|
||||
export const CornerPlayer = () => {
|
||||
const show = useAudioPlayerAtomSelector((v) => v.show)
|
||||
const entryId = useAudioPlayerAtomSelector((v) => v.entryId)
|
||||
|
|
@ -102,6 +108,15 @@ const CornerPlayerImpl = () => {
|
|||
scopes: HotKeyScopeMap.Home,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
setNowPlaying({
|
||||
title: entry?.entries.title || undefined,
|
||||
artist: feed?.title || undefined,
|
||||
album: feed?.image || undefined,
|
||||
artwork: [{ src: entry?.entries.authorAvatar || feed?.image || "" }],
|
||||
})
|
||||
}, [entry, feed])
|
||||
|
||||
const navigateToEntry = useNavigateEntry()
|
||||
usePlayerTracker()
|
||||
|
||||
|
|
|
|||
|
|
@ -160,9 +160,8 @@ export function FeedColumn({ children, className }: PropsWithChildren<{ classNam
|
|||
shortcut={`${index + 1}`}
|
||||
className={cn(
|
||||
active === index && item.className,
|
||||
"flex flex-col items-center gap-1 text-xl",
|
||||
"flex h-11 flex-col items-center gap-1 text-xl",
|
||||
ELECTRON ? "hover:!bg-theme-vibrancyBg" : "",
|
||||
showSidebarUnreadCount && "h-11",
|
||||
active === index && useHotkeysSwitch ? "bg-zinc-500/30" : "",
|
||||
)}
|
||||
onClick={(e) => {
|
||||
|
|
@ -172,7 +171,7 @@ export function FeedColumn({ children, className }: PropsWithChildren<{ classNam
|
|||
}}
|
||||
>
|
||||
{item.icon}
|
||||
{showSidebarUnreadCount && (
|
||||
{showSidebarUnreadCount ? (
|
||||
<div className="text-[0.625rem] font-medium leading-none">
|
||||
{unreadByView[index] > 99 ? (
|
||||
<span className="-mr-0.5">99+</span>
|
||||
|
|
@ -180,6 +179,17 @@ export function FeedColumn({ children, className }: PropsWithChildren<{ classNam
|
|||
unreadByView[index]
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<i
|
||||
className={cn(
|
||||
"i-mgc-round-cute-fi text-[0.25rem]",
|
||||
unreadByView[index]
|
||||
? active === index
|
||||
? "opacity-100"
|
||||
: "opacity-60"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</ActionButton>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -8,15 +8,19 @@ import { FeedCertification } from "~/components/feed-certification"
|
|||
import { FeedIcon } from "~/components/feed-icon"
|
||||
import { OouiUserAnonymous } from "~/components/icons/OouiUserAnonymous"
|
||||
import { Tooltip, TooltipContent, TooltipPortal, TooltipTrigger } from "~/components/ui/tooltip"
|
||||
import { useFeedActions } from "~/hooks/biz/useFeedActions"
|
||||
import { EllipsisHorizontalTextWithTooltip } from "~/components/ui/typography"
|
||||
import { useFeedActions, useInboxActions, useListActions } from "~/hooks/biz/useFeedActions"
|
||||
import { useNavigateEntry } from "~/hooks/biz/useNavigateEntry"
|
||||
import { useRouteParamsSelector } from "~/hooks/biz/useRouteParams"
|
||||
import { useAnyPointDown } from "~/hooks/common"
|
||||
import { nextFrame } from "~/lib/dom"
|
||||
import type { FeedViewType } from "~/lib/enum"
|
||||
import { getNewIssueUrl } from "~/lib/issues"
|
||||
import { showNativeMenu } from "~/lib/native-menu"
|
||||
import { cn } from "~/lib/utils"
|
||||
import { getPreferredTitle, useFeedById } from "~/store/feed"
|
||||
import { useInboxById } from "~/store/inbox"
|
||||
import { useListById } from "~/store/list"
|
||||
import { subscriptionActions, useSubscriptionByFeedId } from "~/store/subscription"
|
||||
import { useFeedUnreadStore } from "~/store/unread"
|
||||
|
||||
|
|
@ -26,9 +30,8 @@ interface FeedItemProps {
|
|||
feedId: string
|
||||
view?: number
|
||||
className?: string
|
||||
showUnreadCount?: boolean
|
||||
}
|
||||
const FeedItemImpl = ({ view, feedId, className, showUnreadCount = true }: FeedItemProps) => {
|
||||
const FeedItemImpl = ({ view, feedId, className }: FeedItemProps) => {
|
||||
const { t } = useTranslation()
|
||||
const subscription = useSubscriptionByFeedId(feedId)
|
||||
const navigate = useNavigateEntry()
|
||||
|
|
@ -43,11 +46,6 @@ const FeedItemImpl = ({ view, feedId, className, showUnreadCount = true }: FeedI
|
|||
entryId: null,
|
||||
view,
|
||||
})
|
||||
if (feed?.type === "list") {
|
||||
subscriptionActions.markReadByFeedIds({
|
||||
listId: feedId,
|
||||
})
|
||||
}
|
||||
// focus to main container in order to let keyboard can navigate entry items by arrow keys
|
||||
nextFrame(() => {
|
||||
getMainContainerElement()?.focus()
|
||||
|
|
@ -69,7 +67,7 @@ const FeedItemImpl = ({ view, feedId, className, showUnreadCount = true }: FeedI
|
|||
if (!feed) return null
|
||||
|
||||
const isFeed = feed.type === "feed" || !feed.type
|
||||
const isList = feed.type === "list"
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
|
|
@ -119,15 +117,8 @@ const FeedItemImpl = ({ view, feedId, className, showUnreadCount = true }: FeedI
|
|||
isFeed && feed.errorAt && "text-red-900 dark:text-red-500",
|
||||
)}
|
||||
>
|
||||
<FeedIcon fallback feed={feed} size={isList ? 32 : 16} />
|
||||
<div
|
||||
className={cn(
|
||||
"truncate",
|
||||
!showUnreadCount && (feedUnread ? "font-bold" : "font-medium opacity-70"),
|
||||
)}
|
||||
>
|
||||
{getPreferredTitle(feed)}
|
||||
</div>
|
||||
<FeedIcon fallback feed={feed} size={16} />
|
||||
<div className="truncate">{getPreferredTitle(feed)}</div>
|
||||
{isFeed && <FeedCertification feed={feed} className="text-[15px]" />}
|
||||
{isFeed && feed.errorAt && (
|
||||
<Tooltip delayDuration={300}>
|
||||
|
|
@ -164,10 +155,148 @@ const FeedItemImpl = ({ view, feedId, className, showUnreadCount = true }: FeedI
|
|||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<UnreadNumber unread={feedUnread} className="ml-2" isList={isList} />
|
||||
<UnreadNumber unread={feedUnread} className="ml-2" />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const FeedItem = memo(FeedItemImpl)
|
||||
|
||||
const ListItemImpl: Component<{
|
||||
listId: string
|
||||
view: FeedViewType
|
||||
}> = ({ view, listId, className }) => {
|
||||
const list = useListById(listId)
|
||||
|
||||
const isActive = useRouteParamsSelector((routerParams) => routerParams.listId === listId)
|
||||
const { items } = useListActions({ listId, view })
|
||||
|
||||
const listUnread = useFeedUnreadStore((state) => state.data[listId] || 0)
|
||||
|
||||
const [isContextMenuOpen, setIsContextMenuOpen] = useState(false)
|
||||
useAnyPointDown(() => {
|
||||
setIsContextMenuOpen(false)
|
||||
})
|
||||
const subscription = useSubscriptionByFeedId(listId)
|
||||
const navigate = useNavigateEntry()
|
||||
const handleNavigate = useCallback(
|
||||
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation()
|
||||
|
||||
navigate({
|
||||
listId,
|
||||
entryId: null,
|
||||
view,
|
||||
})
|
||||
subscriptionActions.markReadByFeedIds({
|
||||
listId,
|
||||
})
|
||||
// focus to main container in order to let keyboard can navigate entry items by arrow keys
|
||||
nextFrame(() => {
|
||||
getMainContainerElement()?.focus()
|
||||
})
|
||||
},
|
||||
[listId, navigate, view],
|
||||
)
|
||||
const { t } = useTranslation()
|
||||
if (!list) return null
|
||||
return (
|
||||
<div
|
||||
data-list-id={listId}
|
||||
className={cn(
|
||||
"flex w-full cursor-menu items-center justify-between rounded-md pr-2.5 text-sm font-medium leading-loose",
|
||||
(isActive || isContextMenuOpen) && "bg-native-active",
|
||||
"py-1.5 pl-2.5",
|
||||
className,
|
||||
)}
|
||||
onClick={handleNavigate}
|
||||
onDoubleClick={() => {
|
||||
window.open(`${WEB_URL}/list/${listId}?view=${view}`, "_blank")
|
||||
}}
|
||||
onContextMenu={(e) => {
|
||||
setIsContextMenuOpen(true)
|
||||
|
||||
showNativeMenu(items, e)
|
||||
}}
|
||||
>
|
||||
<div className={"flex min-w-0 items-center"}>
|
||||
<FeedIcon fallback feed={list} size={28} />
|
||||
<EllipsisHorizontalTextWithTooltip className="truncate">
|
||||
{getPreferredTitle(list)}
|
||||
</EllipsisHorizontalTextWithTooltip>
|
||||
|
||||
{subscription.isPrivate && (
|
||||
<Tooltip delayDuration={300}>
|
||||
<TooltipTrigger>
|
||||
<OouiUserAnonymous className="ml-1 shrink-0 text-base" />
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent>{t("feed_item.not_publicly_visible")}</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<UnreadNumber unread={listUnread} isList className="ml-2" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const ListItem = memo(ListItemImpl)
|
||||
|
||||
const InboxItemImpl: Component<{
|
||||
inboxId: string
|
||||
view: FeedViewType
|
||||
}> = ({ view, inboxId, className }) => {
|
||||
const inbox = useInboxById(inboxId)
|
||||
|
||||
const isActive = useRouteParamsSelector((routerParams) => routerParams.inboxId === inboxId)
|
||||
const { items } = useInboxActions({ inboxId })
|
||||
|
||||
const inboxUnread = useFeedUnreadStore((state) => state.data[inboxId] || 0)
|
||||
|
||||
const [isContextMenuOpen, setIsContextMenuOpen] = useState(false)
|
||||
useAnyPointDown(() => {
|
||||
setIsContextMenuOpen(false)
|
||||
})
|
||||
const navigate = useNavigateEntry()
|
||||
const handleNavigate = useCallback(
|
||||
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation()
|
||||
|
||||
navigate({
|
||||
inboxId,
|
||||
entryId: null,
|
||||
view,
|
||||
})
|
||||
},
|
||||
[inboxId, navigate, view],
|
||||
)
|
||||
if (!inbox) return null
|
||||
return (
|
||||
<div
|
||||
data-inbox-id={inboxId}
|
||||
className={cn(
|
||||
"flex w-full cursor-menu items-center justify-between rounded-md pr-2.5 text-sm font-medium leading-loose",
|
||||
(isActive || isContextMenuOpen) && "bg-native-active",
|
||||
"py-[2px] pl-2.5",
|
||||
className,
|
||||
)}
|
||||
onClick={handleNavigate}
|
||||
onContextMenu={(e) => {
|
||||
setIsContextMenuOpen(true)
|
||||
showNativeMenu(items, e)
|
||||
}}
|
||||
>
|
||||
<div className={"flex min-w-0 items-center"}>
|
||||
<FeedIcon fallback feed={inbox} size={16} />
|
||||
<EllipsisHorizontalTextWithTooltip className="truncate">
|
||||
{getPreferredTitle(inbox)}
|
||||
</EllipsisHorizontalTextWithTooltip>
|
||||
</div>
|
||||
<UnreadNumber unread={inboxUnread} className="ml-2" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const InboxItem = memo(InboxItemImpl)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import * as HoverCard from "@radix-ui/react-hover-card"
|
||||
import { AnimatePresence, m } from "framer-motion"
|
||||
import { Fragment, memo, useMemo, useState } from "react"
|
||||
import { memo, useMemo, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link } from "react-router-dom"
|
||||
|
||||
import { useUISettingKey } from "~/atoms/settings/ui"
|
||||
import { ScrollArea } from "~/components/ui/scroll-area"
|
||||
import { IconOpacityTransition } from "~/components/ux/transition/icon"
|
||||
import { FEED_COLLECTION_LIST, views } from "~/constants"
|
||||
|
|
@ -13,20 +12,17 @@ import { useRouteFeedId } from "~/hooks/biz/useRouteParams"
|
|||
import { useAuthQuery } from "~/hooks/common"
|
||||
import { stopPropagation } from "~/lib/dom"
|
||||
import type { FeedViewType } from "~/lib/enum"
|
||||
import { cn, sortByAlphabet } from "~/lib/utils"
|
||||
import { cn } from "~/lib/utils"
|
||||
import { Queries } from "~/queries"
|
||||
import { getPreferredTitle, useFeedStore } from "~/store/feed"
|
||||
import { getSubscriptionByFeedId, useSubscriptionByView } from "~/store/subscription"
|
||||
import {
|
||||
subscriptionActions,
|
||||
useCategoryOpenStateByView,
|
||||
useSubscriptionByView,
|
||||
} from "~/store/subscription"
|
||||
import { useFeedUnreadStore } from "~/store/unread"
|
||||
|
||||
import {
|
||||
getFeedListSort,
|
||||
setFeedListSortBy,
|
||||
setFeedListSortOrder,
|
||||
useFeedListSort,
|
||||
useFeedListSortSelector,
|
||||
} from "./atom"
|
||||
import { FeedCategory } from "./category"
|
||||
import { getFeedListSort, setFeedListSortBy, setFeedListSortOrder, useFeedListSort } from "./atom"
|
||||
import { SortableFeedList, SortByAlphabeticalInbox, SortByAlphabeticalList } from "./sort-by"
|
||||
import { UnreadNumber } from "./unread-number"
|
||||
|
||||
const useFeedsGroupedData = (view: FeedViewType) => {
|
||||
|
|
@ -61,12 +57,32 @@ const useListsGroupedData = (view: FeedViewType) => {
|
|||
return useMemo(() => {
|
||||
if (!data || data.length === 0) return {}
|
||||
|
||||
const lists = data.filter((s) => "listId" in s)
|
||||
|
||||
const groupFolder = {} as Record<string, string[]>
|
||||
|
||||
for (const subscription of data) {
|
||||
if (!subscription.category && !subscription.defaultCategory) {
|
||||
groupFolder[subscription.feedId] = [subscription.feedId]
|
||||
}
|
||||
for (const subscription of lists) {
|
||||
groupFolder[subscription.feedId] = [subscription.feedId]
|
||||
}
|
||||
|
||||
return groupFolder
|
||||
}, [data])
|
||||
}
|
||||
|
||||
const useInboxesGroupedData = (view: FeedViewType) => {
|
||||
const { data: remoteData } = useAuthQuery(Queries.subscription.byView(view))
|
||||
|
||||
const data = useSubscriptionByView(view) || remoteData
|
||||
|
||||
return useMemo(() => {
|
||||
if (!data || data.length === 0) return {}
|
||||
|
||||
const inboxes = data.filter((s) => "inboxId" in s)
|
||||
|
||||
const groupFolder = {} as Record<string, string[]>
|
||||
|
||||
for (const subscription of inboxes) {
|
||||
groupFolder[subscription.feedId] = [subscription.feedId]
|
||||
}
|
||||
|
||||
return groupFolder
|
||||
|
|
@ -80,10 +96,11 @@ const useUpdateUnreadCount = () => {
|
|||
}
|
||||
|
||||
function FeedListImpl({ className, view }: { className?: string; view: number }) {
|
||||
const [expansion, setExpansion] = useState(false)
|
||||
const feedsData = useFeedsGroupedData(view)
|
||||
const listsData = useListsGroupedData(view)
|
||||
|
||||
const inboxesData = useInboxesGroupedData(view)
|
||||
const categoryOpenStateData = useCategoryOpenStateByView(view)
|
||||
const expansion = Object.values(categoryOpenStateData).every((value) => value === true)
|
||||
useUpdateUnreadCount()
|
||||
|
||||
const totalUnread = useFeedUnreadStore((state) => {
|
||||
|
|
@ -97,13 +114,22 @@ function FeedListImpl({ className, view }: { className?: string; view: number })
|
|||
return unread
|
||||
})
|
||||
|
||||
const hasData = Object.keys(feedsData).length > 0 || Object.keys(listsData).length > 0
|
||||
const hasData =
|
||||
Object.keys(feedsData).length > 0 ||
|
||||
Object.keys(listsData).length > 0 ||
|
||||
Object.keys(inboxesData).length > 0
|
||||
|
||||
const feedId = useRouteFeedId()
|
||||
const navigateEntry = useNavigateEntry()
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
// Data prefetch
|
||||
useAuthQuery(Queries.lists.list())
|
||||
|
||||
const hasListData = Object.keys(listsData).length > 0
|
||||
const hasInboxData = Object.keys(inboxesData).length > 0
|
||||
|
||||
return (
|
||||
<div className={cn(className, "font-medium")}>
|
||||
<div onClick={stopPropagation} className="mx-3 flex items-center justify-between px-2.5 py-1">
|
||||
|
|
@ -125,9 +151,15 @@ function FeedListImpl({ className, view }: { className?: string; view: number })
|
|||
<div className="ml-2 flex items-center gap-3 text-sm text-theme-vibrancyFg">
|
||||
<SortButton />
|
||||
{expansion ? (
|
||||
<i className="i-mgc-list-collapse-cute-re" onClick={() => setExpansion(false)} />
|
||||
<i
|
||||
className="i-mgc-list-collapse-cute-re"
|
||||
onClick={() => subscriptionActions.expandCategoryOpenStateByView(view, false)}
|
||||
/>
|
||||
) : (
|
||||
<i className="i-mgc-list-expansion-cute-re" onClick={() => setExpansion(true)} />
|
||||
<i
|
||||
className="i-mgc-list-expansion-cute-re"
|
||||
onClick={() => subscriptionActions.expandCategoryOpenStateByView(view, true)}
|
||||
/>
|
||||
)}
|
||||
<UnreadNumber unread={totalUnread} className="text-xs !text-inherit" />
|
||||
</div>
|
||||
|
|
@ -153,24 +185,38 @@ function FeedListImpl({ className, view }: { className?: string; view: number })
|
|||
<i className="i-mgc-star-cute-fi size-4 -translate-y-px text-amber-500" />
|
||||
{t("words.starred")}
|
||||
</div>
|
||||
{Object.keys(listsData).length > 0 && (
|
||||
{hasListData && (
|
||||
<>
|
||||
<div className="mt-1 flex h-6 w-full shrink-0 items-center rounded-md px-2.5 text-xs font-semibold text-theme-vibrancyFg transition-colors">
|
||||
{t("words.lists")}
|
||||
</div>
|
||||
<SortableList view={view} expansion={expansion} data={listsData} by="alphabetical" />
|
||||
<SortByAlphabeticalList view={view} data={listsData} />
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-6 w-full shrink-0 items-center rounded-md px-2.5 text-xs font-semibold text-theme-vibrancyFg transition-colors",
|
||||
Object.keys(feedsData).length === 0 ? "mt-0" : "mt-1",
|
||||
)}
|
||||
>
|
||||
{t("words.feeds")}
|
||||
</div>
|
||||
{hasInboxData && (
|
||||
<>
|
||||
<div className="mt-1 flex h-6 w-full shrink-0 items-center rounded-md px-2.5 text-xs font-semibold text-theme-vibrancyFg transition-colors">
|
||||
{t("words.inbox")}
|
||||
</div>
|
||||
<SortByAlphabeticalInbox view={view} data={inboxesData} />
|
||||
</>
|
||||
)}
|
||||
{(hasListData || hasInboxData) && (
|
||||
<div
|
||||
className={cn(
|
||||
"mb-1 flex h-6 w-full shrink-0 items-center rounded-md px-2.5 text-xs font-semibold text-theme-vibrancyFg transition-colors",
|
||||
Object.keys(feedsData).length === 0 ? "mt-0" : "mt-1",
|
||||
)}
|
||||
>
|
||||
{t("words.feeds")}
|
||||
</div>
|
||||
)}
|
||||
{hasData ? (
|
||||
<SortableList view={view} expansion={expansion} data={feedsData} />
|
||||
<SortableFeedList
|
||||
view={view}
|
||||
data={feedsData}
|
||||
categoryOpenStateData={categoryOpenStateData}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full flex-1 items-center font-normal text-zinc-500">
|
||||
<Link
|
||||
|
|
@ -278,114 +324,4 @@ const SortButton = () => {
|
|||
)
|
||||
}
|
||||
|
||||
type FeedListProps = {
|
||||
view: number
|
||||
expansion: boolean
|
||||
data: Record<string, string[]>
|
||||
}
|
||||
const SortByUnreadList = ({ view, expansion, data }: FeedListProps) => {
|
||||
const showUnreadCount = useUISettingKey("sidebarShowUnreadCount")
|
||||
const isDesc = useFeedListSortSelector((s) => s.order === "desc")
|
||||
|
||||
const sortedByUnread = useFeedUnreadStore((state) => {
|
||||
const sortedList = [] as [string, string[]][]
|
||||
const folderUnread = {} as Record<string, number>
|
||||
// Calc total unread count for each folder
|
||||
for (const category in data) {
|
||||
folderUnread[category] = data[category].reduce((acc, cur) => (state.data[cur] || 0) + acc, 0)
|
||||
}
|
||||
|
||||
// Sort by unread count
|
||||
Object.keys(folderUnread)
|
||||
.sort((a, b) => folderUnread[b] - folderUnread[a])
|
||||
.forEach((key) => {
|
||||
sortedList.push([key, data[key]])
|
||||
})
|
||||
|
||||
if (!isDesc) {
|
||||
sortedList.reverse()
|
||||
}
|
||||
return sortedList
|
||||
})
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{sortedByUnread?.map(([category, ids]) => (
|
||||
<FeedCategory
|
||||
showUnreadCount={showUnreadCount}
|
||||
key={category}
|
||||
data={ids}
|
||||
view={view}
|
||||
expansion={expansion}
|
||||
/>
|
||||
))}
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
const SortByAlphabeticalList = ({ view, expansion, data }: FeedListProps) => {
|
||||
const categoryName2RealDisplayNameMap = useFeedStore((state) => {
|
||||
const map = {} as Record<string, string>
|
||||
for (const categoryName in data) {
|
||||
const feedId = data[categoryName][0]
|
||||
|
||||
if (!feedId) {
|
||||
continue
|
||||
}
|
||||
const feed = state.feeds[feedId]
|
||||
if (!feed) {
|
||||
continue
|
||||
}
|
||||
const hascategoryNameNotDefault = !!getSubscriptionByFeedId(feedId)?.category
|
||||
const isSingle = data[categoryName].length === 1
|
||||
if (!isSingle || hascategoryNameNotDefault) {
|
||||
map[categoryName] = categoryName
|
||||
} else {
|
||||
map[categoryName] = getPreferredTitle(feed)!
|
||||
}
|
||||
}
|
||||
return map
|
||||
})
|
||||
|
||||
const isDesc = useFeedListSortSelector((s) => s.order === "desc")
|
||||
|
||||
let sortedByAlphabetical = Object.keys(data).sort((a, b) => {
|
||||
const nameA = categoryName2RealDisplayNameMap[a]
|
||||
const nameB = categoryName2RealDisplayNameMap[b]
|
||||
return sortByAlphabet(nameA, nameB)
|
||||
})
|
||||
if (!isDesc) {
|
||||
sortedByAlphabetical = sortedByAlphabetical.reverse()
|
||||
}
|
||||
|
||||
const showUnreadCount = useUISettingKey("sidebarShowUnreadCount")
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{sortedByAlphabetical.map((category) => (
|
||||
<FeedCategory
|
||||
showUnreadCount={showUnreadCount}
|
||||
key={category}
|
||||
data={data[category]}
|
||||
view={view}
|
||||
expansion={expansion}
|
||||
/>
|
||||
))}
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
const SortableList = (props: FeedListProps & { by?: "count" | "alphabetical" }) => {
|
||||
const userBy = useFeedListSortSelector((s) => s.by)
|
||||
|
||||
switch (props.by || userBy) {
|
||||
case "count": {
|
||||
return <SortByUnreadList {...props} />
|
||||
}
|
||||
case "alphabetical": {
|
||||
return <SortByAlphabeticalList {...props} />
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const FeedList = memo(FeedListImpl)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
import { Fragment } from "react"
|
||||
|
||||
import { sortByAlphabet } from "~/lib/utils"
|
||||
import { getPreferredTitle, useFeedStore } from "~/store/feed"
|
||||
import { getSubscriptionByFeedId } from "~/store/subscription"
|
||||
|
||||
import { useFeedListSortSelector } from "../atom"
|
||||
import { FeedCategory } from "../category"
|
||||
import { InboxItem, ListItem } from "../item"
|
||||
import type { FeedListProps, ListListProps } from "./types"
|
||||
|
||||
export const SortByAlphabeticalFeedList = ({
|
||||
view,
|
||||
data,
|
||||
categoryOpenStateData,
|
||||
}: FeedListProps) => {
|
||||
const categoryName2RealDisplayNameMap = useFeedStore((state) => {
|
||||
const map = {} as Record<string, string>
|
||||
for (const categoryName in data) {
|
||||
const feedId = data[categoryName][0]
|
||||
|
||||
if (!feedId) {
|
||||
continue
|
||||
}
|
||||
const feed = state.feeds[feedId]
|
||||
if (!feed) {
|
||||
continue
|
||||
}
|
||||
const hascategoryNameNotDefault = !!getSubscriptionByFeedId(feedId)?.category
|
||||
const isSingle = data[categoryName].length === 1
|
||||
if (!isSingle || hascategoryNameNotDefault) {
|
||||
map[categoryName] = categoryName
|
||||
} else {
|
||||
map[categoryName] = getPreferredTitle(feed)!
|
||||
}
|
||||
}
|
||||
return map
|
||||
})
|
||||
|
||||
const isDesc = useFeedListSortSelector((s) => s.order === "desc")
|
||||
|
||||
let sortedByAlphabetical = Object.keys(data).sort((a, b) => {
|
||||
const nameA = categoryName2RealDisplayNameMap[a]
|
||||
const nameB = categoryName2RealDisplayNameMap[b]
|
||||
return sortByAlphabet(nameA, nameB)
|
||||
})
|
||||
if (!isDesc) {
|
||||
sortedByAlphabetical = sortedByAlphabetical.reverse()
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{sortedByAlphabetical.map((category) => (
|
||||
<FeedCategory
|
||||
key={category}
|
||||
data={data[category]}
|
||||
view={view}
|
||||
categoryOpenStateData={categoryOpenStateData}
|
||||
/>
|
||||
))}
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
export const SortByAlphabeticalListList = ({ view, data }: ListListProps) => {
|
||||
return (
|
||||
<div>
|
||||
{Object.keys(data).map((listId) => (
|
||||
<ListItem key={listId} listId={listId} view={view} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const SortByAlphabeticalInboxList = ({ view, data }: ListListProps) => {
|
||||
return (
|
||||
<div>
|
||||
{Object.keys(data).map((inboxId) => (
|
||||
<InboxItem key={inboxId} inboxId={inboxId} view={view} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import { Fragment } from "react"
|
||||
|
||||
import { useFeedUnreadStore } from "~/store/unread"
|
||||
|
||||
import { useFeedListSortSelector } from "../atom"
|
||||
import { FeedCategory } from "../category"
|
||||
import type { FeedListProps } from "./types"
|
||||
|
||||
export const SortByUnreadFeedList = ({ view, data, categoryOpenStateData }: FeedListProps) => {
|
||||
const isDesc = useFeedListSortSelector((s) => s.order === "desc")
|
||||
|
||||
const sortedByUnread = useFeedUnreadStore((state) => {
|
||||
const sortedList = [] as [string, string[]][]
|
||||
const folderUnread = {} as Record<string, number>
|
||||
// Calc total unread count for each folder
|
||||
for (const category in data) {
|
||||
folderUnread[category] = data[category].reduce((acc, cur) => (state.data[cur] || 0) + acc, 0)
|
||||
}
|
||||
|
||||
// Sort by unread count
|
||||
Object.keys(folderUnread)
|
||||
.sort((a, b) => folderUnread[b] - folderUnread[a])
|
||||
.forEach((key) => {
|
||||
sortedList.push([key, data[key]])
|
||||
})
|
||||
|
||||
if (!isDesc) {
|
||||
sortedList.reverse()
|
||||
}
|
||||
return sortedList
|
||||
})
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{sortedByUnread?.map(([category, ids]) => (
|
||||
<FeedCategory
|
||||
key={category}
|
||||
data={ids}
|
||||
view={view}
|
||||
categoryOpenStateData={categoryOpenStateData}
|
||||
/>
|
||||
))}
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
import { useFeedListSortSelector } from "../atom"
|
||||
import {
|
||||
SortByAlphabeticalFeedList,
|
||||
SortByAlphabeticalInboxList,
|
||||
SortByAlphabeticalListList,
|
||||
} from "./SortByAlphabeticalList"
|
||||
import { SortByUnreadFeedList } from "./SortByUnreadList"
|
||||
import type { FeedListProps, ListListProps } from "./types"
|
||||
|
||||
export const SortableFeedList = (props: FeedListProps) => {
|
||||
const by = useFeedListSortSelector((s) => s.by)
|
||||
|
||||
switch (by) {
|
||||
case "count": {
|
||||
return <SortByUnreadFeedList {...props} />
|
||||
}
|
||||
case "alphabetical": {
|
||||
return <SortByAlphabeticalFeedList {...props} />
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const SortByAlphabeticalList = (props: ListListProps) => {
|
||||
const by = useFeedListSortSelector((s) => s.by)
|
||||
|
||||
switch (by) {
|
||||
default: {
|
||||
return <SortByAlphabeticalListList {...props} />
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const SortByAlphabeticalInbox = (props: ListListProps) => {
|
||||
const by = useFeedListSortSelector((s) => s.by)
|
||||
|
||||
switch (by) {
|
||||
default: {
|
||||
return <SortByAlphabeticalInboxList {...props} />
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import type { FeedViewType } from "~/lib/enum"
|
||||
|
||||
export type FeedListProps = {
|
||||
view: number
|
||||
data: Record<string, string[]>
|
||||
categoryOpenStateData: Record<string, boolean>
|
||||
}
|
||||
export type SortBy = "count" | "alphabetical"
|
||||
|
||||
export type ListListProps = {
|
||||
view: FeedViewType
|
||||
data: Record<string, string[]>
|
||||
}
|
||||
|
|
@ -11,17 +11,16 @@ export const UnreadNumber = ({
|
|||
isList?: boolean
|
||||
}) => {
|
||||
const showUnreadCount = useUISettingKey("sidebarShowUnreadCount")
|
||||
if (!showUnreadCount) return null
|
||||
|
||||
if (!unread) return null
|
||||
return (
|
||||
<div
|
||||
className={cn("text-[0.65rem] tabular-nums text-zinc-400 dark:text-neutral-500", className)}
|
||||
>
|
||||
{isList ? (
|
||||
<i className="block size-1.5 rounded-full bg-zinc-400 dark:bg-neutral-500" />
|
||||
) : (
|
||||
unread
|
||||
className={cn(
|
||||
"center text-[0.65rem] tabular-nums text-zinc-400 dark:text-neutral-500",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{isList || !showUnreadCount ? <i className="i-mgc-round-cute-fi text-[0.3rem]" /> : unread}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,16 +81,26 @@ const ShortcutModalContent = () => {
|
|||
}
|
||||
|
||||
export const useShortcutsModal = () => {
|
||||
const { present } = useModalStack()
|
||||
const { present, dismiss, getModalStackById } = useModalStack()
|
||||
const id = "shortcuts"
|
||||
|
||||
return useCallback(() => {
|
||||
const showShortcutsModal = useCallback(() => {
|
||||
present({
|
||||
title: "Shortcuts",
|
||||
id,
|
||||
overlay: false,
|
||||
id: "shortcuts",
|
||||
content: () => <ShortcutModalContent />,
|
||||
CustomModalComponent: PlainModal,
|
||||
clickOutsideToDismiss: true,
|
||||
})
|
||||
}, [present])
|
||||
|
||||
return useCallback(() => {
|
||||
const shortcutsModal = getModalStackById(id)
|
||||
if (shortcutsModal && shortcutsModal.modal) {
|
||||
dismiss(id)
|
||||
return
|
||||
}
|
||||
showShortcutsModal()
|
||||
}, [dismiss, getModalStackById, showShortcutsModal])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,18 +49,22 @@ export const usePresentUserProfileModal = (variant: Variant = "dialog") => {
|
|||
return useCallback(
|
||||
(userId: string | undefined, overrideVariant?: Variant) => {
|
||||
if (!userId) return
|
||||
const finalVariant = overrideVariant || variant
|
||||
|
||||
present({
|
||||
title: "User Profile",
|
||||
content: () =>
|
||||
createElement(UserProfileModalContent, {
|
||||
userId,
|
||||
variant: overrideVariant || variant,
|
||||
variant: finalVariant,
|
||||
}),
|
||||
CustomModalComponent: PlainModal,
|
||||
clickOutsideToDismiss: true,
|
||||
modal: variant === "dialog",
|
||||
overlay: variant === "dialog",
|
||||
modalContainerClassName: variant === "drawer" ? "right-4 left-[auto] top-4 bottom-4" : "",
|
||||
modal: finalVariant === "dialog",
|
||||
overlay: finalVariant === "dialog",
|
||||
autoFocus: false,
|
||||
modalContainerClassName:
|
||||
finalVariant === "drawer" ? tw`right-4 left-[auto] safe-inset-top-4 bottom-4` : "",
|
||||
})
|
||||
},
|
||||
[present, variant],
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@ export const UserProfileModalContent: FC<{
|
|||
className={cn(
|
||||
"relative flex flex-col items-center overflow-hidden rounded-xl border bg-theme-background p-8 pb-0",
|
||||
variant === "drawer"
|
||||
? "shadow-drawer-left h-full w-[60ch] max-w-full"
|
||||
? "shadow-drawer-to-left h-full w-[60ch] max-w-full"
|
||||
: "h-[80vh] w-[800px] max-w-full shadow lg:max-h-[calc(100vh-10rem)]",
|
||||
)}
|
||||
>
|
||||
|
|
@ -234,7 +234,7 @@ export const UserProfileModalContent: FC<{
|
|||
<Fragment>
|
||||
<div
|
||||
className={cn(
|
||||
"center m-12 mb-4 flex shrink-0 flex-col",
|
||||
"center m-12 mb-4 flex shrink-0 flex-col f-motion-reduce:duration-700",
|
||||
isHeaderSimple ? "mt-3 flex-row" : "flex-col",
|
||||
)}
|
||||
>
|
||||
|
|
@ -242,7 +242,7 @@ export const UserProfileModalContent: FC<{
|
|||
asChild
|
||||
className={cn("aspect-square", isHeaderSimple ? "size-12" : "size-16")}
|
||||
>
|
||||
<m.span layout>
|
||||
<m.span layout transition={{ duration: 0.35 }}>
|
||||
<AvatarImage
|
||||
className="duration-200 animate-in fade-in-0"
|
||||
asChild
|
||||
|
|
@ -255,6 +255,7 @@ export const UserProfileModalContent: FC<{
|
|||
</Avatar>
|
||||
<m.div
|
||||
layout
|
||||
transition={{ duration: 0.35 }}
|
||||
className={cn(
|
||||
"flex cursor-text select-text flex-col items-center",
|
||||
isHeaderSimple ? "ml-8 items-start" : "",
|
||||
|
|
@ -327,7 +328,7 @@ const SubscriptionGroup: FC<{
|
|||
<div>
|
||||
<button
|
||||
onClick={() => setIsOpened(!isOpened)}
|
||||
className="mb-2 flex w-full items-center justify-between text-2xl font-bold"
|
||||
className="mb-2 mt-8 flex w-full items-center justify-between text-2xl font-bold"
|
||||
type="button"
|
||||
>
|
||||
<h3>{category}</h3>
|
||||
|
|
@ -370,7 +371,7 @@ const SubscriptionItem: FC<{
|
|||
return (
|
||||
<m.div
|
||||
exit={{ opacity: 0, scale: 0.98, transition: { duration: 0.2 } }}
|
||||
className={cn("group relative", isLoose ? "border-b py-5" : "py-2")}
|
||||
className={cn("group relative", isLoose ? "border-b py-5 last:border-b-0" : "py-2")}
|
||||
data-feed-id={subscription.feedId}
|
||||
>
|
||||
<a
|
||||
|
|
|
|||
|
|
@ -27,35 +27,7 @@ import {
|
|||
import { ViewSelectContent } from "~/components/view-select-content"
|
||||
import { stopPropagation } from "~/lib/dom"
|
||||
import { cn } from "~/lib/utils"
|
||||
import type {
|
||||
ActionEntryField,
|
||||
ActionFeedField,
|
||||
ActionOperation,
|
||||
SupportedLanguages,
|
||||
} from "~/models"
|
||||
|
||||
type ActionsInput = {
|
||||
name: string
|
||||
condition: {
|
||||
field?: ActionFeedField
|
||||
operator?: ActionOperation
|
||||
value?: string
|
||||
}[]
|
||||
result: {
|
||||
translation?: string
|
||||
summary?: boolean
|
||||
readability?: boolean
|
||||
rewriteRules?: {
|
||||
from: string
|
||||
to: string
|
||||
}[]
|
||||
blockRules?: {
|
||||
field?: ActionEntryField
|
||||
operator?: ActionOperation
|
||||
value?: string | number
|
||||
}[]
|
||||
}
|
||||
}[]
|
||||
import type { ActionFeedField, ActionOperation, ActionsInput, SupportedLanguages } from "~/models"
|
||||
|
||||
const TransitionOptions: {
|
||||
name: string
|
||||
|
|
@ -620,6 +592,56 @@ export function ActionCard({
|
|||
</>
|
||||
)}
|
||||
</SettingCollapsible>
|
||||
<Divider />
|
||||
|
||||
<SettingCollapsible
|
||||
title={t("actions.action_card.webhooks")}
|
||||
open={!!data.result.webhooks}
|
||||
onOpenChange={(open) => {
|
||||
if (open) {
|
||||
data.result.webhooks = [""]
|
||||
} else {
|
||||
delete data.result.webhooks
|
||||
}
|
||||
onChange(data)
|
||||
}}
|
||||
>
|
||||
{data.result.webhooks && data.result.webhooks.length > 0 && (
|
||||
<>
|
||||
{data.result.webhooks.map((webhook, rewriteIdx) => {
|
||||
return (
|
||||
<div key={rewriteIdx} className="flex items-center gap-2">
|
||||
<DeleteTableCell
|
||||
onClick={() => {
|
||||
if (data.result.webhooks?.length === 1) {
|
||||
delete data.result.webhooks
|
||||
} else {
|
||||
data.result.webhooks?.splice(rewriteIdx, 1)
|
||||
}
|
||||
onChange(data)
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
value={webhook}
|
||||
className="h-8"
|
||||
placeholder="https://"
|
||||
onChange={(e) => {
|
||||
data.result.webhooks![rewriteIdx] = e.target.value
|
||||
onChange(data)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<AddTableRow
|
||||
onClick={() => {
|
||||
data.result.webhooks!.push("")
|
||||
onChange(data)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</SettingCollapsible>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { BoundingBox } from "framer-motion"
|
||||
import { useDragControls } from "framer-motion"
|
||||
import { Resizable } from "re-resizable"
|
||||
import type { PointerEventHandler, PropsWithChildren } from "react"
|
||||
|
|
@ -8,8 +9,9 @@ import { m } from "~/components/common/Motion"
|
|||
import { Logo } from "~/components/icons/logo"
|
||||
import { LetsIconsResizeDownRightLight } from "~/components/icons/resize"
|
||||
import { useResizeableModal } from "~/components/ui/modal"
|
||||
import { ElECTRON_CUSTOM_TITLEBAR_HEIGHT } from "~/constants"
|
||||
import { preventDefault } from "~/lib/dom"
|
||||
import { cn } from "~/lib/utils"
|
||||
import { cn, getOS } from "~/lib/utils"
|
||||
|
||||
import { settings } from "../constants"
|
||||
import { SettingSyncIndicator } from "../helper/SyncIndicator"
|
||||
|
|
@ -26,6 +28,7 @@ export function SettingModalLayout(
|
|||
const setTab = useSetSettingTab()
|
||||
const tab = useSettingTab()
|
||||
const elementRef = useRef<HTMLDivElement>(null)
|
||||
const edgeElementRef = useRef<HTMLDivElement>(null)
|
||||
const { handlePointDown, isResizeable, resizeableStyle } = useResizeableModal(elementRef, {
|
||||
enableResizeable: true,
|
||||
})
|
||||
|
|
@ -54,16 +57,25 @@ export function SettingModalLayout(
|
|||
},
|
||||
[dragController, draggable, handlePointDown],
|
||||
)
|
||||
const measureDragConstraints = useCallback((constraints: BoundingBox) => {
|
||||
if (getOS() === "Windows") {
|
||||
return {
|
||||
...constraints,
|
||||
top: constraints.top + ElECTRON_CUSTOM_TITLEBAR_HEIGHT,
|
||||
}
|
||||
}
|
||||
return constraints
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className={cn("h-full", !isResizeable && "center")}>
|
||||
<div className={cn("h-full", !isResizeable && "center")} ref={edgeElementRef}>
|
||||
<m.div
|
||||
exit={{
|
||||
opacity: 0,
|
||||
scale: 0.96,
|
||||
}}
|
||||
className={cn(
|
||||
"relative flex overflow-hidden rounded-xl rounded-br-none border border-border",
|
||||
"relative flex rounded-xl rounded-br-none border border-border",
|
||||
!overlay && "shadow-perfect",
|
||||
)}
|
||||
style={resizeableStyle}
|
||||
|
|
@ -73,6 +85,8 @@ export function SettingModalLayout(
|
|||
dragListener={false}
|
||||
dragMomentum={false}
|
||||
dragElastic={false}
|
||||
dragConstraints={edgeElementRef}
|
||||
onMeasureDragConstraints={measureDragConstraints}
|
||||
whileDrag={{
|
||||
cursor: "grabbing",
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
TableHeader,
|
||||
TableRow,
|
||||
} from "~/components/ui/table"
|
||||
import { EllipsisHorizontalTextWithTooltip } from "~/components/ui/typography"
|
||||
import { useAuthQuery } from "~/hooks/common"
|
||||
import { Balance } from "~/modules/wallet/balance"
|
||||
import { Queries } from "~/queries"
|
||||
|
|
@ -57,9 +58,9 @@ export const SettingFeeds = () => {
|
|||
className="flex items-center"
|
||||
>
|
||||
<FeedIcon fallback feed={row.feed} size={16} />
|
||||
<span className="inline-block max-w-[200px] truncate">
|
||||
<EllipsisHorizontalTextWithTooltip className="inline-block max-w-[200px] truncate">
|
||||
{row.feed.title}
|
||||
</span>
|
||||
</EllipsisHorizontalTextWithTooltip>
|
||||
</a>
|
||||
</TableCell>
|
||||
<TableCell align="center" className="tabular-nums" size="sm">
|
||||
|
|
|
|||
|
|
@ -245,13 +245,15 @@ export const LanguageSelector = () => {
|
|||
const originalLanguageName = defaultResources[lang].lang.name
|
||||
return (
|
||||
<SelectItem className="group" key={lang} value={lang}>
|
||||
<span className={cn(originalLanguageName !== languageName && "group-hover:hidden")}>
|
||||
<span
|
||||
className={cn(originalLanguageName !== languageName && "group-hover:invisible")}
|
||||
>
|
||||
{languageName}
|
||||
{typeof percent === "number" ? (percent >= 100 ? null : ` (${percent}%)`) : null}
|
||||
</span>
|
||||
{originalLanguageName !== languageName && (
|
||||
<span
|
||||
className="hidden duration-500 animate-in fade-in-0 group-hover:block"
|
||||
className="absolute inset-0 hidden items-center pl-2 group-hover:flex"
|
||||
key={"org"}
|
||||
>
|
||||
{originalLanguageName}
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ export const SettingInvitations = () => {
|
|||
})
|
||||
}}
|
||||
>
|
||||
<i className="i-mgc-heart-hand-cute-re mr-1 text-base" />
|
||||
<i className="i-mgc-love-cute-re mr-1 text-base" />
|
||||
{t("invitation.generateButton")}
|
||||
</Button>
|
||||
<Divider className="mb-6 mt-8" />
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ import { views } from "~/constants"
|
|||
import { useAuthQuery, useI18n } from "~/hooks/common"
|
||||
import { apiClient } from "~/lib/api-fetch"
|
||||
import { cn, isBizId } from "~/lib/utils"
|
||||
import type { FeedModel, ListModel } from "~/models"
|
||||
import type { FeedModel } from "~/models"
|
||||
import { ViewSelectorRadioGroup } from "~/modules/shared/ViewSelectorRadioGroup"
|
||||
import { Balance } from "~/modules/wallet/balance"
|
||||
import { Queries } from "~/queries"
|
||||
|
|
@ -50,6 +50,7 @@ import {
|
|||
useFeedById,
|
||||
useRemoveFeedFromFeedList,
|
||||
} from "~/store/feed"
|
||||
import { useListById } from "~/store/list"
|
||||
import { subscriptionActions, useSubscriptionStore } from "~/store/subscription"
|
||||
|
||||
export const SettingLists = () => {
|
||||
|
|
@ -86,7 +87,9 @@ export const SettingLists = () => {
|
|||
<TableHead size="sm">{t.settings("lists.fee.label")}</TableHead>
|
||||
<TableHead size="sm">{t.settings("lists.subscriptions")}</TableHead>
|
||||
<TableHead size="sm">{t.settings("lists.earnings")}</TableHead>
|
||||
<TableHead size="sm">{t.common("words.actions")}</TableHead>
|
||||
<TableHead size="sm" className="center">
|
||||
{t.common("words.actions")}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody className="border-t-[12px] border-transparent [&_td]:!px-3">
|
||||
|
|
@ -107,9 +110,18 @@ export const SettingLists = () => {
|
|||
</a>
|
||||
</TableCell>
|
||||
<TableCell size="sm">
|
||||
<span className={cn("inline-flex items-center", views[row.view].className)}>
|
||||
{views[row.view].icon}
|
||||
</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span
|
||||
className={cn("inline-flex items-center", views[row.view].className)}
|
||||
>
|
||||
{views[row.view].icon}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent>{t(views[row.view].name)}</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
<TableCell size="sm">
|
||||
<div className="flex items-center gap-1">
|
||||
|
|
@ -121,7 +133,7 @@ export const SettingLists = () => {
|
|||
<TableCell size="sm">
|
||||
<Balance>{BigInt(row.purchaseAmount || 0n)}</Balance>
|
||||
</TableCell>
|
||||
<TableCell size="sm">
|
||||
<TableCell size="sm" className="center">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
|
|
@ -187,11 +199,9 @@ const formSchema = z.object({
|
|||
})
|
||||
|
||||
const ListCreationModalContent = ({ dismiss, id }: { dismiss: () => void; id?: string }) => {
|
||||
const { t } = useTranslation("settings")
|
||||
const { t } = useTranslation(["settings", "common"])
|
||||
|
||||
const appT = useI18n()
|
||||
|
||||
const list = useFeedById(id) as ListModel
|
||||
const list = useListById(id)
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
|
|
@ -228,6 +238,7 @@ const ListCreationModalContent = ({ dismiss, id }: { dismiss: () => void; id?: s
|
|||
Queries.lists.list().invalidate()
|
||||
dismiss()
|
||||
|
||||
if (!list) return
|
||||
if (id) subscriptionActions.changeListView(id, views[list.view].view, views[values.view].view)
|
||||
},
|
||||
async onError() {
|
||||
|
|
@ -334,7 +345,7 @@ const ListCreationModalContent = ({ dismiss, id }: { dismiss: () => void; id?: s
|
|||
/>
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit" isLoading={createMutation.isPending}>
|
||||
{id ? appT.common("words.update") : appT.common("words.create")}
|
||||
{id ? t("common:words.update") : t("common:words.create")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -343,11 +354,15 @@ const ListCreationModalContent = ({ dismiss, id }: { dismiss: () => void; id?: s
|
|||
}
|
||||
|
||||
export const ListFeedsModalContent = ({ id }: { id: string }) => {
|
||||
const list = useFeedById(id) as ListModel
|
||||
const list = useListById(id)
|
||||
const { t } = useTranslation("settings")
|
||||
|
||||
const [feedSearchFor, setFeedSearchFor] = useState("")
|
||||
const addMutation = useAddFeedToFeedList()
|
||||
const addMutation = useAddFeedToFeedList({
|
||||
onSuccess: () => {
|
||||
setFeedSearchFor("")
|
||||
},
|
||||
})
|
||||
|
||||
const allFeeds = useSubscriptionStore((store) => {
|
||||
const feedInfo = [] as { title: string; id: string }[]
|
||||
|
|
@ -366,14 +381,15 @@ export const ListFeedsModalContent = ({ id }: { id: string }) => {
|
|||
|
||||
const autocompleteSuggestions: Suggestion[] = useMemo(() => {
|
||||
return allFeeds
|
||||
.filter((feed) => !list.feedIds?.includes(feed.id))
|
||||
.filter((feed) => !list?.feedIds?.includes(feed.id))
|
||||
.map((feed) => ({
|
||||
name: feed.title,
|
||||
value: feed.id,
|
||||
}))
|
||||
}, [allFeeds, list.feedIds])
|
||||
}, [allFeeds, list?.feedIds])
|
||||
|
||||
const selectedFeedIdRef = useRef<string | null>()
|
||||
if (!list) return null
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { createElement, useCallback } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { toast } from "sonner"
|
||||
|
||||
import { useModalStack } from "~/components/ui/modal/stacked/hooks"
|
||||
|
|
@ -15,7 +16,7 @@ export const useTipModal = ({
|
|||
entryId?: string
|
||||
}) => {
|
||||
const { present } = useModalStack()
|
||||
|
||||
const { t } = useTranslation()
|
||||
return useCallback(() => {
|
||||
if (!feedId || !entryId) {
|
||||
// this should not happen unless there is a bug in the code
|
||||
|
|
@ -24,7 +25,7 @@ export const useTipModal = ({
|
|||
}
|
||||
window.posthog?.capture("tip_modal_opened", { entryId })
|
||||
present({
|
||||
title: "Tip Power",
|
||||
title: t("tip_modal.tip_title"),
|
||||
content: () => createElement(TipModalContent, { userId, feedId, entryId }),
|
||||
})
|
||||
}, [present, userId, feedId, entryId])
|
||||
|
|
|
|||
|
|
@ -13,14 +13,10 @@ import { LoadingCircle } from "~/components/ui/loading"
|
|||
import { views } from "~/constants"
|
||||
import { usePresentFeedFormModal } from "~/hooks/biz/useFeedFormModal"
|
||||
import { useTitle } from "~/hooks/common"
|
||||
import { FeedViewType } from "~/lib/enum"
|
||||
import type { FeedViewType } from "~/lib/enum"
|
||||
import { cn } from "~/lib/utils"
|
||||
import type { FeedModel } from "~/models"
|
||||
import { ArticleItem } from "~/modules/entry-column/Items/article-item"
|
||||
import { NotificationItem } from "~/modules/entry-column/Items/notification-item"
|
||||
import { PictureItem } from "~/modules/entry-column/Items/picture-item"
|
||||
import { SocialMediaItem } from "~/modules/entry-column/Items/social-media-item"
|
||||
import { VideoItem } from "~/modules/entry-column/Items/video-item"
|
||||
import { getItemComponentByView } from "~/modules/entry-column/Items"
|
||||
import type { UniversalItemProps } from "~/modules/entry-column/types"
|
||||
import { useEntriesPreview } from "~/queries/entries"
|
||||
import { useFeed } from "~/queries/feed"
|
||||
|
|
@ -39,32 +35,8 @@ export function Component() {
|
|||
id,
|
||||
})
|
||||
|
||||
let Item: FC<UniversalItemProps>
|
||||
switch (view) {
|
||||
case FeedViewType.Articles: {
|
||||
Item = ArticleItem
|
||||
break
|
||||
}
|
||||
case FeedViewType.SocialMedia: {
|
||||
Item = SocialMediaItem
|
||||
break
|
||||
}
|
||||
case FeedViewType.Pictures: {
|
||||
Item = PictureItem
|
||||
break
|
||||
}
|
||||
case FeedViewType.Videos: {
|
||||
Item = VideoItem
|
||||
break
|
||||
}
|
||||
case FeedViewType.Notifications: {
|
||||
Item = NotificationItem
|
||||
break
|
||||
}
|
||||
default: {
|
||||
Item = ArticleItem
|
||||
}
|
||||
}
|
||||
const Item: FC<UniversalItemProps> = getItemComponentByView(view as FeedViewType)
|
||||
|
||||
const { t } = useTranslation("external")
|
||||
useTitle(feed.data?.feed.title)
|
||||
const presentFeedFormModal = usePresentFeedFormModal()
|
||||
|
|
|
|||
|
|
@ -11,47 +11,46 @@ import { LoadingCircle } from "~/components/ui/loading"
|
|||
import { usePresentFeedFormModal } from "~/hooks/biz/useFeedFormModal"
|
||||
import { useTitle } from "~/hooks/common"
|
||||
import type { ListModel } from "~/models"
|
||||
import { useFeed } from "~/queries/feed"
|
||||
import { useList } from "~/queries/lists"
|
||||
import { useFeedById } from "~/store/feed/hooks"
|
||||
|
||||
export function Component() {
|
||||
const { id } = useParams()
|
||||
|
||||
const feed = useFeed({
|
||||
id,
|
||||
isList: true,
|
||||
const list = useList({
|
||||
id: id!,
|
||||
})
|
||||
const listData = feed.data?.feed as ListModel
|
||||
const isSubscribed = !!feed.data?.subscription
|
||||
const listData = list.data?.list as ListModel
|
||||
const isSubscribed = !!list.data?.subscription
|
||||
|
||||
const { t } = useTranslation("external")
|
||||
useTitle(feed.data?.feed.title)
|
||||
useTitle(list.data?.list.title)
|
||||
const presentFeedFormModal = usePresentFeedFormModal()
|
||||
return (
|
||||
<>
|
||||
{feed.isLoading ? (
|
||||
{list.isLoading ? (
|
||||
<LoadingCircle size="large" className="center fixed inset-0" />
|
||||
) : (
|
||||
feed.data?.feed && (
|
||||
list.data?.list && (
|
||||
<div className="mx-auto mt-12 flex max-w-5xl flex-col items-center justify-center p-4 lg:p-0">
|
||||
<FeedIcon
|
||||
fallback
|
||||
feed={feed.data.feed}
|
||||
feed={list.data.list}
|
||||
className="mask-squircle mask shrink-0"
|
||||
size={64}
|
||||
/>
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="mb-2 mt-4 flex items-center text-2xl font-bold">
|
||||
<h1>{feed.data.feed.title}</h1>
|
||||
<FeedCertification feed={feed.data.feed} />
|
||||
<h1>{list.data.list.title}</h1>
|
||||
<FeedCertification feed={list.data.list} />
|
||||
</div>
|
||||
<div className="mb-8 text-sm text-zinc-500">{feed.data.feed.description}</div>
|
||||
<div className="mb-8 text-sm text-zinc-500">{list.data.list.description}</div>
|
||||
</div>
|
||||
<div className="mb-4 text-sm">
|
||||
{t("feed.followsAndFeeds", {
|
||||
subscriptionCount: feed.data.subscriptionCount,
|
||||
subscriptionNoun: t("feed.follower", { count: feed.data.subscriptionCount }),
|
||||
feedsCount: "feedCount" in feed.data ? feed.data.feedCount : 0,
|
||||
subscriptionCount: list.data?.subscriptionCount,
|
||||
subscriptionNoun: t("feed.follower", { count: list.data?.subscriptionCount }),
|
||||
feedsCount: "feedCount" in listData ? listData.feedCount : 0,
|
||||
feedsNoun: t("feed.feeds", { count: listData?.feedIds?.length }),
|
||||
appName: APP_NAME,
|
||||
})}
|
||||
|
|
@ -82,7 +81,7 @@ export function Component() {
|
|||
{listData.feedIds
|
||||
?.slice(0, 5)
|
||||
.map((feedId) => <FeedRow feedId={feedId} key={feedId} />)}
|
||||
{"feedCount" in feed.data && (
|
||||
{"feedCount" in list.data && (
|
||||
<div
|
||||
onClick={() => {
|
||||
presentFeedFormModal({
|
||||
|
|
@ -92,7 +91,7 @@ export function Component() {
|
|||
className="text-sm text-zinc-500"
|
||||
>
|
||||
{t("feed.follow_to_view_all", {
|
||||
count: feed.data.feedCount,
|
||||
count: list.data.feedCount || 0,
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
import Versions from "../../components/Versions"
|
||||
|
||||
export function Component() {
|
||||
if (!import.meta.env.DEV) return null
|
||||
const ipcHandle = (): void => window.electron?.ipcRenderer.send("ping")
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="creator">Powered by electron-vite</div>
|
||||
<div className="text">
|
||||
<span className="i-mgc-rss-cute-fi" />
|
||||
Build an Electron app with <span className="react">React</span>
|
||||
and <span className="ts">TypeScript</span>
|
||||
</div>
|
||||
<p className="tip">
|
||||
Please try pressing <code>F12</code> to open the devTool
|
||||
</p>
|
||||
<div className="actions">
|
||||
<div className="action">
|
||||
<a href="https://electron-vite.org/" target="_blank" rel="noreferrer">
|
||||
Documentation
|
||||
</a>
|
||||
</div>
|
||||
<div className="action">
|
||||
<a target="_blank" rel="noreferrer" onClick={ipcHandle}>
|
||||
Send IPC
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<Versions />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import { useSearchParams } from "react-router-dom"
|
|||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs"
|
||||
import { DiscoverForm } from "~/modules/discover/form"
|
||||
import { DiscoverImport } from "~/modules/discover/import"
|
||||
import { DiscoverInboxList } from "~/modules/discover/inbox-list-form"
|
||||
import { Recommendations } from "~/modules/discover/recommendations"
|
||||
import { DiscoverRSS3 } from "~/modules/discover/rss3-form"
|
||||
import { DiscoverUser } from "~/modules/discover/user-form"
|
||||
|
|
@ -28,6 +29,10 @@ const tabs: {
|
|||
name: "words.rsshub",
|
||||
value: "rsshub",
|
||||
},
|
||||
{
|
||||
name: "words.inbox",
|
||||
value: "inbox",
|
||||
},
|
||||
{
|
||||
name: "words.rss3",
|
||||
value: "rss3",
|
||||
|
|
@ -36,11 +41,6 @@ const tabs: {
|
|||
name: "words.user",
|
||||
value: "user",
|
||||
},
|
||||
{
|
||||
name: "words.email",
|
||||
value: "email",
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
name: "words.import",
|
||||
value: "import",
|
||||
|
|
@ -84,9 +84,10 @@ export function Component() {
|
|||
)
|
||||
}
|
||||
|
||||
const TabComponent: Record<string, React.FC<{ type: string }>> = {
|
||||
const TabComponent: Record<string, React.FC<{ type?: string }>> = {
|
||||
import: DiscoverImport,
|
||||
rss3: DiscoverRSS3,
|
||||
inbox: DiscoverInboxList,
|
||||
user: DiscoverUser,
|
||||
default: DiscoverForm,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,44 @@
|
|||
import { AnimatePresence } from "framer-motion"
|
||||
import { useParams } from "react-router-dom"
|
||||
|
||||
import { useUISettingKey } from "~/atoms/settings/ui"
|
||||
import { ActionButton } from "~/components/ui/button"
|
||||
import { ROUTE_ENTRY_PENDING, views } from "~/constants"
|
||||
import { useNavigateEntry } from "~/hooks/biz/useNavigateEntry"
|
||||
import { useRouteView } from "~/hooks/biz/useRouteParams"
|
||||
import { cn } from "~/lib/utils"
|
||||
import { EntryContent } from "~/modules/entry-content"
|
||||
import { AppLayoutGridContainerProvider } from "~/providers/app-grid-layout-container-provider"
|
||||
|
||||
export const Component = () => {
|
||||
const { entryId } = useParams()
|
||||
const view = useRouteView()
|
||||
const inWideMode = view ? views[view].wideMode : false
|
||||
const navigate = useNavigateEntry()
|
||||
|
||||
const settingWideMode = useUISettingKey("wideMode")
|
||||
const realEntryId = entryId === ROUTE_ENTRY_PENDING ? "" : entryId
|
||||
const disable = views[view].wideMode || (settingWideMode && !realEntryId)
|
||||
const wideMode = settingWideMode && realEntryId
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
<AppLayoutGridContainerProvider>
|
||||
{!inWideMode && (
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<EntryContent entryId={entryId === ROUTE_ENTRY_PENDING ? "" : entryId} />
|
||||
{!disable && (
|
||||
<div
|
||||
className={cn(
|
||||
"flex min-w-0 flex-1 flex-col",
|
||||
wideMode && "absolute inset-0 z-10 bg-white pl-12",
|
||||
)}
|
||||
>
|
||||
{wideMode && (
|
||||
<ActionButton
|
||||
className="absolute left-2.5 top-2.5 z-10"
|
||||
onClick={() => navigate({ entryId: null })}
|
||||
>
|
||||
<i className="i-mgc-close-cute-re text-2xl" />
|
||||
</ActionButton>
|
||||
)}
|
||||
<EntryContent entryId={realEntryId} />
|
||||
</div>
|
||||
)}
|
||||
</AppLayoutGridContainerProvider>
|
||||
|
|
|
|||
|
|
@ -15,9 +15,10 @@ export function Component() {
|
|||
|
||||
// Memo this initial value to avoid re-render
|
||||
|
||||
const settingWideMode = useUISettingKey("wideMode")
|
||||
const entryColWidth = useMemo(() => getUISettings().entryColWidth, [])
|
||||
const { view } = useRouteParams()
|
||||
const inWideMode = view ? views[view].wideMode : false
|
||||
const inWideMode = (view ? views[view].wideMode : false) || settingWideMode
|
||||
const feedColumnWidth = useUISettingKey("feedColWidth")
|
||||
const { position, separatorProps, isDragging, separatorCursor } = useResizable({
|
||||
axis: "x",
|
||||
|
|
@ -32,7 +33,7 @@ export function Component() {
|
|||
})
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="flex min-w-0 grow">
|
||||
<div ref={containerRef} className="relative flex min-w-0 grow">
|
||||
<div
|
||||
className={cn("h-full shrink-0", inWideMode ? "flex-1" : "border-r", "will-change-[width]")}
|
||||
style={{
|
||||
|
|
|
|||
|
|
@ -86,12 +86,12 @@ export function Component() {
|
|||
return (
|
||||
<div className="center fixed inset-0 flex-col text-balance px-4 text-center">
|
||||
<i className="i-mingcute-device-line mb-2 size-16 text-muted-foreground" />
|
||||
<div>{APP_NAME} is not supported on mobile devices.</div>
|
||||
<div>{APP_NAME} is not yet supported on mobile devices.</div>
|
||||
<div>
|
||||
Your device width is <b>{`${window.innerWidth}`}px</b> less than minimum supported width
|
||||
1024px.
|
||||
Your device width is <b>{`${window.innerWidth}`}px</b>, which is less than the minimum
|
||||
supported width 1024px.
|
||||
</div>
|
||||
<div>Please use desktop app to using {APP_NAME}</div>
|
||||
<div>Please switch to the desktop app to continue using {APP_NAME}</div>
|
||||
|
||||
<div>
|
||||
Download:{" "}
|
||||
|
|
@ -234,7 +234,7 @@ const FeedResponsiveResizerContainer = ({
|
|||
className={cn(
|
||||
"shrink-0 overflow-hidden",
|
||||
"absolute inset-y-0 z-[2]",
|
||||
feedColumnTempShow && !feedColumnShow && "shadow-drawer-right z-[12] border-r",
|
||||
feedColumnTempShow && !feedColumnShow && "shadow-drawer-to-right z-[12] border-r",
|
||||
!feedColumnShow && !feedColumnTempShow ? "-translate-x-full delay-200" : "",
|
||||
!isDragging ? "duration-200" : "",
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { Button } from "~/components/ui/button"
|
|||
import { useAuthQuery } from "~/hooks/common"
|
||||
import { apiClient } from "~/lib/api-fetch"
|
||||
import { toastFetchError } from "~/lib/error-parser"
|
||||
import type { ActionEntryField, ActionFeedField, ActionOperation, ActionsResponse } from "~/models"
|
||||
import type { ActionsInput, ActionsResponse } from "~/models"
|
||||
import { ActionCard } from "~/modules/settings/action-card"
|
||||
import { SettingsTitle } from "~/modules/settings/title"
|
||||
import { defineSettingPageData } from "~/modules/settings/utils"
|
||||
|
|
@ -22,28 +22,6 @@ export const loader = defineSettingPageData({
|
|||
priority,
|
||||
})
|
||||
|
||||
type ActionsInput = {
|
||||
name: string
|
||||
condition: {
|
||||
field?: ActionFeedField
|
||||
operator?: ActionOperation
|
||||
value?: string
|
||||
}[]
|
||||
result: {
|
||||
translation?: string
|
||||
summary?: boolean
|
||||
rewriteRules?: {
|
||||
from: string
|
||||
to: string
|
||||
}[]
|
||||
blockRules?: {
|
||||
field?: ActionEntryField
|
||||
operator?: ActionOperation
|
||||
value?: string | number
|
||||
}[]
|
||||
}
|
||||
}[]
|
||||
|
||||
export function Component() {
|
||||
const { t } = useTranslation("settings")
|
||||
const actions = useAuthQuery(Queries.action.getAll())
|
||||
|
|
@ -63,6 +41,7 @@ export function Component() {
|
|||
action.result.blockRules = action.result.blockRules?.filter(
|
||||
(r) => r.field && r.operator && r.value,
|
||||
)
|
||||
action.result.webhooks = action.result.webhooks?.filter((w) => w)
|
||||
})
|
||||
await apiClient.actions.$put({
|
||||
json: {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { SettingInvitations } from "~/modules/settings/tabs/invitations"
|
|||
import { SettingsTitle } from "~/modules/settings/title"
|
||||
import { defineSettingPageData } from "~/modules/settings/utils"
|
||||
|
||||
const iconName = "i-mgc-heart-hand-cute-re"
|
||||
const iconName = "i-mgc-love-cute-re"
|
||||
const priority = 1070
|
||||
|
||||
export const loader = defineSettingPageData({
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useHotkeys } from "react-hotkeys-hook"
|
|||
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuCheckboxItem,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuPortal,
|
||||
|
|
@ -112,7 +113,11 @@ const Item = memo(({ item }: { item: NativeMenuItem }) => {
|
|||
return <ContextMenuSeparator />
|
||||
}
|
||||
case "text": {
|
||||
const Wrapper = item.submenu ? ContextMenuSubTrigger : ContextMenuItem
|
||||
const Wrapper = item.submenu
|
||||
? ContextMenuSubTrigger
|
||||
: typeof item.checked === "boolean"
|
||||
? ContextMenuCheckboxItem
|
||||
: ContextMenuItem
|
||||
|
||||
const Sub = item.submenu ? ContextMenuSub : Fragment
|
||||
return (
|
||||
|
|
@ -122,6 +127,7 @@ const Item = memo(({ item }: { item: NativeMenuItem }) => {
|
|||
disabled={item.enabled === false || (item.click === undefined && !item.submenu)}
|
||||
onClick={onClick}
|
||||
className="flex items-center gap-2"
|
||||
checked={item.checked}
|
||||
>
|
||||
{item.icon}
|
||||
{item.label}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { getGeneralSettings } from "~/atoms/settings/general"
|
|||
import { getUISettings } from "~/atoms/settings/ui"
|
||||
import { useModalStack } from "~/components/ui/modal"
|
||||
import { FeedForm } from "~/modules/discover/feed-form"
|
||||
import { ListForm } from "~/modules/discover/list-form"
|
||||
import { usePresentUserProfileModal } from "~/modules/profile/hooks"
|
||||
|
||||
export const ExtensionExposeProvider = () => {
|
||||
|
|
@ -33,14 +34,17 @@ export const ExtensionExposeProvider = () => {
|
|||
const presentUserProfile = usePresentUserProfileModal("dialog")
|
||||
useEffect(() => {
|
||||
registerGlobalContext({
|
||||
follow(id, options) {
|
||||
follow(options) {
|
||||
present({
|
||||
title: options?.isList
|
||||
? t("sidebar.feed_actions.edit_list")
|
||||
: t("sidebar.feed_actions.edit_feed"),
|
||||
content: ({ dismiss }) => (
|
||||
<FeedForm asWidget id={id} onSuccess={dismiss} isList={options?.isList} />
|
||||
),
|
||||
content: ({ dismiss }) =>
|
||||
options?.isList ? (
|
||||
<ListForm asWidget id={options?.id} onSuccess={dismiss} />
|
||||
) : (
|
||||
<FeedForm asWidget id={options?.id} url={options?.url} onSuccess={dismiss} />
|
||||
),
|
||||
})
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -7,23 +7,29 @@ import { entryHistoryActions } from "~/store/entry-history/action"
|
|||
|
||||
export const entries = {
|
||||
entries: ({
|
||||
id,
|
||||
feedId,
|
||||
inboxId,
|
||||
listId,
|
||||
view,
|
||||
read,
|
||||
limit,
|
||||
isArchived,
|
||||
}: {
|
||||
id?: number | string
|
||||
feedId?: number | string
|
||||
inboxId?: number | string
|
||||
listId?: number | string
|
||||
view?: number
|
||||
read?: boolean
|
||||
limit?: number
|
||||
isArchived?: boolean
|
||||
}) =>
|
||||
defineQuery(
|
||||
["entries", id, view, read, limit],
|
||||
["entries", inboxId || listId || feedId, view, read, limit],
|
||||
async ({ pageParam }) =>
|
||||
entryActions.fetchEntries({
|
||||
id,
|
||||
feedId,
|
||||
inboxId,
|
||||
listId,
|
||||
view,
|
||||
read,
|
||||
limit,
|
||||
|
|
@ -31,7 +37,7 @@ export const entries = {
|
|||
isArchived,
|
||||
}),
|
||||
{
|
||||
rootKey: ["entries", id],
|
||||
rootKey: ["entries", inboxId || listId || feedId],
|
||||
structuralSharing: false,
|
||||
},
|
||||
),
|
||||
|
|
@ -39,6 +45,10 @@ export const entries = {
|
|||
defineQuery(["entry", id], async () => entryActions.fetchEntryById(id), {
|
||||
rootKey: ["entries"],
|
||||
}),
|
||||
byInboxId: (id: string) =>
|
||||
defineQuery(["entry", id], async () => entryActions.fetchInboxEntryById(id), {
|
||||
rootKey: ["entries"],
|
||||
}),
|
||||
preview: (id: string) =>
|
||||
defineQuery(
|
||||
["entries-preview", id],
|
||||
|
|
@ -57,22 +67,28 @@ export const entries = {
|
|||
),
|
||||
|
||||
checkNew: ({
|
||||
id,
|
||||
feedId,
|
||||
inboxId,
|
||||
listId,
|
||||
view,
|
||||
read,
|
||||
fetchedTime,
|
||||
}: {
|
||||
id?: number | string
|
||||
feedId?: number | string
|
||||
inboxId?: number | string
|
||||
listId?: number | string
|
||||
view?: number
|
||||
read?: boolean
|
||||
fetchedTime: number
|
||||
}) =>
|
||||
defineQuery(
|
||||
["entry-checkNew", id, view, read, fetchedTime],
|
||||
["entry-checkNew", inboxId || listId || feedId, view, read, fetchedTime],
|
||||
async () => {
|
||||
const query = {
|
||||
...getEntriesParams({
|
||||
id,
|
||||
feedId,
|
||||
inboxId,
|
||||
listId,
|
||||
view,
|
||||
}),
|
||||
read,
|
||||
|
|
@ -95,7 +111,7 @@ export const entries = {
|
|||
},
|
||||
|
||||
{
|
||||
rootKey: ["entry-checkNew", id],
|
||||
rootKey: ["entry-checkNew", inboxId || listId || feedId],
|
||||
},
|
||||
),
|
||||
|
||||
|
|
@ -110,22 +126,24 @@ export const entries = {
|
|||
}
|
||||
|
||||
export const useEntries = ({
|
||||
id,
|
||||
feedId,
|
||||
inboxId,
|
||||
listId,
|
||||
view,
|
||||
read,
|
||||
isArchived,
|
||||
isList,
|
||||
}: {
|
||||
id?: number | string
|
||||
feedId?: number | string
|
||||
inboxId?: number | string
|
||||
listId?: number | string
|
||||
view?: number
|
||||
read?: boolean
|
||||
isArchived?: boolean
|
||||
isList?: boolean
|
||||
}) =>
|
||||
useAuthInfiniteQuery(entries.entries({ id, view, read, isArchived }), {
|
||||
enabled: id !== undefined,
|
||||
useAuthInfiniteQuery(entries.entries({ feedId, inboxId, listId, view, read, isArchived }), {
|
||||
enabled: feedId !== undefined || inboxId !== undefined || listId !== undefined,
|
||||
getNextPageParam: (lastPage) =>
|
||||
isList
|
||||
inboxId
|
||||
? lastPage.data?.at(-1)?.entries.insertedAt
|
||||
: lastPage.data?.at(-1)?.entries.publishedAt,
|
||||
initialPageParam: undefined,
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue