~whbboyd/russet

cd1d10560fc53fa23699b12e765ee1534f2f297c — Will Boyd 4 months ago a5f69f2
more useful feed page
M Cargo.lock => Cargo.lock +135 -1
@@ 40,6 40,21 @@ dependencies = [
]

[[package]]
name = "alloc-no-stdlib"
version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"

[[package]]
name = "alloc-stdlib"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
dependencies = [
 "alloc-no-stdlib",
]

[[package]]
name = "allocator-api2"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 121,6 136,22 @@ dependencies = [
]

[[package]]
name = "async-compression"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07dbbf24db18d609b1462965249abdf49129ccad073ec257da372adc83259c60"
dependencies = [
 "brotli",
 "flate2",
 "futures-core",
 "memchr",
 "pin-project-lite",
 "tokio",
 "zstd",
 "zstd-safe",
]

[[package]]
name = "async-trait"
version = "0.1.80"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 316,6 347,27 @@ dependencies = [
]

[[package]]
name = "brotli"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "125740193d7fee5cc63ab9e16c2fdc4e07c74ba755cc53b327d6ea029e9fc569"
dependencies = [
 "alloc-no-stdlib",
 "alloc-stdlib",
 "brotli-decompressor",
]

[[package]]
name = "brotli-decompressor"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65622a320492e09b5e0ac436b14c54ff68199bac392d0e89a6832c4518eea525"
dependencies = [
 "alloc-no-stdlib",
 "alloc-stdlib",
]

[[package]]
name = "bumpalo"
version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 338,6 390,11 @@ name = "cc"
version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b"
dependencies = [
 "jobserver",
 "libc",
 "once_cell",
]

[[package]]
name = "cfg-if"


@@ 485,6 542,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"

[[package]]
name = "crc32fast"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa"
dependencies = [
 "cfg-if",
]

[[package]]
name = "crossbeam-queue"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 698,6 764,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6"

[[package]]
name = "flate2"
version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae"
dependencies = [
 "crc32fast",
 "miniz_oxide",
]

[[package]]
name = "flume"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 1148,6 1224,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9028f49264629065d057f340a86acb84867925865f73bbf8d47b4d149a7e88b8"

[[package]]
name = "jobserver"
version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e"
dependencies = [
 "libc",
]

