Comment pages: partial thread view, give thanks

This commit is contained in:
Jaakko Keränen 2023-06-20 08:49:35 +03:00
parent 374faca7be
commit 2f61940adb
No known key found for this signature in database
GPG key ID: BACCFCFB98DB2EDC
4 changed files with 179 additions and 127 deletions

View file

@ -22,3 +22,4 @@ ALTER TABLE posts ADD INDEX (issueid);
-- Migration from v4 to v5 --
ALTER TABLE users ADD COLUMN timezone VARCHAR(40) DEFAULT 'UTC';
ALTER TABLE users ADD COLUMN recovery VARCHAR(1000) DEFAULT '';
ALTER TABLE notifs ADD COLUMN comment INT;

250
feeds.py
View file

@ -184,8 +184,11 @@ def make_post_page_or_configure_feed(session):
post = db.get_post(id=int(arg), draft=False)
if not post:
return 51, 'Not found'
if post.parent != 0:
return make_post_page(session, post)
if post.subspace != session.context.id:
#return 51, 'Not found'
# Redirect to the correct subspace.
post_sub = db.get_subspace(id=post.subspace)
if post_sub.flags & Subspace.ISSUE_TRACKER:
@ -193,11 +196,6 @@ def make_post_page_or_configure_feed(session):
else:
return 31, f'{session.path}{post_sub.title()}/{post.id}'
if post.parent != 0:
# Comments cannot be viewed individually.
# TODO: Make this possible.
return 30, db.get_post(id=post.parent).page_url()
if arg2 == '/antenna':
# Special viewing mode for Antenna submissions, with the bare minimum.
page = f'# {session.feed_title()}\n'
@ -211,9 +209,7 @@ def make_post_page_or_configure_feed(session):
db.get_notifications(session.user, post_id=post.id, clear=True)
return 30, post.page_url()
page = make_post_page(session, post)
return page
return make_post_page(session, post)
return None
@ -221,8 +217,50 @@ def make_post_page_or_configure_feed(session):
def make_post_page(session, post):
db = session.db
user = session.user
is_comment_page = post.parent != 0
display_order_desc = session.user and \
session.user.sort_cmt == User.SORT_COMMENT_NEWEST
page = ''
page = session.render_post(post)
if is_comment_page:
# Switch to the parent post, but display it in preview mode.
focused_cmt = post
last_age = focused_cmt.age()
post = db.get_post(id=post.parent)
page += f'=> {post.page_url()} Comment on: "{post.title if post.title else shorten_text(strip_links(clean_title(post.summary)), 60)}" in {("u/" if post.sub_owner else "s/") + post.sub_name}\n'
page += f'=> /u/{focused_cmt.poster_name} {focused_cmt.poster_avatar} {focused_cmt.poster_name}\n'
page += f'{last_age}\n\n'
page += session.render_post(focused_cmt)
# Comment actions.
actions = []
if session.is_editable(focused_cmt):
actions.append(f'=> /edit/{focused_cmt.id} ✏️ Edit comment\n')
if session.user and not session.is_context_locked and display_order_desc:
actions.append(f'=> /comment/{post.id} 💬 Comment\n')
if session.is_thanks_enabled() and focused_cmt.user != user.id:
actions.append(f'=> /thanks/{focused_cmt.id} 🙏 Give thanks\n')
if session.is_deletable(focused_cmt) and not session.is_editable(focused_cmt):
actions.append(f'=> /edit/{focused_cmt.id}/delete/{session.get_token()} ❌ Delete comment\n')
actions.append(session.dashboard_link())
if actions:
page += '\n' + ''.join(actions)
op_section = '\n## Original Post\n\n' + session.feed_entry(post)
if not display_order_desc:
page += op_section
else:
page += session.render_post(post)
# Poll options/results.
poll = session.render_poll(post, show_results=not session.user)
if poll:
# Ensure separation.
if len(page) and not page.endswith('\n\n'):
page += '\n'
page += poll
commits = []
incoming_xrefs = []
@ -250,130 +288,125 @@ def make_post_page(session, post):
else:
repo = None
# Poll.
poll = session.render_poll(post, show_results=not session.user)
if poll:
# Ensure separation.
if len(page) and not page.endswith('\n\n'):
if not is_comment_page:
# Post metadata.
if len(page):
page += '\n'
page += poll
# Metadata.
if len(page):
if post.tags:
page += '### ' + post.tags + '\n'
poster_link = f'=> /u/{post.poster_name} {post.poster_avatar} {post.poster_name}\n'
if session.is_context_tracker:
page += f'=> /{session.context.title()} 🐞 Issue #{post.issueid} in {session.context.title()}\n'
elif not session.c_user:
page += f'=> /{session.context.title()} Posted in: {session.context.title()}\n'
page += poster_link
last_age = post.age()
page += f'{last_age}'
if session.is_likes_enabled():
liked = []
if post.num_likes:
liked = db.get_likes(post)
page += ' · 👍 ' + ', '.join(liked)
if session.is_reactions_enabled():
reactions = db.get_reactions(post)
listed = []
for r in reactions:
listed.append(f'{reactions[r]} {r}')
if listed:
page += ' · ' + ' '.join(listed)
page += '\n'
if post.tags:
page += '### ' + post.tags + '\n'
poster_link = f'=> /u/{post.poster_name} {post.poster_avatar} {post.poster_name}\n'
if session.is_context_tracker:
page += f'=> /{session.context.title()} 🐞 Issue #{post.issueid} in {session.context.title()}\n'
elif not session.c_user:
page += f'=> /{session.context.title()} Posted in: {session.context.title()}\n'
page += poster_link
last_age = post.age()
page += f'{last_age}'
if session.is_likes_enabled():
liked = []
if post.num_likes:
liked = db.get_likes(post)
page += ' · 👍 ' + ', '.join(liked)
if session.is_reactions_enabled():
reactions = db.get_reactions(post)
listed = []
for r in reactions:
listed.append(f'{reactions[r]} {r}')
if listed:
page += ' · ' + ' '.join(listed)
page += '\n'
# Post actions.
kind = 'issue' if session.is_context_tracker else 'post'
if session.user and not session.is_context_locked:
page += '\n## Actions\n'
if session.user.id == post.user or session.is_user_mod:
# Post actions.
kind = 'issue' if session.is_context_tracker else 'post'
if session.user and not session.is_context_locked:
page += '\n## Actions\n'
if session.is_editable(post):
page += f'=> /edit/{post.id} ✏️ Edit {kind}\n'
page += f'=> /comment/{post.id} 💬 Comment\n'
page += f'=> /comment/{post.id} 💬 Comment\n'
# Reactions.
if session.user.id != post.user:
if session.is_likes_enabled():
if session.user.name not in liked:
page += f'=> /like/{post.id} 👍 Like\n'
else:
page += f'=> /unlike/{post.id} 👎 Undo like\n'
if session.is_reactions_enabled():
reaction = db.get_user_reaction(post, session.user.id)
if reaction:
page += f'=> /react/{post.id} Change reaction: {reaction}\n'
else:
page += f'=> /react/{post.id} {session.bubble.user_reactions[0]} React\n'
if session.is_thanks_enabled():
page += f'=> /thanks/{post.id} 🙏 Give thanks\n'
# Reactions.
if session.user.id != post.user:
if session.is_likes_enabled():
if session.user.name not in liked:
page += f'=> /like/{post.id} 👍 Like\n'
else:
page += f'=> /unlike/{post.id} 👎 Undo like\n'
if session.is_reactions_enabled():
reaction = db.get_user_reaction(post, session.user.id)
if reaction:
page += f'=> /react/{post.id} Change reaction: {reaction}\n'
else:
page += f'=> /react/{post.id} {session.bubble.user_reactions[0]} React\n'
if session.is_thanks_enabled():
page += f'=> /thanks/{post.id} 🙏 Give thanks\n'
if session.user.id != post.user:
if (FOLLOW_POST, post.id) in session.user_follows:
page += f'=> /unfollow/post/{post.id} Unfollow {kind}\n'
if session.user.id != post.user:
if (FOLLOW_POST, post.id) in session.user_follows:
page += f'=> /unfollow/post/{post.id} Unfollow {kind}\n'
else:
if (MUTE_POST, post.id) in session.user_mutes:
page += f'=> /unmute/post/{post.id} 🔈 Unmute {kind}\n'
else:
page += f'=> /follow/post/{post.id} Follow {kind}\n'
page += f'=> /mute/post/{post.id} 🔇 Mute {kind}\n'
else:
# Own posts can be muted.
if (MUTE_POST, post.id) in session.user_mutes:
page += f'=> /unmute/post/{post.id} 🔈 Unmute {kind}\n'
else:
page += f'=> /follow/post/{post.id} Follow {kind}\n'
page += f'=> /mute/post/{post.id} 🔇 Mute {kind}\n'
else:
# Own posts can be muted.
if (MUTE_POST, post.id) in session.user_mutes:
page += f'=> /unmute/post/{post.id} 🔈 Unmute {kind}\n'
else:
page += f'=> /mute/post/{post.id} 🔇 Mute {kind}\n'
# Moderator actions on a post.
mod_actions = []
if session.user.id == post.user or session.is_user_mod:
if session.is_context_tracker:
if '✔︎' in post.tags:
mod_actions.append(f'=> /edit-tags/{post.id}/open 🐞 Reopen issue\n')
else:
mod_actions.append(f'=> /edit-tags/{post.id}/close ✔︎ Mark as closed\n')
mod_actions.append(f'=> /edit-tags/{post.id} 🏷️ Add/remove tags\n')
if session.is_title_editable(post) and not session.is_editable(post):
mod_actions.append(f'=> /edit/{post.id}/mod-title ✏️ Edit {kind} title\n')
if session.is_movable(post):
mod_actions.append(f'=> /edit/{post.id}/move/{session.get_token()} Move to subspace\n')
if session.is_deletable(post) and not session.is_editable(post):
mod_actions.append(f'=> /edit/{post.id}/delete/{session.get_token()} ❌ Delete {kind}\n')
if session.user.id == post.user and post.sub_owner == post.user:
antenna_feed = f"gemini://{session.bubble.hostname}{session.path}u/{session.user.name}/{post.id}/antenna"
mod_actions.append(f'=> {session.bubble.antenna_url}?{urlparse.quote(antenna_feed)} Submit post to 📡 Antenna\n')
# Moderator actions on a post.
mod_actions = []
if session.user.id == post.user or session.is_user_mod:
if session.is_context_tracker:
if '✔︎' in post.tags:
mod_actions.append(f'=> /edit-tags/{post.id}/open 🐞 Reopen issue\n')
else:
mod_actions.append(f'=> /edit-tags/{post.id}/close ✔︎ Mark as closed\n')
mod_actions.append(f'=> /edit-tags/{post.id} 🏷️ Add/remove tags\n')
if session.is_title_editable(post) and not session.is_editable(post):
mod_actions.append(f'=> /edit/{post.id}/mod-title ✏️ Edit {kind} title\n')
if session.is_movable(post):
mod_actions.append(f'=> /edit/{post.id}/move/{session.get_token()} Move to subspace\n')
if session.is_deletable(post) and not session.is_editable(post):
mod_actions.append(f'=> /edit/{post.id}/delete/{session.get_token()} ❌ Delete {kind}\n')
if session.user.id == post.user and post.sub_owner == post.user:
antenna_feed = f"gemini://{session.bubble.hostname}{session.path}u/{session.user.name}/{post.id}/antenna"
mod_actions.append(f'=> {session.bubble.antenna_url}?{urlparse.quote(antenna_feed)} Submit post to 📡 Antenna\n')
if mod_actions:
page += '\n' + ''.join(mod_actions)
if mod_actions:
page += '\n' + ''.join(mod_actions)
page += '\n' + session.dashboard_link()
page += '\n' + session.dashboard_link()
notifs = db.get_notifications(user=user, post_id=post.id)
if notifs:
page += f'{len(notifs)} notification{plural_s(len(notifs))} on this page:\n'
for notif in notifs:
link, label = notif.entry(with_title=False)
page += f'=> {link} {label}\n'
if len(notifs) > 1:
page += f'=> {post.page_url()}/clear-notif/{session.get_token()} 🧹 Clear\n'
notifs = db.get_notifications(user=user, post_id=post.id)
if notifs:
page += f'{len(notifs)} notification{plural_s(len(notifs))} on this page:\n'
for notif in notifs:
link, label = notif.entry(with_title=False)
page += f'=> {link} {label}\n'
if len(notifs) > 1:
page += f'=> {post.page_url()}/clear-notif/{session.get_token()} 🧹 Clear\n'
# Comments, repository commits, and issue cross-references.
display_order_desc = session.user and \
session.user.sort_cmt == User.SORT_COMMENT_NEWEST
comments = db.get_posts(parent=post.id,
draft=False,
sort_descending=False,
muted_by_user_id=(user.id if user else 0),
limit=None)
if is_comment_page:
# Omit comments older than the focused one.
comments = list(filter(lambda p: p.ts_created > focused_cmt.ts_created, comments))
n = len(comments)
if n > 0 or commits or incoming_xrefs:
if n > 1:
dir_icon = '' if display_order_desc else ''
else:
dir_icon = ''
page += f'\n## {n} Comment{plural_s(n)}{dir_icon}'
page += f'\n## {n} {"Later " if is_comment_page else ""}Comment{plural_s(n)}{dir_icon}\n'
if commits or incoming_xrefs:
# Combine commits and commits into one list.
@ -395,7 +428,7 @@ def make_post_page(session, post):
rendered_comments.append(cmt.incoming_entry())
continue
src = f'=> /u/{cmt.poster_name} {cmt.poster_avatar} {cmt.poster_name}\n'
src = f'=> /u/{cmt.poster_name}/{cmt.id} {cmt.poster_avatar} {cmt.poster_name}\n'
comment_body = session.render_post(cmt)
src += comment_body
@ -435,10 +468,13 @@ def make_post_page(session, post):
for rendered in rendered_comments:
page += '\n' + rendered
if is_comment_page and display_order_desc:
page += op_section
# Show the Comment action at the appropriate place wrt reading direction.
if session.user and not session.is_context_locked and \
len(comments) >= 1 and not display_order_desc:
page += f'\n=> /comment/{post.id} 💬 Comment\n'
page += f'\n=> /comment/{post.id} 💬 Add comment\n'
return page

