Bubble [main]

Flairs: Internal markup, improved rendering with detail levels

=> fb83c111e3aadd361ae323dbcb3c40aea54d803a

diff --git a/50_bubble.py b/50_bubble.py
index a1fc72f..a0a236c 100644
--- a/50_bubble.py
+++ b/50_bubble.py
@@ -249,6 +249,13 @@ Bubble is open source:
 
             return f'=> /dashboard {self.user.avatar} {self.user.name}{notifs}{mode}\n'
 
+        def feed_flair(self, post, context):
+            flair = User.render_flair(post.poster_flair,
+                                      context,
+                                      abbreviate=True,
+                                      user_mod=post.user in self.context_mod_ids)
+            return f' [{flair}]' if flair else ''
+
         def feed_entry(self, post, context=None, omit_rotate_info=False, is_activity_feed=False):
             is_issue_tracker = self.is_context_tracker
             is_comment = post.parent != 0   # Flat feeds intermingle comments with posts.
@@ -276,7 +283,6 @@ Bubble is open source:
             else:
                 age = post.age(tz=self.tz)
             bell = ' ๐Ÿ””' if post.num_notifs else ''
-            flair = f' [{post.poster_flair}]' if post.poster_flair else ''
 
             SHORT_PREVIEW_LEN = 160
 
@@ -320,7 +326,7 @@ Bubble is open source:
                     post_icon = post.poster_avatar
                     post_label = post.poster_name
                     if not (is_user_post and context and context.id == post.subspace):
-                        post_label += flair
+                        post_label += self.feed_flair(post, context)
                     post_path = f'/u/{post.poster_name}'
                     meta_icon = '๐Ÿ’ฌ'
 
@@ -352,7 +358,7 @@ Bubble is open source:
                 # Last line in the metadata.
                 meta = []
                 if is_comment or (sub and not is_activity_feed):
-                    meta.append(post.poster_name + flair)
+                    meta.append(post.poster_name + self.feed_flair(post, context))
                 if cmt:
                     meta.append(cmt)
                 if likes:
diff --git a/db-migrate.sql b/db-migrate.sql
index 5728e8a..cc2fc41 100644
--- a/db-migrate.sql
+++ b/db-migrate.sql
@@ -32,4 +32,7 @@ UPDATE users SET notif=notif|0x040000;
 UPDATE users SET notif=notif|0x100000;
 
 -- Migration from v7 to v8 --
-ALTER TABLE users ADD COLUMN flair VARCHAR(30) DEFAULT '';
\ No newline at end of file
+ALTER TABLE users ADD COLUMN flair VARCHAR(30) DEFAULT '';
+
+-- Migration from v8.0 to v8.1 --
+ALTER TABLE users MODIFY COLUMN flair VARCHAR(1000) DEFAULT '';
diff --git a/feeds.py b/feeds.py
index 66e86a4..5111fa7 100644
--- a/feeds.py
+++ b/feeds.py
@@ -298,7 +298,10 @@ def make_post_page_or_configure_feed(session):
             if session.c_user:
                 page = f'# {session.c_user.avatar} {session.c_user.name}\n'
                 if session.c_user.flair:
-                    page += f"[{session.c_user.flair}]\n"
+                    flair = User.render_flair(session.c_user.flair, session.context,
+                                              long_form=True, db=session.db)
+                    if flair:
+                        page += f"\n{flair}\n"
             else:
                 page = f'# {subspace.title()}\n'
             page += f'=> /{subspace.title()} {subspace.title()}\n'
@@ -345,13 +348,13 @@ def make_post_page(session, post):
     page = ''
     focused_cmt = None
 