[[package]]
name = "js-sys"
version = "0.3.69"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 1879,7 1964,7 @@ dependencies = [

[[package]]
name = "russet"
version = "0.10.1"
version = "0.11.0"
dependencies = [
 "argon2",
 "atom_syndication",


@@ 1901,6 1986,7 @@ dependencies = [
 "tokio",
 "toml",
 "tower",
 "tower-http",
 "tracing",
 "tracing-subscriber",
 "ulid",


@@ 2684,6 2770,26 @@ dependencies = [
]

[[package]]
name = "tower-http"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5"
dependencies = [
 "async-compression",
 "bitflags 2.5.0",
 "bytes",
 "futures-core",
 "http 1.1.0",
 "http-body 1.0.0",
 "http-body-util",
 "pin-project-lite",
 "tokio",
 "tokio-util",
 "tower-layer",
 "tower-service",
]

[[package]]
name = "tower-layer"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 3186,3 3292,31 @@ name = "zeroize"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"

[[package]]
name = "zstd"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d789b1514203a1120ad2429eae43a7bd32b90976a7bb8a05f7ec02fa88cc23a"
dependencies = [
 "zstd-safe",
]

[[package]]
name = "zstd-safe"
version = "7.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cd99b45c6bc03a018c8b8a86025678c87e55526064e38f9df301989dce7ec0a"
dependencies = [
 "zstd-sys",
]

[[package]]
name = "zstd-sys"
version = "2.0.10+zstd.1.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c253a4914af5bafc8fa8c86ee400827e83cf6ec01195ec1f1ed8441bf00d65aa"
dependencies = [
 "cc",
 "pkg-config",
]

M Cargo.toml => Cargo.toml +1 -1
@@ 1,6 1,6 @@
[package]
name = "russet"
version = "0.10.1"
version = "0.11.0"
edition = "2021"
license = "AGPL-3.0"


M src/domain/entries.rs => src/domain/entries.rs +16 -1
@@ 2,7 2,7 @@ use chrono::{ DateTime, TimeDelta, Utc };
use chrono_tz::Tz;
use crate::domain::model::Entry;
use crate::domain::RussetDomainService;
use crate::model::{ EntryId, Pagination, Timestamp, UserId };
use crate::model::{ EntryId, FeedId, Pagination, Timestamp, UserId };
use crate::persistence::model::{ Entry as PersistenceEntry, UserEntry };
use crate::persistence::RussetEntryPersistenceLayer;
use crate::Result;


@@ 35,6 35,21 @@ where Persistence: RussetEntryPersistenceLayer {
			.map(|entry| convert_entry(entry, Some(user_entry), Tz::UTC))?
		)
	}

	pub async fn get_feed_entries(
		&self,
		user_id: &UserId,
		feed_id: &FeedId,
		pagination: &Pagination,
	) -> impl IntoIterator<Item = Result<Entry>> {
		self.persistence
			.get_entries_for_user_feed(user_id, feed_id, pagination)
			.await
			.into_iter()
			.map(|result| result.map(|(entry, user_entry)| convert_entry(entry, user_entry, /*FIXME*/Tz::UTC)))
			.filter(|entry| entry.as_ref().map_or_else(|_| true, |entry| !entry.tombstone))
			.collect::<Vec<Result<Entry>>>()
	}
}

fn convert_entry(entry: PersistenceEntry, user_entry: Option<UserEntry>, tz: Tz) -> Entry {

M src/domain/mod.rs => src/domain/mod.rs +1 -1
@@ 48,7 48,7 @@ where Persistence: std::fmt::Debug {
			.field("persistence", &self.persistence)
			.field("readers", &self.readers)
			.field("pepper", &"<redacted>")
			.field("feed_check_interva", &self.feed_check_interval)
			.field("feed_check_interval", &self.feed_check_interval)
			.field("disable_logins", &self.disable_logins)
			.finish()
	}

M src/http/feed.rs => src/http/feed.rs +18 -4
@@ 1,9 1,9 @@
use axum::extract::{ Path, State };
use axum::extract::{ Form, Path, State };
use axum::http::StatusCode;
use axum::response::{ Html, Redirect };
use crate::domain::model::Feed;
use crate::http::{ AppState, AuthenticatedUser };
use crate::model::FeedId;
use crate::domain::model::{ Entry, Feed };
use crate::http::{ AppState, AuthenticatedUser, PageQuery };
use crate::model::{ FeedId, Pagination };
use crate::persistence::model::User;
use crate::persistence::RussetPersistenceLayer;
use sailfish::TemplateOnce;


@@ 12,7 12,9 @@ use sailfish::TemplateOnce;
#[template(path = "feed.stpl")]
struct FeedPageTemplate<'a> {
	user: Option<&'a User>,
	entries: &'a [Entry],
	feed: &'a Feed,
	page_num: usize,
	page_title: &'a str,
	relative_root: &'a str,
}


@@ 21,14 23,26 @@ pub async fn feed_page<Persistence>(
	Path(feed_id): Path<FeedId>,
	State(state): State<AppState<Persistence>>,
	user: AuthenticatedUser<Persistence>,
	Form(pagination): Form<PageQuery>,
) -> Html<String>
where Persistence: RussetPersistenceLayer {
	let page_num = pagination.page_num.unwrap_or(0);
	let page_size = pagination.page_size.unwrap_or(100);
	let pagination = Pagination { page_num, page_size };
	let feed = state.domain_service.get_feed(&feed_id).await.unwrap();
	let entries = state.domain_service
		.get_feed_entries(&user.user.id, &feed_id, &pagination)
		.await
		.into_iter()
		.filter_map(|entry| entry.ok())
		.collect::<Vec<Entry>>();
	let page_title = format!("Feed - {}", feed.title);
	Html(
		FeedPageTemplate {
			user: Some(&user.user),
			entries: &entries.as_slice(),
			feed: &feed,
			page_num: pagination.page_num,
			page_title: &page_title,
			relative_root: "../",
		}

M templates/feed.stpl => templates/feed.stpl +35 -1
@@ 1,6 1,40 @@
<% include!("head.stpl"); %>
		<p><%= feed.title %></p>
		<p>Feed URL: <a href="<%- feed.url %>"><%= feed.url %></a></p>
		<form><table>
			<thead class="alt"><tr>
				<th scope="col">
					<label for="select-all">All</label>
					<input type="checkbox" name="select-all" />
				</th>
				<th scope="col">Title</th>
				<th scope="col">Date</th>
			</tr></thead>
			<tbody><%
for (i, entry) in entries.iter().enumerate() {
	let mut classes = vec![];
	if i % 2 == 1 { classes.push("alt") };
	if !entry.read { classes.push("unread") };
	let classes = if classes.len() > 0 {
		format!(" class=\"{}\"", classes.join(" "))
	} else {
		"".to_string()
	};
%>
				<tr<%- classes %>>
					<td class="center"><input type="checkbox" name="select-<%= entry.id.to_string() %>" /></td>
					<td><a href="<%- relative_root %>entry/<%- entry.id.to_string() %>"><%= entry.title %></a></td>
					<td class="center"><%= entry.article_date %></td>
				</tr><%
}
%>
			</tbody>
		</table></form>
		<div>Page:
			<% if page_num > 1 { %><a href="<%- relative_root %>?page_num=0">1</a>…<% } %>
			<% if page_num > 0 { %><a href="<%- relative_root %>?page_num=<%- page_num - 1 %>"><%- page_num %></a><% } %>
			<%- page_num + 1 %>
			<a href="<%- relative_root %>?page_num=<%- page_num + 1 %>"><%- page_num + 2 %></a>
		</div>
		<div style="display: flex; justify-content: center;">
			<form action="<%- relative_root %>feed/<%- feed.id.to_string() %>" method="post" class="dialog">
				<div class="controls">

M templates/home.stpl => templates/home.stpl +4 -1
@@ 1,7 1,10 @@
<% include!("head.stpl"); %>
		<form><table>
			<thead class="alt"><tr>
				<th scope="col"><input type="checkbox" name="select-all" /></th>
				<th scope="col">
					<label for="select-all">All</label>
					<input type="checkbox" name="select-all" />
				</th>
				<th scope="col">Title</th>
				<th scope="col">Date</th>
				<th scope="col">Feed</th>