refactoring and bug fixes

This commit is contained in:
rimu 2023-12-22 14:05:39 +13:00
parent 26074bd85e
commit 4a6492a15c
13 changed files with 374 additions and 251 deletions

View file

@ -13,7 +13,8 @@ from app.models import User, Community, CommunityJoinRequest, CommunityMember, C
PostReply, Instance, PostVote, PostReplyVote, File, AllowedInstances, BannedInstances, utcnow PostReply, Instance, PostVote, PostReplyVote, File, AllowedInstances, BannedInstances, utcnow
from app.activitypub.util import public_key, users_total, active_half_year, active_month, local_posts, local_comments, \ from app.activitypub.util import public_key, users_total, active_half_year, active_month, local_posts, local_comments, \
post_to_activity, find_actor_or_create, default_context, instance_blocked, find_reply_parent, find_liked_object, \ post_to_activity, find_actor_or_create, default_context, instance_blocked, find_reply_parent, find_liked_object, \
lemmy_site_data, instance_weight, is_activitypub_request lemmy_site_data, instance_weight, is_activitypub_request, downvote_post_reply, downvote_post, upvote_post_reply, \
upvote_post
from app.utils import gibberish, get_setting, is_image_url, allowlist_html, html_to_markdown, render_template, \ from app.utils import gibberish, get_setting, is_image_url, allowlist_html, html_to_markdown, render_template, \
domain_from_url, markdown_to_html, community_membership, ap_datetime domain_from_url, markdown_to_html, community_membership, ap_datetime
import werkzeug.exceptions import werkzeug.exceptions
@ -254,7 +255,7 @@ def community_profile(actor):
@bp.route('/inbox', methods=['GET', 'POST']) @bp.route('/inbox', methods=['GET', 'POST'])
def shared_inbox(): def shared_inbox():
if request.method == 'POST': if request.method == 'POST':
# save all incoming data to aid in debugging and development # save all incoming data to aid in debugging and development. Set result to 'success' if things go well
activity_log = ActivityPubLog(direction='in', activity_json=request.data, result='failure') activity_log = ActivityPubLog(direction='in', activity_json=request.data, result='failure')
try: try:
@ -456,78 +457,86 @@ def shared_inbox():
db.session.commit() db.session.commit()
else: else:
post_id, parent_comment_id, root_id = find_reply_parent(in_reply_to) post_id, parent_comment_id, root_id = find_reply_parent(in_reply_to)
post_reply = PostReply(user_id=user.id, community_id=community.id, if post_id or parent_comment_id or root_id:
post_id=post_id, parent_id=parent_comment_id, post_reply = PostReply(user_id=user.id, community_id=community.id,
root_id=root_id, post_id=post_id, parent_id=parent_comment_id,
nsfw=community.nsfw, root_id=root_id,
nsfl=community.nsfl, nsfw=community.nsfw,
ap_id=request_json['object']['object']['id'], nsfl=community.nsfl,
ap_create_id=request_json['object']['id'], ap_id=request_json['object']['object']['id'],
ap_announce_id=request_json['id']) ap_create_id=request_json['object']['id'],
if 'source' in request_json['object']['object'] and \ ap_announce_id=request_json['id'])
request_json['object']['object']['source'][ if 'source' in request_json['object']['object'] and \
'mediaType'] == 'text/markdown': request_json['object']['object']['source'][
post_reply.body = request_json['object']['object']['source']['content'] 'mediaType'] == 'text/markdown':
post_reply.body_html = markdown_to_html(post_reply.body) post_reply.body = request_json['object']['object']['source']['content']
elif 'content' in request_json['object']['object']: post_reply.body_html = markdown_to_html(post_reply.body)
post_reply.body_html = allowlist_html( elif 'content' in request_json['object']['object']:
request_json['object']['object']['content']) post_reply.body_html = allowlist_html(
post_reply.body = html_to_markdown(post_reply.body_html) request_json['object']['object']['content'])
post_reply.body = html_to_markdown(post_reply.body_html)
if post_reply is not None: if post_reply is not None:
post = Post.query.get(post_id) post = Post.query.get(post_id)
if post.comments_enabled: if post.comments_enabled:
db.session.add(post_reply) db.session.add(post_reply)
community.post_reply_count += 1 community.post_reply_count += 1
community.last_active = utcnow() community.last_active = utcnow()
post.last_active = utcnow() post.last_active = utcnow()
activity_log.result = 'success' activity_log.result = 'success'
db.session.commit() db.session.commit()
else: else:
activity_log.exception_message = 'Comments disabled' activity_log.exception_message = 'Comments disabled'
else:
activity_log.exception_message = 'Parent not found'
else: else:
activity_log.exception_message = 'Unacceptable type: ' + object_type activity_log.exception_message = 'Unacceptable type: ' + object_type
elif request_json['object']['type'] == 'Like' or request_json['object']['type'] == 'Dislike': elif request_json['object']['type'] == 'Like':
activity_log.activity_type = request_json['object']['type'] activity_log.activity_type = request_json['object']['type']
vote_effect = 1.0 if request_json['object']['type'] == 'Like' else -1.0 user_ap_id = request_json['object']['actor']
if vote_effect < 0 and get_setting('allow_dislike', True) is False: liked_ap_id = request_json['object']['object']
user = find_actor_or_create(user_ap_id)
if user:
liked = find_liked_object(liked_ap_id)
# insert into voted table
if liked is None:
activity_log.exception_message = 'Liked object not found'
elif liked is not None and isinstance(liked, Post):
upvote_post(liked, user)
activity_log.result = 'success'
elif liked is not None and isinstance(liked, PostReply):
upvote_post_reply(liked, user)
activity_log.result = 'success'
else:
activity_log.exception_message = 'Could not detect type of like'
if activity_log.result == 'success':
... # todo: recalculate 'hotness' of liked post/reply
# todo: if vote was on content in local community, federate the vote out to followers
elif request_json['object']['type'] == 'Dislike':
activity_log.activity_type = request_json['object']['type']
if g.site.enable_downvotes is False:
activity_log.exception_message = 'Dislike ignored because of allow_dislike setting' activity_log.exception_message = 'Dislike ignored because of allow_dislike setting'
else: else:
user_ap_id = request_json['object']['actor'] user_ap_id = request_json['object']['actor']
liked_ap_id = request_json['object']['object'] liked_ap_id = request_json['object']['object']
user = find_actor_or_create(user_ap_id) user = find_actor_or_create(user_ap_id)
if user: if user:
vote_weight = instance_weight(user.ap_domain) disliked = find_liked_object(liked_ap_id)
liked = find_liked_object(liked_ap_id)
# insert into voted table # insert into voted table
if liked is None: if disliked is None:
activity_log.exception_message = 'Liked object not found' activity_log.exception_message = 'Liked object not found'
elif liked is not None and isinstance(liked, Post): elif disliked is not None and isinstance(disliked, Post):
existing_vote = PostVote.query.filter_by(user_id=user.id, post_id=liked.id).first() downvote_post(disliked, user)
if existing_vote:
existing_vote.effect = vote_effect * vote_weight
else:
vote = PostVote(user_id=user.id, author_id=liked.user_id, post_id=liked.id,
effect=vote_effect * vote_weight)
db.session.add(vote)
db.session.commit()
activity_log.result = 'success' activity_log.result = 'success'
elif liked is not None and isinstance(liked, PostReply): elif disliked is not None and isinstance(disliked, PostReply):
existing_vote = PostVote.query.filter_by(user_id=user.id, post_id=liked.id).first() downvote_post_reply(disliked, user)
if existing_vote:
existing_vote.effect = vote_effect * vote_weight
else:
vote = PostReplyVote(user_id=user.id, author_id=liked.user_id, post_reply_id=liked.id,
effect=vote_effect * vote_weight)
db.session.add(vote)
db.session.commit()
activity_log.result = 'success' activity_log.result = 'success'
else: else:
activity_log.exception_message = 'Could not detect type of like' activity_log.exception_message = 'Could not detect type of like'
if activity_log.result == 'success': if activity_log.result == 'success':
... # todo: recalculate 'hotness' of liked post/reply ... # todo: recalculate 'hotness' of liked post/reply
# todo: if vote was on content in local community, federate the vote out to followers # todo: if vote was on content in local community, federate the vote out to followers
# Follow: remote user wants to join/follow one of our communities # Follow: remote user wants to join/follow one of our communities
elif request_json['type'] == 'Follow': # Follow is when someone wants to join a community elif request_json['type'] == 'Follow': # Follow is when someone wants to join a community
@ -726,65 +735,15 @@ def shared_inbox():
target_ap_id = request_json['object'] target_ap_id = request_json['object']
post = None post = None
comment = None comment = None
effect = instance_weight(user.ap_domain)
if '/comment/' in target_ap_id: if '/comment/' in target_ap_id:
comment = PostReply.query.filter_by(ap_id=target_ap_id).first() comment = PostReply.query.filter_by(ap_id=target_ap_id).first()
if '/post/' in target_ap_id: if '/post/' in target_ap_id:
post = Post.query.filter_by(ap_id=target_ap_id).first() post = Post.query.filter_by(ap_id=target_ap_id).first()
if user and post: if user and post:
user.last_seen = utcnow() upvote_post(post, user)
existing_vote = PostVote.query.filter_by(user_id=user.id, post_id=post.id).first()
if not existing_vote:
post.up_votes += 1
post.score += effect
vote = PostVote(user_id=user.id, post_id=post.id, author_id=post.author.id,
effect=effect)
post.author.reputation += effect
db.session.add(vote)
else:
# remove previous cast downvote
if existing_vote.effect < 0:
post.author.reputation -= existing_vote.effect
post.down_votes -= 1
post.score -= existing_vote.effect
db.session.delete(existing_vote)
# apply up vote
post.up_votes += 1
post.score += effect
vote = PostVote(user_id=user.id, post_id=post.id, author_id=post.author.id,
effect=effect)
post.author.reputation += effect
db.session.add(vote)
activity_log.result = 'success' activity_log.result = 'success'
elif user and comment: elif user and comment:
user.last_seen = utcnow() upvote_post_reply(comment, user)
existing_vote = PostReplyVote.query.filter_by(user_id=user.id,
post_reply_id=comment.id).first()
if not existing_vote:
comment.up_votes += 1
comment.score += effect
vote = PostReplyVote(user_id=user.id, post_reply_id=comment.id,
author_id=comment.author.id, effect=effect)
comment.author.reputation += effect
db.session.add(vote)
else:
# remove previously cast downvote
if existing_vote.effect < 0:
comment.author.reputation -= existing_vote.effect
comment.down_votes -= 1
comment.score -= existing_vote.effect
db.session.delete(existing_vote)
# apply up vote
comment.up_votes += 1
comment.score += effect
vote = PostReplyVote(user_id=user.id, post_reply_id=comment.id,
author_id=comment.author.id, effect=effect)
comment.author.reputation += effect
db.session.add(vote)
else:
pass # they have already upvoted this reply
activity_log.result = 'success' activity_log.result = 'success'
elif request_json['type'] == 'Dislike': # Downvote elif request_json['type'] == 'Dislike': # Downvote
@ -802,65 +761,10 @@ def shared_inbox():
if '/post/' in target_ap_id: if '/post/' in target_ap_id:
post = Post.query.filter_by(ap_id=target_ap_id).first() post = Post.query.filter_by(ap_id=target_ap_id).first()
if user and comment: if user and comment:
user.last_seen = utcnow() downvote_post_reply(comment, user)
existing_vote = PostReplyVote.query.filter_by(user_id=user.id,
post_reply_id=comment.id).first()
if not existing_vote:
effect = -1.0
comment.down_votes += 1
comment.score -= 1.0
vote = PostReplyVote(user_id=user.id, post_reply_id=comment.id,
author_id=comment.author.id, effect=effect)
comment.author.reputation += effect
db.session.add(vote)
else:
# remove previously cast upvote
if existing_vote.effect > 0:
comment.author.reputation -= existing_vote.effect
comment.up_votes -= 1
comment.score -= existing_vote.effect
db.session.delete(existing_vote)
# apply down vote
effect = -1.0
comment.down_votes += 1
comment.score -= 1.0
vote = PostReplyVote(user_id=user.id, post_reply_id=comment.id,
author_id=comment.author.id, effect=effect)
comment.author.reputation += effect
db.session.add(vote)
else:
pass # they have already downvoted this reply
activity_log.result = 'success' activity_log.result = 'success'
elif user and post: elif user and post:
user.last_seen = utcnow() downvote_post(post, user)
existing_vote = PostVote.query.filter_by(user_id=user.id, post_id=post.id).first()
if not existing_vote:
effect = -1.0
post.down_votes += 1
post.score -= 1.0
vote = PostVote(user_id=user.id, post_id=post.id, author_id=post.author.id,
effect=effect)
post.author.reputation += effect
db.session.add(vote)
else:
# remove previously cast upvote
if existing_vote.effect > 0:
post.author.reputation -= existing_vote.effect
post.up_votes -= 1
post.score -= existing_vote.effect
db.session.delete(existing_vote)
# apply down vote
effect = -1.0
post.down_votes += 1
post.score -= 1.0
vote = PostVote(user_id=user.id, post_id=post.id, author_id=post.author.id,
effect=effect)
post.author.reputation += effect
db.session.add(vote)
else:
pass # they have already downvoted this post
activity_log.result = 'success' activity_log.result = 'success'
else: else:
activity_log.exception_message = 'Could not find user or content for vote' activity_log.exception_message = 'Could not find user or content for vote'
@ -885,6 +789,8 @@ def shared_inbox():
return '' return ''
@bp.route('/c/<actor>/outbox', methods=['GET']) @bp.route('/c/<actor>/outbox', methods=['GET'])
def community_outbox(actor): def community_outbox(actor):
actor = actor.strip() actor = actor.strip()
@ -1003,3 +909,12 @@ def post_ap(post_id):
return resp return resp
else: else:
return show_post(post_id) return show_post(post_id)
@bp.route('/activities/<type>/<id>')
def activities_json(type, id):
activity = ActivityPubLog.query.filter_by(activity_id=f"https://{current_app.config['SERVER_NAME']}/activities/{type}/{id}").first()
if activity:
...
else:
abort(404)

