reply to a reply

This commit is contained in:
rimu 2023-10-15 21:13:32 +13:00
parent 4cd94ecf4c
commit ef275f4fbf
12 changed files with 301 additions and 36 deletions

View file

@ -71,5 +71,5 @@ class CreatePost(FlaskForm):
class NewReplyForm(FlaskForm): class NewReplyForm(FlaskForm):
body = TextAreaField(_l('Body'), render_kw={'placeholder': 'What are your thoughts?'}) body = TextAreaField(_l('Body'), render_kw={'placeholder': 'What are your thoughts?', 'rows': 3})
submit = SubmitField(_l('Comment')) submit = SubmitField(_l('Comment'))

View file

@ -29,7 +29,8 @@ def add_local():
private_key, public_key = RsaKeys.generate_keypair() private_key, public_key = RsaKeys.generate_keypair()
community = Community(title=form.community_name.data, name=form.url.data, description=form.description.data, community = Community(title=form.community_name.data, name=form.url.data, description=form.description.data,
rules=form.rules.data, nsfw=form.nsfw.data, private_key=private_key, rules=form.rules.data, nsfw=form.nsfw.data, private_key=private_key,
public_key=public_key, ap_profile_id=current_app.config['SERVER_NAME'] + '/c/' + form.url.data, public_key=public_key,
ap_profile_id=current_app.config['SERVER_NAME'] + '/c/' + form.url.data,
subscriptions_count=1) subscriptions_count=1)
db.session.add(community) db.session.add(community)
db.session.commit() db.session.commit()
@ -71,7 +72,8 @@ def show_community(community: Community):
mods = community.moderators() mods = community.moderators()
is_moderator = current_user.is_authenticated and any(mod.user_id == current_user.id for mod in mods) is_moderator = current_user.is_authenticated and any(mod.user_id == current_user.id for mod in mods)
is_owner = current_user.is_authenticated and any(mod.user_id == current_user.id and mod.is_owner == True for mod in mods) is_owner = current_user.is_authenticated and any(
mod.user_id == current_user.id and mod.is_owner == True for mod in mods)
if community.private_mods: if community.private_mods:
mod_list = [] mod_list = []
@ -125,7 +127,7 @@ def subscribe(actor):
except Exception as ex: except Exception as ex:
flash('Failed to send request to subscribe: ' + str(ex), 'error') flash('Failed to send request to subscribe: ' + str(ex), 'error')
current_app.logger.error("Exception while trying to subscribe" + str(ex)) current_app.logger.error("Exception while trying to subscribe" + str(ex))
else: # for local communities, joining is instant else: # for local communities, joining is instant
banned = CommunityBan.query.filter_by(user_id=current_user.id, community_id=community.id).first() banned = CommunityBan.query.filter_by(user_id=current_user.id, community_id=community.id).first()
if banned: if banned:
flash('You cannot join this community') flash('You cannot join this community')
@ -237,7 +239,8 @@ def show_post(post_id: int):
flash('Your comment has been added.') flash('Your comment has been added.')
# todo: flush cache # todo: flush cache
# todo: federation # todo: federation
return redirect(url_for('community.show_post', post_id=post_id)) # redirect to current page to avoid refresh resubmitting the form return redirect(url_for('community.show_post',
post_id=post_id)) # redirect to current page to avoid refresh resubmitting the form
else: else:
replies = post_replies(post.id, 'top') replies = post_replies(post.id, 'top')
return render_template('community/post.html', title=post.title, post=post, is_moderator=is_moderator, return render_template('community/post.html', title=post.title, post=post, is_moderator=is_moderator,
@ -250,25 +253,25 @@ def comment_vote(comment_id, vote_direction):
comment = PostReply.query.get_or_404(comment_id) comment = PostReply.query.get_or_404(comment_id)
existing_vote = PostReplyVote.query.filter_by(user_id=current_user.id, post_reply_id=comment.id).first() existing_vote = PostReplyVote.query.filter_by(user_id=current_user.id, post_reply_id=comment.id).first()
if existing_vote: if existing_vote:
if existing_vote.effect > 0: # previous vote was up if existing_vote.effect > 0: # previous vote was up
if vote_direction == 'upvote': # new vote is also up, so remove it if vote_direction == 'upvote': # new vote is also up, so remove it
db.session.delete(existing_vote) db.session.delete(existing_vote)
comment.up_votes -= 1 comment.up_votes -= 1
comment.score -= 1 comment.score -= 1
else: # new vote is down while previous vote was up, so reverse their previous vote else: # new vote is down while previous vote was up, so reverse their previous vote
existing_vote.effect = -1 existing_vote.effect = -1
comment.up_votes -= 1 comment.up_votes -= 1
comment.down_votes += 1 comment.down_votes += 1
comment.score -= 2 comment.score -= 2
downvoted_class = 'voted_down' downvoted_class = 'voted_down'
else: # previous vote was down else: # previous vote was down
if vote_direction == 'upvote': # new vote is upvote if vote_direction == 'upvote': # new vote is upvote
existing_vote.effect = 1 existing_vote.effect = 1
comment.up_votes += 1 comment.up_votes += 1
comment.down_votes -= 1 comment.down_votes -= 1
comment.score += 1 comment.score += 1
upvoted_class = 'voted_up' upvoted_class = 'voted_up'
else: # reverse a previous downvote else: # reverse a previous downvote
db.session.delete(existing_vote) db.session.delete(existing_vote)
comment.down_votes -= 1 comment.down_votes -= 1
comment.score += 2 comment.score += 2
@ -289,3 +292,36 @@ def comment_vote(comment_id, vote_direction):
return render_template('community/_voting_buttons.html', comment=comment, return render_template('community/_voting_buttons.html', comment=comment,
upvoted_class=upvoted_class, upvoted_class=upvoted_class,
downvoted_class=downvoted_class) downvoted_class=downvoted_class)
@bp.route('/post/<int:post_id>/comment/<int:comment_id>')
def show_comment(post_id, comment_id):
...
@bp.route('/post/<int:post_id>/comment/<int:comment_id>/reply', methods=['GET', 'POST'])
def add_reply(post_id: int, comment_id: int):
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)
form = NewReplyForm()
if form.validate_on_submit():
reply = PostReply(user_id=current_user.id, post_id=post.id, parent_id=comment.id, depth=comment.depth + 1,
community_id=post.community.id, body=form.body.data,
body_html=markdown_to_html(form.body.data), body_html_safe=True,
from_bot=current_user.bot, up_votes=1, nsfw=post.nsfw, nsfl=post.nsfl)
db.session.add(reply)
db.session.commit()
reply_vote = PostReplyVote(user_id=current_user.id, author_id=current_user.id, post_reply_id=reply.id,
effect=1.0)
db.session.add(reply_vote)
db.session.commit()
form.body.data = ''
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}'))
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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 B

