Skip to content

Commit

Permalink
feat: articles filter
Browse files Browse the repository at this point in the history
  • Loading branch information
jirikuchta committed May 20, 2024
1 parent c671346 commit be6b9a6
Show file tree
Hide file tree
Showing 10 changed files with 121 additions and 65 deletions.
8 changes: 5 additions & 3 deletions app/api/articles.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ def get_article_or_raise(article_id: int) -> Article:
def list_articles() -> TReturnValue:
subscription_id = request.args.get("subscription_id")
category_id = request.args.get("category_id")
starred_only = "starred_only" in request.args
unread_only = "unread_only" in request.args
starred_only = request.args.get("starred_only") == "true"
unread_only = request.args.get("unread_only") == "true"
sort_by = request.args.get("sort_by", "time_published")
order = request.args.get("order", "desc")
limit = int(request.args.get("limit", 20))
offset = int(request.args.get("offset", 0))

Expand All @@ -47,7 +49,7 @@ def list_articles() -> TReturnValue:

articles = Article.query\
.filter(*filters)\
.order_by(Article.time_published.desc())\
.order_by(getattr(getattr(Article, sort_by), order)())\
.limit(limit)\
.offset(offset)\
.all()
Expand Down
61 changes: 33 additions & 28 deletions static/less/articles.less
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,39 @@ rr-articles {

background-color: var(--color-bg-primary);

> header {
height: 56px;
flex: none;

display: flex;
align-items: center;
gap: 8px;
padding: 0 24px;
background: var(--color-bg-primary);

position: sticky;
top: 0;
z-index: 1;
&.has-shadow { box-shadow: var(--shadow-header); }

h4 {
display: flex;
align-items: center;
gap: 8px;
}

rr-counter {
font-size: 11px;
font-weight: bold;
color: var(--color-grey);
}

button {
margin-left: auto;
font-size: 24px;
}
}

rr-item-articles {
margin: 0 8px 8px 8px;
}
Expand All @@ -25,34 +58,6 @@ rr-articles {
}
}

rr-header-articles {
height: 56px;
flex: none;

display: flex;
align-items: center;
gap: 8px;
padding: 0 24px;
background: var(--color-bg-primary);

position: sticky;
top: 0;
z-index: 1;
&.has-shadow { box-shadow: var(--shadow-header); }

h4 {
display: flex;
align-items: center;
gap: 8px;
}

rr-counter {
font-size: 11px;
font-weight: bold;
color: var(--color-grey);
}
}