View file

@ -5,7 +5,8 @@ from typing import Union, Tuple
from flask import current_app, request from flask import current_app, request
from sqlalchemy import text from sqlalchemy import text
from app import db, cache, constants from app import db, cache, constants
from app.models import User, Post, Community, BannedInstances, File, PostReply, AllowedInstances, Instance, utcnow, Site from app.models import User, Post, Community, BannedInstances, File, PostReply, AllowedInstances, Instance, utcnow, \
Site, PostVote, PostReplyVote
import time import time
import base64 import base64
import requests import requests
@ -439,12 +440,16 @@ def default_context():
def find_reply_parent(in_reply_to: str) -> Tuple[int, int, int]: def find_reply_parent(in_reply_to: str) -> Tuple[int, int, int]:
if 'comment' in in_reply_to: if 'comment' in in_reply_to:
parent_comment = PostReply.get_by_ap_id(in_reply_to) parent_comment = PostReply.get_by_ap_id(in_reply_to)
if not parent_comment:
return (None, None, None)
parent_comment_id = parent_comment.id parent_comment_id = parent_comment.id
post_id = parent_comment.post_id post_id = parent_comment.post_id
root_id = parent_comment.root_id root_id = parent_comment.root_id
elif 'post' in in_reply_to: elif 'post' in in_reply_to:
parent_comment_id = None parent_comment_id = None
post = Post.get_by_ap_id(in_reply_to) post = Post.get_by_ap_id(in_reply_to)
if not post:
return (None, None, None)
post_id = post.id post_id = post.id
root_id = None root_id = None
else: else:
@ -460,6 +465,8 @@ def find_reply_parent(in_reply_to: str) -> Tuple[int, int, int]:
parent_comment_id = parent_comment.id parent_comment_id = parent_comment.id
post_id = parent_comment.post_id post_id = parent_comment.post_id
root_id = parent_comment.root_id root_id = parent_comment.root_id
else:
return (None, None, None)
return post_id, parent_comment_id, root_id return post_id, parent_comment_id, root_id
@ -523,6 +530,128 @@ def is_activitypub_request():
return 'application/ld+json' in request.headers.get('Accept', '') or 'application/activity+json' in request.headers.get('Accept', '') return 'application/ld+json' in request.headers.get('Accept', '') or 'application/activity+json' in request.headers.get('Accept', '')
def downvote_post(post, user):
user.last_seen = utcnow()
existing_vote = PostVote.query.filter_by(user_id=user.id, post_id=post.id).first()
if not existing_vote:
effect = -1.0
post.down_votes += 1
post.score -= 1.0
vote = PostVote(user_id=user.id, post_id=post.id, author_id=post.author.id,
effect=effect)
post.author.reputation += effect
db.session.add(vote)
else:
# remove previously cast upvote
if existing_vote.effect > 0:
post.author.reputation -= existing_vote.effect
post.up_votes -= 1
post.score -= existing_vote.effect
db.session.delete(existing_vote)
# apply down vote
effect = -1.0
post.down_votes += 1
post.score -= 1.0
vote = PostVote(user_id=user.id, post_id=post.id, author_id=post.author.id,
effect=effect)
post.author.reputation += effect
db.session.add(vote)
else:
pass # they have already downvoted this post
def downvote_post_reply(comment, user):
user.last_seen = utcnow()
existing_vote = PostReplyVote.query.filter_by(user_id=user.id,
post_reply_id=comment.id).first()
if not existing_vote:
effect = -1.0
comment.down_votes += 1
comment.score -= 1.0
vote = PostReplyVote(user_id=user.id, post_reply_id=comment.id,
author_id=comment.author.id, effect=effect)
comment.author.reputation += effect
db.session.add(vote)
else:
# remove previously cast upvote
if existing_vote.effect > 0:
comment.author.reputation -= existing_vote.effect
comment.up_votes -= 1
comment.score -= existing_vote.effect
db.session.delete(existing_vote)
# apply down vote
effect = -1.0
comment.down_votes += 1
comment.score -= 1.0
vote = PostReplyVote(user_id=user.id, post_reply_id=comment.id,
author_id=comment.author.id, effect=effect)
comment.author.reputation += effect
db.session.add(vote)
else:
pass # they have already downvoted this reply
def upvote_post_reply(comment, user):
user.last_seen = utcnow()
effect = instance_weight(user.ap_domain)
existing_vote = PostReplyVote.query.filter_by(user_id=user.id,
post_reply_id=comment.id).first()
if not existing_vote:
comment.up_votes += 1
comment.score += effect
vote = PostReplyVote(user_id=user.id, post_reply_id=comment.id,
author_id=comment.author.id, effect=effect)
comment.author.reputation += effect
db.session.add(vote)
else:
# remove previously cast downvote
if existing_vote.effect < 0:
comment.author.reputation -= existing_vote.effect
comment.down_votes -= 1
comment.score -= existing_vote.effect
db.session.delete(existing_vote)
# apply up vote
comment.up_votes += 1
comment.score += effect
vote = PostReplyVote(user_id=user.id, post_reply_id=comment.id,
author_id=comment.author.id, effect=effect)
comment.author.reputation += effect
db.session.add(vote)
else:
pass # they have already upvoted this reply
def upvote_post(post, user):
user.last_seen = utcnow()
effect = instance_weight(user.ap_domain)
existing_vote = PostVote.query.filter_by(user_id=user.id, post_id=post.id).first()
if not existing_vote:
post.up_votes += 1
post.score += effect
vote = PostVote(user_id=user.id, post_id=post.id, author_id=post.author.id,
effect=effect)
post.author.reputation += effect
db.session.add(vote)
else:
# remove previous cast downvote
if existing_vote.effect < 0:
post.author.reputation -= existing_vote.effect
post.down_votes -= 1
post.score -= existing_vote.effect
db.session.delete(existing_vote)
# apply up vote
post.up_votes += 1
post.score += effect
vote = PostVote(user_id=user.id, post_id=post.id, author_id=post.author.id,
effect=effect)
post.author.reputation += effect
db.session.add(vote)
def lemmy_site_data(): def lemmy_site_data():
site = Site.query.get(1) site = Site.query.get(1)
data = { data = {

View file

@ -100,6 +100,7 @@ def show_community(community: Community):
is_moderator = current_user.is_authenticated and any(mod.user_id == current_user.id for mod in mods) is_moderator = current_user.is_authenticated and any(mod.user_id == current_user.id for mod in mods)
is_owner = current_user.is_authenticated and any( is_owner = current_user.is_authenticated and any(
mod.user_id == current_user.id and mod.is_owner == True for mod in mods) mod.user_id == current_user.id and mod.is_owner == True for mod in mods)
is_admin = current_user.is_authenticated and current_user.is_admin()
if community.private_mods: if community.private_mods:
mod_list = [] mod_list = []
@ -121,7 +122,7 @@ def show_community(community: Community):
page=posts.prev_num) if posts.has_prev and page != 1 else None page=posts.prev_num) if posts.has_prev and page != 1 else None
return render_template('community/community.html', community=community, title=community.title, return render_template('community/community.html', community=community, title=community.title,
is_moderator=is_moderator, is_owner=is_owner, mods=mod_list, posts=posts, description=description, 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, og_image=og_image, POST_TYPE_IMAGE=POST_TYPE_IMAGE, POST_TYPE_LINK=POST_TYPE_LINK, SUBSCRIPTION_PENDING=SUBSCRIPTION_PENDING,
SUBSCRIPTION_MEMBER=SUBSCRIPTION_MEMBER, etag=f"{community.id}_{hash(community.last_active)}", SUBSCRIPTION_MEMBER=SUBSCRIPTION_MEMBER, etag=f"{community.id}_{hash(community.last_active)}",
next_url=next_url, prev_url=prev_url, next_url=next_url, prev_url=prev_url,

View file

@ -101,7 +101,9 @@ def retrieve_mods_and_backfill_thread(community: Community, app):
activities_processed += 1 activities_processed += 1
if activities_processed >= 50: if activities_processed >= 50:
break break
community.post_count = activities_processed # todo: figure out why this value is not being saved c = Community.query.get(community.id)
c.post_count = activities_processed
c.last_active = utcnow()
db.session.commit() db.session.commit()

View file

@ -388,27 +388,36 @@ fieldset legend {
background-position: center center; background-position: center center;
background-size: cover; background-size: cover;
border-radius: 5px; border-radius: 5px;
height: 176px;
}
@media (min-width: 992px) {
.community_header {
height: 240px;
}
} }
@media (min-width: 992px) { @media (min-width: 992px) {
.community_header #breadcrumb_nav { .community_header #breadcrumb_nav {
padding-left: 20px; padding-left: 20px;
padding-top: 13px; padding-top: 13px;
} }
.community_header #breadcrumb_nav .breadcrumb { }
padding: 0; .community_header #breadcrumb_nav .breadcrumb {
margin-bottom: 0; background-color: rgba(0, 0, 0, 0.2);
background-color: inherit; display: inline-block;
} padding: 5px 10px;
.community_header #breadcrumb_nav .breadcrumb .breadcrumb-item { border-radius: 6px;
color: white; margin-bottom: 0;
} }
.community_header #breadcrumb_nav .breadcrumb .breadcrumb-item a { .community_header #breadcrumb_nav .breadcrumb .breadcrumb-item {
color: white; color: white;
} display: inline-block;
.community_header #breadcrumb_nav .breadcrumb .breadcrumb-item + .breadcrumb-item::before { }
content: ">"; .community_header #breadcrumb_nav .breadcrumb .breadcrumb-item a {
color: white; color: white;
} }
.community_header #breadcrumb_nav .breadcrumb .breadcrumb-item + .breadcrumb-item::before {
content: ">";
color: white;
} }
.community_header_no_background .community_icon, .community_header .community_icon { .community_header_no_background .community_icon, .community_header .community_icon {
@ -451,14 +460,22 @@ fieldset legend {
text-decoration: none; text-decoration: none;
} }
.post_list .post_teaser .thumbnail { .post_list .post_teaser .thumbnail {
float: right;
padding-left: 0; padding-left: 0;
padding-right: 0; padding-right: 0;
} }
.post_list .post_teaser .thumbnail img { .post_list .post_teaser .thumbnail img {
position: absolute; height: 60px;
right: 70px; width: 60px;
height: 70px; border-radius: 5px;
margin-top: -47px; object-fit: cover;
margin-left: 5px;
}
@media (min-width: 992px) {
.post_list .post_teaser .thumbnail img {
height: 70px;
width: 133px;
}
} }
.url_thumbnail { .url_thumbnail {
@ -498,8 +515,9 @@ fieldset legend {
float: right; float: right;
display: block; display: block;
width: 55px; width: 55px;
padding: 5px; padding: 0 0 5px 5px;
padding-right: 0; line-height: 22px;
font-size: 14px;
} }
.voting_buttons div { .voting_buttons div {
border: solid 1px #0071CE; border: solid 1px #0071CE;

View file

@ -77,28 +77,35 @@ nav, etc which are used site-wide */
background-position: center center; background-position: center center;
background-size: cover; background-size: cover;
border-radius: 5px; border-radius: 5px;
height: 176px;
@include breakpoint(tablet) { @include breakpoint(tablet) {
#breadcrumb_nav { height: 240px;
}
#breadcrumb_nav {
@include breakpoint(tablet) {
padding-left: 20px; padding-left: 20px;
padding-top: 13px; padding-top: 13px;
}
.breadcrumb {
background-color: rgba(0,0,0,0.2);
display: inline-block;
padding: 5px 10px;
border-radius: 6px;
margin-bottom: 0;
.breadcrumb { .breadcrumb-item {
padding: 0; color: white;
margin-bottom: 0; display: inline-block;
background-color: inherit; a {
.breadcrumb-item {
color: white; color: white;
a {
color: white;
}
} }
}
.breadcrumb-item + .breadcrumb-item::before { .breadcrumb-item + .breadcrumb-item::before {
content: ">"; content: ">";
color: white; color: white;
}
} }
} }
} }
@ -155,13 +162,21 @@ nav, etc which are used site-wide */
} }
.thumbnail { .thumbnail {
float: right;
padding-left: 0; padding-left: 0;
padding-right: 0; padding-right: 0;
img { img {
position: absolute; height: 60px;
right: 70px; width: 60px;
height: 70px; border-radius: 5px;
margin-top: -47px; object-fit: cover;
margin-left: 5px;
@include breakpoint(tablet) {
height: 70px;
width: 133px;
}
} }
} }
@ -215,8 +230,9 @@ nav, etc which are used site-wide */
float: right; float: right;
display: block; display: block;
width: 55px; width: 55px;
padding: 5px; padding: 0 0 5px 5px;
padding-right: 0; line-height: 22px;
font-size: 14px;
div { div {
border: solid 1px $primary-colour; border: solid 1px $primary-colour;

View file

@ -355,6 +355,16 @@ nav.navbar {
background-color: #0071CE; background-color: #0071CE;
} }
#outer_container {
margin-top: -4px;
}
@media (min-width: 992px) {
#outer_container {
margin-top: 1rem;
padding-top: 0.25rem;
}
}
.card-title { .card-title {
font-size: 140%; font-size: 140%;
} }
@ -387,20 +397,36 @@ nav.navbar {
padding-top: 8px; padding-top: 8px;
padding-bottom: 12px; padding-bottom: 12px;
} }
.main_pane .url_thumbnail {
width: 120px;
height: auto;
}
.main_pane .url_thumbnail img {
width: 100%;
}
.community_icon { .community_icon {
width: 30px; width: 20vw;
height: auto; height: 20vw;
max-width: 30px;
max-height: 30px;
min-width: 20px;
min-height: 20px;
} }
.community_icon_big { .community_icon_big {
width: 120px; width: 20vw;
height: auto; height: 20vw;
max-width: 120px;
max-height: 120px;
min-width: 80px;
min-height: 80px;
object-fit: cover;
} }
.bump_up { .bump_up {
position: absolute; position: absolute;
top: 104px; top: 115px;
left: 26px; left: 26px;
} }

