Bubble [main]
Added a private thanks mechanism
[1mdiff --git a/50_bubble.py b/50_bubble.py[m
[1mindex da3ce55..e6552e8 100644[m
[1m--- a/50_bubble.py[m
[1m+++ b/50_bubble.py[m
[36m@@ -33,6 +33,7 @@[m [mclass Bubble:[m
self.site_info = cfg.get('info', "Bulletin Boards for Gemini")[m
self.site_info_nouser = cfg.get('info.nouser', self.site_info)[m
self.likes_enabled = cfg.getboolean('user.likes', True)[m
[32m+[m[32m self.thanks_enabled = cfg.getboolean('user.thanks', True)[m
self.user_register = cfg.getboolean('user.register', True)[m
self.admin_certpass = cfg.get('admin.certpass', '')[m
self.antenna_url = cfg.get('antenna.url', 'gemini://warmedal.se/~antenna/submit')[m
[36m@@ -114,6 +115,9 @@[m [mBubble is open source:[m
enabled = enabled and (self.user.flags & User.HIDE_LIKES_FLAG) == 0[m
return enabled[m
[m
[32m+[m[32m def is_thanks_enabled(self):[m
[32m+[m[32m return self.bubble.thanks_enabled[m
[32m+[m
def is_editable(self, post: Post):[m
return self.user.role == User.ADMIN or post.user == self.user.id[m
[m
[36m@@ -475,6 +479,11 @@[m [mIn the "Edit text" prompts of the draft composer:[m
[m
When editing segments in the draft composer, you may sometimes accidentally submit the input and overwrite the contents, losing some text that you meant to keep in there. While Bubble does not currently keep a history of past edited versions, your client may be able to help you. If you navigate backwards, the old contents of the segment may be found in a cached copy of the page.[m
[m
[32m+[m[32m## Reactions: Likes and Thanks[m
[32m+[m
[32m+[m[32m* "Likes" are a way of giving positive feedback in public. The names of people who have liked a post is visible to everyone. Likes can be disabled per-user or site-wide in the Bubble configuration.[m
[32m+[m[32m* "Thanks" are used for showing appreciation privately, in an ephemeral manner. The recipient will see a notification about the thanks. The notification will remain visible in Notification History for a few days, after which all record of it disappears.[m
[32m+[m
## Follows[m
[m
You can follow posts, subspaces, and users to be notified of their activity, and to select what is visible in the Followed feed.[m
[36m@@ -520,7 +529,8 @@[m [mwhen the administrator assigns at least one moderator to it.[m
req.path.startswith(self.path + 'vote/') or \[m
req.path.startswith(self.path + 'follow/') or \[m
req.path.startswith(self.path + 'unfollow/') or \[m
[31m- req.path.startswith(self.path + 'notif/'):[m
[32m+[m[32m req.path.startswith(self.path + 'notif/') or \[m
[32m+[m[32m req.path.startswith(self.path + 'thanks/'):[m
return user_actions(session)[m
[m
elif req.path == self.path + 'dashboard':[m
[1mdiff --git a/feeds.py b/feeds.py[m
[1mindex 57b2149..2fb91b3 100644[m
[1m--- a/feeds.py[m
[1m+++ b/feeds.py[m
[36m@@ -271,11 +271,16 @@[m [mdef make_post_page(session, post):[m
if session.is_editable(post):[m
page += f'=> /edit/{post.id} ✏️ Edit {kind}\n'[m
page += f'=> /comment/{post.id} 💬 Comment\n'[m
[31m- if session.is_likes_enabled():[m
[31m- if session.user.name not in liked:[m
[31m- page += f'=> /like/{post.id} 👍 Like\n'[m
[31m- else:[m
[31m- page += f'=> /unlike/{post.id} 👎 Undo like\n'[m
[32m+[m
[32m+[m[32m # Reactions.[m
[32m+[m[32m if session.user.id != post.user:[m
[32m+[m[32m if session.is_likes_enabled():[m
[32m+[m[32m if session.user.name not in liked:[m
[32m+[m[32m page += f'=> /like/{post.id} 👍 Like\n'[m
[32m+[m[32m else:[m
[32m+[m[32m page += f'=> /unlike/{post.id} 👎 Undo like\n'[m
[32m+[m[32m if session.is_thanks_enabled():[m
[32m+[m[32m page += f'=> /thanks/{post.id} 🙏 Give thanks\n'[m
[m
if session.user.id == post.user or session.is_user_mod:[m
page += f'=> /edit-tags/{post.id} 🏷️ Add/remove tags\n'[m
[1mdiff --git a/model.py b/model.py[m
[1mindex 09e2228..e79a7ae 100644[m
[1m--- a/model.py[m
[1m+++ b/model.py[m
[36m@@ -53,7 +53,9 @@[m [mclass Notification:[m
ADDED_AS_MODERATOR = 0x0400[m
REMOVED_AS_MODERATOR = 0x0800[m
COMMENT_IN_FOLLOWED_SUBSPACE = 0x1000[m
[32m+[m[32m THANKS = 0x2000[m
[m
[32m+[m[32m # Priority order for merging/overriding notifications.[m
PRIORITY = {[m
COMMENT_IN_FOLLOWED_SUBSPACE: 0,[m
POST_IN_FOLLOWED_SUBSPACE: 1,[m
[36m@@ -103,6 +105,9 @@[m [mclass Notification:[m
if self.type == Notification.LIKE:[m
event = f'liked your {kind}'[m
icon = '👍 '[m
[32m+[m[32m elif self.type == Notification.THANKS:[m
[32m+[m[32m event = f'thanked you'[m
[32m+[m[32m icon = '🙏'[m
elif self.type == Notification.COMMENT:[m
event = f'commented on your {kind}'[m
icon = '💬 '[m
[36m@@ -140,6 +145,8 @@[m [mclass Notification:[m
if vis_title:[m
if self.type == Notification.MENTION:[m
event += ' in'[m
[32m+[m[32m elif self.type == Notification.THANKS:[m
[32m+[m[32m event += ' for'[m
elif self.type == Notification.COMMENT_ON_COMMENTED:[m
event += ' about'[m
elif self.type == Notification.COMMENT_IN_FOLLOWED_SUBSPACE:[m
[36m@@ -467,7 +474,6 @@[m [mclass Database:[m
is_sent BOOLEAN DEFAULT FALSE,[m
is_hidden BOOLEAN DEFAULT FALSE,[m
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP,[m
[31m- -- UNIQUE KEY (type, dst, src, post),[m
INDEX (dst),[m
INDEX (dst, post),[m
CONSTRAINT no_self_notif CHECK (src!=dst)[m
[36m@@ -1612,6 +1618,20 @@[m [mclass Database:[m
""")[m
self.commit()[m
[m
[32m+[m[32m def notify_thanks(self, user: User, post: Post):[m
[32m+[m[32m cur = self.conn.cursor()[m
[32m+[m[32m ntype = Notification.THANKS[m
[32m+[m[32m cur.execute(f"""[m
[32m+[m[32m IF (SELECT COUNT(id)=0[m
[32m+[m[32m FROM notifs[m
[32m+[m[32m WHERE is_hidden=FALSE AND type={ntype} AND src=? AND dst=? AND post=?)[m
[32m+[m[32m THEN[m
[32m+[m[32m INSERT INTO notifs (type, src, dst, post) VALUES ({ntype}, ?, ?, ?);[m
[32m+[m[32m END IF[m
[32m+[m[32m """, (user.id, post.user, post.id,[m
[32m+[m[32m user.id, post.user, post.id))[m
[32m+[m[32m self.commit()[m
[32m+[m
def notify_new_poll(self, post: Post):[m
cur = self.conn.cursor()[m
cur.execute(f"""[m
[36m@@ -1803,6 +1823,7 @@[m [mclass Database:[m
indexed = {}[m
for notif in notifs:[m
if not notif.post or notif.type in (Notification.LIKE,[m
[32m+[m[32m Notification.THANKS,[m
Notification.ISSUE_CLOSED):[m
# Subspace notifications are resolved as-is.[m
resolved.append(notif)[m
[1mdiff --git a/user.py b/user.py[m
[1mindex cdefe9b..0266ee6 100644[m
[1m--- a/user.py[m
[1m+++ b/user.py[m
[36m@@ -23,6 +23,22 @@[m [mdef user_actions(session):[m
db.modify_likes(session.user, post, add=(action == 'like'))[m
return 30, post.page_url()[m
[m
[32m+[m[32m elif req.path.startswith(session.path + 'thanks/'):[m
[32m+[m[32m if not user:[m
[32m+[m[32m return 60, 'Login required'[m
[32m+[m[32m found = re.match(r"^thanks/(\d+)$", req.path[len(session.path):])[m
[32m+[m[32m post_id = int(found.group(1))[m
[32m+[m[32m post = db.get_post(id=post_id)[m
[32m+[m[32m if not post:[m
[32m+[m[32m return 51, 'Not found'[m
[32m+[m[32m db.notify_thanks(session.user, post)[m
[32m+[m[32m page = '# 🙏\n\nYou thanked\n'[m
[32m+[m[32m poster = db.get_user(id=post.user)[m
[32m+[m[32m page += f'=> /u/{poster.name} {poster.avatar} {poster.name}\n'[m
[32m+[m[32m page += 'for:\n'[m
[32m+[m[32m page += session.gemini_feed_entry(post, session.context)[m
[32m+[m[32m return page[m
[32m+[m
elif req.path.startswith(session.path + 'vote/'):[m
try:[m
if not user:[m