rr-item-articles {
--picture-width: 120px;
--v-gap: 4px;
Expand Down
8 changes: 8 additions & 0 deletions static/less/popup.less
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ menu.popup {
line-height: 36px;
cursor: pointer;

&.separator {
border-bottom: 1px solid var(--color-border);
}

&.selected {
font-weight: bold;
}

@media (hover: hover) {
&:hover {
background: var(--color-hover);
Expand Down
1 change: 0 additions & 1 deletion static/ts/data/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ const DEFAULTS: Settings = {
navWidth: "20%",
articlesWidth: "40%",
collapsedCategories: [],
unreadOnly: true,
markAsReadOnScroll: true,
showImages: true,
theme: "system",
Expand Down
3 changes: 2 additions & 1 deletion static/ts/data/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ export interface ArticleFilters {
offset?: number
starred_only?: boolean
unread_only?: boolean
sort_by?: "time_published"
order?: "desc" | "asc"
}

export interface FeedLink {
Expand All @@ -56,7 +58,6 @@ export interface Settings {
navWidth: string;
articlesWidth: string;
collapsedCategories: CategoryId[];
unreadOnly: boolean;
markAsReadOnScroll: boolean;
showImages: boolean;
theme: "system" | "light" | "dark";
Expand Down
74 changes: 49 additions & 25 deletions static/ts/ui/articles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ import * as pubsub from "util/pubsub";
import Swipe from "util/swipe";

import FeedIcon from "ui/widget/feed-icon";
import { PopupMenu } from "ui/widget/popup";

import App from "app";
import Counter from "ui/counter";
import Icon from "ui/icon";

export default class Articles extends HTMLElement {
protected _sorting: "newest" | "oldest" = "newest";
protected _unreadOnly = true;
protected markReadTimeout?: number;
protected showMoreObserver = new IntersectionObserver(
entries => entries[0].isIntersecting && this.buildItems(),
Expand Down Expand Up @@ -68,13 +71,11 @@ export default class Articles extends HTMLElement {
let selectedFeed = this.app.feeds.activeItem;

let filters: types.ArticleFilters = {
offset: items.filter(i => settings.getItem("unreadOnly") ? !i.read : true).length
offset: items.filter(i => this._unreadOnly ? !i.read : true).length,
unread_only: this._unreadOnly,
order: this.sorting == "newest" ? "desc" : "asc"
};

if (settings.getItem("unreadOnly") && !(selectedFeed.type == "starred")) {
filters.unread_only = true;
}

if (selectedFeed.type == "starred") {
filters.starred_only = true;
filters.offset = items.length;
Expand All @@ -92,10 +93,7 @@ export default class Articles extends HTMLElement {
return filters;
}

get activeItem() {
return this.items.find(i => i.active) || null;
}

get activeItem() { return this.items.find(i => i.active) || null; }
set activeItem(item: Item | null) {
this.items.forEach(item => item.active = false);
item = item || this.items[0];
Expand All @@ -106,8 +104,35 @@ export default class Articles extends HTMLElement {
this.app.detail.article = item.data;
}

get sorting() { return this._sorting; }
set sorting(value: "newest" | "oldest") {
this._sorting = value;
this.build();
}

get unreadOnly() { return this._unreadOnly; }
set unreadOnly(value: boolean) {
this._unreadOnly = value;
this.build();
}

protected async build() {
this.replaceChildren(new Header());
let navItem = this.app.feeds.activeItem;

let title = document.createElement("h4");
title.append(navItem.icon, navItem.data.title);

let counter = new Counter();
counter.getCount = () => navItem.unreadCount;

let filters = document.createElement("button");
filters.append(new Icon("filter"));
filters.addEventListener("click", e => showFilters(filters, this));

let header = document.createElement("header");
header.append(title, counter, filters);

this.replaceChildren(header);
this.buildItems();
}

Expand Down Expand Up @@ -145,26 +170,25 @@ export default class Articles extends HTMLElement {

customElements.define("rr-articles", Articles);

class Header extends HTMLElement {
connectedCallback() {
let { app } = this;
let navItem = app.feeds.activeItem;
function showFilters(node: HTMLElement, articles: Articles) {
let menu = new PopupMenu();
let item;

let title = document.createElement("h4");
title.append(navItem.icon, navItem.data.title)
item = menu.addItem("Newest first", "sort-down", () => articles.sorting = "newest");
item.classList.toggle("selected", articles.sorting == "newest");

let counter = new Counter();
counter.getCount = () => navItem.unreadCount;
item = menu.addItem("Oldest first", "sort-up", () => articles.sorting = "oldest");
item.classList.toggle("selected", articles.sorting == "oldest");
item.classList.add("separator");

this.append(title, counter);
}
item = menu.addItem("Unread only", "eye", () => articles.unreadOnly = true);
item.classList.toggle("selected", articles.unreadOnly);

get app() {
return this.closest("rr-app") as App;
}
}
item = menu.addItem("All articles", "eye-fill", () => articles.unreadOnly = false);
item.classList.toggle("selected", !articles.unreadOnly);

customElements.define("rr-header-articles", Header);
menu.open(node, "side", [-8, 8]);
}

const ACTIVE_CSS_CLASS = "is-active";
const READ_CSS_CLASS = "is-read";
Expand Down
9 changes: 7 additions & 2 deletions static/ts/ui/icon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ const ICONS = {
"cup-hot-fill": '<svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M.5 6a.5.5 0 0 0-.488.608l1.652 7.434A2.5 2.5 0 0 0 4.104 16h5.792a2.5 2.5 0 0 0 2.44-1.958l.131-.59a3 3 0 0 0 1.3-5.854l.221-.99A.5.5 0 0 0 13.5 6H.5ZM13 12.5a2.01 2.01 0 0 1-.316-.025l.867-3.898A2.001 2.001 0 0 1 13 12.5Z"/> <path d="m4.4.8-.003.004-.014.019a4.167 4.167 0 0 0-.204.31 2.327 2.327 0 0 0-.141.267c-.026.06-.034.092-.037.103v.004a.593.593 0 0 0 .091.248c.075.133.178.272.308.445l.01.012c.118.158.26.347.37.543.112.2.22.455.22.745 0 .188-.065.368-.119.494a3.31 3.31 0 0 1-.202.388 5.444 5.444 0 0 1-.253.382l-.018.025-.005.008-.002.002A.5.5 0 0 1 3.6 4.2l.003-.004.014-.019a4.149 4.149 0 0 0 .204-.31 2.06 2.06 0 0 0 .141-.267c.026-.06.034-.092.037-.103a.593.593 0 0 0-.09-.252A4.334 4.334 0 0 0 3.6 2.8l-.01-.012a5.099 5.099 0 0 1-.37-.543A1.53 1.53 0 0 1 3 1.5c0-.188.065-.368.119-.494.059-.138.134-.274.202-.388a5.446 5.446 0 0 1 .253-.382l.025-.035A.5.5 0 0 1 4.4.8Zm3 0-.003.004-.014.019a4.167 4.167 0 0 0-.204.31 2.327 2.327 0 0 0-.141.267c-.026.06-.034.092-.037.103v.004a.593.593 0 0 0 .091.248c.075.133.178.272.308.445l.01.012c.118.158.26.347.37.543.112.2.22.455.22.745 0 .188-.065.368-.119.494a3.31 3.31 0 0 1-.202.388 5.444 5.444 0 0 1-.253.382l-.018.025-.005.008-.002.002A.5.5 0 0 1 6.6 4.2l.003-.004.014-.019a4.149 4.149 0 0 0 .204-.31 2.06 2.06 0 0 0 .141-.267c.026-.06.034-.092.037-.103a.593.593 0 0 0-.09-.252A4.334 4.334 0 0 0 6.6 2.8l-.01-.012a5.099 5.099 0 0 1-.37-.543A1.53 1.53 0 0 1 6 1.5c0-.188.065-.368.119-.494.059-.138.134-.274.202-.388a5.446 5.446 0 0 1 .253-.382l.025-.035A.5.5 0 0 1 7.4.8Zm3 0-.003.004-.014.019a4.077 4.077 0 0 0-.204.31 2.337 2.337 0 0 0-.141.267c-.026.06-.034.092-.037.103v.004a.593.593 0 0 0 .091.248c.075.133.178.272.308.445l.01.012c.118.158.26.347.37.543.112.2.22.455.22.745 0 .188-.065.368-.119.494a3.198 3.198 0 0 1-.202.388 5.385 5.385 0 0 1-.252.382l-.019.025-.005.008-.002.002A.5.5 0 0 1 9.6 4.2l.003-.004.014-.019a4.149 4.149 0 0 0 .204-.31 2.06 2.06 0 0 0 .141-.267c.026-.06.034-.092.037-.103a.593.593 0 0 0-.09-.252A4.334 4.334 0 0 0 9.6 2.8l-.01-.012a5.099 5.099 0 0 1-.37-.543A1.53 1.53 0 0 1 9 1.5c0-.188.065-.368.119-.494.059-.138.134-.274.202-.388a5.446 5.446 0 0 1 .253-.382l.025-.035A.5.5 0 0 1 10.4.8Z"/></svg>',
"rss": '<svg viewBox="0 0 16 16"><path d="M2 2.857C2 2.383 2.383 2 2.857 2 9.012 2 14 6.987 14 13.143a.856.856 0 11-1.714 0 9.429 9.429 0 00-9.429-9.429A.856.856 0 012 2.857zm0 9.429a1.714 1.714 0 113.429 0 1.714 1.714 0 11-3.429 0zm.857-6.857a7.713 7.713 0 017.714 7.714.856.856 0 11-1.714 0 6 6 0 00-6-6 .856.856 0 110-1.714z"/></svg>',
"folder-fill": '<svg viewBox="0 0 16 16"><path d="M9.828 3h3.982a2 2 0 0 1 1.992 2.181l-.637 7A2 2 0 0 1 13.174 14H2.825a2 2 0 0 1-1.991-1.819l-.637-7a1.99 1.99 0 0 1 .342-1.31L.5 3a2 2 0 0 1 2-2h3.672a2 2 0 0 1 1.414.586l.828.828A2 2 0 0 0 9.828 3m-8.322.12C1.72 3.042 1.95 3 2.19 3h5.396l-.707-.707A1 1 0 0 0 6.172 2H2.5a1 1 0 0 0-1 .981z"/></svg>',
"font": '<svg viewBox="0 0 16 16"><path d="m2.244 13.081.943-2.803H6.66l.944 2.803H8.86L5.54 3.75H4.322L1 13.081zm2.7-7.923L6.34 9.314H3.51l1.4-4.156zm9.146 7.027h.035v.896h1.128V8.125c0-1.51-1.114-2.345-2.646-2.345-1.736 0-2.59.916-2.666 2.174h1.108c.068-.718.595-1.19 1.517-1.19.971 0 1.518.52 1.518 1.464v.731H12.19c-1.647.007-2.522.8-2.522 2.058 0 1.319.957 2.18 2.345 2.18 1.06 0 1.716-.43 2.078-1.011zm-1.763.035c-.752 0-1.456-.397-1.456-1.244 0-.65.424-1.115 1.408-1.115h1.805v.834c0 .896-.752 1.525-1.757 1.525"/></svg>'
"font": '<svg viewBox="0 0 16 16"><path d="m2.244 13.081.943-2.803H6.66l.944 2.803H8.86L5.54 3.75H4.322L1 13.081zm2.7-7.923L6.34 9.314H3.51l1.4-4.156zm9.146 7.027h.035v.896h1.128V8.125c0-1.51-1.114-2.345-2.646-2.345-1.736 0-2.59.916-2.666 2.174h1.108c.068-.718.595-1.19 1.517-1.19.971 0 1.518.52 1.518 1.464v.731H12.19c-1.647.007-2.522.8-2.522 2.058 0 1.319.957 2.18 2.345 2.18 1.06 0 1.716-.43 2.078-1.011zm-1.763.035c-.752 0-1.456-.397-1.456-1.244 0-.65.424-1.115 1.408-1.115h1.805v.834c0 .896-.752 1.525-1.757 1.525"/></svg>',
"filter": '<svg viewBox="0 0 16 16"><path d="M6 10.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5m-2-3a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5m-2-3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5"/></svg>',
"sort-down": '<svg viewBox="0 0 16 16"><path d="M3.5 2.5a.5.5 0 0 0-1 0v8.793l-1.146-1.147a.5.5 0 0 0-.708.708l2 1.999.007.007a.497.497 0 0 0 .7-.006l2-2a.5.5 0 0 0-.707-.708L3.5 11.293zm3.5 1a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5M7.5 6a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1zm0 3a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1zm0 3a.5.5 0 0 0 0 1h1a.5.5 0 0 0 0-1z"/></svg>',
"sort-up": '<svg viewBox="0 0 16 16"><path d="M3.5 12.5a.5.5 0 0 1-1 0V3.707L1.354 4.854a.5.5 0 1 1-.708-.708l2-1.999.007-.007a.5.5 0 0 1 .7.006l2 2a.5.5 0 1 1-.707.708L3.5 3.707zm3.5-9a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5M7.5 6a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1zm0 3a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1zm0 3a.5.5 0 0 0 0 1h1a.5.5 0 0 0 0-1z"/></svg>',
"eye": '<svg viewBox="0 0 16 16"><path d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8M1.173 8a13 13 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5s3.879 1.168 5.168 2.457A13 13 0 0 1 14.828 8q-.086.13-.195.288c-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5s-3.879-1.168-5.168-2.457A13 13 0 0 1 1.172 8z"/><path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5M4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0"/></svg>',
"eye-fill": '<svg viewBox="0 0 16 16"><path d="M10.5 8a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0"/><path d="M0 8s3-5.5 8-5.5S16 8 16 8s-3 5.5-8 5.5S0 8 0 8m8 3.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7"/></svg>'
}

export type IconName = keyof typeof ICONS;
Expand All @@ -35,4 +40,4 @@ export default class Icon extends HTMLElement {
}
}

customElements.define("rr-icon", Icon);
customElements.define("rr-icon", Icon);
5 changes: 3 additions & 2 deletions static/ts/ui/widget/popup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,10 @@ export class PopupMenu extends Popup {
super(document.createElement("menu"));
}

addItem(title: string, ico: IconName, onClick: Function) {
addItem(title: string, ico: IconName | "", onClick: Function) {
let node = document.createElement("li");
node.append(new Icon(ico), title);
ico && node.append(new Icon(ico));
node.append(title);
node.addEventListener("click", e => {
this.close();
onClick();
Expand Down
1 change: 0 additions & 1 deletion static/ts/ui/widget/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ function buildDisplaySection() {
node.append(title);

let options: Partial<Record<keyof Settings, string>> = {
unreadOnly: "Hide Read Articles",
markAsReadOnScroll: "Auto-Mark As Read On Scroll",
showImages: "Show images in article list"
};
Expand Down
16 changes: 14 additions & 2 deletions tests/test_api_list_articles.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import pytest

def test_ok(client, create_subscription, feed_server):
create_subscription()
Expand Down Expand Up @@ -37,7 +38,7 @@ def test_starred_only_filter(client, create_subscription):
article_id = client.get("/api/articles/").json[0]["id"]
client.patch(f"/api/articles/{article_id}/", json={"starred": True})

res = client.get("/api/articles/?starred_only")
res = client.get("/api/articles/?starred_only=true")
assert res.status_code == 200
assert len(res.json) == 1
assert res.json[0]["id"] == article_id
Expand All @@ -48,7 +49,7 @@ def test_unread_only_filter(client, create_subscription, feed_server):
article_id = client.get("/api/articles/").json[0]["id"]
client.patch(f"/api/articles/{article_id}/", json={"read": True})

res = client.get("/api/articles/?unread_only")
res = client.get("/api/articles/?unread_only=true")
assert res.status_code == 200
assert len(res.json) == len(feed_server.feed.items) - 1

Expand All @@ -68,3 +69,14 @@ def test_offset(client, create_subscription, feed_server):
assert res.status_code == 200
assert len(res.json) == len(feed_server.feed.items) - 1
assert res.json[0]["id"] == all_articles[1]["id"]


@pytest.mark.parametrize("order,index", [("desc", 0), ("asc", -1)])
def test_order(order, index, client, create_subscription, feed_server):
create_subscription()
all_articles = client.get("/api/articles/").json

res = client.get(f"/api/articles/?order={order}")
assert res.status_code == 200
assert len(res.json) == len(feed_server.feed.items)
assert res.json[0]["id"] == all_articles[index]["id"]

0 comments on commit be6b9a6

Please sign in to comment.