@dxos/plugin-magazine 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +105 -0
- package/PLUGIN.mdl +653 -0
- package/dist/lib/neutral/FeedArticle-PEWCFFSP.mjs +90 -0
- package/dist/lib/neutral/FeedArticle-PEWCFFSP.mjs.map +7 -0
- package/dist/lib/neutral/FeedProperties-EIO7IZOA.mjs +97 -0
- package/dist/lib/neutral/FeedProperties-EIO7IZOA.mjs.map +7 -0
- package/dist/lib/neutral/MagazineArticle-DEUTR23F.mjs +325 -0
- package/dist/lib/neutral/MagazineArticle-DEUTR23F.mjs.map +7 -0
- package/dist/lib/neutral/MagazinePlugin.mjs +62 -0
- package/dist/lib/neutral/MagazinePlugin.mjs.map +7 -0
- package/dist/lib/neutral/MagazinePlugin.workerd.mjs +12 -0
- package/dist/lib/neutral/MagazinePlugin.workerd.mjs.map +7 -0
- package/dist/lib/neutral/PostArticle-RERHXPXC.mjs +230 -0
- package/dist/lib/neutral/PostArticle-RERHXPXC.mjs.map +7 -0
- package/dist/lib/neutral/PostCard-VDGN7BBA.mjs +60 -0
- package/dist/lib/neutral/PostCard-VDGN7BBA.mjs.map +7 -0
- package/dist/lib/neutral/app-graph-builder-JDPRBVHL.mjs +121 -0
- package/dist/lib/neutral/app-graph-builder-JDPRBVHL.mjs.map +7 -0
- package/dist/lib/neutral/capabilities/index.mjs +21 -0
- package/dist/lib/neutral/capabilities/index.mjs.map +7 -0
- package/dist/lib/neutral/chunk-3K4AQKBZ.mjs +12 -0
- package/dist/lib/neutral/chunk-3K4AQKBZ.mjs.map +7 -0
- package/dist/lib/neutral/chunk-AFPBWBTK.mjs +591 -0
- package/dist/lib/neutral/chunk-AFPBWBTK.mjs.map +7 -0
- package/dist/lib/neutral/chunk-APQRTYT3.mjs +57 -0
- package/dist/lib/neutral/chunk-APQRTYT3.mjs.map +7 -0
- package/dist/lib/neutral/chunk-GU725ZF4.mjs +11 -0
- package/dist/lib/neutral/chunk-GU725ZF4.mjs.map +7 -0
- package/dist/lib/neutral/chunk-J5LGTIGS.mjs +10 -0
- package/dist/lib/neutral/chunk-J5LGTIGS.mjs.map +7 -0
- package/dist/lib/neutral/chunk-JFPR6OV4.mjs +60 -0
- package/dist/lib/neutral/chunk-JFPR6OV4.mjs.map +7 -0
- package/dist/lib/neutral/chunk-KTYLUOUG.mjs +36 -0
- package/dist/lib/neutral/chunk-KTYLUOUG.mjs.map +7 -0
- package/dist/lib/neutral/chunk-MBPX7RPI.mjs +14 -0
- package/dist/lib/neutral/chunk-MBPX7RPI.mjs.map +7 -0
- package/dist/lib/neutral/chunk-MJ2AY2CK.mjs +8 -0
- package/dist/lib/neutral/chunk-MJ2AY2CK.mjs.map +7 -0
- package/dist/lib/neutral/chunk-OYCDRSWB.mjs +624 -0
- package/dist/lib/neutral/chunk-OYCDRSWB.mjs.map +7 -0
- package/dist/lib/neutral/chunk-SAWKLGJU.mjs +28 -0
- package/dist/lib/neutral/chunk-SAWKLGJU.mjs.map +7 -0
- package/dist/lib/neutral/chunk-XCNCK47N.mjs +15 -0
- package/dist/lib/neutral/chunk-XCNCK47N.mjs.map +7 -0
- package/dist/lib/neutral/clear-magazine-KNY4OWY4.mjs +59 -0
- package/dist/lib/neutral/clear-magazine-KNY4OWY4.mjs.map +7 -0
- package/dist/lib/neutral/components/index.mjs +338 -0
- package/dist/lib/neutral/components/index.mjs.map +7 -0
- package/dist/lib/neutral/containers/index.mjs +17 -0
- package/dist/lib/neutral/containers/index.mjs.map +7 -0
- package/dist/lib/neutral/create-object-SYZMRR4H.mjs +98 -0
- package/dist/lib/neutral/create-object-SYZMRR4H.mjs.map +7 -0
- package/dist/lib/neutral/curate-magazine-MA6OLK2X.mjs +184 -0
- package/dist/lib/neutral/curate-magazine-MA6OLK2X.mjs.map +7 -0
- package/dist/lib/neutral/fetch-article-content-CZO6D3QD.mjs +31 -0
- package/dist/lib/neutral/fetch-article-content-CZO6D3QD.mjs.map +7 -0
- package/dist/lib/neutral/index.mjs +18 -0
- package/dist/lib/neutral/index.mjs.map +7 -0
- package/dist/lib/neutral/load-post-content-XGL6VFF6.mjs +65 -0
- package/dist/lib/neutral/load-post-content-XGL6VFF6.mjs.map +7 -0
- package/dist/lib/neutral/meta.json +1 -0
- package/dist/lib/neutral/meta.mjs +8 -0
- package/dist/lib/neutral/meta.mjs.map +7 -0
- package/dist/lib/neutral/navigation-resolver-IN4OMCDR.mjs +18 -0
- package/dist/lib/neutral/navigation-resolver-IN4OMCDR.mjs.map +7 -0
- package/dist/lib/neutral/operation-handler-JE3743BU.mjs +8 -0
- package/dist/lib/neutral/operation-handler-JE3743BU.mjs.map +7 -0
- package/dist/lib/neutral/operations/index.mjs +8 -0
- package/dist/lib/neutral/operations/index.mjs.map +7 -0
- package/dist/lib/neutral/plugin.mjs +16 -0
- package/dist/lib/neutral/plugin.mjs.map +7 -0
- package/dist/lib/neutral/plugin.workerd.mjs +16 -0
- package/dist/lib/neutral/plugin.workerd.mjs.map +7 -0
- package/dist/lib/neutral/react-surface-TZESAMG2.mjs +60 -0
- package/dist/lib/neutral/react-surface-TZESAMG2.mjs.map +7 -0
- package/dist/lib/neutral/routine-templates-A2XTJOZO.mjs +52 -0
- package/dist/lib/neutral/routine-templates-A2XTJOZO.mjs.map +7 -0
- package/dist/lib/neutral/skill-definition-3KLMUEC5.mjs +8 -0
- package/dist/lib/neutral/skill-definition-3KLMUEC5.mjs.map +7 -0
- package/dist/lib/neutral/skills/index.mjs +46 -0
- package/dist/lib/neutral/skills/index.mjs.map +7 -0
- package/dist/lib/neutral/sync-feed-UZX2XHSQ.mjs +99 -0
- package/dist/lib/neutral/sync-feed-UZX2XHSQ.mjs.map +7 -0
- package/dist/lib/neutral/translations.mjs +81 -0
- package/dist/lib/neutral/translations.mjs.map +7 -0
- package/dist/lib/neutral/types/index.mjs +14 -0
- package/dist/lib/neutral/types/index.mjs.map +7 -0
- package/dist/types/dx.config.d.ts +28 -0
- package/dist/types/dx.config.d.ts.map +1 -0
- package/dist/types/src/MagazinePlugin.d.ts +4 -0
- package/dist/types/src/MagazinePlugin.d.ts.map +1 -0
- package/dist/types/src/MagazinePlugin.test.d.ts +2 -0
- package/dist/types/src/MagazinePlugin.test.d.ts.map +1 -0
- package/dist/types/src/MagazinePlugin.workerd.d.ts +4 -0
- package/dist/types/src/MagazinePlugin.workerd.d.ts.map +1 -0
- package/dist/types/src/atoms/atoms.test.d.ts +2 -0
- package/dist/types/src/atoms/atoms.test.d.ts.map +1 -0
- package/dist/types/src/atoms/index.d.ts +7 -0
- package/dist/types/src/atoms/index.d.ts.map +1 -0
- package/dist/types/src/atoms/magazine-posts.d.ts +16 -0
- package/dist/types/src/atoms/magazine-posts.d.ts.map +1 -0
- package/dist/types/src/atoms/post-content.d.ts +7 -0
- package/dist/types/src/atoms/post-content.d.ts.map +1 -0
- package/dist/types/src/atoms/post-curation.d.ts +16 -0
- package/dist/types/src/atoms/post-curation.d.ts.map +1 -0
- package/dist/types/src/atoms/post-display.d.ts +22 -0
- package/dist/types/src/atoms/post-display.d.ts.map +1 -0
- package/dist/types/src/atoms/post-read.d.ts +14 -0
- package/dist/types/src/atoms/post-read.d.ts.map +1 -0
- package/dist/types/src/atoms/post-tags.d.ts +16 -0
- package/dist/types/src/atoms/post-tags.d.ts.map +1 -0
- package/dist/types/src/capabilities/app-graph-builder.d.ts +5 -0
- package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -0
- package/dist/types/src/capabilities/create-object.d.ts +5 -0
- package/dist/types/src/capabilities/create-object.d.ts.map +1 -0
- package/dist/types/src/capabilities/index.d.ts +10 -0
- package/dist/types/src/capabilities/index.d.ts.map +1 -0
- package/dist/types/src/capabilities/navigation-resolver.d.ts +5 -0
- package/dist/types/src/capabilities/navigation-resolver.d.ts.map +1 -0
- package/dist/types/src/capabilities/operation-handler.d.ts +6 -0
- package/dist/types/src/capabilities/operation-handler.d.ts.map +1 -0
- package/dist/types/src/capabilities/react-surface.d.ts +5 -0
- package/dist/types/src/capabilities/react-surface.d.ts.map +1 -0
- package/dist/types/src/capabilities/routine-templates.d.ts +5 -0
- package/dist/types/src/capabilities/routine-templates.d.ts.map +1 -0
- package/dist/types/src/capabilities/skill-definition.d.ts +5 -0
- package/dist/types/src/capabilities/skill-definition.d.ts.map +1 -0
- package/dist/types/src/components/PostContent/PostContent.d.ts +30 -0
- package/dist/types/src/components/PostContent/PostContent.d.ts.map +1 -0
- package/dist/types/src/components/PostContent/PostContent.test.d.ts +2 -0
- package/dist/types/src/components/PostContent/PostContent.test.d.ts.map +1 -0
- package/dist/types/src/components/PostContent/index.d.ts +2 -0
- package/dist/types/src/components/PostContent/index.d.ts.map +1 -0
- package/dist/types/src/components/PostStack/PostStack.d.ts +17 -0
- package/dist/types/src/components/PostStack/PostStack.d.ts.map +1 -0
- package/dist/types/src/components/PostStack/PostStack.stories.d.ts +10 -0
- package/dist/types/src/components/PostStack/PostStack.stories.d.ts.map +1 -0
- package/dist/types/src/components/PostStack/index.d.ts +2 -0
- package/dist/types/src/components/PostStack/index.d.ts.map +1 -0
- package/dist/types/src/components/SubscriptionStack/SubscriptionStack.d.ts +23 -0
- package/dist/types/src/components/SubscriptionStack/SubscriptionStack.d.ts.map +1 -0
- package/dist/types/src/components/SubscriptionStack/SubscriptionStack.stories.d.ts +8 -0
- package/dist/types/src/components/SubscriptionStack/SubscriptionStack.stories.d.ts.map +1 -0
- package/dist/types/src/components/SubscriptionStack/index.d.ts +2 -0
- package/dist/types/src/components/SubscriptionStack/index.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +4 -0
- package/dist/types/src/components/index.d.ts.map +1 -0
- package/dist/types/src/containers/FeedArticle/FeedArticle.d.ts +6 -0
- package/dist/types/src/containers/FeedArticle/FeedArticle.d.ts.map +1 -0
- package/dist/types/src/containers/FeedArticle/FeedArticle.stories.d.ts +12 -0
- package/dist/types/src/containers/FeedArticle/FeedArticle.stories.d.ts.map +1 -0
- package/dist/types/src/containers/FeedArticle/FeedToolbar.d.ts +10 -0
- package/dist/types/src/containers/FeedArticle/FeedToolbar.d.ts.map +1 -0
- package/dist/types/src/containers/FeedArticle/index.d.ts +2 -0
- package/dist/types/src/containers/FeedArticle/index.d.ts.map +1 -0
- package/dist/types/src/containers/FeedProperties/FeedProperties.d.ts +6 -0
- package/dist/types/src/containers/FeedProperties/FeedProperties.d.ts.map +1 -0
- package/dist/types/src/containers/FeedProperties/index.d.ts +2 -0
- package/dist/types/src/containers/FeedProperties/index.d.ts.map +1 -0
- package/dist/types/src/containers/MagazineArticle/MagazineArticle.d.ts +6 -0
- package/dist/types/src/containers/MagazineArticle/MagazineArticle.d.ts.map +1 -0
- package/dist/types/src/containers/MagazineArticle/MagazineArticle.stories.d.ts +9 -0
- package/dist/types/src/containers/MagazineArticle/MagazineArticle.stories.d.ts.map +1 -0
- package/dist/types/src/containers/MagazineArticle/MagazineTile.d.ts +11 -0
- package/dist/types/src/containers/MagazineArticle/MagazineTile.d.ts.map +1 -0
- package/dist/types/src/containers/MagazineArticle/index.d.ts +2 -0
- package/dist/types/src/containers/MagazineArticle/index.d.ts.map +1 -0
- package/dist/types/src/containers/MagazineArticle/useToolbar.d.ts +18 -0
- package/dist/types/src/containers/MagazineArticle/useToolbar.d.ts.map +1 -0
- package/dist/types/src/containers/PostArticle/PostArticle.d.ts +6 -0
- package/dist/types/src/containers/PostArticle/PostArticle.d.ts.map +1 -0
- package/dist/types/src/containers/PostArticle/PostArticle.stories.d.ts +8 -0
- package/dist/types/src/containers/PostArticle/PostArticle.stories.d.ts.map +1 -0
- package/dist/types/src/containers/PostArticle/PostToolbar.d.ts +19 -0
- package/dist/types/src/containers/PostArticle/PostToolbar.d.ts.map +1 -0
- package/dist/types/src/containers/PostArticle/index.d.ts +2 -0
- package/dist/types/src/containers/PostArticle/index.d.ts.map +1 -0
- package/dist/types/src/containers/PostCard/PostCard.d.ts +11 -0
- package/dist/types/src/containers/PostCard/PostCard.d.ts.map +1 -0
- package/dist/types/src/containers/PostCard/index.d.ts +2 -0
- package/dist/types/src/containers/PostCard/index.d.ts.map +1 -0
- package/dist/types/src/containers/SubscriptionsArticle/SubscriptionsArticle.d.ts +5 -0
- package/dist/types/src/containers/SubscriptionsArticle/SubscriptionsArticle.d.ts.map +1 -0
- package/dist/types/src/containers/SubscriptionsArticle/SubscriptionsArticle.stories.d.ts +8 -0
- package/dist/types/src/containers/SubscriptionsArticle/SubscriptionsArticle.stories.d.ts.map +1 -0
- package/dist/types/src/containers/SubscriptionsArticle/index.d.ts +2 -0
- package/dist/types/src/containers/SubscriptionsArticle/index.d.ts.map +1 -0
- package/dist/types/src/containers/index.d.ts +7 -0
- package/dist/types/src/containers/index.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +3 -0
- package/dist/types/src/index.d.ts.map +1 -0
- package/dist/types/src/meta.d.ts +33 -0
- package/dist/types/src/meta.d.ts.map +1 -0
- package/dist/types/src/operations/clear-magazine.d.ts +10 -0
- package/dist/types/src/operations/clear-magazine.d.ts.map +1 -0
- package/dist/types/src/operations/curate-magazine.d.ts +25 -0
- package/dist/types/src/operations/curate-magazine.d.ts.map +1 -0
- package/dist/types/src/operations/curate-magazine.skill.test.d.ts +2 -0
- package/dist/types/src/operations/curate-magazine.skill.test.d.ts.map +1 -0
- package/dist/types/src/operations/curate-magazine.test.d.ts +2 -0
- package/dist/types/src/operations/curate-magazine.test.d.ts.map +1 -0
- package/dist/types/src/operations/extraction/article.d.ts +37 -0
- package/dist/types/src/operations/extraction/article.d.ts.map +1 -0
- package/dist/types/src/operations/extraction/article.test.d.ts +2 -0
- package/dist/types/src/operations/extraction/article.test.d.ts.map +1 -0
- package/dist/types/src/operations/extraction/index.d.ts +2 -0
- package/dist/types/src/operations/extraction/index.d.ts.map +1 -0
- package/dist/types/src/operations/fetch-article-content.d.ts +5 -0
- package/dist/types/src/operations/fetch-article-content.d.ts.map +1 -0
- package/dist/types/src/operations/index.d.ts +3 -0
- package/dist/types/src/operations/index.d.ts.map +1 -0
- package/dist/types/src/operations/load-post-content.d.ts +8 -0
- package/dist/types/src/operations/load-post-content.d.ts.map +1 -0
- package/dist/types/src/operations/sources/article.d.ts +24 -0
- package/dist/types/src/operations/sources/article.d.ts.map +1 -0
- package/dist/types/src/operations/sources/cors.d.ts +12 -0
- package/dist/types/src/operations/sources/cors.d.ts.map +1 -0
- package/dist/types/src/operations/sources/feed-fetcher.d.ts +22 -0
- package/dist/types/src/operations/sources/feed-fetcher.d.ts.map +1 -0
- package/dist/types/src/operations/sources/feed-fetcher.test.d.ts +2 -0
- package/dist/types/src/operations/sources/feed-fetcher.test.d.ts.map +1 -0
- package/dist/types/src/operations/sources/http.d.ts +9 -0
- package/dist/types/src/operations/sources/http.d.ts.map +1 -0
- package/dist/types/src/operations/sources/index.d.ts +6 -0
- package/dist/types/src/operations/sources/index.d.ts.map +1 -0
- package/dist/types/src/operations/sources/rss.d.ts +4 -0
- package/dist/types/src/operations/sources/rss.d.ts.map +1 -0
- package/dist/types/src/operations/sources/rss.test.d.ts +2 -0
- package/dist/types/src/operations/sources/rss.test.d.ts.map +1 -0
- package/dist/types/src/operations/sources/standard-site.d.ts +33 -0
- package/dist/types/src/operations/sources/standard-site.d.ts.map +1 -0
- package/dist/types/src/operations/sync-feed.d.ts +5 -0
- package/dist/types/src/operations/sync-feed.d.ts.map +1 -0
- package/dist/types/src/operations/sync-feed.test.d.ts +2 -0
- package/dist/types/src/operations/sync-feed.test.d.ts.map +1 -0
- package/dist/types/src/operations/util.d.ts +25 -0
- package/dist/types/src/operations/util.d.ts.map +1 -0
- package/dist/types/src/operations/util.test.d.ts +2 -0
- package/dist/types/src/operations/util.test.d.ts.map +1 -0
- package/dist/types/src/paths.d.ts +3 -0
- package/dist/types/src/paths.d.ts.map +1 -0
- package/dist/types/src/plugin.d.ts +4 -0
- package/dist/types/src/plugin.d.ts.map +1 -0
- package/dist/types/src/plugin.workerd.d.ts +3 -0
- package/dist/types/src/plugin.workerd.d.ts.map +1 -0
- package/dist/types/src/skills/index.d.ts +2 -0
- package/dist/types/src/skills/index.d.ts.map +1 -0
- package/dist/types/src/skills/magazine-skill.d.ts +4 -0
- package/dist/types/src/skills/magazine-skill.d.ts.map +1 -0
- package/dist/types/src/stories/ArticleExtractor.stories.d.ts +267 -0
- package/dist/types/src/stories/ArticleExtractor.stories.d.ts.map +1 -0
- package/dist/types/src/stories/MagazineCurate.stories.d.ts +32 -0
- package/dist/types/src/stories/MagazineCurate.stories.d.ts.map +1 -0
- package/dist/types/src/stories/theregister-fixture.test.d.ts +2 -0
- package/dist/types/src/stories/theregister-fixture.test.d.ts.map +1 -0
- package/dist/types/src/templates/magazine-curation.d.ts +7 -0
- package/dist/types/src/templates/magazine-curation.d.ts.map +1 -0
- package/dist/types/src/testing/builder.d.ts +28 -0
- package/dist/types/src/testing/builder.d.ts.map +1 -0
- package/dist/types/src/testing/index.d.ts +2 -0
- package/dist/types/src/testing/index.d.ts.map +1 -0
- package/dist/types/src/translations.d.ts +247 -0
- package/dist/types/src/translations.d.ts.map +1 -0
- package/dist/types/src/types/CreateSubscription.d.ts +29 -0
- package/dist/types/src/types/CreateSubscription.d.ts.map +1 -0
- package/dist/types/src/types/FeedOperation.d.ts +52 -0
- package/dist/types/src/types/FeedOperation.d.ts.map +1 -0
- package/dist/types/src/types/Magazine.d.ts +87 -0
- package/dist/types/src/types/Magazine.d.ts.map +1 -0
- package/dist/types/src/types/Magazine.test.d.ts +2 -0
- package/dist/types/src/types/Magazine.test.d.ts.map +1 -0
- package/dist/types/src/types/Subscription.d.ts +144 -0
- package/dist/types/src/types/Subscription.d.ts.map +1 -0
- package/dist/types/src/types/Subscription.test.d.ts +2 -0
- package/dist/types/src/types/Subscription.test.d.ts.map +1 -0
- package/dist/types/src/types/index.d.ts +5 -0
- package/dist/types/src/types/index.d.ts.map +1 -0
- package/dist/types/src/util/date.d.ts +8 -0
- package/dist/types/src/util/date.d.ts.map +1 -0
- package/dist/types/src/util/index.d.ts +4 -0
- package/dist/types/src/util/index.d.ts.map +1 -0
- package/dist/types/src/util/post-content.d.ts +14 -0
- package/dist/types/src/util/post-content.d.ts.map +1 -0
- package/dist/types/src/util/text.d.ts +18 -0
- package/dist/types/src/util/text.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/dx.config.ts +48 -0
- package/package.json +164 -0
- package/src/MagazinePlugin.test.ts +36 -0
- package/src/MagazinePlugin.tsx +61 -0
- package/src/MagazinePlugin.workerd.ts +31 -0
- package/src/atoms/atoms.test.ts +323 -0
- package/src/atoms/index.ts +10 -0
- package/src/atoms/magazine-posts.ts +70 -0
- package/src/atoms/post-content.ts +34 -0
- package/src/atoms/post-curation.ts +36 -0
- package/src/atoms/post-display.ts +59 -0
- package/src/atoms/post-read.ts +40 -0
- package/src/atoms/post-tags.ts +66 -0
- package/src/capabilities/app-graph-builder.ts +125 -0
- package/src/capabilities/create-object.ts +127 -0
- package/src/capabilities/index.ts +17 -0
- package/src/capabilities/navigation-resolver.ts +21 -0
- package/src/capabilities/operation-handler.ts +16 -0
- package/src/capabilities/react-surface.tsx +53 -0
- package/src/capabilities/routine-templates.ts +16 -0
- package/src/capabilities/skill-definition.ts +14 -0
- package/src/components/PostContent/PostContent.test.ts +81 -0
- package/src/components/PostContent/PostContent.tsx +111 -0
- package/src/components/PostContent/index.ts +5 -0
- package/src/components/PostStack/PostStack.stories.tsx +39 -0
- package/src/components/PostStack/PostStack.tsx +151 -0
- package/src/components/PostStack/index.ts +5 -0
- package/src/components/SubscriptionStack/SubscriptionStack.stories.tsx +52 -0
- package/src/components/SubscriptionStack/SubscriptionStack.tsx +161 -0
- package/src/components/SubscriptionStack/index.ts +5 -0
- package/src/components/index.ts +7 -0
- package/src/containers/FeedArticle/FeedArticle.stories.tsx +102 -0
- package/src/containers/FeedArticle/FeedArticle.tsx +68 -0
- package/src/containers/FeedArticle/FeedToolbar.tsx +47 -0
- package/src/containers/FeedArticle/index.ts +5 -0
- package/src/containers/FeedProperties/FeedProperties.tsx +107 -0
- package/src/containers/FeedProperties/index.ts +5 -0
- package/src/containers/MagazineArticle/MagazineArticle.stories.tsx +244 -0
- package/src/containers/MagazineArticle/MagazineArticle.tsx +146 -0
- package/src/containers/MagazineArticle/MagazineTile.tsx +88 -0
- package/src/containers/MagazineArticle/index.ts +5 -0
- package/src/containers/MagazineArticle/useToolbar.tsx +145 -0
- package/src/containers/PostArticle/PostArticle.stories.tsx +117 -0
- package/src/containers/PostArticle/PostArticle.tsx +140 -0
- package/src/containers/PostArticle/PostToolbar.tsx +120 -0
- package/src/containers/PostArticle/index.ts +5 -0
- package/src/containers/PostCard/PostCard.tsx +70 -0
- package/src/containers/PostCard/index.ts +5 -0
- package/src/containers/SubscriptionsArticle/SubscriptionsArticle.stories.tsx +80 -0
- package/src/containers/SubscriptionsArticle/SubscriptionsArticle.tsx +99 -0
- package/src/containers/SubscriptionsArticle/index.ts +5 -0
- package/src/containers/index.ts +11 -0
- package/src/index.ts +6 -0
- package/src/meta.ts +9 -0
- package/src/operations/clear-magazine.ts +83 -0
- package/src/operations/curate-magazine.skill.conversations.json +1 -0
- package/src/operations/curate-magazine.skill.test.ts +167 -0
- package/src/operations/curate-magazine.test.ts +91 -0
- package/src/operations/curate-magazine.ts +178 -0
- package/src/operations/extraction/article.test.ts +154 -0
- package/src/operations/extraction/article.ts +270 -0
- package/src/operations/extraction/index.ts +5 -0
- package/src/operations/fetch-article-content.ts +27 -0
- package/src/operations/index.ts +13 -0
- package/src/operations/load-post-content.ts +76 -0
- package/src/operations/sources/article.ts +122 -0
- package/src/operations/sources/cors.ts +19 -0
- package/src/operations/sources/feed-fetcher.test.ts +200 -0
- package/src/operations/sources/feed-fetcher.ts +26 -0
- package/src/operations/sources/http.ts +42 -0
- package/src/operations/sources/index.ts +9 -0
- package/src/operations/sources/rss.test.ts +201 -0
- package/src/operations/sources/rss.ts +138 -0
- package/src/operations/sources/standard-site.ts +381 -0
- package/src/operations/sources/testing/feed.xml +477 -0
- package/src/operations/sync-feed.test.ts +146 -0
- package/src/operations/sync-feed.ts +140 -0
- package/src/operations/util.test.ts +67 -0
- package/src/operations/util.ts +65 -0
- package/src/paths.ts +14 -0
- package/src/plugin.ts +11 -0
- package/src/plugin.workerd.ts +6 -0
- package/src/skills/index.ts +5 -0
- package/src/skills/magazine-skill.ts +45 -0
- package/src/stories/ArticleExtractor.stories.tsx +204 -0
- package/src/stories/MagazineCurate.stories.tsx +217 -0
- package/src/stories/fixtures/theregister-ai.xml +45 -0
- package/src/stories/theregister-fixture.test.ts +35 -0
- package/src/templates/magazine-curation.ts +47 -0
- package/src/testing/builder.ts +77 -0
- package/src/testing/index.ts +5 -0
- package/src/translations.ts +81 -0
- package/src/types/CreateSubscription.ts +61 -0
- package/src/types/FeedOperation.ts +134 -0
- package/src/types/Magazine.test.ts +69 -0
- package/src/types/Magazine.ts +214 -0
- package/src/types/Subscription.test.ts +171 -0
- package/src/types/Subscription.ts +366 -0
- package/src/types/index.ts +8 -0
- package/src/util/date.ts +27 -0
- package/src/util/index.ts +7 -0
- package/src/util/post-content.ts +19 -0
- package/src/util/text.ts +98 -0
- package/src/vite-env.d.ts +10 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Effect from 'effect/Effect';
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
|
|
7
|
+
|
|
8
|
+
import { Database, Feed, Filter, Obj, Ref, Tag } from '@dxos/echo';
|
|
9
|
+
import { EchoTestBuilder } from '@dxos/echo-client/testing';
|
|
10
|
+
import { EffectEx } from '@dxos/effect';
|
|
11
|
+
import { EID } from '@dxos/keys';
|
|
12
|
+
import { StateMap, TagIndex } from '@dxos/schema';
|
|
13
|
+
|
|
14
|
+
import { Magazine, Subscription } from './index';
|
|
15
|
+
|
|
16
|
+
describe('per-Post state keyed by entity id', () => {
|
|
17
|
+
let builder: EchoTestBuilder;
|
|
18
|
+
|
|
19
|
+
beforeEach(async () => {
|
|
20
|
+
builder = await new EchoTestBuilder().open();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(async () => {
|
|
24
|
+
await builder.close();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const loadQueuePost = async () => {
|
|
28
|
+
const { db } = await builder.createDatabase({
|
|
29
|
+
types: [Feed.Feed, Tag.Tag, Subscription.Subscription, Subscription.Post, StateMap.StateMap, TagIndex.TagIndex],
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const subscription = db.add(Subscription.makeSubscription({ name: 'Test', url: 'https://example.com/rss' }));
|
|
33
|
+
await db.flush();
|
|
34
|
+
|
|
35
|
+
const postFeed = subscription.feed.target!;
|
|
36
|
+
const post = Obj.make(Subscription.Post, {
|
|
37
|
+
title: 'Article',
|
|
38
|
+
link: 'https://example.com/a',
|
|
39
|
+
source: Ref.make(subscription),
|
|
40
|
+
});
|
|
41
|
+
await EffectEx.runAndForwardErrors(Feed.append(postFeed, [post]).pipe(Effect.provide(Database.layer(db))));
|
|
42
|
+
const [queuePost] = await EffectEx.runAndForwardErrors(
|
|
43
|
+
Feed.query(postFeed, Filter.type(Subscription.Post)).run.pipe(Effect.provide(Database.layer(db))),
|
|
44
|
+
);
|
|
45
|
+
expect(queuePost).toBeDefined();
|
|
46
|
+
return { db, subscription, queuePost: queuePost! };
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
test('setTag indexes queue posts by entity id, not echo uri', async () => {
|
|
50
|
+
const { db, subscription, queuePost } = await loadQueuePost();
|
|
51
|
+
|
|
52
|
+
await Subscription.setTag(subscription, queuePost.id, db, 'starred', true);
|
|
53
|
+
await db.flush();
|
|
54
|
+
|
|
55
|
+
const starredUri = await Subscription.findSystemTagUri(db, 'starred');
|
|
56
|
+
expect(Subscription.hasTag(subscription, queuePost.id, starredUri)).toBe(true);
|
|
57
|
+
expect(Subscription.hasTag(subscription, Obj.getURI(queuePost), starredUri)).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('setReadAt indexes queue posts by entity id, not echo uri', async () => {
|
|
61
|
+
const { subscription, queuePost } = await loadQueuePost();
|
|
62
|
+
|
|
63
|
+
Subscription.setReadAt(subscription, queuePost.id, '2026-01-01T00:00:00Z');
|
|
64
|
+
expect(Subscription.getReadAt(subscription, queuePost.id)).toBe('2026-01-01T00:00:00Z');
|
|
65
|
+
expect(Subscription.getReadAt(subscription, Obj.getURI(queuePost))).toBeUndefined();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('PostContent reverse-ref lookup', () => {
|
|
70
|
+
let builder: EchoTestBuilder;
|
|
71
|
+
|
|
72
|
+
beforeEach(async () => {
|
|
73
|
+
builder = await new EchoTestBuilder().open();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
afterEach(async () => {
|
|
77
|
+
await builder.close();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('findPostContent resolves via post ref and reverse-ref index', async () => {
|
|
81
|
+
const { db } = await builder.createDatabase({
|
|
82
|
+
types: [
|
|
83
|
+
Feed.Feed,
|
|
84
|
+
Subscription.Subscription,
|
|
85
|
+
Subscription.Post,
|
|
86
|
+
Subscription.PostContent,
|
|
87
|
+
Magazine.Magazine,
|
|
88
|
+
StateMap.StateMap,
|
|
89
|
+
TagIndex.TagIndex,
|
|
90
|
+
],
|
|
91
|
+
});
|
|
92
|
+
const space = { db };
|
|
93
|
+
|
|
94
|
+
const subscription = db.add(Subscription.makeSubscription({ name: 'Test', url: 'https://example.com/rss' }));
|
|
95
|
+
await db.flush();
|
|
96
|
+
|
|
97
|
+
const postFeed = subscription.feed.target!;
|
|
98
|
+
const post = Obj.make(Subscription.Post, {
|
|
99
|
+
title: 'Article',
|
|
100
|
+
link: 'https://example.com/a',
|
|
101
|
+
source: Ref.make(subscription),
|
|
102
|
+
});
|
|
103
|
+
await EffectEx.runAndForwardErrors(Feed.append(postFeed, [post]).pipe(Effect.provide(Database.layer(db))));
|
|
104
|
+
const [queuePost] = await EffectEx.runAndForwardErrors(
|
|
105
|
+
Feed.query(postFeed, Filter.type(Subscription.Post)).run.pipe(Effect.provide(Database.layer(db))),
|
|
106
|
+
);
|
|
107
|
+
expect(queuePost).toBeDefined();
|
|
108
|
+
|
|
109
|
+
await Subscription.appendPostContent(space, subscription, {
|
|
110
|
+
post: queuePost!,
|
|
111
|
+
text: '# Hello',
|
|
112
|
+
snippet: 'Hello',
|
|
113
|
+
fetchedAt: '2026-01-01T00:00:00Z',
|
|
114
|
+
});
|
|
115
|
+
await db.flush({ indexes: true });
|
|
116
|
+
|
|
117
|
+
const found = await Subscription.findPostContent(subscription, queuePost!);
|
|
118
|
+
expect(found?.text).toBe('# Hello');
|
|
119
|
+
expect(EID.equals(EID.parse(found!.post.uri), EID.parse(Obj.getURI(queuePost!)))).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('queryPostContentForPost returns newest entry after refresh append', async () => {
|
|
123
|
+
const { db } = await builder.createDatabase({
|
|
124
|
+
types: [
|
|
125
|
+
Feed.Feed,
|
|
126
|
+
Subscription.Subscription,
|
|
127
|
+
Subscription.Post,
|
|
128
|
+
Subscription.PostContent,
|
|
129
|
+
StateMap.StateMap,
|
|
130
|
+
TagIndex.TagIndex,
|
|
131
|
+
],
|
|
132
|
+
});
|
|
133
|
+
const space = { db };
|
|
134
|
+
|
|
135
|
+
const subscription = db.add(Subscription.makeSubscription({ name: 'Test', url: 'https://example.com/rss' }));
|
|
136
|
+
await db.flush();
|
|
137
|
+
|
|
138
|
+
const postFeed = subscription.feed.target!;
|
|
139
|
+
const post = Obj.make(Subscription.Post, {
|
|
140
|
+
title: 'Article',
|
|
141
|
+
link: 'https://example.com/a',
|
|
142
|
+
source: Ref.make(subscription),
|
|
143
|
+
});
|
|
144
|
+
await EffectEx.runAndForwardErrors(Feed.append(postFeed, [post]).pipe(Effect.provide(Database.layer(db))));
|
|
145
|
+
const [queuePost] = await EffectEx.runAndForwardErrors(
|
|
146
|
+
Feed.query(postFeed, Filter.type(Subscription.Post)).run.pipe(Effect.provide(Database.layer(db))),
|
|
147
|
+
);
|
|
148
|
+
expect(queuePost).toBeDefined();
|
|
149
|
+
|
|
150
|
+
await Subscription.appendPostContent(space, subscription, {
|
|
151
|
+
post: queuePost!,
|
|
152
|
+
text: 'v1',
|
|
153
|
+
fetchedAt: '2026-01-01T00:00:00Z',
|
|
154
|
+
});
|
|
155
|
+
await Subscription.appendPostContent(space, subscription, {
|
|
156
|
+
post: queuePost!,
|
|
157
|
+
text: 'v2',
|
|
158
|
+
fetchedAt: '2026-01-02T00:00:00Z',
|
|
159
|
+
});
|
|
160
|
+
await db.flush({ indexes: true });
|
|
161
|
+
|
|
162
|
+
const query = Subscription.queryPostContentForPost(subscription, queuePost!);
|
|
163
|
+
expect(query).toBeDefined();
|
|
164
|
+
|
|
165
|
+
const entries = await db.query(query!).run();
|
|
166
|
+
expect(entries.map((entry) => entry.text).sort()).toEqual(['v1', 'v2']);
|
|
167
|
+
|
|
168
|
+
const latest = await Subscription.findPostContent(subscription, queuePost!);
|
|
169
|
+
expect(latest?.text).toBe('v2');
|
|
170
|
+
});
|
|
171
|
+
});
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
// @import-as-namespace
|
|
6
|
+
|
|
7
|
+
import * as Effect from 'effect/Effect';
|
|
8
|
+
import * as Schema from 'effect/Schema';
|
|
9
|
+
|
|
10
|
+
import { type Space } from '@dxos/client/echo';
|
|
11
|
+
import { Annotation, Database, DXN, Feed, Filter, Obj, Query, Ref, Scope, Tag, Type } from '@dxos/echo';
|
|
12
|
+
import { FormInputAnnotation, LabelAnnotation } from '@dxos/echo/Annotation';
|
|
13
|
+
import { EffectEx } from '@dxos/effect';
|
|
14
|
+
import { type EntityId } from '@dxos/keys';
|
|
15
|
+
import { FactoryAnnotation, type FactoryFn, FeedAnnotation, StateMap, TagIndex } from '@dxos/schema';
|
|
16
|
+
|
|
17
|
+
// TODO: FeedType is currently a closed literal — plugins like plugin-bluesky must add their type here.
|
|
18
|
+
// Consider making it extensible (e.g. a string schema with a well-known-values registry) so plugins can
|
|
19
|
+
// register feed types without modifying core.
|
|
20
|
+
/** Subscription protocol type. */
|
|
21
|
+
export const FeedType = Schema.Literal('standard-site', 'rss', 'bluesky');
|
|
22
|
+
export type FeedType = Schema.Schema.Type<typeof FeedType>;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* System per-Post flags (starred/archived) modelled as space-wide {@link Tag} objects (resolved via
|
|
26
|
+
* `Tag.findOrCreate` on first use). Each entry is a single source of truth carrying its foreign
|
|
27
|
+
* `key` plus display `label`/`hue`.
|
|
28
|
+
*/
|
|
29
|
+
export const SYSTEM_TAGS = {
|
|
30
|
+
starred: { key: { source: 'org.dxos.plugin.magazine', id: 'starred' }, label: 'Starred', hue: 'amber' },
|
|
31
|
+
archived: { key: { source: 'org.dxos.plugin.magazine', id: 'archived' }, label: 'Archived', hue: 'neutral' },
|
|
32
|
+
} as const;
|
|
33
|
+
|
|
34
|
+
/** A system per-Post flag modelled as a tag. */
|
|
35
|
+
export type SystemTag = keyof typeof SYSTEM_TAGS;
|
|
36
|
+
|
|
37
|
+
/** Per-Post mutable state stored on the Subscription (shared across magazines), keyed by Post id. */
|
|
38
|
+
export const PostState = Schema.Struct({
|
|
39
|
+
/** ISO 8601 timestamp when first opened. */
|
|
40
|
+
readAt: Schema.optional(Schema.String),
|
|
41
|
+
});
|
|
42
|
+
export type PostState = Schema.Schema.Type<typeof PostState>;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Default upper bound on the number of (non-starred) Posts retained when
|
|
46
|
+
* syncing a {@link Subscription} or curating a Magazine. Applied when the object
|
|
47
|
+
* has no explicit `keep` value.
|
|
48
|
+
*/
|
|
49
|
+
export const DEFAULT_KEEP = 10;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Subscription feed schema: an RSS/Atom/AT Protocol subscription.
|
|
53
|
+
*/
|
|
54
|
+
export class Subscription extends Type.makeObject<Subscription>(DXN.make('org.dxos.type.subscription.feed', '0.1.0'))(
|
|
55
|
+
Schema.Struct({
|
|
56
|
+
/** User-facing title of the feed. */
|
|
57
|
+
name: Schema.String.pipe(Schema.optional),
|
|
58
|
+
/** The URL of the RSS/Atom feed or AT Protocol handle/DID. */
|
|
59
|
+
url: Schema.String.pipe(Schema.optional),
|
|
60
|
+
/** Protocol type — determines which fetcher is used for sync. */
|
|
61
|
+
type: FeedType.pipe(Schema.optional),
|
|
62
|
+
/** Description of the feed. */
|
|
63
|
+
description: Schema.String.pipe(Schema.optional),
|
|
64
|
+
/** URL of the feed's associated website. */
|
|
65
|
+
link: Schema.String.pipe(Schema.optional),
|
|
66
|
+
/** URL of the feed's icon/image. */
|
|
67
|
+
iconUrl: Schema.String.pipe(Schema.optional),
|
|
68
|
+
/**
|
|
69
|
+
* Maximum number of (non-starred) Posts retained in the feed's queue when syncing.
|
|
70
|
+
* Older posts beyond this bound are dropped; starred posts are preserved regardless.
|
|
71
|
+
* Defaults to {@link DEFAULT_KEEP} when unset.
|
|
72
|
+
*/
|
|
73
|
+
keep: Schema.Number.pipe(
|
|
74
|
+
Schema.annotations({
|
|
75
|
+
title: 'Keep',
|
|
76
|
+
description: 'Number of synced items.',
|
|
77
|
+
}),
|
|
78
|
+
Schema.optional,
|
|
79
|
+
),
|
|
80
|
+
/** Opaque sync cursor — protocol-specific. */
|
|
81
|
+
cursor: Schema.String.pipe(FormInputAnnotation.set(false), Schema.optional),
|
|
82
|
+
/** Backing ECHO feed (queue) for Posts: immutable feed entries appended by sync. */
|
|
83
|
+
feed: Ref.Ref(Feed.Feed).pipe(FormInputAnnotation.set(false)),
|
|
84
|
+
/**
|
|
85
|
+
* Backing ECHO feed (queue) for fetched article bodies — one
|
|
86
|
+
* {@link PostContent} entry per Post whose content has been loaded.
|
|
87
|
+
*
|
|
88
|
+
* Feeds (not the {@link postState} side map) are the right home for big
|
|
89
|
+
* immutable payloads: they're append-only, lazily replicated, and don't
|
|
90
|
+
* inflate the Subscription document. The side map keeps only the small
|
|
91
|
+
* per-Post user state (read/archived/star/imageUrl).
|
|
92
|
+
*
|
|
93
|
+
* Optional for backwards compatibility with older Subscription documents
|
|
94
|
+
* that pre-date this feed; new subscriptions always have one via
|
|
95
|
+
* {@link makeSubscription}.
|
|
96
|
+
*/
|
|
97
|
+
contentFeed: Ref.Ref(Feed.Feed).pipe(FormInputAnnotation.set(false), Schema.optional),
|
|
98
|
+
/**
|
|
99
|
+
* Per-Post mutable state keyed by Post id, shared across every Magazine that references the Post.
|
|
100
|
+
* Posts live immutably in the `feed` queue; their `readAt` marker lives here. (`snippet`/`imageUrl`
|
|
101
|
+
* are derived from the Post, or refined onto `contentFeed` entries — not stored here; star/archive
|
|
102
|
+
* are tags — see `tags`.)
|
|
103
|
+
*/
|
|
104
|
+
postState: Ref.Ref(StateMap.StateMap).pipe(FormInputAnnotation.set(false)),
|
|
105
|
+
/**
|
|
106
|
+
* Per-Post tags keyed by tag uri → Post ids. Boolean flags (starred, archived — see
|
|
107
|
+
* {@link SYSTEM_TAGS}) are modelled as {@link Tag} objects so they
|
|
108
|
+
* participate in the space-wide tag system. Stored as a child {@link TagIndex} object.
|
|
109
|
+
*/
|
|
110
|
+
tags: Ref.Ref(TagIndex.TagIndex).pipe(FormInputAnnotation.set(false)),
|
|
111
|
+
}).pipe(
|
|
112
|
+
LabelAnnotation.set(['name', 'url']),
|
|
113
|
+
Annotation.IconAnnotation.set({ icon: 'ph--rss--regular', hue: 'indigo' }),
|
|
114
|
+
FeedAnnotation.set(true),
|
|
115
|
+
FactoryAnnotation.set(((values) => makeSubscription(values)) as FactoryFn),
|
|
116
|
+
),
|
|
117
|
+
) {}
|
|
118
|
+
|
|
119
|
+
/** Checks if a value is a Subscription.Subscription object. */
|
|
120
|
+
export const instanceOf = (value: unknown): value is Subscription => Obj.instanceOf(Subscription, value);
|
|
121
|
+
|
|
122
|
+
/** Creates a Subscription.Subscription with backing ECHO feeds for posts and fetched content. */
|
|
123
|
+
export const makeSubscription = (
|
|
124
|
+
props: Omit<Obj.MakeProps<typeof Subscription>, 'feed' | 'contentFeed' | 'postState' | 'tags'> = {},
|
|
125
|
+
): Subscription => {
|
|
126
|
+
const postFeed = Feed.make();
|
|
127
|
+
const contentFeed = Feed.make();
|
|
128
|
+
const postState = StateMap.make();
|
|
129
|
+
const tags = TagIndex.make();
|
|
130
|
+
const subscription = Obj.make(Subscription, {
|
|
131
|
+
feed: Ref.make(postFeed),
|
|
132
|
+
contentFeed: Ref.make(contentFeed),
|
|
133
|
+
postState: Ref.make(postState),
|
|
134
|
+
tags: Ref.make(tags),
|
|
135
|
+
...props,
|
|
136
|
+
});
|
|
137
|
+
Obj.setParent(postFeed, subscription);
|
|
138
|
+
Obj.setParent(contentFeed, subscription);
|
|
139
|
+
// Per-Post state and tag index are children: cascade-deleted with the subscription.
|
|
140
|
+
Obj.setParent(postState, subscription);
|
|
141
|
+
Obj.setParent(tags, subscription);
|
|
142
|
+
return subscription;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* A single post/entry within a subscription feed.
|
|
147
|
+
*
|
|
148
|
+
* Immutable feed-entry: every field below is set at sync time by the protocol
|
|
149
|
+
* fetcher and never written to again. Mutable state (read, archived, starred,
|
|
150
|
+
* fetched content, hero image) lives on
|
|
151
|
+
* {@link Subscription.Subscription.postState} keyed by Post id; curation
|
|
152
|
+
* outputs (snippet, rank) live on `Magazine.postState`.
|
|
153
|
+
*/
|
|
154
|
+
export class Post extends Type.makeObject<Post>(DXN.make('org.dxos.type.subscription.post', '0.1.0'))(
|
|
155
|
+
Schema.Struct({
|
|
156
|
+
/** Source subscription feed; populated by `SyncSubscription` so curated posts can show provenance. */
|
|
157
|
+
source: Ref.Ref(Subscription).pipe(FormInputAnnotation.set(false), Schema.optional),
|
|
158
|
+
/** Post title. */
|
|
159
|
+
title: Schema.String.pipe(Schema.optional),
|
|
160
|
+
/** URL link to the original article. */
|
|
161
|
+
link: Schema.String.pipe(Schema.optional),
|
|
162
|
+
/** Plain-text or summary. */
|
|
163
|
+
description: Schema.String.pipe(Schema.optional),
|
|
164
|
+
/** Plain-text or HTML content. */
|
|
165
|
+
content: Schema.String.pipe(Schema.optional),
|
|
166
|
+
/** Author name. */
|
|
167
|
+
author: Schema.String.pipe(Schema.optional),
|
|
168
|
+
/** ISO 8601 publication date. */
|
|
169
|
+
published: Schema.String.pipe(Schema.optional),
|
|
170
|
+
/** Unique identifier (guid) from the feed. */
|
|
171
|
+
guid: Schema.String.pipe(Schema.optional),
|
|
172
|
+
}).pipe(
|
|
173
|
+
LabelAnnotation.set(['title']),
|
|
174
|
+
Annotation.IconAnnotation.set({ icon: 'ph--article--regular', hue: 'orange' }),
|
|
175
|
+
),
|
|
176
|
+
) {}
|
|
177
|
+
|
|
178
|
+
/** Creates a Subscription.Post object. */
|
|
179
|
+
export const makePost = (props: Obj.MakeProps<typeof Post> = {}): Post => Obj.make(Post, props);
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* An entry in a Subscription's {@link Subscription.contentFeed} queue: the
|
|
183
|
+
* fetched body of a Post, linked back to the queue Post in the parallel `feed`
|
|
184
|
+
* queue via {@link post}.
|
|
185
|
+
*
|
|
186
|
+
* Lives in a queue (not on `Subscription.postState`) because fetched bodies
|
|
187
|
+
* can be many KB; storing them inline would inflate the Subscription document
|
|
188
|
+
* unboundedly and force every reader to download every body. The queue is
|
|
189
|
+
* lazily replicated and append-only — a natural fit for immutable content.
|
|
190
|
+
*/
|
|
191
|
+
export class PostContent extends Type.makeObject<PostContent>(
|
|
192
|
+
DXN.make('org.dxos.type.subscription.postContent', '0.1.0'),
|
|
193
|
+
)(
|
|
194
|
+
Schema.Struct({
|
|
195
|
+
/** Source Post (in the Subscription's `feed` queue). */
|
|
196
|
+
post: Ref.Ref(Post).pipe(FormInputAnnotation.set(false)),
|
|
197
|
+
/** Extracted article body, in Markdown. */
|
|
198
|
+
text: Schema.String,
|
|
199
|
+
/** Refined snippet derived from the full article (preferred over the description-derived one). */
|
|
200
|
+
snippet: Schema.optional(Schema.String),
|
|
201
|
+
/** Refined hero image derived from the full article (preferred over the description-derived one). */
|
|
202
|
+
imageUrl: Schema.optional(Schema.String),
|
|
203
|
+
/** ISO 8601 timestamp when the content was fetched. */
|
|
204
|
+
fetchedAt: Schema.String,
|
|
205
|
+
}).pipe(Annotation.HiddenAnnotation.set(true)),
|
|
206
|
+
) {}
|
|
207
|
+
|
|
208
|
+
//
|
|
209
|
+
// Per-Post state, keyed by Post entity id (Posts are immutable queue items). Read marker lives in
|
|
210
|
+
// `postState`; star/archive are system Tags indexed in `tags`. Shared across magazines.
|
|
211
|
+
//
|
|
212
|
+
|
|
213
|
+
/** ISO timestamp the Post was first opened, or undefined. */
|
|
214
|
+
export const getReadAt = (
|
|
215
|
+
subscription: Subscription | Obj.Snapshot<Subscription> | undefined,
|
|
216
|
+
postId: EntityId,
|
|
217
|
+
): string | undefined => {
|
|
218
|
+
const stateMap = subscription?.postState.target;
|
|
219
|
+
return stateMap ? StateMap.bind<PostState>(stateMap).get(postId).readAt : undefined;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
/** Sets/clears the Post's read marker. */
|
|
223
|
+
export const setReadAt = (subscription: Subscription, postId: EntityId, value: string | undefined): void => {
|
|
224
|
+
const stateMap = subscription.postState.target;
|
|
225
|
+
if (stateMap) {
|
|
226
|
+
StateMap.bind<PostState>(stateMap).patch(postId, { readAt: value });
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
/** Resolves the uri of an existing system Tag without creating one; `undefined` when absent. Async. */
|
|
231
|
+
export const findSystemTagUri = async (
|
|
232
|
+
db: Pick<Database.Database, 'query'>,
|
|
233
|
+
which: SystemTag,
|
|
234
|
+
): Promise<string | undefined> => {
|
|
235
|
+
// Prefer the current key; fall back to the legacy 'org.dxos.plugin.feed' source so
|
|
236
|
+
// starred/archived tags created before the plugin-feed → plugin-magazine rename still resolve.
|
|
237
|
+
// Two separate queries ensure deterministic priority when both tags co-exist.
|
|
238
|
+
const [currentTag] = await db.query(Filter.foreignKeys(Tag.Tag, [SYSTEM_TAGS[which].key])).run();
|
|
239
|
+
if (currentTag) {
|
|
240
|
+
return Obj.getURI(currentTag).toString();
|
|
241
|
+
}
|
|
242
|
+
const legacyKey = { source: 'org.dxos.plugin.feed', id: SYSTEM_TAGS[which].key.id };
|
|
243
|
+
const [legacyTag] = await db.query(Filter.foreignKeys(Tag.Tag, [legacyKey])).run();
|
|
244
|
+
return legacyTag ? Obj.getURI(legacyTag).toString() : undefined;
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
/** Whether a Post carries the given (resolved) tag uri on its Subscription. Pure. */
|
|
248
|
+
export const hasTag = (
|
|
249
|
+
subscription: Subscription | Obj.Snapshot<Subscription> | undefined,
|
|
250
|
+
postId: EntityId,
|
|
251
|
+
tagUri: string | undefined,
|
|
252
|
+
): boolean => {
|
|
253
|
+
const tagIndex = subscription?.tags.target;
|
|
254
|
+
return !!(tagIndex && tagUri) && TagIndex.bind(tagIndex).objects(tagUri).includes(postId);
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
/** Sets/clears a system tag (`starred`/`archived`) on a Post (find-or-creates the Tag object). Async. */
|
|
258
|
+
export const setTag = async (
|
|
259
|
+
subscription: Subscription,
|
|
260
|
+
postId: EntityId,
|
|
261
|
+
db: Pick<Database.Database, 'query' | 'add'>,
|
|
262
|
+
which: SystemTag,
|
|
263
|
+
value: boolean,
|
|
264
|
+
): Promise<void> => {
|
|
265
|
+
const tag = await Tag.findOrCreate(db, SYSTEM_TAGS[which]);
|
|
266
|
+
const uri = Obj.getURI(tag).toString();
|
|
267
|
+
const tagIndex = subscription.tags.target;
|
|
268
|
+
if (!tagIndex) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const tags = TagIndex.bind(tagIndex);
|
|
272
|
+
if (value) {
|
|
273
|
+
tags.setTag(uri, postId);
|
|
274
|
+
} else {
|
|
275
|
+
tags.unsetTag(uri, postId);
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
//
|
|
280
|
+
// Content feed (fetched article bodies; carries refined snippet/imageUrl).
|
|
281
|
+
//
|
|
282
|
+
|
|
283
|
+
/** Resolves a Post's source Subscription synchronously if its `source` ref is loaded, else undefined. */
|
|
284
|
+
export const getPostSubscription = (post: Post): Subscription | undefined => post.source?.target;
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Query for {@link PostContent} entries referencing a Post via the reverse-ref index.
|
|
288
|
+
* Scoped to the Subscription's post and content feeds.
|
|
289
|
+
*/
|
|
290
|
+
export const queryPostContentForPost = (
|
|
291
|
+
subscription: Subscription | Obj.Snapshot<Subscription>,
|
|
292
|
+
post: Post | Obj.Snapshot<Post>,
|
|
293
|
+
): Query.Any | undefined => {
|
|
294
|
+
const postFeed = subscription.feed?.target;
|
|
295
|
+
const contentFeed = subscription.contentFeed?.target;
|
|
296
|
+
if (!postFeed || !contentFeed) {
|
|
297
|
+
return undefined;
|
|
298
|
+
}
|
|
299
|
+
const postFeedUri = Obj.getURI(postFeed);
|
|
300
|
+
const contentFeedUri = Obj.getURI(contentFeed);
|
|
301
|
+
return Query.select(Filter.id(post.id))
|
|
302
|
+
.referencedBy(PostContent, 'post')
|
|
303
|
+
.from(Scope.feed(postFeedUri), Scope.feed(contentFeedUri));
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
/** Pick the newest PostContent when multiple entries reference the same Post. */
|
|
307
|
+
export const pickLatestPostContent = (entries: readonly PostContent[]): PostContent | undefined => {
|
|
308
|
+
if (entries.length === 0) {
|
|
309
|
+
return undefined;
|
|
310
|
+
}
|
|
311
|
+
return [...entries].sort((entryA, entryB) => entryB.fetchedAt.localeCompare(entryA.fetchedAt))[0];
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
/** Looks up PostContent for a Post (newest wins); undefined if absent. */
|
|
315
|
+
export const findPostContent = async (
|
|
316
|
+
subscription: Subscription | Obj.Snapshot<Subscription>,
|
|
317
|
+
post: Post | Obj.Snapshot<Post>,
|
|
318
|
+
): Promise<PostContent | undefined> => {
|
|
319
|
+
const db = Obj.getDatabase(subscription);
|
|
320
|
+
const query = queryPostContentForPost(subscription, post);
|
|
321
|
+
if (!db || !query) {
|
|
322
|
+
return undefined;
|
|
323
|
+
}
|
|
324
|
+
return pickLatestPostContent(await db.query(query).run());
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
/** Returns the Subscription's `contentFeed`, lazily creating and attaching one when missing. */
|
|
328
|
+
const ensureContentFeed = (subscription: Subscription): Feed.Feed => {
|
|
329
|
+
const existing = subscription.contentFeed?.target;
|
|
330
|
+
if (existing) {
|
|
331
|
+
return existing;
|
|
332
|
+
}
|
|
333
|
+
const created = Feed.make();
|
|
334
|
+
Obj.update(subscription, (subscription) => {
|
|
335
|
+
const mutable = subscription as Obj.Mutable<typeof subscription>;
|
|
336
|
+
mutable.contentFeed = Ref.make(created);
|
|
337
|
+
});
|
|
338
|
+
Obj.setParent(created, subscription);
|
|
339
|
+
return created;
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Appends a {@link PostContent} entry (body + refined snippet/imageUrl) to a Subscription's
|
|
344
|
+
* `contentFeed`, lazily creating the feed on first use.
|
|
345
|
+
*/
|
|
346
|
+
export const appendPostContent = async (
|
|
347
|
+
space: Pick<Space, 'db'>,
|
|
348
|
+
subscription: Subscription,
|
|
349
|
+
entry: {
|
|
350
|
+
post: Post | Obj.Snapshot<Post>;
|
|
351
|
+
text: string;
|
|
352
|
+
snippet?: string;
|
|
353
|
+
imageUrl?: string;
|
|
354
|
+
fetchedAt?: string;
|
|
355
|
+
},
|
|
356
|
+
): Promise<void> => {
|
|
357
|
+
const echoFeed = ensureContentFeed(subscription);
|
|
358
|
+
const content = Obj.make(PostContent, {
|
|
359
|
+
post: Ref.fromURI(Obj.getURI(entry.post)),
|
|
360
|
+
text: entry.text,
|
|
361
|
+
...(entry.snippet ? { snippet: entry.snippet } : {}),
|
|
362
|
+
...(entry.imageUrl ? { imageUrl: entry.imageUrl } : {}),
|
|
363
|
+
fetchedAt: entry.fetchedAt ?? new Date().toISOString(),
|
|
364
|
+
});
|
|
365
|
+
await Feed.append(echoFeed, [content]).pipe(Effect.provide(Database.layer(space.db)), EffectEx.runAndForwardErrors);
|
|
366
|
+
};
|
package/src/util/date.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
/** Format an ISO 8601 date string for tile/reader display. Returns undefined for missing/invalid input. */
|
|
6
|
+
export const formatDate = (iso: string | undefined): string | undefined => {
|
|
7
|
+
if (!iso) {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
const date = new Date(iso);
|
|
11
|
+
if (Number.isNaN(date.getTime())) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
return date.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Sortable timestamp from an ISO date string (e.g. a Post's `published`); missing/unparseable
|
|
19
|
+
* values sort last (newest-first sorts treat them as `-Infinity`).
|
|
20
|
+
*/
|
|
21
|
+
export const publishedTimestamp = (published: string | undefined): number => {
|
|
22
|
+
if (!published) {
|
|
23
|
+
return Number.NEGATIVE_INFINITY;
|
|
24
|
+
}
|
|
25
|
+
const ms = Date.parse(published);
|
|
26
|
+
return Number.isNaN(ms) ? Number.NEGATIVE_INFINITY : ms;
|
|
27
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Subscription } from '../types';
|
|
6
|
+
import { extractImageUrls, makeSnippet, stripHtml } from './text';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Pure derivations of a Post's display snippet/hero-image, preferring the refined values on a
|
|
10
|
+
* fetched {@link Subscription.PostContent} entry when present, else derived from `post.description`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Plain-text snippet: the refined contentFeed value if loaded, else derived from `post.description`. */
|
|
14
|
+
export const getSnippet = (post: { description?: string }, content?: Subscription.PostContent): string =>
|
|
15
|
+
content?.snippet ?? makeSnippet(stripHtml(post.description ?? ''));
|
|
16
|
+
|
|
17
|
+
/** Hero image url: the refined contentFeed value if loaded, else derived from `post.description`. */
|
|
18
|
+
export const getImageUrl = (post: { description?: string }, content?: Subscription.PostContent): string | undefined =>
|
|
19
|
+
content?.imageUrl ?? extractImageUrls(post.description ?? '')[0];
|