diff --git a/backend/db/user.go b/backend/db/user.go index dd222a9..7f6834e 100644 --- a/backend/db/user.go +++ b/backend/db/user.go @@ -555,9 +555,10 @@ func (db *DB) UpdateUser( memberTitle *string, listPrivate *bool, links *[]string, avatar *string, + timezone *string, customPreferences *CustomPreferences, ) (u User, err error) { - if displayName == nil && bio == nil && links == nil && avatar == nil && memberTitle == nil && listPrivate == nil && customPreferences == nil { + if displayName == nil && bio == nil && links == nil && avatar == nil && memberTitle == nil && listPrivate == nil && timezone == nil && customPreferences == nil { sql, args, err := sq.Select("*").From("users").Where("id = ?", id).ToSql() if err != nil { return u, errors.Wrap(err, "building sql") @@ -593,6 +594,13 @@ func (db *DB) UpdateUser( builder = builder.Set("member_title", *memberTitle) } } + if timezone != nil { + if *timezone == "" { + builder = builder.Set("timezone", nil) + } else { + builder = builder.Set("timezone", *timezone) + } + } if links != nil { builder = builder.Set("links", *links) } diff --git a/backend/routes/user/patch_user.go b/backend/routes/user/patch_user.go index a5c634c..a07f6e9 100644 --- a/backend/routes/user/patch_user.go +++ b/backend/routes/user/patch_user.go @@ -25,6 +25,7 @@ type PatchUserRequest struct { Pronouns *[]db.PronounEntry `json:"pronouns"` Fields *[]db.Field `json:"fields"` Avatar *string `json:"avatar"` + Timezone *string `json:"timezone"` ListPrivate *bool `json:"list_private"` CustomPreferences *db.CustomPreferences `json:"custom_preferences"` Flags *[]xid.ID `json:"flags"` @@ -91,6 +92,19 @@ func (s *Server) patchUser(w http.ResponseWriter, r *http.Request) error { } } + // validate timezone + if req.Timezone != nil { + if *req.Timezone != "" { + _, err := time.LoadLocation(*req.Timezone) + if err != nil { + return server.APIError{ + Code: server.ErrBadRequest, + Details: fmt.Sprintf("%q is not a valid timezone", *req.Timezone), + } + } + } + } + // validate links if req.Links != nil { if len(*req.Links) > db.MaxUserLinksLength { @@ -224,7 +238,7 @@ func (s *Server) patchUser(w http.ResponseWriter, r *http.Request) error { } } - u, err = s.DB.UpdateUser(ctx, tx, claims.UserID, req.DisplayName, req.Bio, req.MemberTitle, req.ListPrivate, req.Links, avatarHash, req.CustomPreferences) + u, err = s.DB.UpdateUser(ctx, tx, claims.UserID, req.DisplayName, req.Bio, req.MemberTitle, req.ListPrivate, req.Links, avatarHash, req.Timezone, req.CustomPreferences) if err != nil && errors.Cause(err) != db.ErrNothingToUpdate { log.Errorf("updating user: %v", err) return err diff --git a/frontend/src/lib/api/entities.ts b/frontend/src/lib/api/entities.ts index 523cc0e..d4e6b7a 100644 --- a/frontend/src/lib/api/entities.ts +++ b/frontend/src/lib/api/entities.ts @@ -57,6 +57,7 @@ export interface MeUser extends User { fediverse_instance: string | null; list_private: boolean; last_sid_reroll: string; + timezone: string | null; } export interface Field { diff --git a/frontend/src/routes/edit/profile/+page.svelte b/frontend/src/routes/edit/profile/+page.svelte index da01fe3..b4b7673 100644 --- a/frontend/src/routes/edit/profile/+page.svelte +++ b/frontend/src/routes/edit/profile/+page.svelte @@ -21,14 +21,16 @@ CardBody, CardHeader, FormGroup, + InputGroup, Icon, Input, Popover, TabContent, TabPane, + InputGroupText, } from "sveltestrap"; import { encode } from "base64-arraybuffer"; - import { DateTime } from "luxon"; + import { DateTime, FixedOffsetZone } from "luxon"; import { apiFetchClient } from "$lib/api/fetch"; import { PUBLIC_SHORT_BASE } from "$env/static/public"; import IconButton from "$lib/components/IconButton.svelte"; @@ -60,6 +62,7 @@ let flags: PrideFlag[] = window.structuredClone(data.user.flags); let list_private = data.user.list_private; let custom_preferences = window.structuredClone(data.user.custom_preferences); + let timezone = data.user.timezone; let avatar: string | null; let avatar_files: FileList | null; @@ -98,6 +101,7 @@ member_title, list_private, custom_preferences, + timezone, ); $: getAvatar(avatar_files).then((b64) => (avatar = b64)); @@ -114,6 +118,7 @@ member_title: string, list_private: boolean, custom_preferences: CustomPreferences, + timezone: string | null, ) => { if (bio !== (user.bio || "")) return true; if (display_name !== (user.display_name || "")) return true; @@ -126,6 +131,7 @@ if (!customPreferencesEqual(custom_preferences, user.custom_preferences)) return true; if (avatar !== null) return true; if (list_private !== user.list_private) return true; + if (timezone !== user.timezone) return true; return false; }; @@ -208,6 +214,24 @@ return uri; }; + let currentTime = ""; + let displayTimezone = ""; + $: setTime(timezone); + + const setTime = (timezone: string | null) => { + if (!timezone) { + currentTime = ""; + displayTimezone = ""; + return; + } + + const offset = DateTime.now().setZone(timezone).offset; + const zone = FixedOffsetZone.instance(offset); + + currentTime = now.setZone(zone).toLocaleString(DateTime.TIME_SIMPLE); + displayTimezone = zone.formatOffset(now.toUnixInteger(), "narrow"); + }; + const moveName = (index: number, up: boolean) => { if (up && index == 0) return; if (!up && index == names.length - 1) return; @@ -361,6 +385,7 @@ fields, member_title, list_private, + timezone: timezone || "", custom_preferences, flags: flags.map((flag) => flag.id), }); @@ -403,6 +428,10 @@ addToast({ body: "Copied the short link to your clipboard!", duration: 2000 }); }; + const detectTimezone = () => { + timezone = DateTime.local().zoneName; + }; + interface SnapshotData { bio: string; display_name: string; @@ -413,6 +442,7 @@ fields: Field[]; flags: PrideFlag[]; list_private: boolean; + timezone: string | null; custom_preferences: CustomPreferences; avatar: string | null; @@ -432,6 +462,7 @@ fields, flags, list_private, + timezone, custom_preferences, avatar, newName, @@ -448,6 +479,7 @@ fields = value.fields; flags = value.flags; list_private = value.list_private; + timezone = value.timezone; custom_preferences = value.custom_preferences; avatar = value.avatar; newName = value.newName; @@ -787,6 +819,31 @@ pronouns.cc/@{data.user.name}/[member-name].

+
+
+

+ You can optionally set your timezone, which will show your current local time on your + profile. +

+ + + + + + +

+ {#if timezone} + This will show up on your profile like this: + + {currentTime} (UTC{displayTimezone}) +
+ {/if} + + Your timezone is never shared directly, only the difference between UTC and your + current timezone is. + +

+
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 7d8ed95..5f528ea 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -5,6 +5,7 @@ import { plugin as markdown, Mode as MarkdownMode } from "vite-plugin-markdown"; export default defineConfig({ plugins: [sveltekit(), markdown({ mode: [MarkdownMode.HTML] })], server: { + host: "127.0.0.1", proxy: { "/api": { target: "http://localhost:8080", diff --git a/scripts/seeddb/main.go b/scripts/seeddb/main.go index 3755617..d18b354 100644 --- a/scripts/seeddb/main.go +++ b/scripts/seeddb/main.go @@ -48,7 +48,7 @@ func run(c *cli.Context) error { return err } - _, err = pg.UpdateUser(ctx, tx, u.ID, ptr("testing"), ptr("This is a bio!"), nil, ptr(false), &[]string{"https://pronouns.cc"}, nil, nil) + _, err = pg.UpdateUser(ctx, tx, u.ID, ptr("testing"), ptr("This is a bio!"), nil, ptr(false), &[]string{"https://pronouns.cc"}, nil, nil, nil) if err != nil { fmt.Println("error setting user info:", err) return err