1
0
Fork 0
forked from Mirrors/gomuks

web/rightpanel: improve user info view slightly

This commit is contained in:
Tulir Asokan 2024-12-06 01:11:32 +02:00
parent 1cc39d40c9
commit 5b9f458b75
4 changed files with 175 additions and 123 deletions

View file

@ -132,7 +132,11 @@ div.right-panel-content.user {
}
}
div.devices > ul {
div.devices > details > summary > h4 {
display: inline-block;
}
div.devices > details > ul {
list-style-type: none;
padding: 0;
margin: 0;

View file

@ -13,20 +13,18 @@
//
// 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, useMemo, useReducer, useState } from "react"
import { use, useEffect, useState } from "react"
import { PuffLoader, ScaleLoader } from "react-spinners"
import Client from "@/api/client.ts"
import { getAvatarURL } from "@/api/media.ts"
import { RoomListEntry, RoomStateStore, useRoomState } from "@/api/statestore"
import { MemberEventContent, ProfileView, ProfileViewDevice, RoomID, TrustState, UserID } from "@/api/types"
import { RoomStateStore, useRoomState } from "@/api/statestore"
import { MemberEventContent, ProfileView, UserID } from "@/api/types"
import { getLocalpart } from "@/util/validation.ts"
import ClientContext from "../ClientContext.ts"
import { LightboxContext } from "../modal/Lightbox.tsx"
import ListEntry from "../roomlist/Entry.tsx"
import { RoomContext } from "../roomview/roomcontext.ts"
import EncryptedOffIcon from "@/icons/encrypted-off.svg?react"
import EncryptedQuestionIcon from "@/icons/encrypted-question.svg?react"
import EncryptedIcon from "@/icons/encrypted.svg?react"
import DeviceList from "./UserInfoDeviceList.tsx"
import MutualRooms from "./UserInfoMutualRooms.tsx"
import ErrorIcon from "@/icons/error.svg?react"
interface UserInfoProps {
@ -45,6 +43,8 @@ const UserInfo = ({ userID }: UserInfoProps) => {
const [view, setView] = useState<ProfileView | null>(null)
const [errors, setErrors] = useState<string[]>([])
useEffect(() => {
setErrors([])
setView(null)
client.rpc.getProfileView(roomCtx?.store.roomID, userID).then(
resp => {
setView(resp)
@ -110,121 +110,6 @@ function renderFullInfo(
</>
}
interface DeviceListProps {
view: ProfileView
room?: RoomStateStore
}
function trustStateDescription(state: TrustState): string {
switch (state) {
case "blacklisted":
return "Device has been blacklisted manually"
case "unverified":
return "Device has not been verified by cross-signing keys, or cross-signing keys were not found"
case "verified":
return "Device was verified manually"
case "cross-signed-untrusted":
return "Device is cross-signed, cross-signing keys are NOT trusted"
case "cross-signed-tofu":
return "Device is cross-signed, cross-signing keys were trusted on first use"
case "cross-signed-verified":
return "Device is cross-signed, cross-signing keys were verified manually"
default:
return "Invalid trust state"
}
}
function renderDevice(device: ProfileViewDevice) {
let Icon = EncryptedIcon
if (device.trust_state === "blacklisted") {
Icon = EncryptedOffIcon
} else if (device.trust_state === "cross-signed-untrusted" || device.trust_state === "unverified") {
Icon = EncryptedQuestionIcon
}
return <li key={device.device_id} className="device">
<div
className={`icon-wrapper trust-${device.trust_state}`}
title={trustStateDescription(device.trust_state)}
><Icon/></div>
<div title={device.device_id}>{device.name || device.device_id}</div>
</li>
}
const DeviceList = ({ view, room }: DeviceListProps) => {
const isEncrypted = room?.meta.current.encryption_event?.algorithm === "m.megolm.v1.aes-sha2"
const encryptionMessage = isEncrypted
? "Messages in this room are end-to-end encrypted."
: "Messages in this room are not end-to-end encrypted."
if (!view.devices_tracked) {
return <div className="devices not-tracked">
<h4>Security</h4>
<p>{encryptionMessage}</p>
<p>This user's device list is not being tracked.</p>
<hr/>
</div>
}
let verifiedMessage = null
if (view.user_trusted) {
verifiedMessage = <p className="verified-message verified" title={view.master_key}>
<EncryptedIcon/> You have verified this user
</p>
} else if (view.master_key) {
if (view.master_key === view.first_master_key) {
verifiedMessage = <p className="verified-message tofu" title={view.master_key}>
<EncryptedIcon/> Trusted master key on first use
</p>
} else {
verifiedMessage = <p className="verified-message tofu-broken" title={view.master_key}>
<EncryptedQuestionIcon/> Master key has changed
</p>
}
}
return <div className="devices">
<h4>Security</h4>
<p>{encryptionMessage}</p>
{verifiedMessage}
<h4>{view.devices.length} devices</h4>
<ul>{view.devices.map(renderDevice)}</ul>
<hr/>
</div>
}
interface MutualRoomsProps {
client: Client
rooms: RoomID[]
}
const MutualRooms = ({ client, rooms }: MutualRoomsProps) => {
const [maxCount, increaseMaxCount] = useReducer(count => count + 10, 5)
const mappedRooms = useMemo(() => rooms.map((roomID): RoomListEntry | null => {
const roomData = client.store.rooms.get(roomID)
if (!roomData || roomData.hidden) {
return null
}
return {
room_id: roomID,
dm_user_id: roomData.meta.current.lazy_load_summary?.heroes?.length === 1
? roomData.meta.current.lazy_load_summary.heroes[0] : undefined,
name: roomData.meta.current.name ?? "Unnamed room",
avatar: roomData.meta.current.avatar,
search_name: "",
sorting_timestamp: 0,
unread_messages: 0,
unread_notifications: 0,
unread_highlights: 0,
marked_unread: false,
}
}).filter((data): data is RoomListEntry => !!data), [client, rooms])
return <div className="mutual-rooms">
<h4>Shared rooms</h4>
{mappedRooms.slice(0, maxCount).map(room => <div key={room.room_id}>
<ListEntry room={room} isActive={false} hidden={false}/>
</div>)}
{mappedRooms.length > maxCount && <button className="show-more" onClick={increaseMaxCount}>
Show {mappedRooms.length - maxCount} more
</button>}
<hr/>
</div>
}
export default UserInfo

View file

@ -0,0 +1,103 @@
// 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 { RoomStateStore } from "@/api/statestore"
import { ProfileView, ProfileViewDevice, TrustState } from "@/api/types"
import EncryptedOffIcon from "@/icons/encrypted-off.svg?react"
import EncryptedQuestionIcon from "@/icons/encrypted-question.svg?react"
import EncryptedIcon from "@/icons/encrypted.svg?react"
function trustStateDescription(state: TrustState): string {
switch (state) {
case "blacklisted":
return "Device has been blacklisted manually"
case "unverified":
return "Device has not been verified by cross-signing keys, or cross-signing keys were not found"
case "verified":
return "Device was verified manually"
case "cross-signed-untrusted":
return "Device is cross-signed, cross-signing keys are NOT trusted"
case "cross-signed-tofu":
return "Device is cross-signed, cross-signing keys were trusted on first use"
case "cross-signed-verified":
return "Device is cross-signed, cross-signing keys were verified manually"
default:
return "Invalid trust state"
}
}
function renderDevice(device: ProfileViewDevice) {
let Icon = EncryptedIcon
if (device.trust_state === "blacklisted") {
Icon = EncryptedOffIcon
} else if (device.trust_state === "cross-signed-untrusted" || device.trust_state === "unverified") {
Icon = EncryptedQuestionIcon
}
return <li key={device.device_id} className="device">
<div
className={`icon-wrapper trust-${device.trust_state}`}
title={trustStateDescription(device.trust_state)}
><Icon/></div>
<div title={device.device_id}>{device.name || device.device_id}</div>
</li>
}
interface DeviceListProps {
view: ProfileView
room?: RoomStateStore
}
const DeviceList = ({ view, room }: DeviceListProps) => {
const isEncrypted = room?.meta.current.encryption_event?.algorithm === "m.megolm.v1.aes-sha2"
const encryptionMessage = isEncrypted
? "Messages in this room are end-to-end encrypted."
: "Messages in this room are not end-to-end encrypted."
if (!view.devices_tracked) {
return <div className="devices not-tracked">
<h4>Security</h4>
<p>{encryptionMessage}</p>
<p>This user's device list is not being tracked.</p>
<hr/>
</div>
}
let verifiedMessage = null
if (view.user_trusted) {
verifiedMessage = <p className="verified-message verified" title={view.master_key}>
<EncryptedIcon/> You have verified this user
</p>
} else if (view.master_key) {
if (view.master_key === view.first_master_key) {
verifiedMessage = <p className="verified-message tofu" title={view.master_key}>
<EncryptedIcon/> Trusted master key on first use
</p>
} else {
verifiedMessage = <p className="verified-message tofu-broken" title={view.master_key}>
<EncryptedQuestionIcon/> Master key has changed
</p>
}
}
return <div className="devices">
<h4>Security</h4>
<p>{encryptionMessage}</p>
{verifiedMessage}
<details>
<summary><h4>{view.devices.length} devices</h4></summary>
<ul>{view.devices.map(renderDevice)}</ul>
</details>
<hr/>
</div>
}
export default DeviceList

View file

@ -0,0 +1,60 @@
// 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 { useMemo, useReducer } from "react"
import Client from "@/api/client.ts"
import { RoomListEntry } from "@/api/statestore"
import { RoomID } from "@/api/types"
import ListEntry from "../roomlist/Entry.tsx"
interface MutualRoomsProps {
client: Client
rooms: RoomID[]
}
const MutualRooms = ({ client, rooms }: MutualRoomsProps) => {
const [maxCount, increaseMaxCount] = useReducer(count => count + 10, 3)
const mappedRooms = useMemo(() => rooms.map((roomID): RoomListEntry | null => {
const roomData = client.store.rooms.get(roomID)
if (!roomData || roomData.hidden) {
return null
}
return {
room_id: roomID,
dm_user_id: roomData.meta.current.lazy_load_summary?.heroes?.length === 1
? roomData.meta.current.lazy_load_summary.heroes[0] : undefined,
name: roomData.meta.current.name ?? "Unnamed room",
avatar: roomData.meta.current.avatar,
search_name: "",
sorting_timestamp: 0,
unread_messages: 0,
unread_notifications: 0,
unread_highlights: 0,
marked_unread: false,
}
}).filter((data): data is RoomListEntry => !!data), [client, rooms])
return <div className="mutual-rooms">
<h4>Shared rooms</h4>
{mappedRooms.slice(0, maxCount).map(room => <div key={room.room_id}>
<ListEntry room={room} isActive={false} hidden={false}/>
</div>)}
{mappedRooms.length > maxCount && <button className="show-more" onClick={increaseMaxCount}>
Show {mappedRooms.length - maxCount} more
</button>}
<hr/>
</div>
}
export default MutualRooms