re-add subscriptions

This commit is contained in:
Lera Elvoé 2025-07-01 23:20:36 +03:00
parent 52f6484db1
commit 29bb9872d3
Signed by: yagich
SSH Key Fingerprint: SHA256:6xjGb6uA7lAVcULa7byPEN//rQ0wPoG+UzYVMfZnbvc
7 changed files with 210 additions and 10 deletions

View File

@ -105,4 +105,11 @@ def create_app():
def ts_datetime(ts, format): def ts_datetime(ts, format):
return datetime.utcfromtimestamp(ts or int(time.time())).strftime(format) return datetime.utcfromtimestamp(ts or int(time.time())).strftime(format)
@app.template_filter("pluralize")
def pluralize(number, singular = "", plural = "s"):
if number == 1:
return singular
return plural
return app return app

View File

@ -56,6 +56,18 @@ class Users(Model):
WHERE users.id = ?""" WHERE users.id = ?"""
return db.fetch_one(q, self.id) return db.fetch_one(q, self.id)
def get_all_subscriptions(self):
q = """
SELECT threads.title AS thread_title, threads.slug AS thread_slug
FROM
threads
JOIN
subscriptions ON subscriptions.thread_id = threads.id
WHERE
subscriptions.user_id = ?"""
return db.query(q, self.id)
class Topics(Model): class Topics(Model):
table = "topics" table = "topics"

View File

@ -3,7 +3,7 @@ from flask import (
) )
from .users import login_required, mod_only, get_active_user, is_logged_in from .users import login_required, mod_only, get_active_user, is_logged_in
from ..db import db from ..db import db
from ..models import Threads, Topics, Posts from ..models import Threads, Topics, Posts, Subscriptions
from .posts import create_post from .posts import create_post
from slugify import slugify from slugify import slugify
import math import math
@ -39,7 +39,17 @@ def thread(slug):
topic = Topics.find({"id": thread.topic_id}) topic = Topics.find({"id": thread.topic_id})
other_topics = Topics.select() other_topics = Topics.select()
#TODO: subscription last seen is_subscribed = False
if is_logged_in():
subscription = Subscriptions.find({
'thread_id': thread.id,
'user_id': get_active_user().id,
})
if subscription:
subscription.update({
'last_seen': int(time.time())
})
is_subscribed = True
return render_template( return render_template(
"threads/thread.html", "threads/thread.html",
@ -49,6 +59,7 @@ def thread(slug):
posts = posts, posts = posts,
topic = topic, topic = topic,
topics = other_topics, topics = other_topics,
is_subscribed = is_subscribed,
) )
@ -122,3 +133,39 @@ def sticky(slug):
@mod_only(".thread", slug = lambda slug: slug) @mod_only(".thread", slug = lambda slug: slug)
def move(slug): def move(slug):
pass pass
@bp.post("/<slug>/subscribe")
@login_required
def subscribe(slug):
user = get_active_user()
thread = Threads.find({'slug': slug})
if not thread:
return 'no'
subscription = Subscriptions.find({
'user_id': user.id,
'thread_id': thread.id,
})
if request.form['subscribe'] == 'subscribe':
if subscription:
subscription.delete()
Subscriptions.create({
'user_id': user.id,
'thread_id': thread.id,
'last_seen': int(time.time()),
})
elif request.form['subscribe'] == 'unsubscribe':
if not subscription:
return 'no'
subscription.delete()
elif request.form['subscribe'] == 'read':
if not subscription:
return 'no'
subscription.update({
'last_seen': int(time.time())
})
last_visible_post = request.form.get('last_visible_post', default=None)
if last_visible_post is not None:
return redirect(url_for('.thread', slug=thread.slug, after=last_visible_post))
else:
return redirect(url_for('users.inbox', username=user.username))

View File

