From 4271ee3bcac21fe2ca9b0a55ec68a120fd2fa771 Mon Sep 17 00:00:00 2001 From: rimu <3310831+rimu@users.noreply.github.com> Date: Mon, 16 Oct 2023 21:38:36 +1300 Subject: [PATCH] post replies - more elegant handling of long conversations --- app/community/routes.py | 25 +++-- app/community/util.py | 25 +++++ app/constants.py | 1 + app/static/scss/_mixins.scss | 4 + app/static/structure.css | 21 ++++ app/static/structure.scss | 18 +++ app/static/styles.css | 4 + app/templates/community/_post_full.html | 62 ++++++++++ .../community/continue_discussion.html | 106 ++++++++++++++++++ app/templates/community/post.html | 88 +++------------ 10 files changed, 276 insertions(+), 78 deletions(-) create mode 100644 app/templates/community/_post_full.html create mode 100644 app/templates/community/continue_discussion.html diff --git a/app/community/routes.py b/app/community/routes.py index 281de697..93afd3ae 100644 --- a/app/community/routes.py +++ b/app/community/routes.py @@ -5,10 +5,11 @@ from flask_login import login_user, logout_user, current_user, login_required from flask_babel import _ from sqlalchemy import or_ -from app import db +from app import db, constants from app.activitypub.signature import RsaKeys, HttpSignature from app.community.forms import SearchRemoteCommunity, AddLocalCommunity, CreatePost, NewReplyForm -from app.community.util import search_for_community, community_url_exists, actor_to_community, post_replies +from app.community.util import search_for_community, community_url_exists, actor_to_community, post_replies, \ + get_comment_branch from app.constants import SUBSCRIPTION_MEMBER, SUBSCRIPTION_OWNER, POST_TYPE_LINK, POST_TYPE_ARTICLE, POST_TYPE_IMAGE from app.models import User, Community, CommunityMember, CommunityJoinRequest, CommunityBan, Post, PostReply, \ PostReplyVote @@ -81,7 +82,7 @@ def show_community(community: Community): mod_user_ids = [mod.user_id for mod in mods] mod_list = User.query.filter(User.id.in_(mod_user_ids)).all() - if current_user.ignore_bots: + if current_user.is_anonymous or current_user.ignore_bots: posts = community.posts.filter(Post.from_bot == False).all() else: posts = community.posts @@ -244,7 +245,7 @@ def show_post(post_id: int): else: replies = post_replies(post.id, 'top') return render_template('community/post.html', title=post.title, post=post, is_moderator=is_moderator, - canonical=post.ap_id, form=form, replies=replies) + canonical=post.ap_id, form=form, replies=replies, THREAD_CUTOFF_DEPTH=constants.THREAD_CUTOFF_DEPTH) @bp.route('/comment//', methods=['POST']) @@ -295,8 +296,15 @@ def comment_vote(comment_id, vote_direction): @bp.route('/post//comment/') -def show_comment(post_id, comment_id): - ... +def continue_discussion(post_id, comment_id): + post = Post.query.get_or_404(post_id) + comment = PostReply.query.get_or_404(comment_id) + mods = post.community.moderators() + is_moderator = current_user.is_authenticated and any(mod.user_id == current_user.id for mod in mods) + replies = get_comment_branch(post.id, comment.id, 'top') + + return render_template('community/continue_discussion.html', title=_('Discussing %(title)s', title=post.title), post=post, + is_moderator=is_moderator, comment=comment, replies=replies) @bp.route('/post//comment//reply', methods=['GET', 'POST']) @@ -321,7 +329,10 @@ def add_reply(post_id: int, comment_id: int): flash('Your comment has been added.') # todo: flush cache # todo: federation - return redirect(url_for('community.show_post', post_id=post_id, _anchor=f'comment_{reply.id}')) + if reply.depth <= constants.THREAD_CUTOFF_DEPTH: + return redirect(url_for('community.show_post', post_id=post_id, _anchor=f'comment_{reply.parent_id}')) + else: + return redirect(url_for('community.continue_discussion', post_id=post_id, comment_id=reply.parent_id)) else: return render_template('community/add_reply.html', title=_('Discussing %(title)s', title=post.title), post=post, is_moderator=is_moderator, form=form, comment=comment) diff --git a/app/community/util.py b/app/community/util.py index 1e399170..b733bdd1 100644 --- a/app/community/util.py +++ b/app/community/util.py @@ -101,3 +101,28 @@ def post_replies(post_id: int, sort_by: str, show_first: int = 0) -> List[PostRe parent_comment['replies'].append(comments_dict[comment.id]) return [comment for comment in comments_dict.values() if comment['comment'].parent_id is None] + + +def get_comment_branch(post_id: int, comment_id: int, sort_by: str) -> List[PostReply]: + # Fetch the specified parent comment and its replies + parent_comment = PostReply.query.get(comment_id) + if parent_comment is None: + return [] + + comments = PostReply.query.filter(PostReply.post_id == post_id) + if sort_by == 'hot': + comments = comments.order_by(desc(PostReply.ranking)) + elif sort_by == 'top': + comments = comments.order_by(desc(PostReply.score)) + elif sort_by == 'new': + comments = comments.order_by(desc(PostReply.posted_at)) + + comments_dict = {comment.id: {'comment': comment, 'replies': []} for comment in comments.all()} + + for comment in comments: + if comment.parent_id is not None: + parent_comment = comments_dict.get(comment.parent_id) + if parent_comment: + parent_comment['replies'].append(comments_dict[comment.id]) + + return [comment for comment in comments_dict.values() if comment['comment'].id == comment_id] diff --git a/app/constants.py b/app/constants.py index 1074dd06..531dd5aa 100644 --- a/app/constants.py +++ b/app/constants.py @@ -14,3 +14,4 @@ SUBSCRIPTION_MEMBER = 1 SUBSCRIPTION_NONMEMBER = 0 SUBSCRIPTION_BANNED = -1 +THREAD_CUTOFF_DEPTH = 4 \ No newline at end of file diff --git a/app/static/scss/_mixins.scss b/app/static/scss/_mixins.scss index 2d6910a7..2662a5ce 100644 --- a/app/static/scss/_mixins.scss +++ b/app/static/scss/_mixins.scss @@ -18,3 +18,7 @@ @media (max-width: 576px) { @content ; } } } + +.pl-0 { + padding-left: 0; +} \ No newline at end of file diff --git a/app/static/structure.css b/app/static/structure.css index 1fed54fa..c439e327 100644 --- a/app/static/structure.css +++ b/app/static/structure.css @@ -1,5 +1,9 @@ /* This file contains SCSS used for creating the general structure of pages. Selectors should be things like body, h1, nav, etc which are used site-wide */ +.pl-0 { + padding-left: 0; +} + /* for more info about the feather font used for icons see https://at-ui.github.io/feather-font/ */ /* use https://fontdrop.info/ to get the unicode values of the featuer.ttf file */ @font-face { @@ -362,9 +366,22 @@ fieldset legend { text-decoration: none; } +.comments > .comment { + margin-left: 0; +} + +#replies { + scroll-margin-top: 5em; +} + +.post_replies > .col { + padding-right: 5px; +} + .comment { clear: both; margin-bottom: 20px; + margin-left: 20px; } .comment .limit_height { position: relative; @@ -439,6 +456,10 @@ fieldset legend { .comment .comment_actions a { text-decoration: none; } +.comment .replies { + margin-top: 15px; + border-left: solid 1px #ddd; +} .add_reply .form-control-label { display: none; diff --git a/app/static/structure.scss b/app/static/structure.scss index 3fc43fc9..5b8d5a96 100644 --- a/app/static/structure.scss +++ b/app/static/structure.scss @@ -136,9 +136,22 @@ nav, etc which are used site-wide */ } } +.comments > .comment { + margin-left: 0; +} + +#replies { + scroll-margin-top: 5em; +} + +.post_replies > .col { + padding-right: 5px; +} + .comment { clear: both; margin-bottom: 20px; + margin-left: 20px; .limit_height { position: relative; @@ -231,6 +244,11 @@ nav, etc which are used site-wide */ text-decoration: none; } } + + .replies { + margin-top: 15px; + border-left: solid 1px $light-grey; + } } .add_reply { diff --git a/app/static/styles.css b/app/static/styles.css index 9c3b1355..3f0b9775 100644 --- a/app/static/styles.css +++ b/app/static/styles.css @@ -1,4 +1,8 @@ /* */ +.pl-0 { + padding-left: 0; +} + /* for more info about the feather font used for icons see https://at-ui.github.io/feather-font/ */ /* use https://fontdrop.info/ to get the unicode values of the featuer.ttf file */ @font-face { diff --git a/app/templates/community/_post_full.html b/app/templates/community/_post_full.html new file mode 100644 index 00000000..12c9daa2 --- /dev/null +++ b/app/templates/community/_post_full.html @@ -0,0 +1,62 @@ +
+ {% if post.image_id %} +
+ +

