Skip to main content
Glama
deso-protocol

DeSo MCP Server

Official
manage-members-dialog.tsx13.9 kB
import { Button, Dialog, DialogBody, DialogFooter, DialogHeader, } from "@material-tailwind/react"; import { UserContext } from "contexts/UserContext"; import { AccessGroupEntryResponse, AccessGroupPrivateInfo, addAccessGroupMembers, encrypt, getAllAccessGroupsMemberOnly, getBulkAccessGroups, identity, publicKeyToBase58Check, removeAccessGroupMembers, waitForTransactionFound, } from "deso-protocol"; import difference from "lodash/difference"; import React, { Fragment, useContext, useRef, useState } from "react"; import { IoCloseCircleOutline, IoPeopleCircleOutline } from "react-icons/io5"; import ClipLoader from "react-spinners/ClipLoader"; import { toast } from "react-toastify"; import useKeyDown from "../hooks/useKeyDown"; import { useMembers } from "../hooks/useMembers"; import { useMobile } from "../hooks/useMobile"; import { DEFAULT_KEY_MESSAGING_GROUP_NAME } from "../utils/constants"; import { Conversation } from "../utils/types"; import { MessagingDisplayAvatar } from "./messaging-display-avatar"; import { SearchUsers } from "./search-users"; export interface ManageMembersDialogProps { onSuccess: () => void; conversation: Conversation; isGroupOwner: boolean; } export const ManageMembersDialog = ({ onSuccess, conversation, isGroupOwner, }: ManageMembersDialogProps) => { const { appUser } = useContext(UserContext); const [open, setOpen] = useState(false); const [loading, setLoading] = useState(false); const [updating, setUpdating] = useState(false); const { members, addMember, removeMember, onPairMissing, currentMemberKeys } = useMembers(setLoading, open, conversation); const membersAreaRef = useRef<HTMLDivElement>(null); const { isMobile } = useMobile(); const handleOpen = () => setOpen(!open); const groupName = conversation.messages[0].RecipientInfo.AccessGroupKeyName; useKeyDown(() => { if (open) { setOpen(false); } }, ["Escape"]); const formSubmit = async (event: React.FormEvent) => { event.preventDefault(); const areMembersDefined = Array.isArray(members) && members.length > 0; if (!areMembersDefined) { toast.error("At least one member should be added"); return; } const memberKeys = members.map((e) => e.id); const memberKeysToAdd = difference(memberKeys, currentMemberKeys); const memberKeysToRemove = currentMemberKeys.filter( (e) => !memberKeys.includes(e) ); setUpdating(true); try { await addMembers(groupName, memberKeysToAdd); await removeMembers(groupName, memberKeysToRemove); } finally { setUpdating(false); } onSuccess(); handleOpen(); }; // TODO: migrate this to the deso-protocol library as a convenience wrapper const addMembers = async (groupName: string, memberKeys: Array<string>) => { if (!appUser) { return Promise.reject(new Error("You are not logged in.")); } return updateMembers( groupName, memberKeys, async (groupEntries?: Array<AccessGroupEntryResponse>) => { let accessGroupKeyInfo: AccessGroupPrivateInfo; // We first try to decrypt the group's private key that was encrypted to the group owner's // default key. This is safer than simply using the standard derivation since // the standard derivation could have been computed incorrectly. This way we know that // the new member will have the same encryption key as the rest of the group. try { const resp = await getAllAccessGroupsMemberOnly({ PublicKeyBase58Check: appUser.PublicKeyBase58Check, }); const encryptedKey = (resp.AccessGroupsMember ?? []).find( (accessGroupEntry) => accessGroupEntry && accessGroupEntry.AccessGroupOwnerPublicKeyBase58Check === appUser.PublicKeyBase58Check && accessGroupEntry.AccessGroupKeyName === groupName && accessGroupEntry.AccessGroupMemberEntryResponse )?.AccessGroupMemberEntryResponse?.EncryptedKey; if (encryptedKey) { const keys = await identity.decryptAccessGroupKeyPair(encryptedKey); const pkBs58Check = await publicKeyToBase58Check(keys.public); accessGroupKeyInfo = { AccessGroupPublicKeyBase58Check: pkBs58Check, AccessGroupPrivateKeyHex: keys.seedHex, AccessGroupKeyName: groupName, }; } } catch (e) { // If, for any reason, we fail to recover the group's private key // we will fall back to the standard derivation. accessGroupKeyInfo = await identity.accessGroupStandardDerivation( groupName ); } const { submittedTransactionResponse } = await addAccessGroupMembers({ AccessGroupOwnerPublicKeyBase58Check: appUser.PublicKeyBase58Check, AccessGroupKeyName: groupName, AccessGroupMemberList: await Promise.all( (groupEntries || []).map(async (accessGroupEntry) => { return { AccessGroupMemberPublicKeyBase58Check: accessGroupEntry.AccessGroupOwnerPublicKeyBase58Check, AccessGroupMemberKeyName: accessGroupEntry.AccessGroupKeyName, EncryptedKey: await encrypt( accessGroupEntry.AccessGroupPublicKeyBase58Check, accessGroupKeyInfo.AccessGroupPrivateKeyHex ), }; }) ), MinFeeRateNanosPerKB: 1000, }); if (!submittedTransactionResponse) { throw new Error( "Failed to submit transaction to add members to group." ); } return waitForTransactionFound(submittedTransactionResponse.TxnHashHex); } ); }; const removeMembers = async ( groupName: string, memberKeys: Array<string> ) => { if (!appUser) { toast.error("You are not logged in."); return; } return updateMembers( groupName, memberKeys, async (groupEntries?: Array<AccessGroupEntryResponse>) => { const { submittedTransactionResponse } = await removeAccessGroupMembers( { AccessGroupOwnerPublicKeyBase58Check: appUser.PublicKeyBase58Check, AccessGroupKeyName: groupName, AccessGroupMemberList: (groupEntries || []).map( (accessGroupEntry) => { return { AccessGroupMemberPublicKeyBase58Check: accessGroupEntry.AccessGroupOwnerPublicKeyBase58Check, AccessGroupMemberKeyName: accessGroupEntry.AccessGroupKeyName, EncryptedKey: "", }; } ), MinFeeRateNanosPerKB: 1000, } ); if (!submittedTransactionResponse) { throw new Error( "Failed to submit transaction to update group members." ); } return waitForTransactionFound(submittedTransactionResponse.TxnHashHex); } ); }; const updateMembers = async ( groupName: string, memberKeys: Array<string>, updateAction: ( AccessGroupEntries?: Array<AccessGroupEntryResponse> ) => Promise<void> ) => { if (memberKeys.length === 0) { return Promise.resolve(); } const { AccessGroupEntries, PairsNotFound } = await getBulkAccessGroups({ GroupOwnerAndGroupKeyNamePairs: memberKeys.map((pubKey) => { return { GroupOwnerPublicKeyBase58Check: pubKey, GroupKeyName: DEFAULT_KEY_MESSAGING_GROUP_NAME, }; }), }); if (PairsNotFound?.length) { onPairMissing(); return; } await updateAction(AccessGroupEntries).catch(() => toast.error("Something went wrong while submitting the transaction") ); }; return ( <Fragment> <Button onClick={handleOpen} className="text-blue-400 bg-transparent p-0 shadow-none hover:shadow-none flex items-center" > <IoPeopleCircleOutline className="mr-2 text-2xl" /> <span className="hidden items-center md:flex capitalize font-medium text-base"> View Members </span> </Button> <Dialog open={open} handler={handleOpen} dismiss={{ enabled: false, }} className="bg-[#050e1d] text-blue-100 border border-blue-900 min-w-none max-w-none w-[90%] md:w-[40%]" > <DialogHeader className="text-blue-100 p-5 border-b border-blue-600/20"> <div className="flex justify-between w-full items-center"> <span> All Members ( {loading ? ( <ClipLoader color={"#0d3679"} loading={true} size={16} /> ) : ( currentMemberKeys.length )} ) </span> <div className="text-sm text-right font-normal text-blue-300/60 flex"> <div> <strong className="text-blue-300/80">Group Name</strong> <br /> {groupName} </div> {!isGroupOwner && ( <div className="pl-2"> <IoCloseCircleOutline className="text-2xl cursor-pointer" onClick={() => setOpen(false)} ></IoCloseCircleOutline> </div> )} </div> </div> </DialogHeader> <form name="start-group-chat-form" onSubmit={formSubmit}> <DialogBody divider className="border-none p-5 pb-0"> <div className="mb-0"> {isGroupOwner && ( <SearchUsers className="text-white placeholder:text-blue-100 bg-blue-900/20 placeholder-gray" onSelected={(member) => addMember(member, () => { setTimeout(() => { membersAreaRef.current?.scrollTo( 0, membersAreaRef.current.scrollHeight ); }, 0); }) } /> )} <div className="max-h-[400px] mt-3 pr-3 overflow-y-auto custom-scrollbar overflow-hidden" ref={membersAreaRef} > {loading ? ( <div className="text-center"> <ClipLoader color={"#0d3679"} loading={true} size={44} className="mt-4" /> </div> ) : ( members.map((member) => ( <div className="flex p-1.5 md:p-4 items-center cursor-pointer text-white bg-blue-900/20 border border-blue-600/20 rounded-md my-2" key={member.id} > <MessagingDisplayAvatar username={member.text} publicKey={member.id} diameter={isMobile ? 40 : 50} classNames="mx-0" /> <div className="flex justify-between items-center flex-1 overflow-auto"> <div className="mx-2 md:ml-4 max-w-[calc(100%-105px)]"> <div className="font-medium truncate"> {member.text} </div> {isGroupOwner && currentMemberKeys.includes(member.id) && ( <div className="text-xs md:text-sm text-blue-300/80 mt-1"> Already in the chat </div> )} </div> {isGroupOwner && member.id !== appUser?.PublicKeyBase58Check && ( <Button size="sm" className="rounded-full mr-1 md:mr-3 px-3 py-2 border text-white bg-red-400/20 hover:bg-red-400/30 border-red-600/60 shadow-none hover:shadow-none normal-case text-sm md:px-4" onClick={() => removeMember(member.id)} > Remove </Button> )} </div> </div> )) )} </div> </div> </DialogBody> <DialogFooter className="border-t border-blue-600/20"> {isGroupOwner && ( <> <Button onClick={handleOpen} className="rounded-full mr-3 py-2 bg-transparent border border-blue-600/60 shadow-none hover:shadow-none normal-case text-sm px-4" > <span>Cancel</span> </Button> <Button type="submit" className="bg-[#ffda59] text-[#6d4800] rounded-full py-2 hover:shadow-none normal-case text-sm px-4 flex items-center" disabled={updating} > {updating && ( <ClipLoader color="white" loading={true} size={20} className="mr-2" /> )} <span>Update Group</span> </Button> </> )} </DialogFooter> </form> </Dialog> </Fragment> ); };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/deso-protocol/deso-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server