mirror of
https://github.com/Qortal/qortal-mobile.git
synced 2025-03-15 04:12:32 +00:00
added admin space and qortalrequests
This commit is contained in:
parent
6d416dc5e9
commit
fcc1c9abf4
15
src/assets/Icons/AdminsIcon.tsx
Normal file
15
src/assets/Icons/AdminsIcon.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
|
||||
export const AdminsIcon= ({ color = 'white', height = 48, width = 50 }) => {
|
||||
return (
|
||||
<svg width={width} height={height} viewBox="0 0 50 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19.278 10.622C19.278 8.52117 19.901 6.46752 21.0681 4.72074C22.2353 2.97396 23.8942 1.61251 25.8351 0.808557C27.7761 0.00460249 29.9118 -0.205749 31.9722 0.204103C34.0327 0.613956 35.9254 1.6256 37.4109 3.11112C38.8964 4.59663 39.908 6.48929 40.3179 8.54976C40.7278 10.6102 40.5174 12.746 39.7134 14.6869C38.9095 16.6278 37.548 18.2867 35.8013 19.4539C34.0545 20.621 32.0008 21.244 29.9 21.244C27.0838 21.2408 24.3839 20.1207 22.3926 18.1294C20.4013 16.1381 19.2812 13.4382 19.278 10.622ZM17.16 37.736C17.3428 37.8483 17.5137 37.979 17.67 38.126L17.5 40.426C17.3249 40.549 17.1372 40.6529 16.94 40.736C16.6401 40.6901 16.3332 40.7363 16.0601 40.8684C15.7869 41.0005 15.5603 41.2124 15.41 41.476L14.54 42.996C14.3907 43.2597 14.325 43.5625 14.3518 43.8644C14.3786 44.1662 14.4966 44.4528 14.69 44.686C14.7144 44.8985 14.7177 45.1129 14.7 45.326L12.8 46.636C12.5952 46.5775 12.3974 46.4971 12.21 46.396C12.0564 46.1331 11.8277 45.9221 11.5533 45.7903C11.2788 45.6584 10.9712 45.6116 10.67 45.656L8.95 45.926C8.6485 45.9709 8.36799 46.1071 8.14628 46.3163C7.92457 46.5255 7.7723 46.7976 7.71 47.096C7.56006 47.2479 7.39599 47.3852 7.22 47.506L5.01 46.826C4.92855 46.6296 4.86822 46.4251 4.83 46.216C4.94414 45.9337 4.9689 45.6231 4.90095 45.3263C4.83299 45.0294 4.67559 44.7606 4.45 44.556L3.17 43.376C2.94532 43.1703 2.66372 43.0373 2.36214 42.9945C2.06055 42.9517 1.75306 43.001 1.48 43.136C1.26901 43.1167 1.06096 43.0731 0.86 43.006L0 40.866C0.107104 40.6785 0.230942 40.5012 0.37 40.336C0.662067 40.2503 0.920588 40.0766 1.11029 39.8386C1.3 39.6006 1.41164 39.3098 1.43 39.006L1.56 37.266C1.58108 36.9627 1.50882 36.6603 1.35293 36.3993C1.19705 36.1383 0.96501 35.9312 0.688 35.806C0.579009 35.6225 0.482136 35.4321 0.398 35.236L1.548 33.226C1.75938 33.1906 1.97386 33.1772 2.188 33.186C2.43756 33.3593 2.73415 33.4522 3.038 33.4522C3.34185 33.4522 3.63844 33.3593 3.888 33.186L5.328 32.196C5.57858 32.0241 5.77044 31.7795 5.87773 31.4952C5.98501 31.2109 6.00255 30.9006 5.928 30.606C5.99996 30.4048 6.09034 30.2106 6.198 30.026L8.478 29.676C8.63866 29.8159 8.78292 29.9736 8.908 30.146C8.92721 30.4496 9.03917 30.7399 9.22874 30.9778C9.41832 31.2156 9.67637 31.3896 9.968 31.476L11.638 31.986C11.9289 32.0736 12.2396 32.0708 12.5289 31.9779C12.8181 31.8851 13.0724 31.7065 13.258 31.466C13.46 31.3985 13.6675 31.3484 13.878 31.316L15.578 32.876C15.5633 33.0885 15.5298 33.2994 15.478 33.506C15.2538 33.7116 15.0975 33.9804 15.0296 34.2769C14.9617 34.5733 14.9856 34.8834 15.098 35.166L15.738 36.786C15.8515 37.0693 16.0481 37.3116 16.3019 37.4812C16.5557 37.6507 16.8548 37.7396 17.16 37.736ZM10.51 38.156C10.3773 37.678 10.0609 37.2718 9.63 37.026C9.35098 36.8624 9.03343 36.7762 8.71 36.776C8.29776 36.7752 7.89685 36.9109 7.56974 37.1618C7.24264 37.4126 7.0077 37.7647 6.90156 38.163C6.79541 38.5613 6.82401 38.9836 6.9829 39.364C7.14179 39.7444 7.42205 40.0615 7.78 40.266C7.99021 40.3909 8.2238 40.4713 8.46633 40.5022C8.70886 40.5332 8.95516 40.5141 9.19 40.446C9.42745 40.3851 9.65005 40.2765 9.8443 40.127C10.0385 39.9774 10.2004 39.79 10.32 39.576C10.4445 39.3633 10.5255 39.1279 10.5581 38.8836C10.5907 38.6393 10.5744 38.3909 10.51 38.153V38.156ZM44.168 29.124C40.8 25.786 35.977 24.165 29.929 24.268C23.891 24.168 19.148 25.746 15.782 28.986L18.104 31.122C18.3885 31.3825 18.5612 31.743 18.586 32.128C18.6519 33.0412 18.5153 33.9577 18.186 34.812L18.232 34.931C19.0589 35.3243 19.7879 35.8969 20.366 36.607C20.612 36.904 20.7341 37.2843 20.707 37.669L20.451 41.298H48.374C48.518 41.2982 48.6604 41.2672 48.7913 41.2073C48.9223 41.1474 49.0387 41.0599 49.1327 40.9508C49.2267 40.8416 49.2961 40.7135 49.336 40.5752C49.3759 40.4368 49.3855 40.2914 49.364 40.149C48.8358 36.0186 47.0175 32.1604 44.168 29.124Z" fill={color}/>
|
||||
</svg>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
);
|
||||
};
|
||||
|
@ -54,7 +54,7 @@ import {
|
||||
updateThreadActivity,
|
||||
walletVersion,
|
||||
} from "./background";
|
||||
import { decryptGroupEncryption, encryptAndPublishSymmetricKeyGroupChat, publishGroupEncryptedResource, publishOnQDN } from "./backgroundFunctions/encryption";
|
||||
import { decryptGroupEncryption, encryptAndPublishSymmetricKeyGroupChat, encryptAndPublishSymmetricKeyGroupChatForAdmins, publishGroupEncryptedResource, publishOnQDN } from "./backgroundFunctions/encryption";
|
||||
import { PUBLIC_NOTIFICATION_CODE_FIRST_SECRET_KEY } from "./constants/codes";
|
||||
import { encryptSingle } from "./qdn/encryption/group-encryption";
|
||||
import { _createPoll, _voteOnPoll } from "./qortalRequests/get";
|
||||
@ -1250,6 +1250,41 @@ export async function encryptAndPublishSymmetricKeyGroupChatCase(
|
||||
}
|
||||
}
|
||||
|
||||
export async function encryptAndPublishSymmetricKeyGroupChatForAdminsCase(
|
||||
request,
|
||||
event
|
||||
) {
|
||||
try {
|
||||
const { groupId, previousData, admins } = request.payload;
|
||||
const { data, numberOfMembers } =
|
||||
await encryptAndPublishSymmetricKeyGroupChatForAdmins({
|
||||
groupId,
|
||||
previousData,
|
||||
admins
|
||||
});
|
||||
|
||||
event.source.postMessage(
|
||||
{
|
||||
requestId: request.requestId,
|
||||
action: "encryptAndPublishSymmetricKeyGroupChatForAdmins",
|
||||
payload: data,
|
||||
type: "backgroundMessageResponse",
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
} catch (error) {
|
||||
event.source.postMessage(
|
||||
{
|
||||
requestId: request.requestId,
|
||||
action: "encryptAndPublishSymmetricKeyGroupChat",
|
||||
error: error?.message,
|
||||
type: "backgroundMessageResponse",
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function publishGroupEncryptedResourceCase(request, event) {
|
||||
try {
|
||||
const {encryptedData, identifier} = request.payload;
|
||||
|
@ -98,6 +98,7 @@ import {
|
||||
versionCase,
|
||||
createPollCase,
|
||||
voteOnPollCase,
|
||||
encryptAndPublishSymmetricKeyGroupChatForAdminsCase,
|
||||
} from "./background-cases";
|
||||
import { getData, removeKeysAndLogout, storeData } from "./utils/chromeStorage";
|
||||
import {BackgroundFetch} from '@transistorsoft/capacitor-background-fetch';
|
||||
@ -2822,6 +2823,9 @@ function setupMessageListener() {
|
||||
case "encryptAndPublishSymmetricKeyGroupChat":
|
||||
encryptAndPublishSymmetricKeyGroupChatCase(request, event);
|
||||
break;
|
||||
case "encryptAndPublishSymmetricKeyGroupChatForAdmins":
|
||||
encryptAndPublishSymmetricKeyGroupChatForAdminsCase(request, event);
|
||||
break;
|
||||
case "publishGroupEncryptedResource":
|
||||
publishGroupEncryptedResourceCase(request, event);
|
||||
break;
|
||||
|
@ -85,6 +85,77 @@ const getPublicKeys = async (groupNumber: number) => {
|
||||
return members
|
||||
}
|
||||
|
||||
export const getPublicKeysByAddress = async (admins) => {
|
||||
const validApi = await getBaseApi()
|
||||
|
||||
|
||||
let members: any = [];
|
||||
if (Array.isArray(admins)) {
|
||||
for (const address of admins) {
|
||||
if (address) {
|
||||
const resAddress = await fetch(`${validApi}/addresses/${address}`);
|
||||
const resData = await resAddress.json();
|
||||
const publicKey = resData.publicKey;
|
||||
members.push(publicKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return members
|
||||
}
|
||||
|
||||
|
||||
export const encryptAndPublishSymmetricKeyGroupChatForAdmins = async ({groupId, previousData, admins}: {
|
||||
groupId: number,
|
||||
previousData: Object,
|
||||
}) => {
|
||||
try {
|
||||
|
||||
let highestKey = 0
|
||||
if(previousData){
|
||||
highestKey = Math.max(...Object.keys((previousData || {})).filter(item=> !isNaN(+item)).map(Number));
|
||||
|
||||
}
|
||||
|
||||
const resKeyPair = await getKeyPair()
|
||||
const parsedData = resKeyPair
|
||||
const privateKey = parsedData.privateKey
|
||||
const userPublicKey = parsedData.publicKey
|
||||
const groupmemberPublicKeys = await getPublicKeysByAddress(admins.map((admin)=> admin.address))
|
||||
|
||||
|
||||
const symmetricKey = createSymmetricKeyAndNonce()
|
||||
const nextNumber = highestKey + 1
|
||||
const objectToSave = {
|
||||
...previousData,
|
||||
[nextNumber]: symmetricKey
|
||||
}
|
||||
|
||||
const symmetricKeyAndNonceBase64 = await objectToBase64(objectToSave)
|
||||
|
||||
const encryptedData = encryptDataGroup({
|
||||
data64: symmetricKeyAndNonceBase64,
|
||||
publicKeys: groupmemberPublicKeys,
|
||||
privateKey,
|
||||
userPublicKey
|
||||
})
|
||||
if(encryptedData){
|
||||
const registeredName = await getNameInfo()
|
||||
const data = await publishData({
|
||||
registeredName, file: encryptedData, service: 'DOCUMENT_PRIVATE', identifier: `admins-symmetric-qchat-group-${groupId}`, uploadType: 'file', isBase64: true, withFee: true
|
||||
})
|
||||
return {
|
||||
data,
|
||||
numberOfMembers: groupmemberPublicKeys.length
|
||||
}
|
||||
|
||||
} else {
|
||||
throw new Error('Cannot encrypt content')
|
||||
}
|
||||
} catch (error: any) {
|
||||
throw new Error(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const encryptAndPublishSymmetricKeyGroupChat = async ({groupId, previousData}: {
|
||||
|
66
src/components/Chat/AdminSpace.tsx
Normal file
66
src/components/Chat/AdminSpace.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
import React, {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { GroupMail } from "../Group/Forum/GroupMail";
|
||||
import { MyContext, isMobile } from "../../App";
|
||||
import { getRootHeight } from "../../utils/mobile/mobileUtils";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import { AdminSpaceInner } from "./AdminSpaceInner";
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const AdminSpace = ({
|
||||
selectedGroup,
|
||||
adminsWithNames,
|
||||
userInfo,
|
||||
secretKey,
|
||||
getSecretKey,
|
||||
isAdmin,
|
||||
myAddress,
|
||||
hide,
|
||||
defaultThread,
|
||||
setDefaultThread
|
||||
}) => {
|
||||
const { rootHeight } = useContext(MyContext);
|
||||
const [isMoved, setIsMoved] = useState(false);
|
||||
useEffect(() => {
|
||||
if (hide) {
|
||||
setTimeout(() => setIsMoved(true), 300); // Wait for the fade-out to complete before moving
|
||||
} else {
|
||||
setIsMoved(false); // Reset the position immediately when showing
|
||||
}
|
||||
}, [hide]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
// reference to change height
|
||||
height: isMobile ? `calc(${rootHeight} - 127px` : "calc(100vh - 70px)",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
opacity: hide ? 0 : 1,
|
||||
visibility: hide && 'hidden',
|
||||
position: hide ? 'fixed' : 'relative',
|
||||
left: hide && '-1000px'
|
||||
}}
|
||||
>
|
||||
{!isAdmin && <Box sx={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
paddingTop: '25px'
|
||||
}}><Typography>Sorry, this space is only for Admins.</Typography></Box>}
|
||||
{isAdmin && <AdminSpaceInner adminsWithNames={adminsWithNames} selectedGroup={selectedGroup} />}
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
150
src/components/Chat/AdminSpaceInner.tsx
Normal file
150
src/components/Chat/AdminSpaceInner.tsx
Normal file
@ -0,0 +1,150 @@
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react'
|
||||
import { MyContext, getArbitraryEndpointReact, getBaseApiReact } from '../../App';
|
||||
import { Box, Button, Typography } from '@mui/material';
|
||||
import { decryptResource, validateSecretKey } from '../Group/Group';
|
||||
import { getFee } from '../../background';
|
||||
import { base64ToUint8Array } from '../../qdn/encryption/group-encryption';
|
||||
import { uint8ArrayToObject } from '../../backgroundFunctions/encryption';
|
||||
import { formatTimestampForum } from '../../utils/time';
|
||||
import { Spacer } from '../../common/Spacer';
|
||||
|
||||
|
||||
export const getPublishesFromAdminsAdminSpace = async (admins: string[], groupId) => {
|
||||
const queryString = admins.map((name) => `name=${name}`).join("&");
|
||||
const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT_PRIVATE&identifier=admins-symmetric-qchat-group-${
|
||||
groupId
|
||||
}&exactmatchnames=true&limit=0&reverse=true&${queryString}&prefix=true`;
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error("network error");
|
||||
}
|
||||
const adminData = await response.json();
|
||||
|
||||
const filterId = adminData.filter(
|
||||
(data: any) =>
|
||||
data.identifier === `admins-symmetric-qchat-group-${groupId}`
|
||||
);
|
||||
if (filterId?.length === 0) {
|
||||
return false;
|
||||
}
|
||||
const sortedData = filterId.sort((a: any, b: any) => {
|
||||
// Get the most recent date for both a and b
|
||||
const dateA = a.updated ? new Date(a.updated) : new Date(a.created);
|
||||
const dateB = b.updated ? new Date(b.updated) : new Date(b.created);
|
||||
|
||||
// Sort by most recent
|
||||
return dateB.getTime() - dateA.getTime();
|
||||
});
|
||||
|
||||
return sortedData[0];
|
||||
};
|
||||
|
||||
export const AdminSpaceInner = ({selectedGroup, adminsWithNames}) => {
|
||||
const [adminGroupSecretKey, setAdminGroupSecretKey] = useState(null)
|
||||
const [isFetchingAdminGroupSecretKey, setIsFetchingAdminGroupSecretKey] = useState(true)
|
||||
const [adminGroupSecretKeyPublishDetails, setAdminGroupSecretKeyPublishDetails] = useState(null)
|
||||
|
||||
const [isLoadingPublishKey, setIsLoadingPublishKey] = useState(false)
|
||||
const { show, setTxList, setInfoSnackCustom,
|
||||
setOpenSnackGlobal } = useContext(MyContext);
|
||||
|
||||
|
||||
const getAdminGroupSecretKey = useCallback(async ()=> {
|
||||
try {
|
||||
if(!selectedGroup) return
|
||||
const getLatestPublish = await getPublishesFromAdminsAdminSpace(adminsWithNames.map((admin)=> admin?.name), selectedGroup)
|
||||
if(getLatestPublish === false) return
|
||||
let data;
|
||||
|
||||
const res = await fetch(
|
||||
`${getBaseApiReact()}/arbitrary/DOCUMENT_PRIVATE/${getLatestPublish.name}/${
|
||||
getLatestPublish.identifier
|
||||
}?encoding=base64`
|
||||
);
|
||||
data = await res.text();
|
||||
|
||||
const decryptedKey: any = await decryptResource(data);
|
||||
const dataint8Array = base64ToUint8Array(decryptedKey.data);
|
||||
const decryptedKeyToObject = uint8ArrayToObject(dataint8Array);
|
||||
if (!validateSecretKey(decryptedKeyToObject))
|
||||
throw new Error("SecretKey is not valid");
|
||||
setAdminGroupSecretKey(decryptedKeyToObject)
|
||||
setAdminGroupSecretKeyPublishDetails(getLatestPublish)
|
||||
} catch (error) {
|
||||
|
||||
} finally {
|
||||
setIsFetchingAdminGroupSecretKey(false)
|
||||
}
|
||||
}, [adminsWithNames, selectedGroup])
|
||||
|
||||
const createCommonSecretForAdmins = async ()=> {
|
||||
try {
|
||||
const fee = await getFee('ARBITRARY')
|
||||
await show({
|
||||
message: "Would you like to perform an ARBITRARY transaction?" ,
|
||||
publishFee: fee.fee + ' QORT'
|
||||
})
|
||||
setIsLoadingPublishKey(true)
|
||||
|
||||
|
||||
window.sendMessage("encryptAndPublishSymmetricKeyGroupChatForAdmins", {
|
||||
groupId: selectedGroup,
|
||||
previousData: null,
|
||||
admins: adminsWithNames
|
||||
})
|
||||
.then((response) => {
|
||||
|
||||
if (!response?.error) {
|
||||
setInfoSnackCustom({
|
||||
type: "success",
|
||||
message: "Successfully re-encrypted secret key. It may take a couple of minutes for the changes to propagate. Refresh the group in 5 mins.",
|
||||
});
|
||||
setOpenSnackGlobal(true);
|
||||
return
|
||||
}
|
||||
setInfoSnackCustom({
|
||||
type: "error",
|
||||
message: response?.error || "unable to re-encrypt secret key",
|
||||
});
|
||||
setOpenSnackGlobal(true);
|
||||
})
|
||||
.catch((error) => {
|
||||
setInfoSnackCustom({
|
||||
type: "error",
|
||||
message: error?.message || "unable to re-encrypt secret key",
|
||||
});
|
||||
setOpenSnackGlobal(true);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
getAdminGroupSecretKey()
|
||||
}, [getAdminGroupSecretKey]);
|
||||
return (
|
||||
<Box sx={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: '10px'
|
||||
}}>
|
||||
<Spacer height="25px" />
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '20px',
|
||||
width: '300px',
|
||||
maxWidth: '90%'
|
||||
}}>
|
||||
{isFetchingAdminGroupSecretKey && <Typography>Fetching Admins secret keys</Typography>}
|
||||
{!isFetchingAdminGroupSecretKey && !adminGroupSecretKey && <Typography>No secret key published yet</Typography>}
|
||||
{adminGroupSecretKeyPublishDetails && (
|
||||
<Typography>Last encryption date: {formatTimestampForum(adminGroupSecretKeyPublishDetails?.updated || adminGroupSecretKeyPublishDetails?.created)}</Typography>
|
||||
)}
|
||||
<Button onClick={createCommonSecretForAdmins} variant="contained">Publish admin secret key</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
@ -27,6 +27,7 @@ import { isFocusedParentGroupAtom } from '../../atoms/global'
|
||||
import { useRecoilState } from 'recoil'
|
||||
import AppViewerContainer from '../Apps/AppViewerContainer'
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import { throttle } from 'lodash'
|
||||
|
||||
const uid = new ShortUniqueId({ length: 5 });
|
||||
|
||||
@ -53,6 +54,7 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
|
||||
const groupSocketTimeoutRef = useRef(null); // Group Socket Timeout reference
|
||||
const editorRef = useRef(null);
|
||||
const { queueChats, addToQueue, processWithNewMessages } = useMessageQueue();
|
||||
const handleUpdateRef = useRef(null);
|
||||
|
||||
|
||||
const lastReadTimestamp = useRef(null)
|
||||
@ -476,23 +478,24 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
|
||||
hasInitializedWebsocket.current = true
|
||||
}, [secretKey])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (!editorRef?.current) return;
|
||||
const handleUpdate = () => {
|
||||
const htmlContent = editorRef?.current.getHTML();
|
||||
const stringified = JSON.stringify(htmlContent);
|
||||
const size = new Blob([stringified]).size;
|
||||
|
||||
handleUpdateRef.current = throttle(() => {
|
||||
const htmlContent = editorRef.current.getHTML();
|
||||
const size = new TextEncoder().encode(htmlContent).length;
|
||||
setMessageSize(size + 100);
|
||||
};
|
||||
}, 1200);
|
||||
|
||||
// Add a listener for the editorRef?.current's content updates
|
||||
editorRef?.current.on('update', handleUpdate);
|
||||
const currentEditor = editorRef.current;
|
||||
|
||||
currentEditor.on("update", handleUpdateRef.current);
|
||||
|
||||
// Cleanup the listener on unmount
|
||||
return () => {
|
||||
editorRef?.current.off('update', handleUpdate);
|
||||
currentEditor.off("update", handleUpdateRef.current);
|
||||
};
|
||||
}, [editorRef?.current]);
|
||||
}, [editorRef, setMessageSize]);
|
||||
|
||||
|
||||
useEffect(()=> {
|
||||
@ -579,6 +582,9 @@ const clearEditorContent = () => {
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const sendMessage = async ()=> {
|
||||
try {
|
||||
if(isSending) return
|
||||
|
@ -1,42 +1,39 @@
|
||||
import React, { useState, useRef, useMemo, useEffect } from 'react';
|
||||
import React, { useState, useRef, useMemo } from 'react';
|
||||
import { ListItemIcon, Menu, MenuItem, Typography, styled } from '@mui/material';
|
||||
import MailOutlineIcon from '@mui/icons-material/MailOutline';
|
||||
import NotificationsOffIcon from '@mui/icons-material/NotificationsOff';
|
||||
import { executeEvent } from '../utils/events';
|
||||
|
||||
const CustomStyledMenu = styled(Menu)(({ theme }) => ({
|
||||
'& .MuiPaper-root': {
|
||||
backgroundColor: '#f9f9f9',
|
||||
borderRadius: '12px',
|
||||
padding: theme.spacing(1),
|
||||
boxShadow: '0 5px 15px rgba(0, 0, 0, 0.2)',
|
||||
'& .MuiPaper-root': {
|
||||
backgroundColor: '#f9f9f9',
|
||||
borderRadius: '12px',
|
||||
padding: theme.spacing(1),
|
||||
boxShadow: '0 5px 15px rgba(0, 0, 0, 0.2)',
|
||||
},
|
||||
'& .MuiMenuItem-root': {
|
||||
fontSize: '14px',
|
||||
color: '#444',
|
||||
transition: '0.3s background-color',
|
||||
'&:hover': {
|
||||
backgroundColor: '#f0f0f0',
|
||||
},
|
||||
'& .MuiMenuItem-root': {
|
||||
fontSize: '14px', // Smaller font size for the menu item text
|
||||
color: '#444',
|
||||
transition: '0.3s background-color',
|
||||
'&:hover': {
|
||||
backgroundColor: '#f0f0f0', // Explicit hover state
|
||||
},
|
||||
|
||||
},
|
||||
}));
|
||||
},
|
||||
}));
|
||||
|
||||
export const ContextMenu = ({ children, groupId, getUserSettings, mutedGroups }) => {
|
||||
const [menuPosition, setMenuPosition] = useState(null);
|
||||
const longPressTimeout = useRef(null);
|
||||
const preventClick = useRef(false); // Flag to prevent click after long-press or right-click
|
||||
const touchStartPosition = useRef({ x: 0, y: 0 });
|
||||
const touchMoved = useRef(false);
|
||||
|
||||
const isMuted = useMemo(()=> {
|
||||
return mutedGroups.includes(groupId)
|
||||
}, [mutedGroups, groupId])
|
||||
const isMuted = useMemo(() => mutedGroups.includes(groupId), [mutedGroups, groupId]);
|
||||
|
||||
// Handle right-click (context menu) for desktop
|
||||
const handleContextMenu = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation(); // Prevent parent click
|
||||
|
||||
// Set flag to prevent any click event after right-click
|
||||
event.stopPropagation();
|
||||
preventClick.current = true;
|
||||
|
||||
setMenuPosition({
|
||||
@ -47,76 +44,81 @@ export const ContextMenu = ({ children, groupId, getUserSettings, mutedGroups })
|
||||
|
||||
// Handle long-press for mobile
|
||||
const handleTouchStart = (event) => {
|
||||
touchMoved.current = false; // Reset moved state
|
||||
touchStartPosition.current = {
|
||||
x: event.touches[0].clientX,
|
||||
y: event.touches[0].clientY,
|
||||
};
|
||||
|
||||
longPressTimeout.current = setTimeout(() => {
|
||||
preventClick.current = true; // Prevent the next click after long-press
|
||||
event.stopPropagation(); // Prevent parent click
|
||||
setMenuPosition({
|
||||
mouseX: event.touches[0].clientX,
|
||||
mouseY: event.touches[0].clientY,
|
||||
});
|
||||
if (!touchMoved.current) {
|
||||
preventClick.current = true;
|
||||
event.stopPropagation();
|
||||
setMenuPosition({
|
||||
mouseX: event.touches[0].clientX,
|
||||
mouseY: event.touches[0].clientY,
|
||||
});
|
||||
}
|
||||
}, 500); // Long press duration
|
||||
};
|
||||
|
||||
const handleTouchMove = (event) => {
|
||||
const currentPosition = {
|
||||
x: event.touches[0].clientX,
|
||||
y: event.touches[0].clientY,
|
||||
};
|
||||
|
||||
const distanceMoved = Math.sqrt(
|
||||
Math.pow(currentPosition.x - touchStartPosition.current.x, 2) +
|
||||
Math.pow(currentPosition.y - touchStartPosition.current.y, 2)
|
||||
);
|
||||
|
||||
if (distanceMoved > 10) {
|
||||
touchMoved.current = true; // Mark as moved
|
||||
clearTimeout(longPressTimeout.current); // Cancel the long press
|
||||
}
|
||||
};
|
||||
|
||||
const handleTouchEnd = (event) => {
|
||||
clearTimeout(longPressTimeout.current);
|
||||
|
||||
if (preventClick.current) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation(); // Prevent synthetic click after long-press
|
||||
preventClick.current = false; // Reset the flag
|
||||
event.stopPropagation();
|
||||
preventClick.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
const handleSetGroupMute = ()=> {
|
||||
try {
|
||||
let value = [...mutedGroups]
|
||||
if(isMuted){
|
||||
value = value.filter((group)=> group !== groupId)
|
||||
} else {
|
||||
value.push(groupId)
|
||||
}
|
||||
window.sendMessage("addUserSettings", {
|
||||
keyValue: {
|
||||
key: 'mutedGroups',
|
||||
value,
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
if (response?.error) {
|
||||
console.error("Error adding user settings:", response.error);
|
||||
} else {
|
||||
console.log("User settings added successfully");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Failed to add user settings:", error.message || "An error occurred");
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
getUserSettings()
|
||||
}, 400);
|
||||
|
||||
} catch (error) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
const handleClose = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.stopPropagation();
|
||||
setMenuPosition(null);
|
||||
};
|
||||
|
||||
const handleSetGroupMute = () => {
|
||||
const value = isMuted
|
||||
? mutedGroups.filter((group) => group !== groupId)
|
||||
: [...mutedGroups, groupId];
|
||||
|
||||
window
|
||||
.sendMessage("addUserSettings", {
|
||||
keyValue: { key: 'mutedGroups', value },
|
||||
})
|
||||
.then((response) => {
|
||||
if (response?.error) console.error("Error adding user settings:", response.error);
|
||||
else console.log("User settings added successfully");
|
||||
})
|
||||
.catch((error) => console.error("Failed to add user settings:", error.message));
|
||||
|
||||
setTimeout(() => getUserSettings(), 400);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
onContextMenu={handleContextMenu} // For desktop right-click
|
||||
onTouchStart={handleTouchStart} // For mobile long-press start
|
||||
onTouchEnd={handleTouchEnd} // For mobile long-press end
|
||||
|
||||
onContextMenu={handleContextMenu}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchMove={handleTouchMove}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
>
|
||||
{children}
|
||||
@ -131,16 +133,14 @@ export const ContextMenu = ({ children, groupId, getUserSettings, mutedGroups })
|
||||
? { top: menuPosition.mouseY, left: menuPosition.mouseX }
|
||||
: undefined
|
||||
}
|
||||
onClick={(e)=> {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<MenuItem onClick={(e) => {
|
||||
handleClose(e)
|
||||
executeEvent("markAsRead", {
|
||||
groupId
|
||||
});
|
||||
}}>
|
||||
<MenuItem
|
||||
onClick={(e) => {
|
||||
handleClose(e);
|
||||
executeEvent("markAsRead", { groupId });
|
||||
}}
|
||||
>
|
||||
<ListItemIcon sx={{ minWidth: '32px' }}>
|
||||
<MailOutlineIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
@ -148,18 +148,22 @@ export const ContextMenu = ({ children, groupId, getUserSettings, mutedGroups })
|
||||
Mark As Read
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={(e) => {
|
||||
|
||||
handleClose(e)
|
||||
handleSetGroupMute()
|
||||
|
||||
}}>
|
||||
<MenuItem
|
||||
onClick={(e) => {
|
||||
handleClose(e);
|
||||
handleSetGroupMute();
|
||||
}}
|
||||
>
|
||||
<ListItemIcon sx={{ minWidth: '32px' }}>
|
||||
<NotificationsOffIcon fontSize="small" sx={{
|
||||
color: isMuted && 'red'
|
||||
}} />
|
||||
<NotificationsOffIcon
|
||||
fontSize="small"
|
||||
sx={{ color: isMuted && 'red' }}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
<Typography variant="inherit" sx={{ fontSize: '14px', color: isMuted && 'red' }}>
|
||||
<Typography
|
||||
variant="inherit"
|
||||
sx={{ fontSize: '14px', color: isMuted && 'red' }}
|
||||
>
|
||||
{isMuted ? 'Unmute ' : 'Mute '}Push Notifications
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
@ -167,5 +171,3 @@ export const ContextMenu = ({ children, groupId, getUserSettings, mutedGroups })
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
@ -729,7 +729,7 @@ font-size: 23px;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
line-height: normal;
|
||||
white-space: nowrap;
|
||||
white-space: wrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
`
|
||||
|
@ -93,6 +93,7 @@ import { AppsNavBar } from "../Apps/AppsNavBar";
|
||||
import { AppsDesktop } from "../Apps/AppsDesktop";
|
||||
import { formatEmailDate } from "./QMailMessages";
|
||||
import { useHandleMobileNativeBack } from "../../hooks/useHandleMobileNativeBack";
|
||||
import { AdminSpace } from "../Chat/AdminSpace";
|
||||
|
||||
// let touchStartY = 0;
|
||||
// let disablePullToRefresh = false;
|
||||
@ -2308,7 +2309,7 @@ export const Group = ({
|
||||
handleNewEncryptionNotification={
|
||||
setNewEncryptionNotification
|
||||
}
|
||||
hide={groupSection !== "chat" || !secretKey}
|
||||
hide={groupSection !== "chat" || !secretKey || selectedDirect || newChat}
|
||||
|
||||
handleSecretKeyCreationInProgress={
|
||||
handleSecretKeyCreationInProgress
|
||||
@ -2422,6 +2423,7 @@ export const Group = ({
|
||||
defaultThread={defaultThread}
|
||||
setDefaultThread={setDefaultThread}
|
||||
/>
|
||||
<AdminSpace adminsWithNames={adminsWithNames} selectedGroup={selectedGroup?.groupId} myAddress={myAddress} userInfo={userInfo} hide={groupSection !== "adminSpace"} isAdmin={admins.includes(myAddress)} />
|
||||
</>
|
||||
)}
|
||||
|
||||
|
@ -15,6 +15,7 @@ import { NotificationIcon2 } from "../../assets/Icons/NotificationIcon2";
|
||||
import { ChatIcon } from "../../assets/Icons/ChatIcon";
|
||||
import { ThreadsIcon } from "../../assets/Icons/ThreadsIcon";
|
||||
import { MembersIcon } from "../../assets/Icons/MembersIcon";
|
||||
import { AdminsIcon } from "../../assets/Icons/AdminsIcon";
|
||||
|
||||
export const GroupMenu = ({ setGroupSection, groupSection, setOpenManageMembers, goToAnnouncements, goToChat, hasUnreadChat, hasUnreadAnnouncements }) => {
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
@ -80,6 +81,9 @@ export const GroupMenu = ({ setGroupSection, groupSection, setOpenManageMembers,
|
||||
)}
|
||||
{groupSection === "forum" &&(
|
||||
<> <ThreadsIcon color={hasUnreadAnnouncements || hasUnreadChat ? 'var(--unread)' : 'white'} /> {" Threads"}</>
|
||||
)}
|
||||
{groupSection === "adminSpace" &&(
|
||||
<> <AdminsIcon height={15} width={15} color={hasUnreadAnnouncements || hasUnreadChat ? 'var(--unread)' : 'white'} /> {" Admins"}</>
|
||||
)}
|
||||
</Box>
|
||||
<ArrowDownIcon color="white" />
|
||||
@ -196,6 +200,25 @@ export const GroupMenu = ({ setGroupSection, groupSection, setOpenManageMembers,
|
||||
},
|
||||
}} primary="Members" />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
setGroupSection("adminSpace");
|
||||
handleClose();
|
||||
}}
|
||||
>
|
||||
<ListItemIcon sx={{
|
||||
minWidth: '24px !important'
|
||||
}}>
|
||||
<AdminsIcon color={"#fff"} height={15} width={15} />
|
||||
|
||||
</ListItemIcon>
|
||||
<ListItemText sx={{
|
||||
"& .MuiTypography-root": {
|
||||
fontSize: "12px",
|
||||
fontWeight: 600,
|
||||
},
|
||||
}} primary="Admins" />
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Box>
|
||||
);
|
||||
|
@ -44,6 +44,7 @@ import signTradeBotTransaction from "../transactions/signTradeBotTransaction";
|
||||
import { executeEvent } from "../utils/events";
|
||||
import { extractComponents } from "../components/Chat/MessageDisplay";
|
||||
import { decryptResource, getGroupAdmins, getPublishesFromAdmins, validateSecretKey } from "../components/Group/Group";
|
||||
import { getPublishesFromAdminsAdminSpace } from "../components/Chat/AdminSpaceInner";
|
||||
|
||||
const btcFeePerByte = 0.00000100
|
||||
const ltcFeePerByte = 0.00000030
|
||||
@ -382,10 +383,11 @@ export const encryptData = async (data, sender) => {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export const encryptQortalGroupData = async (data, sender) => {
|
||||
let data64 = data.data64;
|
||||
let groupId = data?.groupId
|
||||
|
||||
let isAdmins = data?.isAdmins
|
||||
if(!groupId){
|
||||
throw new Error('Please provide a groupId')
|
||||
}
|
||||
@ -395,7 +397,10 @@ export const encryptQortalGroupData = async (data, sender) => {
|
||||
if (!data64) {
|
||||
throw new Error("Please include data to encrypt");
|
||||
}
|
||||
|
||||
|
||||
let secretKeyObject
|
||||
if(!isAdmins){
|
||||
if(groupSecretkeys[groupId] && groupSecretkeys[groupId].secretKeyObject && groupSecretkeys[groupId]?.timestamp && (Date.now() - groupSecretkeys[groupId]?.timestamp) < 1200000){
|
||||
secretKeyObject = groupSecretkeys[groupId].secretKeyObject
|
||||
}
|
||||
@ -428,7 +433,44 @@ url
|
||||
timestamp: Date.now()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
if(groupSecretkeys[`admins-${groupId}`] && groupSecretkeys[`admins-${groupId}`].secretKeyObject && groupSecretkeys[`admins-${groupId}`]?.timestamp && (Date.now() - groupSecretkeys[`admins-${groupId}`]?.timestamp) < 1200000){
|
||||
secretKeyObject = groupSecretkeys[`admins-${groupId}`].secretKeyObject
|
||||
}
|
||||
|
||||
if(!secretKeyObject){
|
||||
const { names } =
|
||||
await getGroupAdmins(groupId)
|
||||
|
||||
const publish =
|
||||
await getPublishesFromAdminsAdminSpace(names, groupId);
|
||||
if(publish === false) throw new Error('No group key found.')
|
||||
const url = await createEndpoint(`/arbitrary/DOCUMENT_PRIVATE/${publish.name}/${
|
||||
publish.identifier
|
||||
}?encoding=base64`);
|
||||
|
||||
const res = await fetch(
|
||||
url
|
||||
);
|
||||
const resData = await res.text();
|
||||
const decryptedKey: any = await decryptResource(resData);
|
||||
|
||||
const dataint8Array = base64ToUint8Array(decryptedKey.data);
|
||||
const decryptedKeyToObject = uint8ArrayToObject(dataint8Array);
|
||||
|
||||
if (!validateSecretKey(decryptedKeyToObject))
|
||||
throw new Error("SecretKey is not valid");
|
||||
secretKeyObject = decryptedKeyToObject
|
||||
groupSecretkeys[`admins-${groupId}`] = {
|
||||
secretKeyObject,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
const resGroupEncryptedResource = encryptSingle({
|
||||
data64, secretKeyObject: secretKeyObject,
|
||||
@ -444,17 +486,17 @@ url
|
||||
export const decryptQortalGroupData = async (data, sender) => {
|
||||
let data64 = data.data64;
|
||||
let groupId = data?.groupId
|
||||
let isAdmins = data?.isAdmins
|
||||
if(!groupId){
|
||||
throw new Error('Please provide a groupId')
|
||||
}
|
||||
if (data.fileId) {
|
||||
data64 = await getFileFromContentScript(data.fileId);
|
||||
}
|
||||
|
||||
if (!data64) {
|
||||
throw new Error("Please include data to encrypt");
|
||||
}
|
||||
|
||||
let secretKeyObject
|
||||
if(!isAdmins){
|
||||
if(groupSecretkeys[groupId] && groupSecretkeys[groupId].secretKeyObject && groupSecretkeys[groupId]?.timestamp && (Date.now() - groupSecretkeys[groupId]?.timestamp) < 1200000){
|
||||
secretKeyObject = groupSecretkeys[groupId].secretKeyObject
|
||||
}
|
||||
@ -485,6 +527,40 @@ url
|
||||
timestamp: Date.now()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if(groupSecretkeys[`admins-${groupId}`] && groupSecretkeys[`admins-${groupId}`].secretKeyObject && groupSecretkeys[`admins-${groupId}`]?.timestamp && (Date.now() - groupSecretkeys[`admins-${groupId}`]?.timestamp) < 1200000){
|
||||
secretKeyObject = groupSecretkeys[`admins-${groupId}`].secretKeyObject
|
||||
}
|
||||
if(!secretKeyObject){
|
||||
const { names } =
|
||||
await getGroupAdmins(groupId)
|
||||
|
||||
const publish =
|
||||
await getPublishesFromAdminsAdminSpace(names, groupId);
|
||||
if(publish === false) throw new Error('No group key found.')
|
||||
const url = await createEndpoint(`/arbitrary/DOCUMENT_PRIVATE/${publish.name}/${
|
||||
publish.identifier
|
||||
}?encoding=base64`);
|
||||
|
||||
const res = await fetch(
|
||||
url
|
||||
);
|
||||
const resData = await res.text();
|
||||
const decryptedKey: any = await decryptResource(resData);
|
||||
|
||||
const dataint8Array = base64ToUint8Array(decryptedKey.data);
|
||||
const decryptedKeyToObject = uint8ArrayToObject(dataint8Array);
|
||||
if (!validateSecretKey(decryptedKeyToObject))
|
||||
throw new Error("SecretKey is not valid");
|
||||
secretKeyObject = decryptedKeyToObject
|
||||
groupSecretkeys[`admins-${groupId}`] = {
|
||||
secretKeyObject,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
const resGroupDecryptResource = decryptSingle({
|
||||
data64, secretKeyObject: secretKeyObject, skipDecodeBase64: true
|
||||
|
Loading…
x
Reference in New Issue
Block a user