View file

@ -54,6 +54,14 @@ nav.navbar {
} }
} }
#outer_container {
margin-top: -4px;
@include breakpoint(tablet) {
margin-top: 1rem;
padding-top: 0.25rem;
}
}
.card-title { .card-title {
font-size: 140%; font-size: 140%;
} }
@ -85,21 +93,39 @@ nav.navbar {
background-color: white; background-color: white;
padding-top: 8px; padding-top: 8px;
padding-bottom: 12px; padding-bottom: 12px;
.url_thumbnail {
width: 120px;
height: auto;
img {
width: 100%;
}
}
} }
.community_icon { .community_icon {
width: 30px; width: 20vw;
height: auto; height: 20vw;
max-width: 30px;
max-height: 30px;
min-width: 20px;
min-height: 20px;
} }
.community_icon_big { .community_icon_big {
width: 120px; width: 20vw;
height: auto; height: 20vw;
max-width: 120px;
max-height: 120px;
min-width: 80px;
min-height: 80px;
object-fit: cover;
} }
.bump_up { .bump_up {
position: absolute; position: absolute;
top: 104px; top: 115px;
left: 26px; left: 26px;
} }

View file

@ -90,7 +90,7 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div id="outer_container" class="container-lg flex-shrink-0 mt-3 pt-1"> <div id="outer_container" class="container-lg flex-shrink-0">
{% with messages = get_flashed_messages(with_categories=True) %} {% with messages = get_flashed_messages(with_categories=True) %}
{% if messages %} {% if messages %}
{% for category, message in messages %} {% for category, message in messages %}

