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() %}Community icon{% 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() %}Community icon{% endif -%} + c/{{ post.community.name }}{% endif -%} + by {{ render_username(post.author) }} {% if post.image_id and not low_bandwidth -%}
    {% endif -%} - {% if show_post_community -%}c/{{ post.community.name }}{% endif -%} - by {{ render_username(post.author) }} + {% if show_post_community -%} + {% if post.community.icon_image() %}Community icon{% endif -%} + c/{{ post.community.name }}{% endif -%} + by {{ render_username(post.author) }} {% if post.body_html -%}
    {{ first_paragraph(post.body_html) | safe }} @@ -33,4 +35,4 @@ {% endif -%}
    -{% 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() %}Community icon{% endif -%} + c/{{ post.community.name }}{% endif -%} + by {{ render_username(post.author) }} {% include "post/post_teaser/_utilities_bar.html" %}
    @@ -27,4 +29,4 @@
    {% 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() %}Community icon{% 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 %} +
    +
    + + {% include 'user/alerts/_type.html' %} + {% include 'user/alerts/_' + type + '.html' %} + + +
    +
    +{% 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 @@ +
    + + {{ _('All') }} + + + {{ _('Mine') }} + + + {{ _('Others') }} + +
    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 @@ +
    + + {{ _('Posts') }} + + + {{ _('Comments') }} + + + {{ _('Communities') }} + + + {{ _('Topics') }} + + + {{ _('Users') }} + +
    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()