{{ post.title }}

+ {% if post.url %} +

{{ post.url|shorten_url }} + {% if post.type == post_type_link %} + (domain) + {% endif %} +

+ {% endif %} +

submitted {{ moment(post.posted_at).fromNow() }} by {{ post.author.user_name }}

+
+
+ {% if post.url %} + {{ post.image.alt_text }} + {% else %} + {{ post.image.alt_text }} + {% endif %} +
+ {% else %} + +

{{ post.title }}

+ {% if post.url %} +

{{ post.url|shorten_url }} + External link + {% if post.type == post_type_link %} + (domain) + {% endif %} +

+ {% endif %} +

submitted {{ moment(post.posted_at).fromNow() }} by + {{ post.author.user_name }} +

+ {% endif %} +
+ +{% if post.body_html %} +
+
+ {{ post.body_html|safe }} +
+
+
+{% endif %} \ No newline at end of file diff --git a/app/templates/community/continue_discussion.html b/app/templates/community/continue_discussion.html new file mode 100644 index 00000000..85116e84 --- /dev/null +++ b/app/templates/community/continue_discussion.html @@ -0,0 +1,106 @@ +{% extends "base.html" %} +{% from 'bootstrap/form.html' import render_form %} + +{% block app_content %} +
+
+ {% include 'community/_post_full.html' %} +

