diff --git a/web/src/ui/rightpanel/RightPanel.css b/web/src/ui/rightpanel/RightPanel.css index e66c244..1db79b5 100644 --- a/web/src/ui/rightpanel/RightPanel.css +++ b/web/src/ui/rightpanel/RightPanel.css @@ -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; diff --git a/web/src/ui/rightpanel/UserInfo.tsx b/web/src/ui/rightpanel/UserInfo.tsx index aca64b3..5f07d09 100644 --- a/web/src/ui/rightpanel/UserInfo.tsx +++ b/web/src/ui/rightpanel/UserInfo.tsx @@ -13,20 +13,18 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -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(null) const [errors, setErrors] = useState([]) 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
  • -
    -
    {device.name || device.device_id}
    -
  • -} - -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)}
    -
    -
    -} - -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
    -

    Shared rooms

    - {mappedRooms.slice(0, maxCount).map(room =>
    -
    )} - {mappedRooms.length > maxCount && } -
    -
    -} 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
    +

    Shared rooms

    + {mappedRooms.slice(0, maxCount).map(room =>
    +
    )} + {mappedRooms.length > maxCount && } +
    +
    +} + +export default MutualRooms