-}
export default UserInfo
diff --git a/web/src/ui/rightpanel/UserInfoDeviceList.tsx b/web/src/ui/rightpanel/UserInfoDeviceList.tsx
new file mode 100644
index 0000000..8b0edd6
--- /dev/null
+++ b/web/src/ui/rightpanel/UserInfoDeviceList.tsx
@@ -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 .
+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
+
+
{device.name || device.device_id}
+
+}
+
+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
+
Security
+
{encryptionMessage}
+
This user's device list is not being tracked.
+
+
+ }
+ let verifiedMessage = null
+ if (view.user_trusted) {
+ verifiedMessage =
+ You have verified this user
+
+ } else if (view.master_key) {
+ if (view.master_key === view.first_master_key) {
+ verifiedMessage =
+ Trusted master key on first use
+
+ } else {
+ verifiedMessage =
+ Master key has changed
+
+ }
+ }
+ return
+
Security
+
{encryptionMessage}
+ {verifiedMessage}
+
+
{view.devices.length} devices
+
{view.devices.map(renderDevice)}
+
+
+
+}
+
+export default DeviceList
diff --git a/web/src/ui/rightpanel/UserInfoMutualRooms.tsx b/web/src/ui/rightpanel/UserInfoMutualRooms.tsx
new file mode 100644
index 0000000..51cb0f0
--- /dev/null
+++ b/web/src/ui/rightpanel/UserInfoMutualRooms.tsx
@@ -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 .
+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