More changes

This commit is contained in:
Tulir Asokan 2025-01-03 14:19:04 +02:00
parent e1600b19ff
commit f48a76e373
3 changed files with 50 additions and 46 deletions

View file

@ -86,11 +86,6 @@ export interface PronounSet {
language: string language: string
} }
export interface ExtendedUserProfile extends UserProfile {
"us.cloke.msc4175.tz"?: string
"io.fsky.nyx.pronouns"?: PronounSet[]
}
export type Membership = "join" | "leave" | "ban" | "invite" | "knock" export type Membership = "join" | "leave" | "ban" | "invite" | "knock"
export interface MemberEventContent extends UserProfile { export interface MemberEventContent extends UserProfile {

View file

@ -1,10 +1,11 @@
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import Client from "@/api/client.ts" import Client from "@/api/client.ts"
import { ExtendedUserProfile, PronounSet } from "@/api/types" import { PronounSet, UserProfile } from "@/api/types"
import { ensureArray, ensureString } from "@/util/validation.ts" import { ensureArray, ensureString } from "@/util/validation.ts"
interface ExtendedProfileProps { interface ExtendedProfileProps {
profile: ExtendedUserProfile profile: UserProfile
refreshProfile: () => void
client: Client client: Client
userID: string userID: string
} }
@ -12,6 +13,7 @@ interface ExtendedProfileProps {
interface SetTimezoneProps { interface SetTimezoneProps {
tz?: string tz?: string
client: Client client: Client
refreshProfile: () => void
} }
const getCurrentTimezone = () => new Intl.DateTimeFormat().resolvedOptions().timeZone const getCurrentTimezone = () => new Intl.DateTimeFormat().resolvedOptions().timeZone
@ -30,7 +32,7 @@ const currentTimeAdjusted = (tz: string) => {
} }
} }
function ClockElement({ tz }: { tz: string }) { const ClockElement = ({ tz }: { tz: string }) => {
const [time, setTime] = useState(currentTimeAdjusted(tz)) const [time, setTime] = useState(currentTimeAdjusted(tz))
useEffect(() => { useEffect(() => {
let interval: number | undefined let interval: number | undefined
@ -41,29 +43,37 @@ function ClockElement({ tz }: { tz: string }) {
}, (1001 - Date.now() % 1000)) }, (1001 - Date.now() % 1000))
return () => interval ? clearInterval(interval) : clearTimeout(timeout) return () => interval ? clearInterval(interval) : clearTimeout(timeout)
}, [tz]) }, [tz])
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),
)
}
}
// TODO: You are unable to set a timezone if you do not already have one set in your profile.
// The defaulting to the current timezone causes `newTz !== tz` to never be true when the user has
// no timezone set.
return <> return <>
<div title={tz}>Time:</div>
<div title={tz}>{time}</div>
</>
}
const SetTimeZoneElement = ({ tz, client, refreshProfile }: SetTimezoneProps) => {
const zones = Intl.supportedValuesOf("timeZone")
const saveTz = (newTz: string) => {
if (!zones.includes(newTz)) {
return
}
client.rpc.setProfileField("us.cloke.msc4175.tz", newTz).then(
() => refreshProfile(),
err => {
console.error("Failed to set time zone:", err)
window.alert(`Failed to set time zone: ${err}`)
},
)
}
return <>
<label htmlFor="userprofile-timezone-input">Set time zone:</label>
<input <input
list="timezones" list="timezones"
className="text-input" className="text-input"
id="userprofile-timezone-input"
defaultValue={tz || getCurrentTimezone()} defaultValue={tz || getCurrentTimezone()}
onChange={(e) => setTz(e.currentTarget.value)} onKeyDown={evt => evt.key === "Enter" && saveTz(evt.currentTarget.value)}
onBlur={evt => evt.currentTarget.value !== tz && saveTz(evt.currentTarget.value)}
/> />
<datalist id="timezones"> <datalist id="timezones">
{zones.map((zone) => <option key={zone} value={zone} />)} {zones.map((zone) => <option key={zone} value={zone} />)}
@ -72,7 +82,7 @@ function SetTimezoneElement({ tz, client }: SetTimezoneProps) {
} }
export default function UserExtendedProfile({ profile, client, userID }: ExtendedProfileProps) { const UserExtendedProfile = ({ profile, refreshProfile, client, userID }: ExtendedProfileProps)=> {
if (!profile) { if (!profile) {
return null return null
} }
@ -86,24 +96,19 @@ export default function UserExtendedProfile({ profile, client, userID }: Extende
// otherwise there's an ugly and pointless <hr/> for no real reason. // otherwise there's an ugly and pointless <hr/> for no real reason.
const pronouns = ensureArray(profile["io.fsky.nyx.pronouns"]) as PronounSet[] const pronouns = ensureArray(profile["io.fsky.nyx.pronouns"]) as PronounSet[]
const userTimezone = ensureString(profile["us.cloke.msc4175.tz"]) const userTimeZone = ensureString(profile["us.cloke.msc4175.tz"])
return <> return <>
<hr/> <hr/>
<div className="extended-profile"> <div className="extended-profile">
{userTimezone && <> {userTimeZone && <ClockElement tz={userTimeZone} />}
<div title={userTimezone}>Time:</div> {userID === client.userID &&
<ClockElement tz={userTimezone} /> <SetTimeZoneElement tz={userTimeZone} client={client} refreshProfile={refreshProfile} />}
</>} {pronouns.length > 0 && <>
{userID === client.userID && <>
<div>Set Timezone:</div>
<SetTimezoneElement tz={userTimezone} client={client} />
</>}
{pronouns.length >= 1 && <>
<div>Pronouns:</div> <div>Pronouns:</div>
<div> <div>{pronouns.map(pronounSet => ensureString(pronounSet.summary)).join(" / ")}</div>
{pronouns.map((pronounSet: PronounSet) => (pronounSet.summary)).join("/")}
</div>
</>} </>}
</div> </div>
</> </>
} }
export default UserExtendedProfile

View file

@ -13,7 +13,7 @@
// //
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import { use, useEffect, useState } from "react" import { use, useCallback, useEffect, useState } from "react"
import { PuffLoader } from "react-spinners" import { PuffLoader } from "react-spinners"
import { getAvatarURL } from "@/api/media.ts" import { getAvatarURL } from "@/api/media.ts"
import { useRoomMember } from "@/api/statestore" import { useRoomMember } from "@/api/statestore"
@ -39,15 +39,17 @@ const UserInfo = ({ userID }: UserInfoProps) => {
const member = (memberEvt?.content ?? null) as MemberEventContent | null const member = (memberEvt?.content ?? null) as MemberEventContent | null
const [globalProfile, setGlobalProfile] = useState<UserProfile | null>(null) const [globalProfile, setGlobalProfile] = useState<UserProfile | null>(null)
const [errors, setErrors] = useState<string[] | null>(null) const [errors, setErrors] = useState<string[] | null>(null)
useEffect(() => { const refreshProfile = useCallback((clearState = false) => {
setErrors(null) if (clearState) {
setGlobalProfile(null) setErrors(null)
setGlobalProfile(null)
}
client.rpc.getProfile(userID).then( client.rpc.getProfile(userID).then(
setGlobalProfile, setGlobalProfile,
err => setErrors([`${err}`]), err => setErrors([`${err}`]),
) )
}, [roomCtx, userID, client]) }, [userID, client])
useEffect(() => refreshProfile(true), [refreshProfile])
const displayname = member?.displayname || globalProfile?.displayname || getLocalpart(userID) const displayname = member?.displayname || globalProfile?.displayname || getLocalpart(userID)
return <> return <>
@ -65,7 +67,9 @@ 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 && <UserExtendedProfile profile={globalProfile} client={client} userID={userID}/>} {globalProfile && <UserExtendedProfile
profile={globalProfile} refreshProfile={refreshProfile} client={client} userID={userID}
/>}
<hr/> <hr/>
{userID !== client.userID && <> {userID !== client.userID && <>
<MutualRooms client={client} userID={userID}/> <MutualRooms client={client} userID={userID}/>