Features/verified user badge (#12)

* feat: verified user badge

* fix: style lint issue

* fix: custom styles

* feat: right panel user info badge
pull/26786/head
Badi Ifaoui 2023-12-07 13:17:36 +01:00 committed by Badi Ifaoui
parent c907cba70d
commit f97c103a73
16 changed files with 3617 additions and 126 deletions

View File

@ -10,5 +10,7 @@
"src/components/views/spaces/SpacePanel.tsx": "src/components/views/spaces/SpacePanel.tsx",
"src/hooks/useRoomName.ts": "src/hooks/useRoomName.ts",
"src/editor/commands.tsx": "src/editor/commands.tsx",
"src/autocomplete/Autocompleter.ts": "src/autocomplete/Autocompleter.ts"
"src/autocomplete/Autocompleter.ts": "src/autocomplete/Autocompleter.ts",
"src/components/views/dialogs/InviteDialog.tsx": "src/components/views/dialogs/InviteDialog.tsx",
"src/components/views/right_panel/UserInfo.tsx": "src/components/views/right_panel/UserInfo.tsx"
}

View File

@ -8,6 +8,7 @@
"base_url": "https://vector.im"
}
},
"bots_backend_url": "https://http://matrix.superhero.com/wallet",
"disable_custom_urls": false,
"disable_guests": false,
"disable_login_language_selector": false,

View File

@ -74,6 +74,7 @@
"@matrix-org/olm": "3.2.15",
"@matrix-org/react-sdk-module-api": "^2.2.1",
"gfm.css": "^1.1.2",
"jotai": "^2.6.0",
"jsrsasign": "^10.5.25",
"katex": "^0.16.0",
"lodash": "^4.17.21",

View File

