diff --git a/app/__init__.py b/app/__init__.py index b6b787d..7c38839 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -105,4 +105,11 @@ def create_app(): def ts_datetime(ts, 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 diff --git a/app/models.py b/app/models.py index 4a0a45a..fb27b6c 100644 --- a/app/models.py +++ b/app/models.py @@ -56,6 +56,18 @@ class Users(Model): WHERE users.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): table = "topics" diff --git a/app/routes/threads.py b/app/routes/threads.py index 06f0fe8..23c4286 100644 --- a/app/routes/threads.py +++ b/app/routes/threads.py @@ -3,7 +3,7 @@ from flask import ( ) from .users import login_required, mod_only, get_active_user, is_logged_in from ..db import db -from ..models import Threads, Topics, Posts +from ..models import Threads, Topics, Posts, Subscriptions from .posts import create_post from slugify import slugify import math @@ -39,7 +39,17 @@ def thread(slug): topic = Topics.find({"id": thread.topic_id}) 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( "threads/thread.html", @@ -49,6 +59,7 @@ def thread(slug): posts = posts, topic = topic, topics = other_topics, + is_subscribed = is_subscribed, ) @@ -122,3 +133,39 @@ def sticky(slug): @mod_only(".thread", slug = lambda slug: slug) def move(slug): pass + + +@bp.post("//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)) diff --git a/app/routes/users.py b/app/routes/users.py index cce6b8b..61fccbb 100644 --- a/app/routes/users.py +++ b/app/routes/users.py @@ -2,7 +2,8 @@ from flask import ( Blueprint, render_template, request, redirect, url_for, flash, session, current_app ) 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 ..auth import digest, verify import secrets @@ -202,12 +203,6 @@ def settings(username): return "stub" -@bp.get("//inbox") -@login_required -def inbox(username): - return "stub" - - @bp.post("/log_out") @login_required def log_out(): @@ -282,3 +277,86 @@ def guest_user(user_id): "permission": PermissionLevel.GUEST.value, }) return redirect(url_for(".page", username=target_user.username)) + + +@bp.get("//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) diff --git a/app/templates/common/macros.html b/app/templates/common/macros.html index 50b7d3f..303879c 100644 --- a/app/templates/common/macros.html +++ b/app/templates/common/macros.html @@ -89,7 +89,7 @@ {% 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" %} {% if editing %} {% set postclass = postclass + " editing" %} diff --git a/app/templates/threads/thread.html b/app/templates/threads/thread.html index 0ba24d8..15bbc34 100644 --- a/app/templates/threads/thread.html +++ b/app/templates/threads/thread.html @@ -20,6 +20,11 @@
{% if can_subscribe %} +
+ + + +
{% endif %} {% if can_lock %}
diff --git a/app/templates/users/inbox.html b/app/templates/users/inbox.html new file mode 100644 index 0000000..3970eaa --- /dev/null +++ b/app/templates/users/inbox.html @@ -0,0 +1,51 @@ +{% from "common/macros.html" import timestamp, full_post %} +{% extends "base.html" %} +{% block title %}inbox{% endblock %} +{% block content %} +
+ {% if all_subscriptions is none %} + You have no subscriptions.
+ {% else %} + Your subscriptions: + + {% 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 %} +
+
+ + {% set latest_post_id = thread.posts[-1].id %} + {% set unread_posts_text = " (" + (thread.unread_count | string) + (" unread post" | pluralize) %} + {{thread.thread_title + unread_posts_text}}, latest at {{ timestamp(thread.newest_post_time) }}) +
+ + +
+
+ + +
+
+
+ {% for post in thread.posts %} + {{ full_post(post, no_reply = true) }} + {% endfor %} +
+
+ {% endfor %} + {% endif %} +
+{% endblock %}