Back to main discussion

+
+
+ {% macro render_comment(comment) %} +
+
+
+ {% with comment=comment['comment'] %} + {% include "community/_voting_buttons.html" %} + {% endwith %} +
+ +
+ {% if comment['comment'].author.avatar_id %} + + Avatar + {% endif %} + + {{ comment['comment'].author.user_name}} + {% if comment['comment'].author.id == post.author.id%}[S]{% endif %} + {{ moment(comment['comment'].posted_at).fromNow(refresh=True) }} +
+
+ {{ comment['comment'].body_html | safe }} +
+
+
+ reply +
+ {% if comment['replies'] %} +
+ {% for reply in comment['replies'] %} + {{ render_comment(reply) | safe }} + {% endfor %} +
+ {% endif %} +
+ {% endmacro %} +
+ {% for reply in replies %} + {{ render_comment(reply) | safe }} + {% endfor %} +
+
+
+
+ +
+
+
+
+
+ {% if current_user.is_authenticated and current_user.subscribed(post.community) %} + {{ _('Unsubscribe') }} + {% else %} + {{ _('Subscribe') }} + {% endif %} +
+ +
+
+ +
+
+
+
+
+

{{ _('About community') }}

+
+
+

{{ post.community.description|safe }}

+

{{ post.community.rules|safe }}

+ {% if len(mods) > 0 and not post.community.private_mods %} +

Moderators

+
    + {% for mod in mods %} +
  1. {{ mod.user_name }}
  2. + {% endfor %} +
+ {% endif %} +
+
+ {% if is_moderator %} +
+
+

{{ _('Community Settings') }}

+
+ +
+ {% endif %} +
+
+ +{% endblock %} diff --git a/app/templates/community/post.html b/app/templates/community/post.html index fd93b23f..af833234 100644 --- a/app/templates/community/post.html +++ b/app/templates/community/post.html @@ -4,68 +4,7 @@ {% block app_content %}
-
- {% if post.image_id %} -
- -

{{ post.title }}

- {% if post.url %} -

{{ post.url|shorten_url }} - {% if post.type == post_type_link %} - (domain) - {% endif %} -

- {% endif %} -

submitted {{ moment(post.posted_at).fromNow() }} by {{ post.author.user_name }}

-
-
- {% if post.url %} - {{ post.image.alt_text }} - {% else %} - {{ post.image.alt_text }} - {% endif %} -
- {% else %} - -

{{ post.title }}

- {% if post.url %} -

{{ post.url|shorten_url }} - External link - {% if post.type == post_type_link %} - (domain) - {% endif %} -

- {% endif %} -

submitted {{ moment(post.posted_at).fromNow() }} by - {{ post.author.user_name }} -

- {% endif %} -
- - {% if post.body_html %} -
-
- {{ post.body_html|safe }} -
-
-
- {% endif %} + {% include 'community/_post_full.html' %} {% if post.comments_enabled %}
@@ -82,7 +21,7 @@
{% macro render_comment(comment) %} -
+
{% with comment=comment['comment'] %} @@ -96,8 +35,8 @@ Avatar {% endif %} - {{ comment['comment'].author.user_name}} - {% if comment['comment'].author.id == post.author.id%}[S]{% endif %} + {{ comment['comment'].author.user_name }} + {% if comment['comment'].author.id == post.author.id %}[S]{% endif %} {{ moment(comment['comment'].posted_at).fromNow(refresh=True) }}
@@ -108,17 +47,24 @@ reply
{% if comment['replies'] %} -
- {% for reply in comment['replies'] %} - {{ render_comment(reply) | safe }} - {% endfor %} -
+ {% if comment['comment'].depth <= THREAD_CUTOFF_DEPTH %} +
+ {% for reply in comment['replies'] %} + {{ render_comment(reply) | safe }} + {% endfor %} +
+ {% else %} + + {% endif %} {% endif %}
{% endmacro %} -
+
{% for reply in replies %} {{ render_comment(reply) | safe }} {% endfor %}