From: xangelo Date: Thu, 8 Feb 2024 21:01:29 +0000 (-0500) Subject: add reader view X-Git-Url: https://git.xangelo.ca/?a=commitdiff_plain;h=5e7b6f6c7ca3690d26f9fa5b8188a37153657808;p=river.git add reader view --- diff --git a/migrations/20240208181752_reader_app_is_read.ts b/migrations/20240208181752_reader_app_is_read.ts new file mode 100644 index 0000000..28fd1ac --- /dev/null +++ b/migrations/20240208181752_reader_app_is_read.ts @@ -0,0 +1,16 @@ +import type { Knex } from "knex"; + + +export async function up(knex: Knex): Promise { + return knex.schema.alterTable('feed_entry', t => { + t.boolean('is_read'); + }); +} + + +export async function down(knex: Knex): Promise { + return knex.schema.alterTable('feed_entry', t => { + t.dropColumn('is_read'); + }); +} + diff --git a/src/public/index.html b/src/public/index.html index 9076bdd..93b2d5f 100644 --- a/src/public/index.html +++ b/src/public/index.html @@ -9,6 +9,12 @@ +

River of News

diff --git a/src/public/reader.html b/src/public/reader.html new file mode 100644 index 0000000..cfb6cc5 --- /dev/null +++ b/src/public/reader.html @@ -0,0 +1,49 @@ + + + + River + + + + + + + +

+
+

River of News

+

+ An auto-updating list of news +

+
+
+ +
+
+
+
+

+ Another project by xangelo +

+
+ + + diff --git a/src/public/style.css b/src/public/style.css index 50bf413..88541c3 100644 --- a/src/public/style.css +++ b/src/public/style.css @@ -44,6 +44,26 @@ footer { } } +.rss-reader { + .sidebar { + padding-top: 1em; + } + .feed-entry { + padding: 0.5rem 1rem; + .excerpt { + margin-bottom: 2rem; + } + } + .feed-entry.unread { + border: var(--border); + border-width: 0 0 0 5px; + } + + .feed-entry:hover { + background-color: var(--clight); + } +} + @media (prefers-color-scheme: dark) { :root { /* foreground | background color */ diff --git a/src/server.ts b/src/server.ts index 3ac2371..e82719a 100644 --- a/src/server.ts +++ b/src/server.ts @@ -3,8 +3,9 @@ import express from 'express'; import knex from 'knex'; import { join } from 'path'; import Parser from 'rss-parser'; -import { FeedEntrySchema, FeedSchema, FeedWithEntrySchema } from './types'; -import { renderFeedItem, renderFeedItemHeader } from './views'; +import { FeedEntrySchema, FeedSchema, FeedSchemaWithUnread, FeedWithEntrySchema } from './types'; +import { renderFeedItem, renderFeedItemHeader, renderReaderAppFeedEntries, renderReaderAppFeedEntry, renderReaderAppFeedList } from './views'; +import { createHash } from 'crypto'; const db = knex({ client: 'better-sqlite3', @@ -72,8 +73,10 @@ async function queryFeeds() { const data = await parser.parseURL(feed.url); const items: FeedEntrySchema[] = data.items.map(item => { + const id = createHash('sha256'); + id.update(item.guid || item.id || Date.now()+Math.random()); return { - id: item.guid || item.id, + id: id.digest('hex'), title: item.title, link: item.link, feed_id: feed.id, @@ -81,6 +84,7 @@ async function queryFeeds() { author: item.creator || item.author, created_at: timestamp(now), updated_at: timestamp(now), + is_read: false, meta: { comment_link: item.comments, snippet: contentExtractor(feed, item) @@ -201,7 +205,42 @@ app.post('/feeds', async (req, res) => { }); app.get('/feeds', async (req, res) => { - res.json(await db.select('*').from('feeds').orderBy('created_at')); + const feedList: FeedSchemaWithUnread[] = await db.raw(` +select f.*, count(fe.id) as unread +from feeds f +join feed_entry fe on fe.feed_id = f.id +where fe.is_read is false +group by fe.feed_id +`); + + if(req.accepts('html')) { + res.send(renderReaderAppFeedList(feedList)); + return; + } + + res.json(feedList); +}); + +app.get('/feeds/:feed_id', async (req, res) => { + const feedEntries = await db.select('*').from('feed_entry').where({ + feed_id: req.params.feed_id + }).orderBy('pub_date', 'desc').limit(100); + + if(req.accepts('html')) { + res.send(renderReaderAppFeedEntries(feedEntries)) + return; + } + res.json(feedEntries); +}); + +app.post('/feed_entry/:feed_entry_id', async (req, res) => { + const item: FeedWithEntrySchema[] = await db('feed_entry').update({ + is_read: true + }).where({ + id: req.params.feed_entry_id + }).returning('*'); + + res.send(renderReaderAppFeedEntry(item.pop())); }); app.delete('/feeds/:feed_id', async (req, res) => { diff --git a/src/types.ts b/src/types.ts index ca6cc5c..121076f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -10,6 +10,10 @@ export type FeedSchema = { favicon: string; } & KnexTimestamps; +export type FeedSchemaWithUnread = FeedSchema & { + unread: number +} + export type FeedEntrySchema = { id: string; feed_id: number; @@ -17,6 +21,7 @@ export type FeedEntrySchema = { link: string; pub_date: number; author: string; + is_read: boolean; meta: T; } & KnexTimestamps; diff --git a/src/views.ts b/src/views.ts index f456ad5..2a88a8b 100644 --- a/src/views.ts +++ b/src/views.ts @@ -1,5 +1,5 @@ import { fuzzyTime } from "./time"; -import { FeedWithEntrySchema } from "./types"; +import { FeedSchema, FeedSchemaWithUnread, FeedWithEntrySchema } from "./types"; export function renderFeedItemHeader(entry: FeedWithEntrySchema): string { return ` @@ -24,3 +24,40 @@ export function renderFeedItem(entry: FeedWithEntrySchema): string { ` } + + +export function renderReaderAppFeedListItem(feed: FeedSchemaWithUnread, autoload: boolean = false): string { + return ` +${feed.title} (${feed.unread}) + `; + +} + +export function renderReaderAppFeedList(list: FeedSchemaWithUnread[]): string { + return list.map((feed, idx) => { + return renderReaderAppFeedListItem(feed, idx === 0); + }).join("
"); +} + +export function renderReaderAppFeedEntry(entry: FeedWithEntrySchema): string { + const meta = JSON.parse(entry.meta); + const date = new Date(entry.pub_date * 1000); + return ` +
+
+ ${entry.title} + + ${fuzzyTime(entry.pub_date * 1000)} + +
+ ${meta.snippet ? `
${meta.snippet}
` : ''} + +
+ + `; + +} + +export function renderReaderAppFeedEntries(list: FeedWithEntrySchema[]): string { + return list.map(renderReaderAppFeedEntry).join("\n"); +}