mirror of
https://github.com/tulir/gomuks.git
synced 2025-04-20 10:33:41 -05:00
Enable setting timezone
This commit is contained in:
parent
f5e2584754
commit
9a70af64ce
6 changed files with 120 additions and 37 deletions
|
@ -86,6 +86,10 @@ func (h *HiClient) handleJSONCommand(ctx context.Context, req *JSONCommand) (any
|
||||||
return unmarshalAndCall(req.Data, func(params *getProfileParams) (*mautrix.RespUserProfile, error) {
|
return unmarshalAndCall(req.Data, func(params *getProfileParams) (*mautrix.RespUserProfile, error) {
|
||||||
return h.Client.GetProfile(ctx, params.UserID)
|
return h.Client.GetProfile(ctx, params.UserID)
|
||||||
})
|
})
|
||||||
|
case "set_profile_field":
|
||||||
|
return unmarshalAndCall(req.Data, func(params *setProfileFieldParams) (bool, error) {
|
||||||
|
return true, h.Client.UnstableSetProfileField(ctx, params.Field, params.Value)
|
||||||
|
})
|
||||||
case "get_mutual_rooms":
|
case "get_mutual_rooms":
|
||||||
return unmarshalAndCall(req.Data, func(params *getProfileParams) ([]id.RoomID, error) {
|
return unmarshalAndCall(req.Data, func(params *getProfileParams) ([]id.RoomID, error) {
|
||||||
return h.GetMutualRooms(ctx, params.UserID)
|
return h.GetMutualRooms(ctx, params.UserID)
|
||||||
|
@ -273,6 +277,11 @@ type getProfileParams struct {
|
||||||
UserID id.UserID `json:"user_id"`
|
UserID id.UserID `json:"user_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type setProfileFieldParams struct {
|
||||||
|
Field string `json:"field"`
|
||||||
|
Value any `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
type getEventParams struct {
|
type getEventParams struct {
|
||||||
RoomID id.RoomID `json:"room_id"`
|
RoomID id.RoomID `json:"room_id"`
|
||||||
EventID id.EventID `json:"event_id"`
|
EventID id.EventID `json:"event_id"`
|
||||||
|
|
|
@ -39,7 +39,7 @@ import type {
|
||||||
RoomSummary,
|
RoomSummary,
|
||||||
TimelineRowID,
|
TimelineRowID,
|
||||||
UserID,
|
UserID,
|
||||||
UserProfile,
|
UserProfile, JSONValue,
|
||||||
} from "./types"
|
} from "./types"
|
||||||
|
|
||||||
export interface ConnectionEvent {
|
export interface ConnectionEvent {
|
||||||
|
@ -180,6 +180,10 @@ export default abstract class RPCClient {
|
||||||
return this.request("get_profile", { user_id })
|
return this.request("get_profile", { user_id })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setProfileField(field: string, value: JSONValue): Promise<boolean> {
|
||||||
|
return this.request("set_profile_field", { field, value })
|
||||||
|
}
|
||||||
|
|
||||||
getMutualRooms(user_id: UserID): Promise<RoomID[]> {
|
getMutualRooms(user_id: UserID): Promise<RoomID[]> {
|
||||||
return this.request("get_mutual_rooms", { user_id })
|
return this.request("get_mutual_rooms", { user_id })
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,14 @@ export type RoomVersion = "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" |
|
||||||
export type RoomType = "" | "m.space"
|
export type RoomType = "" | "m.space"
|
||||||
export type RelationType = "m.annotation" | "m.reference" | "m.replace" | "m.thread"
|
export type RelationType = "m.annotation" | "m.reference" | "m.replace" | "m.thread"
|
||||||
|
|
||||||
|
export type JSONValue =
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| null
|
||||||
|
| JSONValue[]
|
||||||
|
| {[key: string]: JSONValue}
|
||||||
|
|
||||||
export interface RoomPredecessor {
|
export interface RoomPredecessor {
|
||||||
room_id: RoomID
|
room_id: RoomID
|
||||||
event_id: EventID
|
event_id: EventID
|
||||||
|
|
|
@ -72,6 +72,13 @@ div.right-panel-content.user {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input.text-input {
|
||||||
|
border: 0;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-bottom: 1px solid var(--blockquote-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
div.displayname {
|
div.displayname {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
@ -92,15 +99,10 @@ div.right-panel-content.user {
|
||||||
}
|
}
|
||||||
|
|
||||||
div.extended-profile {
|
div.extended-profile {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.25rem;
|
|
||||||
|
|
||||||
div.profile-row {
|
|
||||||
display: grid;
|
display: grid;
|
||||||
|
gap: 0.25rem;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import { UserProfile } from "@/api/types"
|
import { UserProfile } from "@/api/types"
|
||||||
import { ensureArray } from "@/util/validation.ts"
|
import { ensureArray } from "@/util/validation.ts"
|
||||||
|
import Client from "@/api/client.ts";
|
||||||
|
|
||||||
interface PronounSet {
|
interface PronounSet {
|
||||||
subject: string
|
subject: string
|
||||||
|
@ -18,13 +19,25 @@ interface ExtendedProfileAttributes {
|
||||||
|
|
||||||
interface ExtendedProfileProps {
|
interface ExtendedProfileProps {
|
||||||
profile: UserProfile & ExtendedProfileAttributes
|
profile: UserProfile & ExtendedProfileAttributes
|
||||||
|
client: Client
|
||||||
|
userID: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SetTimezoneProps {
|
||||||
|
tz?: string
|
||||||
|
client: Client
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCurrentTimezone = () => Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||||
|
|
||||||
const currentTimeAdjusted = (tz: string) => {
|
const currentTimeAdjusted = (tz: string) => {
|
||||||
const lang = navigator.language || "en-US"
|
const lang = navigator.language || "en-US"
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
|
try {
|
||||||
return new Intl.DateTimeFormat(lang, { timeStyle: "long", timeZone: tz }).format(now)
|
return new Intl.DateTimeFormat(lang, { timeStyle: "long", timeZone: tz }).format(now)
|
||||||
|
} catch (e) {
|
||||||
|
return `Error: ${e}`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ClockElement({ tz }: { tz: string }) {
|
function ClockElement({ tz }: { tz: string }) {
|
||||||
|
@ -38,24 +51,68 @@ function ClockElement({ tz }: { tz: string }) {
|
||||||
return <div>{time}</div>
|
return <div>{time}</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function SetTimezoneElement({ tz, client }: SetTimezoneProps) {
|
||||||
|
const zones = Intl.supportedValuesOf("timeZone")
|
||||||
|
const setTz = (newTz: string) => {
|
||||||
|
if (zones.includes(newTz) && newTz !== tz) {
|
||||||
|
return client.rpc.setProfileField("us.cloke.msc4175.tz", newTz).then(
|
||||||
|
() => client.rpc.getProfile(client.userID),
|
||||||
|
(err) => console.error("Error setting timezone", err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default function UserExtendedProfile({ profile }: ExtendedProfileProps) {
|
return (
|
||||||
|
<>
|
||||||
|
<input
|
||||||
|
list={"timezones"}
|
||||||
|
className={"text-input"}
|
||||||
|
defaultValue={tz || getCurrentTimezone()}
|
||||||
|
onChange={(e) => setTz(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<datalist id={"timezones"}>
|
||||||
|
{
|
||||||
|
zones.map((zone) => <option key={zone} value={zone} />)
|
||||||
|
}
|
||||||
|
</datalist>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default function UserExtendedProfile({ profile, client, userID }: ExtendedProfileProps) {
|
||||||
if (!profile) return null
|
if (!profile) return null
|
||||||
|
|
||||||
|
const extendedProfileKeys = ["us.cloke.msc4175.tz", "io.fsky.nyx.pronouns"]
|
||||||
|
const hasExtendedProfile = extendedProfileKeys.some((key) => key in profile)
|
||||||
|
if (!hasExtendedProfile && client.userID !== userID) return null
|
||||||
|
// Explicitly only return something if the profile has the keys we're looking for.
|
||||||
|
// otherwise there's an ugly and pointless <hr/> for no real reason.
|
||||||
|
|
||||||
const pronouns: PronounSet[] = ensureArray(profile["io.fsky.nyx.pronouns"]) as PronounSet[]
|
const pronouns: PronounSet[] = ensureArray(profile["io.fsky.nyx.pronouns"]) as PronounSet[]
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<hr/>
|
||||||
<div className={"extended-profile"}>
|
<div className={"extended-profile"}>
|
||||||
{
|
{
|
||||||
profile["us.cloke.msc4175.tz"] && (
|
profile["us.cloke.msc4175.tz"] && (
|
||||||
<div className={"profile-row"}>
|
<>
|
||||||
<div title={profile["us.cloke.msc4175.tz"]}>Time:</div>
|
<div title={profile["us.cloke.msc4175.tz"]}>Time:</div>
|
||||||
<ClockElement tz={profile["us.cloke.msc4175.tz"]} />
|
<ClockElement tz={profile["us.cloke.msc4175.tz"]} />
|
||||||
</div>
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
userID === client.userID && (
|
||||||
|
<>
|
||||||
|
<div>Set Timezone:</div>
|
||||||
|
<SetTimezoneElement tz={profile["us.cloke.msc4175.tz"]} client={client} />
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
pronouns.length >= 1 && (
|
pronouns.length >= 1 && (
|
||||||
<div className={"profile-row"}>
|
<>
|
||||||
<div>Pronouns:</div>
|
<div>Pronouns:</div>
|
||||||
<div>
|
<div>
|
||||||
{
|
{
|
||||||
|
@ -66,9 +123,10 @@ export default function UserExtendedProfile({ profile }: ExtendedProfileProps) {
|
||||||
).join("/")
|
).join("/")
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,6 +47,8 @@ const UserInfo = ({ userID }: UserInfoProps) => {
|
||||||
err => setErrors([`${err}`]),
|
err => setErrors([`${err}`]),
|
||||||
)
|
)
|
||||||
}, [roomCtx, userID, client])
|
}, [roomCtx, userID, client])
|
||||||
|
|
||||||
|
|
||||||
const displayname = member?.displayname || globalProfile?.displayname || getLocalpart(userID)
|
const displayname = member?.displayname || globalProfile?.displayname || getLocalpart(userID)
|
||||||
return <>
|
return <>
|
||||||
<div className="avatar-container">
|
<div className="avatar-container">
|
||||||
|
@ -63,7 +65,7 @@ const UserInfo = ({ userID }: UserInfoProps) => {
|
||||||
</div>
|
</div>
|
||||||
<div className="displayname" title={displayname}>{displayname}</div>
|
<div className="displayname" title={displayname}>{displayname}</div>
|
||||||
<div className="userid" title={userID}>{userID}</div>
|
<div className="userid" title={userID}>{userID}</div>
|
||||||
{globalProfile && <><hr/><UserExtendedProfile profile={globalProfile}/></>}
|
{globalProfile && <UserExtendedProfile profile={globalProfile} client={client} userID={userID}/>}
|
||||||
<hr/>
|
<hr/>
|
||||||
{userID !== client.userID && <>
|
{userID !== client.userID && <>
|
||||||
<MutualRooms client={client} userID={userID}/>
|
<MutualRooms client={client} userID={userID}/>
|
||||||
|
|
Loading…
Add table
Reference in a new issue