View file

@ -0,0 +1,85 @@
(function () {
function hideFieldsetContent(fieldset, options) {
const content = fieldset.querySelectorAll('*:not(legend)');
if (options.animation) {
content.forEach((element) => {
element.style.display = 'none';
});
} else {
content.forEach((element) => {
element.style.display = 'none';
});
}
fieldset.classList.remove('expanded');
fieldset.classList.add('collapsed');
content.forEach((element) => {
element.setAttribute('aria-expanded', 'false');
});
if (!options.animation) {
fieldset.dispatchEvent(new Event('update'));
}
}
function showFieldsetContent(fieldset, options) {
const content = fieldset.querySelectorAll('*:not(legend)');
if (options.animation) {
content.forEach((element) => {
element.style.display = '';
});
} else {
content.forEach((element) => {
element.style.display = '';
});
}
fieldset.classList.remove('collapsed');
fieldset.classList.add('expanded');
content.forEach((element) => {
element.setAttribute('aria-expanded', 'true');
});
if (!options.animation) {
fieldset.dispatchEvent(new Event('update'));
}
}
function doToggle(fieldset, setting) {
if (fieldset.classList.contains('collapsed')) {
showFieldsetContent(fieldset, setting);
} else if (fieldset.classList.contains('expanded')) {
hideFieldsetContent(fieldset, setting);
}
}
function coolfieldset(selector, options) {
const fieldsets = document.querySelectorAll(selector);
const setting = { collapsed: false, animation: true, speed: 'medium', ...options };
fieldsets.forEach((fieldset) => {
const legend = fieldset.querySelector('legend');
const content = fieldset.querySelectorAll('*:not(legend)');
content.forEach((element) => {
const wrapper = document.createElement('div');
wrapper.classList.add('wrapper');
element.parentNode.insertBefore(wrapper, element);
wrapper.appendChild(element);
});
if (setting.collapsed) {
hideFieldsetContent(fieldset, { animation: false });
} else {
fieldset.classList.add('expanded');
}
legend.addEventListener('click', () => doToggle(fieldset, setting));
});
}
window.coolfieldset = coolfieldset;
})();
// Usage:
// coolfieldset('.coolfieldset', { collapsed: true, animation: true, speed: 'slow' });
document.addEventListener('DOMContentLoaded', function () {
coolfieldset('.coolfieldset', { collapsed: true, animation: true, speed: 'slow' });
});

