diff --git a/backend/db/user.go b/backend/db/user.go index ccb0965..dd222a9 100644 --- a/backend/db/user.go +++ b/backend/db/user.go @@ -52,6 +52,7 @@ type User struct { IsAdmin bool ListPrivate bool LastSIDReroll time.Time `db:"last_sid_reroll"` + Timezone *string DeletedAt *time.Time SelfDelete *bool @@ -113,6 +114,21 @@ func (u User) NumProviders() (numProviders int) { return numProviders } +// UTCOffset returns the user's UTC offset in seconds. If the user does not have a timezone set, `ok` is false. +func (u User) UTCOffset() (offset int, ok bool) { + if u.Timezone == nil { + return 0, false + } + + loc, err := time.LoadLocation(*u.Timezone) + if err != nil { + return 0, false + } + + _, offset = time.Now().In(loc).Zone() + return offset, true +} + type Badge int32 const ( diff --git a/backend/routes/user/get_user.go b/backend/routes/user/get_user.go index f6a9aae..d1163a3 100644 --- a/backend/routes/user/get_user.go +++ b/backend/routes/user/get_user.go @@ -28,12 +28,14 @@ type GetUserResponse struct { CustomPreferences db.CustomPreferences `json:"custom_preferences"` Flags []db.UserFlag `json:"flags"` Badges db.Badge `json:"badges"` + UTCOffset *int `json:"utc_offset"` } type GetMeResponse struct { GetUserResponse CreatedAt time.Time `json:"created_at"` + Timezone *string `json:"timezone"` MaxInvites int `json:"max_invites"` IsAdmin bool `json:"is_admin"` @@ -87,6 +89,10 @@ func dbUserToResponse(u db.User, fields []db.Field, members []db.Member, flags [ resp.Badges |= db.BadgeAdmin } + if offset, ok := u.UTCOffset(); ok { + resp.UTCOffset = &offset + } + resp.Members = make([]PartialMember, len(members)) for i := range members { resp.Members[i] = PartialMember{ @@ -195,6 +201,7 @@ func (s *Server) getMeUser(w http.ResponseWriter, r *http.Request) error { render.JSON(w, r, GetMeResponse{ GetUserResponse: dbUserToResponse(u, fields, members, flags), CreatedAt: u.ID.Time(), + Timezone: u.Timezone, MaxInvites: u.MaxInvites, IsAdmin: u.IsAdmin, ListPrivate: u.ListPrivate, diff --git a/backend/routes/user/patch_user.go b/backend/routes/user/patch_user.go index 716dcca..a5c634c 100644 --- a/backend/routes/user/patch_user.go +++ b/backend/routes/user/patch_user.go @@ -311,6 +311,8 @@ func (s *Server) patchUser(w http.ResponseWriter, r *http.Request) error { // echo the updated user back on success render.JSON(w, r, GetMeResponse{ GetUserResponse: dbUserToResponse(u, fields, nil, flags), + CreatedAt: u.ID.Time(), + Timezone: u.Timezone, MaxInvites: u.MaxInvites, IsAdmin: u.IsAdmin, ListPrivate: u.ListPrivate, diff --git a/frontend/src/lib/api/entities.ts b/frontend/src/lib/api/entities.ts index 61c5c92..523cc0e 100644 --- a/frontend/src/lib/api/entities.ts +++ b/frontend/src/lib/api/entities.ts @@ -14,6 +14,7 @@ export interface User { links: string[]; member_title: string | null; badges: number; + utc_offset: number | null; names: FieldEntry[]; pronouns: Pronoun[]; diff --git a/frontend/src/routes/@[username]/+page.svelte b/frontend/src/routes/@[username]/+page.svelte index a181ee7..0492ad2 100644 --- a/frontend/src/routes/@[username]/+page.svelte +++ b/frontend/src/routes/@[username]/+page.svelte @@ -13,7 +13,9 @@ Modal, ModalBody, ModalFooter, + Tooltip, } from "sveltestrap"; + import { DateTime, Duration, FixedOffsetZone, Zone } from "luxon"; import FieldCard from "$lib/components/FieldCard.svelte"; import PronounLink from "$lib/components/PronounLink.svelte"; import PartialMemberCard from "$lib/components/PartialMemberCard.svelte"; @@ -77,6 +79,24 @@ let memberNameValid = true; $: memberNameValid = memberNameRegex.test(newMemberName); + let currentTime: string | null; + let timezone: string | null; + $: setTime(data.utc_offset); + + const setTime = (offset: number | null) => { + if (!offset) { + currentTime = null; + timezone = null; + return; + } + + const now = DateTime.now(); + const zone = FixedOffsetZone.instance(offset / 60); + + currentTime = now.setZone(zone).toLocaleString(DateTime.TIME_SIMPLE); + timezone = zone.formatOffset(now.toUnixInteger(), "narrow"); + }; + const createMember = async () => { try { const member = await apiFetchClient("/members", "POST", { @@ -168,6 +188,10 @@ {:else}

@{data.name}

{/if} + {#if data.utc_offset} + Current time + {currentTime} (UTC{timezone}) + {/if} {#if profileEmpty && $userStore?.id === data.id}

diff --git a/scripts/migrate/019_timezones.sql b/scripts/migrate/019_timezones.sql new file mode 100644 index 0000000..f45c69c --- /dev/null +++ b/scripts/migrate/019_timezones.sql @@ -0,0 +1,5 @@ +-- +migrate Up + +-- 2023-07-30: Add user timezones + +alter table users add column timezone text null;