mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-01-23 19:36:56 -08:00
post replies - more elegant handling of long conversations
This commit is contained in:
parent
ef275f4fbf
commit
4271ee3bca
10 changed files with 276 additions and 78 deletions
|
@ -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/<int:comment_id>/<vote_direction>', methods=['POST'])
|
||||
|
@ -295,8 +296,15 @@ def comment_vote(comment_id, vote_direction):
|
|||
|
||||
|
||||
@bp.route('/post/<int:post_id>/comment/<int:comment_id>')
|
||||
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/<int:post_id>/comment/<int:comment_id>/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)
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -14,3 +14,4 @@ SUBSCRIPTION_MEMBER = 1
|
|||
SUBSCRIPTION_NONMEMBER = 0
|
||||
SUBSCRIPTION_BANNED = -1
|
||||
|
||||
THREAD_CUTOFF_DEPTH = 4
|
|
@ -18,3 +18,7 @@
|
|||
@media (max-width: 576px) { @content ; }
|
||||
}
|
||||
}
|
||||
|
||||
.pl-0 {
|
||||
padding-left: 0;
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
62
app/templates/community/_post_full.html
Normal file
62
app/templates/community/_post_full.html
Normal file
|
@ -0,0 +1,62 @@
|
|||
<div class="row">
|
||||
{% if post.image_id %}
|
||||
<div class="col-8">
|
||||
<nav aria-label="breadcrumb" id="breadcrumb_nav" title="Navigation">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="/">{{ _('Home') }}</a></li>
|
||||
<li class="breadcrumb-item"><a href="/communities">{{ _('Communities') }}</a></li>
|
||||
<li class="breadcrumb-item"><a href="/c/{{ post.community.link() }}">{{ post.community.title }}</a></li>
|
||||
<li class="breadcrumb-item active">{{ post.title|shorten(15) }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<h1 class="mt-2">{{ post.title }}</h1>
|
||||
{% if post.url %}
|
||||
<p><small><a href="{{ post.url }}" rel="nofollow ugc">{{ post.url|shorten_url }}</a>
|
||||
{% if post.type == post_type_link %}
|
||||
<span class="domain_link">(<a href="/d/{{ post.domain_id }}">domain</a>)</span>
|
||||
{% endif %}
|
||||
</small></p>
|
||||
{% endif %}
|
||||
<p><small>submitted {{ moment(post.posted_at).fromNow() }} by {{ post.author.user_name }}</small></p>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
{% if post.url %}
|
||||
<a href="post.url" rel="nofollow ugc"><img src="{{ post.image.source_url }}" alt="{{ post.image.alt_text }}"
|
||||
width="100" /></a>
|
||||
{% else %}
|
||||
<a href="post.image.source_url"><img src="{{ post.image.source_url }}" alt="{{ post.image.alt_text }}"
|
||||
width="100" /></a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<nav aria-label="breadcrumb" id="breadcrumb_nav" title="Navigation">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="/">{{ _('Home') }}</a></li>
|
||||
<li class="breadcrumb-item"><a href="/communities">{{ _('Communities') }}</a></li>
|
||||
<li class="breadcrumb-item"><a href="/c/{{ post.community.link() }}">{{ post.community.title }}</a></li>
|
||||
<li class="breadcrumb-item active">{{ post.title|shorten(15) }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<h1 class="mt-2">{{ post.title }}</h1>
|
||||
{% if post.url %}
|
||||
<p><small><a href="{{ post.url }}" rel="nofollow ugc">{{ post.url|shorten_url }}
|
||||
<img src="/static/images/external_link_black.svg" class="external_link_icon" alt="External link" /></a>
|
||||
{% if post.type == post_type_link %}
|
||||
<span class="domain_link">(<a href="/d/{{ post.domain_id }}">domain</a>)</span>
|
||||
{% endif %}
|
||||
</small></p>
|
||||
{% endif %}
|
||||
<p class="small">submitted {{ moment(post.posted_at).fromNow() }} by
|
||||
<a href="{{ url_for('activitypub.user_profile', actor=post.author.user_name) }}">{{ post.author.user_name }}</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if post.body_html %}
|
||||
<div class="row post_full">
|
||||
<div class="col">
|
||||
{{ post.body_html|safe }}
|
||||
</div>
|
||||
<hr />
|
||||
</div>
|
||||
{% endif %}
|
106
app/templates/community/continue_discussion.html
Normal file
106
app/templates/community/continue_discussion.html
Normal file
|
@ -0,0 +1,106 @@
|
|||
{% extends "base.html" %}
|
||||
{% from 'bootstrap/form.html' import render_form %}
|
||||
|
||||
{% block app_content %}
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-8 position-relative">
|
||||
{% include 'community/_post_full.html' %}
|
||||
<p><a href="{{ url_for('community.show_post', post_id=post.id, _anchor='replies') }}">Back to main discussion</a></p>
|
||||
<div class="row post_replies">
|
||||
<div class="col">
|
||||
{% macro render_comment(comment) %}
|
||||
<div id="comment_{{ comment['comment'].id }}" class="comment">
|
||||
<div class="limit_height">
|
||||
<div class="voting_buttons">
|
||||
{% with comment=comment['comment'] %}
|
||||
{% include "community/_voting_buttons.html" %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<div class="hide_button"><a href='#'>[-] hide</a></div>
|
||||
<div class="comment_author">
|
||||
{% if comment['comment'].author.avatar_id %}
|
||||
<a href="/u/{{ comment['comment'].author.link() }}" title="{{ comment['comment'].author.ap_id }}">
|
||||
<img src="{{ comment['comment'].author.avatar_image() }}" alt="Avatar" /></a>
|
||||
{% endif %}
|
||||
<a href="/u/{{ comment['comment'].author.link() }}" title="{{ comment['comment'].author.link() }}">
|
||||
<strong>{{ comment['comment'].author.user_name}}</strong></a>
|
||||
{% if comment['comment'].author.id == post.author.id%}<span title="Submitter of original post" aria-label="submitter">[S]</span>{% endif %}
|
||||
<span class="text-muted small">{{ moment(comment['comment'].posted_at).fromNow(refresh=True) }}</span>
|
||||
</div>
|
||||
<div class="comment_body hidable">
|
||||
{{ comment['comment'].body_html | safe }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="comment_actions hidable">
|
||||
<a href="{{ url_for('community.add_reply', post_id=post.id, comment_id=comment['comment'].id) }}" rel="nofollow"><span class="fe fe-reply"></span> reply</a>
|
||||
</div>
|
||||
{% if comment['replies'] %}
|
||||
<div class="replies hidable">
|
||||
{% for reply in comment['replies'] %}
|
||||
{{ render_comment(reply) | safe }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
<div class="comments">
|
||||
{% for reply in replies %}
|
||||
{{ render_comment(reply) | safe }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
{% if current_user.is_authenticated and current_user.subscribed(post.community) %}
|
||||
<a class="w-100 btn btn-primary" href="/community/{{ post.community.link() }}/unsubscribe">{{ _('Unsubscribe') }}</a>
|
||||
{% else %}
|
||||
<a class="w-100 btn btn-primary" href="/community/{{ post.community.link() }}/subscribe">{{ _('Subscribe') }}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<a class="w-100 btn btn-primary" href="/community/{{ post.community.link() }}/submit">{{ _('Create post') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
<form method="get">
|
||||
<input type="search" name="search" class="form-control mt-2" placeholder="{{ _('Search this community') }}" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h2>{{ _('About community') }}</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>{{ post.community.description|safe }}</p>
|
||||
<p>{{ post.community.rules|safe }}</p>
|
||||
{% if len(mods) > 0 and not post.community.private_mods %}
|
||||
<h3>Moderators</h3>
|
||||
<ol>
|
||||
{% for mod in mods %}
|
||||
<li><a href="/u/{{ mod.user_name }}">{{ mod.user_name }}</a></li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if is_moderator %}
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h2>{{ _('Community Settings') }}</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p><a href="#" class="btn btn-primary">{{ _('Moderate') }}</a></p>
|
||||
<p><a href="#" class="btn btn-primary">{{ _('Settings') }}</a></p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -4,68 +4,7 @@
|
|||
{% block app_content %}
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-8 position-relative">
|
||||
<div class="row">
|
||||
{% if post.image_id %}
|
||||
<div class="col-8">
|
||||
<nav aria-label="breadcrumb" id="breadcrumb_nav" title="Navigation">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="/">{{ _('Home') }}</a></li>
|
||||
<li class="breadcrumb-item"><a href="/communities">{{ _('Communities') }}</a></li>
|
||||
<li class="breadcrumb-item"><a href="/c/{{ post.community.link() }}">{{ post.community.title }}</a></li>
|
||||
<li class="breadcrumb-item active">{{ post.title|shorten(15) }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<h1 class="mt-2">{{ post.title }}</h1>
|
||||
{% if post.url %}
|
||||
<p><small><a href="{{ post.url }}" rel="nofollow ugc">{{ post.url|shorten_url }}</a>
|
||||
{% if post.type == post_type_link %}
|
||||
<span class="domain_link">(<a href="/d/{{ post.domain_id }}">domain</a>)</span>
|
||||
{% endif %}
|
||||
</small></p>
|
||||
{% endif %}
|
||||
<p><small>submitted {{ moment(post.posted_at).fromNow() }} by {{ post.author.user_name }}</small></p>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
{% if post.url %}
|
||||
<a href="post.url" rel="nofollow ugc"><img src="{{ post.image.source_url }}" alt="{{ post.image.alt_text }}"
|
||||
width="100" /></a>
|
||||
{% else %}
|
||||
<a href="post.image.source_url"><img src="{{ post.image.source_url }}" alt="{{ post.image.alt_text }}"
|
||||
width="100" /></a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<nav aria-label="breadcrumb" id="breadcrumb_nav" title="Navigation">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="/">{{ _('Home') }}</a></li>
|
||||
<li class="breadcrumb-item"><a href="/communities">{{ _('Communities') }}</a></li>
|
||||
<li class="breadcrumb-item"><a href="/c/{{ post.community.link() }}">{{ post.community.title }}</a></li>
|
||||
<li class="breadcrumb-item active">{{ post.title|shorten(15) }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<h1 class="mt-2">{{ post.title }}</h1>
|
||||
{% if post.url %}
|
||||
<p><small><a href="{{ post.url }}" rel="nofollow ugc">{{ post.url|shorten_url }}
|
||||
<img src="/static/images/external_link_black.svg" class="external_link_icon" alt="External link" /></a>
|
||||
{% if post.type == post_type_link %}
|
||||
<span class="domain_link">(<a href="/d/{{ post.domain_id }}">domain</a>)</span>
|
||||
{% endif %}
|
||||
</small></p>
|
||||
{% endif %}
|
||||
<p class="small">submitted {{ moment(post.posted_at).fromNow() }} by
|
||||
<a href="{{ url_for('activitypub.user_profile', actor=post.author.user_name) }}">{{ post.author.user_name }}</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if post.body_html %}
|
||||
<div class="row post_full">
|
||||
<div class="col">
|
||||
{{ post.body_html|safe }}
|
||||
</div>
|
||||
<hr />
|
||||
</div>
|
||||
{% endif %}
|
||||
{% include 'community/_post_full.html' %}
|
||||
|
||||
{% if post.comments_enabled %}
|
||||
<div class="row post_reply_form">
|
||||
|
@ -82,7 +21,7 @@
|
|||
<div class="row post_replies">
|
||||
<div class="col">
|
||||
{% macro render_comment(comment) %}
|
||||
<div id="comment_{{ comment['comment'].id }}" class="comment" style="margin-left: {{ comment['comment'].depth * 20 }}px;">
|
||||
<div id="comment_{{ comment['comment'].id }}" class="comment">
|
||||
<div class="limit_height">
|
||||
<div class="voting_buttons">
|
||||
{% with comment=comment['comment'] %}
|
||||
|
@ -108,17 +47,24 @@
|
|||
<a href="{{ url_for('community.add_reply', post_id=post.id, comment_id=comment['comment'].id) }}" rel="nofollow"><span class="fe fe-reply"></span> reply</a>
|
||||
</div>
|
||||
{% if comment['replies'] %}
|
||||
{% if comment['comment'].depth <= THREAD_CUTOFF_DEPTH %}
|
||||
<div class="replies hidable">
|
||||
{% for reply in comment['replies'] %}
|
||||
{{ render_comment(reply) | safe }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="continue_thread hidable">
|
||||
<a href="{{ url_for('community.continue_discussion', post_id=post.id, comment_id=comment['comment'].id) }}">
|
||||
Continue thread</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endmacro %}
|
||||
|
||||
<div class="comments">
|
||||
<div id="replies" class="comments">
|
||||
{% for reply in replies %}
|
||||
{{ render_comment(reply) | safe }}
|
||||
{% endfor %}
|
||||
|
|
Loading…
Reference in a new issue