View file

@ -15,7 +15,7 @@ function setupShowMoreLinks() {
const comments = document.querySelectorAll('.comment'); const comments = document.querySelectorAll('.comment');
comments.forEach(comment => { comments.forEach(comment => {
const content = comment.querySelector('.comment_body'); const content = comment.querySelector('.limit_height');
if (content && content.clientHeight > 400) { if (content && content.clientHeight > 400) {
content.style.overflow = 'hidden'; content.style.overflow = 'hidden';
content.style.maxHeight = '400px'; content.style.maxHeight = '400px';
@ -33,6 +33,7 @@ function setupShowMoreLinks() {
showMoreLink.innerHTML = '<i class="fe fe-angles-up" title="Collapse"></i>'; showMoreLink.innerHTML = '<i class="fe fe-angles-up" title="Collapse"></i>';
} else { } else {
content.style.overflow = 'hidden'; content.style.overflow = 'hidden';
content.style.maxHeight = '400px';
showMoreLink.innerHTML = '<i class="fe fe-angles-down" title="Read more"></i>'; showMoreLink.innerHTML = '<i class="fe fe-angles-down" title="Read more"></i>';
} }
}); });
@ -95,6 +96,11 @@ function setupHideButtons() {
hidable.style.display = isHidden ? 'block' : 'none'; hidable.style.display = isHidden ? 'block' : 'none';
}); });
const moreHidables = parentElement.parentElement.querySelectorAll('.hidable');
moreHidables.forEach(hidable => {
hidable.style.display = isHidden ? 'block' : 'none';
});
// Toggle the content of hideEl // Toggle the content of hideEl
if (isHidden) { if (isHidden) {
hideEl.innerHTML = "<a href='#'>[-] hide</a>"; hideEl.innerHTML = "<a href='#'>[-] hide</a>";

View file

@ -366,14 +366,13 @@ fieldset legend {
clear: both; clear: both;
margin-bottom: 20px; margin-bottom: 20px;
} }
.comment .comment_body { .comment .limit_height {
overflow: hidden;
position: relative; position: relative;
} }
.comment .comment_body.expanded { .comment .limit_height.expanded {
max-height: none; max-height: none;
} }
.comment .comment_body.expanded .show-more { .comment .limit_height.expanded .show-more {
display: none; display: none;
} }
.comment .show-more { .comment .show-more {
@ -441,4 +440,8 @@ fieldset legend {
text-decoration: none; text-decoration: none;
} }
.add_reply .form-control-label {
display: none;
}
/*# sourceMappingURL=structure.css.map */ /*# sourceMappingURL=structure.css.map */

View file

@ -140,8 +140,7 @@ nav, etc which are used site-wide */
clear: both; clear: both;
margin-bottom: 20px; margin-bottom: 20px;
.comment_body { .limit_height {
overflow: hidden;
position: relative; position: relative;
&.expanded { &.expanded {
@ -232,4 +231,10 @@ nav, etc which are used site-wide */
text-decoration: none; text-decoration: none;
} }
} }
}
.add_reply {
.form-control-label {
display: none;
}
} }

View file

@ -324,4 +324,34 @@ nav.navbar {
margin-bottom: 3px; margin-bottom: 3px;
} }
.coolfieldset, .coolfieldset.expanded {
border: 1px solid #ddd;
border-radius: 5px;
padding: 0 20px;
}
.coolfieldset.collapsed {
border: 0;
border-top: 1px solid #bbb;
border-radius: 0;
}
.coolfieldset legend {
padding-left: 13px;
font-weight: bold;
cursor: pointer;
background-color: white;
display: block;
position: relative;
top: -11px;
}
.coolfieldset legend, .coolfieldset.expanded legend {
background: whitesmoke url(/static/images/expanded.gif) no-repeat center left;
}
.coolfieldset.collapsed legend {
background: whitesmoke url(/static/images/collapsed.gif) no-repeat center left;
}
/*# sourceMappingURL=styles.css.map */ /*# sourceMappingURL=styles.css.map */