-    def commenter_flair(cmt, post):
-        cmt_flair = [cmt.poster_flair] if cmt.poster_flair else []
-        if post and cmt.user == post.user:
-            cmt_flair = ['op'] + cmt_flair
-        elif cmt.user in session.context_mod_ids:
-            cmt_flair = ['mod'] + cmt_flair
-        return f" [{', '.join(cmt_flair)}]" if cmt_flair else ""
+    def commenter_flair(cmt, post, abbreviate, with_context=None):
+        flair = User.render_flair(cmt.poster_flair,
+                                  with_context if with_context else session.context,
+                                  abbreviate=abbreviate,
+                                  user_mod=cmt.user in session.context_mod_ids,
+                                  user_op=post and cmt.user == post.user)
+        return f' [{flair}]' if flair else ''
 
     if is_comment_page:
         # Switch to the parent post, but display it in preview mode.
@@ -370,7 +373,9 @@ def make_post_page(session, post):
                 return 51, 'Not found'
             page += f'=> /help/deleted-post ๐Ÿ”’ Comment on a deleted post (ID:{post_id})\n\n'
         page += session.render_post(focused_cmt)
-        flair = commenter_flair(focused_cmt, post)
+        flair = commenter_flair(focused_cmt, post,
+                                abbreviate=False,
+                                with_context=db.get_subspace(post.subspace))
         page += f'\n=> /u/{focused_cmt.poster_name} {focused_cmt.poster_avatar} {focused_cmt.poster_name}{flair}\n'
         page += f'{focused_cmt.age()}\n'
 
@@ -449,7 +454,13 @@ def make_post_page(session, post):
             page += '\n'
         if post.tags:
             page += '### ' + post.tags + '\n'
-        flair = f" [{post.poster_flair}]" if post.poster_flair else ""
+        #flair = f" [{post.poster_flair}]" if post.poster_flair else ""
+
+        flair = User.render_flair(post.poster_flair,
+                                  session.context,
+                                  user_mod=post.user in session.context_mod_ids)
+        if flair: flair = f" [{flair}]"
+
         poster_link = f'=> /u/{post.poster_name} {post.poster_avatar} {post.poster_name}{flair}\n'
         if session.is_context_tracker:
             page += f'=> /{session.context.title()} ๐Ÿž Issue #{post.issueid} in {session.context.title()}\n'
@@ -590,7 +601,7 @@ def make_post_page(session, post):
                     cmt.ymd_hm(tz=session.tz, date_fmt='%b %d', time_prefix='at ') if elapsed_hours < 24 * 180 else \
                     cmt.ymd_hm(tz=session.tz, time_prefix='at ')
 
-            cmt_flair = commenter_flair(cmt, post)
+            cmt_flair = commenter_flair(cmt, post, abbreviate=True)
             if not session.is_archive:
                 src = f'=> /u/{cmt.poster_name}/{cmt.id} {cmt.poster_avatar} {cmt.poster_name}{cmt_flair} ยท {comment_age}:\n'
             else:
@@ -715,13 +726,14 @@ def make_feed_page(session):
     elif not context:
         topinfo += f"{session.bubble.site_info if session.user else session.bubble.site_info_nouser}\n"
     else:
-        if c_user and (c_user.info or c_user.url):
+        if c_user and (c_user.info or c_user.url or c_user.flair):
             if c_user.info:
                 topinfo += c_user.info + '\n'
-                if c_user.flair:
-                    topinfo += f"[{c_user.flair}]\n"
             if c_user.url:
                 topinfo += f'=> {c_user.url}\n'
+            if c_user.flair:
+                flair = User.render_flair(c_user.flair, context=None, long_form=True, db=session.db)
+                topinfo += f'\n{flair}'
         elif context:
             if context.info:
                 topinfo += context.info + '\n'
diff --git a/model.py b/model.py
index e839ec4..1330c77 100644
--- a/model.py
+++ b/model.py
@@ -233,6 +233,15 @@ class User:
     # Roles:
     BASIC, ADMIN, LIMITED = range(3)
 
+    # Flair types:
+    FLAIRS = {
+        'โ™ก': 'Self description',
+        '๐Ÿ๏ธ': 'Absence',
+        'โœ๏ธ': 'Writing style',
+        '๐Ÿ—ฃ๏ธ': 'Interaction style',
+        '๐Ÿ›‚': 'Note from moderator',
+    }
+
     # Sort modes:
     SORT_POST_RECENT    = 'r'
     SORT_POST_HOTNESS   = 'h'