View file

@ -5,7 +5,7 @@
<div class="row"> <div class="row">
<div class="col-12 col-md-8 position-relative main_pane"> <div class="col-12 col-md-8 position-relative main_pane">
{% if community.header_image() != '' %} {% if community.header_image() != '' %}
<div class="community_header" style="height: 240px; background-image: url({{ community.header_image() }});"> <div class="community_header" style="background-image: url({{ community.header_image() }});">
<nav aria-label="breadcrumb" id="breadcrumb_nav" title="Navigation"> <nav aria-label="breadcrumb" id="breadcrumb_nav" title="Navigation">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/">{{ _('Home') }}</a></li> <li class="breadcrumb-item"><a href="/">{{ _('Home') }}</a></li>
@ -88,18 +88,18 @@
<p>{{ community.rules|safe }}</p> <p>{{ community.rules|safe }}</p>
{% if len(mods) > 0 and not community.private_mods %} {% if len(mods) > 0 and not community.private_mods %}
<h3>Moderators</h3> <h3>Moderators</h3>
<ol> <ul>
{% for mod in mods %} {% for mod in mods %}
<li>{{ render_username(mod) }}</li> <li>{{ render_username(mod) }}</li>
{% endfor %} {% endfor %}
</ol> </ul>
{% endif %} {% endif %}
<p class="mt-4"> <p class="mt-4">
<a class="no-underline" href="{{ rss_feed }}" rel="nofollow"><span class="fe fe-rss"></span> RSS feed</a> <a class="no-underline" href="{{ rss_feed }}" rel="nofollow"><span class="fe fe-rss"></span> RSS feed</a>
</p> </p>
</div> </div>
</div> </div>
{% if is_moderator or current_user.is_admin() %} {% if is_moderator or is_admin %}
<div class="card mt-3"> <div class="card mt-3">
<div class="card-header"> <div class="card-header">
<h2>{{ _('Community Settings') }}</h2> <h2>{{ _('Community Settings') }}</h2>