@ -2,7 +2,8 @@ from flask import (
Blueprint, render_template, request, redirect, url_for, flash, session, current_app Blueprint, render_template, request, redirect, url_for, flash, session, current_app
) )
from functools import wraps from functools import wraps
from ..models import Users, Sessions from ..db import db
from ..models import Users, Sessions, Subscriptions
from ..constants import InfoboxKind, PermissionLevel from ..constants import InfoboxKind, PermissionLevel
from ..auth import digest, verify from ..auth import digest, verify
import secrets import secrets
@ -202,12 +203,6 @@ def settings(username):
return "stub" return "stub"
@bp.get("/<username>/inbox")
@login_required
def inbox(username):
return "stub"
@bp.post("/log_out") @bp.post("/log_out")
@login_required @login_required
def log_out(): def log_out():
@ -282,3 +277,86 @@ def guest_user(user_id):
"permission": PermissionLevel.GUEST.value, "permission": PermissionLevel.GUEST.value,
}) })
return redirect(url_for(".page", username=target_user.username)) return redirect(url_for(".page", username=target_user.username))
@bp.get("/<username>/inbox")
@login_required
def inbox(username):
user = get_active_user()
if username != user.username:
return redirect(url_for(".inbox", username = user.username))
new_posts = []
subscription = Subscriptions.find({"user_id": user.id})
all_subscriptions = None
total_unreads_count = None
if subscription:
all_subscriptions = user.get_all_subscriptions()
q = """
WITH thread_metadata AS (
SELECT
posts.thread_id, threads.slug AS thread_slug, threads.title AS thread_title, COUNT(*) AS unread_count, MAX(posts.created_at) AS newest_post_time
FROM
posts
LEFT JOIN
threads ON threads.id = posts.thread_id
LEFT JOIN
subscriptions ON subscriptions.thread_id = posts.thread_id
WHERE subscriptions.user_id = ? AND posts.created_at > subscriptions.last_seen
GROUP BY posts.thread_id
)
SELECT
tm.thread_id, tm.thread_slug, tm.thread_title, tm.unread_count, tm.newest_post_time,
posts.id, posts.created_at, post_history.content, post_history.edited_at, users.username, users.status, avatars.file_path AS avatar_path, posts.thread_id, users.id AS user_id, post_history.original_markup, users.signature_rendered
FROM
thread_metadata tm
JOIN
posts ON posts.thread_id = tm.thread_id
JOIN
post_history ON posts.current_revision_id = post_history.id
JOIN
users ON posts.user_id = users.id
LEFT JOIN
threads ON threads.id = posts.thread_id
LEFT JOIN
avatars ON users.avatar_id = avatars.id
LEFT JOIN
subscriptions ON subscriptions.thread_id = posts.thread_id
WHERE
subscriptions.user_id = ? AND posts.created_at > subscriptions.last_seen
ORDER BY
tm.newest_post_time DESC, posts.created_at ASC"""
new_posts_raw = db.query(q, user.id, user.id)
current_thread_id = None
current_thread_group = None
total_unreads_count = 0
for row in new_posts_raw:
if row['thread_id'] != current_thread_id:
current_thread_group = {
'thread_id': row['thread_id'],
'thread_title': row['thread_title'],
'unread_count': row['unread_count'],
'thread_slug': row['thread_slug'],
'newest_post_time': row['newest_post_time'],
'posts': [],
}
total_unreads_count += int(row['unread_count'])
new_posts.append(current_thread_group)
current_thread_id = row['thread_id']
current_thread_group['posts'].append({
'id': row['id'],
'created_at': row['created_at'],
'content': row['content'],
'edited_at': row['edited_at'],
'username': row['username'],
'status': row['status'],
'avatar_path': row['avatar_path'],
'thread_id': row['thread_id'],
'user_id': row['user_id'],
'original_markup': row['original_markup'],
'signature_rendered': row['signature_rendered']
})
return render_template("users/inbox.html", new_posts = new_posts, total_unreads_count = total_unreads_count, all_subscriptions = all_subscriptions)

View File

@ -89,7 +89,7 @@
</form> </form>
{% endmacro %} {% endmacro %}
{% macro full_post(post, render_sig = True, is_latest = False, editing = False, active_user = None) %} {% macro full_post(post, render_sig = True, is_latest = False, editing = False, active_user = None, no_reply = false) %}
{% set postclass = "post" %} {% set postclass = "post" %}
{% if editing %} {% if editing %}
{% set postclass = postclass + " editing" %} {% set postclass = postclass + " editing" %}

View File

@ -20,6 +20,11 @@
</span> </span>
<div> <div>
{% if can_subscribe %} {% if can_subscribe %}
<form class="modform" action="{{ url_for('threads.subscribe', slug=thread.slug) }}" method="post">
<input type='hidden' name='last_visible_post' value='{{posts[-1].id}}'>
<input type='hidden' name='subscribe' value='{{ 'unsubscribe' if is_subscribed else 'subscribe' }}'>
<input type='submit' value='{{ 'Unsubscribe' if is_subscribed else 'Subscribe' }}'>
</form>
{% endif %} {% endif %}
{% if can_lock %} {% if can_lock %}
<form class="modform" action="{{ url_for("threads.lock", slug=thread.slug) }}" method="post"> <form class="modform" action="{{ url_for("threads.lock", slug=thread.slug) }}" method="post">

View File

@ -0,0 +1,51 @@
{% from "common/macros.html" import timestamp, full_post %}
{% extends "base.html" %}
{% block title %}inbox{% endblock %}
{% block content %}
<div class="inbox-container">
{% if all_subscriptions is none %}
You have no subscriptions.<br>
{% else %}
Your subscriptions:
<ul>
{% for sub in all_subscriptions %}
<li>
<a href=" {{ url_for("threads.thread", slug=sub.thread_slug) }} ">{{ sub.thread_title }}</a>
<form class="modform" method="post" action="{{ url_for("threads.subscribe", slug = sub.thread_slug) }}">
<input type="hidden" name="subscribe" value="unsubscribe">
<input class="warn" type="submit" value="Unsubscribe">
</form>
</li>
{% endfor %}
</ul>
{% endif %}
{% if not new_posts %}
You have no unread posts.
{% else %}
You have {{ total_unreads_count }} unread post{{(total_unreads_count | int) | pluralize }}:
{% for thread in new_posts %}
<div class="accordion">
<div class="accordion-header">
<button type="button" class="accordion-toggle"></button>
{% set latest_post_id = thread.posts[-1].id %}
{% set unread_posts_text = " (" + (thread.unread_count | string) + (" unread post" | pluralize) %}
<a class="accordion-title" href="{{ url_for("threads.thread", slug=latest_post_slug, after=latest_post_id, _anchor="post-" + (latest_post_id | string)) }}" title="Jump to latest post">{{thread.thread_title + unread_posts_text}}, latest at {{ timestamp(thread.newest_post_time) }})</a>
<form class="modform" method="post" action="{{ url_for("threads.subscribe", slug = thread.thread_slug) }}">
<input type="hidden" name="subscribe" value="read">
<input type="submit" value="Mark thread as Read">
</form>
<form class="modform" method="post" action="{{ url_for("threads.subscribe", slug = thread.thread_slug) }}">
<input type="hidden" name="subscribe" value="unsubscribe">
<input class="warn" type="submit" value="Unsubscribe">
</form>
</div>
<div class="accordion-content">
{% for post in thread.posts %}
{{ full_post(post, no_reply = true) }}
{% endfor %}
</div>
</div>
{% endfor %}
{% endif %}
</div>
{% endblock %}