@@ -289,6 +298,90 @@ class User:
 
         return None
 
+    def render_flair(user_flair, context, abbreviate=False, long_form=False, db=None,
+                     user_mod=False, user_op=False):
+        """
+        Arguments:
+            db (Database): database object for looking up subspace names
+                in the long form.
+            context (Subspace): where the flair is being shown. If None,
+                the flair is being shown in the home feed.
+            abbreviate (bool): user-provided text is omitted and only
+                icons are shown.
+            long_form (bool): a description of the flair type is included
+                and the output is formatted onto multiple lines instead
+                of being a single line.
+        """
+        if not user_flair.strip():
+            return ''
+
+        out = ''
+
+        for flair in user_flair.split('\n'):
+            pos = flair.find(':')
+            scope = flair[:pos].strip() if pos >= 0 else None
+            is_admin_assigned = (scope and scope.startswith('*'))
+            if is_admin_assigned:
+                scope = scope[1:]
+            label = flair[pos + 1:].strip() if pos >= 0 else flair
+            icon = ''
+            if len(label):
+                for key in User.FLAIRS:
+                    if label.startswith(key):
+                        icon = key
+                        label = label[len(key):].strip()
+                        break
+
+            #print(user_flair, icon, label, scope)
+
+            has_abbrev = False
+
+            if long_form:
+                # Show everything in the long form.
+                if icon:
+                    out += icon + ' '
+                if scope:
+                    subspace = db.get_subspace(id=abs(int(scope)))
+                    if subspace:
+                        scope = f" (in {subspace.title()})"
+                    else:
+                        scope = " (in a deleted subspace)"
+                else:
+                    scope = ''
+                if icon:
+                    out += f"{User.FLAIRS[icon]}: "
+                elif not scope and is_admin_assigned:
+                    out += '๐Ÿ“› Assigned flair: '
+                else:
+                    out += '๐Ÿ“› Personal flair: '
+                out += f"{label}{scope}{' (set by admin)' if is_admin_assigned else ''}\n"
+
+            elif not scope or (context and int(scope) == context.id):
+                # A global flair is displayed everywhere, otherwise the scope must match
+                # current context.
+                if len(out): out += ' ' if abbreviate else ', '
+                out += icon
+                if not abbreviate or not scope:
+                    if len(out): out += ' '
+                    out += label
+                elif not icon:
+                    has_abbrev = True
+
+        if not long_form:
+            if user_op or user_mod:
+                if len(out):
+                    out = ', ' + out
+                if user_op and user_mod:
+                    out = 'OP/mod' + out
+                elif user_op:
+                    out = 'OP' + out
+                elif user_mod:
+                    out = 'mod' + out
+            if has_abbrev:
+                out += '...'
+
+        return out
+
 
 class Subspace:
     OMIT_FROM_ALL_FLAG = 0x1
@@ -500,7 +593,7 @@ class Database:
         db.execute("""CREATE TABLE IF NOT EXISTS users (
             id          INT PRIMARY KEY AUTO_INCREMENT,
             name        VARCHAR(30) UNIQUE,
-            flair       VARCHAR(30) DEFAULT '',
+            flair       VARCHAR(1000) DEFAULT '',
             info        VARCHAR(1000) DEFAULT '',
             url         VARCHAR(1000) DEFAULT '',
             recovery    VARCHAR(1000) DEFAULT '',
Proxy Information
Original URL
gemini://git.skyjake.fi/bubble/main/cdiff/fb83c111e3aadd361ae323dbcb3c40aea54d803a
Status Code
Success (20)
Meta
text/gemini; charset=utf-8
Capsule Response Time
28.346769 milliseconds
Gemini-to-HTML Time
0.564206 milliseconds

This content has been proxied by September (ba2dc).