forked from Mirrors/gomuks
web/rightpanel: improve user info view slightly
This commit is contained in:
parent
1cc39d40c9
commit
5b9f458b75
4 changed files with 175 additions and 123 deletions
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
103
web/src/ui/rightpanel/UserInfoDeviceList.tsx
Normal file
103
web/src/ui/rightpanel/UserInfoDeviceList.tsx
Normal 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
|
60
web/src/ui/rightpanel/UserInfoMutualRooms.tsx
Normal file
60
web/src/ui/rightpanel/UserInfoMutualRooms.tsx
Normal 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
|
Loading…
Add table
Reference in a new issue