View file

@ -76,7 +76,7 @@ class Notification:
MENTION: 11
}
def __init__(self, id, type, dst, src, post, subspace, is_sent, ts,
def __init__(self, id, type, dst, src, post, subspace, comment, is_sent, ts,
src_name=None, post_title=None, post_issueid=None, post_summary=None,
post_subname=None, post_subowner=None, subname=None, reaction=None):
self.id = id
@ -85,6 +85,7 @@ class Notification:
self.src = src
self.post = post
self.subspace = subspace
self.comment = comment
self.is_sent = is_sent
self.ts = ts
self.src_name = src_name
@ -503,6 +504,7 @@ class Database:
src INT,
post INT,
subspace INT,
comment INT, -- if not NULL, notification is about a comment (can be linked to)
is_sent BOOLEAN DEFAULT FALSE,
is_hidden BOOLEAN DEFAULT FALSE,
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
@ -1233,18 +1235,19 @@ class Database:
self.notify_followers(user, parent_post.id,
Notification.COMMENT_IN_FOLLOWED_SUBSPACE,
FOLLOW_SUBSPACE, parent_post.subspace)
FOLLOW_SUBSPACE, parent_post.subspace, comment_id=post.id)
self.notify_followers(user, parent_post.id,
Notification.COMMENT_BY_FOLLOWED_USER,
FOLLOW_USER, user.id)
FOLLOW_USER, user.id, comment_id=post.id)
self.notify_followers(user, parent_post.id,
Notification.COMMENT_ON_FOLLOWED_POST,
FOLLOW_POST, parent_post.id)
FOLLOW_POST, parent_post.id, comment_id=post.id)
if parent_post.user != user.id:
# Notify post author of a new comment.
cur.execute("INSERT IGNORE INTO notifs (type, dst, src, post) VALUES (?, ?, ?, ?)",
(Notification.COMMENT, parent_post.user, user.id, parent_post.id))
cur.execute("INSERT IGNORE INTO notifs (type, dst, src, post, comment) "
"VALUES (?, ?, ?, ?, ?)",
(Notification.COMMENT, parent_post.user, user.id, parent_post.id, post.id))
self.commit()
@ -1746,9 +1749,9 @@ class Database:
notif_post_id = post.parent if post.parent else post.id
where_names_cond = f"name IN ({('?,' * len(names))[:-1]})"
cur.execute(f"""
INSERT IGNORE INTO notifs (type, dst, src, post)
INSERT IGNORE INTO notifs (type, dst, src, post, comment)
SELECT
{Notification.MENTION}, id, {post.user}, {notif_post_id}
{Notification.MENTION}, id, {post.user}, {notif_post_id}, {post.id if post.parent else "NULL"}
FROM users
WHERE id!={post.user} AND {where_names_cond}
""", names)
@ -1768,24 +1771,27 @@ class Database:
for (i,) in cur: uids.append(i)
for uid in uids:
cur.execute("""
INSERT IGNORE INTO notifs (type, dst, src, post)
VALUES (?, ?, ?, ?)
""", (Notification.COMMENT_ON_COMMENTED, uid, new_comment.user, new_comment.parent))
INSERT IGNORE INTO notifs (type, dst, src, post, comment)
VALUES (?, ?, ?, ?, ?)
""", (Notification.COMMENT_ON_COMMENTED, uid, new_comment.user, new_comment.parent,
new_comment.id))
if uids:
self.commit()
def notify_followers(self, actor: User, post_id, notif_type, follow_type, target_id):
def notify_followers(self, actor: User, post_id, notif_type, follow_type, target_id,
comment_id=None):
cur = self.conn.cursor()
cur.execute(f"""
INSERT IGNORE INTO notifs (type, dst, src, post)
INSERT IGNORE INTO notifs (type, dst, src, post, comment)
SELECT
{notif_type},
user,
{actor.id},
{post_id}
{post_id},
?
FROM follow
WHERE type={follow_type} AND target={target_id}
""")
""", (comment_id,))
self.commit()
def notify_thanks(self, user: User, post: Post):
@ -1980,8 +1986,12 @@ class Database:
cur = self.conn.cursor()
cur.execute(f"""
SELECT
n.id, n.type, n.dst, n.src, n.post, n.subspace, n.is_sent, UNIX_TIMESTAMP(n.ts),
u.name, p.title, p.issueid, p.summary, s.name, s.owner, s2.name, r.reaction
n.id, n.type, n.dst, n.src, n.post, n.subspace, n.comment, n.is_sent, UNIX_TIMESTAMP(n.ts),
u.name,
p.title, p.issueid, p.summary,
s.name, s.owner,
s2.name,
r.reaction
FROM notifs n
JOIN users u ON src=u.id
LEFT JOIN posts p ON post=p.id
@ -1996,9 +2006,9 @@ class Database:
WHERE {' AND '.join(cond)}
""", tuple(values))
notifs = []
for (id, type, dst, src, post, subspace, is_sent, ts, src_name, post_title, post_issueid,
for (id, type, dst, src, post, subspace, comment, is_sent, ts, src_name, post_title, post_issueid,
post_summary, post_subname, post_subowner, subname, reaction) in cur:
notifs.append(Notification(id, type, dst, src, post, subspace, is_sent, ts,
notifs.append(Notification(id, type, dst, src, post, subspace, comment, is_sent, ts,
src_name, post_title, post_issueid, post_summary,
post_subname, post_subowner, subname, reaction))

View file

@ -174,12 +174,17 @@ def user_actions(session):
notif = db.get_notification(session.user, notif_id, clear=True)
if not notif:
return 30, '/dashboard'
if notif.comment:
cmt = db.get_post(id=notif.comment)
if not cmt:
return 51, 'Not found'
return 30, cmt.page_url()
if notif.post:
post = db.get_post(id=notif.post)
if not post:
return 51, 'Not found'
return 30, post.page_url()
elif notif.subspace:
if notif.subspace:
subs = db.get_subspace(id=notif.subspace)
if not subs:
return 51, 'Not found'