diff --git a/app/community/routes.py b/app/community/routes.py index 88325be6..96b53bdb 100644 --- a/app/community/routes.py +++ b/app/community/routes.py @@ -30,7 +30,7 @@ from app.utils import get_setting, render_template, allowlist_html, markdown_to_ shorten_string, gibberish, community_membership, ap_datetime, \ request_etag_matches, return_304, instance_banned, can_create_post, can_upvote, can_downvote, user_filters_posts, \ joined_communities, moderating_communities, blocked_domains, mimetype_from_url, blocked_instances, \ - community_moderators, communities_banned_from, show_ban_message + community_moderators, communities_banned_from, show_ban_message, recently_upvoted_posts, recently_downvoted_posts from feedgen.feed import FeedGenerator from datetime import timezone, timedelta @@ -241,12 +241,21 @@ def show_community(community: Community): prev_url = url_for('activitypub.community_profile', actor=community.ap_id if community.ap_id is not None else community.name, page=posts.prev_num, sort=sort, layout=post_layout) if posts.has_prev and page != 1 else None + # Voting history + if current_user.is_authenticated: + recently_upvoted = recently_upvoted_posts(current_user.id) + recently_downvoted = recently_downvoted_posts(current_user.id) + else: + recently_upvoted = [] + recently_downvoted = [] + return render_template('community/community.html', community=community, title=community.title, breadcrumbs=breadcrumbs, is_moderator=is_moderator, is_owner=is_owner, is_admin=is_admin, mods=mod_list, posts=posts, description=description, og_image=og_image, POST_TYPE_IMAGE=POST_TYPE_IMAGE, POST_TYPE_LINK=POST_TYPE_LINK, SUBSCRIPTION_PENDING=SUBSCRIPTION_PENDING, SUBSCRIPTION_MEMBER=SUBSCRIPTION_MEMBER, SUBSCRIPTION_OWNER=SUBSCRIPTION_OWNER, SUBSCRIPTION_MODERATOR=SUBSCRIPTION_MODERATOR, etag=f"{community.id}{sort}{post_layout}_{hash(community.last_active)}", related_communities=related_communities, next_url=next_url, prev_url=prev_url, low_bandwidth=low_bandwidth, + recently_upvoted=recently_upvoted, recently_downvoted=recently_downvoted, rss_feed=f"https://{current_app.config['SERVER_NAME']}/community/{community.link()}/feed", rss_feed_name=f"{community.title} on PieFed", content_filters=content_filters, moderating_communities=moderating_communities(current_user.get_id()), joined_communities=joined_communities(current_user.get_id()), sort=sort, diff --git a/app/main/routes.py b/app/main/routes.py index e1accd8f..09e1a6f0 100644 --- a/app/main/routes.py +++ b/app/main/routes.py @@ -25,7 +25,7 @@ from sqlalchemy_searchable import search from app.utils import render_template, get_setting, gibberish, request_etag_matches, return_304, blocked_domains, \ ap_datetime, ip_address, retrieve_block_list, shorten_string, markdown_to_text, user_filters_home, \ joined_communities, moderating_communities, parse_page, theme_list, get_request, markdown_to_html, allowlist_html, \ - blocked_instances, communities_banned_from, topic_tree + blocked_instances, communities_banned_from, topic_tree, recently_upvoted_posts, recently_downvoted_posts from app.models import Community, CommunityMember, Post, Site, User, utcnow, Domain, Topic, File, Instance, \ InstanceRole, Notification from PIL import Image @@ -140,9 +140,18 @@ def home_page(type, sort): active_communities = active_communities.filter(Community.id.not_in(banned_from)) active_communities = active_communities.order_by(desc(Community.last_active)).limit(5).all() + # Voting history + if current_user.is_authenticated: + recently_upvoted = recently_upvoted_posts(current_user.id) + recently_downvoted = recently_downvoted_posts(current_user.id) + else: + recently_upvoted = [] + recently_downvoted = [] + return render_template('index.html', posts=posts, active_communities=active_communities, show_post_community=True, POST_TYPE_IMAGE=POST_TYPE_IMAGE, POST_TYPE_LINK=POST_TYPE_LINK, - low_bandwidth=low_bandwidth, + low_bandwidth=low_bandwidth, recently_upvoted=recently_upvoted, + recently_downvoted=recently_downvoted, SUBSCRIPTION_PENDING=SUBSCRIPTION_PENDING, SUBSCRIPTION_MEMBER=SUBSCRIPTION_MEMBER, etag=f"{type}_{sort}_{hash(str(g.site.last_active))}", next_url=next_url, prev_url=prev_url, #rss_feed=f"https://{current_app.config['SERVER_NAME']}/feed", @@ -294,6 +303,9 @@ def list_files(directory): @bp.route('/test') def test(): + x = recently_upvoted_posts(1) + + return x md = "::: spoiler I'm all for ya having fun and your right to hurt yourself.\n\nI am a former racer, commuter, and professional Buyer for a chain of bike shops. I'm also disabled from the crash involving the 6th and 7th cars that have hit me in the last 170k+ miles of riding. I only barely survived what I simplify as a \"broken neck and back.\" Cars making U-turns are what will get you if you ride long enough, \n\nespecially commuting. It will look like just another person turning in front of you, you'll compensate like usual, and before your brain can even register what is really happening, what was your normal escape route will close and you're going to crash really hard. It is the only kind of crash that your intuition is useless against.\n:::" diff --git a/app/post/routes.py b/app/post/routes.py index 9e1dd406..c3f72e3d 100644 --- a/app/post/routes.py +++ b/app/post/routes.py @@ -24,7 +24,8 @@ from app.utils import get_setting, render_template, allowlist_html, markdown_to_ shorten_string, markdown_to_text, gibberish, ap_datetime, return_304, \ request_etag_matches, ip_address, user_ip_banned, instance_banned, can_downvote, can_upvote, post_ranking, \ reply_already_exists, reply_is_just_link_to_gif_reaction, confidence, moderating_communities, joined_communities, \ - blocked_instances, blocked_domains, community_moderators, blocked_phrases, show_ban_message + blocked_instances, blocked_domains, community_moderators, blocked_phrases, show_ban_message, recently_upvoted_posts, \ + recently_downvoted_posts, recently_upvoted_post_replies, recently_downvoted_post_replies def show_post(post_id: int): @@ -239,12 +240,26 @@ def show_post(post_id: int): breadcrumb.url = '/communities' breadcrumbs.append(breadcrumb) + # Voting history + if current_user.is_authenticated: + recently_upvoted = recently_upvoted_posts(current_user.id) + recently_downvoted = recently_downvoted_posts(current_user.id) + recently_upvoted_replies = recently_upvoted_post_replies(current_user.id) + recently_downvoted_replies = recently_downvoted_post_replies(current_user.id) + else: + recently_upvoted = [] + recently_downvoted = [] + recently_upvoted_replies = [] + recently_downvoted_replies = [] + response = render_template('post/post.html', title=post.title, post=post, is_moderator=is_moderator, community=post.community, breadcrumbs=breadcrumbs, related_communities=related_communities, mods=mod_list, canonical=post.ap_id, form=form, replies=replies, THREAD_CUTOFF_DEPTH=constants.THREAD_CUTOFF_DEPTH, description=description, og_image=og_image, POST_TYPE_IMAGE=constants.POST_TYPE_IMAGE, POST_TYPE_LINK=constants.POST_TYPE_LINK, POST_TYPE_ARTICLE=constants.POST_TYPE_ARTICLE, noindex=not post.author.indexable, + recently_upvoted=recently_upvoted, recently_downvoted=recently_downvoted, + recently_upvoted_replies=recently_upvoted_replies, recently_downvoted_replies=recently_downvoted_replies, etag=f"{post.id}{sort}_{hash(post.last_active)}", markdown_editor=current_user.is_authenticated and current_user.markdown_editor, low_bandwidth=request.cookies.get('low_bandwidth', '0') == '1', SUBSCRIPTION_MEMBER=SUBSCRIPTION_MEMBER, moderating_communities=moderating_communities(current_user.get_id()), @@ -259,7 +274,6 @@ def show_post(post_id: int): @login_required @validation_required def post_vote(post_id: int, vote_direction): - upvoted_class = downvoted_class = '' post = Post.query.get_or_404(post_id) existing_vote = PostVote.query.filter_by(user_id=current_user.id, post_id=post.id).first() if existing_vote: @@ -275,7 +289,6 @@ def post_vote(post_id: int, vote_direction): post.up_votes -= 1 post.down_votes += 1 post.score -= 2 - downvoted_class = 'voted_down' else: # previous vote was down if vote_direction == 'downvote': # new vote is also down, so remove it db.session.delete(existing_vote) @@ -286,18 +299,15 @@ def post_vote(post_id: int, vote_direction): post.up_votes += 1 post.down_votes -= 1 post.score += 2 - upvoted_class = 'voted_up' else: if vote_direction == 'upvote': effect = 1 post.up_votes += 1 post.score += 1 - upvoted_class = 'voted_up' else: effect = -1 post.down_votes += 1 post.score -= 1 - downvoted_class = 'voted_down' vote = PostVote(user_id=current_user.id, post_id=post.id, author_id=post.author.id, effect=effect) # upvotes do not increase reputation in low quality communities @@ -346,17 +356,25 @@ def post_vote(post_id: int, vote_direction): current_user.recalculate_attitude() db.session.commit() post.flush_cache() + + recently_upvoted = [] + recently_downvoted = [] + if vote_direction == 'upvote': + recently_upvoted = [post_id] + elif vote_direction == 'downvote': + recently_downvoted = [post_id] + cache.delete_memoized(recently_upvoted_posts, current_user.id) + cache.delete_memoized(recently_downvoted_posts, current_user.id) + template = 'post/_post_voting_buttons.html' if request.args.get('style', '') == '' else 'post/_post_voting_buttons_masonry.html' - return render_template(template, post=post, community=post.community, - upvoted_class=upvoted_class, - downvoted_class=downvoted_class) + return render_template(template, post=post, community=post.community, recently_upvoted=recently_upvoted, + recently_downvoted=recently_downvoted) @bp.route('/comment//', methods=['POST']) @login_required @validation_required def comment_vote(comment_id, vote_direction): - upvoted_class = downvoted_class = '' comment = PostReply.query.get_or_404(comment_id) existing_vote = PostReplyVote.query.filter_by(user_id=current_user.id, post_reply_id=comment.id).first() if existing_vote: @@ -423,9 +441,20 @@ def comment_vote(comment_id, vote_direction): db.session.commit() comment.post.flush_cache() + + recently_upvoted = [] + recently_downvoted = [] + if vote_direction == 'upvote': + recently_upvoted = [comment_id] + elif vote_direction == 'downvote': + recently_downvoted = [comment_id] + cache.delete_memoized(recently_upvoted_post_replies, current_user.id) + cache.delete_memoized(recently_downvoted_post_replies, current_user.id) + return render_template('post/_comment_voting_buttons.html', comment=comment, - upvoted_class=upvoted_class, - downvoted_class=downvoted_class, community=comment.community) + recently_upvoted_replies=recently_upvoted, + recently_downvoted_replies=recently_downvoted, + community=comment.community) @bp.route('/post//comment/') diff --git a/app/templates/post/_comment_voting_buttons.html b/app/templates/post/_comment_voting_buttons.html index 3677d0e3..1435942d 100644 --- a/app/templates/post/_comment_voting_buttons.html +++ b/app/templates/post/_comment_voting_buttons.html @@ -1,6 +1,6 @@ {% if current_user.is_authenticated and current_user.verified %} {% if can_upvote(current_user, community) %} -
@@ -8,7 +8,7 @@ {% endif %} {{ comment.up_votes - comment.down_votes }} {% if can_downvote(current_user, community) %} -
diff --git a/app/templates/post/_post_voting_buttons.html b/app/templates/post/_post_voting_buttons.html index 76ad9bd4..ed101f22 100644 --- a/app/templates/post/_post_voting_buttons.html +++ b/app/templates/post/_post_voting_buttons.html @@ -1,6 +1,6 @@ {% if current_user.is_authenticated and current_user.verified %} {% if can_upvote(current_user, post.community) %} -
{{ shorten_number(post.up_votes) }} @@ -8,7 +8,7 @@
{% endif %} {% if can_downvote(current_user, post.community) %} -
{{ shorten_number(post.down_votes) }} diff --git a/app/templates/post/_post_voting_buttons_masonry.html b/app/templates/post/_post_voting_buttons_masonry.html index 7ce6a438..82d11b3c 100644 --- a/app/templates/post/_post_voting_buttons_masonry.html +++ b/app/templates/post/_post_voting_buttons_masonry.html @@ -1,13 +1,13 @@ {% if current_user.is_authenticated and current_user.verified %} {% if can_upvote(current_user, post.community) %} -
{% endif %} {% if can_downvote(current_user, post.community) %} -
diff --git a/app/utils.py b/app/utils.py index e00d0890..82317870 100644 --- a/app/utils.py +++ b/app/utils.py @@ -1,5 +1,6 @@ from __future__ import annotations +import bisect import hashlib import mimetypes import random @@ -861,3 +862,37 @@ def show_ban_message(): resp = make_response(redirect(url_for('main.index'))) resp.set_cookie('sesion', '17489047567495', expires=datetime(year=2099, month=12, day=30)) return resp + + +# search a sorted list using a binary search. Faster than using 'in' with a unsorted list. +def in_sorted_list(arr, target): + index = bisect.bisect_left(arr, target) + return index < len(arr) and arr[index] == target + + +@cache.memoize(timeout=600) +def recently_upvoted_posts(user_id) -> List[int]: + post_ids = db.session.execute(text('SELECT post_id FROM "post_vote" WHERE user_id = :user_id AND effect > 0 ORDER BY id DESC LIMIT 1000'), + {'user_id': user_id}).scalars() + return sorted(post_ids) # sorted so that in_sorted_list can be used + + +@cache.memoize(timeout=600) +def recently_downvoted_posts(user_id) -> List[int]: + post_ids = db.session.execute(text('SELECT post_id FROM "post_vote" WHERE user_id = :user_id AND effect < 0 ORDER BY id DESC LIMIT 1000'), + {'user_id': user_id}).scalars() + return sorted(post_ids) + + +@cache.memoize(timeout=600) +def recently_upvoted_post_replies(user_id) -> List[int]: + reply_ids = db.session.execute(text('SELECT post_reply_id FROM "post_reply_vote" WHERE user_id = :user_id AND effect > 0 ORDER BY id DESC LIMIT 1000'), + {'user_id': user_id}).scalars() + return sorted(reply_ids) # sorted so that in_sorted_list can be used + + +@cache.memoize(timeout=600) +def recently_downvoted_post_replies(user_id) -> List[int]: + reply_ids = db.session.execute(text('SELECT post_reply_id FROM "post_reply_vote" WHERE user_id = :user_id AND effect < 0 ORDER BY id DESC LIMIT 1000'), + {'user_id': user_id}).scalars() + return sorted(reply_ids) diff --git a/pyfedi.py b/pyfedi.py index 8efd05bb..0d1b4b4b 100644 --- a/pyfedi.py +++ b/pyfedi.py @@ -11,7 +11,8 @@ from flask import session, g, json, request, current_app from app.constants import POST_TYPE_LINK, POST_TYPE_IMAGE, POST_TYPE_ARTICLE from app.models import Site from app.utils import getmtime, gibberish, shorten_string, shorten_url, digits, user_access, community_membership, \ - can_create_post, can_upvote, can_downvote, shorten_number, ap_datetime, current_theme, community_link_to_href + can_create_post, can_upvote, can_downvote, shorten_number, ap_datetime, current_theme, community_link_to_href, \ + in_sorted_list app = create_app() cli.register(app) @@ -42,6 +43,7 @@ with app.app_context(): app.jinja_env.globals['can_create'] = can_create_post app.jinja_env.globals['can_upvote'] = can_upvote app.jinja_env.globals['can_downvote'] = can_downvote + app.jinja_env.globals['in_sorted_list'] = in_sorted_list app.jinja_env.globals['theme'] = current_theme app.jinja_env.globals['file_exists'] = os.path.exists app.jinja_env.filters['community_links'] = community_link_to_href