notify about new posts in communities

This commit is contained in:
rimu 2024-01-07 12:47:06 +13:00
parent f777ea5dfd
commit 2bc1ab47d0
10 changed files with 144 additions and 13 deletions

View file

@ -7,6 +7,7 @@ from typing import Union, Tuple
from flask import current_app, request, g, url_for
from sqlalchemy import text
from app import db, cache, constants, celery
from app.community.util import notify_about_post
from app.models import User, Post, Community, BannedInstances, File, PostReply, AllowedInstances, Instance, utcnow, \
PostVote, PostReplyVote, ActivityPubLog, Notification, Site
import time
@ -1040,6 +1041,9 @@ def create_post(activity_log: ActivityPubLog, community: Community, request_json
if post.image_id:
make_image_sizes(post.image_id, 266, None, 'posts')
notify_about_post(post)
if user.reputation > 100:
vote = PostVote(user_id=1, author_id=post.user_id,
post_id=post.id,

View file

@ -9,7 +9,8 @@ from app.activitypub.util import default_context
from app.community.forms import SearchRemoteCommunity, AddLocalCommunity, CreatePostForm, ReportCommunityForm, \
DeleteCommunityForm
from app.community.util import search_for_community, community_url_exists, actor_to_community, \
opengraph_parse, url_to_thumbnail_file, save_post, save_icon_file, save_banner_file, send_to_remote_instance
opengraph_parse, url_to_thumbnail_file, save_post, save_icon_file, save_banner_file, send_to_remote_instance, \
notify_about_post
from app.constants import SUBSCRIPTION_MEMBER, SUBSCRIPTION_OWNER, POST_TYPE_LINK, POST_TYPE_ARTICLE, POST_TYPE_IMAGE, \
SUBSCRIPTION_PENDING
from app.models import User, Community, CommunityMember, CommunityJoinRequest, CommunityBan, Post, \
@ -223,13 +224,13 @@ def subscribe(actor):
success = post_request(community.ap_inbox_url, follow, current_user.private_key,
current_user.profile_id() + '#main-key')
if success:
flash('Your request to join has been sent to ' + community.title)
flash(_('You have joined %(name)s', name=community.title))
else:
flash('There was a problem while trying to join.', 'error')
flash(_('There was a problem while trying to join.'), 'error')
else: # for local communities, joining is instant
banned = CommunityBan.query.filter_by(user_id=current_user.id, community_id=community.id).first()
if banned:
flash('You cannot join this community')
flash(_('You cannot join this community'))
else:
member = CommunityMember(user_id=current_user.id, community_id=community.id)
db.session.add(member)
@ -340,6 +341,8 @@ def add_post(actor):
post.ap_id = f"https://{current_app.config['SERVER_NAME']}/post/{post.id}"
db.session.commit()
notify_about_post(post)
page = {
'type': 'Page',
'id': post.ap_id,
@ -486,3 +489,22 @@ def community_block_instance(community_id: int):
db.session.commit()
flash(_('Content from %(name)s will be hidden.', name=community.instance.domain))
return redirect(community.local_url())
@bp.route('/<int:community_id>/notification', methods=['GET', 'POST'])
@login_required
def community_notification(community_id: int):
community = Community.query.get_or_404(community_id)
member_info = CommunityMember.query.filter(CommunityMember.community_id == community.id,
CommunityMember.user_id == current_user.id).first()
# existing community members get their notification flag toggled
if member_info and not member_info.is_banned:
member_info.notify_new_posts = not member_info.notify_new_posts
db.session.commit()
else: # people who are not yet members become members, with notify on.
if not community.user_is_banned(current_user):
new_member = CommunityMember(community_id=community.id, user_id=current_user.id, notify_new_posts=True)
db.session.add(new_member)
db.session.commit()
return render_template('community/_notification_toggle.html', community=community)

View file

@ -13,9 +13,9 @@ from app.activitypub.signature import post_request
from app.activitypub.util import find_actor_or_create, actor_json_to_model, post_json_to_model
from app.constants import POST_TYPE_ARTICLE, POST_TYPE_LINK, POST_TYPE_IMAGE
from app.models import Community, File, BannedInstances, PostReply, PostVote, Post, utcnow, CommunityMember, Site, \
Instance
Instance, Notification, User
from app.utils import get_request, gibberish, markdown_to_html, domain_from_url, validate_image, allowlist_html, \
html_to_markdown, is_image_url, ensure_directory_exists, inbox_domain, post_ranking
html_to_markdown, is_image_url, ensure_directory_exists, inbox_domain, post_ranking, shorten_string
from sqlalchemy import desc, text
import os
from opengraph_parse import parse_page
@ -387,4 +387,15 @@ def send_to_remote_instance_task(instance_id: int, community_id: int, payload):
instance.start_trying_again = utcnow() + timedelta(seconds=instance.failures ** 4)
if instance.failures > 2:
instance.dormant = True
db.session.commit()
db.session.commit()
def notify_about_post(post: Post):
people_to_notify = CommunityMember.query.filter_by(community_id=post.community_id, notify_new_posts=True, is_banned=False)
for person in people_to_notify:
if person.user_id != post.user_id:
new_notification = Notification(title=shorten_string(post.title, 25), url=f"/post/{post.id}", user_id=person.user_id, author_id=post.user_id)
db.session.add(new_notification)
user = User.query.get(person.user_id) # todo: make this more efficient by doing a join with CommunityMember at the start of the function
user.unread_notifications += 1
db.session.commit()

View file

@ -253,6 +253,11 @@ class Community(db.Model):
else:
return any(moderator.user_id == user.id and moderator.is_owner for moderator in self.moderators())
def user_is_banned(self, user):
membership = CommunityMember.query.filter(CommunityMember.community_id == self.id, CommunityMember.user_id == user.id).first()
return membership.is_banned if membership else False
def profile_id(self):
return self.ap_profile_id if self.ap_profile_id else f"https://{current_app.config['SERVER_NAME']}/c/{self.name}"
@ -265,6 +270,14 @@ class Community(db.Model):
else:
return f"https://{current_app.config['SERVER_NAME']}/c/{self.ap_id}"
def notify_new_posts(self, user_id: int) -> bool:
member_info = CommunityMember.query.filter(CommunityMember.community_id == self.id,
CommunityMember.is_banned == False,
CommunityMember.user_id == user_id).first()
if not member_info:
return False
return member_info.notify_new_posts
# instances that have users which are members of this community. (excluding the current instance)
def following_instances(self, include_dormant=False) -> List[Instance]:
instances = Instance.query.join(User, User.instance_id == Instance.id).join(CommunityMember, CommunityMember.user_id == User.id)
@ -782,6 +795,7 @@ class CommunityMember(db.Model):
is_moderator = db.Column(db.Boolean, default=False)
is_owner = db.Column(db.Boolean, default=False)
is_banned = db.Column(db.Boolean, default=False)
notify_new_posts = db.Column(db.Boolean, default=False)
created_at = db.Column(db.DateTime, default=utcnow)

View file

@ -198,7 +198,7 @@
content: "\e967";
}
.fe-bell {
.fe-bell, .fe-no-bell {
position: relative;
top: 1px;
}
@ -206,6 +206,18 @@
content: "\e91e";
}
.fe-no-bell::before {
content: "\e91f";
}
h1 {
.fe-bell, .fe-no-bell {
font-size: 18px;
top: -5px;
left: 17px;
}
}
.fe-magnify {
position: relative;
top: 1px;

View file

@ -225,7 +225,7 @@ nav, etc which are used site-wide */
content: "\e967";
}
.fe-bell {
.fe-bell, .fe-no-bell {
position: relative;
top: 1px;
}
@ -234,6 +234,16 @@ nav, etc which are used site-wide */
content: "\e91e";
}
.fe-no-bell::before {
content: "\e91f";
}
h1 .fe-bell, h1 .fe-no-bell {
font-size: 18px;
top: -5px;
left: 17px;
}
.fe-magnify {
position: relative;
top: 1px;

View file

@ -224,7 +224,7 @@
content: "\e967";
}
.fe-bell {
.fe-bell, .fe-no-bell {
position: relative;
top: 1px;
}
@ -233,6 +233,16 @@
content: "\e91e";
}
.fe-no-bell::before {
content: "\e91f";
}
h1 .fe-bell, h1 .fe-no-bell {
font-size: 18px;
top: -5px;
left: 17px;
}
.fe-magnify {
position: relative;
top: 1px;

View file

@ -0,0 +1,4 @@
<a href="/community/{{ community.id }}/notification"
class="fe {{ 'fe-bell' if community.notify_new_posts(current_user.id) else 'fe-no-bell' }} no-underline"
hx-post="/community/{{ community.id }}/notification" hx-trigger="click throttle:1s" hx-swap="outerHTML"
title="{{ _('Notify about every new post. Not advisable in high traffic communities!') }}"></a>

View file

@ -15,7 +15,11 @@
</nav>
</div>
<img class="community_icon_big bump_up rounded-circle" src="{{ community.icon_image() }}" />
<h1 class="mt-2">{{ community.title }}</h1>
<h1 class="mt-2">{{ community.title }}
{% if current_user.is_authenticated %}
{% include 'community/_notification_toggle.html' %}
{% endif %}
</h1>
{% elif community.icon_id %}
<div class="row">
<nav aria-label="breadcrumb" id="breadcrumb_nav" title="Navigation">
@ -29,7 +33,11 @@
<img class="community_icon_big rounded-circle" src="{{ community.icon_image() }}" />
</div>
<div class="col-10">
<h1 class="mt-3">{{ community.title }}</h1>
<h1 class="mt-3">{{ community.title }}
{% if current_user.is_authenticated %}
{% include 'community/_notification_toggle.html' %}
{% endif %}
</h1>
</div>
</div>
{% else %}
@ -40,7 +48,11 @@
<li class="breadcrumb-item active">{{ community.title|shorten }}</li>
</ol>
</nav>
<h1 class="mt-2">{{ community.title }}</h1>
<h1 class="mt-2">{{ community.title }}
{% if current_user.is_authenticated %}
{% include 'community/_notification_toggle.html' %}
{% endif %}
</h1>
{% endif %}
{% include "community/_community_nav.html" %}
<div class="post_list">

View file

@ -0,0 +1,32 @@
"""community notifications
Revision ID: dc49309fc13e
Revises: fd5d3a9cb584
Create Date: 2024-01-07 10:35:55.484246
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'dc49309fc13e'
down_revision = 'fd5d3a9cb584'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('community_member', schema=None) as batch_op:
batch_op.add_column(sa.Column('notify_new_posts', sa.Boolean(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('community_member', schema=None) as batch_op:
batch_op.drop_column('notify_new_posts')
# ### end Alembic commands ###