diff --git a/pkg/hicli/json-commands.go b/pkg/hicli/json-commands.go index 0a07c25..b719234 100644 --- a/pkg/hicli/json-commands.go +++ b/pkg/hicli/json-commands.go @@ -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 h.Client.GetProfile(ctx, params.UserID) }) + case "get_presence": + return unmarshalAndCall(req.Data, func(params *getProfileParams) (*mautrix.RespPresence, error) { + return h.Client.GetPresence(ctx, params.UserID) + }) case "get_mutual_rooms": return unmarshalAndCall(req.Data, func(params *getProfileParams) ([]id.RoomID, error) { return h.GetMutualRooms(ctx, params.UserID) diff --git a/web/src/api/rpc.ts b/web/src/api/rpc.ts index 7f9004f..2dd3f09 100644 --- a/web/src/api/rpc.ts +++ b/web/src/api/rpc.ts @@ -25,6 +25,7 @@ import type { Mentions, MessageEventContent, PaginationResponse, + Presence, ProfileEncryptionInfo, RPCCommand, RPCEvent, @@ -180,6 +181,10 @@ export default abstract class RPCClient { return this.request("get_profile", { user_id }) } + getPresence(user_id: UserID): Promise { + return this.request("get_presence", { user_id }) + } + getMutualRooms(user_id: UserID): Promise { return this.request("get_mutual_rooms", { user_id }) } diff --git a/web/src/api/types/mxtypes.ts b/web/src/api/types/mxtypes.ts index 78e6146..b8f7e89 100644 --- a/web/src/api/types/mxtypes.ts +++ b/web/src/api/types/mxtypes.ts @@ -68,6 +68,15 @@ export interface UserProfile { [custom: string]: unknown } +export type PresenceState = "online" | "offline" | "unavailable" + +export interface Presence { + currently_active?: boolean + last_active_ago?: number + presence: PresenceState + status_msg?: string | null +} + export type Membership = "join" | "leave" | "ban" | "invite" | "knock" export interface MemberEventContent extends UserProfile { diff --git a/web/src/ui/rightpanel/RightPanel.css b/web/src/ui/rightpanel/RightPanel.css index 76be03f..91a9b2e 100644 --- a/web/src/ui/rightpanel/RightPanel.css +++ b/web/src/ui/rightpanel/RightPanel.css @@ -83,6 +83,20 @@ div.right-panel-content.user { font-family: var(--monospace-font-stack); } + div.presence { + text-align: center; + font-size: 1.125rem; + + } + + div.statusmessage { + text-align: center; + font-size: 1rem; + font-style: italic; + /* Wrap words that are so long that they spill out of bounds */ + word-wrap: break-word; + } + div.userid, div.displayname { /* Ensure names aren't too long */ display: -webkit-box; diff --git a/web/src/ui/rightpanel/UserInfo.tsx b/web/src/ui/rightpanel/UserInfo.tsx index 37e1313..0613130 100644 --- a/web/src/ui/rightpanel/UserInfo.tsx +++ b/web/src/ui/rightpanel/UserInfo.tsx @@ -17,7 +17,7 @@ import { use, useEffect, useState } from "react" import { PuffLoader } from "react-spinners" import { getAvatarURL } from "@/api/media.ts" import { useRoomMember } from "@/api/statestore" -import { MemberEventContent, UserID, UserProfile } from "@/api/types" +import { MemberEventContent, UserID, UserProfile, Presence } from "@/api/types" import { getLocalpart } from "@/util/validation.ts" import ClientContext from "../ClientContext.ts" import { LightboxContext } from "../modal/Lightbox.tsx" @@ -30,6 +30,12 @@ interface UserInfoProps { userID: UserID } +const PresenceEmojis = { + online: "🟢", + offline: "⚫", + unavailable: "🔴", +} + const UserInfo = ({ userID }: UserInfoProps) => { const client = use(ClientContext)! const roomCtx = use(RoomContext) @@ -37,6 +43,7 @@ const UserInfo = ({ userID }: UserInfoProps) => { const memberEvt = useRoomMember(client, roomCtx?.store, userID) const member = (memberEvt?.content ?? null) as MemberEventContent | null const [globalProfile, setGlobalProfile] = useState(null) + const [presence, setPresence] = useState({ presence: "offline"}) const [errors, setErrors] = useState(null) useEffect(() => { setErrors(null) @@ -45,6 +52,10 @@ const UserInfo = ({ userID }: UserInfoProps) => { setGlobalProfile, err => setErrors([`${err}`]), ) + client.rpc.getPresence(userID).then( + setPresence, + err => setErrors([`${err}`]), + ) }, [roomCtx, userID, client]) const displayname = member?.displayname || globalProfile?.displayname || getLocalpart(userID) @@ -63,6 +74,17 @@ const UserInfo = ({ userID }: UserInfoProps) => {
{displayname}
{userID}
+ {presence && ( + <> +
{PresenceEmojis[presence.presence]} {presence.presence}
+ { + presence.status_msg?.length && ( +
{presence.status_msg}
+ ) + } + + ) + }
{userID !== client.userID && <>