View file

@ -2,7 +2,7 @@
{% from 'bootstrap5/form.html' import render_form %} {% from 'bootstrap5/form.html' import render_form %}
{% block app_content %} {% block app_content %}
<div class="row g-2 justify-content-between"> <div class="row g-2 justify-content-between mt-2">
<div class="col-auto"> <div class="col-auto">
<div class="btn-group"> <div class="btn-group">
<a href="/communities" class="btn {{ 'btn-primary' if request.path == '/communities' else 'btn-outline-secondary' }}"> <a href="/communities" class="btn {{ 'btn-primary' if request.path == '/communities' else 'btn-outline-secondary' }}">

View file

@ -39,13 +39,13 @@
<div class="voting_buttons"> <div class="voting_buttons">
{% include "post/_post_voting_buttons.html" %} {% include "post/_post_voting_buttons.html" %}
</div> </div>
<h1 class="mt-2">{{ post.title }}</h1>
{% if post.type == POST_TYPE_LINK and post.image_id and not (post.url and 'youtube.com' in post.url) %} {% if post.type == POST_TYPE_LINK and post.image_id and not (post.url and 'youtube.com' in post.url) %}
<div class="url_thumbnail"> <div class="url_thumbnail">
<img src="{{ post.image.thumbnail_url() }}" alt="{{ post.image.alt_text }}" <a href="{{ post.url }}"><img src="{{ post.image.thumbnail_url() }}" alt="{{ post.image.alt_text }}"
width="{{ post.image.thumbnail_width }}" height="{{ post.image.thumbnail_height }}" /> width="{{ post.image.thumbnail_width }}" height="{{ post.image.thumbnail_height }}" /></a>
</div> </div>
{% endif %} {% endif %}
<h1 class="mt-2">{{ post.title }}</h1>
<p class="small">submitted {{ moment(post.posted_at).fromNow() }} by <p class="small">submitted {{ moment(post.posted_at).fromNow() }} by
{{ render_username(post.author) }} {{ render_username(post.author) }}
{% if post.edited_at %} edited {{ moment(post.edited_at).fromNow() }}{% endif %} {% if post.edited_at %} edited {{ moment(post.edited_at).fromNow() }}{% endif %}
@ -68,17 +68,8 @@
width="{{ post.image.thumbnail_width }}" height="{{ post.image.thumbnail_height }}" /></a> width="{{ post.image.thumbnail_width }}" height="{{ post.image.thumbnail_height }}" /></a>
{% endif %} {% endif %}
{% endif %} {% endif %}
{{ post.body_html|safe if post.body_html else '' }}
</div> </div>
{% endif %} {% endif %}
<a href="{{ url_for('post.post_options', post_id=post.id) }}" class="post_options_link" rel="nofollow"><span class="fe fe-options" title="Options"> </span></a> <a href="{{ url_for('post.post_options', post_id=post.id) }}" class="post_options_link" rel="nofollow"><span class="fe fe-options" title="Options"> </span></a>
</div> </div>
{% if post.body_html %}
<div class="row post_full">
<div class="col">
{{ post.body_html|safe }}
</div>
</div>
{% endif %}