View file

@ -91,3 +91,33 @@ nav.navbar {
margin-left: 4px; margin-left: 4px;
margin-bottom: 3px; margin-bottom: 3px;
} }
.coolfieldset, .coolfieldset.expanded{
border:1px solid $light-grey;
border-radius: 5px;
padding: 0 20px;
}
.coolfieldset.collapsed{
border:0;
border-top:1px solid $grey;
border-radius: 0;
}
.coolfieldset legend{
padding-left:13px;
font-weight:bold;
cursor:pointer;
background-color: white;
display: block;
position: relative;
top: -11px;
}
.coolfieldset legend, .coolfieldset.expanded legend{
background: whitesmoke url(/static/images/expanded.gif) no-repeat center left;
}
.coolfieldset.collapsed legend{
background: whitesmoke url(/static/images/collapsed.gif) no-repeat center left;
}

View file

@ -0,0 +1,68 @@
{% extends "base.html" %}
{% from 'bootstrap/form.html' import render_form %}
{% block app_content %}
<script src="/static/js/coolfieldset.js"></script>
<div class="row">
<div class="col-12 col-md-8 position-relative add_reply">
<fieldset class="coolfieldset mt-4"><legend class="w-auto">Original post</legend>
<h3>{{ post.title }}</h3>
{{ post.body_html | safe }}
</fieldset>
<fieldset class="coolfieldset mt-4"><legend class="w-auto">Comment you are replying to</legend>
{{ comment.body_html | safe}}
</fieldset>
{{ render_form(form) }}
</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 %}

View file

@ -82,28 +82,30 @@
<div class="row post_replies"> <div class="row post_replies">
<div class="col"> <div class="col">
{% macro render_comment(comment) %} {% macro render_comment(comment) %}
<div class="comment" style="margin-left: {{ comment['comment'].depth * 20 }}px;"> <div id="comment_{{ comment['comment'].id }}" class="comment" style="margin-left: {{ comment['comment'].depth * 20 }}px;">
<div class="voting_buttons"> <div class="limit_height">
{% with comment=comment['comment'] %} <div class="voting_buttons">
{% include "community/_voting_buttons.html" %} {% with comment=comment['comment'] %}
{% endwith %} {% include "community/_voting_buttons.html" %}
</div> {% endwith %}
<div class="hide_button"><a href='#'>[-] hide</a></div> </div>
<div class="comment_author"> <div class="hide_button"><a href='#'>[-] hide</a></div>
{% if comment['comment'].author.avatar_id %} <div class="comment_author">
<a href="/u/{{ comment['comment'].author.link() }}" title="{{ comment['comment'].author.ap_id }}"> {% if comment['comment'].author.avatar_id %}
<img src="{{ comment['comment'].author.avatar_image() }}" alt="Avatar" /></a> <a href="/u/{{ comment['comment'].author.link() }}" title="{{ comment['comment'].author.ap_id }}">
{% endif %} <img src="{{ comment['comment'].author.avatar_image() }}" alt="Avatar" /></a>
<a href="/u/{{ comment['comment'].author.link() }}" title="{{ comment['comment'].author.link() }}"> {% endif %}
<strong>{{ comment['comment'].author.user_name}}</strong></a> <a href="/u/{{ comment['comment'].author.link() }}" title="{{ comment['comment'].author.link() }}">
{% if comment['comment'].author.id == post.author.id%}<span title="Submitter of original post" aria-label="submitter">[S]</span>{% endif %} <strong>{{ comment['comment'].author.user_name}}</strong></a>
<span class="text-muted small">{{ moment(comment['comment'].posted_at).fromNow(refresh=True) }}</span> {% if comment['comment'].author.id == post.author.id%}<span title="Submitter of original post" aria-label="submitter">[S]</span>{% endif %}
</div> <span class="text-muted small">{{ moment(comment['comment'].posted_at).fromNow(refresh=True) }}</span>
<div class="comment_body hidable"> </div>
{{ comment['comment'].body_html | safe }} <div class="comment_body hidable">
{{ comment['comment'].body_html | safe }}
</div>
</div> </div>
<div class="comment_actions hidable"> <div class="comment_actions hidable">
<a href="#"><span class="fe fe-reply"></span> reply</a> <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> </div>
{% if comment['replies'] %} {% if comment['replies'] %}
<div class="replies hidable"> <div class="replies hidable">