add profile wall

pull/48/head
Aevann1 2022-12-03 03:49:07 +02:00 committed by Snakes
parent 814552cdfe
commit cb4d941fd0
Signed by: Snakes
GPG Key ID: E745A82778055C7E
16 changed files with 429 additions and 26 deletions

View File

@ -191,7 +191,7 @@ function comment_edit(id){
xhr[0].send(xhr[1]);
}
function post_comment(fullname, hide){
function post_comment(fullname, wall_user_id, hide){
const btn = document.getElementById('save-reply-to-'+fullname)
btn.disabled = true
btn.classList.add('disabled');
@ -210,7 +210,9 @@ function post_comment(fullname, hide){
catch(e) {}
const xhr = new XMLHttpRequest();
xhr.open("post", "/comment");
if (wall_user_id == 'None') url = "/comment"
else url = '/wall_comment'
xhr.open("post", url);
xhr.setRequestHeader('xhr', 'xhr');
xhr.onload=function(){
let data

View File

@ -33,6 +33,7 @@ class Comment(Base):
id = Column(Integer, primary_key=True)
author_id = Column(Integer, ForeignKey("users.id"))
parent_submission = Column(Integer, ForeignKey("submissions.id"))
wall_user_id = Column(Integer, ForeignKey("users.id"))
created_utc = Column(Integer)
edited_utc = Column(Integer, default=0)
is_banned = Column(Boolean, default=False)
@ -74,6 +75,7 @@ class Comment(Base):
flags = relationship("CommentFlag", order_by="CommentFlag.created_utc")
options = relationship("CommentOption", order_by="CommentOption.id")
casino_game = relationship("Casino_Game")
wall_user = relationship("User", primaryjoin="User.id==Comment.wall_user_id")
def __init__(self, *args, **kwargs):
if "created_utc" not in kwargs:

View File

@ -496,7 +496,7 @@ class User(Base):
@property
@lazy
def fullname(self):
return f"t1_{self.id}"
return f"u_{self.id}"
@property
@lazy

View File

@ -215,7 +215,7 @@ def comment(v):
parent_submission=parent_post.id,
parent_comment_id=parent_comment_id,
level=level,
over_18=parent_post.over_18 or request.values.get("over_18")=="true",
over_18=parent_post.over_18,
is_bot=is_bot,
app_id=v.client.application.id if v.client else None,
body_html=body_html,
@ -344,6 +344,241 @@ def comment(v):
return {"comment": render_template("comments.html", v=v, comments=[c])}
#- API
@app.post("/wall_comment")
@limiter.limit("1/second;20/minute;200/hour;1000/day")
@auth_required
@ratelimit_user("1/second;20/minute;200/hour;1000/day")
def wall_comment(v):
if v.is_suspended: abort(403, "You can't perform this action while banned.")
parent_fullname = request.values.get("parent_fullname").strip()
if len(parent_fullname) < 3: abort(400)
id = parent_fullname[2:]
parent_comment_id = None
if parent_fullname.startswith("u_"):
parent = get_account(id, v=v)
parent_user = parent
parent_author = parent
elif parent_fullname.startswith("c_"):
parent = get_comment(id, v=v)
if parent.deleted_utc != 0: abort(404)
parent_user = parent.wall_user
parent_comment_id = parent.id
parent_author = parent_user
else: abort(400)
level = 1 if isinstance(parent, User) else parent.level + 1
# if not User.can_see(v, parent): abort(404)
if level > COMMENT_MAX_DEPTH: abort(400, f"Max comment level is {COMMENT_MAX_DEPTH}")
body = sanitize_raw_body(request.values.get("body", ""), False)
if v.longpost and (len(body) < 280 or ' [](' in body or body.startswith('[](')):
abort(403, "You have to type more than 280 characters!")
elif v.bird and len(body) > 140:
abort(403, "You have to type less than 140 characters!")
if not body and not request.files.get('file'):
abort(400, "You need to actually write something!")
if v.admin_level < PERMS['POST_COMMENT_MODERATION'] and parent_author.any_block_exists(v):
abort(403, "You can't reply to users who have blocked you or users that you have blocked.")
options = []
for i in list(poll_regex.finditer(body))[:POLL_MAX_OPTIONS]:
options.append(i.group(1))
body = body.replace(i.group(0), "")
choices = []
for i in list(choice_regex.finditer(body))[:POLL_MAX_OPTIONS]:
choices.append(i.group(1))
body = body.replace(i.group(0), "")
if request.files.get("file") and not g.is_tor:
files = request.files.getlist('file')[:4]
for file in files:
if file.content_type.startswith('image/'):
oldname = f'/images/{time.time()}'.replace('.','') + '.webp'
file.save(oldname)
image = process_image(oldname, v)
if image == "": abort(400, "Image upload failed")
if v.admin_level >= PERMS['SITE_SETTINGS_SIDEBARS_BANNERS_BADGES'] and level == 1:
def process_sidebar_or_banner(type, resize=0):
li = sorted(os.listdir(f'files/assets/images/{SITE_NAME}/{type}'),
key=lambda e: int(e.split('.webp')[0]))[-1]
num = int(li.split('.webp')[0]) + 1
filename = f'files/assets/images/{SITE_NAME}/{type}/{num}.webp'
copyfile(oldname, filename)
process_image(filename, v, resize=resize)
body += f"\n\n![]({image})"
elif file.content_type.startswith('video/'):
body += f"\n\n{SITE_FULL}{process_video(file, v)}"
elif file.content_type.startswith('audio/'):
body += f"\n\n{SITE_FULL}{process_audio(file, v)}"
else:
abort(415)
body = body.strip()[:COMMENT_BODY_LENGTH_LIMIT]
body_for_sanitize = body
if v.owoify:
body_for_sanitize = owoify(body_for_sanitize)
if v.marsify:
body_for_sanitize = marsify(body_for_sanitize)
torture = (v.agendaposter and not v.marseyawarded)
body_html = sanitize(body_for_sanitize, limit_pings=5, count_marseys=not v.marsify, torture=torture)
if '!wordle' not in body.lower() and AGENDAPOSTER_PHRASE not in body.lower():
existing = g.db.query(Comment.id).filter(Comment.author_id == v.id,
Comment.deleted_utc == 0,
Comment.parent_comment_id == parent_comment_id,
Comment.parent_submission == None,
Comment.body_html == body_html
).first()
if existing: abort(409, f"You already made that comment: /comment/{existing.id}")
is_bot = (v.client is not None
and v.id not in PRIVILEGED_USER_BOTS
or (SITE == 'pcmemes.net' and v.id == SNAPPY_ID))
execute_antispam_comment_check(body, v)
execute_antispam_duplicate_comment_check(v, body_html)
if len(body_html) > COMMENT_BODY_HTML_LENGTH_LIMIT: abort(400)
c = Comment(author_id=v.id,
wall_user_id=parent_user.id,
parent_comment_id=parent_comment_id,
level=level,
is_bot=is_bot,
app_id=v.client.application.id if v.client else None,
body_html=body_html,
body=body,
)
c.upvotes = 1
g.db.add(c)
g.db.flush()
execute_blackjack(v, c, c.body, "comment")
execute_under_siege(v, c, c.body, "comment")
if c.level == 1: c.top_comment_id = c.id
else: c.top_comment_id = parent.top_comment_id
for option in options:
body_html = filter_emojis_only(option)
if len(body_html) > 500: abort(400, "Poll option too long!")
option = CommentOption(
comment_id=c.id,
body_html=body_html,
exclusive=0
)
g.db.add(option)
for choice in choices:
body_html = filter_emojis_only(choice)
if len(body_html) > 500: abort(400, "Poll option too long!")
choice = CommentOption(
comment_id=c.id,
body_html=body_html,
exclusive=1
)
g.db.add(choice)
if v.agendaposter and not v.marseyawarded and AGENDAPOSTER_PHRASE not in c.body.lower():
c.is_banned = True
c.ban_reason = "AutoJanny"
g.db.add(c)
body = AGENDAPOSTER_MSG.format(username=v.username, type='comment', AGENDAPOSTER_PHRASE=AGENDAPOSTER_PHRASE)
body_jannied_html = AGENDAPOSTER_MSG_HTML.format(id=v.id, username=v.username, type='comment', AGENDAPOSTER_PHRASE=AGENDAPOSTER_PHRASE)
c_jannied = Comment(author_id=AUTOJANNY_ID,
parent_submission=None,
wall_user_id=parent_user.id,
distinguish_level=6,
parent_comment_id=c.id,
level=level+1,
is_bot=True,
body=body,
body_html=body_jannied_html,
top_comment_id=c.top_comment_id,
)
g.db.add(c_jannied)
g.db.flush()
n = Notification(comment_id=c_jannied.id, user_id=v.id)
g.db.add(n)
if not v.shadowbanned:
notify_users = NOTIFY_USERS(body, v)
if parent_author.id != v.id:
notify_users.add(parent_author.id)
for x in notify_users-bots:
n = Notification(comment_id=c.id, user_id=x)
g.db.add(n)
if VAPID_PUBLIC_KEY != DEFAULT_CONFIG_VALUE and parent_author.id != v.id and not v.shadowbanned:
title = f'New comment on your wall by @{c.author_name}'
if len(c.body) > 500: notifbody = c.body[:500] + '...'
else: notifbody = c.body
url = f'{SITE_FULL}/comment/{c.id}?context=8&read=true#context'
push_notif(parent_author.id, title, notifbody, url)
vote = CommentVote(user_id=v.id,
comment_id=c.id,
vote_type=1,
)
g.db.add(vote)
v.comment_count = g.db.query(Comment).filter(
Comment.author_id == v.id,
Comment.parent_submission != None,
Comment.deleted_utc == 0
).count()
g.db.add(v)
c.voted = 1
if v.marseyawarded and marseyaward_body_regex.search(body_html):
abort(403, "You can only type marseys!")
check_for_treasure(body, c)
if FEATURES['WORDLE'] and "!wordle" in body:
answer = random.choice(WORDLE_LIST)
c.wordle_result = f'_active_{answer}'
check_slots_command(v, v, c)
if c.level > 5:
n = g.db.query(Notification).filter_by(
comment_id=c.parent_comment.parent_comment.parent_comment.parent_comment_id,
user_id=c.parent_comment.author_id,
).one_or_none()
if n: g.db.delete(n)
g.db.flush()
if v.client: return c.json(db=g.db)
return {"comment": render_template("comments.html", v=v, comments=[c])}
@app.post("/edit_comment/<cid>")
@limiter.limit("1/second;10/minute;100/hour;200/day")

View File

@ -714,6 +714,50 @@ def userpagelisting(user:User, site=None, v=None, page:int=1, sort="new", t="all
@app.get("/@<username>")
@app.get("/@<username>.json")
@auth_desired_with_logingate
def u_username_wall(username, v=None):
u = get_user(username, v=v, include_blocks=True, include_shadowbanned=False)
if username != u.username:
return redirect(f"/@{u.username}/comments")
is_following = v and u.has_follower(v)
if not u.is_visible_to(v):
if g.is_api_or_xhr or request.path.endswith(".json"):
abort(403, f"@{u.username}'s userpage is private")
return render_template("userpage/private.html", u=u, v=v, is_following=is_following), 403
if v and hasattr(u, 'is_blocking') and u.is_blocking:
if g.is_api_or_xhr or request.path.endswith(".json"):
abort(403, f"You are blocking @{u.username}.")
return render_template("userpage/blocking.html", u=u, v=v), 403
try: page = max(int(request.values.get("page", "1")), 1)
except: page = 1
comments, output = get_comments_v_properties(v, True, None, Comment.wall_user_id == u.id)
comments = comments.filter(Comment.level == 1)
if not v or (v.id != u.id and v.admin_level < PERMS['POST_COMMENT_MODERATION']):
comments = comments.filter(
Comment.is_banned == False,
Comment.ghost == False,
Comment.deleted_utc == 0
)
comments = comments.order_by(Comment.created_utc.desc()).offset(PAGE_SIZE * (page - 1)).limit(PAGE_SIZE+1)
comments = [c[0] for c in comments.all()]
next_exists = (len(comments) > PAGE_SIZE)
comments = comments[:PAGE_SIZE]
if (v and v.client) or request.path.endswith(".json"):
return {"data": [c.json(g.db) for c in comments]}
return render_template("userpage/wall.html", u=u, v=v, listing=comments, page=page, next_exists=next_exists, is_following=is_following, standalone=True, render_replies=True)
@app.get("/@<username>/posts")
@app.get("/@<username>/posts.json")
@auth_desired_with_logingate
def u_username(username, v=None):
u = get_user(username, v=v, include_blocks=True, include_shadowbanned=False)
if username != u.username:
@ -766,7 +810,7 @@ def u_username(username, v=None):
if (v and v.client) or request.path.endswith(".json"):
return {"data": [x.json(g.db) for x in listing]}
return render_template("userpage.html",
return render_template("userpage/userpage.html",
unban=u.unban_string,
u=u,
v=v,
@ -780,7 +824,7 @@ def u_username(username, v=None):
if (v and v.client) or request.path.endswith(".json"):
return {"data": [x.json(g.db) for x in listing]}
return render_template("userpage.html",
return render_template("userpage/userpage.html",
u=u,
v=v,
listing=listing,
@ -1005,7 +1049,7 @@ def saved_posts(v:User, username):
try: page = max(1, int(request.values.get("page", 1)))
except: abort(400, "Invalid page input!")
return get_saves_and_subscribes(v, "userpage.html", SaveRelationship, page, False)
return get_saves_and_subscribes(v, "userpage/userpage.html", SaveRelationship, page, False)
@app.get("/@<username>/saved/comments")
@auth_required
@ -1021,7 +1065,7 @@ def subscribed_posts(v:User, username):
try: page = max(1, int(request.values.get("page", 1)))
except: abort(400, "Invalid page input!")
return get_saves_and_subscribes(v, "userpage.html", Subscription, page, False)
return get_saves_and_subscribes(v, "userpage/userpage.html", Subscription, page, False)
@app.post("/fp/<fp>")
@auth_required

View File

@ -71,7 +71,7 @@ def vote_post_comment(target_id, new, v, cls, vote_cls):
target = get_post(target_id)
elif cls == Comment:
target = get_comment(target_id)
if not target.parent_submission: abort(404)
if not target.parent_submission and not target.wall_user_id: abort(404)
else:
abort(404)

View File

@ -1,4 +1,4 @@
{% extends "userpage.html" %}
{% extends "userpage/userpage.html" %}
{% block pagetype %}userpage{% endblock %}
{% block desktopBanner %}{% endblock %}
{% block desktopUserBanner %}{% endblock %}

View File

@ -1,4 +1,4 @@
{% extends "userpage.html" %}
{% extends "userpage/userpage.html" %}
{% block pagetype %}userpage{% endblock %}
{% block desktopBanner %}{% endblock %}
{% block desktopUserBanner %}{% endblock %}

View File

@ -1,4 +1,4 @@
{% extends "userpage.html" %}
{% extends "userpage/userpage.html" %}
{% block pagetype %}userpage{% endblock %}
{% block desktopBanner %}{% endblock %}
{% block desktopUserBanner %}{% endblock %}

View File

@ -92,7 +92,7 @@
{% endif %}
{% elif c.author_id==AUTOJANNY_ID %}
<span class="font-weight-bold">Notification</span>
{% else %}
{% elif not c.wall_user_id %}
{% if c.sentto == MODMAIL_ID %}
<span class="font-weight-bold">Sent to admins</span>
{% else %}
@ -266,7 +266,7 @@
<div id="comment-text-{{c.id}}" class="comment-text mb-0 {% if c.author.agendaposter and not (c.parent_submission and c.post.sub == 'chudrama') %}agendaposter{% endif %} {% if c.author.rainbow %}rainbow-text{% endif %}">
{{c.realbody(v) | safe}}
</div>
{% if c.parent_submission %}
{% if c.parent_submission or c.wall_user_id %}
{% if v and v.id==c.author_id %}
<div id="comment-edit-{{c.id}}" class="d-none comment-write collapsed child">
@ -422,7 +422,7 @@
<button type="button" id="save-{{c.id}}" class="btn caction py-0 nobackground px-1 {% if c.id not in v.saved_comment_idlist %}d-md-inline-block{% endif %} text-muted d-none" onclick="postToastSwitch(this,'/save_comment/{{c.id}}','save-{{c.id}}','unsave-{{c.id}}','d-md-inline-block')"><i class="fas fa-save"></i>Save</button>
{% endif %}
{% if c.parent_submission %}
{% if c.parent_submission or c.wall_user_id %}
{% if v and c.author_id == v.id %}
<button type="button" class="btn caction py-0 nobackground px-1 text-muted" onclick="toggleEdit('{{c.id}}')"><i class="fas fa-edit fa-fw"></i>Edit</button>
@ -485,7 +485,7 @@
{% endif %}
{% endif %}
{% if c.parent_submission and (c.author_id==v.id or v.admin_level >= PERMS['POST_COMMENT_MODERATION'] or (c.post.sub and v.mods(c.post.sub))) %}
{% if (c.parent_submission or c.wall_user_id) and (c.author_id==v.id or v.admin_level >= PERMS['POST_COMMENT_MODERATION'] or (c.post.sub and v.mods(c.post.sub))) %}
<button type="button" id="unmark-{{c.id}}" class="dropdown-item list-inline-item d-none {% if c.over_18 %}d-md-block{% endif %} text-success" onclick="postToastSwitch(this,'/toggle_comment_nsfw/{{c.id}}','mark-{{c.id}}','unmark-{{c.id}}','d-md-block')"><i class="fas fa-eye-evil text-success fa-fw"></i>Unmark +18</button>
<button type="button" id="mark-{{c.id}}" class="dropdown-item list-inline-item d-none {% if not c.over_18 %}d-md-block{% endif %} text-danger" onclick="postToastSwitch(this,'/toggle_comment_nsfw/{{c.id}}','mark-{{c.id}}','unmark-{{c.id}}','d-md-block')"><i class="fas fa-eye-evil text-danger fa-fw"></i>Mark +18</button>
{% endif %}
@ -537,7 +537,7 @@
<input autocomplete="off" id="file-upload-reply-{{c.fullname}}" accept="image/*, video/*, audio/*" type="file" multiple="multiple" name="file" {% if g.is_tor %}disabled{% endif %} onchange="changename('filename-show-reply-{{c.fullname}}','file-upload-reply-{{c.fullname}}')" hidden>
</label>
</div>
<button type="button" id="save-reply-to-{{c.fullname}}" class="btn btn-primary ml-2 fl-r commentmob" onclick="post_comment('{{c.fullname}}', 'reply-to-{{c.id}}');remove_dialog()">Comment</button>
<button type="button" id="save-reply-to-{{c.fullname}}" class="btn btn-primary ml-2 fl-r commentmob" onclick="post_comment('{{c.fullname}}', '{{c.wall_user_id}}', 'reply-to-{{c.id}}');remove_dialog()">Comment</button>
<button type="button" onclick="document.getElementById('reply-to-{{c.id}}').classList.add('d-none');remove_dialog()" class="btn btn-link text-muted ml-auto cancel-form fl-r commentmob">Cancel</button>
</form>
<div id="form-preview-{{c.fullname}}" class="preview mb-3 mt-5"></div>
@ -696,7 +696,7 @@
</div>
<div class="modal-body">
<ul class="list-group comment-actions">
{% if c.parent_submission %}
{% if c.parent_submission or c.wall_user_id %}
{% if v.admin_level >= PERMS['APPS_MODERATION'] and c.oauth_app %}
<a href="{{c.oauth_app.permalink}}/comments" class="list-group-item text-info"><i class="fas fa-code text-info mr-2"></i>API App</a>
{% endif %}

View File

@ -366,7 +366,7 @@
<input autocomplete="off" id="file-upload-reply-{{p.fullname}}" accept="image/*, video/*, audio/*" type="file" multiple="multiple" name="file" {% if g.is_tor %}disabled{% endif %} onchange="changename('filename-show-reply-{{p.fullname}}','file-upload-reply-{{p.fullname}}')" hidden>
</label>
</div>
<button type="button" id="save-reply-to-{{p.fullname}}" form="reply-to-{{p.fullname}}" class="btn btn-primary text-whitebtn ml-auto fl-r" onclick="post_comment('{{p.fullname}}');remove_dialog()">Comment</button>
<button type="button" id="save-reply-to-{{p.fullname}}" form="reply-to-{{p.fullname}}" class="btn btn-primary text-whitebtn ml-auto fl-r" onclick="post_comment('{{p.fullname}}', 'None');remove_dialog()">Comment</button>
</form>
<div id="form-preview-{{p.fullname}}" class="preview mb-3 mt-5"></div>
<div class="form-text text-small p-0 m-0"><a href="/formatting" {% if v and v.newtab %}data-target="t"target="_blank"{% endif %}>Formatting help</a></div>

View File

@ -1,4 +1,4 @@
{% extends "userpage.html" %}
{% extends "userpage/userpage.html" %}
{% block content %}
@ -7,7 +7,10 @@
<div class="flex-row box-shadow-bottom d-flex justify-content-center justify-content-md-between align-items-center">
<ul class="nav settings-nav" id="profile-content--nav">
<li class="nav-item">
<a class="nav-link" href="/@{{u.username}}">Posts <span class="count">({{u.post_count}})</span></a>
<a class="nav-link" href="/@{{u.username}}">Wall</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/@{{u.username}}/posts">Posts <span class="count">({{u.post_count}})</span></a>
</li>
<li class="nav-item">
<a class="nav-link {% if not 'saved' in request.path %}active{% endif %}" href="/@{{u.username}}/comments">Comments <span class="count">({{u.comment_count}})</span></a>

View File

@ -1,4 +1,4 @@
{% extends "userpage.html" %}
{% extends "userpage/userpage.html" %}
{% block content %}

View File

@ -474,7 +474,10 @@
<div class="flex-row box-shadow-bottom d-flex justify-content-center justify-content-md-between align-items-center">
<ul class="nav settings-nav" id="profile-content--nav">
<li class="nav-item">
<a class="nav-link {% if not 'saved' in request.path %}active{% endif %}" href="/@{{u.username}}">Posts <span class="count">({{u.post_count}})</span></a>
<a class="nav-link" href="/@{{u.username}}">Wall</a>
</li>
<li class="nav-item">
<a class="nav-link {% if not 'saved' in request.path %}active{% endif %}" href="/@{{u.username}}/posts">Posts <span class="count">({{u.post_count}})</span></a>
</li>
<li class="nav-item">
<a class="nav-link" href="/@{{u.username}}/comments">Comments <span class="count">({{u.comment_count}})</span></a>
@ -595,12 +598,12 @@
</nav>
{% endif %}
{% if not request.path.endswith('/comments') %}
{% if not request.path.endswith('/comments') and not request.path.endswith(u.username) %}
<script defer src="{{'js/vendor/marked.js' | asset}}"></script>
<script defer src="{{'js/markdown.js' | asset}}"></script>
{% endif %}
{% if v and v.id != u.id and '/comments' not in request.path %}
{% if v and v.id != u.id and not request.path.endswith('/comments') and not request.path.endswith(u.username) %}
{% include "modals/emoji.html" %}
{% endif %}

View File

@ -1,4 +1,4 @@
{% extends "userpage.html" %}
{% extends "userpage/userpage.html" %}
{% block pagetype %}userpage{% endblock %}
{% block desktopBanner %}{% endblock %}
{% block desktopUserBanner %}{% endblock %}

View File

@ -0,0 +1,114 @@
{% extends "userpage/userpage.html" %}
{% block content %}
<div class="row no-gutters">
<div class="col">
<div class="flex-row box-shadow-bottom d-flex justify-content-center justify-content-md-between align-items-center">
<ul class="nav settings-nav" id="profile-content--nav">
<li class="nav-item">
<a class="nav-link active" href="/@{{u.username}}">Wall</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/@{{u.username}}/posts">Posts <span class="count">({{u.post_count}})</span></a>
</li>
<li class="nav-item">
<a class="nav-link" href="/@{{u.username}}/comments">Comments <span class="count">({{u.comment_count}})</span></a>
</li>
{% if u.id == v.id %}
<li class="nav-item">
<a class="nav-link" href="/@{{u.username}}/saved/posts">Saved Posts <span class="count">({{u.saved_count}})</span></a>
</li>
<li class="nav-item">
<a class="nav-link {% if 'saved' in request.path %}active{% endif %}" href="/@{{u.username}}/saved/comments">Saved Comments <span class="count">({{u.saved_comment_count}})</span></a>
</li>
<li class="nav-item">
<a class="nav-link" href="/@{{u.username}}/subscribed/posts">Subscribed <span class="count">({{u.subscribed_count}})</span></a>
</li>
{% endif %}
</ul>
</div>
</div>
</div>
{% if v %}
<div id="comment-form-space-{{u.fullname}}" class="comment-write mb-3 mt-4">
<form id="reply-to-{{u.fullname}}" action="/comment" method="post">
<input type="hidden" name="formkey" value="{{v|formkey}}">
<input type="hidden" name="parent_fullname" value="p_{{u.id}}">
<input autocomplete="off" id="reply-form-submission-{{u.fullname}}" type="hidden" name="submission" value="{{u.id}}">
<textarea required autocomplete="off" {% if v.longpost %}minlength="280"{% elif v.bird %}maxlength="140"{% endif %} minlength="1" maxlength="10000" data-preview="form-preview-{{u.fullname}}" oninput="markdown(this);charLimit('reply-form-body-{{u.fullname}}','charcount-{{u.fullname}}')" id="reply-form-body-{{u.fullname}}" data-fullname="{{u.fullname}}" class="comment-box form-control rounded" name="body" form="reply-to-{{u.fullname}}" aria-label="With textarea" placeholder="Add your comment..." rows="3"></textarea>
<div class="text-small font-weight-bold mt-1" id="charcount-{{u.fullname}}" style="right: 1rem; bottom: 0.5rem; z-index: 3;"></div>
<div class="comment-format">
<label class="btn btn-secondary format d-inline-block m-0" for="gif-reply-btn-{{u.fullname}}">
<span id="gif-reply-btn-{{u.fullname}}" class="font-weight-bolder text-uppercase" onclick="commentForm('reply-form-body-{{u.fullname}}');getGif()" aria-hidden="true" data-bs-toggle="modal" data-bs-target="#gifModal" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Add GIF">GIF</span>
</label>
&nbsp;
<div onclick="loadEmojis('reply-form-body-{{u.fullname}}')" class="btn btn-secondary format d-inline-block m-0" id="emoji-reply-btn-{{u.fullname}}" aria-hidden="true" data-bs-toggle="modal" data-bs-target="#emojiModal" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Add Emoji"><i class="fas fa-smile-beam"></i></div>
&nbsp;
<label class="format btn btn-secondary m-0 ml-1 {% if v %}d-inline-block{% else %}d-none{% endif %}" for="file-upload-reply-{{u.fullname}}">
<div id="filename-show-reply-{{u.fullname}}"><i class="fas fa-file"></i></div>
<input autocomplete="off" id="file-upload-reply-{{u.fullname}}" accept="image/*, video/*, audio/*" type="file" multiple="multiple" name="file" {% if g.is_tor %}disabled{% endif %} onchange="changename('filename-show-reply-{{u.fullname}}','file-upload-reply-{{u.fullname}}')" hidden>
</label>
</div>
<button type="button" id="save-reply-to-{{u.fullname}}" form="reply-to-{{u.fullname}}" class="btn btn-primary text-whitebtn ml-auto fl-r" onclick="post_comment('{{u.fullname}}', '{{u.id}}');remove_dialog()">Comment</button>
</form>
<div id="form-preview-{{u.fullname}}" class="preview mb-3 mt-5"></div>
<div class="form-text text-small p-0 m-0"><a href="/formatting" {% if v and v.newtab %}data-target="t"target="_blank"{% endif %}>Formatting help</a></div>
</div>
{% else %}
<div class="comment-write mb-3 mt-4">
<textarea autocomplete="off" maxlength="10000" class="comment-box form-control rounded" name="body" aria-label="With textarea" placeholder="Add your comment..." rows="3" onclick="location.href='/login?redirect={{request.full_path | urlencode}}';"></textarea>
</div>
<div class="card border-0 mt-4">
<div class="card-body">
<h5 class="card-title">Jump in the discussion.</h5>
<p class="card-text">No email address required.</p>
<div>
<a href="/signup?redirect={{request.full_path | urlencode}}" class="btn btn-primary">Sign up</a>
<a href="/login?redirect={{request.full_path | urlencode}}" class="btn btn-link text-muted">Sign in</a>
</div>
</div>
</div>
{% endif %}
<div class="row no-gutters {% if listing %}mt-md-3{% elif not listing %}my-md-3{% endif %}" style="margin-top: 10px;">
<div class="col">
<div class="comment-section" id="replies-of-{{u.fullname}}">
{% with comments=listing %}
{% include "comments.html" %}
{% endwith %}
</div>
{% if not listing %}
<div class="text-center px-3 my-3">
<span class="fa-stack fa-2x text-muted mb-4">
<i class="fas fa-square text-gray-500 opacity-25 fa-stack-2x"></i>
<i class="fas text-gray-500 fa-scroll-old fa-stack-1x text-lg"></i>
</span>
<h5>There's no comments on your wall yet!</h5>
</div>
{% endif %}
</div>
</div>
{% if u.song %}
{% if v and v.id == u.id %}
<div id="v_username" class="d-none">{{v.username}}</div>
{% else %}
<div id="u_username" class="d-none">{{u.username}}</div>
{% endif %}
{% endif %}
{% if v %}
<div id='tax' class="d-none">{% if v.patron or u.patron %}0{% else %}0.03{% endif %}</div>
<div id="username" class="d-none">{{u.username}}</div>
<script defer src="{{'js/userpage_v.js' | asset}}"></script>
{% endif %}
<script defer src="{{'js/userpage.js' | asset}}"></script>
{% endblock %}