mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-01-23 11:26:56 -08:00
Merge branch 'main' into feature/image_post_editing
This commit is contained in:
commit
3f1f97b2ee
25 changed files with 551 additions and 220 deletions
|
@ -300,6 +300,12 @@ def user_profile(actor):
|
|||
actor_data['source'] = {'content': user.about, 'mediaType': 'text/markdown'}
|
||||
if user.matrix_user_id and main_user_name:
|
||||
actor_data['matrixUserId'] = user.matrix_user_id
|
||||
if user.extra_fields.count() > 0:
|
||||
actor_data['attachment'] = []
|
||||
for field in user.extra_fields:
|
||||
actor_data['attachment'].append({'type': 'PropertyValue',
|
||||
'name': field.label,
|
||||
'value': field.text})
|
||||
resp = jsonify(actor_data)
|
||||
resp.content_type = 'application/activity+json'
|
||||
resp.headers.set('Link', f'<https://{current_app.config["SERVER_NAME"]}/u/{actor}>; rel="alternate"; type="text/html"')
|
||||
|
|
|
@ -16,7 +16,8 @@ from sqlalchemy.exc import IntegrityError
|
|||
from app import db, cache, constants, celery
|
||||
from app.models import User, Post, Community, BannedInstances, File, PostReply, AllowedInstances, Instance, utcnow, \
|
||||
PostVote, PostReplyVote, ActivityPubLog, Notification, Site, CommunityMember, InstanceRole, Report, Conversation, \
|
||||
Language, Tag, Poll, PollChoice, UserFollower, CommunityBan, CommunityJoinRequest, NotificationSubscription, Licence
|
||||
Language, Tag, Poll, PollChoice, UserFollower, CommunityBan, CommunityJoinRequest, NotificationSubscription, \
|
||||
Licence, UserExtraField
|
||||
from app.activitypub.signature import signed_get_request, post_request
|
||||
import time
|
||||
from app.constants import *
|
||||
|
@ -522,6 +523,11 @@ def refresh_user_profile_task(user_id):
|
|||
user.about_html = markdown_to_html(user.about) # prefer Markdown if provided, overwrite version obtained from HTML
|
||||
else:
|
||||
user.about = html_to_text(user.about_html)
|
||||
if 'attachment' in activity_json and isinstance(activity_json['attachment'], list):
|
||||
user.extra_fields = []
|
||||
for field_data in activity_json['attachment']:
|
||||
if field_data['type'] == 'PropertyValue':
|
||||
user.extra_fields.append(UserExtraField(label=field_data['name'].strip(), text=field_data['value'].strip()))
|
||||
if 'type' in activity_json:
|
||||
user.bot = True if activity_json['type'] == 'Service' else False
|
||||
user.ap_fetched_at = utcnow()
|
||||
|
@ -769,6 +775,11 @@ def actor_json_to_model(activity_json, address, server):
|
|||
cover = File(source_url=activity_json['image']['url'])
|
||||
user.cover = cover
|
||||
db.session.add(cover)
|
||||
if 'attachment' in activity_json and isinstance(activity_json['attachment'], list):
|
||||
user.extra_fields = []
|
||||
for field_data in activity_json['attachment']:
|
||||
if field_data['type'] == 'PropertyValue':
|
||||
user.extra_fields.append(UserExtraField(label=field_data['name'].strip(), text=field_data['value'].strip()))
|
||||
try:
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
|
|
22
app/cli.py
22
app/cli.py
|
@ -27,7 +27,7 @@ from app.models import Settings, BannedInstances, Interest, Role, User, RolePerm
|
|||
from app.post.routes import post_delete_post
|
||||
from app.utils import file_get_contents, retrieve_block_list, blocked_domains, retrieve_peertube_block_list, \
|
||||
shorten_string, get_request, html_to_text, blocked_communities, ap_datetime, gibberish, get_request_instance, \
|
||||
instance_banned
|
||||
instance_banned, recently_upvoted_post_replies, recently_upvoted_posts, jaccard_similarity
|
||||
|
||||
|
||||
def register(app):
|
||||
|
@ -464,6 +464,26 @@ def register(app):
|
|||
db.session.query(ActivityPubLog).filter(ActivityPubLog.created_at < utcnow() - timedelta(days=3)).delete()
|
||||
db.session.commit()
|
||||
|
||||
@app.cli.command("detect_vote_manipulation")
|
||||
def detect_vote_manipulation():
|
||||
with app.app_context():
|
||||
print('Getting user ids...')
|
||||
all_user_ids = [user.id for user in User.query.filter(User.last_seen > datetime.utcnow() - timedelta(days=7))]
|
||||
print('Checking...')
|
||||
for i, first_user_id in enumerate(all_user_ids):
|
||||
current_user_upvoted_posts = ['post/' + str(id) for id in recently_upvoted_posts(first_user_id)]
|
||||
current_user_upvoted_replies = ['reply/' + str(id) for id in recently_upvoted_post_replies(first_user_id)]
|
||||
|
||||
current_user_upvotes = set(current_user_upvoted_posts + current_user_upvoted_replies)
|
||||
if len(current_user_upvotes) > 12:
|
||||
print(i)
|
||||
for j in range(i + 1, len(all_user_ids)):
|
||||
other_user_id = all_user_ids[j]
|
||||
if jaccard_similarity(current_user_upvotes, other_user_id) >= 95:
|
||||
first_user = User.query.get(first_user_id)
|
||||
other_user = User.query.get(other_user_id)
|
||||
print(f'{first_user.link()} votes the same as {other_user.link()}')
|
||||
|
||||
@app.cli.command("migrate_community_notifs")
|
||||
def migrate_community_notifs():
|
||||
with app.app_context():
|
||||
|
|
|
@ -726,6 +726,7 @@ class User(UserMixin, db.Model):
|
|||
activity = db.relationship('ActivityLog', backref='account', lazy='dynamic', cascade="all, delete-orphan")
|
||||
posts = db.relationship('Post', lazy='dynamic', cascade="all, delete-orphan")
|
||||
post_replies = db.relationship('PostReply', lazy='dynamic', cascade="all, delete-orphan")
|
||||
extra_fields = db.relationship('UserExtraField', lazy='dynamic', cascade="all, delete-orphan")
|
||||
|
||||
roles = db.relationship('Role', secondary=user_role, lazy='dynamic', cascade="all, delete")
|
||||
|
||||
|
@ -2056,6 +2057,13 @@ class UserNote(db.Model):
|
|||
created_at = db.Column(db.DateTime, default=utcnow)
|
||||
|
||||
|
||||
class UserExtraField(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), index=True)
|
||||
label = db.Column(db.String(50))
|
||||
text = db.Column(db.String(256))
|
||||
|
||||
|
||||
class UserBlock(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
blocker_id = db.Column(db.Integer, db.ForeignKey('user.id'), index=True)
|
||||
|
|
|
@ -680,7 +680,7 @@ def add_reply(post_id: int, comment_id: int):
|
|||
inoculation=inoculation[randint(0, len(inoculation) - 1)] if g.site.show_inoculation_block else None)
|
||||
|
||||
|
||||
@bp.route('/post/<int:post_id>/options', methods=['GET'])
|
||||
@bp.route('/post/<int:post_id>/options_menu', methods=['GET'])
|
||||
def post_options(post_id: int):
|
||||
post = Post.query.get_or_404(post_id)
|
||||
if post.deleted:
|
||||
|
@ -701,7 +701,7 @@ def post_options(post_id: int):
|
|||
menu_topics=menu_topics(), site=g.site)
|
||||
|
||||
|
||||
@bp.route('/post/<int:post_id>/comment/<int:comment_id>/options', methods=['GET'])
|
||||
@bp.route('/post/<int:post_id>/comment/<int:comment_id>/options_menu', methods=['GET'])
|
||||
def post_reply_options(post_id: int, comment_id: int):
|
||||
post = Post.query.get_or_404(post_id)
|
||||
post_reply = PostReply.query.get_or_404(comment_id)
|
||||
|
|
|
@ -981,6 +981,13 @@ time {
|
|||
height: 44px;
|
||||
line-height: 44px;
|
||||
}
|
||||
.post_utilities_bar div .dropdown-item {
|
||||
white-space: unset;
|
||||
line-height: 30px;
|
||||
}
|
||||
.post_utilities_bar div .dropdown-item:active {
|
||||
line-height: 26px;
|
||||
}
|
||||
.post_utilities_bar .notify_toggle {
|
||||
margin-left: auto; /* pull right */
|
||||
}
|
||||
|
@ -1343,7 +1350,7 @@ time {
|
|||
display: inline;
|
||||
}
|
||||
}
|
||||
.comment .comment_author img {
|
||||
.comment .comment_author .author_link img {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border-radius: 50%;
|
||||
|
@ -1384,12 +1391,23 @@ time {
|
|||
margin-left: auto; /* pull right */
|
||||
font-size: 87%;
|
||||
}
|
||||
.comment .comment_actions .dropdown-item {
|
||||
white-space: unset;
|
||||
line-height: 30px;
|
||||
}
|
||||
.comment .comment_actions .dropdown-item:active {
|
||||
line-height: 26px;
|
||||
}
|
||||
.comment .replies {
|
||||
margin-top: 0;
|
||||
border-left: solid 1px #ddd;
|
||||
border-top: solid 1px #ddd;
|
||||
}
|
||||
|
||||
.hide-labels label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#add_local_community_form #url {
|
||||
width: 297px;
|
||||
display: inline-block;
|
||||
|
@ -1480,10 +1498,6 @@ fieldset legend {
|
|||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.list-group-item:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.skip-link:focus {
|
||||
top: 0;
|
||||
}
|
||||
|
@ -1701,6 +1715,9 @@ h1 .warning_badge {
|
|||
.side_pane img {
|
||||
max-width: 100%;
|
||||
}
|
||||
.side_pane .list-group-item:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
[data-bs-theme=dark] .main_pane {
|
||||
border-color: #424549;
|
||||
|
@ -1942,6 +1959,32 @@ form h5 {
|
|||
padding-top: 0.12rem !important;
|
||||
}
|
||||
|
||||
.render_username {
|
||||
position: relative;
|
||||
}
|
||||
.render_username .author_link {
|
||||
display: inline-block;
|
||||
}
|
||||
@media (min-width: 1280px) {
|
||||
.render_username .author_link:hover + .user_preview, .render_username .user_preview:hover {
|
||||
display: inline-block !important;
|
||||
position: absolute;
|
||||
top: 17px;
|
||||
left: 0;
|
||||
background-color: white;
|
||||
z-index: 20;
|
||||
}
|
||||
.render_username .user_preview .card {
|
||||
width: 300px;
|
||||
}
|
||||
.render_username .user_preview .card .preview_avatar_image {
|
||||
max-width: 50%;
|
||||
}
|
||||
.render_username .user_preview .card .preview_avatar_image img {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* high contrast */
|
||||
@media (prefers-contrast: more) {
|
||||
:root {
|
||||
|
|
|
@ -581,6 +581,15 @@ time {
|
|||
min-width: $min-touch-target;
|
||||
height: $min-touch-target;
|
||||
line-height: $min-touch-target;
|
||||
|
||||
.dropdown-item {
|
||||
white-space: unset;
|
||||
line-height: 30px;
|
||||
|
||||
&:active {
|
||||
line-height: 26px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notify_toggle {
|
||||
|
@ -1001,7 +1010,7 @@ time {
|
|||
}
|
||||
}
|
||||
|
||||
.comment_author {
|
||||
.comment_author .author_link {
|
||||
img {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
|
@ -1046,6 +1055,15 @@ time {
|
|||
margin-left: auto; /* pull right */
|
||||
font-size: 87%;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
white-space: unset;
|
||||
line-height: 30px;
|
||||
|
||||
&:active {
|
||||
line-height: 26px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.replies {
|
||||
|
@ -1055,6 +1073,10 @@ time {
|
|||
}
|
||||
}
|
||||
|
||||
.hide-labels label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#add_local_community_form {
|
||||
#url {
|
||||
width: 297px;
|
||||
|
@ -1148,10 +1170,6 @@ fieldset {
|
|||
}
|
||||
}
|
||||
|
||||
.list-group-item:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.skip-link:focus {
|
||||
top: 0;
|
||||
}
|
||||
|
@ -1388,6 +1406,10 @@ h1 .warning_badge {
|
|||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.list-group-item:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
[data-bs-theme=dark] .main_pane {
|
||||
|
@ -1653,6 +1675,37 @@ form {
|
|||
padding-top: .12rem !important;
|
||||
}
|
||||
|
||||
.render_username {
|
||||
position: relative;
|
||||
|
||||
.author_link {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@include breakpoint(laptop) {
|
||||
.author_link:hover + .user_preview, .user_preview:hover {
|
||||
display: inline-block !important;
|
||||
position: absolute;
|
||||
top: 17px;
|
||||
left: 0;
|
||||
background-color: white;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.user_preview .card {
|
||||
width: 300px;
|
||||
|
||||
.preview_avatar_image {
|
||||
max-width: 50%;
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* high contrast */
|
||||
@import "scss/high_contrast";
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
[deleted]
|
||||
{% endif -%}
|
||||
{% else -%}
|
||||
<a href="/u/{{ user.link() }}" title="{{ user.ap_id if user.ap_id != none else user.user_name }}" aria-label="{{ _('Author') }}">
|
||||
<a href="/u/{{ user.link() }}" aria-label="{{ _('Author') }}" class="author_link" title="">
|
||||
{% if user.avatar_id and not low_bandwidth and not collapsed -%}
|
||||
<img src="{{ user.avatar_thumbnail() }}" alt="" loading="lazy" />
|
||||
{% endif -%}
|
||||
|
@ -33,6 +33,12 @@
|
|||
<span class="user_note" title="{{ _('User note: %(note)s', note=user_note) }}">[{{ user_note | truncate(12, True) }}]</span>
|
||||
{% endif -%}
|
||||
{% endif -%}
|
||||
<div class="d-none user_preview" id="preview_{{ user.id }}"
|
||||
hx-get="{{ url_for('user.user_preview', user_id=user.id) }}"
|
||||
hx-trigger="intersect once"
|
||||
hx-target="this"
|
||||
hx-swap="innerHTML"
|
||||
></div>
|
||||
{% endif -%}
|
||||
</span>
|
||||
{% endmacro -%}
|
||||
|
|
|
@ -1,36 +1,15 @@
|
|||
{% if theme() and file_exists('app/templates/themes/' + theme() + '/base.html') %}
|
||||
{% extends 'themes/' + theme() + '/base.html' %}
|
||||
{% else %}
|
||||
{% extends "base.html" %}
|
||||
{% endif %}
|
||||
{% set active_child = 'chats' %}
|
||||
{% from 'bootstrap/form.html' import render_form %}
|
||||
|
||||
{% block app_content %}
|
||||
<div class="row">
|
||||
<div class="col col-login mx-auto">
|
||||
<div class="card mt-5">
|
||||
<div class="card-body p-6">
|
||||
<div class="card-title">{{ _('Options for conversation with "%(member_names)s"', member_names=conversation.member_names(current_user.id)) }}</div>
|
||||
<ul class="option_list">
|
||||
<li><a href="{{ url_for('chat.chat_delete', conversation_id=conversation.id) }}" class="no-underline confirm_first" rel="nofollow"><span class="fe fe-delete"></span>
|
||||
{{ _('Delete conversation') }}</a></li>
|
||||
{% for member in conversation.members %}
|
||||
{% if member.id != current_user.id %}
|
||||
<li><a href="{{ url_for('user.block_profile', actor=member.link()) }}" class="no-underline"><span class="fe fe-block"></span>
|
||||
{{ _('Block @%(author_name)s', author_name=member.display_name()) }}</a></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% for instance in conversation.instances() %}
|
||||
<li><a href="{{ url_for('chat.block_instance', instance_id=instance.id) }}" class="no-underline"><span class="fe fe-block"></span>
|
||||
{{ _("Block chats and posts from instance: %(name)s", name=instance.domain) }}</a></li>
|
||||
{% endfor %}
|
||||
<li><a href="{{ url_for('chat.chat_report', conversation_id=conversation.id) }}" class="no-underline" rel="nofollow"><span class="fe fe-report"></span>
|
||||
{{ _('Report to moderators') }}</a></li>
|
||||
</ul>
|
||||
<p>{{ _('If you are reporting abuse then do not delete the conversation - moderators will not be able to read it if you delete it.') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
<li><a href="{{ url_for('chat.chat_delete', conversation_id=conversation.id) }}" class="dropdown-item no-underline confirm_first" rel="nofollow"><span class="fe fe-delete"></span>
|
||||
{{ _('Delete conversation') }}</a></li>
|
||||
{% for member in conversation.members %}
|
||||
{% if member.id != current_user.id %}
|
||||
<li><a href="{{ url_for('user.block_profile', actor=member.link()) }}" class="dropdown-item no-underline"><span class="fe fe-block"></span>
|
||||
{{ _('Block @%(author_name)s', author_name=member.display_name()) }}</a></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% for instance in conversation.instances() %}
|
||||
<li><a href="{{ url_for('chat.block_instance', instance_id=instance.id) }}" class="dropdown-item no-underline"><span class="fe fe-block"></span>
|
||||
{{ _("Block chats and posts from instance: %(name)s", name=instance.domain) }}</a></li>
|
||||
{% endfor %}
|
||||
<li><a href="{{ url_for('chat.chat_report', conversation_id=conversation.id) }}" class="dropdown-item no-underline" rel="nofollow"><span class="fe fe-report"></span>
|
||||
{{ _('Report to moderators') }}</a></li>
|
||||
<p class="p-2" style="max-width: 200px;">{{ _('If you are reporting abuse then do not delete the conversation - moderators will not be able to read it if you delete it.') }}</p>
|
||||
|
|
|
@ -72,7 +72,23 @@
|
|||
</div>
|
||||
{% endfor %}
|
||||
{{ render_form(form) }}
|
||||
<a class="conversation_options btn btn-outline-secondary" href="{{ url_for('chat.chat_options', conversation_id=current_conversation) }}" class="btn btn-outline-secondary">{{ _('Options') }}</a>
|
||||
<div class="dropdown">
|
||||
<a
|
||||
class="conversation_options btn btn-outline-secondary"
|
||||
data-bs-toggle="dropdown" rel="nofollow noindex"
|
||||
href="{{ url_for('chat.chat_options', conversation_id=current_conversation) }}"
|
||||
class="btn btn-outline-secondary">
|
||||
{{ _('Options') }}
|
||||
</a>
|
||||
<ul class="dropdown-menu" style="max-width: 240px">
|
||||
<div
|
||||
hx-get="{{ url_for('chat.chat_options', conversation_id=current_conversation) }}"
|
||||
hx-trigger="intersect once"
|
||||
hx-target="this"
|
||||
hx-swap="outerHTML"
|
||||
></div>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
@ -80,4 +96,4 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -16,9 +16,9 @@
|
|||
{% endif -%}
|
||||
<p>{% if post.reports > 0 and current_user.is_authenticated and post.community.is_moderator(current_user) -%}
|
||||
<span class="red fe fe-report" title="{{ _('Reported. Check post for issues.') }}"></span>
|
||||
{% endif -%}<small>submitted {{ arrow.get(post.posted_at).humanize(locale=locale) }} by
|
||||
{% endif -%}<small>submitted <time datetime="{{ arrow.get(post.posted_at).format('YYYY-MM-DD HH:mm:ss ZZ') }}" title="{{ arrow.get(post.posted_at).format('YYYY-MM-DD HH:mm:ss ZZ') }}">{{ arrow.get(post.posted_at).humanize(locale=locale) }}</time> by
|
||||
{{ render_username(post.author) }}
|
||||
{% if post.edited_at -%} edited {{ arrow.get(post.edited_at).humanize(locale=locale) }}{% endif -%}</small>
|
||||
{% if post.edited_at -%} edited <time datetime="{{ arrow.get(post.posted_at).format('YYYY-MM-DD HH:mm:ss ZZ') }}" title="{{ arrow.get(post.posted_at).format('YYYY-MM-DD HH:mm:ss ZZ') }}">{{ arrow.get(post.edited_at).humanize(locale=locale) }}{% endif -%}</time></small>
|
||||
</p>
|
||||
{% if post.type == POST_TYPE_IMAGE -%}
|
||||
<div class="post_image">
|
||||
|
@ -181,7 +181,19 @@
|
|||
{% endif -%}
|
||||
</div>
|
||||
<div class="post_options_link">
|
||||
<a href="{{ url_for('post.post_options', post_id=post.id) }}" rel="nofollow"><span class="fe fe-options" title="Options"> </span></a>
|
||||
<div class="dropdown">
|
||||
<a href="{{ url_for('post.post_options', post_id=post.id) if low_bandwidth else '#' }}"
|
||||
data-bs-toggle="dropdown"
|
||||
rel="nofollow"><span class="fe fe-options" title="Options"></span></a>
|
||||
<ul class="dropdown-menu" style="width: 320px">
|
||||
<div
|
||||
hx-get="{{ url_for('post.post_options', post_id=post.id) }}"
|
||||
hx-trigger="intersect once"
|
||||
hx-target="this"
|
||||
hx-swap="outerHTML"
|
||||
></div>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
{% endif -%}
|
||||
</div>
|
||||
<div class="col-auto text-muted small pt-05">
|
||||
{{ arrow.get(post_reply.posted_at).humanize(locale=locale) }}{% if post_reply.edited_at -%}, edited {{ arrow.get(post_reply.edited_at).humanize(locale=locale) }}{% endif -%}
|
||||
<time datetime="{{ arrow.get(post_reply.posted_at).format('YYYY-MM-DD HH:mm:ss ZZ') }}" title="{{ arrow.get(post_reply.posted_at).format('YYYY-MM-DD HH:mm:ss ZZ') }}">{{ arrow.get(post_reply.posted_at).humanize(locale=locale) }}</time>{% if post_reply.edited_at -%}, edited <time datetime="{{ arrow.get(post_reply.posted_at) }}" title="{{ arrow.get(post_reply.posted_at) }}">{{ arrow.get(post_reply.edited_at).humanize(locale=locale) }}</time>{% endif -%}
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
{% if post_reply.reports and current_user.is_authenticated and post_reply.post.community.is_moderator(current_user) -%}
|
||||
|
@ -95,7 +95,22 @@
|
|||
</div>
|
||||
<div class="comment_actions_link">
|
||||
{% if not post_reply.post.deleted -%}
|
||||
<a href="{{ url_for('post.post_reply_options', post_id=post_reply.post.id, comment_id=post_reply.id) }}" rel="nofollow noindex" aria-label="{{ _('Comment options') }}"><span class="fe fe-options" title="Options"> </span></a>
|
||||
<div class="dropdown">
|
||||
<a
|
||||
href="{{ url_for('post.post_reply_options', post_id=post_reply.post.id, comment_id=post_reply.id) if low_bandwidth else '#' }}"
|
||||
data-bs-toggle="dropdown" rel="nofollow noindex"
|
||||
aria-label="{{ _('Comment options') }}">
|
||||
<span class="fe fe-options" title="Options"> </span>
|
||||
</a>
|
||||
<ul class="dropdown-menu" style="width: 320px">
|
||||
<div
|
||||
hx-get="{{ url_for('post.post_reply_options', post_id=post_reply.post.id, comment_id=post_reply.id) }}"
|
||||
hx-trigger="intersect once"
|
||||
hx-target="this"
|
||||
hx-swap="outerHTML"
|
||||
></div>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif -%}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
{# do nothing - blocked by keyword filter #}
|
||||
{% else -%}
|
||||
<div class="h-entry pb-0 post_teaser type_{{ post.type }}{{ ' reported' if post.reports > 0 and current_user.is_authenticated and post.community.is_moderator() }}{{ ' blocked' if content_blocked }}{{ ' blur' if blur_content }}"
|
||||
{% if content_blocked -%} title="{{ _('Filtered: ') }}{{ content_blocked }}"{% else %} title="Post: {{ post.title }}" aria-label="Post: {{ post.title }}"{% endif -%} tabindex="0">
|
||||
{% if content_blocked -%} title="{{ _('Filtered: ') }}{{ content_blocked }}"{% else %} title="Post: {{ post.title }}" aria-label="Post: {{ post.title }}"{% endif %} tabindex="0">
|
||||
<div class="row">
|
||||
{% if post.type == POST_TYPE_ARTICLE %}
|
||||
{% include "post/post_teaser/_article.html" -%}
|
||||
|
|
|
@ -7,19 +7,23 @@
|
|||
</div>
|
||||
{% endif -%}
|
||||
<span title="{{ post.up_votes }}, {{ post.down_votes }}" aria-live="assertive" aria-label="{{ _('Score: ') }}{{ post.up_votes - post.down_votes }}.">{{ shorten_number(post.up_votes - post.down_votes) }}</span>
|
||||
{% if can_downvote(current_user, post.community) and not disable_voting -%}
|
||||
{%- if can_downvote(current_user, post.community) and not disable_voting -%}
|
||||
<div class="downvote_button {{ 'voted_down' if in_sorted_list(recently_downvoted, post.id) }}" role="button" aria-label="{{ _('DownVote button, %(count)d downvotes so far.', count=post.down_votes) }}" aria-live="assertive"
|
||||
hx-post="/post/{{ post.id }}/downvote" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons_new" tabindex="0">
|
||||
<span class="fe fe-arrow-down"></span>
|
||||
<img class="htmx-indicator" src="/static/images/spinner.svg" alt="" style="opacity: 0;">
|
||||
</div>
|
||||
{% endif -%}
|
||||
{%- endif -%}
|
||||
{% else -%}
|
||||
<div class="upvote_button digits_{{ digits(post.up_votes) }} {{ upvoted_class }} redirect_login">
|
||||
<span class="fe fe-arrow-up"></span>
|
||||
</div>
|
||||
<span title="{{ post.up_votes }}, {{ post.down_votes }}" aria-live="assertive" aria-label="{{ _('Score: ') }}{{ post.up_votes - post.down_votes }}.">{{ shorten_number(post.up_votes - post.down_votes) }}</span>
|
||||
<div class="downvote_button digits_{{ digits(post.down_votes) }} {{ downvoted_class }} redirect_login">
|
||||
<span class="fe fe-arrow-down"></span>
|
||||
</div>
|
||||
{% if not disable_voting -%}
|
||||
<div class="upvote_button digits_{{ digits(post.up_votes) }} {{ upvoted_class }} redirect_login">
|
||||
<span class="fe fe-arrow-up"></span>
|
||||
</div>
|
||||
{% endif -%}
|
||||
<span title="{{ post.up_votes }}, {{ post.down_votes }}" aria-live="assertive" aria-label="{{ _('Score: ') }}{{ post.up_votes - post.down_votes }}.">{{ shorten_number(post.up_votes - post.down_votes) }}</span>
|
||||
{%- if not disable_voting -%}
|
||||
<div class="downvote_button digits_{{ digits(post.down_votes) }} {{ downvoted_class }} redirect_login">
|
||||
<span class="fe fe-arrow-down"></span>
|
||||
</div>
|
||||
{%- endif -%}
|
||||
{% endif -%}
|
||||
|
|
|
@ -1,83 +1,62 @@
|
|||
{% if theme() and file_exists('app/templates/themes/' + theme() + '/base.html') -%}
|
||||
{% extends 'themes/' + theme() + '/base.html' -%}
|
||||
{% else -%}
|
||||
{% extends "base.html" -%}
|
||||
{% endif -%} -%}
|
||||
{% from 'bootstrap/form.html' import render_form -%}
|
||||
|
||||
{% block app_content -%}
|
||||
<div class="row">
|
||||
<div class="col col-login mx-auto">
|
||||
<div class="card mt-5">
|
||||
<div class="card-body p-6">
|
||||
<div class="card-title">{{ _('Options for "%(post_title)s"', post_title=post.title) }} {% if current_user.is_authenticated -%}{% include 'post/_post_notification_toggle.html' -%}{% endif -%}</div>
|
||||
<ul class="option_list">
|
||||
{% if current_user.is_authenticated -%}
|
||||
{% if post.user_id == current_user.id -%}
|
||||
<li><a href="{{ url_for('post.post_edit', post_id=post.id) }}" class="no-underline" rel="nofollow"><span class="fe fe-edit"></span>
|
||||
{{ _('Edit') }}</a></li>
|
||||
{% endif -%}
|
||||
{% if post.user_id == current_user.id or post.community.is_moderator() or post.community.is_owner() or current_user.is_admin() -%}
|
||||
{% if post.deleted -%}
|
||||
<li><a href="{{ url_for('post.post_restore', post_id=post.id) }}" class="no-underline confirm_first" rel="nofollow"><span class="fe fe-arrow-up"></span>
|
||||
{{ _('Restore') }}</a></li>
|
||||
<li><a href="{{ url_for('post.post_purge', post_id=post.id) }}" class="no-underline confirm_first" rel="nofollow"><span class="fe fe-delete red"></span>
|
||||
{{ _('Purge') }}</a></li>
|
||||
{% else -%}
|
||||
<li><a href="{{ url_for('post.post_delete', post_id=post.id) }}" class="no-underline confirm_first" rel="nofollow"><span class="fe fe-delete"></span>
|
||||
{{ _('Delete') }}</a></li>
|
||||
{% endif -%}
|
||||
{% endif -%}
|
||||
{% if existing_bookmark -%}
|
||||
<li><a href="{{ url_for('post.post_remove_bookmark', post_id=post.id) }}" class="no-underline" rel="nofollow"><span class="fe fe-bookmark"></span>
|
||||
{{ _('Remove bookmark') }}</a></li>
|
||||
{% else -%}
|
||||
<li><a href="{{ url_for('post.post_bookmark', post_id=post.id) }}" class="no-underline" rel="nofollow"><span class="fe fe-bookmark"></span>
|
||||
{{ _('Bookmark') }}</a></li>
|
||||
{% endif -%}
|
||||
{% if post.user_id == current_user.id and not post.mea_culpa -%}
|
||||
<li><a href="{{ url_for('post.post_mea_culpa', post_id=post.id) }}" class="no-underline"><span class="fe fe-mea-culpa"></span>
|
||||
{{ _("I made a mistake with this post and have changed my mind about the topic") }}</a></li>
|
||||
{% endif -%}
|
||||
{% if post.user_id != current_user.id -%}
|
||||
{% if post.type == POST_TYPE_LINK and post.author.bot and (post.cross_posts is none or len(post.cross_posts) == 0) -%}
|
||||
<li><a class="no-underline" aria-label="{{ _('Cross-post') }}" href="{{ url_for('post.post_cross_post', post_id=post.id) }}"><span class="fe fe-cross-post"></span>
|
||||
{{ _('Cross-post to another community') }}</a></li>
|
||||
{% endif -%}
|
||||
<li><a href="{{ url_for('post.post_block_user', post_id=post.id) }}" class="no-underline"><span class="fe fe-block"></span>
|
||||
{{ _('Block post author @%(author_name)s', author_name=post.author.user_name) }}</a></li>
|
||||
<li><a href="{{ url_for('post.post_block_community', post_id=post.id) }}" class="no-underline"><span class="fe fe-block"></span>
|
||||
{{ _('Block community %(community_name)s', community_name=post.community.display_name()) }}</a></li>
|
||||
{% if post.community.is_moderator() or current_user.is_admin() -%}
|
||||
<li><a href="{{ url_for('community.community_ban_user', community_id=post.community.id, user_id=post.author.id) }}" class="no-underline"><span class="fe fe-block red"></span>
|
||||
{{ _('Ban post author @%(author_name)s from %(community_name)s', author_name=post.author.user_name, community_name=post.community.title) }}</a></li>
|
||||
{% endif -%}
|
||||
{% if post.domain_id -%}
|
||||
<li><a href="{{ url_for('post.post_block_domain', post_id=post.id) }}" class="no-underline"><span class="fe fe-block"></span>
|
||||
{{ _('Block domain %(domain)s', domain=post.domain.name) }}</a></li>
|
||||
{% endif -%}
|
||||
{% if post.instance_id and post.instance_id != 1 -%}
|
||||
<li><a href="{{ url_for('post.post_block_instance', post_id=post.id) }}" class="no-underline"><span class="fe fe-block"></span>
|
||||
{{ _("Hide every post from author's instance: %(name)s", name=post.instance.domain) }}</a></li>
|
||||
{% endif -%}
|
||||
{% endif -%}
|
||||
{% endif -%}
|
||||
{% if post.ap_id -%}
|
||||
<li><a href="{{ post.ap_id }}" rel="nofollow" class="no-underline"><span class="fe fe-external"></span>
|
||||
{{ _('View original on %(domain)s', domain=post.instance.domain) }}</a></li>
|
||||
{% endif -%}
|
||||
<li><a href="{{ url_for('post.post_report', post_id=post.id) }}" class="no-underline" rel="nofollow"><span class="fe fe-report"></span>
|
||||
{{ _('Report to moderators') }}</a></li>
|
||||
{% if current_user.is_authenticated and (current_user.is_admin() or current_user.is_staff()) -%}
|
||||
<li><a href="{{ url_for('post.post_view_voting_activity', post_id=post.id) }}" class="no-underline" rel="nofollow"><span class="fe fe-sticky-left"></span>
|
||||
{{ _('View Voting Activity') }}</a></li>
|
||||
<li><a href="{{ url_for('post.post_fixup_from_remote', post_id=post.id) }}" class="no-underline" rel="nofollow"><span class="fe fe-sticky-right"></span>
|
||||
{{ _('Fixup from remote') }}</a></li>
|
||||
{% endif -%}
|
||||
</ul>
|
||||
<p>{{ _('If you want to perform more than one of these (e.g. block and report), hold down Ctrl and click, then complete the operation in the new tabs that open.') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock -%}
|
||||
{% if current_user.is_authenticated -%}
|
||||
{% if post.user_id == current_user.id -%}
|
||||
<li><a href="{{ url_for('post.post_edit', post_id=post.id) }}" style="white-space: normal" class="dropdown-item no-underline" rel="nofollow"><span class="fe fe-edit"></span>
|
||||
{{ _('Edit') }}</a></li>
|
||||
{% endif -%}
|
||||
{% if post.user_id == current_user.id or post.community.is_moderator() or post.community.is_owner() or current_user.is_admin() -%}
|
||||
{% if post.deleted -%}
|
||||
<li><a href="{{ url_for('post.post_restore', post_id=post.id) }}" style="white-space: normal" class="dropdown-item no-underline confirm_first" rel="nofollow"><span class="fe fe-arrow-up"></span>
|
||||
{{ _('Restore') }}</a></li>
|
||||
<li><a href="{{ url_for('post.post_purge', post_id=post.id) }}" style="white-space: normal" class="dropdown-item no-underline confirm_first" rel="nofollow"><span class="fe fe-delete red"></span>
|
||||
{{ _('Purge') }}</a></li>
|
||||
{% else -%}
|
||||
<li><a href="{{ url_for('post.post_delete', post_id=post.id) }}" style="white-space: normal" class="dropdown-item no-underline confirm_first" rel="nofollow"><span class="fe fe-delete"></span>
|
||||
{{ _('Delete') }}</a></li>
|
||||
{% endif -%}
|
||||
{% endif -%}
|
||||
{% if existing_bookmark -%}
|
||||
<li><a href="{{ url_for('post.post_remove_bookmark', post_id=post.id) }}" style="white-space: normal" class="dropdown-item no-underline" rel="nofollow"><span class="fe fe-bookmark"></span>
|
||||
{{ _('Remove bookmark') }}</a></li>
|
||||
{% else -%}
|
||||
<li><a href="{{ url_for('post.post_bookmark', post_id=post.id) }}" style="white-space: normal" class="dropdown-item no-underline" rel="nofollow"><span class="fe fe-bookmark"></span>
|
||||
{{ _('Bookmark') }}</a></li>
|
||||
{% endif -%}
|
||||
{% if post.user_id == current_user.id and not post.mea_culpa -%}
|
||||
<li><a href="{{ url_for('post.post_mea_culpa', post_id=post.id) }}" style="white-space: normal" class="dropdown-item no-underline"><span class="fe fe-mea-culpa"></span>
|
||||
{{ _("I made a mistake with this post and have changed my mind about the topic") }}</a></li>
|
||||
{% endif -%}
|
||||
{% if post.user_id != current_user.id -%}
|
||||
{% if post.type == POST_TYPE_LINK and post.author.bot and (post.cross_posts is none or len(post.cross_posts) == 0) -%}
|
||||
<li><a class="dropdown-item no-underline" style="white-space: normal" aria-label="{{ _('Cross-post') }}" href="{{ url_for('post.post_cross_post', post_id=post.id) }}"><span class="fe fe-cross-post"></span>
|
||||
{{ _('Cross-post to another community') }}</a></li>
|
||||
{% endif -%}
|
||||
<li><a href="{{ url_for('post.post_block_user', post_id=post.id) }}" style="white-space: normal" class="dropdown-item no-underline"><span class="fe fe-block"></span>
|
||||
{{ _('Block post author @%(author_name)s', author_name=post.author.user_name) }}</a></li>
|
||||
<li><a href="{{ url_for('post.post_block_community', post_id=post.id) }}" style="white-space: normal" class="dropdown-item no-underline"><span class="fe fe-block"></span>
|
||||
{{ _('Block community %(community_name)s', community_name=post.community.display_name()) }}</a></li>
|
||||
{% if post.community.is_moderator() or current_user.is_admin() -%}
|
||||
<li><a href="{{ url_for('community.community_ban_user', community_id=post.community.id, user_id=post.author.id) }}" style="white-space: normal" class="dropdown-item no-underline"><span class="fe fe-block red"></span>
|
||||
{{ _('Ban post author @%(author_name)s from %(community_name)s', author_name=post.author.user_name, community_name=post.community.title) }}</a></li>
|
||||
{% endif -%}
|
||||
{% if post.domain_id -%}
|
||||
<li><a href="{{ url_for('post.post_block_domain', post_id=post.id) }}" style="white-space: normal" class="dropdown-item no-underline"><span class="fe fe-block"></span>
|
||||
{{ _('Block domain %(domain)s', domain=post.domain.name) }}</a></li>
|
||||
{% endif -%}
|
||||
{% if post.instance_id and post.instance_id != 1 -%}
|
||||
<li><a href="{{ url_for('post.post_block_instance', post_id=post.id) }}" style="white-space: normal" class="dropdown-item no-underline"><span class="fe fe-block"></span>
|
||||
{{ _("Hide every post from author's instance: %(name)s", name=post.instance.domain) }}</a></li>
|
||||
{% endif -%}
|
||||
{% endif -%}
|
||||
{% endif -%}
|
||||
{% if post.ap_id -%}
|
||||
<li><a href="{{ post.ap_id }}" style="white-space: normal" rel="nofollow" class="dropdown-item no-underline"><span class="fe fe-external"></span>
|
||||
{{ _('View original on %(domain)s', domain=post.instance.domain) }}</a></li>
|
||||
{% endif -%}
|
||||
<li><a href="{{ url_for('post.post_report', post_id=post.id) }}" style="white-space: normal" class="dropdown-item no-underline" rel="nofollow"><span class="fe fe-report"></span>
|
||||
{{ _('Report to moderators') }}</a></li>
|
||||
{% if current_user.is_authenticated and (current_user.is_admin() or current_user.is_staff()) -%}
|
||||
<li><a href="{{ url_for('post.post_view_voting_activity', post_id=post.id) }}" style="white-space: normal" class="dropdown-item no-underline" rel="nofollow"><span class="fe fe-sticky-left"></span>
|
||||
{{ _('View Voting Activity') }}</a></li>
|
||||
<li><a href="{{ url_for('post.post_fixup_from_remote', post_id=post.id) }}" style="white-space: normal" class="dropdown-item no-underline" rel="nofollow"><span class="fe fe-sticky-right"></span>
|
||||
{{ _('Fixup from remote') }}</a></li>
|
||||
{% endif -%}
|
||||
|
|
|
@ -1,63 +1,42 @@
|
|||
{% if theme() and file_exists('app/templates/themes/' + theme() + '/base.html') -%}
|
||||
{% extends 'themes/' + theme() + '/base.html' -%}
|
||||
{% else -%}
|
||||
{% extends "base.html" -%}
|
||||
{% endif -%} -%}
|
||||
{% from 'bootstrap/form.html' import render_form -%}
|
||||
|
||||
{% block app_content -%}
|
||||
<div class="row">
|
||||
<div class="col col-login mx-auto">
|
||||
<div class="card mt-5">
|
||||
<div class="card-body p-6">
|
||||
<div class="card-title">{{ _('Options for comment on "%(post_title)s"', post_title=post.title) }}</div>
|
||||
<ul class="option_list">
|
||||
{% if current_user.is_authenticated -%}
|
||||
{% if post_reply.user_id == current_user.id -%}
|
||||
<li><a href="{{ url_for('post.post_reply_edit', post_id=post.id, comment_id=post_reply.id) }}" class="no-underline" rel="nofollow"><span class="fe fe-edit"></span>
|
||||
{{ _('Edit') }}</a></li>
|
||||
{% endif -%}
|
||||
{% if post_reply.user_id == current_user.id or post.community.is_moderator() or post.community.is_owner() or current_user.is_admin() -%}
|
||||
{% if post_reply.deleted -%}
|
||||
<li><a href="{{ url_for('post.post_reply_restore', post_id=post.id, comment_id=post_reply.id) }}" class="no-underline confirm_first" rel="nofollow"><span class="fe fe-arrow-up"></span>
|
||||
{{ _('Restore') }}</a></li>
|
||||
{% if not post_reply.has_replies() -%}
|
||||
{% if post.community.is_moderator() or current_user.is_admin() or (post_reply.user_id == current_user.id and post_reply.deleted_by == post_reply.user_id) -%}
|
||||
<li><a href="{{ url_for('post.post_reply_purge', post_id=post.id, comment_id=post_reply.id) }}" class="no-underline confirm_first" rel="nofollow"><span class="fe fe-delete red"></span>
|
||||
{{ _('Purge') }}</a></li>
|
||||
{% endif -%}
|
||||
{% endif -%}
|
||||
{% else -%}
|
||||
<li><a href="{{ url_for('post.post_reply_delete', post_id=post.id, comment_id=post_reply.id) }}" class="no-underline confirm_first" rel="nofollow"><span class="fe fe-delete"></span>
|
||||
{{ _('Delete') }}</a></li>
|
||||
{% endif -%}
|
||||
{% endif -%}
|
||||
{% if existing_bookmark -%}
|
||||
<li><a href="{{ url_for('post.post_reply_remove_bookmark', post_id=post.id, comment_id=post_reply.id) }}" class="no-underline" rel="nofollow"><span class="fe fe-bookmark"></span>
|
||||
{{ _('Remove bookmark') }}</a></li>
|
||||
{% else -%}
|
||||
<li><a href="{{ url_for('post.post_reply_bookmark', post_id=post.id, comment_id=post_reply.id) }}" class="no-underline" rel="nofollow"><span class="fe fe-bookmark"></span>
|
||||
{{ _('Bookmark') }}</a></li>
|
||||
{% endif -%}
|
||||
{% if post_reply.user_id != current_user.id -%}
|
||||
<li><a href="{{ url_for('post.post_reply_block_user', post_id=post.id, comment_id=post_reply.id) }}" class="no-underline"><span class="fe fe-block"></span>
|
||||
{{ _('Block author @%(author_name)s', author_name=post_reply.author.user_name) }}</a></li>
|
||||
{% if post_reply.instance_id and post_reply.instance_id != 1 -%}
|
||||
<li><a href="{{ url_for('post.post_reply_block_instance', post_id=post.id, comment_id=post_reply.id) }}" class="no-underline"><span class="fe fe-block"></span>
|
||||
{{ _("Hide every post from author's instance: %(name)s", name=post_reply.instance.domain) }}</a></li>
|
||||
{% endif -%}
|
||||
{% endif -%}
|
||||
{% if current_user.is_authenticated and (current_user.is_admin() or current_user.is_staff()) -%}
|
||||
<li><a href="{{ url_for('post.post_reply_view_voting_activity', comment_id=post_reply.id) }}" class="no-underline" rel="nofollow"><span class="fe fe-sticky-left"></span>
|
||||
{{ _('View Voting Activity') }}</a></li>
|
||||
{% endif -%}
|
||||
{% endif -%}
|
||||
<li><a href="{{ url_for('post.post_reply_report', post_id=post.id, comment_id=post_reply.id) }}" rel="nofollow" class="no-underline"><span class="fe fe-report"></span>
|
||||
{{ _('Report to moderators') }}</a></li>
|
||||
</ul>
|
||||
<p>{{ _('If you want to perform more than one of these (e.g. block and report), hold down Ctrl and click, then complete the operation in the new tabs that open.') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock -%}
|
||||
{% if current_user.is_authenticated -%}
|
||||
{% if post_reply.user_id == current_user.id -%}
|
||||
<li><a href="{{ url_for('post.post_reply_edit', post_id=post.id, comment_id=post_reply.id) }}" class="dropdown-item no-underline" rel="nofollow"><span class="fe fe-edit"></span>
|
||||
{{ _('Edit') }}</a></li>
|
||||
{% endif -%}
|
||||
{% if post_reply.user_id == current_user.id or post.community.is_moderator() or post.community.is_owner() or current_user.is_admin() -%}
|
||||
{% if post_reply.deleted -%}
|
||||
<li><a href="{{ url_for('post.post_reply_restore', post_id=post.id, comment_id=post_reply.id) }}" class="dropdown-item no-underline confirm_first" rel="nofollow"><span class="fe fe-arrow-up"></span>
|
||||
{{ _('Restore') }}</a></li>
|
||||
{% if not post_reply.has_replies() -%}
|
||||
{% if post.community.is_moderator() or current_user.is_admin() or (post_reply.user_id == current_user.id and post_reply.deleted_by == post_reply.user_id) -%}
|
||||
<li><a href="{{ url_for('post.post_reply_purge', post_id=post.id, comment_id=post_reply.id) }}" class="dropdown-item no-underline confirm_first" rel="nofollow"><span class="fe fe-delete red"></span>
|
||||
{{ _('Purge') }}</a></li>
|
||||
{% endif -%}
|
||||
{% endif -%}
|
||||
{% else -%}
|
||||
<li><a href="{{ url_for('post.post_reply_delete', post_id=post.id, comment_id=post_reply.id) }}" class="dropdown-item no-underline confirm_first" rel="nofollow"><span class="fe fe-delete"></span>
|
||||
{{ _('Delete') }}</a></li>
|
||||
{% endif -%}
|
||||
{% endif -%}
|
||||
{% if existing_bookmark -%}
|
||||
<li><a href="{{ url_for('post.post_reply_remove_bookmark', post_id=post.id, comment_id=post_reply.id) }}" class="dropdown-item no-underline" rel="nofollow"><span class="fe fe-bookmark"></span>
|
||||
{{ _('Remove bookmark') }}</a></li>
|
||||
{% else -%}
|
||||
<li><a href="{{ url_for('post.post_reply_bookmark', post_id=post.id, comment_id=post_reply.id) }}" class="dropdown-item no-underline" rel="nofollow"><span class="fe fe-bookmark"></span>
|
||||
{{ _('Bookmark') }}</a></li>
|
||||
{% endif -%}
|
||||
{% if post_reply.user_id != current_user.id -%}
|
||||
<li><a href="{{ url_for('post.post_reply_block_user', post_id=post.id, comment_id=post_reply.id) }}" class="dropdown-item no-underline"><span class="fe fe-block"></span>
|
||||
{{ _('Block author @%(author_name)s', author_name=post_reply.author.user_name) }}</a></li>
|
||||
{% if post_reply.instance_id and post_reply.instance_id != 1 -%}
|
||||
<li><a href="{{ url_for('post.post_reply_block_instance', post_id=post.id, comment_id=post_reply.id) }}" class="dropdown-item no-underline"><span class="fe fe-block"></span>
|
||||
{{ _("Hide every post from author's instance: %(name)s", name=post_reply.instance.domain) }}</a></li>
|
||||
{% endif -%}
|
||||
{% endif -%}
|
||||
{% if current_user.is_authenticated and (current_user.is_admin() or current_user.is_staff()) -%}
|
||||
<li><a href="{{ url_for('post.post_reply_view_voting_activity', comment_id=post_reply.id) }}" class="dropdown-item no-underline" rel="nofollow"><span class="fe fe-sticky-left"></span>
|
||||
{{ _('View Voting Activity') }}</a></li>
|
||||
{% endif -%}
|
||||
{% endif -%}
|
||||
<li><a href="{{ url_for('post.post_reply_report', post_id=post.id, comment_id=post_reply.id) }}" rel="nofollow" class="dropdown-item no-underline"><span class="fe fe-report"></span>
|
||||
{{ _('Report to moderators') }}</a></li>
|
||||
|
|
|
@ -26,4 +26,4 @@
|
|||
<span class="author small">{% if show_post_community -%}<a href="/c/{{ post.community.link() }}" aria-label="{{ _('Go to community %(name)s', name=post.community.name) }}">
|
||||
{% if post.community.icon_id and not low_bandwidth %}<img class="community_icon_small rounded-circle" src="{{ post.community.icon_image('tiny') }}" alt="Community icon" />{% endif -%}
|
||||
c/{{ post.community.name }}</a>{% endif -%}
|
||||
by {{ render_username(post.author) }} <time datetime="{{ post.last_active }}">{{ post.posted_at_localized(sort, locale) }}</time></span>
|
||||
by {{ render_username(post.author) }} <time datetime="{{ post.last_active }}" title="{{ post.last_active }}">{{ post.posted_at_localized(sort, locale) }}</time></span>
|
||||
|
|
|
@ -20,6 +20,18 @@
|
|||
</div>
|
||||
{% endif -%}
|
||||
<div class="post_options_link">
|
||||
<a href="{{ url_for('post.post_options', post_id=post.id) }}" rel="nofollow" aria-label="{{ _('Options') }}"><span class="fe fe-options" title="Options"> </span></a>
|
||||
</div>
|
||||
<div class="dropdown">
|
||||
<a href="{{ url_for('post.post_options', post_id=post.id) if low_bandwidth else '#' }}"
|
||||
data-bs-toggle="dropdown"
|
||||
rel="nofollow noindex"><span class="fe fe-options" title="Options"></span></a>
|
||||
<ul class="dropdown-menu" style="width: 320px">
|
||||
<div
|
||||
hx-get="{{ url_for('post.post_options', post_id=post.id) }}"
|
||||
hx-trigger="intersect once"
|
||||
hx-target="this"
|
||||
hx-swap="outerHTML"
|
||||
></div>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -44,6 +44,28 @@
|
|||
<a href="#" aria-hidden="true" class="markdown_editor_enabler create_post_markdown_editor_enabler" data-id="about">{{ _('Enable markdown editor') }}</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<fieldset class="coolfieldset mt-2 mb-3">
|
||||
<legend>{{ _('Extra fields') }}</legend>
|
||||
<p>{{ _('Your homepage, pronouns, age, etc.') }}</p>
|
||||
<table class="hide-labels">
|
||||
<tr>
|
||||
<td>{{ render_field(form.extra_label_1) }}</td>
|
||||
<td>{{ render_field(form.extra_text_1) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ render_field(form.extra_label_2) }}</td>
|
||||
<td>{{ render_field(form.extra_text_2) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ render_field(form.extra_label_3) }}</td>
|
||||
<td>{{ render_field(form.extra_text_3) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ render_field(form.extra_label_4) }}</td>
|
||||
<td>{{ render_field(form.extra_text_4) }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</fieldset>
|
||||
{{ render_field(form.bot) }}
|
||||
{{ render_field(form.matrixuserid) }}
|
||||
<small class="field_hint">e.g. @something:matrix.org. Include leading @ and use : before server</small>
|
||||
|
@ -69,7 +91,8 @@
|
|||
hx-swap="outerHTML">{{ _('Remove image') }}</a></p>
|
||||
<div id="cover_div" class="community_header mb-4" style="display: none; height: 240px; background-image: url({{ user.cover_image() }});"></div>
|
||||
{% endif %}
|
||||
{{ render_field(form.submit) }}
|
||||
|
||||
<p class="mt-4">{{ render_field(form.submit) }}</p>
|
||||
</form>
|
||||
<p class="mt-4 pt-4">
|
||||
<a class="btn btn-warning" href="{{ url_for('user.delete_account') }}">{{ _('Delete account') }}</a>
|
||||
|
|
|
@ -119,11 +119,26 @@
|
|||
{% if current_user.is_authenticated and current_user.is_admin() and user.reputation %}{{ _('Reputation') }}: <span title="{{ _('Reputation: The Karma of the account. Total up votes minus down votes they got.') }}">{{ user.reputation | round | int }}</span><br />{% endif %}
|
||||
{{ _('Posts') }}: {{ user.post_count }}<br />
|
||||
{{ _('Comments') }}: {{ user.post_reply_count }}<br />
|
||||
{% if current_user.is_authenticated %}{{ _('User note') }}: {{ user.get_note(current_user) }}<br />{% endif %}
|
||||
{% if current_user.is_authenticated %}{{ _('Note') }}: {{ user.get_note(current_user) }}<br />{% endif %}
|
||||
</p>
|
||||
<div class="profile_bio">
|
||||
{{ user.about_html|safe }}
|
||||
</div>
|
||||
{% if user.extra_fields -%}
|
||||
<ul class="list-group">
|
||||
{% for field in user.extra_fields -%}
|
||||
<li class="list-group-item">
|
||||
<p class="mb-0"><strong>{{ field.label }}</strong><br>
|
||||
{% if field.text.startswith('http') -%}
|
||||
<a href="{{ field.text }}" rel="nofollow noindex ugc">{{ field.text }}</a>
|
||||
{% else -%}
|
||||
{{ field.text }}
|
||||
{% endif -%}
|
||||
</p>
|
||||
</li>
|
||||
{% endfor -%}
|
||||
</ul>
|
||||
{% endif -%}
|
||||
{% if posts %}
|
||||
<h2 class="mt-4">Posts</h2>
|
||||
<div class="post_list">
|
||||
|
|
57
app/templates/user/user_preview.html
Normal file
57
app/templates/user/user_preview.html
Normal file
|
@ -0,0 +1,57 @@
|
|||
<div class="card" title="{{ user.display_name() }}">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
{% if user.avatar_id -%}
|
||||
<div class="col-auto preview_avatar_image">
|
||||
<img src="{{ user.avatar_image() }}" alt="" loading="lazy" />
|
||||
</div>
|
||||
{% endif -%}
|
||||
<div class="col-auto">
|
||||
<a href="/u/{{ user.link() }}">{{ user.display_name() }}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-auto">
|
||||
<p>{{ _('Instance') }}: <a href="{{ url_for('instance.instance_overview', instance_domain=user.instance_domain()) }}">{{ user.instance_domain() }}</a>
|
||||
{% if user.is_instance_admin() or (user.is_local() and user.is_admin()) %}<span class="red">({{ _('Admin') }})</span>{% endif %}<br />
|
||||
{% if user.is_admin() or user.is_staff() %}{{ _('Role permissions') }}: {% if user.is_admin() %}{{ _('Admin') }}{% endif %} {% if user.is_staff() %}{{ _('Staff') }}{% endif %}<br />{% endif %}
|
||||
{{ _('Joined') }}: {{ arrow.get(user.created).humanize(locale=locale) }}<br />
|
||||
{% if current_user.is_authenticated and current_user.is_admin() %}{{ _('Referer') }}: <span title="{{ _('Which website linked to PieFed when the user initially registered.') }}">{{ user.referrer if user.referrer }}</span><br />{% endif %}
|
||||
{% if current_user.is_authenticated and current_user.is_admin() %}{{ _('IP and country code') }}: <span title="{{ _('IP address of last interaction.') }}">{{ user.ip_address if user.ip_address }}{% if user.ip_address_country %} ({{ user.ip_address_country }}){% endif %}</span><br />{% endif %}
|
||||
{% if current_user.is_authenticated and current_user.is_admin() and user.last_seen %}{{ _('Active') }}: {{ arrow.get(user.last_seen).humanize(locale=locale) }}<br />{% endif %}
|
||||
{% if user.bot %}
|
||||
{{ _('Bot Account') }}<br />
|
||||
{% endif %}
|
||||
{{ _('Attitude') }}: <span title="{{ _('Ratio of upvotes cast to downvotes cast. Higher is more positive.') }}">{{ (user.attitude * 100) | round | int }}%</span><br />
|
||||
{% if current_user.is_authenticated and current_user.is_admin() and user.reputation %}{{ _('Reputation') }}: <span title="{{ _('Reputation: The Karma of the account. Total up votes minus down votes they got.') }}">{{ user.reputation | round | int }}</span><br />{% endif %}
|
||||
{{ _('Posts') }}: {{ user.post_count }}<br />
|
||||
{{ _('Comments') }}: {{ user.post_reply_count }}<br />
|
||||
{% if current_user.is_authenticated %}{{ _('Note') }}: {{ user.get_note(current_user) }}<br />{% endif %}
|
||||
</p>
|
||||
<div class="profile_bio">
|
||||
{{ user.about_html|safe }}
|
||||
</div>
|
||||
{% if user.extra_fields -%}
|
||||
<ul class="list-group mb-3">
|
||||
{% for field in user.extra_fields -%}
|
||||
<li class="list-group-item">
|
||||
<p class="mb-0"><strong>{{ field.label }}</strong><br>
|
||||
{% if field.text.startswith('http') -%}
|
||||
<a href="{{ field.text }}" rel="nofollow noindex ugc">{{ field.text }}</a>
|
||||
{% else -%}
|
||||
{{ field.text }}
|
||||
{% endif -%}
|
||||
</p>
|
||||
</li>
|
||||
{% endfor -%}
|
||||
</ul>
|
||||
{% endif -%}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-auto text-center">
|
||||
<a href="/u/{{ user.link() }}" class="btn btn-primary btn-sm">{{ _('View profile') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -14,6 +14,14 @@ class ProfileForm(FlaskForm):
|
|||
password_field = PasswordField(_l('Set new password'), validators=[Optional(), Length(min=1, max=50)],
|
||||
render_kw={"autocomplete": 'new-password'})
|
||||
about = TextAreaField(_l('Bio'), validators=[Optional(), Length(min=3, max=5000)], render_kw={'rows': 5})
|
||||
extra_label_1 = StringField(_l('Extra field 1 - label'), validators=[Optional(), Length(max=50)], render_kw={"placeholder": _l('Label')})
|
||||
extra_text_1 = StringField(_l('Extra field 1 - text'), validators=[Optional(), Length(max=256)], render_kw={"placeholder": _l('Content')})
|
||||
extra_label_2 = StringField(_l('Extra field 2 - label'), validators=[Optional(), Length(max=50)], render_kw={"placeholder": _l('Label')})
|
||||
extra_text_2 = StringField(_l('Extra field 2 - text'), validators=[Optional(), Length(max=256)], render_kw={"placeholder": _l('Content')})
|
||||
extra_label_3 = StringField(_l('Extra field 3 - label'), validators=[Optional(), Length(max=50)], render_kw={"placeholder": _l('Label')})
|
||||
extra_text_3 = StringField(_l('Extra field 3 - text'), validators=[Optional(), Length(max=256)], render_kw={"placeholder": _l('Content')})
|
||||
extra_label_4 = StringField(_l('Extra field 4 - label'), validators=[Optional(), Length(max=50)], render_kw={"placeholder": _l('Label')})
|
||||
extra_text_4 = StringField(_l('Extra field 4 - text'), validators=[Optional(), Length(max=256)], render_kw={"placeholder": _l('Content')})
|
||||
matrixuserid = StringField(_l('Matrix User ID'), validators=[Optional(), Length(max=255)],
|
||||
render_kw={'autocomplete': 'off'})
|
||||
profile_file = FileField(_l('Avatar image'), render_kw={'accept': 'image/*'})
|
||||
|
|
|
@ -16,7 +16,8 @@ from app.constants import *
|
|||
from app.email import send_verification_email
|
||||
from app.models import Post, Community, CommunityMember, User, PostReply, PostVote, Notification, utcnow, File, Site, \
|
||||
Instance, Report, UserBlock, CommunityBan, CommunityJoinRequest, CommunityBlock, Filter, Domain, DomainBlock, \
|
||||
InstanceBlock, NotificationSubscription, PostBookmark, PostReplyBookmark, read_posts, Topic, UserNote
|
||||
InstanceBlock, NotificationSubscription, PostBookmark, PostReplyBookmark, read_posts, Topic, UserNote, \
|
||||
UserExtraField
|
||||
from app.user import bp
|
||||
from app.user.forms import ProfileForm, SettingsForm, DeleteAccountForm, ReportUserForm, \
|
||||
FilterForm, KeywordFilterEditForm, RemoteFollowForm, ImportExportForm, UserNoteForm
|
||||
|
@ -129,6 +130,15 @@ def edit_profile(actor):
|
|||
current_user.about = piefed_markdown_to_lemmy_markdown(form.about.data)
|
||||
current_user.about_html = markdown_to_html(form.about.data)
|
||||
current_user.matrix_user_id = form.matrixuserid.data
|
||||
current_user.extra_fields = []
|
||||
if form.extra_label_1.data.strip() != '' and form.extra_text_1.data.strip() != '':
|
||||
current_user.extra_fields.append(UserExtraField(label=form.extra_label_1.data.strip(), text=form.extra_text_1.data.strip()))
|
||||
if form.extra_label_2.data.strip() != '' and form.extra_text_2.data.strip() != '':
|
||||
current_user.extra_fields.append(UserExtraField(label=form.extra_label_2.data.strip(), text=form.extra_text_2.data.strip()))
|
||||
if form.extra_label_3.data.strip() != '' and form.extra_text_3.data.strip() != '':
|
||||
current_user.extra_fields.append(UserExtraField(label=form.extra_label_3.data.strip(), text=form.extra_text_3.data.strip()))
|
||||
if form.extra_label_4.data.strip() != '' and form.extra_text_4.data.strip() != '':
|
||||
current_user.extra_fields.append(UserExtraField(label=form.extra_label_4.data.strip(), text=form.extra_text_4.data.strip()))
|
||||
current_user.bot = form.bot.data
|
||||
profile_file = request.files['profile_file']
|
||||
if profile_file and profile_file.filename != '':
|
||||
|
@ -169,7 +179,13 @@ def edit_profile(actor):
|
|||
form.title.data = current_user.title
|
||||
form.email.data = current_user.email
|
||||
form.about.data = current_user.about
|
||||
i = 1
|
||||
for extra_field in current_user.extra_fields:
|
||||
getattr(form, f"extra_label_{i}").data = extra_field.label
|
||||
getattr(form, f"extra_text_{i}").data = extra_field.text
|
||||
i += 1
|
||||
form.matrixuserid.data = current_user.matrix_user_id
|
||||
form.bot.data = current_user.bot
|
||||
form.password_field.data = ''
|
||||
|
||||
return render_template('user/edit_profile.html', title=_('Edit profile'), form=form, user=current_user,
|
||||
|
@ -1363,3 +1379,11 @@ def edit_user_note(actor):
|
|||
|
||||
return render_template('user/edit_note.html', title=_('Edit note'), form=form, user=user,
|
||||
menu_topics=menu_topics(), site=g.site)
|
||||
|
||||
|
||||
@bp.route('/user/<int:user_id>/preview')
|
||||
def user_preview(user_id):
|
||||
user = User.query.get_or_404(user_id)
|
||||
if (user.deleted or user.banned) and current_user.is_anonymous:
|
||||
abort(404)
|
||||
return render_template('user/user_preview.html', user=user)
|
||||
|
|
20
app/utils.py
20
app/utils.py
|
@ -1257,3 +1257,23 @@ def community_ids_from_instances(instance_ids) -> List[int]:
|
|||
def get_task_session() -> Session:
|
||||
# Use the same engine as the main app, but create an independent session
|
||||
return Session(bind=db.engine)
|
||||
|
||||
|
||||
user2_cache = {}
|
||||
|
||||
|
||||
def jaccard_similarity(user1_upvoted: set, user2_id: int):
|
||||
if user2_id not in user2_cache:
|
||||
user2_upvoted_posts = ['post/' + str(id) for id in recently_upvoted_posts(user2_id)]
|
||||
user2_upvoted_replies = ['reply/' + str(id) for id in recently_upvoted_post_replies(user2_id)]
|
||||
user2_cache[user2_id] = set(user2_upvoted_posts + user2_upvoted_replies)
|
||||
|
||||
user2_upvoted = user2_cache[user2_id]
|
||||
|
||||
if len(user2_upvoted) > 12:
|
||||
intersection = len(user1_upvoted.intersection(user2_upvoted))
|
||||
union = len(user1_upvoted.union(user2_upvoted))
|
||||
|
||||
return (intersection / union) * 100
|
||||
else:
|
||||
return 0
|
||||
|
|
41
migrations/versions/f961f446ae17_user_extra_fields.py
Normal file
41
migrations/versions/f961f446ae17_user_extra_fields.py
Normal file
|
@ -0,0 +1,41 @@
|
|||
"""user extra fields
|
||||
|
||||
Revision ID: f961f446ae17
|
||||
Revises: 1189f921aca6
|
||||
Create Date: 2024-12-22 14:56:43.714502
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'f961f446ae17'
|
||||
down_revision = '1189f921aca6'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('user_extra_field',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||
sa.Column('label', sa.String(length=50), nullable=True),
|
||||
sa.Column('text', sa.String(length=256), nullable=True),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
with op.batch_alter_table('user_extra_field', schema=None) as batch_op:
|
||||
batch_op.create_index(batch_op.f('ix_user_extra_field_user_id'), ['user_id'], unique=False)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('user_extra_field', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_user_extra_field_user_id'))
|
||||
|
||||
op.drop_table('user_extra_field')
|
||||
# ### end Alembic commands ###
|
Loading…
Reference in a new issue