mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-01-23 19:36:56 -08:00
finish notifications
This commit is contained in:
parent
f0a4e01fe9
commit
5752b8eaeb
14 changed files with 261 additions and 22 deletions
|
@ -44,7 +44,7 @@ class CreatePostForm(FlaskForm):
|
|||
# flair = SelectField(_l('Flair'), coerce=int)
|
||||
nsfw = BooleanField(_l('NSFW'))
|
||||
nsfl = BooleanField(_l('NSFL'))
|
||||
notify_author = BooleanField(_l('Send me post reply notifications'))
|
||||
notify_author = BooleanField(_l('Notify about replies'))
|
||||
submit = SubmitField(_l('Post'))
|
||||
|
||||
def validate(self, extra_validators=None) -> bool:
|
||||
|
|
|
@ -180,6 +180,7 @@ class User(UserMixin, db.Model):
|
|||
indexable = db.Column(db.Boolean, default=False)
|
||||
bot = db.Column(db.Boolean, default=False)
|
||||
ignore_bots = db.Column(db.Boolean, default=False)
|
||||
unread_notifications = db.Column(db.Integer, default=0)
|
||||
|
||||
avatar = db.relationship('File', foreign_keys=[avatar_id], single_parent=True, cascade="all, delete-orphan")
|
||||
cover = db.relationship('File', foreign_keys=[cover_id], single_parent=True, cascade="all, delete-orphan")
|
||||
|
@ -312,6 +313,9 @@ class User(UserMixin, db.Model):
|
|||
return User.query.get(id)
|
||||
|
||||
def purge_content(self):
|
||||
files = File.query.join(Post).filter(Post.user_id == self.id).all()
|
||||
for file in files:
|
||||
file.delete_from_disk()
|
||||
db.session.query(ActivityLog).filter(ActivityLog.user_id == self.id).delete()
|
||||
db.session.query(PostVote).filter(PostVote.user_id == self.id).delete()
|
||||
db.session.query(PostReplyVote).filter(PostReplyVote.user_id == self.id).delete()
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
from flask_wtf import FlaskForm
|
||||
from wtforms import TextAreaField, SubmitField
|
||||
from wtforms import TextAreaField, SubmitField, BooleanField
|
||||
from wtforms.validators import DataRequired, Length
|
||||
from flask_babel import _, lazy_gettext as _l
|
||||
|
||||
|
||||
class NewReplyForm(FlaskForm):
|
||||
body = TextAreaField(_l('Body'), render_kw={'placeholder': 'What are your thoughts?', 'rows': 3}, validators={DataRequired(), Length(min=3, max=5000)})
|
||||
notify_author = BooleanField(_l('Notify about replies'))
|
||||
submit = SubmitField(_l('Comment'))
|
|
@ -12,7 +12,7 @@ from app.community.forms import CreatePostForm
|
|||
from app.post.util import post_replies, get_comment_branch, post_reply_count
|
||||
from app.constants import SUBSCRIPTION_MEMBER, SUBSCRIPTION_OWNER, POST_TYPE_LINK, POST_TYPE_ARTICLE, POST_TYPE_IMAGE
|
||||
from app.models import Post, PostReply, \
|
||||
PostReplyVote, PostVote
|
||||
PostReplyVote, PostVote, Notification
|
||||
from app.post import bp
|
||||
from app.utils import get_setting, render_template, allowlist_html, markdown_to_html, validation_required, \
|
||||
shorten_string, markdown_to_text, domain_from_url, validate_image, gibberish
|
||||
|
@ -27,7 +27,12 @@ def show_post(post_id: int):
|
|||
if current_user.is_authenticated and current_user.verified and form.validate_on_submit():
|
||||
reply = PostReply(user_id=current_user.id, post_id=post.id, 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)
|
||||
from_bot=current_user.bot, up_votes=1, nsfw=post.nsfw, nsfl=post.nsfl,
|
||||
notify_author=form.notify_author.data)
|
||||
if post.notify_author and current_user.id != post.user_id: # todo: check if replier is blocked
|
||||
notification = Notification(title=_('Reply: ') + shorten_string(form.body.data), user_id=post.user_id,
|
||||
author_id=current_user.id, url=url_for('post.show_post', post_id=post.id))
|
||||
db.session.add(notification)
|
||||
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,
|
||||
|
@ -42,6 +47,7 @@ def show_post(post_id: int):
|
|||
post_id=post_id)) # redirect to current page to avoid refresh resubmitting the form
|
||||
else:
|
||||
replies = post_replies(post.id, 'top')
|
||||
form.notify_author.data = True
|
||||
|
||||
og_image = post.image.source_url if post.image_id else None
|
||||
description = shorten_string(markdown_to_text(post.body), 150) if post.body else None
|
||||
|
@ -178,8 +184,13 @@ def add_reply(post_id: int, comment_id: int):
|
|||
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)
|
||||
from_bot=current_user.bot, up_votes=1, nsfw=post.nsfw, nsfl=post.nsfl,
|
||||
notify_author=form.notify_author.data)
|
||||
db.session.add(reply)
|
||||
if comment.notify_author and current_user.id != comment.user_id: # todo: check if replier is blocked
|
||||
notification = Notification(title=_('Reply: ') + shorten_string(form.body.data), user_id=comment.user_id,
|
||||
author_id=current_user.id, url=url_for('post.show_post', post_id=post.id))
|
||||
db.session.add(notification)
|
||||
db.session.commit()
|
||||
reply_vote = PostReplyVote(user_id=current_user.id, author_id=current_user.id, post_reply_id=reply.id,
|
||||
effect=1.0)
|
||||
|
@ -195,6 +206,7 @@ def add_reply(post_id: int, comment_id: int):
|
|||
else:
|
||||
return redirect(url_for('post.continue_discussion', post_id=post_id, comment_id=reply.parent_id))
|
||||
else:
|
||||
form.notify_author.data = True
|
||||
return render_template('post/add_reply.html', title=_('Discussing %(title)s', title=post.title), post=post,
|
||||
is_moderator=is_moderator, form=form, comment=comment)
|
||||
|
||||
|
|
|
@ -182,6 +182,10 @@
|
|||
content: "\e967";
|
||||
}
|
||||
|
||||
.fe-bell::before {
|
||||
content: "\e91e";
|
||||
}
|
||||
|
||||
.fe-image {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
|
|
|
@ -185,6 +185,10 @@ nav, etc which are used site-wide */
|
|||
content: "\e967";
|
||||
}
|
||||
|
||||
.fe-bell::before {
|
||||
content: "\e91e";
|
||||
}
|
||||
|
||||
.fe-image {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
|
@ -403,6 +407,23 @@ fieldset legend {
|
|||
.post_reply_form label {
|
||||
display: none;
|
||||
}
|
||||
.post_reply_form .form-check {
|
||||
position: absolute;
|
||||
bottom: -14px;
|
||||
left: 122px;
|
||||
}
|
||||
.post_reply_form .form-check label {
|
||||
display: inherit;
|
||||
}
|
||||
|
||||
.add_reply .form-control-label {
|
||||
display: none;
|
||||
}
|
||||
.add_reply .form-check {
|
||||
position: absolute;
|
||||
bottom: -14px;
|
||||
left: 122px;
|
||||
}
|
||||
|
||||
.post_list .post_teaser {
|
||||
border-bottom: solid 2px #ddd;
|
||||
|
@ -548,8 +569,8 @@ fieldset legend {
|
|||
border-top: solid 1px #ddd;
|
||||
}
|
||||
|
||||
.add_reply .form-control-label {
|
||||
display: none;
|
||||
.table tr th {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=structure.css.map */
|
||||
|
|
|
@ -115,7 +115,30 @@ nav, etc which are used site-wide */
|
|||
label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.form-check {
|
||||
position: absolute;
|
||||
bottom: -14px;
|
||||
left: 122px;
|
||||
|
||||
label {
|
||||
display: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add_reply {
|
||||
.form-control-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.form-check {
|
||||
position: absolute;
|
||||
bottom: -14px;
|
||||
left: 122px;
|
||||
}
|
||||
}
|
||||
|
||||
.post_list {
|
||||
.post_teaser {
|
||||
|
||||
|
@ -294,8 +317,8 @@ nav, etc which are used site-wide */
|
|||
}
|
||||
}
|
||||
|
||||
.add_reply {
|
||||
.form-control-label {
|
||||
display: none;
|
||||
.table {
|
||||
tr th {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
|
@ -184,6 +184,10 @@
|
|||
content: "\e967";
|
||||
}
|
||||
|
||||
.fe-bell::before {
|
||||
content: "\e91e";
|
||||
}
|
||||
|
||||
.fe-image {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
|
|
|
@ -71,11 +71,13 @@
|
|||
{% else %}
|
||||
<li class="nav-item"><a class="nav-link" href="/">{{ _('Home') }}</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/communities">{{ _('Communities') }}</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/u/{{ current_user.user_name }}">{{ current_user.user_name }}</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/u/{{ current_user.user_name }}">{{ _('Account') }}</a></li>
|
||||
{% if user_access('change instance settings', current_user.id) %}
|
||||
<li class="nav-item"><a class="nav-link" href="/admin/">{{ _('Admin') }}</a></li>
|
||||
{% endif %}
|
||||
<li class="nav-item"><a class="nav-link" href="/auth/logout">{{ _('Log out') }}</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/notifications"><span class="fe fe-bell"></span>
|
||||
{{ current_user.unread_notifications if current_user.unread_notifications else '' }}</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -12,8 +12,10 @@
|
|||
<fieldset class="coolfieldset mt-4"><legend class="w-auto">Comment you are replying to</legend>
|
||||
{{ comment.body_html | safe}}
|
||||
</fieldset>
|
||||
<div class="position-relative">
|
||||
{{ render_form(form) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
<div class="row post_reply_form">
|
||||
<hr class="mt-1" />
|
||||
<div class="col">
|
||||
<div class="reply_form_inner">
|
||||
<div class="reply_form_inner position-relative">
|
||||
{{ render_form(form) }}
|
||||
</div>
|
||||
</div>
|
||||
|
|
105
app/templates/user/notifications.html
Normal file
105
app/templates/user/notifications.html
Normal file
|
@ -0,0 +1,105 @@
|
|||
{% extends "base.html" %}
|
||||
{% from 'bootstrap/form.html' import render_form %}
|
||||
|
||||
{% block app_content %}
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-8 position-relative main_pane">
|
||||
<nav aria-label="breadcrumb" id="breadcrumb_nav" title="Navigation">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="/">{{ _('Home') }}</a></li>
|
||||
<li class="breadcrumb-item"><a href="/users">{{ _('People') }}</a></li>
|
||||
<li class="breadcrumb-item active">{{ user.user_name|shorten }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<h1 class="mt-2">{{ _('Notifications') }}</h1>
|
||||
|
||||
{% if notifications %}
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th>Notification</th>
|
||||
<th>When</th>
|
||||
<th><a href="{{ url_for('user.notifications_all_read') }}" class="btn btn-primary btn-sm">Mark all read</a></th>
|
||||
</tr>
|
||||
{% for notification in notifications %}
|
||||
<tr>
|
||||
<td>{% if not notification.read %}<strong>{% endif %}
|
||||
<a href="{{ url_for('user.notification_goto', notification_id=notification.id) }}">{{ notification.title }}</a>
|
||||
{% if not notification.read %}</strong>{% endif %}
|
||||
</td>
|
||||
<td>{{ moment(notification.created_at).fromNow(refresh=True) }}</td>
|
||||
<td>
|
||||
<a href="{{ url_for('user.notification_delete', notification_id=notification.id) }}" class="no-underline"><span class="fe fe-delete"> Delete</span></a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<p>No notifications to show.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-4">
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h2>{{ _('Manage') }}</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<a class="w-100 btn btn-primary" href="/u/{{ current_user.user_name }}/profile">{{ _('Profile') }}</a>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<a class="w-100 btn btn-primary" href="/u/{{ current_user.user_name }}/settings">{{ _('Settings') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if len(subscribed) > 0 or len(moderates) > 0 %}
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h2>{{ _('Communities') }}</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if len(subscribed) > 0 %}
|
||||
<h4>Subscribed to</h4>
|
||||
<ul>
|
||||
{% for community in subscribed %}
|
||||
<li>
|
||||
<a href="/c/{{ community.link() }}"><img src="{{ community.icon_image() }}" class="community_icon rounded-circle" loading="lazy" />{{ community.display_name() }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if len(moderates) > 0 %}
|
||||
<h4>Moderates</h4>
|
||||
<ul>
|
||||
{% for community in moderates %}
|
||||
<li>
|
||||
<a href="/c/{{ community.link() }}"><img src="{{ community.icon_image() }}" class="community_icon rounded-circle" loading="lazy" />{{ community.display_name() }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if upvoted %}
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h2>{{ _('Upvoted') }}</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul>
|
||||
{% for post in upvoted %}
|
||||
<li><a href="{{ url_for('post.show_post', post_id=post.id) }}">{{ post.title }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,13 +1,15 @@
|
|||
from datetime import datetime, timedelta
|
||||
|
||||
from flask import redirect, url_for, flash, request, make_response, session, Markup, current_app, abort
|
||||
from flask_login import login_user, logout_user, current_user, login_required
|
||||
from flask_babel import _
|
||||
|
||||
from app import db
|
||||
from app.models import Post, Community, CommunityMember, User, PostReply, PostVote
|
||||
from app.models import Post, Community, CommunityMember, User, PostReply, PostVote, Notification
|
||||
from app.user import bp
|
||||
from app.user.forms import ProfileForm, SettingsForm
|
||||
from app.utils import get_setting, render_template, markdown_to_html, user_access, markdown_to_text, shorten_string
|
||||
from sqlalchemy import desc, or_
|
||||
from sqlalchemy import desc, or_, text
|
||||
|
||||
|
||||
def show_profile(user):
|
||||
|
@ -105,7 +107,7 @@ def ban_profile(actor):
|
|||
abort(404)
|
||||
|
||||
if user.id == current_user.id:
|
||||
flash('You cannot ban yourself.', 'error')
|
||||
flash(_('You cannot ban yourself.'), 'error')
|
||||
else:
|
||||
user.banned = True
|
||||
db.session.commit()
|
||||
|
@ -130,7 +132,7 @@ def unban_profile(actor):
|
|||
abort(404)
|
||||
|
||||
if user.id == current_user.id:
|
||||
flash('You cannot unban yourself.', 'error')
|
||||
flash(_('You cannot unban yourself.'), 'error')
|
||||
else:
|
||||
user.banned = False
|
||||
db.session.commit()
|
||||
|
@ -154,7 +156,7 @@ def delete_profile(actor):
|
|||
if user is None:
|
||||
abort(404)
|
||||
if user.id == current_user.id:
|
||||
flash('You cannot delete yourself.', 'error')
|
||||
flash(_('You cannot delete yourself.'), 'error')
|
||||
else:
|
||||
user.banned = True
|
||||
user.deleted = True
|
||||
|
@ -180,7 +182,7 @@ def ban_purge_profile(actor):
|
|||
abort(404)
|
||||
|
||||
if user.id == current_user.id:
|
||||
flash('You cannot purge yourself.', 'error')
|
||||
flash(_('You cannot purge yourself.'), 'error')
|
||||
else:
|
||||
user.banned = True
|
||||
user.deleted = True
|
||||
|
@ -199,3 +201,56 @@ def ban_purge_profile(actor):
|
|||
|
||||
goto = request.args.get('redirect') if 'redirect' in request.args else f'/u/{actor}'
|
||||
return redirect(goto)
|
||||
|
||||
|
||||
@bp.route('/notifications', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def notifications():
|
||||
"""Remove notifications older than 30 days"""
|
||||
db.session.query(Notification).filter(
|
||||
Notification.created_at < datetime.utcnow() - timedelta(days=30)).delete()
|
||||
db.session.commit()
|
||||
|
||||
# Update unread notifications count
|
||||
current_user.unread_notifications = Notification.query.filter_by(user_id=current_user.id, read=False).count()
|
||||
db.session.commit()
|
||||
|
||||
notification_list = Notification.query.filter_by(user_id=current_user.id).order_by(desc(Notification.created_at)).all()
|
||||
|
||||
return render_template('user/notifications.html', title=_('Notifications'), notifications=notification_list, user=current_user)
|
||||
|
||||
|
||||
@bp.route('/notification/<int:notification_id>/goto', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def notification_goto(notification_id):
|
||||
notification = Notification.query.get_or_404(notification_id)
|
||||
if notification.user_id == current_user.id:
|
||||
if not notification.read:
|
||||
current_user.unread_notifications -= 1
|
||||
notification.read = True
|
||||
db.session.commit()
|
||||
return redirect(notification.url)
|
||||
else:
|
||||
abort(403)
|
||||
|
||||
|
||||
@bp.route('/notification/<int:notification_id>/delete', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def notification_delete(notification_id):
|
||||
notification = Notification.query.get_or_404(notification_id)
|
||||
if notification.user_id == current_user.id:
|
||||
if not notification.read:
|
||||
current_user.unread_notifications -= 1
|
||||
db.session.delete(notification)
|
||||
db.session.commit()
|
||||
return redirect(url_for('user.notifications'))
|
||||
|
||||
|
||||
@bp.route('/notifications/all_read', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def notifications_all_read():
|
||||
db.session.execute(text('UPDATE notification SET read=true WHERE user_id = :user_id'), {'user_id': current_user.id})
|
||||
current_user.unread_notifications = 0
|
||||
db.session.commit()
|
||||
flash(_('All notifications marked as read.'))
|
||||
return redirect(url_for('user.notifications'))
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
"""notifications
|
||||
|
||||
Revision ID: f5706aa6b94c
|
||||
Revision ID: cae2e31293e8
|
||||
Revises: ee45b9ea4a0c
|
||||
Create Date: 2023-11-30 19:21:04.549875
|
||||
Create Date: 2023-11-30 22:49:24.942158
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
|
@ -10,7 +10,7 @@ import sqlalchemy as sa
|
|||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'f5706aa6b94c'
|
||||
revision = 'cae2e31293e8'
|
||||
down_revision = 'ee45b9ea4a0c'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
@ -36,11 +36,17 @@ def upgrade():
|
|||
with op.batch_alter_table('post_reply', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('notify_author', sa.Boolean(), nullable=True))
|
||||
|
||||
with op.batch_alter_table('user', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('unread_notifications', sa.Integer(), nullable=True))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('user', schema=None) as batch_op:
|
||||
batch_op.drop_column('unread_notifications')
|
||||
|
||||
with op.batch_alter_table('post_reply', schema=None) as batch_op:
|
||||
batch_op.drop_column('notify_author')
|
||||
|
Loading…
Reference in a new issue