mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-01-23 19:36:56 -08:00
Merge pull request 'Adding a "Hide Read" function' (#331) from JollyDevelopment/pyfedi:jollydev/hide-read-function into main
Reviewed-on: https://codeberg.org/rimu/pyfedi/pulls/331
This commit is contained in:
commit
39cffce73c
15 changed files with 262 additions and 0 deletions
|
@ -228,6 +228,13 @@ def show_community(community: Community):
|
|||
posts = posts.filter(Post.nsfl == False)
|
||||
if current_user.hide_nsfw == 1:
|
||||
posts = posts.filter(Post.nsfw == False)
|
||||
if current_user.hide_read_posts:
|
||||
cu_rp = current_user.read_post.all()
|
||||
cu_rp_ids = []
|
||||
for p in cu_rp:
|
||||
cu_rp_ids.append(p.id)
|
||||
for p_id in cu_rp_ids:
|
||||
posts = posts.filter(Post.id != p_id)
|
||||
content_filters = user_filters_posts(current_user.id)
|
||||
posts = posts.filter(Post.deleted == False)
|
||||
|
||||
|
|
|
@ -38,6 +38,16 @@ def show_domain(domain_id):
|
|||
content_filters = user_filters_posts(current_user.id)
|
||||
else:
|
||||
content_filters = {}
|
||||
|
||||
# don't show posts a user has already interacted with
|
||||
if current_user.hide_read_posts:
|
||||
cu_rp = current_user.read_post.all()
|
||||
cu_rp_ids = []
|
||||
for p in cu_rp:
|
||||
cu_rp_ids.append(p.id)
|
||||
for p_id in cu_rp_ids:
|
||||
posts = posts.filter(Post.id != p_id)
|
||||
|
||||
# pagination
|
||||
posts = posts.paginate(page=page, per_page=100, error_out=False)
|
||||
next_url = url_for('domain.show_domain', domain_id=domain_id, page=posts.next_num) if posts.has_next else None
|
||||
|
|
|
@ -71,6 +71,13 @@ def home_page(sort, view_filter):
|
|||
posts = posts.filter(Post.nsfl == False)
|
||||
if current_user.hide_nsfw == 1:
|
||||
posts = posts.filter(Post.nsfw == False)
|
||||
if current_user.hide_read_posts:
|
||||
cu_rp = current_user.read_post.all()
|
||||
cu_rp_ids = []
|
||||
for p in cu_rp:
|
||||
cu_rp_ids.append(p.id)
|
||||
for p_id in cu_rp_ids:
|
||||
posts = posts.filter(Post.id != p_id)
|
||||
|
||||
domains_ids = blocked_domains(current_user.id)
|
||||
if domains_ids:
|
||||
|
|
|
@ -613,6 +613,12 @@ user_role = db.Table('user_role',
|
|||
db.PrimaryKeyConstraint('user_id', 'role_id')
|
||||
)
|
||||
|
||||
# table to hold users' 'read' post ids
|
||||
read_posts = db.Table('read_posts',
|
||||
db.Column('user_id', db.Integer, db.ForeignKey('user.id'), index=True),
|
||||
db.Column('read_post_id', db.Integer, db.ForeignKey('post.id'), index=True),
|
||||
db.Column('interacted_at', db.DateTime, index=True, default=utcnow) # this is when the content is interacte with
|
||||
)
|
||||
|
||||
class User(UserMixin, db.Model):
|
||||
query_class = FullTextSearchQuery
|
||||
|
@ -692,6 +698,12 @@ class User(UserMixin, db.Model):
|
|||
|
||||
roles = db.relationship('Role', secondary=user_role, lazy='dynamic', cascade="all, delete")
|
||||
|
||||
hide_read_posts = db.Column(db.Boolean, default=False)
|
||||
# db relationship tracked by the "read_posts" table
|
||||
# this is the User side, so its referencing the Post side
|
||||
# read_by is the corresponding Post object variable
|
||||
read_post = db.relationship('Post', secondary=read_posts, back_populates='read_by', lazy='dynamic')
|
||||
|
||||
def __repr__(self):
|
||||
return '<User {}_{}>'.format(self.user_name, self.id)
|
||||
|
||||
|
@ -1017,6 +1029,18 @@ class User(UserMixin, db.Model):
|
|||
except Exception as e:
|
||||
return str(e)
|
||||
|
||||
# mark a post as 'read' for this user
|
||||
def mark_post_as_read(self, post):
|
||||
# check if its already marked as read, if not, mark it as read
|
||||
if not self.has_read_post(post):
|
||||
self.read_post.append(post)
|
||||
|
||||
# check if post has been read by this user
|
||||
# returns true if the post has been read, false if not
|
||||
def has_read_post(self, post):
|
||||
return self.read_post.filter(read_posts.c.read_post_id == post.id).count() > 0
|
||||
|
||||
|
||||
|
||||
class ActivityLog(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
@ -1079,6 +1103,11 @@ class Post(db.Model):
|
|||
replies = db.relationship('PostReply', lazy='dynamic', backref='post')
|
||||
language = db.relationship('Language', foreign_keys=[language_id])
|
||||
|
||||
# db relationship tracked by the "read_posts" table
|
||||
# this is the Post side, so its referencing the User side
|
||||
# read_post is the corresponding User object variable
|
||||
read_by = db.relationship('User', secondary=read_posts, back_populates='read_post', lazy='dynamic')
|
||||
|
||||
def is_local(self):
|
||||
return self.ap_id is None or self.ap_id.startswith('https://' + current_app.config['SERVER_NAME'])
|
||||
|
||||
|
@ -1560,6 +1589,7 @@ class PostReply(db.Model):
|
|||
return undo
|
||||
|
||||
|
||||
|
||||
class Domain(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(255), index=True)
|
||||
|
|
|
@ -247,6 +247,12 @@ def show_post(post_id: int):
|
|||
if post.type == POST_TYPE_LINK and body_has_no_archive_link(post.body_html) and url_needs_archive(post.url):
|
||||
archive_link = generate_archive_link(post.url)
|
||||
|
||||
# for logged in users who have the 'hide read posts' function enabled
|
||||
# mark this post as read
|
||||
if current_user.is_authenticated and current_user.hide_read_posts:
|
||||
current_user.mark_post_as_read(post)
|
||||
db.session.commit()
|
||||
|
||||
response = render_template('post/post.html', title=post.title, post=post, is_moderator=is_moderator, is_owner=community.is_owner(),
|
||||
community=post.community,
|
||||
breadcrumbs=breadcrumbs, related_communities=related_communities, mods=mod_list,
|
||||
|
@ -330,6 +336,12 @@ def post_vote(post_id: int, vote_direction):
|
|||
cache.delete_memoized(recently_upvoted_posts, current_user.id)
|
||||
cache.delete_memoized(recently_downvoted_posts, current_user.id)
|
||||
|
||||
# for logged in users who have the 'hide read posts' function enabled
|
||||
# mark this post as read
|
||||
if current_user.is_authenticated and current_user.hide_read_posts:
|
||||
current_user.mark_post_as_read(post)
|
||||
db.session.commit()
|
||||
|
||||
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, recently_upvoted=recently_upvoted,
|
||||
recently_downvoted=recently_downvoted)
|
||||
|
|
|
@ -113,3 +113,4 @@ def url_needs_archive(url) -> bool:
|
|||
|
||||
def generate_archive_link(url) -> bool:
|
||||
return 'https://archive.ph/' + url
|
||||
|
||||
|
|
|
@ -197,6 +197,7 @@
|
|||
<li><a class="dropdown-item{% if active_child == 'edit_profile' %} active{% endif %}" href="/user/settings">{{ _('Edit profile & settings') }}</a></li>
|
||||
<li><a class="dropdown-item{% if active_child == 'chats' %} active{% endif %}" href="/chat">{{ _('Chats') }}</a></li>
|
||||
<li><a class="dropdown-item{% if active_child == 'bookmarks' %} active{% endif %}" href="/bookmarks">{{ _('Bookmarks') }}</a></li>
|
||||
<li><a class="dropdown-item{% if active_child == 'read_posts' %} active{% endif %}" href="/read-posts">{{ _('Read Posts') }}</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item"><a class="nav-link" href="/donate">{{ _('Donate') }}</a></li>
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
<h5> Visibility </h5>
|
||||
{{ render_field(form.searchable) }}
|
||||
{{ render_field(form.indexable) }}
|
||||
{{ render_field(form.hide_read_posts) }}
|
||||
<h5> Preferences </h5>
|
||||
{{ render_field(form.interface_language) }}
|
||||
{{ render_field(form.markdown_editor) }}
|
||||
|
|
39
app/templates/user/read_posts.html
Normal file
39
app/templates/user/read_posts.html
Normal file
|
@ -0,0 +1,39 @@
|
|||
{% if theme() and file_exists('app/templates/themes/' + theme() + '/base.html') %}
|
||||
{% extends 'themes/' + theme() + '/base.html' %}
|
||||
{% else %}
|
||||
{% extends "base.html" %}
|
||||
{% endif %}
|
||||
{% set active_child = 'read_posts' %}
|
||||
|
||||
{% block app_content %}
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-8 position-relative main_pane">
|
||||
<nav class="mb-2" 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="/u/{{ user.link() }}">{{ user.display_name() }}</a></li>
|
||||
<li class="breadcrumb-item active">{{ _('Read Posts') }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<h1>{{ _('Read Posts') }}</h1>
|
||||
<div class="post_list">
|
||||
{% for post in posts.items -%}
|
||||
{% include 'post/_post_teaser.html' %}
|
||||
{% endfor -%}
|
||||
</div>
|
||||
|
||||
<nav aria-label="Pagination" class="mt-4" role="navigation">
|
||||
{% if prev_url %}
|
||||
<a href="{{ prev_url }}" class="btn btn-primary" rel="nofollow">
|
||||
<span aria-hidden="true">←</span> {{ _('Previous page') }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if next_url %}
|
||||
<a href="{{ next_url }}" class="btn btn-primary" rel="nofollow">
|
||||
{{ _('Next page') }} <span aria-hidden="true">→</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -65,6 +65,13 @@ def show_topic(topic_path):
|
|||
posts = posts.filter(Post.nsfl == False)
|
||||
if current_user.hide_nsfw == 1:
|
||||
posts = posts.filter(Post.nsfw == False)
|
||||
if current_user.hide_read_posts:
|
||||
cu_rp = current_user.read_post.all()
|
||||
cu_rp_ids = []
|
||||
for p in cu_rp:
|
||||
cu_rp_ids.append(p.id)
|
||||
for p_id in cu_rp_ids:
|
||||
posts = posts.filter(Post.id != p_id)
|
||||
posts = posts.filter(Post.deleted == False)
|
||||
content_filters = user_filters_posts(current_user.id)
|
||||
|
||||
|
|
|
@ -43,6 +43,7 @@ class SettingsForm(FlaskForm):
|
|||
markdown_editor = BooleanField(_l('Use markdown editor GUI when writing'))
|
||||
searchable = BooleanField(_l('Show profile in user list'))
|
||||
indexable = BooleanField(_l('My posts appear in search results'))
|
||||
hide_read_posts = BooleanField(_l('Do not display posts with which I have already interacted (opened/upvoted/downvoted)'))
|
||||
manually_approves_followers = BooleanField(_l('Manually approve followers'))
|
||||
vote_privately = BooleanField(_l('Vote privately'))
|
||||
sorts = [('hot', _l('Hot')),
|
||||
|
|
|
@ -379,6 +379,7 @@ def user_settings():
|
|||
current_user.newsletter = form.newsletter.data
|
||||
current_user.searchable = form.searchable.data
|
||||
current_user.indexable = form.indexable.data
|
||||
current_user.hide_read_posts = form.hide_read_posts.data
|
||||
current_user.default_sort = form.default_sort.data
|
||||
current_user.default_filter = form.default_filter.data
|
||||
current_user.theme = form.theme.data
|
||||
|
@ -405,6 +406,7 @@ def user_settings():
|
|||
form.email_unread.data = current_user.email_unread
|
||||
form.searchable.data = current_user.searchable
|
||||
form.indexable.data = current_user.indexable
|
||||
form.hide_read_posts.data = current_user.hide_read_posts
|
||||
form.default_sort.data = current_user.default_sort
|
||||
form.default_filter.data = current_user.default_filter
|
||||
form.theme.data = current_user.theme
|
||||
|
@ -1170,3 +1172,40 @@ def fediverse_redirect(actor):
|
|||
send_to = request.cookies.get('remote_instance_url')
|
||||
form.instance_url.data = send_to
|
||||
return render_template('user/fediverse_redirect.html', form=form, user=user, send_to=send_to, current_app=current_app)
|
||||
|
||||
@bp.route('/read-posts')
|
||||
@login_required
|
||||
def user_read_posts():
|
||||
page = request.args.get('page', 1, type=int)
|
||||
low_bandwidth = request.cookies.get('low_bandwidth', '0') == '1'
|
||||
|
||||
posts = Post.query.filter(Post.deleted == False)
|
||||
|
||||
if current_user.ignore_bots == 1:
|
||||
posts = posts.filter(Post.from_bot == False)
|
||||
if current_user.hide_nsfl == 1:
|
||||
posts = posts.filter(Post.nsfl == False)
|
||||
if current_user.hide_nsfw == 1:
|
||||
posts = posts.filter(Post.nsfw == False)
|
||||
|
||||
# get the list of post.ids that the
|
||||
# current_user has already read/voted on
|
||||
cu_rp = current_user.read_post.all()
|
||||
cu_rp_ids = []
|
||||
for p in cu_rp:
|
||||
cu_rp_ids.append(p.id)
|
||||
|
||||
# filter for just those post.ids
|
||||
posts = posts.filter(Post.id.in_(cu_rp_ids))
|
||||
|
||||
posts = posts.paginate(page=page, per_page=100 if current_user.is_authenticated and not low_bandwidth else 50,
|
||||
error_out=False)
|
||||
next_url = url_for('user.user_read_posts', page=posts.next_num) if posts.has_next else None
|
||||
prev_url = url_for('user.user_read_posts', page=posts.prev_num) if posts.has_prev and page != 1 else None
|
||||
|
||||
return render_template('user/read_posts.html', title=_('Read Posts'), posts=posts, show_post_community=True,
|
||||
low_bandwidth=low_bandwidth, user=current_user,
|
||||
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)
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
"""adding created_at to read_posts table
|
||||
|
||||
Revision ID: 238d7c60d676
|
||||
Revises: 6e0b98e1fdc6
|
||||
Create Date: 2024-09-28 09:26:22.000646
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '238d7c60d676'
|
||||
down_revision = '6e0b98e1fdc6'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('read_posts', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('interacted_at', sa.DateTime(), nullable=True))
|
||||
batch_op.create_index(batch_op.f('ix_read_posts_interacted_at'), ['interacted_at'], unique=False)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('read_posts', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_read_posts_interacted_at'))
|
||||
batch_op.drop_column('interacted_at')
|
||||
|
||||
# ### end Alembic commands ###
|
41
migrations/versions/27b71f6cb21e_adding_read_posts_table.py
Normal file
41
migrations/versions/27b71f6cb21e_adding_read_posts_table.py
Normal file
|
@ -0,0 +1,41 @@
|
|||
"""adding read_posts table
|
||||
|
||||
Revision ID: 27b71f6cb21e
|
||||
Revises: fdaeb0b2c078
|
||||
Create Date: 2024-09-27 10:17:54.020762
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '27b71f6cb21e'
|
||||
down_revision = 'fdaeb0b2c078'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('read_posts',
|
||||
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||
sa.Column('read_post_id', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['read_post_id'], ['post.id'], ),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['user.id'], )
|
||||
)
|
||||
with op.batch_alter_table('read_posts', schema=None) as batch_op:
|
||||
batch_op.create_index(batch_op.f('ix_read_posts_read_post_id'), ['read_post_id'], unique=False)
|
||||
batch_op.create_index(batch_op.f('ix_read_posts_user_id'), ['user_id'], unique=False)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('read_posts', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_read_posts_user_id'))
|
||||
batch_op.drop_index(batch_op.f('ix_read_posts_read_post_id'))
|
||||
|
||||
op.drop_table('read_posts')
|
||||
# ### end Alembic commands ###
|
|
@ -0,0 +1,32 @@
|
|||
"""adding hide_read_posts bool to User
|
||||
|
||||
Revision ID: 6e0b98e1fdc6
|
||||
Revises: 27b71f6cb21e
|
||||
Create Date: 2024-09-27 10:35:19.451448
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '6e0b98e1fdc6'
|
||||
down_revision = '27b71f6cb21e'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('user', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('hide_read_posts', sa.Boolean(), 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('hide_read_posts')
|
||||
|
||||
# ### end Alembic commands ###
|
Loading…
Add table
Reference in a new issue