v0.6.2: order sources by most recent message (recency)
- Sidebar sources ordered by Max(created_at) desc instead of alphabetically; "All sources" stays pinned at top (rendered separately). - Live updates move a source to the top when it gets a new message, so the recency order holds without a reload. 152 tests pass; JS re-validated with node --check. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
ab3e92268a
commit
c1be2face7
4 changed files with 31 additions and 15 deletions
10
CHANGELOG.md
10
CHANGELOG.md
|
|
@ -9,6 +9,13 @@ former name, "Daybook".)
|
||||||
|
|
||||||
_Nothing yet._
|
_Nothing yet._
|
||||||
|
|
||||||
|
## [0.6.2] — 2026-09-07
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Source sidebar is now ordered by **most recent message** (Telegram-style)
|
||||||
|
instead of alphabetically; "All sources" stays pinned at the top. Live
|
||||||
|
updates move a source to the top when it receives a new message.
|
||||||
|
|
||||||
## [0.6.1] — 2026-09-07
|
## [0.6.1] — 2026-09-07
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
@ -214,7 +221,8 @@ First tagged release.
|
||||||
- Dockerised (Gunicorn + whitenoise), SQLite, `git`-based deploy with a
|
- Dockerised (Gunicorn + whitenoise), SQLite, `git`-based deploy with a
|
||||||
runbook (`deploy/cloud_agent_deploy.md`) and health check.
|
runbook (`deploy/cloud_agent_deploy.md`) and health check.
|
||||||
|
|
||||||
[Unreleased]: https://git.cygnus.systems/peter/daybook/compare/v0.6.1...HEAD
|
[Unreleased]: https://git.cygnus.systems/peter/daybook/compare/v0.6.2...HEAD
|
||||||
|
[0.6.2]: https://git.cygnus.systems/peter/daybook/compare/v0.6.1...v0.6.2
|
||||||
[0.6.1]: https://git.cygnus.systems/peter/daybook/compare/v0.6.0...v0.6.1
|
[0.6.1]: https://git.cygnus.systems/peter/daybook/compare/v0.6.0...v0.6.1
|
||||||
[0.6.0]: https://git.cygnus.systems/peter/daybook/compare/v0.5.1...v0.6.0
|
[0.6.0]: https://git.cygnus.systems/peter/daybook/compare/v0.5.1...v0.6.0
|
||||||
[0.5.1]: https://git.cygnus.systems/peter/daybook/compare/v0.5.0...v0.5.1
|
[0.5.1]: https://git.cygnus.systems/peter/daybook/compare/v0.5.0...v0.5.1
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ from django.contrib.auth import views as auth_views
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.db import OperationalError, transaction
|
from django.db import OperationalError, transaction
|
||||||
from django.db.models import Count, Q, Sum
|
from django.db.models import Count, Max, Q, Sum
|
||||||
from django.http import FileResponse, Http404, HttpResponse, HttpResponseForbidden, JsonResponse
|
from django.http import FileResponse, Http404, HttpResponse, HttpResponseForbidden, JsonResponse
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.template.defaultfilters import date as date_filter, slugify
|
from django.template.defaultfilters import date as date_filter, slugify
|
||||||
|
|
@ -586,10 +586,12 @@ def feed(request):
|
||||||
source_scoped = apply_feed_filters(
|
source_scoped = apply_feed_filters(
|
||||||
live, "", selected_label, selected_kind, title_query
|
live, "", selected_label, selected_kind, title_query
|
||||||
)
|
)
|
||||||
|
# Sources ordered by most recent message first (Telegram-style); "All
|
||||||
|
# sources" is rendered separately and stays pinned at the top.
|
||||||
source_counts = (
|
source_counts = (
|
||||||
source_scoped.values("source")
|
source_scoped.values("source")
|
||||||
.annotate(count=Count("id", filter=Q(read_at__isnull=True)))
|
.annotate(count=Count("id", filter=Q(read_at__isnull=True)), latest=Max("created_at"))
|
||||||
.order_by("source")
|
.order_by("-latest")
|
||||||
)
|
)
|
||||||
keep_count = live.filter(is_kept=True).count()
|
keep_count = live.filter(is_kept=True).count()
|
||||||
trash_count = Artifact.objects.filter(owner=request.user, deleted_at__isnull=False).count()
|
trash_count = Artifact.objects.filter(owner=request.user, deleted_at__isnull=False).count()
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# Single source of truth for the app version. Bump this, commit, then tag the
|
# Single source of truth for the app version. Bump this, commit, then tag the
|
||||||
# commit `vX.Y.Z` and deploy. Surfaced on Settings -> About and in
|
# commit `vX.Y.Z` and deploy. Surfaced on Settings -> About and in
|
||||||
# GET /api/capabilities ("version").
|
# GET /api/capabilities ("version").
|
||||||
__version__ = "0.6.1"
|
__version__ = "0.6.2"
|
||||||
|
|
|
||||||
|
|
@ -443,15 +443,17 @@
|
||||||
`<span class="source-name">${escapeHtml(name)}</span>` +
|
`<span class="source-name">${escapeHtml(name)}</span>` +
|
||||||
`<span class="source-count">${count}</span>`;
|
`<span class="source-count">${count}</span>`;
|
||||||
|
|
||||||
// Insert alphabetically among the per-source links (after "All sources").
|
// Newest activity goes to the top of the per-source list (just under
|
||||||
|
// the pinned "All sources"), matching the recency ordering.
|
||||||
const list = document.querySelector(".source-list");
|
const list = document.querySelector(".source-list");
|
||||||
const others = sourceLinks().filter(
|
const firstSource = list.querySelector(".source-link:not(.all-sources-link)");
|
||||||
(l) => l.querySelector(".source-name")?.textContent.trim() !== "All sources",
|
list.insertBefore(link, firstSource);
|
||||||
);
|
};
|
||||||
const after = others.find(
|
|
||||||
(l) => (l.querySelector(".source-name")?.textContent.trim() || "") > name,
|
const moveSourceToTop = (link) => {
|
||||||
);
|
const list = document.querySelector(".source-list");
|
||||||
list.insertBefore(link, after || null);
|
const firstSource = list.querySelector(".source-link:not(.all-sources-link)");
|
||||||
|
if (firstSource !== link) list.insertBefore(link, firstSource);
|
||||||
};
|
};
|
||||||
|
|
||||||
// From the newly-arrived rows: bump each source's unread count (adding a
|
// From the newly-arrived rows: bump each source's unread count (adding a
|
||||||
|
|
@ -466,8 +468,12 @@
|
||||||
Object.entries(counts).forEach(([source, n]) => {
|
Object.entries(counts).forEach(([source, n]) => {
|
||||||
total += n;
|
total += n;
|
||||||
const existing = findSourceLink(source);
|
const existing = findSourceLink(source);
|
||||||
if (existing) bumpSourceBadge(existing, n);
|
if (existing) {
|
||||||
else addSourceLink(source, n);
|
bumpSourceBadge(existing, n);
|
||||||
|
moveSourceToTop(existing); // most-recent activity -> top (recency order)
|
||||||
|
} else {
|
||||||
|
addSourceLink(source, n);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
const allLink = findSourceLink("All sources");
|
const allLink = findSourceLink("All sources");
|
||||||
if (allLink && total) bumpSourceBadge(allLink, total);
|
if (allLink && total) bumpSourceBadge(allLink, total);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue