diff --git a/.github/workflows/build-eas.yml b/.github/workflows/build-eas.yml index e1e152157a..b21a7e78f2 100644 --- a/.github/workflows/build-eas.yml +++ b/.github/workflows/build-eas.yml @@ -12,9 +12,12 @@ on: workflow_dispatch: inputs: profile: - type: string + type: choice default: preview - description: "Build profile, preview or production" + options: + - preview + - production + description: "Build profile" jobs: build: @@ -54,4 +57,10 @@ jobs: with: name: app-ios path: apps/mobile/build.ipa - retention-days: 5 + retention-days: 90 + + - name: Submit to App Store + if: github.event.inputs.profile == 'production' + run: | + cd apps/mobile + eas submit --platform ios --path build.ipa --non-interactive diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f2ac850e5..ce52d211ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # CHANGELOG -## [0.3.6](https://github.com/RSSNext/follow/compare/v0.2.7-beta.0...v0.3.6) (2025-02-20) +## [0.3.7](https://github.com/RSSNext/follow/compare/v0.2.7-beta.0...v0.3.7) (2025-02-24) ### Bug Fixes @@ -224,6 +224,8 @@ * lint ([dafaf0a](https://github.com/RSSNext/follow/commit/dafaf0a97d40a6df659a7869d7c11f5cf529dfe7)) * list form width ([6487be1](https://github.com/RSSNext/follow/commit/6487be1c9f8c085f6722595d59f85d1176c503c5)) * list unread dot position ([dad33a1](https://github.com/RSSNext/follow/commit/dad33a12db4b270801da6b8c621b3049e6672c27)) +* **listEntryCover:** fix the cover mistakely render as audio ([#2813](https://github.com/RSSNext/follow/issues/2813)) ([e73eb54](https://github.com/RSSNext/follow/commit/e73eb540ed8f9325392ba6ba5829350ee27ed31e)) +* lists fetch ([6fe8dc9](https://github.com/RSSNext/follow/commit/6fe8dc910f65e096343ad732bd19fef34f18b01e)) * **lists:** fix duplicate addition issue in manage feeds functionality ([#2544](https://github.com/RSSNext/follow/issues/2544)) ([d77ed3c](https://github.com/RSSNext/follow/commit/d77ed3cb37424e97490bbf4883f9326acc446d54)) * load dynamic render assets ([3499535](https://github.com/RSSNext/follow/commit/3499535b4091e3a82734c416528b0766e70f0b63)) * load instagram image fail ([f8fd58f](https://github.com/RSSNext/follow/commit/f8fd58f78ec11bc221eb9190ac7b6a66dd858f9c)), closes [#1539](https://github.com/RSSNext/follow/issues/1539) @@ -249,10 +251,14 @@ * missing translation in pitcure masonry ([902d681](https://github.com/RSSNext/follow/commit/902d68180d1cc872cbe4435efbcae108f0d732b5)) * mobile need login modal ([0fb16f2](https://github.com/RSSNext/follow/commit/0fb16f2352e9776731522204b838be59f66127a4)) * mobile pop back to entry list will refresh data ([3c18768](https://github.com/RSSNext/follow/commit/3c18768968aeb4635933ae616c52de55b6fad2ab)) +* **mobile:** auth redirect and 2fa support ([#2829](https://github.com/RSSNext/follow/issues/2829)) ([cc51d86](https://github.com/RSSNext/follow/commit/cc51d861c92fe5cd884210aaefecc80bad581257)) * **mobile:** background color ([9f6934f](https://github.com/RSSNext/follow/commit/9f6934f844c995a1dcbb326123420e6ee9b05a96)) * **mobile:** can not load native module ([#2791](https://github.com/RSSNext/follow/issues/2791)) ([f226de5](https://github.com/RSSNext/follow/commit/f226de53ab664c252e7d0806dbfdfe91dcd8135d)) * **mobile:** context menu crash when released too quickly ([#2732](https://github.com/RSSNext/follow/issues/2732)) ([1327f56](https://github.com/RSSNext/follow/commit/1327f5671c61f901537193cfe2960b7189c0a5a6)) +* **mobile:** disable scroll-to-top for timeline view selector ([3b5fb5f](https://github.com/RSSNext/follow/commit/3b5fb5febeb4b49d7d2943a205af3c8e5f5c3b9d)) * **mobile:** improve responsive design in CornerPlayer ([949a07e](https://github.com/RSSNext/follow/commit/949a07ed0e3cb8b8ea582ebd926f143da7386736)) +* **mobile:** remove shadow from PlayerTabBar ([79631bb](https://github.com/RSSNext/follow/commit/79631bbcbc1212a307678b04dbfff93dda793b03)) +* **mobile:** revert ref bind for nav scroll view ref ([#2856](https://github.com/RSSNext/follow/issues/2856)) ([8a0049d](https://github.com/RSSNext/follow/commit/8a0049dc1585c72d9e79fe1471c52fc8851a124a)) * **mobile:** svg background transparent ([#2820](https://github.com/RSSNext/follow/issues/2820)) ([6b991d3](https://github.com/RSSNext/follow/commit/6b991d379e608e1686ce88022b7bb57d6de6d11d)) * **mobile:** unread style not working, bring views back ([#2765](https://github.com/RSSNext/follow/issues/2765)) ([fad9f94](https://github.com/RSSNext/follow/commit/fad9f944d57d5739d6edfa7749d9802438f361c8)) * **mobile:** update email login style ([9019e9b](https://github.com/RSSNext/follow/commit/9019e9bbc6ebcdcbd8a0f21844e04f1df17dd84a)) @@ -262,6 +268,7 @@ * modal bottom buttons align ([#1216](https://github.com/RSSNext/follow/issues/1216)) ([b97096d](https://github.com/RSSNext/follow/commit/b97096dbd06bb7687b6c1313de45d8e91dee25af)) * modal close button overlaps the select content ([#1166](https://github.com/RSSNext/follow/issues/1166)) ([3c103ed](https://github.com/RSSNext/follow/commit/3c103ed15daeb1f57a001d615c047c303087ba46)) * modal exiting transition type ([5bb0100](https://github.com/RSSNext/follow/commit/5bb01006ad04f8e5ed93076905b78b74ce293f79)) +* **modal:** adjust modal header drag and resize behavior ([38e8c6c](https://github.com/RSSNext/follow/commit/38e8c6c2f4517b550273f8f09cb136107d2bebf2)) * **modal:** close button can not click on electron ([c544dfc](https://github.com/RSSNext/follow/commit/c544dfcc8d4613e8d4f6025dd8a60cf679881680)) * **modal:** drawer modal layout symmetric padding ([7141e86](https://github.com/RSSNext/follow/commit/7141e869dfc9328e3eeeda909ea2ffc7e6dd0e34)) * **modal:** fixed close modal button position ([78e9424](https://github.com/RSSNext/follow/commit/78e942473fff309e372708b56a59a9009d5bb0e9)) @@ -539,6 +546,7 @@ * basic masonry entry list and entry content view ([#2493](https://github.com/RSSNext/follow/issues/2493)) ([e569681](https://github.com/RSSNext/follow/commit/e569681198cabbfeb4c8133e421533c37b1fd96d)) * bigger font size in mobile ([3e9a92d](https://github.com/RSSNext/follow/commit/3e9a92d16a80139c851671748367f009a3af4de9)) * bigger view selector ([973c0d2](https://github.com/RSSNext/follow/commit/973c0d214d13313372c4ced0c68ec3c85db4ef4b)) +* bring back lists in views ([aeaa29b](https://github.com/RSSNext/follow/commit/aeaa29b13b0466848d6b35d4a7da232834322888)) * bring rehypeUrlToAnchor back ([6f0cc4d](https://github.com/RSSNext/follow/commit/6f0cc4d566c9f2f552f0fedf1a8e570a817e0f56)), closes [#1373](https://github.com/RSSNext/follow/issues/1373) * checkLanguage utils ([225e470](https://github.com/RSSNext/follow/commit/225e470db84649bd23657d82b3c6a71b94cccc5f)) * collection panel active button status ([a1676fc](https://github.com/RSSNext/follow/commit/a1676fc535430c3bd4c614a5261e3152e1bf2561)) @@ -637,11 +645,14 @@ * **mobile:** Add image headers for cross-origin image loading ([053517d](https://github.com/RSSNext/follow/commit/053517db633103fb0a7f8796684181f9f3d6688b)) * **mobile:** add loading indicator for infinite scroll in entry list ([22ac1be](https://github.com/RSSNext/follow/commit/22ac1be1a5a82a1453ef200b25c49ce4f6d93ca6)) * **mobile:** Add media preview accessories and no-media rendering support ([9113c0a](https://github.com/RSSNext/follow/commit/9113c0a2e53bdbc4c9b4e83f719198a83f6b7601)) +* **mobile:** add native iOS toast module with SPIndicator ([8dbb634](https://github.com/RSSNext/follow/commit/8dbb6343fb299a7b1e15288405d45bfb72dec09b)) * **mobile:** add PortalProvider to EntryContentWebView and adjust ActivityIndicator ([713dc47](https://github.com/RSSNext/follow/commit/713dc47531f3e5fb4247cc9dc278ae2c6de6cdc6)) * **mobile:** add preview image component with advanced gesture handling ([1d4e37e](https://github.com/RSSNext/follow/commit/1d4e37e35a36eaeacbea70723464a3759131cd2b)) * **mobile:** add reader inline style rendering option ([160eb14](https://github.com/RSSNext/follow/commit/160eb14258f24afe08f5103b237e4391c09bd93e)) +* **mobile:** add reusable MediaCarousel component ([48461f1](https://github.com/RSSNext/follow/commit/48461f15e68f584372e75d41c51775d86300518f)) * **mobile:** add social entry list component ([#2733](https://github.com/RSSNext/follow/issues/2733)) ([903b989](https://github.com/RSSNext/follow/commit/903b98953aa74fe472bf3793c6a4b8edbfe519bf)) * **mobile:** add triggerAsChild prop to EntryReadHistory component ([#2773](https://github.com/RSSNext/follow/issues/2773)) ([c7b1513](https://github.com/RSSNext/follow/commit/c7b151394a2e3cfb73bb1c16b7f40d8256e65718)) +* **mobile:** audio player ([#2801](https://github.com/RSSNext/follow/issues/2801)) ([8e2dfe9](https://github.com/RSSNext/follow/commit/8e2dfe99c87d868e67f466ad2ed1687e95bf6882)) * **mobile:** auto mark as read when scrolling ([#2781](https://github.com/RSSNext/follow/issues/2781)) ([c8f580c](https://github.com/RSSNext/follow/commit/c8f580c0a1f7228ebf390f293691213b762d4499)) * **mobile:** deletion button ([79b4703](https://github.com/RSSNext/follow/commit/79b47030260e7667e23e725b2337922139b6ae6b)) * **mobile:** enhance CORS handling and image loading in WebView ([ad06db9](https://github.com/RSSNext/follow/commit/ad06db9744f3b001a8bf0d004a1492e656091ab1)) @@ -649,17 +660,21 @@ * **mobile:** Enhance grid view with dynamic media rendering and improved item handling ([27818f6](https://github.com/RSSNext/follow/commit/27818f6559bfb5b95bf529e38814eb6b83ad0d29)) * **mobile:** enhance HTML renderer with image rendering and blurhash support ([b534667](https://github.com/RSSNext/follow/commit/b534667c059c7b60edaa9c0dfe6e2bfebe85fc7e)) * **mobile:** enhance iOS image preview with SDWebImage and improved QuickLook handling ([0011861](https://github.com/RSSNext/follow/commit/0011861e3f9155ba4c28864f9bd7f452fdc2942e)) +* **mobile:** enhance player UI with floating tab bar and control improvements ([577f07f](https://github.com/RSSNext/follow/commit/577f07f85e3e7556cae5652a24317a8842c92316)) * **mobile:** Enhance PreviewImage and EntryGridItem with interactive preview and animation ([bdff613](https://github.com/RSSNext/follow/commit/bdff6138676fe9c8c185861abfd03fa74f2f29c9)) +* **mobile:** enhance tab bar icon and label animations ([9c720a7](https://github.com/RSSNext/follow/commit/9c720a7d8c918234c9aef52405aca230b401c90e)) * **mobile:** entry list by feed or category; pagination ([#2717](https://github.com/RSSNext/follow/issues/2717)) ([a63bb82](https://github.com/RSSNext/follow/commit/a63bb82935336a7f8efb5bc7e75cc2f6463d0a3c)) * **mobile:** implement collection entry list ([#2787](https://github.com/RSSNext/follow/issues/2787)) ([c251aea](https://github.com/RSSNext/follow/commit/c251aeaf65c6a13668cbe984dedc49012a86eb98)) * **mobile:** Implement global image preview with advanced gestures ([113c404](https://github.com/RSSNext/follow/commit/113c4045b79a5b5d81d644f68222063b006a1f5f)) * **mobile:** implement native image preview with QuickLook on iOS ([975285e](https://github.com/RSSNext/follow/commit/975285ec30599fe297108c5aa3e8a163e9a27d14)) +* **mobile:** implement scroll-to-top functionality ([#2831](https://github.com/RSSNext/follow/issues/2831)) ([9b3439f](https://github.com/RSSNext/follow/commit/9b3439f71162824df7732f585a16dfc2e349ebdd)) * **mobile:** improve entry list and detail view with UI refinements ([a527987](https://github.com/RSSNext/follow/commit/a52798729ec378b71367d6b90c6e65687cbe75b3)) * **mobile:** introduce expo image ([9ea93b8](https://github.com/RSSNext/follow/commit/9ea93b80238f47788e0f984f03faf3c9a3434a81)) * **mobile:** load archive entries ([#2725](https://github.com/RSSNext/follow/issues/2725)) ([d8a796d](https://github.com/RSSNext/follow/commit/d8a796d3ab4ca79496aef6e6597d2f9108d40058)) * **mobile:** prefetch entry content ([03a1438](https://github.com/RSSNext/follow/commit/03a1438133ee809d15a757b300490de4c62f63a3)) * **mobile:** refactor EntryList to use pager view ([#2802](https://github.com/RSSNext/follow/issues/2802)) ([714a933](https://github.com/RSSNext/follow/commit/714a93336118c381ca7046c13f79e2e700ee9cfe)) * **mobile:** refresh control for entry list ([#2723](https://github.com/RSSNext/follow/issues/2723)) ([c01984d](https://github.com/RSSNext/follow/commit/c01984d50f0488b74893180a2a43ad59ba3ec36a)) +* **mobile:** render as read ([#2835](https://github.com/RSSNext/follow/issues/2835)) ([c0afb39](https://github.com/RSSNext/follow/commit/c0afb39788f72ef29bd814d97752fe438c3027e3)) * **mobile:** unread state for entry ([#2730](https://github.com/RSSNext/follow/issues/2730)) ([ddd6a2c](https://github.com/RSSNext/follow/commit/ddd6a2c8cdf8fc6a06acd52345eb8c5b157c2e12)) * **modal:** enhance Follow modal with improved navigation and loading state ([cf31c0f](https://github.com/RSSNext/follow/commit/cf31c0f5fae3fd6dac1e1e571e870ea57e05ed0b)) * move feed to new category in context menu ([#2072](https://github.com/RSSNext/follow/issues/2072)) ([d684e14](https://github.com/RSSNext/follow/commit/d684e14056581744ac78ca697fa5ca059294245e)) @@ -676,6 +691,7 @@ * optimize translation display ([3f01ce8](https://github.com/RSSNext/follow/commit/3f01ce87fae9e46fcbb330c24732c4733b9f7f59)) * optimize translation display ([75b9135](https://github.com/RSSNext/follow/commit/75b9135a02fb6e94abb5c0670c9f12f354a88f0e)) * optimize unread indicator styles ([f44b1a5](https://github.com/RSSNext/follow/commit/f44b1a5604af9ee1949f35af814dcda04af691d2)) +* optional daily ([d07ec23](https://github.com/RSSNext/follow/commit/d07ec236e5c90165638ab341623400f8f661d49d)) * **podcast:** add PodcastButton component to mobile float bar ([#2514](https://github.com/RSSNext/follow/issues/2514)) ([037791e](https://github.com/RSSNext/follow/commit/037791eae13bf0b9322eba332a852f73cd737bd0)) * prefer origin addresses for content images ([d4d4345](https://github.com/RSSNext/follow/commit/d4d43451dec839b64239e1835b2ac1a1aa2478be)) * **profile:** enhance email management and avatar fallback for better mobile support ([#2776](https://github.com/RSSNext/follow/issues/2776)) ([4295dec](https://github.com/RSSNext/follow/commit/4295decfa9869599386499e405e2e30ba2796a80)) @@ -927,8 +943,8 @@ * ci ([8852a81](https://github.com/RSSNext/follow/commit/8852a81d4cfe80a0704bcf52220c39e9ccbe98de)) * 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:** cannot release macOS APP ([#957](https://github.com/RSSNext/follow/issues/957)) ([4a14738](https://github.com/RSSNext/follow/commit/4a14738a99c7fd26dbf22c8de968a6896cbdc28b)), closes [/github.com/actions/runner/issues/2958#issuecomment-2186602747](https://github.com//github.com/actions/runner/issues/2958/issues/issuecomment-2186602747) -* **ci:** cannot release macOS APP ([#957](https://github.com/RSSNext/follow/issues/957)) ([#1078](https://github.com/RSSNext/follow/issues/1078)) ([cd00a20](https://github.com/RSSNext/follow/commit/cd00a2064228890f4891f5b15825d1f5a0d681e9)), closes [/github.com/actions/runner/issues/2958#issuecomment-2186602747](https://github.com//github.com/actions/runner/issues/2958/issues/issuecomment-2186602747) +* **ci:** cannot release macOS APP ([#957](https://github.com/RSSNext/follow/issues/957)) ([4a14738](https://github.com/RSSNext/follow/commit/4a14738a99c7fd26dbf22c8de968a6896cbdc28b)) +* **ci:** cannot release macOS APP ([#957](https://github.com/RSSNext/follow/issues/957)) ([#1078](https://github.com/RSSNext/follow/issues/1078)) ([cd00a20](https://github.com/RSSNext/follow/commit/cd00a2064228890f4891f5b15825d1f5a0d681e9)) * **ci:** fetch all depth ([a504a12](https://github.com/RSSNext/follow/commit/a504a127bca93a6675b3ff02bcea0b1eca6e9707)) * **ci:** nightly build ([a4ce7b1](https://github.com/RSSNext/follow/commit/a4ce7b16f476b8d72a0b1761709ec1ceba591242)) * **ci:** nightly linux build ([702b1d9](https://github.com/RSSNext/follow/commit/702b1d976d52c49647d0433f2dd162c034d0abb4)) diff --git a/apps/main/src/index.ts b/apps/main/src/index.ts index 7104401e3a..64c69de88d 100644 --- a/apps/main/src/index.ts +++ b/apps/main/src/index.ts @@ -59,6 +59,13 @@ function bootstrap() { } }) + app.on("activate", () => { + // On macOS it's common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + mainWindow = getMainWindowOrCreate() + mainWindow.show() + }) + // This method will be called when Electron has finished // initialization and is ready to create browser windows. // Some APIs can only be used after this event occurs. @@ -158,13 +165,6 @@ function bootstrap() { }, ) - app.on("activate", () => { - // On macOS it's common to re-create a window in the app when the - // dock icon is clicked and there are no other windows open. - mainWindow = getMainWindowOrCreate() - mainWindow.show() - }) - app.on("open-url", (_, url) => { if (mainWindow && !mainWindow.isDestroyed()) { if (mainWindow.isMinimized()) mainWindow.restore() diff --git a/apps/main/src/window.ts b/apps/main/src/window.ts index d1add6ba33..0a7265513f 100644 --- a/apps/main/src/window.ts +++ b/apps/main/src/window.ts @@ -41,6 +41,7 @@ export function createWindow( show: false, resizable: configs?.resizable ?? true, autoHideMenuBar: true, + alwaysOnTop: false, webPreferences: { preload: path.join(__dirname, "../preload/index.mjs"), sandbox: false, diff --git a/apps/mobile/babel.config.js b/apps/mobile/babel.config.js index 1e806cc3b7..f0d47bb622 100644 --- a/apps/mobile/babel.config.js +++ b/apps/mobile/babel.config.js @@ -12,6 +12,7 @@ module.exports = function (api) { "es-toolkit/compat": "../../node_modules/es-toolkit/dist/compat/index.js", "es-toolkit": "../../node_modules/es-toolkit/dist/index.js", "better-auth/react": "../../node_modules/better-auth/dist/react.js", + "better-auth/client/plugins": "../../node_modules/better-auth/dist/client/plugins.js", "@better-auth/expo/client": "../../node_modules/@better-auth/expo/dist/client.js", }, extensions: [".js", ".jsx", ".ts", ".tsx"], diff --git a/apps/mobile/native/expo-module.config.json b/apps/mobile/native/expo-module.config.json index 50199d33fc..6976c4ef1e 100644 --- a/apps/mobile/native/expo-module.config.json +++ b/apps/mobile/native/expo-module.config.json @@ -1,7 +1,7 @@ { "platforms": ["apple", "android"], "apple": { - "modules": ["SharedWebViewModule", "HelperModule"] + "modules": ["SharedWebViewModule", "HelperModule", "ToasterModule"] }, "android": { "modules": [] diff --git a/apps/mobile/native/ios/FollowNative.podspec b/apps/mobile/native/ios/FollowNative.podspec index 855b13a564..1e7701d9b6 100644 --- a/apps/mobile/native/ios/FollowNative.podspec +++ b/apps/mobile/native/ios/FollowNative.podspec @@ -21,6 +21,7 @@ Pod::Spec.new do |s| s.dependency 'ExpoModulesCore' s.dependency 'SnapKit', '~> 5.7.0' s.dependency 'SDWebImage', '~> 5.0' + s.dependency 'SPIndicator', '~> 1.0.0' # Swift/Objective-C compatibility s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', diff --git a/apps/mobile/native/ios/SharedWebView/ModalWebViewController.swift b/apps/mobile/native/ios/SharedWebView/ModalWebViewController.swift index a26e125b91..5d290ff82f 100644 --- a/apps/mobile/native/ios/SharedWebView/ModalWebViewController.swift +++ b/apps/mobile/native/ios/SharedWebView/ModalWebViewController.swift @@ -56,7 +56,8 @@ class ModalWebViewController: UIViewController { private func setupWebView() { view.addSubview(webView) webView.snp.makeConstraints { make in - make.edges.equalToSuperview() + make.top.equalTo(view.safeAreaLayoutGuide) + make.left.right.bottom.equalToSuperview() } } diff --git a/apps/mobile/native/ios/Toaster/ToasterModule.swift b/apps/mobile/native/ios/Toaster/ToasterModule.swift new file mode 100644 index 0000000000..22a1e96d64 --- /dev/null +++ b/apps/mobile/native/ios/Toaster/ToasterModule.swift @@ -0,0 +1,63 @@ +// +// ToasterModule.swift +// +// Created by Innei on 2025/2/21. +// + +import ExpoModulesCore +import SPIndicator + +enum ToastType: String, Enumerable { + case error + case info + case warn + case success +} + +extension ToastType { + func type() -> SPIndicatorIconPreset { + switch self + { + case .error: .error + case .warn: .custom(UIImage(systemName: "exclamationmark.triangle")!.withTintColor(.orange)) + case .info: .custom(UIImage(systemName: "info.circle")!.withTintColor(.blue)) + case .success: .done + } + } + + func haptic() -> SPIndicatorHaptic { + switch self { + case .error: .error + case .info: .success + case .warn: .warning + case .success: .error + } + } +} +struct ToastOptions: Record { + @Field + var message: String? + @Field + var type: ToastType = .info + + @Field + var duration: Double = 1.5 + @Field + var title: String + +} + +public class ToasterModule: Module { + public func definition() -> ModuleDefinition { + Name("Toaster") + + Function("toast") { (value: ToastOptions) in + + DispatchQueue.main.sync { + let indicatorView = SPIndicatorView( + title: value.title, message: value.message, preset: value.type.type()) + indicatorView.present(duration: value.duration, haptic: value.type.haptic()) + } + } + } +} diff --git a/apps/mobile/src/atoms/scroll-to-top.ts b/apps/mobile/src/atoms/scroll-to-top.ts deleted file mode 100644 index 50cb6ee49e..0000000000 --- a/apps/mobile/src/atoms/scroll-to-top.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { jotaiStore } from "@follow/utils" -import type { FlashList } from "@shopify/flash-list" -import { atom } from "jotai" -import { useEffect, useRef } from "react" - -const defaultScrollToTop = { scrollToTop: () => {} } -const scrollToTopAtom = atom<{ scrollToTop: () => void }>(defaultScrollToTop) - -export const scrollToTop = () => { - const { scrollToTop } = jotaiStore.get(scrollToTopAtom) - scrollToTop() -} - -export const useScrollToTopRef = >(enabled = true) => { - const ref = useRef(null) - - useEffect(() => { - if (!enabled) return - const scrollToTop = () => { - ref.current?.scrollToOffset({ animated: true, offset: 0 }) - } - jotaiStore.set(scrollToTopAtom, { scrollToTop }) - return () => { - if (jotaiStore.get(scrollToTopAtom).scrollToTop !== scrollToTop) return - jotaiStore.set(scrollToTopAtom, defaultScrollToTop) - } - }, [enabled, ref]) - return ref -} diff --git a/apps/mobile/src/components/common/SafeNavigationScrollView.tsx b/apps/mobile/src/components/common/SafeNavigationScrollView.tsx index 1cc18f4447..16177f45ca 100644 --- a/apps/mobile/src/components/common/SafeNavigationScrollView.tsx +++ b/apps/mobile/src/components/common/SafeNavigationScrollView.tsx @@ -16,7 +16,6 @@ import type { ReanimatedScrollEvent } from "react-native-reanimated/lib/typescri import { useSafeAreaInsets } from "react-native-safe-area-context" import { useColor } from "react-native-uikit-colors" -import { scrollToTop } from "@/src/atoms/scroll-to-top" import { AttachNavigationScrollViewContext, SetAttachNavigationScrollViewContext, @@ -175,13 +174,9 @@ export const NavigationBlurEffectHeader = ({ {options.headerBackground?.()} - -
null} - /> - + +
null} /> + {hideableBottom} ) diff --git a/apps/mobile/src/components/ui/carousel/MediaCarousel.tsx b/apps/mobile/src/components/ui/carousel/MediaCarousel.tsx new file mode 100644 index 0000000000..4ff362ea47 --- /dev/null +++ b/apps/mobile/src/components/ui/carousel/MediaCarousel.tsx @@ -0,0 +1,106 @@ +import { useEffect, useState } from "react" +import { ScrollView, View } from "react-native" +import Animated, { + interpolateColor, + useAnimatedStyle, + useSharedValue, + withSpring, +} from "react-native-reanimated" + +import type { MediaModel } from "@/src/database/schemas/types" + +import { ImageContextMenu } from "../image/ImageContextMenu" +import type { PreviewImageProps } from "../image/PreviewImage" +import { PreviewImage } from "../image/PreviewImage" + +export const MediaCarousel = ({ + media, + onPreview, + aspectRatio, + Accessory, + AccessoryProps, +}: { + media: MediaModel[] + onPreview: () => void + aspectRatio: number +} & Pick) => { + const [containerWidth, setContainerWidth] = useState(0) + const hasMany = media.length > 1 + + // const activeIndex = useSharedValue(0) + const [activeIndex, setActiveIndex] = useState(0) + + return ( + { + setContainerWidth(e.nativeEvent.layout.width) + }} + > + { + setActiveIndex(Math.round(e.nativeEvent.contentOffset.x / containerWidth)) + }} + scrollEventThrottle={16} + scrollEnabled={hasMany} + horizontal + showsHorizontalScrollIndicator={false} + pagingEnabled + className="flex-1" + contentContainerClassName="flex-row" + style={{ aspectRatio }} + > + {media.map((m, index) => { + if (m.type === "photo") { + return ( + + + + + + ) + } + + return ( + { + // open player + }} + imageUrl={m.url} + aspectRatio={m.width && m.height ? m.width / m.height : 1} + /> + ) + })} + + {/* Indicators */} + {hasMany && ( + + {media.map((_, index) => ( + + ))} + + )} + + ) +} + +const Indicator = ({ index, activeIndex }: { index: number; activeIndex: number }) => { + const activeValue = useSharedValue(0) + useEffect(() => { + activeValue.value = withSpring(index === activeIndex ? 1 : 0) + }, [activeIndex, activeValue, index]) + const animatedStyle = useAnimatedStyle(() => ({ + backgroundColor: interpolateColor( + activeValue.value, + [0, 1], + ["rgba(0, 0, 0, 0.5)", "rgba(255, 255, 255, 0.9)"], + ), + })) + return +} diff --git a/apps/mobile/src/components/ui/image/PreviewImage.tsx b/apps/mobile/src/components/ui/image/PreviewImage.tsx index f35cacff66..ac5981d463 100644 --- a/apps/mobile/src/components/ui/image/PreviewImage.tsx +++ b/apps/mobile/src/components/ui/image/PreviewImage.tsx @@ -5,7 +5,7 @@ import { Pressable, View } from "react-native" import { usePreviewImage } from "./PreviewPageProvider" -interface PreviewImageProps { +export interface PreviewImageProps { imageUrl: string blurhash?: string | undefined aspectRatio: number diff --git a/apps/mobile/src/components/ui/tabbar/BottomTabs.tsx b/apps/mobile/src/components/ui/tabbar/BottomTabs.tsx index d32ab2a5b0..58c40d6dd3 100644 --- a/apps/mobile/src/components/ui/tabbar/BottomTabs.tsx +++ b/apps/mobile/src/components/ui/tabbar/BottomTabs.tsx @@ -2,7 +2,7 @@ import type { BottomTabBarProps } from "@react-navigation/bottom-tabs" import { Tabs } from "expo-router" import type { ForwardRefExoticComponent } from "react" import { forwardRef, useMemo, useRef, useState } from "react" -import type { FlatList, ScrollView } from "react-native" +import type { ScrollView } from "react-native" import { useSharedValue } from "react-native-reanimated" import { BottomTabHeightProvider } from "./BottomTabHeightProvider" @@ -15,6 +15,7 @@ import { BottomTabBarVisibleContext, SetBottomTabBarVisibleContext, } from "./contexts/BottomTabBarVisibleContext" +import { useNavigationScrollToTop } from "./hooks" import { Tabbar } from "./Tabbar" type ExtractReactForwardRefExoticComponent = @@ -29,6 +30,7 @@ export const BottomTabs: ForwardRefExoticComponent< useState | null>(null) const currentTarget = useRef(undefined) + const scrollToTop = useNavigationScrollToTop(attachNavigationScrollViewRef) return ( @@ -47,24 +49,7 @@ export const BottomTabs: ForwardRefExoticComponent< } if (currentTarget.current === e.target) { - const $scroller = attachNavigationScrollViewRef?.current as any - - if ("scrollTo" in $scroller) { - ;($scroller as ScrollView).scrollTo({ - y: 0, - animated: true, - }) - } else if ("scrollToIndex" in $scroller) { - ;($scroller as FlatList).scrollToIndex({ - index: 0, - animated: true, - }) - } else if ("scrollToOffset" in $scroller) { - ;($scroller as FlatList).scrollToOffset({ - offset: 0, - animated: true, - }) - } + scrollToTop() return } diff --git a/apps/mobile/src/components/ui/tabbar/Tabbar.tsx b/apps/mobile/src/components/ui/tabbar/Tabbar.tsx index 02c3472cba..14cd69a4e8 100644 --- a/apps/mobile/src/components/ui/tabbar/Tabbar.tsx +++ b/apps/mobile/src/components/ui/tabbar/Tabbar.tsx @@ -17,6 +17,7 @@ import { useSafeAreaInsets } from "react-native-safe-area-context" import { SetBottomTabBarHeightContext } from "@/src/components/ui/tabbar/contexts/BottomTabBarHeightContext" import { quickSpringPreset, softSpringPreset } from "@/src/constants/spring" +import { PlayerTabBar } from "@/src/modules/player/PlayerTabBar" import { accentColor, useColor } from "@/src/theme/colors" import { ThemedBlurView } from "../../common/ThemedBlurView" @@ -42,7 +43,7 @@ export const Tabbar: FC = (props) => { return ( = (props) => { }} > + {routes.map((route, index) => { const focused = index === state.index diff --git a/apps/mobile/src/components/ui/tabbar/hooks.ts b/apps/mobile/src/components/ui/tabbar/hooks.ts index d7144e3d22..dd0a9b02df 100644 --- a/apps/mobile/src/components/ui/tabbar/hooks.ts +++ b/apps/mobile/src/components/ui/tabbar/hooks.ts @@ -1,8 +1,38 @@ -import { useContext } from "react" +import { useCallback, useContext } from "react" +import type { FlatList, ScrollView } from "react-native" +import { AttachNavigationScrollViewContext } from "./contexts/AttachNavigationScrollViewContext" import { BottomTabBarHeightContext } from "./contexts/BottomTabBarHeightContext" export const useBottomTabBarHeight = () => { const height = useContext(BottomTabBarHeightContext) return height } + +export const useNavigationScrollToTop = ( + overrideScrollerRef?: React.RefObject | React.RefObject> | null, +) => { + const attachNavigationScrollViewRef = useContext(AttachNavigationScrollViewContext) + return useCallback(() => { + const $scroller = overrideScrollerRef?.current ?? attachNavigationScrollViewRef?.current + if (!$scroller) return + + if ("scrollTo" in $scroller) { + ;($scroller as ScrollView).scrollTo({ + y: 0, + animated: true, + }) + } else if ("scrollToIndex" in $scroller) { + ;($scroller as FlatList).scrollToIndex({ + index: 0, + animated: true, + }) + } else if ("scrollToOffset" in $scroller) { + ;($scroller as FlatList).scrollToOffset({ + offset: 0, + animated: true, + }) + } + return + }, [attachNavigationScrollViewRef, overrideScrollerRef]) +} diff --git a/apps/mobile/src/lib/api-fetch.ts b/apps/mobile/src/lib/api-fetch.ts index f1c590b04e..cc4db5a6bd 100644 --- a/apps/mobile/src/lib/api-fetch.ts +++ b/apps/mobile/src/lib/api-fetch.ts @@ -1,8 +1,8 @@ /* eslint-disable no-console */ import type { AppType } from "@follow/shared" -import { router } from "expo-router" import { FetchError, ofetch } from "ofetch" +import { userActions } from "../store/user/store" import { getCookie } from "./auth" import { getApiUrl } from "./env" @@ -40,7 +40,7 @@ export const apiFetch = ofetch.create({ console.log(`<--- [Error] ${response.status} ${options.method} ${request as string}`) } if (response.status === 401) { - router.replace("/login") + userActions.removeCurrentUser() } else { console.error(error) } diff --git a/apps/mobile/src/lib/auth.ts b/apps/mobile/src/lib/auth.ts index 33a8029acd..bc744d9988 100644 --- a/apps/mobile/src/lib/auth.ts +++ b/apps/mobile/src/lib/auth.ts @@ -1,5 +1,6 @@ import { expoClient } from "@better-auth/expo/client" import { useQuery } from "@tanstack/react-query" +import { twoFactorClient } from "better-auth/client/plugins" import { createAuthClient } from "better-auth/react" import type * as better_call from "better-call" import * as SecureStore from "expo-secure-store" @@ -15,6 +16,7 @@ export const sessionTokenKey = "__Secure-better-auth.session_token" const authClient = createAuthClient({ baseURL: `${getApiUrl()}/better-auth`, plugins: [ + twoFactorClient(), { id: "getProviders", $InferServerPlugin: {} as (typeof authPlugins)[0], @@ -36,7 +38,7 @@ const authClient = createAuthClient({ }) // @keep-sorted -export const { getCookie, getProviders, signIn, signOut, useSession } = authClient +export const { getCookie, getProviders, signIn, signOut, twoFactor, useSession } = authClient export interface AuthProvider { name: string diff --git a/apps/mobile/src/lib/toast.tsx b/apps/mobile/src/lib/toast.tsx index 31c500b1de..38dca77b32 100644 --- a/apps/mobile/src/lib/toast.tsx +++ b/apps/mobile/src/lib/toast.tsx @@ -1,3 +1,6 @@ +import { requireNativeModule } from "expo" +import { Platform } from "react-native" + import { ToastManager } from "../components/ui/toast/manager" import type { ToastProps } from "../components/ui/toast/types" @@ -11,11 +14,32 @@ type Toast = { success: (message: string, options?: CommandToastOptions) => void info: (message: string, options?: CommandToastOptions) => void } + +interface NativeToasterOptions { + title: string + message: string + type: "error" | "success" | "info" | "warn" + /** + * seconds + */ + duration?: number +} export const toast = { show: toastInstance.show.bind(toastInstance), } as Toast ;(["error", "success", "info"] as const).forEach((type) => { toast[type] = (message: string, options: CommandToastOptions = {}) => { + if (Platform.OS === "ios") { + const NativeToaster = requireNativeModule("Toaster") + NativeToaster.toast({ + title: "", + message, + type, + duration: options.duration ? options.duration / 1000 : 1.5, + } as NativeToasterOptions) + return + } + toastInstance.show({ type, message, diff --git a/apps/mobile/src/modules/entry-list/EntryListContentGrid.tsx b/apps/mobile/src/modules/entry-list/EntryListContentGrid.tsx index 5c61cea938..07a6389854 100644 --- a/apps/mobile/src/modules/entry-list/EntryListContentGrid.tsx +++ b/apps/mobile/src/modules/entry-list/EntryListContentGrid.tsx @@ -1,15 +1,14 @@ import { useTypeScriptHappyCallback } from "@follow/hooks" import type { MasonryFlashListProps } from "@shopify/flash-list" import type { ElementRef } from "react" -import { forwardRef, useCallback } from "react" +import { forwardRef } from "react" import { ActivityIndicator, View } from "react-native" import { useFetchEntriesControls } from "@/src/modules/screen/atoms" -import { useEntryStore } from "@/src/store/entry/store" import { TimelineSelectorMasonryList } from "../screen/TimelineSelectorList" import { useOnViewableItemsChanged } from "./hooks" -import type { MasonryItem } from "./templates/EntryGridItem" +// import type { MasonryItem } from "./templates/EntryGridItem" import { EntryGridItem } from "./templates/EntryGridItem" export const EntryListContentGrid = forwardRef< @@ -19,60 +18,15 @@ export const EntryListContentGrid = forwardRef< } & Omit, "data" | "renderItem"> >(({ entryIds, ...rest }, ref) => { const { fetchNextPage, refetch, isRefetching, hasNextPage } = useFetchEntriesControls() - const onViewableItemsChanged = useOnViewableItemsChanged( - (item) => (item.key as any).split("-")[0], - ) - - const data = useEntryStore( - useCallback( - (state) => { - const data: (MasonryItem & { index: number })[] = [] - - let index = 0 - for (const id of entryIds) { - const entry = state.data[id] - if (!entry) { - continue - } - if (!entry.media) { - continue - } - - for (const media of entry.media) { - if (media.type === "photo") { - data.push({ - id, - index: index++, - type: "image", - imageUrl: media.url, - blurhash: media.blurhash, - width: media.width, - height: media.height, - }) - } else if (media.type === "video") { - data.push({ - id, - index: index++, - type: "video", - videoUrl: media.url, - videoPreviewImageUrl: media.preview_image_url, - }) - } - } - } - return data - }, - [entryIds], - ), - ) + const onViewableItemsChanged = useOnViewableItemsChanged() return ( { - return + data={entryIds} + renderItem={useTypeScriptHappyCallback(({ item }: { item: string }) => { + return }, [])} keyExtractor={defaultKeyExtractor} onViewableItemsChanged={onViewableItemsChanged} @@ -94,7 +48,6 @@ export const EntryListContentGrid = forwardRef< ) }) -const defaultKeyExtractor = (item: MasonryItem & { index: number }) => { - const key = `${item.id}-${item.index}` - return key +const defaultKeyExtractor = (item: string) => { + return item } diff --git a/apps/mobile/src/modules/entry-list/EntryListContext.tsx b/apps/mobile/src/modules/entry-list/EntryListContext.tsx new file mode 100644 index 0000000000..10ecdca763 --- /dev/null +++ b/apps/mobile/src/modules/entry-list/EntryListContext.tsx @@ -0,0 +1,8 @@ +import type { FeedViewType } from "@follow/constants" +import { createContext, useContext } from "react" + +export const EntryListContextViewContext = createContext(null!) + +export const useEntryListContextView = () => { + return useContext(EntryListContextViewContext) +} diff --git a/apps/mobile/src/modules/entry-list/EntryListSelector.tsx b/apps/mobile/src/modules/entry-list/EntryListSelector.tsx index 1d784891ac..7d6dc10602 100644 --- a/apps/mobile/src/modules/entry-list/EntryListSelector.tsx +++ b/apps/mobile/src/modules/entry-list/EntryListSelector.tsx @@ -1,10 +1,15 @@ import { FeedViewType } from "@follow/constants" +import type { FlashList } from "@shopify/flash-list" +import type { RefObject } from "react" +import { useContext, useEffect, useRef } from "react" +import type { ScrollView } from "react-native" -import { useScrollToTopRef } from "@/src/atoms/scroll-to-top" +import { SetAttachNavigationScrollViewContext } from "@/src/components/ui/tabbar/contexts/AttachNavigationScrollViewContext" import { EntryListContentGrid } from "@/src/modules/entry-list/EntryListContentGrid" import { EntryListContentArticle } from "./EntryListContentArticle" import { EntryListContentSocial } from "./EntryListContentSocial" +import { EntryListContextViewContext } from "./EntryListContext" export function EntryListSelector({ entryIds, @@ -15,7 +20,15 @@ export function EntryListSelector({ viewId: FeedViewType active?: boolean }) { - const ref = useScrollToTopRef(active) + const setAttachNavigationScrollViewRef = useContext(SetAttachNavigationScrollViewContext) + + const ref = useRef>(null) + useEffect(() => { + if (!active) return + if (setAttachNavigationScrollViewRef) { + setAttachNavigationScrollViewRef(ref as unknown as RefObject) + } + }, [setAttachNavigationScrollViewRef, ref, active]) let ContentComponent: typeof EntryListContentSocial | typeof EntryListContentGrid = EntryListContentArticle @@ -35,5 +48,9 @@ export function EntryListSelector({ } } - return + return ( + + + + ) } diff --git a/apps/mobile/src/modules/entry-list/hooks.ts b/apps/mobile/src/modules/entry-list/hooks.ts index 9804cedf34..8b7226350d 100644 --- a/apps/mobile/src/modules/entry-list/hooks.ts +++ b/apps/mobile/src/modules/entry-list/hooks.ts @@ -10,6 +10,7 @@ export function useOnViewableItemsChanged( idExtractor: (item: ViewToken) => string = defaultIdExtractor, ): (info: { viewableItems: ViewToken[]; changed: ViewToken[] }) => void { const markAsReadWhenScrolling = useGeneralSettingKey("scrollMarkUnread") + const markAsReadWhenRendering = useGeneralSettingKey("renderMarkUnread") const [stableIdExtractor] = useState(() => idExtractor) @@ -23,7 +24,15 @@ export function useOnViewableItemsChanged( unreadSyncService.markEntryAsRead(stableIdExtractor(item)) }) } + + if (markAsReadWhenRendering) { + viewableItems + .filter((item) => item.isViewable) + .forEach((item) => { + unreadSyncService.markEntryAsRead(stableIdExtractor(item)) + }) + } }, - [markAsReadWhenScrolling, stableIdExtractor], + [markAsReadWhenRendering, markAsReadWhenScrolling, stableIdExtractor], ) } diff --git a/apps/mobile/src/modules/entry-list/index.tsx b/apps/mobile/src/modules/entry-list/index.tsx index cb5823dda6..b61ba3b63d 100644 --- a/apps/mobile/src/modules/entry-list/index.tsx +++ b/apps/mobile/src/modules/entry-list/index.tsx @@ -1,16 +1,10 @@ import { FeedViewType } from "@follow/constants" -import { useIsFocused } from "@react-navigation/native" import { useEffect, useMemo } from "react" import { Animated, StyleSheet } from "react-native" import PagerView from "react-native-pager-view" import { views } from "@/src/constants/views" -import { - selectTimeline, - useSelectedFeed, - useSelectedView, - useSetDrawerSwipeDisabled, -} from "@/src/modules/screen/atoms" +import { selectTimeline, useSelectedFeed, useSelectedView } from "@/src/modules/screen/atoms" import { useEntryIdsByCategory, useEntryIdsByFeedId, @@ -24,16 +18,6 @@ import { EntryListSelector } from "./EntryListSelector" import { usePagerView } from "./usePagerView" export function EntryList() { - const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled() - const isFocused = useIsFocused() - useEffect(() => { - if (isFocused) { - setDrawerSwipeDisabled(false) - } else { - setDrawerSwipeDisabled(true) - } - }, [setDrawerSwipeDisabled, isFocused]) - const selectedFeed = useSelectedFeed() const Content = useMemo(() => { diff --git a/apps/mobile/src/modules/entry-list/templates/EntryGridItem.tsx b/apps/mobile/src/modules/entry-list/templates/EntryGridItem.tsx index 567b68e2ad..97746b2aaf 100644 --- a/apps/mobile/src/modules/entry-list/templates/EntryGridItem.tsx +++ b/apps/mobile/src/modules/entry-list/templates/EntryGridItem.tsx @@ -1,4 +1,5 @@ import { FeedViewType } from "@follow/constants" +import { uniqBy } from "es-toolkit/compat" import { LinearGradient } from "expo-linear-gradient" import { useEffect, useMemo } from "react" import { ScrollView, Text, View } from "react-native" @@ -10,104 +11,122 @@ import { EntryContentWebView, setWebViewEntry, } from "@/src/components/native/webview/EntryContentWebView" +import { MediaCarousel } from "@/src/components/ui/carousel/MediaCarousel" import { RelativeDateTime } from "@/src/components/ui/datetime/RelativeDateTime" import { FeedIcon } from "@/src/components/ui/icon/feed-icon" -import { ImageContextMenu } from "@/src/components/ui/image/ImageContextMenu" import { PreviewImage } from "@/src/components/ui/image/PreviewImage" import { ItemPressable } from "@/src/components/ui/pressable/ItemPressable" +import type { MediaModel } from "@/src/database/schemas/types" +import { openLink } from "@/src/lib/native" import { useEntry } from "@/src/store/entry/hooks" import { useFeed } from "@/src/store/feed/hooks" -import { useSelectedView } from "../../screen/atoms" - -export type MasonryItem = { - id: string -} & ( - | { - type: "image" - imageUrl: string - blurhash?: string - height?: number - width?: number - } - | { - type: "video" - videoUrl: string - videoPreviewImageUrl?: string - } -) -export function EntryGridItem(props: MasonryItem) { - const { type, id } = props - const view = useSelectedView() +import { useEntryListContextView } from "../EntryListContext" + +export function EntryGridItem({ id }: { id: string }) { const item = useEntry(id) + const view = useEntryListContextView() const pictureViewFilterNoImage = useUISettingKey("pictureViewFilterNoImage") + if (!item || !item.media) { + return null + } - const Content = useMemo(() => { - switch (type) { - case "image": { - const { imageUrl, blurhash, height, width } = props - const aspectRatio = height && width ? width / height : 16 / 9 - - return imageUrl ? ( - - { - if (item) { - setWebViewEntry(item) - } - }} - imageUrl={imageUrl} - blurhash={blurhash} - aspectRatio={aspectRatio} - Accessory={EntryGridItemAccessory} - AccessoryProps={{ - id, - }} - /> - - ) : ( - - No media available - - ) - } - case "video": { - const { videoPreviewImageUrl } = props - - return ( - <> - {videoPreviewImageUrl ? ( - - - - ) : ( - - No media available - - )} - - {item?.title} - - - ) - } - } - }, [type, JSON.stringify(props), item?.title]) - if (!item) { + const hasMedia = item.media.length > 0 + + if (pictureViewFilterNoImage && !hasMedia && view === FeedViewType.Pictures) { return null } + if (!hasMedia) { + return ( + + No media available + + ) + } + + const WrapperComponent = view === FeedViewType.Videos ? ItemPressable : View - if ( - pictureViewFilterNoImage && - type === "image" && - !props.imageUrl && - view === FeedViewType.Pictures - ) { + return ( + { + if (!item.url) { + return + } + if (view === FeedViewType.Videos) { + openLink(item.url) + } + }} + > + { + if (item) { + setWebViewEntry(item) + } + }} + /> + + ) +} + +const MediaItems = ({ + media, + view, + entryId, + onPreview, + title, +}: { + media: MediaModel[] + view: FeedViewType + entryId: string + onPreview: () => void + title: string +}) => { + const firstMedia = media[0] + + const uniqMedia = useMemo(() => { + return uniqBy(media, "url") + }, [media]) + + if (!firstMedia) { return null } - return {Content} + const { height } = firstMedia + const { width } = firstMedia + const aspectRatio = width && height ? width / height : 1 + + if (view === FeedViewType.Videos) { + const mediaUrl = firstMedia.preview_image_url || firstMedia.url + const aspectRatio = 16 / 9 + return ( + + + {mediaUrl && ( + + )} + + + {title} + + + ) + } + + return ( + + ) } const EntryGridItemAccessory = ({ id }: { id: string }) => { diff --git a/apps/mobile/src/modules/login/email.tsx b/apps/mobile/src/modules/login/email.tsx index c0cb63c27d..ae89941c58 100644 --- a/apps/mobile/src/modules/login/email.tsx +++ b/apps/mobile/src/modules/login/email.tsx @@ -1,5 +1,6 @@ import { zodResolver } from "@hookform/resolvers/zod" import { useMutation } from "@tanstack/react-query" +import { router } from "expo-router" import { useContext, useEffect } from "react" import type { Control } from "react-hook-form" import { useController, useForm } from "react-hook-form" @@ -34,9 +35,17 @@ async function onSubmit(values: FormValue) { email: values.email, password: values.password, }) + .then((res) => { + if (res.error) { + throw new Error(res.error.message) + } + // @ts-expect-error + if (res.data.twoFactorRedirect) { + router.push("/2fa") + } + }) .catch((error) => { - console.error(error) - toast.error("Login failed") + toast.error(`Failed to login: ${error.message}`) }) } @@ -82,7 +91,7 @@ export function EmailLogin() { const disableColor = useColor("gray3") - const canLogin = useSharedValue(0) + const canLogin = useSharedValue(1) useEffect(() => { canLogin.value = withTiming(submitMutation.isPending || !formState.isValid ? 1 : 0) }, [submitMutation.isPending, formState.isValid, canLogin]) @@ -139,7 +148,7 @@ export function EmailLogin() { {submitMutation.isPending ? ( ) : ( - Continue + Continue )} diff --git a/apps/mobile/src/modules/player/FloatingBar.tsx b/apps/mobile/src/modules/player/FloatingBar.tsx deleted file mode 100644 index d802f41d42..0000000000 --- a/apps/mobile/src/modules/player/FloatingBar.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { cn } from "@follow/utils" -import { Image } from "expo-image" -import { router, usePathname } from "expo-router" -import { Pressable, Text, View } from "react-native" -import ReAnimated, { SlideInDown, SlideOutDown } from "react-native-reanimated" - -import { ThemedBlurView } from "@/src/components/common/ThemedBlurView" -import { useActiveTrack } from "@/src/lib/player" - -import { PlayPauseButton, SeekButton } from "./control" - -const allowedRoutes = new Set(["/", "/subscriptions", "/player"]) - -export function FloatingBar({ className }: { className?: string }) { - const activeTrack = useActiveTrack() - const pathname = usePathname() - if (!activeTrack || !allowedRoutes.has(pathname)) { - return null - } - - return ( - - { - router.push("/player") - }} - > - - - - - {activeTrack.title} - - - - - - - - - - ) -} diff --git a/apps/mobile/src/modules/player/PlayerTabBar.tsx b/apps/mobile/src/modules/player/PlayerTabBar.tsx new file mode 100644 index 0000000000..55f4255316 --- /dev/null +++ b/apps/mobile/src/modules/player/PlayerTabBar.tsx @@ -0,0 +1,48 @@ +import { cn } from "@follow/utils" +import { Image } from "expo-image" +import { router, usePathname } from "expo-router" +import { Pressable, Text, View } from "react-native" +import Animated, { SlideInDown, SlideOutDown } from "react-native-reanimated" + +import { useActiveTrack } from "@/src/lib/player" + +import { PlayPauseButton, SeekButton } from "./control" + +const allowedRoutes = new Set(["/", "/subscriptions", "/player"]) + +export function PlayerTabBar({ className }: { className?: string }) { + const activeTrack = useActiveTrack() + const pathname = usePathname() + if (!activeTrack || !allowedRoutes.has(pathname)) { + return null + } + + return ( + + + { + router.push("/player") + }} + > + + + + + {activeTrack.title} + + + + + + + + + + + ) +} diff --git a/apps/mobile/src/modules/player/control.tsx b/apps/mobile/src/modules/player/control.tsx index 61b4789bf9..eed9c65df4 100644 --- a/apps/mobile/src/modules/player/control.tsx +++ b/apps/mobile/src/modules/player/control.tsx @@ -1,10 +1,11 @@ import { cn } from "@follow/utils" import { router } from "expo-router" -import { Text, TouchableOpacity, View } from "react-native" +import { StyleSheet, Text, TouchableOpacity, View } from "react-native" import { Slider } from "react-native-awesome-slider" -import { useDerivedValue, useSharedValue } from "react-native-reanimated" +import { FadeOut, useDerivedValue, useSharedValue, ZoomIn } from "react-native-reanimated" import * as DropdownMenu from "zeego/dropdown-menu" +import { ReAnimatedPressable } from "@/src/components/common/AnimatedComponents" import { Back2CuteReIcon } from "@/src/icons/back_2_cute_re" import { Forward2CuteReIcon } from "@/src/icons/forward_2_cute_re" import { PauseCuteFiIcon } from "@/src/icons/pause_cute_fi" @@ -31,7 +32,10 @@ export function PlayPauseButton({ size = 24, className, color }: ControlButtonPr const label = useColor("label") return ( - { playing ? player.pause() : player.play() }} @@ -41,7 +45,7 @@ export function PlayPauseButton({ size = 24, className, color }: ControlButtonPr ) : ( )} - + ) } @@ -191,8 +195,9 @@ export function ProgressBar() { @@ -200,12 +205,14 @@ export function ProgressBar() { - {"-"} {trackRemainingTime} + {"-"} + {trackRemainingTime} @@ -250,3 +257,9 @@ export function VolumeBar() { ) } + +const styles = StyleSheet.create({ + text: { + fontVariant: ["tabular-nums"], + }, +}) diff --git a/apps/mobile/src/modules/screen/TimelineViewSelector.tsx b/apps/mobile/src/modules/screen/TimelineViewSelector.tsx index 9f33b24a8d..da16e9b642 100644 --- a/apps/mobile/src/modules/screen/TimelineViewSelector.tsx +++ b/apps/mobile/src/modules/screen/TimelineViewSelector.tsx @@ -30,6 +30,7 @@ export function TimelineViewSelector() { diff --git a/apps/mobile/src/screens/(headless)/2fa.tsx b/apps/mobile/src/screens/(headless)/2fa.tsx new file mode 100644 index 0000000000..8a17b6e94e --- /dev/null +++ b/apps/mobile/src/screens/(headless)/2fa.tsx @@ -0,0 +1,117 @@ +import { useMutation } from "@tanstack/react-query" +import { router } from "expo-router" +import { useMemo, useState } from "react" +import { + ActivityIndicator, + Text, + TextInput, + TouchableOpacity, + TouchableWithoutFeedback, + useAnimatedValue, + View, +} from "react-native" +import { KeyboardAvoidingView, KeyboardController } from "react-native-keyboard-controller" +import { useColor } from "react-native-uikit-colors" + +import { + NavigationBlurEffectHeader, + NavigationContext, +} from "@/src/components/common/SafeNavigationScrollView" +import { MingcuteLeftLineIcon } from "@/src/icons/mingcute_left_line" +import { twoFactor } from "@/src/lib/auth" +import { queryClient } from "@/src/lib/query-client" +import { toast } from "@/src/lib/toast" +import { whoamiQueryKey } from "@/src/store/user/hooks" + +function isAuthCodeValid(authCode: string) { + return ( + authCode.length === 6 && !Array.from(authCode).some((c) => Number.isNaN(Number.parseInt(c))) + ) +} + +export default function TwoFactorAuthScreen() { + const scrollY = useAnimatedValue(0) + const label = useColor("label") + const [authCode, setAuthCode] = useState("") + + const submitMutation = useMutation({ + mutationFn: async (value: string) => { + const res = await twoFactor.verifyTotp({ code: value }) + if (res.error) { + throw new Error(res.error.message) + } + await queryClient.invalidateQueries({ queryKey: whoamiQueryKey }) + }, + onError(error) { + toast.error(`Failed to verify: ${error.message}`) + setAuthCode("") + }, + onSuccess() { + router.replace("/") + }, + }) + + return ( + ({ scrollY }), [scrollY])}> + + + { + return ( + router.back()}> + + + ) + }} + /> + { + KeyboardController.dismiss() + }} + accessible={false} + > + + + + Verify with your authenticator app + + + + + Enter Follow Auth Code + + + + + + + + { + submitMutation.mutate(authCode) + }} + > + {submitMutation.isPending ? ( + + ) : ( + Submit + )} + + + + + + + ) +} diff --git a/apps/mobile/src/screens/(headless)/debug.tsx b/apps/mobile/src/screens/(headless)/debug.tsx index f987a3740f..439dfb1b8f 100644 --- a/apps/mobile/src/screens/(headless)/debug.tsx +++ b/apps/mobile/src/screens/(headless)/debug.tsx @@ -101,11 +101,7 @@ export default function DebugPanel() { { title: "Toast", onPress: () => { - toast.show({ - message: "Hello, world!".repeat(10), - type: "success", - variant: "center-replace", - }) + toast.error("Hello, world!".repeat(10)) }, }, { diff --git a/apps/mobile/src/screens/(headless)/login.tsx b/apps/mobile/src/screens/(headless)/login.tsx index c80da55523..eb991d92ea 100644 --- a/apps/mobile/src/screens/(headless)/login.tsx +++ b/apps/mobile/src/screens/(headless)/login.tsx @@ -6,7 +6,7 @@ import { useWhoami } from "@/src/store/user/hooks" export default function LoginPage() { const whoami = useWhoami() - if (whoami?.id) { + if (whoami?.id && !__DEV__) { return } diff --git a/apps/mobile/src/screens/(stack)/(tabs)/index.tsx b/apps/mobile/src/screens/(stack)/(tabs)/index.tsx index 28c760da64..aaa4328afe 100644 --- a/apps/mobile/src/screens/(stack)/(tabs)/index.tsx +++ b/apps/mobile/src/screens/(stack)/(tabs)/index.tsx @@ -7,5 +7,6 @@ export default function Index() { useEffect(() => { prepareEntryRenderWebView() }, []) + return } diff --git a/apps/mobile/src/screens/(stack)/_layout.tsx b/apps/mobile/src/screens/(stack)/_layout.tsx index 7e858399b0..5761eee160 100644 --- a/apps/mobile/src/screens/(stack)/_layout.tsx +++ b/apps/mobile/src/screens/(stack)/_layout.tsx @@ -1,6 +1,13 @@ -import { Stack } from "expo-router" +import { Redirect, Stack } from "expo-router" + +import { useWhoami } from "@/src/store/user/hooks" export default function AppRootLayout() { + const whoami = useWhoami() + + if (!whoami?.id) { + return + } return ( - + ) } diff --git a/apps/mobile/src/services/user.ts b/apps/mobile/src/services/user.ts index 22bc4fb692..c35f98c701 100644 --- a/apps/mobile/src/services/user.ts +++ b/apps/mobile/src/services/user.ts @@ -1,3 +1,5 @@ +import { eq } from "drizzle-orm" + import { db } from "../database" import { usersTable } from "../database/schemas" import type { UserSchema } from "../database/schemas/types" @@ -20,6 +22,10 @@ class UserServiceStatic implements Hydratable { const users = await db.query.usersTable.findMany() userActions.upsertManyInSession(users) } + + async removeCurrentUser() { + await db.update(usersTable).set({ isMe: 0 }).where(eq(usersTable.isMe, 1)) + } } export const UserService = new UserServiceStatic() diff --git a/apps/mobile/src/store/entry/hooks.ts b/apps/mobile/src/store/entry/hooks.ts index 36c8ad47d5..6635cfd1a4 100644 --- a/apps/mobile/src/store/entry/hooks.ts +++ b/apps/mobile/src/store/entry/hooks.ts @@ -7,9 +7,9 @@ import { entrySyncServices, useEntryStore } from "./store" import type { EntryModel, FetchEntriesProps } from "./types" export const usePrefetchEntries = (props: Omit | null) => { - const { feedId, inboxId, listId, view, read, limit, isArchived } = props || {} + const { feedId, inboxId, listId, view, read, limit } = props || {} return useInfiniteQuery({ - queryKey: ["entries", feedId, inboxId, listId, view, read, limit, isArchived], + queryKey: ["entries", feedId, inboxId, listId, view, read, limit], queryFn: ({ pageParam }) => entrySyncServices.fetchEntries({ ...props, pageParam }), getNextPageParam: (lastPage) => listId diff --git a/apps/mobile/src/store/entry/store.ts b/apps/mobile/src/store/entry/store.ts index 5ca5da1945..cddd4b547b 100644 --- a/apps/mobile/src/store/entry/store.ts +++ b/apps/mobile/src/store/entry/store.ts @@ -252,8 +252,7 @@ class EntryActions { class EntrySyncServices { async fetchEntries(props: FetchEntriesProps) { - const { feedId, inboxId, listId, view, read, limit, pageParam, isArchived, isCollection } = - props + const { feedId, inboxId, listId, view, read, limit, pageParam, isCollection } = props const params = getEntriesParams({ feedId, inboxId, @@ -265,7 +264,6 @@ class EntrySyncServices { publishedAfter: pageParam, read, limit, - isArchived, isCollection, ...params, }, diff --git a/apps/mobile/src/store/entry/types.ts b/apps/mobile/src/store/entry/types.ts index 2555a6cafe..383bc432af 100644 --- a/apps/mobile/src/store/entry/types.ts +++ b/apps/mobile/src/store/entry/types.ts @@ -9,6 +9,5 @@ export type FetchEntriesProps = { read?: boolean limit?: number pageParam?: string - isArchived?: boolean isCollection?: boolean } diff --git a/apps/mobile/src/store/user/store.ts b/apps/mobile/src/store/user/store.ts index e69c781ebb..64e6c1735b 100644 --- a/apps/mobile/src/store/user/store.ts +++ b/apps/mobile/src/store/user/store.ts @@ -4,7 +4,7 @@ import { UserService } from "@/src/services/user" import { createImmerSetter, createTransaction, createZustandStore } from "../internal/helper" -export type UserModel = Omit +export type UserModel = UserSchema type UserStore = { users: Record whoami: UserModel | null @@ -40,6 +40,9 @@ class UserActions { immerSet((state) => { for (const user of users) { state.users[user.id] = user + if (user.isMe) { + state.whoami = user + } } }) } @@ -55,6 +58,17 @@ class UserActions { ) await tx.run() } + + async removeCurrentUser() { + const tx = createTransaction() + tx.store(() => { + immerSet((state) => { + state.whoami = null + }) + }) + tx.persist(() => UserService.removeCurrentUser()) + await tx.run() + } } export const userSyncService = new UserSyncService() diff --git a/apps/renderer/src/components/ui/modal/stacked/modal.tsx b/apps/renderer/src/components/ui/modal/stacked/modal.tsx index d0dd389876..574231961f 100644 --- a/apps/renderer/src/components/ui/modal/stacked/modal.tsx +++ b/apps/renderer/src/components/ui/modal/stacked/modal.tsx @@ -361,12 +361,12 @@ export const ModalInternal = memo( defaultSize={resizeDefaultSize} className="flex grow flex-col" > -
- +
+ {!!icon && {icon}} {title} diff --git a/apps/renderer/src/constants/app.tsx b/apps/renderer/src/constants/app.tsx index 73d4d576fe..2747f6b0fe 100644 --- a/apps/renderer/src/constants/app.tsx +++ b/apps/renderer/src/constants/app.tsx @@ -10,8 +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 ROUTE_TIMELINE_OF_VIEW = "view-" -export const ROUTE_TIMELINE_OF_LIST = "list-" -export const ROUTE_TIMELINE_OF_INBOX = "inbox-" export const INBOX_PREFIX_ID = "inbox-" diff --git a/apps/renderer/src/hooks/biz/useNavigateEntry.ts b/apps/renderer/src/hooks/biz/useNavigateEntry.ts index 950372383e..ce4ada9e1d 100644 --- a/apps/renderer/src/hooks/biz/useNavigateEntry.ts +++ b/apps/renderer/src/hooks/biz/useNavigateEntry.ts @@ -10,9 +10,9 @@ 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, - ROUTE_TIMELINE_OF_INBOX, - ROUTE_TIMELINE_OF_LIST, ROUTE_TIMELINE_OF_VIEW, } from "~/constants" @@ -44,8 +44,8 @@ export const useNavigateEntry = () => { /* * /timeline/:timelineId/:feedId/:entryId - * timelineId: view-1, list-xxx, inbox-xxx - * feedId: xxx, folder-xxx + * timelineId: view-1 + * feedId: xxx, folder-xxx, list-xxx, inbox-xxx * entryId: xxx */ export const navigateEntry = (options: NavigateEntryOptions) => { @@ -63,18 +63,18 @@ export const navigateEntry = (options: NavigateEntryOptions) => { finalFeedId = `${ROUTE_FEED_IN_FOLDER}${folderName}` } - finalFeedId = encodeURIComponent(finalFeedId) - - if (view !== undefined) { - finalTimelineId = `${ROUTE_TIMELINE_OF_VIEW}${view}` + if (listId) { + finalFeedId = `${ROUTE_FEED_IN_LIST}${listId}` } if (inboxId) { - finalTimelineId = `${ROUTE_TIMELINE_OF_INBOX}${inboxId}` + finalFeedId = `${ROUTE_FEED_IN_INBOX}${inboxId}` } - if (listId) { - finalTimelineId = `${ROUTE_TIMELINE_OF_LIST}${listId}` + finalFeedId = encodeURIComponent(finalFeedId) + + if (view !== undefined) { + finalTimelineId = `${ROUTE_TIMELINE_OF_VIEW}${view}` } resetShowSourceContent() diff --git a/apps/renderer/src/hooks/biz/useRouteParams.ts b/apps/renderer/src/hooks/biz/useRouteParams.ts index 7dad483b0b..77c356f8c3 100644 --- a/apps/renderer/src/hooks/biz/useRouteParams.ts +++ b/apps/renderer/src/hooks/biz/useRouteParams.ts @@ -7,9 +7,9 @@ import { FEED_COLLECTION_LIST, ROUTE_ENTRY_PENDING, ROUTE_FEED_IN_FOLDER, + ROUTE_FEED_IN_INBOX, + ROUTE_FEED_IN_LIST, ROUTE_FEED_PENDING, - ROUTE_TIMELINE_OF_INBOX, - ROUTE_TIMELINE_OF_LIST, ROUTE_TIMELINE_OF_VIEW, } from "~/constants" import { getListById } from "~/store/list" @@ -38,8 +38,8 @@ export interface BizRouteParams { } const parseRouteParams = (params: Params): BizRouteParams => { - const listId = params.timelineId?.startsWith(ROUTE_TIMELINE_OF_LIST) - ? params.timelineId.slice(ROUTE_TIMELINE_OF_LIST.length) + const listId = params.feedId?.startsWith(ROUTE_FEED_IN_LIST) + ? params.feedId.slice(ROUTE_FEED_IN_LIST.length) : undefined const list = listId ? getListById(listId) : undefined @@ -59,8 +59,8 @@ const parseRouteParams = (params: Params): BizRouteParams => { folderName: params.feedId?.startsWith(ROUTE_FEED_IN_FOLDER) ? params.feedId.slice(ROUTE_FEED_IN_FOLDER.length) : undefined, - inboxId: params.timelineId?.startsWith(ROUTE_TIMELINE_OF_INBOX) - ? params.timelineId.slice(ROUTE_TIMELINE_OF_INBOX.length) + inboxId: params.feedId?.startsWith(ROUTE_FEED_IN_INBOX) + ? params.feedId.slice(ROUTE_FEED_IN_INBOX.length) : undefined, listId, timelineId: params.timelineId, diff --git a/apps/renderer/src/hooks/biz/useTimelineList.ts b/apps/renderer/src/hooks/biz/useTimelineList.ts index a408a3ef6d..d4747e4d8b 100644 --- a/apps/renderer/src/hooks/biz/useTimelineList.ts +++ b/apps/renderer/src/hooks/biz/useTimelineList.ts @@ -1,21 +1,11 @@ import { useMemo } from "react" -import { useAllInboxes, useAllLists, useViewWithSubscription } from "~/store/subscription/hooks" +import { useViewWithSubscription } from "~/store/subscription/hooks" export const useTimelineList = () => { - const lists = useAllLists() - const inboxes = useAllInboxes() const views = useViewWithSubscription() - const listsIds = useMemo(() => lists.map((list) => `list-${list.listId}`), [lists]) - const inboxesIds = useMemo(() => inboxes.map((inbox) => `inbox-${inbox.inboxId}`), [inboxes]) const viewsIds = useMemo(() => views.map((view) => `view-${view}`), [views]) - return { - views: viewsIds, - lists: listsIds, - inboxes: inboxesIds, - - all: useMemo(() => [...viewsIds, ...inboxesIds, ...listsIds], [viewsIds, listsIds, inboxesIds]), - } + return viewsIds } diff --git a/apps/renderer/src/modules/entry-column/hooks/useEntriesByView.ts b/apps/renderer/src/modules/entry-column/hooks/useEntriesByView.ts index a55268307f..f00c1be052 100644 --- a/apps/renderer/src/modules/entry-column/hooks/useEntriesByView.ts +++ b/apps/renderer/src/modules/entry-column/hooks/useEntriesByView.ts @@ -13,13 +13,7 @@ import { useFolderFeedsByFeedId } from "~/store/subscription" import { feedUnreadActions } from "~/store/unread" const anyString = [] as string[] -export const useEntriesByView = ({ - onReset, - isArchived, -}: { - onReset?: () => void - isArchived?: boolean -}) => { +export const useEntriesByView = ({ onReset }: { onReset?: () => void }) => { const { feedId, isAllFeeds, view, isCollection, inboxId, listId } = useRouteParams() const unreadOnly = useGeneralSettingKey("unreadOnly") @@ -36,7 +30,6 @@ export const useEntriesByView = ({ listId, view, ...(unreadOnly === true && { read: false }), - isArchived, } if (feedId && listId && isBizId(feedId)) { @@ -44,7 +37,7 @@ export const useEntriesByView = ({ } return params - }, [feedId, folderIds, inboxId, isArchived, listId, unreadOnly, view]) + }, [feedId, folderIds, inboxId, listId, unreadOnly, view]) const query = useEntries(entriesOptions) const [fetchedTime, setFetchedTime] = useState() @@ -111,15 +104,12 @@ export const useEntriesByView = ({ const isFetchingFirstPage = query.isFetching && !query.isFetchingNextPage useEffect(() => { - if (isArchived) { - return - } if (!isFetchingFirstPage) { prevEntryIdsRef.current = entryIds onReset?.() } - }, [isFetchingFirstPage, isArchived]) + }, [isFetchingFirstPage]) const entryIdsAsDeps = entryIds.toString() @@ -139,12 +129,8 @@ export const useEntriesByView = ({ const sortEntries = useMemo( () => - isCollection - ? sortEntriesIdByStarAt(entryIds) - : listId - ? sortEntriesIdByEntryInsertedAt(entryIds) - : sortEntriesIdByEntryPublishedAt(entryIds), - [entryIds, isCollection, listId], + isCollection ? sortEntriesIdByStarAt(entryIds) : sortEntriesIdByEntryPublishedAt(entryIds), + [entryIds, isCollection], ) const groupByDate = useGeneralSettingKey("groupByDate") @@ -214,17 +200,6 @@ function sortEntriesIdByStarAt(entries: string[]) { }) } -function sortEntriesIdByEntryInsertedAt(entries: string[]) { - const entriesId2Map = entryActions.getFlattenMapEntries() - return entries - .slice() - .sort( - (a, b) => - entriesId2Map[b]?.entries.insertedAt.localeCompare(entriesId2Map[a]?.entries.insertedAt!) || - 0, - ) -} - const useFetchEntryContentByStream = (remoteEntryIds?: string[]) => { const { mutate: updateEntryContent } = useMutation({ mutationKey: ["stream-entry-content", remoteEntryIds], diff --git a/apps/renderer/src/modules/entry-column/hooks/useMarkAll.ts b/apps/renderer/src/modules/entry-column/hooks/useMarkAll.ts index 8289bed7c2..bd0335510a 100644 --- a/apps/renderer/src/modules/entry-column/hooks/useMarkAll.ts +++ b/apps/renderer/src/modules/entry-column/hooks/useMarkAll.ts @@ -10,7 +10,7 @@ export interface MarkAllFilter { } export const useMarkAllByRoute = (filter?: MarkAllFilter) => { const routerParams = useRouteParams() - const { feedId, view, inboxId } = routerParams + const { feedId, view, inboxId, listId } = routerParams const folderIds = useFolderFeedsByFeedId({ feedId, view, @@ -27,6 +27,12 @@ export const useMarkAllByRoute = (filter?: MarkAllFilter) => { view, filter, }) + } else if (listId) { + subscriptionActions.markReadByFeedIds({ + listId, + view, + filter, + }) } else if (folderIds?.length) { subscriptionActions.markReadByFeedIds({ feedIds: folderIds, diff --git a/apps/renderer/src/modules/entry-column/index.tsx b/apps/renderer/src/modules/entry-column/index.tsx index 7379c5f45f..ba4d014280 100644 --- a/apps/renderer/src/modules/entry-column/index.tsx +++ b/apps/renderer/src/modules/entry-column/index.tsx @@ -1,12 +1,10 @@ import { useMobile } from "@follow/components/hooks/useMobile.js" -import { Button } from "@follow/components/ui/button/index.js" import { FeedViewType, views } from "@follow/constants" import { useTitle } from "@follow/hooks" import type { FeedModel } from "@follow/models/types" import { isBizId } from "@follow/utils/utils" import type { Range, Virtualizer } from "@tanstack/react-virtual" -import { memo, useCallback, useEffect, useRef, useState } from "react" -import { useTranslation } from "react-i18next" +import { memo, useCallback, useEffect, useRef } from "react" import { useGeneralSettingKey } from "~/atoms/settings/general" import { FeedFoundCanBeFollowError } from "~/components/errors/FeedFoundCanBeFollowErrorFallback" @@ -28,18 +26,14 @@ import { EntryEmptyList, EntryList } from "./list" import { EntryColumnWrapper } from "./wrapper" function EntryColumnImpl() { - const { t } = useTranslation() - const [isArchived, setIsArchived] = useState(false) - const unreadOnly = useGeneralSettingKey("unreadOnly") const listRef = useRef>() const entries = useEntriesByView({ onReset: useCallback(() => { listRef.current?.scrollToIndex(0) }, []), - isArchived, }) - const { entriesIds, isFetchingNextPage, groupedCounts } = entries + const { entriesIds, groupedCounts } = entries useSnapEntryIdList(entriesIds) const { @@ -48,14 +42,8 @@ function EntryColumnImpl() { feedId: routeFeedId, isPendingEntry, isCollection, - inboxId, - listId, } = useRouteParams() - useEffect(() => { - setIsArchived(false) - }, [view, routeFeedId]) - const activeEntry = useEntry(activeEntryId) const feed = useFeedById(routeFeedId) const title = useFeedHeaderTitle() @@ -76,35 +64,6 @@ function EntryColumnImpl() { const handleMarkReadInRange = useEntryMarkReadHandler(entriesIds) - useEffect(() => { - if (isArchived) { - if (entries.hasNextPage) { - entries.fetchNextPage() - } else { - entries.refetch() - } - } - }, [isArchived]) - - // Common conditions for both showArchivedButton and shouldLoadArchivedEntries - const commonConditions = - !isArchived && !unreadOnly && !isCollection && routeFeedId !== ROUTE_FEED_PENDING - - // Determine if the archived button should be shown - const showArchivedButton = commonConditions && feed?.type === "feed" - const hasNoEntries = entries.data?.pages?.[0]?.data?.length === 0 && !entries.isLoading - - // Determine if archived entries should be loaded - const shouldLoadArchivedEntries = - commonConditions && (feed?.type === "feed" || !feed) && !inboxId && !listId && hasNoEntries - - // automatically fetch archived entries when there is no entries in timeline - useEffect(() => { - if (shouldLoadArchivedEntries) { - setIsArchived(true) - } - }, [shouldLoadArchivedEntries]) - const handleScroll = useCallback(() => { if (!isInteracted.current) { isInteracted.current = true @@ -185,7 +144,7 @@ function EntryColumnImpl() { onPullToRefresh={entries.refetch} key={`${routeFeedId}-${view}`} > - {entriesIds.length === 0 && !showArchivedButton ? ( + {entriesIds.length === 0 ? ( entries.isLoading ? null : ( ) @@ -201,15 +160,6 @@ function EntryColumnImpl() { fetchNextPage={fetchNextPage} refetch={entries.refetch} groupCounts={groupedCounts} - Footer={ - !isFetchingNextPage && showArchivedButton ? ( -
- -
- ) : null - } /> )} diff --git a/apps/renderer/src/modules/entry-column/layouts/EntryListHeader.desktop.tsx b/apps/renderer/src/modules/entry-column/layouts/EntryListHeader.desktop.tsx index 238f99d692..dd68ea9511 100644 --- a/apps/renderer/src/modules/entry-column/layouts/EntryListHeader.desktop.tsx +++ b/apps/renderer/src/modules/entry-column/layouts/EntryListHeader.desktop.tsx @@ -39,7 +39,7 @@ export const EntryListHeader: FC<{ const unreadOnly = useGeneralSettingKey("unreadOnly") - const { feedId, entryId, view, listId } = routerParams + const { feedId, entryId, view } = routerParams const headerTitle = useFeedHeaderTitle() @@ -58,7 +58,6 @@ export const EntryListHeader: FC<{ const isOnline = useIsOnline() const feed = useFeedById(feedId) - const isList = !!listId const containerRef = React.useRef(null) const titleStyleBasedView = ["pl-6", "pl-7", "pl-7", "pl-7", "px-5", "pl-6"] @@ -127,26 +126,22 @@ export const EntryListHeader: FC<{ /> ))} - {!isList && ( - <> - setGeneralSetting("unreadOnly", !unreadOnly)} - > - {unreadOnly ? ( - - ) : ( - - )} - - - - )} + setGeneralSetting("unreadOnly", !unreadOnly)} + > + {unreadOnly ? ( + + ) : ( + + )} + +
diff --git a/apps/renderer/src/modules/power/my-wallet-section/index.tsx b/apps/renderer/src/modules/power/my-wallet-section/index.tsx index 6f153e56a6..2058221933 100644 --- a/apps/renderer/src/modules/power/my-wallet-section/index.tsx +++ b/apps/renderer/src/modules/power/my-wallet-section/index.tsx @@ -11,6 +11,7 @@ import { cn } from "@follow/utils/utils" import { useMutation } from "@tanstack/react-query" import { Trans, useTranslation } from "react-i18next" +import { useServerConfigs } from "~/atoms/server-configs" import { CopyButton } from "~/components/ui/button/CopyButton" import { apiClient } from "~/lib/api-fetch" import { getBlockchainExplorerUrl } from "~/lib/utils" @@ -30,6 +31,7 @@ export const MyWalletSection = ({ className }: { className?: string }) => { const wallet = useWallet() const myWallet = wallet.data?.[0] + const serverConfigs = useServerConfigs() const rewardDescriptionModal = useRewardDescriptionModal() const refreshMutation = useMutation({ @@ -133,35 +135,39 @@ export const MyWalletSection = ({ className }: { className?: string }) => { - - -
{t("wallet.power.rewardDescription")}
-
- - {BigInt(myWallet.todayDailyPower || 0n)} - - ), - Link:
-
-
-
- - + {!!serverConfigs?.DAILY_POWER_SUPPLY && ( + <> + + +
{t("wallet.power.rewardDescription")}
+
+ + {BigInt(myWallet.todayDailyPower || 0n)} + + ), + Link:
- - {BigInt(myWallet.todayDailyPower || 0n)} -
- -
+
+
+
+ + +
+ + {BigInt(myWallet.todayDailyPower || 0n)} +
+ +
+ + )}
) } diff --git a/apps/renderer/src/modules/timeline-column/FeedItem.tsx b/apps/renderer/src/modules/timeline-column/FeedItem.tsx index dae89ff92b..91e65a36f3 100644 --- a/apps/renderer/src/modules/timeline-column/FeedItem.tsx +++ b/apps/renderer/src/modules/timeline-column/FeedItem.tsx @@ -27,7 +27,7 @@ import { FeedTitle } from "~/modules/feed/feed-title" import { getPreferredTitle, useFeedById } from "~/store/feed" import { useInboxById } from "~/store/inbox" import { useListById } from "~/store/list" -import { subscriptionActions, useSubscriptionByFeedId } from "~/store/subscription" +import { useSubscriptionByFeedId } from "~/store/subscription" import { useFeedUnreadStore } from "~/store/unread" import { useSelectedFeedIdsState } from "./atom" @@ -255,9 +255,6 @@ const ListItemImpl: Component<{ 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() @@ -304,7 +301,7 @@ const ListItemImpl: Component<{ )} - + ) } diff --git a/apps/renderer/src/modules/timeline-column/FeedList.desktop.tsx b/apps/renderer/src/modules/timeline-column/FeedList.desktop.tsx index bf79b0b581..08e05d792d 100644 --- a/apps/renderer/src/modules/timeline-column/FeedList.desktop.tsx +++ b/apps/renderer/src/modules/timeline-column/FeedList.desktop.tsx @@ -2,6 +2,7 @@ import { useDraggable } from "@dnd-kit/core" import { ScrollArea } from "@follow/components/ui/scroll-area/index.js" import { cn, isKeyForMultiSelectPressed } from "@follow/utils/utils" import { forwardRef, memo, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react" +import { useTranslation } from "react-i18next" import Selecto from "react-selecto" import { useEventListener } from "usehooks-ts" @@ -15,20 +16,38 @@ import { useSelectedFeedIdsState, } from "./atom" import { DraggableContext } from "./context" -import { EmptyFeedList, ListHeader, StarredItem, useFeedsGroupedData } from "./FeedList.shared" +import { + EmptyFeedList, + ListHeader, + StarredItem, + useFeedsGroupedData, + useInboxesGroupedData, + useListsGroupedData, +} from "./FeedList.shared" import { useShouldFreeUpSpace } from "./hook" -import { SortableFeedList } from "./sort-by" +import { SortableFeedList, SortByAlphabeticalInbox, SortByAlphabeticalList } from "./sort-by" const FeedListImpl = forwardRef( ({ className, view }, ref) => { const feedsData = useFeedsGroupedData(view) + const listsData = useListsGroupedData(view) + const inboxesData = useInboxesGroupedData(view) + const categoryOpenStateData = useCategoryOpenStateByView(view) - const hasData = Object.keys(feedsData).length > 0 + const hasData = + Object.keys(feedsData).length > 0 || + Object.keys(listsData).length > 0 || + Object.keys(inboxesData).length > 0 + + const { t } = useTranslation() // Data prefetch useAuthQuery(Queries.lists.list()) + const hasListData = Object.keys(listsData).length > 0 + const hasInboxData = Object.keys(inboxesData).length > 0 + const scrollerRef = useRef(null) const selectoRef = useRef(null) const [selectedFeedIds, setSelectedFeedIds] = useSelectedFeedIdsState() @@ -196,6 +215,32 @@ const FeedListImpl = forwardRef + {hasListData && ( + <> +
+ {t("words.lists")} +
+ + + )} + {hasInboxData && ( + <> +
+ {t("words.inbox")} +
+ + + )} + {(hasListData || hasInboxData) && ( +
+ {t("words.feeds")} +
+ )}
{hasData ? ( diff --git a/apps/renderer/src/modules/timeline-column/InboxList.tsx b/apps/renderer/src/modules/timeline-column/InboxList.tsx deleted file mode 100644 index b18692caaf..0000000000 --- a/apps/renderer/src/modules/timeline-column/InboxList.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { env } from "@follow/shared/env" -import { useTranslation } from "react-i18next" - -import { CopyButton } from "~/components/ui/button/CopyButton" -import { useInboxById } from "~/store/inbox" -import { useFeedUnreadStore } from "~/store/unread" - -import { UnreadNumber } from "./UnreadNumber" - -export const InboxList = ({ id }: { id: string }) => { - const { t } = useTranslation() - const inboxUnread = useFeedUnreadStore((state) => state.data[id] || 0) - const inbox = useInboxById(id) - - return ( -
-
-
{inbox?.title || t("words.inbox")}
-
- -
-
-
-
- {t("discover.inbox.description")} - - {t("discover.inbox.webhooks_docs")} - -
-
-
{t("discover.inbox.handle")}
-
{id}
-
-
-
{t("discover.inbox.email")}
-
- - {id} - {env.VITE_INBOXES_EMAIL} - - -
-
-
-
- ) -} diff --git a/apps/renderer/src/modules/timeline-column/ListFeedList.tsx b/apps/renderer/src/modules/timeline-column/ListFeedList.tsx deleted file mode 100644 index b6aa86cafe..0000000000 --- a/apps/renderer/src/modules/timeline-column/ListFeedList.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { ScrollArea } from "@follow/components/ui/scroll-area/index.js" -import { - Tooltip, - TooltipContent, - TooltipPortal, - TooltipTrigger, -} from "@follow/components/ui/tooltip/index.js" -import { cn } from "@follow/utils/utils" -import dayjs from "dayjs" -import type { FC, MouseEventHandler } from "react" -import { useTranslation } from "react-i18next" - -import { useNavigateEntry } from "~/hooks/biz/useNavigateEntry" -import { useRouteParamsSelector } from "~/hooks/biz/useRouteParams" -import { useList } from "~/queries/lists" -import { useFeedById } from "~/store/feed" -import { useListById } from "~/store/list" - -import { FeedIcon } from "../feed/feed-icon" -import { FeedTitle } from "../feed/feed-title" -import { feedColumnStyles } from "./styles" - -export const ListFeedList: FC<{ listId: string }> = ({ listId }) => { - const { t } = useTranslation() - const cachedList = useListById(listId) - const res = useList({ id: listId, noExtras: true }) - const list = res.data?.list || cachedList - const currentFeedId = useRouteParamsSelector((s) => s.feedId) - - if (!list) return null - return ( - <> -
-
{list?.title || t("words.inbox")}
-
- - {list?.feedIds.map((feedId) => ( - - ))} - - - ) -} - -interface FeedItemProps { - feedId: string - isActive: boolean -} -const FeedItem = ({ feedId, isActive }: FeedItemProps) => { - const feed = useFeedById(feedId) - const { t } = useTranslation() - const navigate = useNavigateEntry() - - if (!feed) return null - - const handleClick: MouseEventHandler = (e) => { - e.stopPropagation() - navigate({ feedId }) - } - return ( -
-
- - - {feed?.errorAt && ( - - - - - - -
- - {t("feed_item.error_since")}{" "} - {dayjs - .duration(dayjs(feed.errorAt).diff(dayjs(), "minute"), "minute") - .humanize(true)} -
- {!!feed.errorMessage && ( -
- - {feed.errorMessage} -
- )} -
-
-
- )} -
-
- ) -} diff --git a/apps/renderer/src/modules/timeline-column/QuickSelectorPanel.tsx b/apps/renderer/src/modules/timeline-column/QuickSelectorPanel.tsx deleted file mode 100644 index 425133db0e..0000000000 --- a/apps/renderer/src/modules/timeline-column/QuickSelectorPanel.tsx +++ /dev/null @@ -1,258 +0,0 @@ -import { Divider } from "@follow/components/ui/divider/Divider.js" -import { RootPortal } from "@follow/components/ui/portal/index.js" -import { ScrollArea } from "@follow/components/ui/scroll-area/ScrollArea.js" -import { EllipsisHorizontalTextWithTooltip } from "@follow/components/ui/typography/EllipsisWithTooltip.js" -import type { FeedViewType } from "@follow/constants" -import { views } from "@follow/constants" -import { cn } from "@follow/utils/utils" -import { AnimatePresence, m, useAnimationControls } from "framer-motion" -import type { HTMLAttributes } from "react" -import { forwardRef, useEffect, useState } from "react" -import { useTranslation } from "react-i18next" - -import { useShowContextMenu } from "~/atoms/context-menu" -import { useUISettingKey } from "~/atoms/settings/ui" -import { - ROUTE_TIMELINE_OF_INBOX, - ROUTE_TIMELINE_OF_LIST, - ROUTE_TIMELINE_OF_VIEW, -} from "~/constants" -import { useListActions } from "~/hooks/biz/useFeedActions" -import { useNavigateEntry } from "~/hooks/biz/useNavigateEntry" -import { useContextMenu } from "~/hooks/common" -import { useInboxById } from "~/store/inbox" -import { useListById } from "~/store/list" - -import { FeedIcon } from "../feed/feed-icon" -import { feedColumnStyles } from "./styles" - -interface QuickSelectorPanelProps { - rootContainer: HTMLElement | null - timelineList: { - views: string[] - lists: string[] - inboxes: string[] - all: string[] - } - activeTimelineId: string | undefined - open: boolean -} - -export const QuickSelectorPanel = forwardRef( - ({ rootContainer, timelineList, activeTimelineId, open: shouldOpen }, ref) => { - const feedColWidth = useUISettingKey("feedColWidth") - const animateControls = useAnimationControls() - const [renderPanel, setRenderPanel] = useState(false) - - useEffect(() => { - let cancel = false - if (shouldOpen) { - setRenderPanel(true) - animateControls.start({ - transform: "translateX(0%)", - }) - } else { - animateControls - .start({ - transform: "translateX(-100%)", - }) - .then(() => { - if (cancel) return - setRenderPanel(false) - }) - } - return () => { - cancel = true - } - }, [animateControls, renderPanel, shouldOpen]) - - return ( - - -
- - {renderPanel && ( - <> -
- Quick Selector - - -
- - - - - - - - - )} -
-
-
-
- ) - }, -) - -interface TimelineItemProps { - timelineId: string - isActive: boolean -} -const TimelineListViewItem = ({ timelineId, isActive }: TimelineItemProps) => { - const id = Number.parseInt(timelineId.slice(ROUTE_TIMELINE_OF_VIEW.length), 10) as FeedViewType - const item = views.find((item) => item.view === id)! - const { t } = useTranslation() - - return ( - -
- {item.icon} - - {t(item.name as any)} - -
-
- ) -} - -const ItemBase: Component< - { timelineId: string; isActive: boolean } & HTMLAttributes -> = ({ timelineId, isActive, children, className, ...props }) => { - const navigate = useNavigateEntry() - return ( - - ) -} - -const TimelineListListItem = ({ timelineId, isActive }: TimelineItemProps) => { - const id = timelineId.slice(ROUTE_TIMELINE_OF_LIST.length) - const list = useListById(id) - - const items = useListActions({ listId: id }) - const showContextMenu = useShowContextMenu() - - const contextMenuProps = useContextMenu({ - onContextMenu: async (e) => { - await showContextMenu(items, e) - }, - }) - - if (!list) return null - return ( - -
- - - {list.title} - -
-
- ) -} - -const TimelineInboxListItem = ({ timelineId, isActive }: TimelineItemProps) => { - const id = timelineId.slice(ROUTE_TIMELINE_OF_INBOX.length) - const inbox = useInboxById(id) - - if (!inbox) return null - return ( - -
- - - {inbox.title} - -
-
- ) -} - -interface TimelineSectionProps { - title: string - items: string[] - ItemComponent: React.ComponentType - activeTimelineId?: string | undefined -} - -const TimelineSection = ({ - title, - items, - activeTimelineId, - ItemComponent, -}: TimelineSectionProps) => { - return ( - <> -
{title}
- {items.length > 0 ? ( -
- {items.map((timelineId) => ( - - ))} -
- ) : ( -
Nothing in {title}
- )} - - ) -} diff --git a/apps/renderer/src/modules/timeline-column/TimelineList.tsx b/apps/renderer/src/modules/timeline-column/TimelineList.tsx index ba8b942aa5..1306947fe0 100644 --- a/apps/renderer/src/modules/timeline-column/TimelineList.tsx +++ b/apps/renderer/src/modules/timeline-column/TimelineList.tsx @@ -1,24 +1,12 @@ import type { FeedViewType } from "@follow/constants" -import { - ROUTE_TIMELINE_OF_INBOX, - ROUTE_TIMELINE_OF_LIST, - ROUTE_TIMELINE_OF_VIEW, -} from "~/constants" +import { ROUTE_TIMELINE_OF_VIEW } from "~/constants" import { FeedList } from "./FeedList" -import { InboxList } from "./InboxList" -import { ListFeedList } from "./ListFeedList" export default function TimelineList({ timelineId }: { timelineId: string }) { if (timelineId.startsWith(ROUTE_TIMELINE_OF_VIEW)) { const id = Number.parseInt(timelineId.slice(ROUTE_TIMELINE_OF_VIEW.length), 10) as FeedViewType return - } else if (timelineId.startsWith(ROUTE_TIMELINE_OF_LIST)) { - const listId = timelineId.slice(ROUTE_TIMELINE_OF_LIST.length) - return - } else if (timelineId.startsWith(ROUTE_TIMELINE_OF_INBOX)) { - const id = timelineId.slice(ROUTE_TIMELINE_OF_INBOX.length) - return } } diff --git a/apps/renderer/src/modules/timeline-column/TimelineSelector.tsx b/apps/renderer/src/modules/timeline-column/TimelineSelector.tsx deleted file mode 100644 index cc2b6d7731..0000000000 --- a/apps/renderer/src/modules/timeline-column/TimelineSelector.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { DividerVertical } from "@follow/components/ui/divider/Divider.js" -import { useTriangleMenu } from "@follow/hooks" -import { getNodeXInScroller, isNodeVisibleInScroller } from "@follow/utils/dom" -import { clsx } from "@follow/utils/utils" -import { Fragment, useEffect, useRef, useState } from "react" - -import { useTimelineColumnShow } from "~/atoms/sidebar" -import { ROOT_CONTAINER_ID } from "~/constants/dom" -import { useRouteParamsSelector } from "~/hooks/biz/useRouteParams" -import { useTimelineList } from "~/hooks/biz/useTimelineList" - -import styles from "./index.module.css" -import { QuickSelectorPanel } from "./QuickSelectorPanel" -import { TimelineSwitchButton } from "./TimelineSwitchButton" - -export const TimelineSelector = ({ timelineId }: { timelineId: string | undefined }) => { - const timelineList = useTimelineList() - const scrollRef = useRef(null) - useEffect(() => { - const $scroll = scrollRef.current - if (!$scroll) { - return - } - const handler = () => { - if (!timelineId) return - const targetElement = [...$scroll.children] - .filter(($el) => $el.tagName === "BUTTON") - .find(($el, index) => index === timelineList.all.indexOf(timelineId)) - if (!targetElement) { - return - } - - const isInCurrentView = isNodeVisibleInScroller(targetElement, $scroll) - if (!targetElement || isInCurrentView) { - return - } - const targetX = getNodeXInScroller(targetElement, $scroll) - 12 - - $scroll.scrollTo({ - left: targetX, - behavior: "smooth", - }) - } - handler() - }, [timelineId]) - - const [rootContainer, setRootContainer] = useState(null) - - const containerRef = useRef(null) - - useEffect(() => { - const $root = document.querySelector(`#${ROOT_CONTAINER_ID}`) - if (!$root) return - - setRootContainer($root as HTMLElement) - }, []) - - const activeTimelineId = useRouteParamsSelector((s) => s.timelineId) - - const [panelRef, setPanelRef] = useState(null) - const triggerRef = useRef(null) - const shouldOpen = useTriangleMenu(triggerRef, panelRef!, 0) - - const timelineColumnShow = useTimelineColumnShow() - - return ( - -
-
{ - e.preventDefault() - e.currentTarget.scrollLeft += e.deltaY - }} - > - {timelineList.views.map((timelineId) => ( - - ))} - - {timelineList.inboxes.length > 0 && } - {timelineList.inboxes.map((timelineId) => ( - - ))} - {timelineList.lists.length > 0 && } - {timelineList.lists.map((timelineId) => ( - - ))} -
- -
-
- - {timelineColumnShow && ( - - )} - - ) -} diff --git a/apps/renderer/src/modules/timeline-column/TimelineSwitchButton.tsx b/apps/renderer/src/modules/timeline-column/TimelineSwitchButton.tsx index 33c9e8c8c8..b00d3b4812 100644 --- a/apps/renderer/src/modules/timeline-column/TimelineSwitchButton.tsx +++ b/apps/renderer/src/modules/timeline-column/TimelineSwitchButton.tsx @@ -2,31 +2,17 @@ import { useDroppable } from "@dnd-kit/core" import { ActionButton } from "@follow/components/ui/button/index.js" import type { FeedViewType } from "@follow/constants" import { views } from "@follow/constants" -import { nextFrame } from "@follow/utils/dom" import { cn } from "@follow/utils/utils" import type { FC } from "react" import { startTransition, useCallback } from "react" import { useTranslation } from "react-i18next" -import { useShowContextMenu } from "~/atoms/context-menu" -import { getMainContainerElement } from "~/atoms/dom" import { useUISettingKey } from "~/atoms/settings/ui" -import { - ROUTE_TIMELINE_OF_INBOX, - ROUTE_TIMELINE_OF_LIST, - ROUTE_TIMELINE_OF_VIEW, -} from "~/constants" -import { useListActions } from "~/hooks/biz/useFeedActions" +import { ROUTE_TIMELINE_OF_VIEW } from "~/constants" import { useNavigateEntry } from "~/hooks/biz/useNavigateEntry" import { useRouteParamsSelector } from "~/hooks/biz/useRouteParams" -import { useContextMenu } from "~/hooks/common" -import { useInboxById } from "~/store/inbox" -import { useListById } from "~/store/list" -import { subscriptionActions } from "~/store/subscription" -import { useFeedUnreadStore } from "~/store/unread" import { useUnreadByView } from "~/store/unread/hooks" -import { FeedIcon } from "../feed/feed-icon" import { resetSelectedFeedIds } from "./atom" export function TimelineSwitchButton({ timelineId }: { timelineId: string }) { @@ -45,12 +31,6 @@ export function TimelineSwitchButton({ timelineId }: { timelineId: string }) { if (timelineId.startsWith(ROUTE_TIMELINE_OF_VIEW)) { const id = Number.parseInt(timelineId.slice(ROUTE_TIMELINE_OF_VIEW.length), 10) as FeedViewType return - } else if (timelineId.startsWith(ROUTE_TIMELINE_OF_LIST)) { - const id = timelineId.slice(ROUTE_TIMELINE_OF_LIST.length) - return - } else if (timelineId.startsWith(ROUTE_TIMELINE_OF_INBOX)) { - const id = timelineId.slice(ROUTE_TIMELINE_OF_INBOX.length) - return } } @@ -80,11 +60,10 @@ const ViewSwitchButton: FC<{ shortcut={`${view + 1}`} className={cn( isActive && item.className, - "flex h-11 shrink-0 flex-col items-center gap-1 text-[1.375rem]", + "flex h-11 w-9 shrink-0 flex-col items-center gap-1 text-[1.375rem]", ELECTRON ? "hover:!bg-theme-item-hover" : "", isOver && "border-theme-accent-400 bg-theme-accent-400/60", )} - size="lg" onClick={(e) => { startTransition(() => { setActive() @@ -108,117 +87,3 @@ const ViewSwitchButton: FC<{ ) } - -const ListSwitchButton: FC<{ - listId: string - isActive: boolean - setActive: () => void -}> = ({ listId, isActive, setActive }) => { - const list = useListById(listId) - const listUnread = useFeedUnreadStore((state) => state.data[listId] || 0) - - const handleNavigate = useCallback( - (e: React.MouseEvent) => { - e.stopPropagation() - - setActive() - subscriptionActions.markReadByFeedIds({ - listId, - }) - // focus to main container in order to let keyboard can navigate entry items by arrow keys - nextFrame(() => { - getMainContainerElement()?.focus() - }) - }, - [listId, setActive], - ) - - const items = useListActions({ listId }) - const showContextMenu = useShowContextMenu() - const contextMenuProps = useContextMenu({ - onContextMenu: async (e) => { - await showContextMenu(items, e) - }, - }) - - if (!list) return null - - return ( - - - {!!listUnread && ( -
- -
- )} - {!listUnread && ( - - {list.title} - - )} -
- ) -} - -const InboxSwitchButton: FC<{ - inboxId: string - isActive: boolean - setActive: () => void -}> = ({ inboxId, isActive, setActive }) => { - const inbox = useInboxById(inboxId) - const inboxUnread = useFeedUnreadStore((state) => state.data[inboxId] || 0) - const showSidebarUnreadCount = useUISettingKey("sidebarShowUnreadCount") - - const handleNavigate = useCallback( - (e: React.MouseEvent) => { - e.stopPropagation() - - setActive() - // focus to main container in order to let keyboard can navigate entry items by arrow keys - nextFrame(() => { - getMainContainerElement()?.focus() - }) - }, - [setActive], - ) - - if (!inbox) return null - - return ( - - - {showSidebarUnreadCount ? ( -
- {inboxUnread > 99 ? 99+ : inboxUnread} -
- ) : ( - - )} -
- ) -} diff --git a/apps/renderer/src/modules/timeline-column/UnreadNumber.tsx b/apps/renderer/src/modules/timeline-column/UnreadNumber.tsx index b904628f80..b6e6a969f7 100644 --- a/apps/renderer/src/modules/timeline-column/UnreadNumber.tsx +++ b/apps/renderer/src/modules/timeline-column/UnreadNumber.tsx @@ -2,15 +2,7 @@ import { cn } from "@follow/utils/utils" import { useUISettingKey } from "~/atoms/settings/ui" -export const UnreadNumber = ({ - unread, - className, - isList, -}: { - unread: number - className?: string - isList?: boolean -}) => { +export const UnreadNumber = ({ unread, className }: { unread: number; className?: string }) => { const showUnreadCount = useUISettingKey("sidebarShowUnreadCount") if (!unread) return null @@ -21,7 +13,7 @@ export const UnreadNumber = ({ className, )} > - {isList || !showUnreadCount ? : unread} + {!showUnreadCount ? : unread}
) } diff --git a/apps/renderer/src/modules/timeline-column/index.tsx b/apps/renderer/src/modules/timeline-column/index.tsx index 8698c85be3..521cd4d0db 100644 --- a/apps/renderer/src/modules/timeline-column/index.tsx +++ b/apps/renderer/src/modules/timeline-column/index.tsx @@ -26,7 +26,7 @@ import { getSelectedFeedIds, resetSelectedFeedIds, setSelectedFeedIds } from "./ import { useShouldFreeUpSpace } from "./hook" import { TimelineColumnHeader } from "./TimelineColumnHeader" import TimelineList from "./TimelineList" -import { TimelineSelector } from "./TimelineSelector" +import { TimelineSwitchButton } from "./TimelineSwitchButton" const lethargy = new Lethargy() @@ -66,7 +66,7 @@ export function FeedColumn({ children, className }: PropsWithChildren<{ classNam (args: string | ((prev: string | undefined, index: number) => string)) => { let nextActive if (typeof args === "function") { - const index = timelineId ? timelineList.all.indexOf(timelineId) : 0 + const index = timelineId ? timelineList.indexOf(timelineId) : 0 nextActive = args(timelineId, index) } else { nextActive = args @@ -75,7 +75,7 @@ export function FeedColumn({ children, className }: PropsWithChildren<{ classNam navigateBackHome(nextActive) resetSelectedFeedIds() }, - [navigateBackHome, timelineId, timelineList.all], + [navigateBackHome, timelineId, timelineList], ) useWheel( @@ -84,7 +84,7 @@ export function FeedColumn({ children, className }: PropsWithChildren<{ classNam const s = lethargy.check(event) if (s) { if (!wait && Math.abs(dex) > 20) { - setActive((_, i) => timelineList.all[clamp(i + dx, 0, timelineList.all.length - 1)]!) + setActive((_, i) => timelineList[clamp(i + dx, 0, timelineList.length - 1)]!) return true } else { return @@ -108,13 +108,13 @@ export function FeedColumn({ children, className }: PropsWithChildren<{ classNam if (isHotkeyPressed("Left")) { setActive((_, i) => { if (i === 0) { - return timelineList.all.at(-1)! + return timelineList.at(-1)! } else { - return timelineList.all[i - 1]! + return timelineList[i - 1]! } }) } else { - setActive((_, i) => timelineList.all[(i + 1) % timelineList.all.length]!) + setActive((_, i) => timelineList[(i + 1) % timelineList.length]!) } }, { scopes: HotKeyScopeMap.Home }, @@ -152,7 +152,13 @@ export function FeedColumn({ children, className }: PropsWithChildren<{ classNam )} - +
+
+ {timelineList.map((timelineId) => ( + + ))} +
+
- {timelineList.all.map((timelineId) => ( + {timelineList.map((timelineId) => (
@@ -187,7 +193,7 @@ const SwipeWrapper: FC<{ }> = memo(({ children, active }) => { const reduceMotion = useReduceMotion() const timelineList = useTimelineList() - const index = timelineList.all.indexOf(active) + const index = timelineList.indexOf(active) const feedColumnWidth = useUISettingKey("feedColWidth") const containerRef = useRef(null) diff --git a/apps/renderer/src/queries/entries.ts b/apps/renderer/src/queries/entries.ts index 11d7bc03cb..8c0a3be2c8 100644 --- a/apps/renderer/src/queries/entries.ts +++ b/apps/renderer/src/queries/entries.ts @@ -14,7 +14,6 @@ export const entries = { view, read, limit, - isArchived, }: { feedId?: number | string inboxId?: number | string @@ -22,10 +21,9 @@ export const entries = { view?: number read?: boolean limit?: number - isArchived?: boolean }) => defineQuery( - ["entries", inboxId || listId || feedId, view, read, limit, isArchived], + ["entries", inboxId || listId || feedId, view, read, limit], async ({ pageParam }) => entryActions.fetchEntries({ feedId, @@ -35,7 +33,6 @@ export const entries = { read, limit, pageParam: pageParam as string, - isArchived, }), { rootKey: ["entries", inboxId || listId || feedId], @@ -133,38 +130,30 @@ export const useEntries = ({ listId, view, read, - isArchived, }: { feedId?: number | string inboxId?: number | string listId?: number | string view?: number read?: boolean - isArchived?: boolean }) => { const fetchUnread = read === false const feedUnreadDirty = useFeedUnreadIsDirty((feedId as string) || "") - return useAuthInfiniteQuery( - entries.entries({ feedId, inboxId, listId, view, read, isArchived }), - { - enabled: feedId !== undefined || inboxId !== undefined || listId !== undefined, - getNextPageParam: (lastPage) => - listId - ? lastPage.data?.at(-1)?.entries.insertedAt - : lastPage.data?.at(-1)?.entries.publishedAt, - initialPageParam: undefined, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - // DON'T refetch when the router is pop to previous page - refetchOnMount: fetchUnread && feedUnreadDirty && !history.isPop ? "always" : false, + return useAuthInfiniteQuery(entries.entries({ feedId, inboxId, listId, view, read }), { + enabled: feedId !== undefined || inboxId !== undefined || listId !== undefined, + getNextPageParam: (lastPage) => lastPage.data?.at(-1)?.entries.publishedAt, + initialPageParam: undefined, + refetchOnWindowFocus: false, + refetchOnReconnect: false, + // DON'T refetch when the router is pop to previous page + refetchOnMount: fetchUnread && feedUnreadDirty && !history.isPop ? "always" : false, - staleTime: - // Force refetch unread entries when feed is dirty - // HACK: disable refetch when the router is pop to previous page - history.isPop ? Infinity : fetchUnread && feedUnreadDirty ? 0 : defaultStaleTime, - }, - ) + staleTime: + // Force refetch unread entries when feed is dirty + // HACK: disable refetch when the router is pop to previous page + history.isPop ? Infinity : fetchUnread && feedUnreadDirty ? 0 : defaultStaleTime, + }) } export const useEntriesPreview = ({ id }: { id?: string }) => diff --git a/apps/renderer/src/store/entry/store.ts b/apps/renderer/src/store/entry/store.ts index dd7e347495..49fde264bc 100644 --- a/apps/renderer/src/store/entry/store.ts +++ b/apps/renderer/src/store/entry/store.ts @@ -113,7 +113,6 @@ class EntryActions { read, limit, pageParam, - isArchived, }: { feedId?: number | string inboxId?: number | string @@ -122,7 +121,6 @@ class EntryActions { read?: boolean limit?: number pageParam?: string - isArchived?: boolean }) { if (inboxId) { const data = await apiClient.entries.inbox @@ -147,7 +145,7 @@ class EntryActions { }) if (data.data) { - this.upsertMany(structuredClone(data.data), { isArchived }) + this.upsertMany(structuredClone(data.data)) } return data } @@ -163,7 +161,6 @@ class EntryActions { publishedAfter: pageParam, read, limit, - isArchived, ...params, }, }) @@ -183,7 +180,7 @@ class EntryActions { } } if (data.data) { - this.upsertMany(structuredClone(data.data), { isArchived }) + this.upsertMany(structuredClone(data.data)) } return data } @@ -263,7 +260,7 @@ class EntryActions { }) } - upsertMany(data: CombinedEntryModel[], options?: { isArchived?: boolean }) { + upsertMany(data: CombinedEntryModel[]) { const feeds = [] as FeedOrListRespModel[] const entries = [] as EntryModel[] const inboxes = [] as InboxModel[] @@ -370,7 +367,7 @@ class EntryActions { } } - if (item.settings && draft.flatMapEntries[item.entries.id] && !options?.isArchived) { + if (item.settings && draft.flatMapEntries[item.entries.id]) { draft.flatMapEntries[item.entries.id]!.settings = item.settings } } diff --git a/apps/renderer/src/store/feed/hooks.ts b/apps/renderer/src/store/feed/hooks.ts index c1ea420484..656831b119 100644 --- a/apps/renderer/src/store/feed/hooks.ts +++ b/apps/renderer/src/store/feed/hooks.ts @@ -3,13 +3,7 @@ import type { FeedModel, FeedOrListRespModel, InboxModel, ListModel } from "@fol import { useCallback } from "react" import { useTranslation } from "react-i18next" -import { - FEED_COLLECTION_LIST, - ROUTE_FEED_IN_FOLDER, - ROUTE_FEED_PENDING, - ROUTE_TIMELINE_OF_INBOX, - ROUTE_TIMELINE_OF_LIST, -} from "~/constants" +import { FEED_COLLECTION_LIST, ROUTE_FEED_IN_FOLDER, ROUTE_FEED_PENDING } from "~/constants" import { useRouteParams } from "~/hooks/biz/useRouteParams" import { useInboxStore } from "../inbox" @@ -77,17 +71,10 @@ export const useInboxByIdSelector = ( export const useFeedHeaderTitle = () => { const { t } = useTranslation() - const { feedId: currentFeedId, view, listId, inboxId, timelineId } = useRouteParams() + const { feedId: currentFeedId, view, listId: currentListId } = useRouteParams() - const listTitle = useListByIdSelector(listId, getPreferredTitle) - const inboxTitle = useInboxByIdSelector(inboxId, getPreferredTitle) const feedTitle = useFeedByIdSelector(currentFeedId, getPreferredTitle) - - if (timelineId?.startsWith(ROUTE_TIMELINE_OF_LIST)) { - return listTitle - } else if (timelineId?.startsWith(ROUTE_TIMELINE_OF_INBOX)) { - return inboxTitle - } + const listTitle = useListByIdSelector(currentListId, getPreferredTitle) switch (currentFeedId) { case ROUTE_FEED_PENDING: { @@ -100,7 +87,7 @@ export const useFeedHeaderTitle = () => { if (currentFeedId?.startsWith(ROUTE_FEED_IN_FOLDER)) { return currentFeedId.replace(ROUTE_FEED_IN_FOLDER, "") } - return feedTitle + return feedTitle || listTitle } } } diff --git a/apps/renderer/src/store/subscription/hooks.ts b/apps/renderer/src/store/subscription/hooks.ts index 32a3bd9633..9ae21bd468 100644 --- a/apps/renderer/src/store/subscription/hooks.ts +++ b/apps/renderer/src/store/subscription/hooks.ts @@ -58,7 +58,7 @@ export const useCategoriesByView = (view: FeedViewType) => (state) => new Set( subscriptionByViewSelector(view)(state) - .map((subscription) => subscription!.category) + .map((subscription) => subscription?.category) .filter((category) => category !== null && category !== undefined) .filter(Boolean), ), diff --git a/apps/renderer/src/store/subscription/store.ts b/apps/renderer/src/store/subscription/store.ts index c811dc1fbb..d4d0c64128 100644 --- a/apps/renderer/src/store/subscription/store.ts +++ b/apps/renderer/src/store/subscription/store.ts @@ -196,14 +196,13 @@ class SubscriptionActions { state.listIds.add(subscription.listId) } else if (subscription.inboxId) { state.inboxIds.add(subscription.inboxId) - } else { - state.feedIdByView[subscription.view].push(subscription.feedId) } state.data[subscription.feedId] = omit(subscription, [ "feeds", "lists", "inboxes", ]) as SubscriptionFlatModel + state.feedIdByView[subscription.view].push(subscription.feedId) }) }) } diff --git a/changelog/0.3.7.md b/changelog/0.3.7.md new file mode 100644 index 0000000000..f635253cab --- /dev/null +++ b/changelog/0.3.7.md @@ -0,0 +1,7 @@ +# What's new in v0.3.7 + +## New Features + +- Revert to the previous list display styles. + ![Timeline selector3](https://github.com/RSSNext/assets/blob/main/timeline-selector3.png?raw=true) +- The entries of the list is now displayed directly in the timeline. diff --git a/package.json b/package.json index 9d0e0a786b..e26c0ca3ac 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "Follow", "type": "module", - "version": "0.3.6", + "version": "0.3.7", "private": true, "packageManager": "pnpm@9.12.3", "description": "Follow everything in one place", @@ -196,5 +196,5 @@ ] }, "productName": "Follow", - "mainHash": "d2184ce595ab3b45f933edb9938b6a6fe53d8c85558d0899b9ffb8f72850cf25" + "mainHash": "ed3532be011d04d18a4148a025b6ae1282bd25aa8260f8aa7e9121f855d11314" } diff --git a/packages/shared/src/hono.ts b/packages/shared/src/hono.ts index 9dfdb0c09b..133c277c87 100644 --- a/packages/shared/src/hono.ts +++ b/packages/shared/src/hono.ts @@ -2984,164 +2984,6 @@ declare const subscriptionsRelations: drizzle_orm.Relations<"subscriptions", { rsshubUsage: drizzle_orm.One<"rsshub_usage", true>; }>; -declare const timeline: drizzle_orm_pg_core.PgTableWithColumns<{ - name: "timeline"; - schema: undefined; - columns: { - userId: drizzle_orm_pg_core.PgColumn<{ - name: "user_id"; - tableName: "timeline"; - dataType: "string"; - columnType: "PgText"; - data: string; - driverParam: string; - notNull: true; - hasDefault: false; - isPrimaryKey: false; - isAutoincrement: false; - hasRuntimeDefault: false; - enumValues: [string, ...string[]]; - baseColumn: never; - identity: undefined; - generated: undefined; - }, {}, {}>; - feedId: drizzle_orm_pg_core.PgColumn<{ - name: "feedId"; - tableName: "timeline"; - dataType: "string"; - columnType: "PgText"; - data: string; - driverParam: string; - notNull: true; - hasDefault: false; - isPrimaryKey: false; - isAutoincrement: false; - hasRuntimeDefault: false; - enumValues: [string, ...string[]]; - baseColumn: never; - identity: undefined; - generated: undefined; - }, {}, {}>; - entryId: drizzle_orm_pg_core.PgColumn<{ - name: "entry_id"; - tableName: "timeline"; - dataType: "string"; - columnType: "PgText"; - data: string; - driverParam: string; - notNull: true; - hasDefault: false; - isPrimaryKey: false; - isAutoincrement: false; - hasRuntimeDefault: false; - enumValues: [string, ...string[]]; - baseColumn: never; - identity: undefined; - generated: undefined; - }, {}, {}>; - publishedAt: drizzle_orm_pg_core.PgColumn<{ - name: "published_at"; - tableName: "timeline"; - dataType: "date"; - columnType: "PgTimestamp"; - data: Date; - driverParam: string; - notNull: true; - hasDefault: false; - isPrimaryKey: false; - isAutoincrement: false; - hasRuntimeDefault: false; - enumValues: undefined; - baseColumn: never; - identity: undefined; - generated: undefined; - }, {}, {}>; - insertedAt: drizzle_orm_pg_core.PgColumn<{ - name: "inserted_at"; - tableName: "timeline"; - dataType: "date"; - columnType: "PgTimestamp"; - data: Date; - driverParam: string; - notNull: true; - hasDefault: false; - isPrimaryKey: false; - isAutoincrement: false; - hasRuntimeDefault: false; - enumValues: undefined; - baseColumn: never; - identity: undefined; - generated: undefined; - }, {}, {}>; - view: drizzle_orm_pg_core.PgColumn<{ - name: "view"; - tableName: "timeline"; - dataType: "number"; - columnType: "PgSmallInt"; - data: number; - driverParam: string | number; - notNull: true; - hasDefault: false; - isPrimaryKey: false; - isAutoincrement: false; - hasRuntimeDefault: false; - enumValues: undefined; - baseColumn: never; - identity: undefined; - generated: undefined; - }, {}, {}>; - read: drizzle_orm_pg_core.PgColumn<{ - name: "read"; - tableName: "timeline"; - dataType: "boolean"; - columnType: "PgBoolean"; - data: boolean; - driverParam: boolean; - notNull: false; - hasDefault: false; - isPrimaryKey: false; - isAutoincrement: false; - hasRuntimeDefault: false; - enumValues: undefined; - baseColumn: never; - identity: undefined; - generated: undefined; - }, {}, {}>; - }; - dialect: "pg"; -}>; -declare const timelineOpenAPISchema: zod.ZodObject<{ - userId: zod.ZodString; - feedId: zod.ZodString; - entryId: zod.ZodString; - publishedAt: zod.ZodString; - insertedAt: zod.ZodString; - view: zod.ZodNumber; - read: zod.ZodNullable; -}, zod.UnknownKeysParam, zod.ZodTypeAny, { - userId: string; - view: number; - feedId: string; - insertedAt: string; - publishedAt: string; - entryId: string; - read: boolean | null; -}, { - userId: string; - view: number; - feedId: string; - insertedAt: string; - publishedAt: string; - entryId: string; - read: boolean | null; -}>; -declare const timelineRelations: drizzle_orm.Relations<"timeline", { - entries: drizzle_orm.One<"entries", true>; - feeds: drizzle_orm.One<"feeds", true>; - collections: drizzle_orm.One<"collections", true>; - subscriptions: drizzle_orm.One<"subscriptions", true>; -}>; - declare const inboxesEntries: drizzle_orm_pg_core.PgTableWithColumns<{ name: "inboxes_entries"; schema: undefined; @@ -4561,6 +4403,7 @@ declare const listsRelations: drizzle_orm.Relations<"lists", { owner: drizzle_orm.One<"user", true>; listsSubscriptions: drizzle_orm.Many<"lists_subscriptions">; }>; +type ListModel = InferInsertModel; declare const listsSubscriptions: drizzle_orm_pg_core.PgTableWithColumns<{ name: "lists_subscriptions"; @@ -4634,23 +4477,6 @@ declare const listsSubscriptions: drizzle_orm_pg_core.PgTableWithColumns<{ identity: undefined; generated: undefined; }, {}, {}>; - lastViewedAt: drizzle_orm_pg_core.PgColumn<{ - name: "last_viewed_at"; - tableName: "lists_subscriptions"; - dataType: "date"; - columnType: "PgTimestamp"; - data: Date; - driverParam: string; - notNull: false; - hasDefault: false; - isPrimaryKey: false; - isAutoincrement: false; - hasRuntimeDefault: false; - enumValues: undefined; - baseColumn: never; - identity: undefined; - generated: undefined; - }, {}, {}>; createdAt: drizzle_orm_pg_core.PgColumn<{ name: "created_at"; tableName: "lists_subscriptions"; @@ -4693,7 +4519,6 @@ declare const listsSubscriptionsOpenAPISchema: zod.ZodObject<{ listId: zod.ZodString; view: zod.ZodNumber; title: zod.ZodNullable; - lastViewedAt: zod.ZodNullable; createdAt: zod.ZodString; isPrivate: zod.ZodBoolean; }, zod.UnknownKeysParam, zod.ZodTypeAny, { @@ -4703,7 +4528,6 @@ declare const listsSubscriptionsOpenAPISchema: zod.ZodObject<{ view: number; isPrivate: boolean; listId: string; - lastViewedAt: string | null; }, { createdAt: string; userId: string; @@ -4711,109 +4535,12 @@ declare const listsSubscriptionsOpenAPISchema: zod.ZodObject<{ view: number; isPrivate: boolean; listId: string; - lastViewedAt: string | null; }>; declare const listsSubscriptionsRelations: drizzle_orm.Relations<"lists_subscriptions", { users: drizzle_orm.One<"user", true>; lists: drizzle_orm.One<"lists", true>; }>; -declare const listsTimeline: drizzle_orm_pg_core.PgTableWithColumns<{ - name: "lists_timeline"; - schema: undefined; - columns: { - listId: drizzle_orm_pg_core.PgColumn<{ - name: "list_id"; - tableName: "lists_timeline"; - dataType: "string"; - columnType: "PgText"; - data: string; - driverParam: string; - notNull: true; - hasDefault: false; - isPrimaryKey: false; - isAutoincrement: false; - hasRuntimeDefault: false; - enumValues: [string, ...string[]]; - baseColumn: never; - identity: undefined; - generated: undefined; - }, {}, {}>; - feedId: drizzle_orm_pg_core.PgColumn<{ - name: "feedId"; - tableName: "lists_timeline"; - dataType: "string"; - columnType: "PgText"; - data: string; - driverParam: string; - notNull: true; - hasDefault: false; - isPrimaryKey: false; - isAutoincrement: false; - hasRuntimeDefault: false; - enumValues: [string, ...string[]]; - baseColumn: never; - identity: undefined; - generated: undefined; - }, {}, {}>; - entryId: drizzle_orm_pg_core.PgColumn<{ - name: "entry_id"; - tableName: "lists_timeline"; - dataType: "string"; - columnType: "PgText"; - data: string; - driverParam: string; - notNull: true; - hasDefault: false; - isPrimaryKey: false; - isAutoincrement: false; - hasRuntimeDefault: false; - enumValues: [string, ...string[]]; - baseColumn: never; - identity: undefined; - generated: undefined; - }, {}, {}>; - insertedAt: drizzle_orm_pg_core.PgColumn<{ - name: "inserted_at"; - tableName: "lists_timeline"; - dataType: "date"; - columnType: "PgTimestamp"; - data: Date; - driverParam: string; - notNull: true; - hasDefault: false; - isPrimaryKey: false; - isAutoincrement: false; - hasRuntimeDefault: false; - enumValues: undefined; - baseColumn: never; - identity: undefined; - generated: undefined; - }, {}, {}>; - }; - dialect: "pg"; -}>; -declare const listsTimelineOpenAPISchema: zod.ZodObject<{ - listId: zod.ZodString; - feedId: zod.ZodString; - entryId: zod.ZodString; - insertedAt: zod.ZodString; -}, zod.UnknownKeysParam, zod.ZodTypeAny, { - feedId: string; - insertedAt: string; - entryId: string; - listId: string; -}, { - feedId: string; - insertedAt: string; - entryId: string; - listId: string; -}>; -declare const listsTimelineRelations: drizzle_orm.Relations<"lists_timeline", { - entries: drizzle_orm.One<"entries", true>; - feeds: drizzle_orm.One<"feeds", true>; -}>; - declare const messaging: drizzle_orm_pg_core.PgTableWithColumns<{ name: "messaging"; schema: undefined; @@ -5240,6 +4967,210 @@ declare const settings: drizzle_orm_pg_core.PgTableWithColumns<{ dialect: "pg"; }>; +declare const timeline: drizzle_orm_pg_core.PgTableWithColumns<{ + name: "timeline"; + schema: undefined; + columns: { + userId: drizzle_orm_pg_core.PgColumn<{ + name: "user_id"; + tableName: "timeline"; + dataType: "string"; + columnType: "PgText"; + data: string; + driverParam: string; + notNull: true; + hasDefault: false; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: [string, ...string[]]; + baseColumn: never; + identity: undefined; + generated: undefined; + }, {}, {}>; + feedId: drizzle_orm_pg_core.PgColumn<{ + name: "feedId"; + tableName: "timeline"; + dataType: "string"; + columnType: "PgText"; + data: string; + driverParam: string; + notNull: true; + hasDefault: false; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: [string, ...string[]]; + baseColumn: never; + identity: undefined; + generated: undefined; + }, {}, {}>; + entryId: drizzle_orm_pg_core.PgColumn<{ + name: "entry_id"; + tableName: "timeline"; + dataType: "string"; + columnType: "PgText"; + data: string; + driverParam: string; + notNull: true; + hasDefault: false; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: [string, ...string[]]; + baseColumn: never; + identity: undefined; + generated: undefined; + }, {}, {}>; + publishedAt: drizzle_orm_pg_core.PgColumn<{ + name: "published_at"; + tableName: "timeline"; + dataType: "date"; + columnType: "PgTimestamp"; + data: Date; + driverParam: string; + notNull: true; + hasDefault: false; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: undefined; + baseColumn: never; + identity: undefined; + generated: undefined; + }, {}, {}>; + insertedAt: drizzle_orm_pg_core.PgColumn<{ + name: "inserted_at"; + tableName: "timeline"; + dataType: "date"; + columnType: "PgTimestamp"; + data: Date; + driverParam: string; + notNull: true; + hasDefault: false; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: undefined; + baseColumn: never; + identity: undefined; + generated: undefined; + }, {}, {}>; + view: drizzle_orm_pg_core.PgColumn<{ + name: "view"; + tableName: "timeline"; + dataType: "number"; + columnType: "PgSmallInt"; + data: number; + driverParam: string | number; + notNull: true; + hasDefault: false; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: undefined; + baseColumn: never; + identity: undefined; + generated: undefined; + }, {}, {}>; + read: drizzle_orm_pg_core.PgColumn<{ + name: "read"; + tableName: "timeline"; + dataType: "boolean"; + columnType: "PgBoolean"; + data: boolean; + driverParam: boolean; + notNull: false; + hasDefault: false; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: undefined; + baseColumn: never; + identity: undefined; + generated: undefined; + }, {}, {}>; + from: drizzle_orm_pg_core.PgColumn<{ + name: "from"; + tableName: "timeline"; + dataType: "array"; + columnType: "PgArray"; + data: string[]; + driverParam: string | string[]; + notNull: false; + hasDefault: false; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: [string, ...string[]]; + baseColumn: drizzle_orm.Column<{ + name: "from"; + tableName: "timeline"; + dataType: "string"; + columnType: "PgText"; + data: string; + driverParam: string; + notNull: false; + hasDefault: false; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: [string, ...string[]]; + baseColumn: never; + identity: undefined; + generated: undefined; + }, {}, {}>; + identity: undefined; + generated: undefined; + }, {}, { + baseBuilder: drizzle_orm_pg_core.PgColumnBuilder<{ + name: "from"; + dataType: "string"; + columnType: "PgText"; + data: string; + enumValues: [string, ...string[]]; + driverParam: string; + }, {}, {}, drizzle_orm.ColumnBuilderExtraConfig>; + size: undefined; + }>; + }; + dialect: "pg"; +}>; +declare const timelineOpenAPISchema: zod.ZodObject<{ + userId: zod.ZodString; + feedId: zod.ZodString; + entryId: zod.ZodString; + publishedAt: zod.ZodString; + insertedAt: zod.ZodString; + view: zod.ZodNumber; + read: zod.ZodNullable; + from: zod.ZodNullable>; +}, zod.UnknownKeysParam, zod.ZodTypeAny, { + userId: string; + view: number; + from: string[] | null; + feedId: string; + insertedAt: string; + publishedAt: string; + entryId: string; + read: boolean | null; +}, { + userId: string; + view: number; + from: string[] | null; + feedId: string; + insertedAt: string; + publishedAt: string; + entryId: string; + read: boolean | null; +}>; +declare const timelineRelations: drizzle_orm.Relations<"timeline", { + entries: drizzle_orm.One<"entries", true>; + feeds: drizzle_orm.One<"feeds", true>; + collections: drizzle_orm.One<"collections", true>; + subscriptions: drizzle_orm.One<"subscriptions", true>; +}>; + declare const user: drizzle_orm_pg_core.PgTableWithColumns<{ name: "user"; schema: undefined; @@ -14313,7 +14244,6 @@ declare const _routes: hono_hono_base.HonoBase, "/">; type AppType = typeof _routes; -export { type ActionsModel, type AirdropActivity, type AppType, type AttachmentsModel, type AuthSession, type AuthUser, CommonEntryFields, type ConditionItem, type DetailModel, type EntriesModel, type EntryReadHistoriesModel, type ExtraModel, type FeedModel, type MediaModel, type MessagingData, MessagingType, type SettingsModel, type UrlReadsModel, account, achievements, achievementsOpenAPISchema, actions, actionsItemOpenAPISchema, actionsOpenAPISchema, actionsRelations, activityEnum, airdrops, airdropsOpenAPISchema, attachmentsZodSchema, authPlugins, boosts, collections, collectionsOpenAPISchema, collectionsRelations, detailModelSchema, entries, entriesOpenAPISchema, entriesRelations, entryReadHistories, entryReadHistoriesOpenAPISchema, entryReadHistoriesRelations, extraZodSchema, feedPowerTokens, feedPowerTokensOpenAPISchema, feedPowerTokensRelations, feeds, feedsOpenAPISchema, feedsRelations, inboxHandleSchema, inboxes, inboxesEntries, inboxesEntriesInsertOpenAPISchema, type inboxesEntriesModel, inboxesEntriesOpenAPISchema, inboxesEntriesRelations, inboxesOpenAPISchema, inboxesRelations, invitations, invitationsOpenAPISchema, invitationsRelations, languageSchema, levels, levelsOpenAPISchema, levelsRelations, lists, listsOpenAPISchema, listsRelations, listsSubscriptions, listsSubscriptionsOpenAPISchema, listsSubscriptionsRelations, listsTimeline, listsTimelineOpenAPISchema, listsTimelineRelations, lower, mediaZodSchema, messaging, messagingOpenAPISchema, messagingRelations, rsshub, rsshubOpenAPISchema, rsshubPurchase, rsshubUsage, rsshubUsageOpenAPISchema, rsshubUsageRelations, session, settings, subscriptions, subscriptionsOpenAPISchema, subscriptionsRelations, timeline, timelineOpenAPISchema, timelineRelations, transactionType, transactions, transactionsOpenAPISchema, transactionsRelations, twoFactor, urlReads, urlReadsOpenAPISchema, user, users, usersOpenApiSchema, usersRelations, verification, wallets, walletsOpenAPISchema, walletsRelations }; +export { type ActionsModel, type AirdropActivity, type AppType, type AttachmentsModel, type AuthSession, type AuthUser, CommonEntryFields, type ConditionItem, type DetailModel, type EntriesModel, type EntryReadHistoriesModel, type ExtraModel, type FeedModel, type ListModel, type MediaModel, type MessagingData, MessagingType, type SettingsModel, type UrlReadsModel, account, achievements, achievementsOpenAPISchema, actions, actionsItemOpenAPISchema, actionsOpenAPISchema, actionsRelations, activityEnum, airdrops, airdropsOpenAPISchema, attachmentsZodSchema, authPlugins, boosts, collections, collectionsOpenAPISchema, collectionsRelations, detailModelSchema, entries, entriesOpenAPISchema, entriesRelations, entryReadHistories, entryReadHistoriesOpenAPISchema, entryReadHistoriesRelations, extraZodSchema, feedPowerTokens, feedPowerTokensOpenAPISchema, feedPowerTokensRelations, feeds, feedsOpenAPISchema, feedsRelations, inboxHandleSchema, inboxes, inboxesEntries, inboxesEntriesInsertOpenAPISchema, type inboxesEntriesModel, inboxesEntriesOpenAPISchema, inboxesEntriesRelations, inboxesOpenAPISchema, inboxesRelations, invitations, invitationsOpenAPISchema, invitationsRelations, languageSchema, levels, levelsOpenAPISchema, levelsRelations, lists, listsOpenAPISchema, listsRelations, listsSubscriptions, listsSubscriptionsOpenAPISchema, listsSubscriptionsRelations, lower, mediaZodSchema, messaging, messagingOpenAPISchema, messagingRelations, rsshub, rsshubOpenAPISchema, rsshubPurchase, rsshubUsage, rsshubUsageOpenAPISchema, rsshubUsageRelations, session, settings, subscriptions, subscriptionsOpenAPISchema, subscriptionsRelations, timeline, timelineOpenAPISchema, timelineRelations, transactionType, transactions, transactionsOpenAPISchema, transactionsRelations, twoFactor, urlReads, urlReadsOpenAPISchema, user, users, usersOpenApiSchema, usersRelations, verification, wallets, walletsOpenAPISchema, walletsRelations }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 76f526cc0a..82a0ab7f85 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4004,8 +4004,8 @@ packages: '@radix-ui/react-avatar@1.1.3': resolution: {integrity: sha512-Paen00T4P8L8gd9bNsRMw7Cbaz85oxiv+hzomsRZgFm2byltPFDtfcoqlWJ8GyZlIBWgLssJlzLCnKU0G0302g==} peerDependencies: - '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -4057,9 +4057,9 @@ packages: '@radix-ui/react-context-menu@2.2.6': resolution: {integrity: sha512-aUP99QZ3VU84NPsHeaFt4cQUNgJqFsLLOt/RbbWXszZ6MP0DpDyjkFZORr4RpAEx3sUBk+Kc8h13yGtC5Qw8dg==} peerDependencies: - '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 - react: 18.3.1 + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -4070,7 +4070,7 @@ packages: '@radix-ui/react-context@1.1.1': resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==} peerDependencies: - '@types/react': npm:@types/react@^18.3.12 + '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -4101,8 +4101,8 @@ packages: '@radix-ui/react-dismissable-layer@1.1.5': resolution: {integrity: sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==} peerDependencies: - '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -4127,7 +4127,7 @@ packages: '@radix-ui/react-focus-guards@1.1.1': resolution: {integrity: sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==} peerDependencies: - '@types/react': npm:@types/react@^18.3.12 + '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -4184,8 +4184,8 @@ packages: '@radix-ui/react-menu@2.1.6': resolution: {integrity: sha512-tBBb5CXDJW3t2mo9WlO7r6GTmWV0F0uzHZVFmlRmYpiSK1CDU5IKojP1pm7oknpBOrFZx/YgBRW9oorPO2S/Lg==} peerDependencies: - '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -4223,8 +4223,8 @@ packages: '@radix-ui/react-popper@1.2.2': resolution: {integrity: sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==} peerDependencies: - '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -4236,8 +4236,8 @@ packages: '@radix-ui/react-portal@1.1.4': resolution: {integrity: sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==} peerDependencies: - '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -4288,8 +4288,8 @@ packages: '@radix-ui/react-roving-focus@1.1.2': resolution: {integrity: sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw==} peerDependencies: - '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -4406,7 +4406,7 @@ packages: '@radix-ui/react-use-callback-ref@1.1.0': resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==} peerDependencies: - '@types/react': npm:@types/react@^18.3.12 + '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -4415,7 +4415,7 @@ packages: '@radix-ui/react-use-controllable-state@1.1.0': resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==} peerDependencies: - '@types/react': npm:@types/react@^18.3.12 + '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -4469,8 +4469,8 @@ packages: '@radix-ui/react-visually-hidden@1.1.2': resolution: {integrity: sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q==} peerDependencies: - '@types/react': 18.3.12 - '@types/react-dom': 18.3.1 + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -4552,7 +4552,7 @@ packages: '@react-native-menu/menu@1.2.2': resolution: {integrity: sha512-Uk65PAhwNkCVBAqJu5t2H9biV+m0JLwJc7m3v2X2A/W8SFJmUqYabBsLH4fOWKI3a7kkR9QDT6HruliIKSfM8w==} peerDependencies: - react: 18.3.1 + react: '*' react-native: '*' '@react-native-picker/picker@2.11.0': @@ -5263,7 +5263,7 @@ packages: '@tanstack/react-query@5.66.7': resolution: {integrity: sha512-qd3q/tUpF2K1xItfPZddk1k/8pSXnovg41XyCqJgPoyYEirMBtB0sVEVVQ/CsAOngzgWtBPXimVf4q4kM9uO6A==} peerDependencies: - react: 18.3.1 + react: ^18 || ^19 '@tanstack/react-virtual@3.13.0': resolution: {integrity: sha512-CchF0NlLIowiM2GxtsoKBkXA4uqSnY2KvnXo+kyUFD4a4ll6+J0qzoRsUPMwXV/H26lRsxgJIr/YmjYum2oEjg==} @@ -8486,7 +8486,7 @@ packages: resolution: {integrity: sha512-LUnfrddmee1xLOkyG2NN1l9xQbtvMX3fbM1brEGHg0SKSndvjod3FQdhTzZEYAariqW2RSxQR8v1IsheIoLQXg==} peerDependencies: expo: '*' - react-native: '*' + react-native: npm:react-native@0.77.0 expo-module-scripts@4.0.4: resolution: {integrity: sha512-ytFufVi7HTFLxxf8fbtz3DJuegq3489WPk9pz8Yqm3tYfZ+6/yufPYxLvk6N8b4yBW05oNr/Cqp7MWLpm0xPcA==} @@ -12422,7 +12422,7 @@ packages: resolution: {integrity: sha512-sc5qgX4YtM6IxjtosjgQLdsal120MvU+YWs0F2MdgQWijps22AXLDCUoBnZZ8vxVhVyJ2WnnIPrmtVBvVJjSuQ==} engines: {node: '>= 18.0.0'} peerDependencies: - react: 18.3.1 + react: '*' react-native: '*' react-native-gesture-handler: '>=2.0.0' react-native-reanimated: '>=3.0.0' @@ -12536,7 +12536,7 @@ packages: resolution: {integrity: sha512-9PPREdwH3tMR8otUK1voiFw9AFNtC37Del2fZwbCMBKjOyClC+f3Fwp7KqUouP0A5c3zjEc3T8TetBGQGPQE9A==} peerDependencies: react: '*' - react-native: npm:react-native@0.77.0 + react-native: '*' react-native-gesture-handler: '*' react-native-reanimated: '*' @@ -12550,7 +12550,7 @@ packages: resolution: {integrity: sha512-E5N/eK/+HtAVJUAzXpm1cWz8ROheV9jb0TI6h2bM+333U+DWibTTnT2T1122FkCoXLXIYavtm2FR2if+5jH8cA==} peerDependencies: react: '>=16.8.6' - react-native: npm:react-native@0.77.0 + react-native: '>=0.60.0-rc.2' react-native-windows: '>=0.63.0' shaka-player: ^4.7.9 peerDependenciesMeta: @@ -12564,13 +12564,13 @@ packages: peerDependencies: nativewind: '>=4.1.0' react: '>=18.0.0' - react-native: '>=0.76.0' + react-native: npm:react-native@0.77.0 tailwindcss: '>=3.0.0' react-native-volume-manager@2.0.8: resolution: {integrity: sha512-aZM47/mYkdQ4CbXpKYO6Ajiczv7fxbQXZ9c0H8gRuQUaS3OCz/MZABer6o9aDWq0KMNsQ7q7GVFLRPnSSeeMmw==} peerDependencies: - react: 18.3.1 + react: '*' react-native: '*' react-native-web@0.19.13: