diff --git a/50_bubble.py b/50_bubble.py

index e29a0b7..039def0 100644

--- a/50_bubble.py

+++ b/50_bubble.py

@@ -109,10 +109,15 @@ Bubble is open source:

         self.token = None

         self.notif_count = None

         self.is_short_preview = False

+ self.tz = pytz.timezone('UTC')



     def set_user(self, user):

         self.user = user

         if user:

+ try:

+ self.tz = pytz.timezone(user.timezone)

+ except:

+ pass

             if user.flags & User.ASCII_ICONS_FLAG:

                 self.CHECKS = [ '[_]', '[x]' ]

                 # TODO: Add more of these.

@@ -213,7 +218,7 @@ Bubble is open source:

             cmt = 'View post' if post.num_cmts == 0 and is_user_post else \

                   '' if post.num_cmts == 0 else \

                   f'{post.num_cmts} comment{plural_s(post.num_cmts)}'

- age = post.age()

+ age = post.age(tz=self.tz)

         bell = ' 馃敂' if post.num_notifs else ''



         author = '馃寬 ' + sub if sub else (post.poster_avatar + ' ' + post.poster_name)

diff --git a/db-migrate.sql b/db-migrate.sql

index 9262e7a..391761b 100644

--- a/db-migrate.sql

+++ b/db-migrate.sql

@@ -20,4 +20,5 @@ ALTER TABLE posts ADD INDEX (user);

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 '';

diff --git a/model.py b/model.py

index 7cb3ca2..c45808d 100644

--- a/model.py

+++ b/model.py

@@ -96,14 +96,16 @@ class Notification:

     self.subname = subname

     self.reaction = reaction



- def ymd_date(self, fmt='%Y-%m-%d'):

+ def ymd_date(self, fmt='%Y-%m-%d', tz=None):

     dt = datetime.datetime.fromtimestamp(self.ts, UTC)

+ if tz:

+ dt = dt.astimezone(tz)

     return dt.strftime(fmt)



 def age(self):

     return ago_text(self.ts)



- def entry(self, show_age=True, with_time=False, with_title=True) -> tuple:

+ def entry(self, show_age=True, with_time=False, with_title=True, tz=None) -> tuple:

     """Returns (link, label) to use in the notification list."""



     event = ''

@@ -170,7 +172,7 @@ class Notification:

             event += f' "{vis_title}"'



     age = f' 路 {self.age()}' if show_age else ''

- hm_time = f" at {self.ymd_date('%H:%M')}" if with_time else ''

+ hm_time = f" at {self.ymd_date('%H:%M', tz)}" if with_time else ''

     return f'/notif/{self.id}', f'{icon}{self.src_name} {event}{hm_time}{age}'





@@ -193,7 +195,8 @@ class User:

 SHORT_PREVIEW_FLAG      = 0x0020



 def __init__(self, id, name, info, url, recovery, avatar, role, flags, notif, email, email_inter, \

- email_range, password, ts_password, ts_created, ts_active, sort_post, sort_cmt):

+ email_range, password, ts_password, ts_created, ts_active, sort_post, sort_cmt,

+ timezone):

     self.id = id

     self.name = name

     self.info = info

@@ -212,6 +215,7 @@ class User:

     self.ts_active = ts_active

     self.sort_post = sort_post

     self.sort_cmt = sort_cmt

+ self.timezone = timezone



 def subspace_link(self, prefix=''):

     return f'=> /u/{self.name} {self.avatar} {prefix}u/{self.name}\n'

@@ -265,8 +269,10 @@ class Commit:

     self.msg = msg

     self.ts = ts



- def ymd_date(self):

+ def ymd_date(self, tz=None):

     dt = datetime.datetime.fromtimestamp(self.ts, UTC)

+ if tz:

+ dt = dt.astimezone(tz)

     return dt.strftime('%Y-%m-%d')



 def entry(self, view_url, outgoing=False):

@@ -320,19 +326,23 @@ class Post:

     else:

         return '(untitled issue)' if self.issueid else '(untitled post)'



- def ymd_date(self):

+ def ymd_date(self, tz=None):

     dt = datetime.datetime.fromtimestamp(self.ts_created, UTC)

+ if tz:

+ dt = dt.astimezone(tz)

     return dt.strftime('%Y-%m-%d')



- def ymd_hm_tz(self):

+ def ymd_hm_tz(self, tz=None):

     dt = datetime.datetime.fromtimestamp(self.ts_created, UTC)

- return dt.strftime('%Y-%m-%d %H:%M UTC')

+ if tz:

+ dt = dt.astimezone(tz)

+ return dt.strftime(f'%Y-%m-%d %H:%M %z')



- def age(self):

+ def age(self, tz=None):

     if self.is_draft:

         return 'Now'

     else:

- return ago_text(self.ts_created)

+ return ago_text(self.ts_created, tz=tz)



 def page_url(self):

     if self.sub_owner:

@@ -412,7 +422,8 @@ class Database:

         ts_active   TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

         ts_email    TIMESTAMP DEFAULT '2000-01-01 00:00:00',

         sort_post   CHAR(1) DEFAULT 'r',

- sort_cmt CHAR(1) DEFAULT 'n'

+ sort_cmt CHAR(1) DEFAULT 'n',

+ timezone VARCHAR(40) DEFAULT 'UTC'

     )""", (0xffff,))



     db.execute("""CREATE TABLE IF NOT EXISTS certs (

@@ -662,6 +673,7 @@ class Database:

             UNIX_TIMESTAMP(u.ts_active),

             u.sort_post,

             u.sort_cmt,

+ u.timezone,

             s.id

         FROM users u

             JOIN subspaces s ON s.owner=u.id

@@ -669,10 +681,12 @@ class Database:



     for (id, name, info, url, recovery, avatar, role, flags, notif, email, email_inter,

          email_range, password, ts_password, ts_created, ts_active,

- sort_post, sort_cmt, user_subspace_id) in cur:

+ sort_post, sort_cmt, timezone, user_subspace_id) in cur:

         user = User(id, name, info, url, recovery, avatar, role, flags, notif,

- email, email_inter, email_range, password, \

- ts_password, ts_created, ts_active, sort_post, sort_cmt)

+ email, email_inter, email_range,

+ password, ts_password,

+ ts_created, ts_active,

+ sort_post, sort_cmt, timezone)

         user.moderated_subspace_ids = [user_subspace_id] + self.get_moderated_subspace_ids(user)

         return user



@@ -721,7 +735,7 @@ class Database:

                 email=None, email_inter=None, email_range=None,

                 notif=None, flags=None,

                 password=None, password_expiration_offset_minutes=0,

- sort_post=None, sort_cmt=None,

+ sort_post=None, sort_cmt=None, timezone=None,

                 active=False):

     if type(user) is User:

         user = user.id

@@ -768,6 +782,9 @@ class Database:

     if sort_cmt:

         stm.append('sort_cmt=?')

         values.append(sort_cmt)

+ if timezone:

+ stm.append('timezone=?')

+ values.append(timezone)

     if active:

         stm.append('ts_active=CURRENT_TIMESTAMP()')

     if stm:

@@ -1045,7 +1062,7 @@ class Database:



     mods = []

     for (id, avatar, name) in cur:

- mods.append(User(id, name, None, None, None, avatar, None, None, None, None, None, None,

+ mods.append(User(id, name, None, None, None, avatar, None, None, None, None, None, None, None,

                          None, None, None, None, None, None))

     return mods



@@ -2261,7 +2278,7 @@ class Search:

         for (ts, id, name, avatar, info, url) in cur:

             self.results.append(((exact_match(name), ts),

                                 User(id, name, info, url, None, avatar, None, None, None,

- None, None, None, None, None, ts, None, None, None)))

+ None, None, None, None, None, ts, None, None, None, None)))



         # Subspaces.

         cur.execute(f"""

diff --git a/requirements.txt b/requirements.txt

new file mode 100644

index 0000000..af44f19

--- /dev/null

+++ b/requirements.txt

@@ -0,0 +1 @@

+pytz

diff --git a/settings.py b/settings.py

index 2fb5b76..4766529 100644

--- a/settings.py

+++ b/settings.py

@@ -1,4 +1,5 @@

import math

+import pytz

from utils import *

from model import Notification, User, Subspace, FOLLOW_USER, FOLLOW_SUBSPACE, \

 MUTE_USER, MUTE_SUBSPACE

@@ -176,7 +177,7 @@ def make_settings_page(session):

     return 30, '/settings/notif'



 elif req.path == session.path + 'settings/email-range':

- prompt = 'Hour range when emails are not sent, in UTC: (Examples: "0-6", "21-3")'

+ prompt = f'{session.tz.localize(datetime.datetime.now()).tzname()} hour range (inclusive) when emails are not sent: (Examples: "0-6", "21-3")'

     pattern = re.compile(r'(\d+)-(\d+)')

     if req.query == None:

         return 10, prompt

@@ -188,6 +189,12 @@ def make_settings_page(session):

     begin = min(23, begin)

     end = max(0, end)

     end = min(23, end)

+ # Convert from user's time zone to UTC. The actual date doesn't matter,

+ # we are just using the hours.

+ dt_begin = session.tz.localize(datetime.datetime(2023, 6, 19, begin, 0, 0))

+ dt_end = session.tz.localize(datetime.datetime(2023, 6, 19, end, 0, 0))

+ begin = dt_begin.astimezone(pytz.utc).hour

+ end = dt_end.astimezone(pytz.utc).hour

     db.update_user(session.user, email_range=f"{begin}-{end}")

     return 30, '/settings/notif'



@@ -287,7 +294,20 @@ def make_settings_page(session):

         page += '\n### Email\n'

         page += f'=> /settings/email 馃摟 Send to: {session.user.email if session.user.email else "(not set)"}\n'

         page += f'\n=> /settings/email-inter Interval: {session.user.email_inter} minutes\n'

- email_range = "(not set)" if not session.user.email_range else session.user.email_range

+ #email_range = "(not set)" if not session.user.email_range else session.user.email_range

+

+ # Convert the range to local hours.

+ if session.user.email_range:

+ begin, end = map(int, session.user.email_range.split('-'))

+ # The date doesn't matter, we are just using the hours.

+ dt_begin = datetime.datetime(2023, 6, 19, begin, 0, 0, tzinfo=UTC)

+ dt_end = datetime.datetime(2023, 6, 19, end, 0, 0, tzinfo=UTC)

+ dt_begin = dt_begin.astimezone(session.tz)

+ dt_end = dt_end.astimezone(session.tz)

+ email_range = f"{dt_begin.hour}-{dt_end.hour}"

+ else:

+ email_range = "(not set)"

+

         page += f'=> /settings/email-range "Do not disturb" range: {email_range}\n'



         return page

@@ -507,6 +527,18 @@ def make_settings_page(session):

             page += '\n'

     return page



+ elif req.path == session.path + 'settings/timezone':

+ if is_empty_query(req):

+ page = 'Select a time zone:\n\n'

+ for tz in pytz.all_timezones:

+ page += f'=> ?{tz} {tz}\n'

+ return page

+ tz = clean_query(req)

+ if not tz in pytz.all_timezones:

+ return 50, 'Invalid time zone'

+ db.update_user(session.user, timezone=tz)

+ return 30, '/settings/display'

+

 elif req.path == session.path + 'settings/display':

     page += f'# {session.user.name}: Settings\n'

     page += '=> /settings 鈿欙笍 Go back\n\n'

@@ -515,10 +547,12 @@ def make_settings_page(session):



     page += '## Display\n\n'

     page += f'=> /settings/short-preview {session.CHECKS[nonzero(session.user.flags & User.SHORT_PREVIEW_FLAG)]} Short post previews\n'

+ page += f'\n=> /settings/timezone Time zone: {user.timezone}\n'

     page += f'=> /settings/ascii Display icons as: {ICON_MODE[nonzero(session.user.flags & User.ASCII_ICONS_FLAG)]}\n'

     return page



- elif req.path == session.path + 'settings':

+ elif req.path == session.path + 'settings' or \

+ req.path == session.path + 'settings/':

     page = f'# {session.user.name}: Settings\n'

     page += f'\n=> /dashboard Back to Dashboard\n'

     page += f'=> / Back to front page\n\n'

diff --git a/subspace.py b/subspace.py

index 9afc67a..bff379d 100644

--- a/subspace.py

+++ b/subspace.py

@@ -329,7 +329,7 @@ def make_search_page(session):

             kind = "Comment" if obj.parent else f"Issue #{obj.issueid}" if obj.issueid else "Post"

             title = f' "{shorten_text(obj.title, 30)}"' if obj.title else ''

             scope_desc = f"in {ctx} " if not scope and not obj.sub_owner else ""

- page += f'=> /{ctx}/{obj.id} {kind}{title} {scope_desc}by {obj.poster_avatar} {obj.poster_name} on {obj.ymd_date()} {" 路 " if obj.tags else ""}{obj.tags}\n'

+ page += f'=> /{ctx}/{obj.id} {kind}{title} {scope_desc}by {obj.poster_avatar} {obj.poster_name} on {obj.ymd_date(tz=session.tz)} {" 路 " if obj.tags else ""}{obj.tags}\n'

             SEGTYPES = ['content', 'URL', 'image', 'attachment', 'poll option']

             if result[2] != Segment.TEXT:

                 page += f'(matching {SEGTYPES[result[2]]}) '

diff --git a/user.py b/user.py

index 12ad553..29490aa 100644

--- a/user.py

+++ b/user.py

@@ -152,8 +152,8 @@ def user_actions(session):

         notifs = db.get_notifications(session.user, include_hidden=True, sort_desc=True)

         cur_ymd = None

         for notif in notifs:

- ymd = notif.ymd_date()

- link, label = notif.entry(show_age=False, with_time=True)

+ ymd = notif.ymd_date(tz=session.tz)

+ link, label = notif.entry(show_age=False, with_time=True, tz=session.tz)

             if cur_ymd != ymd:

                 cur_ymd = ymd

                 page += f'## {ymd}\n\n'

@@ -168,7 +168,7 @@ def user_actions(session):

             page += '\nNo activity.\n'

         for notif in notifs:

             link, label = notif.entry(show_age=False)

- page += f'=> {link} {notif.ymd_date()} {label}\n'

+ page += f'=> {link} {notif.ymd_date(tz=session.tz)} {label}\n'

         return page



     notif = db.get_notification(session.user, notif_id, clear=True)

@@ -242,7 +242,7 @@ def make_dashboard_page(session):

             sub = f' 路 s/{post.sub_name}'

         else:

             sub = ''

- page += f'=> /edit/{post.id} {post.ymd_date()} {post.title_text()}{sub}\n'

+ page += f'=> /edit/{post.id} {post.ymd_date(tz=session.tz)} {post.title_text()}{sub}\n'



 subs = db.get_subspaces(mod=session.user.id)

 n = len(subs)

diff --git a/utils.py b/utils.py

index c0b883c..1b8c5bb 100644

--- a/utils.py

+++ b/utils.py

@@ -169,7 +169,8 @@ def shorten_text(text, n):





def time_delta_text(sec, date_ts, suffix='ago', now='Now',

- date_prefix='', date_fmt='%Y-%m-%d', date_sep=' 路 '):

+ date_prefix='', date_fmt='%Y-%m-%d', date_sep=' 路 ',

+ tz=None):

 if sec < 2:

     return now

 if sec < 60:

@@ -182,6 +183,8 @@ def time_delta_text(sec, date_ts, suffix='ago', now='Now',

     return f'{hours} hour{plural_s(hours)} {suffix}'

 days = round(sec / 3600 / 24)

 dt = datetime.datetime.fromtimestamp(date_ts, UTC)

+ if tz:

+ dt = dt.astimezone(tz)

 age = date_prefix + dt.strftime(date_fmt)

 if days < 14:

     return age + f'{date_sep}{days} day{plural_s(days)} {suffix}'

@@ -195,9 +198,9 @@ def time_delta_text(sec, date_ts, suffix='ago', now='Now',

 return age + f'{date_sep}{years} year{plural_s(years)} {suffix}'





-def ago_text(ts, suffix='ago', now='Now'):

+def ago_text(ts, suffix='ago', now='Now', tz=None):

 sec = max(0, int(time.time()) - ts)

- return time_delta_text(sec, ts, suffix, now)

+ return time_delta_text(sec, ts, suffix, now, tz=tz)





def is_empty_query(req):

diff --git a/worker.py b/worker.py

index 7773beb..0af06cf 100644

--- a/worker.py

+++ b/worker.py

@@ -69,7 +69,7 @@ class Emailer (threading.Thread):

         pending_notifs.append(User(id, name, None, None, None, None, None,

                                    None, enabled_types, email, None,

                                    email_range, None, None, None, None,

- None, None))

+ None, None, None))



     messages = {}

     footer = \

Proxy Information
Original URL
gemini://git.skyjake.fi/bubble/main/pcdiff/374faca7bee892540231982b2b3ab5d36419d3a1
Status Code
Success (20)
Meta
text/plain
Capsule Response Time
29.882552 milliseconds
Gemini-to-HTML Time
5.489607 milliseconds

This content has been proxied by September (ba2dc).