diff --git a/app/static/styles.css b/app/static/styles.css
index 35a4839c..3120ba2d 100644
--- a/app/static/styles.css
+++ b/app/static/styles.css
@@ -359,8 +359,7 @@ h1 .fe-bell, h1 .fe-no-bell {
top: 2px;
}
.fe-poll:before {
- content: "\e91b";
- /* possibly e985 */
+ content: "\e91b"; /* possibly e985 */
}
.fe-sticky-left::before {
@@ -513,25 +512,20 @@ a {
.skip-link {
position: absolute;
- top: -40px;
- /* Adjust as needed to hide the link off-screen */
+ top: -40px; /* Adjust as needed to hide the link off-screen */
left: 0;
- background-color: #fff;
- /* Background color to cover the link for screen readers */
- z-index: 1060;
- /* Ensure it's above other content */
+ background-color: #fff; /* Background color to cover the link for screen readers */
+ z-index: 1060; /* Ensure it's above other content */
}
.skip-link:focus {
- top: 0;
- /* Bring the link back into view when it receives focus */
+ top: 0; /* Bring the link back into view when it receives focus */
}
#outer_container a:not(.btn):hover, footer a:not(.btn):hover {
text-decoration: underline;
}
-#outer_container a:not(.btn):has(span.fe):hover, footer a:not(.btn):has(span.fe):hover {
- /* do not have underlines on icons */
+#outer_container a:not(.btn):has(span.fe):hover, footer a:not(.btn):has(span.fe):hover { /* do not have underlines on icons */
text-decoration: none;
}
#outer_container a:not(.btn):active, footer a:not(.btn):active {
@@ -660,8 +654,7 @@ div.navbar {
background-repeat: no-repeat;
background-position: center center;
background-size: cover;
- border-radius: 5px 5px 0 0;
- /* top-left | top-right | bottom-right | bottom-left */
+ border-radius: 5px 5px 0 0; /* top-left | top-right | bottom-right | bottom-left */
height: 176px;
margin-left: -12px;
margin-right: -12px;
@@ -878,8 +871,7 @@ div.navbar {
margin: 0;
display: -webkit-box;
-webkit-box-orient: vertical;
- -webkit-line-clamp: 2;
- /* Limits the text to a specific number of lines */
+ -webkit-line-clamp: 2; /* Limits the text to a specific number of lines */
line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
@@ -919,8 +911,7 @@ div.navbar {
}
.post_teaser_video_preview .video-wrapper {
position: relative;
- padding-bottom: 56.25%;
- /* 16:9 aspect ratio */
+ padding-bottom: 56.25%; /* 16:9 aspect ratio */
height: 0;
overflow: hidden;
}
@@ -1022,8 +1013,7 @@ div.navbar {
#masonry .item img {
width: 100%;
display: block;
- height: auto;
- /* Ensure aspect ratio is maintained */
+ height: auto; /* Ensure aspect ratio is maintained */
}
#masonry .item .masonry_thumb a, #masonry .item .masonry_thumb a:active {
border: none;
@@ -1195,8 +1185,7 @@ div.navbar {
display: inline-block;
}
.voting_buttons_new .upvote_button, .voting_buttons_new .downvote_button {
- position: relative;
- /* so the htmx-indicators can be position: absolute */
+ position: relative; /* so the htmx-indicators can be position: absolute */
display: inline-block;
text-align: center;
cursor: pointer;
@@ -1735,6 +1724,12 @@ h1 .warning_badge {
object-fit: cover;
}
+.community_icon_small {
+ width: 20px;
+ height: 20px;
+ margin-right: 2px;
+}
+
.bump_up {
position: absolute;
top: 115px;
@@ -1956,7 +1951,6 @@ form h5 {
--bs-link-color-rgb: black;
--bs-link-hover-color-rgb: #333;
}
-
[data-bs-theme=dark] {
--bs-body-bg: black;
--bs-link-color: white;
@@ -1966,84 +1960,65 @@ form h5 {
--bs-body-color: white;
--bs-body-color-rgb: white;
}
-
.btn-primary {
--bs-btn-bg: black;
--bs-btn-border-color: black;
--bs-btn-hover-bg: #333;
}
-
[data-bs-theme=dark] .btn-primary {
--bs-btn-bg: white;
--bs-btn-border-color: white;
--bs-btn-hover-bg: white;
--bs-btn-color: black;
}
-
[data-bs-theme=dark] .btn-primary:hover {
--bs-btn-hover-color: black;
}
-
.btn-outline-secondary {
--bs-btn-color: black;
}
-
[data-bs-theme=dark] .btn-outline-secondary {
--bs-btn-color: white;
}
-
.post_list .post_teaser {
border-bottom: solid 1px black;
}
-
[data-bs-theme=dark] .post_list .post_teaser {
border-bottom: solid 1px white;
}
-
.domain_link, .domain_link a {
color: black;
}
-
[data-bs-theme=dark] .domain_link, [data-bs-theme=dark] .domain_link a {
color: ghostwhite;
}
-
.main_pane, .voting_buttons div {
border: solid 1px #333;
}
-
[data-bs-theme=dark] .main_pane, [data-bs-theme=dark] .voting_buttons div {
border: solid 1px #eee;
}
-
div.navbar {
border-bottom: solid 1px #333;
}
-
[data-bs-theme=dark] div.navbar {
border-bottom: solid 1px #fff;
}
-
.nav-link {
color: black;
}
-
[data-bs-theme=dark] .nav-link {
color: white;
}
-
.card {
--bs-card-border-color: #333;
}
-
[data-bs-theme=dark] .card {
--bs-card-border-color: #fff;
}
-
.coolfieldset.collapsed legend, .coolfieldset legend, .coolfieldset.expanded legend {
background-color: white;
}
-
[data-bs-theme=dark] .coolfieldset.collapsed legend, [data-bs-theme=dark] .coolfieldset legend, [data-bs-theme=dark] .coolfieldset.expanded legend {
background-color: black;
}
@@ -2054,19 +2029,16 @@ form h5 {
@media print {
#navbarSupportedContent, #side_pane, .voting_buttons, .comment_actions, footer, .post_reply_form, #post_replies .btn-group,
-.navbar .nav-link, .post_utilities_bar, .community_header, .community_icon_big, .mobile_create_post {
+ .navbar .nav-link, .post_utilities_bar, .community_header, .community_icon_big, .mobile_create_post {
display: none;
}
-
.navbar {
padding-top: 0;
padding-bottom: 0;
}
-
.comments > .comment, .post_teaser {
break-inside: avoid;
}
-
.main_pane {
border: none;
padding: 0;
@@ -2074,16 +2046,13 @@ form h5 {
.main_pane > .btn-group {
display: none;
}
-
.div.navbar {
border-bottom: none;
}
-
.limit_height {
overflow: visible !important;
max-height: initial;
}
-
.comment .show-more {
display: none;
}
diff --git a/app/static/styles.scss b/app/static/styles.scss
index 76658fc7..ba2dd848 100644
--- a/app/static/styles.scss
+++ b/app/static/styles.scss
@@ -1412,6 +1412,12 @@ h1 .warning_badge {
object-fit: cover;
}
+.community_icon_small {
+ width: 20px;
+ height: 20px;
+ margin-right: 2px;
+}
+
.bump_up {
position: absolute;
top: 115px;
diff --git a/app/templates/base.html b/app/templates/base.html
index 5ea81870..6f55245a 100644
--- a/app/templates/base.html
+++ b/app/templates/base.html
@@ -197,6 +197,7 @@
{{ _('Edit profile & settings') }}
{{ _('Chats') }}
{{ _('Bookmarks') }}
+ {{ _('Activity Alerts') }}
{% if current_user.is_authenticated and current_user.hide_read_posts -%}
{{ _('Read Posts') }}
{% endif -%}
diff --git a/app/templates/post/post_teaser/_article.html b/app/templates/post/post_teaser/_article.html
index ef26bea4..ea1b2379 100644
--- a/app/templates/post/post_teaser/_article.html
+++ b/app/templates/post/post_teaser/_article.html
@@ -7,8 +7,10 @@
{% endif -%}
{% if post.sticky -%}{% endif -%}
- {% if show_post_community -%}c/{{ post.community.name }}{% endif -%}
- by {{ render_username(post.author) }}
+ {% if show_post_community -%}
+ {% if post.community.icon_image() %}{% endif -%}
+ c/{{ post.community.name }}{% endif -%}
+ by {{ render_username(post.author) }}
{% if post.body_html -%}
{{ first_paragraph(post.body_html) | safe }}
@@ -27,4 +29,4 @@
{% endif -%}
-{% endif -%}
\ No newline at end of file
+{% endif -%}
diff --git a/app/templates/post/post_teaser/_image.html b/app/templates/post/post_teaser/_image.html
index 0af98ea2..c5e49104 100644
--- a/app/templates/post/post_teaser/_image.html
+++ b/app/templates/post/post_teaser/_image.html
@@ -10,8 +10,10 @@
{% endif -%}
{% if post.sticky -%}{% endif -%}
- {% if show_post_community -%}c/{{ post.community.name }}{% endif -%}
- by {{ render_username(post.author) }}
+ {% if show_post_community -%}
+ {% if post.community.icon_image() %}{% endif -%}
+ c/{{ post.community.name }}{% endif -%}
+ by {{ render_username(post.author) }}
{% if post.image_id and not low_bandwidth -%}
-{% endif -%}
\ No newline at end of file
+{% endif -%}
diff --git a/app/templates/post/post_teaser/_poll.html b/app/templates/post/post_teaser/_poll.html
index a720ca1b..8158f0a7 100644
--- a/app/templates/post/post_teaser/_poll.html
+++ b/app/templates/post/post_teaser/_poll.html
@@ -8,8 +8,10 @@
{% endif -%}
{% if post.sticky -%}{% endif -%}
- {% if show_post_community -%}c/{{ post.community.name }}{% endif -%}
- by {{ render_username(post.author) }}
+ {% if show_post_community -%}
+ {% if post.community.icon_image() %}{% endif -%}
+ c/{{ post.community.name }}{% endif -%}
+ by {{ render_username(post.author) }}
{% include "post/post_teaser/_utilities_bar.html" %}
{% endif -%}
-
\ No newline at end of file
+
diff --git a/app/templates/post/post_teaser/_video.html b/app/templates/post/post_teaser/_video.html
index 5fc51894..61836b04 100644
--- a/app/templates/post/post_teaser/_video.html
+++ b/app/templates/post/post_teaser/_video.html
@@ -15,8 +15,10 @@
{% endif -%}
{% if post.sticky -%}{% endif -%}
- {% if show_post_community -%}c/{{ post.community.name }}{% endif -%}
- by {{ render_username(post.author) }}
+ {% if show_post_community -%}
+ {% if post.community.icon_image() %}{% endif -%}
+ c/{{ post.community.name }}{% endif -%}
+ by {{ render_username(post.author) }}
{% if not low_bandwidth %}
@@ -48,4 +50,4 @@
{% endif -%}
{% include "post/post_teaser/_utilities_bar.html" %}
-
\ No newline at end of file
+
diff --git a/app/templates/user/alerts.html b/app/templates/user/alerts.html
new file mode 100644
index 00000000..a440af69
--- /dev/null
+++ b/app/templates/user/alerts.html
@@ -0,0 +1,35 @@
+{% if theme() and file_exists('app/templates/themes/' + theme() + '/base.html') %}
+ {% extends 'themes/' + theme() + '/base.html' %}
+{% else %}
+ {% extends "base.html" %}
+{% endif %}
+{% set active_child = 'alerts' %}
+
+{% block app_content %}
+
+{% endblock %}
diff --git a/app/templates/user/alerts/_comments.html b/app/templates/user/alerts/_comments.html
new file mode 100644
index 00000000..128b3734
--- /dev/null
+++ b/app/templates/user/alerts/_comments.html
@@ -0,0 +1,21 @@
+{% include 'user/alerts/_filter.html' %}
+
+
{{ _('You will be notified of replies to these comments') }}
+ {% for reply in entities.items %}
+
+
+
+
+ {% with comment=dict(comment=reply) %}
+ {% include 'post/_reply_notification_toggle.html' %}
+ {% endwith %}
+
+
+
+ {% else %}
+
{{ _('You have not subscribed to any comments. Use the bell icon on each comment to do so.') }}
+ {% endfor %}
+
+
diff --git a/app/templates/user/alerts/_communities.html b/app/templates/user/alerts/_communities.html
new file mode 100644
index 00000000..421cbcba
--- /dev/null
+++ b/app/templates/user/alerts/_communities.html
@@ -0,0 +1,19 @@
+{% include 'user/alerts/_filter.html' %}
+
+
{{ _('You will be notified of new posts in these communities') }}
+ {% for community in entities.items %}
+
+
+
+
+ {% include 'community/_notification_toggle.html' %}
+
+
+
+ {% else %}
+
{{ _('You have not subscribed to an alert for new content in any communities. Use the bell icon in each community to do so.') }}
+ {% endfor %}
+
+
diff --git a/app/templates/user/alerts/_filter.html b/app/templates/user/alerts/_filter.html
new file mode 100644
index 00000000..c6cb0197
--- /dev/null
+++ b/app/templates/user/alerts/_filter.html
@@ -0,0 +1,11 @@
+
diff --git a/app/templates/user/alerts/_posts.html b/app/templates/user/alerts/_posts.html
new file mode 100644
index 00000000..980c8ff6
--- /dev/null
+++ b/app/templates/user/alerts/_posts.html
@@ -0,0 +1,19 @@
+{% include 'user/alerts/_filter.html' %}
+
+
{{ _('You will be notified of top-level replies to these posts') }}
+ {% for post in entities.items %}
+
+
+
+
+ {% include 'post/_post_notification_toggle.html' %}
+
+
+
+ {% else %}
+
{{ _('You have not subscribed to any posts. Use the bell icon on each post to do so.') }}
+ {% endfor %}
+
+
diff --git a/app/templates/user/alerts/_topics.html b/app/templates/user/alerts/_topics.html
new file mode 100644
index 00000000..9e008e83
--- /dev/null
+++ b/app/templates/user/alerts/_topics.html
@@ -0,0 +1,18 @@
+
+
{{ _('You will be notified of new posts in communities covered by these topics') }}
+ {% for topic in entities.items -%}
+
+
+
+
+ {% include 'topic/_notification_toggle.html' -%}
+
+
+
+ {% else -%}
+
{{ _('You have not subscribed to any topics. Use the bell icon on each topic to do so.') }}
+ {% endfor -%}
+
+
diff --git a/app/templates/user/alerts/_type.html b/app/templates/user/alerts/_type.html
new file mode 100644
index 00000000..6e06d535
--- /dev/null
+++ b/app/templates/user/alerts/_type.html
@@ -0,0 +1,17 @@
+
diff --git a/app/templates/user/alerts/_users.html b/app/templates/user/alerts/_users.html
new file mode 100644
index 00000000..e8ce1386
--- /dev/null
+++ b/app/templates/user/alerts/_users.html
@@ -0,0 +1,18 @@
+
+
{{ _('You will be notified of new posts by these users') }}
+ {% for user in entities.items %}
+
+
+
+
+ {% include 'user/_notification_toggle.html' %}
+
+
+
+ {% else %}
+
{{ _('You have not subscribed to an alert for new content from any users. Use the bell icon on each person to do so.') }}
+ {% endfor %}
+
+
diff --git a/app/user/routes.py b/app/user/routes.py
index 8f3b15f8..8815a6de 100644
--- a/app/user/routes.py
+++ b/app/user/routes.py
@@ -12,12 +12,11 @@ from app.activitypub.signature import post_request, default_context
from app.activitypub.util import find_actor_or_create
from app.auth.util import random_token
from app.community.util import save_icon_file, save_banner_file, retrieve_mods_and_backfill
-from app.constants import SUBSCRIPTION_MEMBER, SUBSCRIPTION_PENDING, NOTIF_USER, POST_TYPE_VIDEO, POST_TYPE_LINK, \
- POST_TYPE_IMAGE, POST_TYPE_POLL
+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
+ InstanceBlock, NotificationSubscription, PostBookmark, PostReplyBookmark, read_posts, Topic
from app.user import bp
from app.user.forms import ProfileForm, SettingsForm, DeleteAccountForm, ReportUserForm, \
FilterForm, KeywordFilterEditForm, RemoteFollowForm, ImportExportForm
@@ -1143,6 +1142,86 @@ def user_bookmarks_comments():
next_url=next_url, prev_url=prev_url)
+@bp.route('/alerts')
+@bp.route('/alerts//')
+@login_required
+def user_alerts(type='posts', filter='all'):
+ page = request.args.get('page', 1, type=int)
+ low_bandwidth = request.cookies.get('low_bandwidth', '0') == '1'
+
+ if type == 'comments':
+ if filter == 'mine':
+ entities = PostReply.query.filter_by(deleted=False, user_id=current_user.id).\
+ join(NotificationSubscription, NotificationSubscription.entity_id == PostReply.id).\
+ filter_by(type=NOTIF_REPLY, user_id=current_user.id).order_by(desc(NotificationSubscription.created_at))
+ elif filter == 'others':
+ entities = PostReply.query.filter(PostReply.deleted == False, PostReply.user_id != current_user.id).\
+ join(NotificationSubscription, NotificationSubscription.entity_id == PostReply.id).\
+ filter_by(type=NOTIF_REPLY, user_id=current_user.id).order_by(desc(NotificationSubscription.created_at))
+ else: # default to 'all' filter
+ entities = PostReply.query.filter_by(deleted=False).\
+ join(NotificationSubscription, NotificationSubscription.entity_id == PostReply.id).\
+ filter_by(type=NOTIF_REPLY, user_id=current_user.id).order_by(desc(NotificationSubscription.created_at))
+ title = _('Reply Alerts')
+
+ elif type == 'communities':
+ if filter == 'mine':
+ entities = Community.query.\
+ join(CommunityMember, CommunityMember.community_id == Community.id).\
+ filter_by(user_id=current_user.id, is_moderator=True).\
+ join(NotificationSubscription, NotificationSubscription.entity_id == CommunityMember.community_id).\
+ filter_by(type=NOTIF_COMMUNITY, user_id=current_user.id).order_by(desc(NotificationSubscription.created_at))
+ elif filter == 'others':
+ entities = Community.query.\
+ join(CommunityMember, CommunityMember.community_id == Community.id).\
+ filter_by(user_id=current_user.id, is_moderator=False).\
+ join(NotificationSubscription, NotificationSubscription.entity_id == CommunityMember.community_id).\
+ filter_by(type=NOTIF_COMMUNITY, user_id=current_user.id).order_by(desc(NotificationSubscription.created_at))
+ else: # default to 'all' filter
+ entities = Community.query.\
+ join(NotificationSubscription, NotificationSubscription.entity_id == Community.id).\
+ filter_by(type=NOTIF_COMMUNITY, user_id=current_user.id).order_by(desc(NotificationSubscription.created_at))
+ title = _('Community Alerts')
+
+ elif type == 'topics':
+ # ignore filter
+ entities = Topic.query.join(NotificationSubscription, NotificationSubscription.entity_id == Topic.id).\
+ filter_by(type=NOTIF_TOPIC, user_id=current_user.id).order_by(desc(NotificationSubscription.created_at))
+ title = _('Topic Alerts')
+
+ elif type == 'users':
+ # ignore filter
+ entities = User.query.join(NotificationSubscription, NotificationSubscription.entity_id == User.id).\
+ filter_by(type=NOTIF_USER, user_id=current_user.id).order_by(desc(NotificationSubscription.created_at))
+ title = _('User Alerts')
+
+ else: # default to 'posts' type
+ if filter == 'mine':
+ entities = Post.query.filter_by(deleted=False, user_id=current_user.id).\
+ join(NotificationSubscription, NotificationSubscription.entity_id == Post.id).\
+ filter_by(type=NOTIF_POST, user_id=current_user.id).order_by(desc(NotificationSubscription.created_at))
+ elif filter == 'others':
+ entities = Post.query.filter(Post.deleted == False, Post.user_id != current_user.id).\
+ join(NotificationSubscription, NotificationSubscription.entity_id == Post.id).\
+ filter_by(type=NOTIF_POST, user_id=current_user.id).order_by(desc(NotificationSubscription.created_at))
+ else: # default to 'all' filter
+ entities = Post.query.filter_by(deleted=False).\
+ join(NotificationSubscription, NotificationSubscription.entity_id == Post.id).\
+ filter_by(type=NOTIF_POST, user_id=current_user.id).order_by(desc(NotificationSubscription.created_at))
+ title = _('Post Alerts')
+
+ entities = entities.paginate(page=page, per_page=100 if not low_bandwidth else 50, error_out=False)
+ next_url = url_for('user.user_alerts', page=entities.next_num, type=type, filter=filter) if entities.has_next else None
+ prev_url = url_for('user.user_alerts', page=entities.prev_num, type=type, filter=filter) if entities.has_prev and page != 1 else None
+
+ return render_template('user/alerts.html', title=title, entities=entities,
+ low_bandwidth=low_bandwidth, user=current_user, type=type, filter=filter,
+ moderating_communities=moderating_communities(current_user.get_id()),
+ joined_communities=joined_communities(current_user.get_id()),
+ menu_topics=menu_topics(), site=g.site,
+ next_url=next_url, prev_url=prev_url)
+
+
@bp.route('/u//fediverse_redirect', methods=['GET', 'POST'])
def fediverse_redirect(actor):
actor = actor.strip()