View file

@ -1,9 +1,19 @@
<div class="post_teaser"> <div class="post_teaser">
<div class="row"> <div class="row">
<div class="col col-md-10"> <div class="col-12">
<div class="row main_row"> <div class="row main_row">
<div class="col"> <div class="col">
<h3> <h3>
<div class="voting_buttons">
{% include "post/_post_voting_buttons.html" %}
</div>
{% if post.image_id %}
<div class="thumbnail">
<a href="{{ url_for('activitypub.post_ap', post_id=post.id) }}"><img src="{{ post.image.thumbnail_url() }}"
alt="{{ post.image.alt_text if post.image.alt_text else '' }}"
height="50" /></a>
</div>
{% endif %}
<a href="{{ url_for('activitypub.post_ap', post_id=post.id) }}">{{ post.title }}</a> <a href="{{ url_for('activitypub.post_ap', post_id=post.id) }}">{{ post.title }}</a>
{% if post.type == POST_TYPE_IMAGE %}<span class="fe fe-image"> </span>{% endif %} {% if post.type == POST_TYPE_IMAGE %}<span class="fe fe-image"> </span>{% endif %}
{% if post.type == POST_TYPE_LINK and post.domain_id %} {% if post.type == POST_TYPE_LINK and post.domain_id %}
@ -17,13 +27,7 @@
{% endif %} {% endif %}
</h3> </h3>
<span class="small">{{ render_username(post.author) }} · {{ moment(post.posted_at).fromNow() }}</span> <span class="small">{{ render_username(post.author) }} · {{ moment(post.posted_at).fromNow() }}</span>
{% if post.image_id %}
<div class="thumbnail">
<a href="{{ url_for('activitypub.post_ap', post_id=post.id) }}"><img src="{{ post.image.thumbnail_url() }}"
alt="{{ post.image.alt_text if post.image.alt_text else '' }}"
height="50" /></a>
</div>
{% endif %}
</div> </div>
</div> </div>
@ -35,11 +39,6 @@
<div class="col-2"><a href="{{ url_for('post.post_options', post_id=post.id) }}" rel="nofollow"><span class="fe fe-options" title="Options"> </span></a></div> <div class="col-2"><a href="{{ url_for('post.post_options', post_id=post.id) }}" rel="nofollow"><span class="fe fe-options" title="Options"> </span></a></div>
</div> </div>
</div> </div>
<div class="col col-md-2">
<div class="voting_buttons pt-2">
{% include "post/_post_voting_buttons.html" %}
</div>
</div>
</div> </div>