@ -1,46 +1,51 @@
.sh_RoomTokenGatedRoom {
.sh_RoomTokenGatedRoom,
.mx_InviteDialog_tile_nameStack_name {
align-items: center;
display: flex;
}
.sh_RoomTokenGatedRoomIcon {
.sh_RoomTokenGatedRoomIcon,
.sh_VerifiedIcon {
width: 16px;
height: 16px;
margin-right: 4px;
}
h2 .sh_RoomTokenGatedRoomIcon {
h2 .sh_RoomTokenGatedRoomIcon,
h2 .sh_VerifiedIcon {
width: 26px;
height: 26px;
}
.sh_VerifiedIcon {
margin-right: 4px;
margin-left: 4px;
}
.mx_UserInfo_profile .sh_VerifiedIcon {
width: 18px;
height: 18px;
margin-top: 3px;
}
.mx_QuickSettingsButton.sh_SuperheroDexButton::before {
-webkit-mask-image: url(../../themes/superhero/img/icons/diamond.svg);
mask-image: url(../../themes/superhero/img/icons/diamond.svg);
-webkit-mask-image: url("../../themes/superhero/img/icons/diamond.svg");
mask-image: url("../../themes/superhero/img/icons/diamond.svg");
}
.mx_QuickSettingsButton.sh_MintTokenButton::before {
-webkit-mask-image: url(../../themes/superhero/img/icons/tokens.svg);
mask-image: url(../../themes/superhero/img/icons/tokens.svg);
-webkit-mask-image: url("../../themes/superhero/img/icons/tokens.svg");
mask-image: url("../../themes/superhero/img/icons/tokens.svg");
}
/* START - Update @user chat message highlighting */
.mx_EventTile.mx_EventTile_highlight, .mx_EventTile.mx_EventTile_highlight .markdown-body, .mx_EventTile.mx_EventTile_highlight .mx_EventTile_edited {
.mx_EventTile.mx_EventTile_highlight,
.mx_EventTile.mx_EventTile_highlight .markdown-body,
.mx_EventTile.mx_EventTile_highlight .mx_EventTile_edited {
color: var(--cpd-color-orange-1000) !important;
/* font-weight: bold; */
}
.mx_EventTile[data-layout="irc"].mx_EventTile_highlight .mx_EventTile_line, .mx_EventTile[data-layout="irc"].mx_EventTile_highlight .markdown-body .mx_EventTile_line, .mx_EventTile[data-layout="group"].mx_EventTile_highlight .mx_EventTile_line, .mx_EventTile[data-layout="group"].mx_EventTile_highlight .markdown-body .mx_EventTile_line {
/* background-color:var(--cpd-color-alpha-yellow-300)!important; */
}
.mx_EventTile[data-layout="group"].mx_EventTile_highlight .markdown-body .mx_EventTile_line, .mx_EventTile[data-layout="group"].mx_EventTile_highlight .mx_EventTile_line, .mx_EventTile[data-layout="irc"].mx_EventTile_highlight .markdown-body .mx_EventTile_line, .mx_EventTile[data-layout="irc"].mx_EventTile_highlight .mx_EventTile_line {
/* background-color:var(--cpd-color-alpha-yellow-700)!important; */
}
/* END - Update @user chat message highlighting */
/* START - Custom Side Panel styling */
.mx_SpacePanel {
--activeBackground-color: var(--cpd-color-alpha-gray-500) !important;
@ -51,10 +56,6 @@ h2 .sh_RoomTokenGatedRoomIcon {
margin-left: 14px !important;
}
.mx_SpacePanel .mx_SpaceButton.mx_SpaceButton_active.mx_SpaceButton_narrow .mx_SpaceButton_selectionWrapper {
background: white!important;
}
.mx_SpaceButton_home .mx_SpaceButton_icon {
background: white !important;
outline: 1px solid white;
@ -72,6 +73,7 @@ h2 .sh_RoomTokenGatedRoomIcon {
}
.mx_SpacePanel .mx_SpaceButton.mx_SpaceButton_active.mx_SpaceButton_narrow .mx_SpaceButton_selectionWrapper {
background: white !important;
outline: 1px rgb(36, 36, 36) solid !important;
border: 3px rgb(36, 36, 36) solid !important;
}
@ -172,7 +174,7 @@ h2 .sh_RoomTokenGatedRoomIcon {
.cpd-theme-dark .mx_SpacePanel {
/* --activeBackground-color: var(--cpd-color-alpha-gray-500)!important; */
/* background-image: linear-gradient(25deg, #700483, #110177)!important; */
background: linear-gradient(25deg, rgba(204, 58, 230, 0.20) 0%, rgba(97, 71, 255, 0.20) 100%), #313338;
background: linear-gradient(25deg, rgba(204, 58, 230, 0.2) 0%, rgba(97, 71, 255, 0.2) 100%), #313338;
}
.cpd-theme-dark .mx_SpacePanel .mx_SpaceButton.mx_SpaceButton_home .mx_SpaceButton_icon::before {
@ -187,7 +189,6 @@ h2 .sh_RoomTokenGatedRoomIcon {
color: black !important;
}
/* CPD Light Theme Overwrite */
.cpd-theme-light.cpd-theme-light {
--cpd-color-text-action-accent: #6147ff !important;
@ -821,4 +822,3 @@ h2 .sh_RoomTokenGatedRoomIcon {
--cpd-color-gray-200: #181a1f;
--cpd-color-gray-100: #14171b; */
}

View File

@ -0,0 +1,13 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_28984_171441)">
<circle cx="11.9996" cy="12" r="8.4" fill="white" />
<path fill-rule="evenodd" clip-rule="evenodd"
d="M7.8904 21.1595H5.5979C3.70684 21.1595 2.82998 20.2826 2.82998 18.3916V16.1096C2.82998 15.8877 2.77716 15.7399 2.61869 15.5708L1.00231 13.9439C-0.328817 12.6127 -0.339382 11.3767 1.00231 10.0456L2.61869 8.41862C2.77716 8.26016 2.82998 8.10169 2.82998 7.8904V5.5979C2.82998 3.69627 3.70684 2.82998 5.5979 2.82998H7.8904C8.10169 2.82998 8.26016 2.77716 8.41862 2.61869L10.0456 1.00231C11.3767 -0.328817 12.6127 -0.339382 13.9439 1.00231L15.5708 2.61869C15.7399 2.77716 15.8877 2.82998 16.1096 2.82998H18.3916C20.2932 2.82998 21.1595 3.7174 21.1595 5.5979V7.8904C21.1595 8.10169 21.2228 8.26016 21.3813 8.41862L22.9977 10.0456C24.3288 11.3767 24.3394 12.6127 22.9977 13.9439L21.3813 15.5708C21.2228 15.7399 21.1595 15.8877 21.1595 16.1096V18.3916C21.1595 20.2932 20.2826 21.1595 18.3916 21.1595H16.1096C15.8877 21.1595 15.7399 21.2228 15.5708 21.3813L13.9439 22.9977C12.6127 24.3288 11.3767 24.3394 10.0456 22.9977L8.41862 21.3813C8.26016 21.2228 8.10169 21.1595 7.8904 21.1595ZM8.07776 11.86C8.26875 11.9111 8.44125 12.0362 8.62992 12.2852L10.7052 14.9637L15.7837 6.66738C16.0591 6.21357 16.413 6 16.8064 6C17.4229 6 18 6.44048 18 7.09453C18 7.40156 17.8427 7.72192 17.6722 8.00229L11.8099 17.3326C11.5213 17.7598 11.1278 18 10.6427 18C10.1704 18 9.79014 17.7998 9.43594 17.3326L6.32783 13.4883C6.21973 13.344 6.13594 13.1962 6.08027 13.043C6.02783 12.8985 6 12.7494 6 12.594C6 11.9266 6.49834 11.406 7.14111 11.406C7.4987 11.406 7.7912 11.5272 8.07776 11.86Z"
fill="#1161FE" />
</g>
<defs>
<clipPath id="clip0_28984_171441">
<rect width="24" height="24" fill="white" />
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

3
src/atoms.ts Normal file
View File

@ -0,0 +1,3 @@
import { atomWithStorage } from "jotai/utils";
export const verifiedAccountsAtom = atomWithStorage<Record<string, string>>("VERIFIED_ACCOUNTS", {});

File diff suppressed because it is too large Load Diff

View File

@ -18,7 +18,9 @@ import { IPublicRoomsChunkRoom, Room } from "matrix-js-sdk/src/matrix";
import React, { useCallback, useMemo } from "react";
import { Icon as TokenGatedRoomIcon } from "../../../../res/themes/superhero/img/icons/tokengated-room.svg";
import { isTokenGatedRoom, useRoomName } from "../../../hooks/useRoomName";
import { useRoomName } from "../../../hooks/useRoomName";
import { useVerifiedRoom } from "../../../hooks/useVerifiedRoom";
import { UserVerifiedBadge } from "./UserVerifiedBadge";
interface IProps {
room?: Room | IPublicRoomsChunkRoom;
@ -28,9 +30,13 @@ interface IProps {
export const RoomName = ({ room, children, maxLength }: IProps): JSX.Element => {
const roomName = useRoomName(room);
const isVerifiedRoom = useVerifiedRoom(room);
const isVerifiedRoom = useMemo(() => {
return isTokenGatedRoom(room);
const roomUsers: string[] = useMemo(() => {
return (room as Room)
.getMembers()
.map((m) => m.userId)
.filter((userId) => !!userId && userId != (room as Room).myUserId);
}, [room]);
const truncatedRoomName = useMemo(() => {
@ -45,9 +51,10 @@ export const RoomName = ({ room, children, maxLength }: IProps): JSX.Element =>
<span className="sh_RoomTokenGatedRoom">
{isVerifiedRoom && <TokenGatedRoomIcon className="sh_RoomTokenGatedRoomIcon" />}
<span dir="auto">{truncatedRoomName}</span>
{roomUsers?.length && <UserVerifiedBadge userId={roomUsers[0]} />}
</span>
),
[truncatedRoomName, isVerifiedRoom],
[truncatedRoomName, isVerifiedRoom, roomUsers],
);
if (children) return children(renderRoomName());

View File

@ -0,0 +1,14 @@
import React from "react";
import { Icon as VerifiedIcon } from "../../../../res/themes/superhero/img/icons/verified.svg";
import { useVerifiedUser } from "../../../hooks/useVerifiedUser";
export interface UserVerifiedBadgeProps {
userId: string;
}
export const UserVerifiedBadge = ({ userId }: UserVerifiedBadgeProps): JSX.Element => {
const isVerifiedUser = useVerifiedUser(userId);
return <>{isVerifiedUser && <VerifiedIcon className="sh_VerifiedIcon" />}</>;
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,38 @@
import { useAtom } from "jotai";
import React, { useEffect } from "react";
import { verifiedAccountsAtom } from "../atoms";
/**
* Provides the superhero context to its children components.
* @param children The child components to be wrapped by the provider.
* @returns The superhero provider component.
*/
export const SuperheroProvider = ({ children, config }: any): any => {
const [verifiedAccounts, setVerifiedAccounts] = useAtom(verifiedAccountsAtom);
function loadVerifiedAccounts(): void {
if (config.bots_backend_url) {
fetch(`${config.bots_backend_url}/ae-wallet-bot/get-verified-accounts`, {
method: "POST",
})
.then((res) => res.json())
.then(setVerifiedAccounts);
}
}
useEffect(() => {
if (!verifiedAccounts?.length) {
loadVerifiedAccounts();
}
const interval = setInterval(() => {
loadVerifiedAccounts();
}, 10000);
return () => clearInterval(interval);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <>{children}</>;
};

View File

@ -34,15 +34,6 @@ export function getRoomName(room?: Room | IPublicRoomsChunkRoom, oobName?: IOOBD
);
}
/**
* Determines if a room is a token gated room
* @param room - The room model
* @returns {boolean} true if the room is token gated
*/
export function isTokenGatedRoom(room?: Room | IPublicRoomsChunkRoom): boolean {
return !!room?.name?.includes("[TG]");
}
/**
* Determines the room name from a combination of the room model and potential
* out-of-band information

View File

@ -0,0 +1,24 @@
import { IPublicRoomsChunkRoom, Room } from "matrix-js-sdk/src/matrix";
import { useMemo } from "react";
/**
* Determines if a room is a token gated room
* @param room - The room model
* @returns {boolean} true if the room is token gated
*/
export function isTokenGatedRoom(room?: Room | IPublicRoomsChunkRoom): boolean {
return !!room?.name?.startsWith("[TG]");
}
/**
* Custom hook to check if a room is verified
* @param room - The room model
* @returns {boolean} true if the room is verified, false otherwise
*/
export function useVerifiedRoom(room?: Room | IPublicRoomsChunkRoom): boolean {
const isVerifiedRoom = useMemo(() => {
return isTokenGatedRoom(room);
}, [room]);
return isVerifiedRoom;
}

View File

@ -0,0 +1,19 @@
import { useMemo } from "react";
import { useAtom } from "jotai";
import { verifiedAccountsAtom } from "../atoms";
/**
* Custom hook to check if a user is verified.
* @param userId - The ID of the user to check.
* @returns A boolean indicating whether the user is verified or not.
*/
export function useVerifiedUser(userId?: string): boolean {
const [verifiedAccounts] = useAtom(verifiedAccountsAtom);
const isVerifiedUser: boolean = useMemo(() => {
return !!(userId && !!verifiedAccounts[userId]);
}, [userId, verifiedAccounts]);
return isVerifiedUser;
}

View File

@ -40,6 +40,7 @@ import { parseQs } from "./url_utils";
import VectorBasePlatform from "./platform/VectorBasePlatform";
import { getInitialScreenAfterLogin, getScreenFromLocation, init as initRouting, onNewScreen } from "./routing";
import { UserFriendlyError } from "../languageHandler";
import { SuperheroProvider } from "../context/SuperheroProvider";
// add React and ReactPerf to the global namespace, to make them easier to access via the console
// this incidentally means we can forget our React imports in JSX files without penalty.
@ -116,6 +117,7 @@ export async function loadApp(fragParams: {}, matrixChatRef: React.Ref<MatrixCha
return (
<wrapperOpts.Wrapper>
<SuperheroProvider config={config}>
<MatrixChat
ref={matrixChatRef}
onNewScreen={onNewScreen}
@ -127,6 +129,7 @@ export async function loadApp(fragParams: {}, matrixChatRef: React.Ref<MatrixCha
initialScreenAfterLogin={initialScreenAfterLogin}
defaultDeviceDisplayName={defaultDeviceName}
/>
</SuperheroProvider>
</wrapperOpts.Wrapper>
);
}

View File

@ -7913,6 +7913,11 @@ jest@^29.0.0:
import-local "^3.0.2"
jest-cli "^29.7.0"
jotai@^2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/jotai/-/jotai-2.6.0.tgz#68b5d634f78a9ea55adfb8d92206ef59304b5dd5"
integrity sha512-Vt6hsc04Km4j03l+Ax+Sc+FVft5cRJhqgxt6GTz6GM2eM3DyX3CdBdzcG0z2FrlZToL1/0OAkqDghIyARWnSuQ==
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"