1
0
Fork 0
forked from Mirrors/gomuks

web/rightpanel: add basic user view

This commit is contained in:
Tulir Asokan 2024-11-12 17:58:18 +02:00
parent fe6156302d
commit 24f2e3722d
9 changed files with 166 additions and 8 deletions

View file

@ -77,6 +77,10 @@ func (h *HiClient) handleJSONCommand(ctx context.Context, req *JSONCommand) (any
return unmarshalAndCall(req.Data, func(params *setTypingParams) (bool, error) {
return true, h.SetTyping(ctx, params.RoomID, time.Duration(params.Timeout)*time.Millisecond)
})
case "get_profile":
return unmarshalAndCall(req.Data, func(params *getProfileParams) (*mautrix.RespUserProfile, error) {
return h.Client.GetProfile(ctx, params.UserID)
})
case "get_event":
return unmarshalAndCall(req.Data, func(params *getEventParams) (*database.Event, error) {
return h.GetEvent(ctx, params.RoomID, params.EventID)
@ -194,6 +198,10 @@ type setTypingParams struct {
Timeout int `json:"timeout"`
}
type getProfileParams struct {
UserID id.UserID `json:"user_id"`
}
type getEventParams struct {
RoomID id.RoomID `json:"room_id"`
EventID id.EventID `json:"event_id"`

View file

@ -14,7 +14,7 @@
// 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/>.
import { parseMXC } from "@/util/validation.ts"
import { MemberEventContent, UserID } from "./types"
import { UserID, UserProfile } from "./types"
export const getMediaURL = (mxc?: string, encrypted: boolean = false): string | undefined => {
const [server, mediaID] = parseMXC(mxc)
@ -78,7 +78,7 @@ function getFallbackCharacter(from: unknown, idx: number): string {
return Array.from(from.slice(0, (idx + 1) * 2))[idx]?.toUpperCase().toWellFormed() ?? ""
}
export const getAvatarURL = (userID: UserID, content?: Partial<MemberEventContent>): string | undefined => {
export const getAvatarURL = (userID: UserID, content?: UserProfile | null): string | undefined => {
const fallbackCharacter = getFallbackCharacter(content?.displayname, 0) || getFallbackCharacter(userID, 1)
const backgroundColor = getUserColor(userID)
const [server, mediaID] = parseMXC(content?.avatar_url)

View file

@ -34,6 +34,7 @@ import type {
RoomStateGUID,
TimelineRowID,
UserID,
UserProfile,
} from "./types"
export interface ConnectionEvent {
@ -159,6 +160,10 @@ export default abstract class RPCClient {
return this.request("set_typing", { room_id, timeout })
}
getProfile(user_id: UserID): Promise<UserProfile> {
return this.request("get_profile", { user_id })
}
ensureGroupSessionShared(room_id: RoomID): Promise<boolean> {
return this.request("ensure_group_session_shared", { room_id })
}

View file

@ -62,10 +62,14 @@ export interface EncryptedEventContent {
device_id?: DeviceID
}
export interface MemberEventContent {
membership: "join" | "leave" | "ban" | "invite" | "knock"
export interface UserProfile {
displayname?: string
avatar_url?: ContentURI
[custom: string]: unknown
}
export interface MemberEventContent extends UserProfile {
membership: "join" | "leave" | "ban" | "invite" | "knock"
reason?: string
}

View file

@ -26,8 +26,21 @@ import RoomView from "./roomview/RoomView.tsx"
import { useResizeHandle } from "./util/useResizeHandle.tsx"
import "./MainScreen.css"
function objectIsEqual(a: RightPanelProps | null, b: RightPanelProps | null): boolean {
if (a === null || b === null) {
return a === null && b === null
}
for (const key of Object.keys(a)) {
// @ts-expect-error 3:<
if (a[key] !== b[key]) {
return false
}
}
return true
}
const rpReducer = (prevState: RightPanelProps | null, newState: RightPanelProps | null) => {
if (prevState?.type === newState?.type) {
if (objectIsEqual(prevState, newState)) {
return null
}
return newState

View file

@ -29,9 +29,9 @@ export interface LightboxParams {
alt: string
}
type openLightbox = (params: LightboxParams | React.MouseEvent<HTMLImageElement>) => void
export type OpenLightboxType = (params: LightboxParams | React.MouseEvent<HTMLImageElement>) => void
export const LightboxContext = createContext<openLightbox>(() =>
export const LightboxContext = createContext<OpenLightboxType>(() =>
console.error("Tried to open lightbox without being inside context"))
export const LightboxWrapper = ({ children }: { children: React.ReactNode }) => {

View file

@ -50,3 +50,42 @@ div.right-panel-content.pinned-messages {
margin: auto;
}
}
div.right-panel-content.user {
display: flex;
flex-direction: column;
padding: 1rem;
div.avatar-container {
width: calc((var(--right-panel-width) - 4rem));
height: calc((var(--right-panel-width) - 4rem));
display: flex;
justify-content: center;
align-items: center;
padding: 1rem;
> img {
width: 100%;
height: 100%;
}
}
div.displayname {
font-size: 1.5rem;
font-weight: bold;
text-align: center;
}
div.userid {
text-align: center;
font-family: var(--monospace-font-stack);
}
div.userid, div.displayname {
/* Ensure names aren't too long */
display: -webkit-box;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
word-break: break-word;
}
}

View file

@ -17,6 +17,7 @@ import { JSX, use } from "react"
import type { UserID } from "@/api/types"
import MainScreenContext from "../MainScreenContext.ts"
import PinnedMessages from "./PinnedMessages.tsx"
import UserInfo from "./UserInfo.tsx"
import BackIcon from "@/icons/back.svg?react"
import CloseIcon from "@/icons/close.svg?react"
import "./RightPanel.css"
@ -52,7 +53,7 @@ function renderRightPanelContent(props: RightPanelProps): JSX.Element | null {
case "members":
return <>Member list is not yet implemented</>
case "user":
return <>{props.userID}</>
return <UserInfo userID={props.userID} />
}
}

View file

@ -0,0 +1,88 @@
// gomuks - A Matrix client written in Go.
// Copyright (C) 2024 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// 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/>.
import { use, useEffect, useState } from "react"
import { PuffLoader } from "react-spinners"
import { getAvatarURL } from "@/api/media.ts"
import { useRoomState } from "@/api/statestore"
import { MemberEventContent, UserID, UserProfile } from "@/api/types"
import ClientContext from "../ClientContext.ts"
import { LightboxContext, OpenLightboxType } from "../modal/Lightbox.tsx"
import { RoomContext } from "../roomview/roomcontext.ts"
interface UserInfoProps {
userID: UserID
}
const UserInfo = ({ userID }: UserInfoProps) => {
const roomCtx = use(RoomContext)
const openLightbox = use(LightboxContext)!
const memberEvt = useRoomState(roomCtx?.store, "m.room.member", userID)
const memberEvtContent = memberEvt?.content as MemberEventContent
if (!memberEvtContent) {
return <NonMemberInfo userID={userID}/>
}
return renderUserInfo({ userID, profile: memberEvtContent, error: null, openLightbox })
}
const NonMemberInfo = ({ userID }: UserInfoProps) => {
const openLightbox = use(LightboxContext)!
const client = use(ClientContext)!
const [profile, setProfile] = useState<UserProfile | null>(null)
const [error, setError] = useState<unknown>(null)
useEffect(() => {
client.rpc.getProfile(userID).then(setProfile, setError)
}, [userID, client])
return renderUserInfo({ userID, profile, error, openLightbox })
}
interface RenderUserInfoParams {
userID: UserID
profile: UserProfile | null
error: unknown
openLightbox: OpenLightboxType
}
function renderUserInfo({ userID, profile, error, openLightbox }: RenderUserInfoParams) {
const displayname = profile?.displayname ?? userID
if (profile === null) {
return <>
<div className="avatar-container">
<PuffLoader
color="var(--primary-color)"
size="100%"
className="avatar-loader"
/>
</div>
<div className="displayname">&nbsp;</div>
<div className="userid">{userID}</div>
</>
}
return <>
<div className="avatar-container">
<img
className="avatar"
src={getAvatarURL(userID, profile)}
onClick={openLightbox}
alt=""
/>
</div>
<div className="displayname" title={displayname}>{displayname}</div>
{displayname !== userID ? <div className="userid" title={userID}>{userID}</div> : null}
{error && <div className="error">{`${error}`}</div>}
</>
}
export default UserInfo