mirror of
https://github.com/Qortal/Qortal-Hub.git
synced 2025-04-23 19:37:52 +00:00
added attachment embed and q-manager to groups
This commit is contained in:
parent
b661bd3869
commit
1f900dd72b
@ -264,7 +264,7 @@ export function setupContentSecurityPolicy(customScheme: string): void {
|
|||||||
frame-src ${frameSources.join(' ')};
|
frame-src ${frameSources.join(' ')};
|
||||||
script-src 'self' 'wasm-unsafe-eval' 'unsafe-inline' 'unsafe-eval' ${allowedSources.join(' ')};
|
script-src 'self' 'wasm-unsafe-eval' 'unsafe-inline' 'unsafe-eval' ${allowedSources.join(' ')};
|
||||||
object-src 'self';
|
object-src 'self';
|
||||||
connect-src ${connectSources.join(' ')};
|
connect-src 'self' blob: ${connectSources.join(' ')};
|
||||||
img-src 'self' data: blob: ${allowedSources.join(' ')};
|
img-src 'self' data: blob: ${allowedSources.join(' ')};
|
||||||
media-src 'self' blob: ${allowedSources.join(' ')};
|
media-src 'self' blob: ${allowedSources.join(' ')};
|
||||||
style-src 'self' 'unsafe-inline';
|
style-src 'self' 'unsafe-inline';
|
||||||
|
@ -126,6 +126,7 @@ import { handleGetFileFromIndexedDB } from "./utils/indexedDB";
|
|||||||
import { CoreSyncStatus } from "./components/CoreSyncStatus";
|
import { CoreSyncStatus } from "./components/CoreSyncStatus";
|
||||||
import { Wallets } from "./Wallets";
|
import { Wallets } from "./Wallets";
|
||||||
import { RandomSentenceGenerator } from "./utils/seedPhrase/RandomSentenceGenerator";
|
import { RandomSentenceGenerator } from "./utils/seedPhrase/RandomSentenceGenerator";
|
||||||
|
import { useFetchResources } from "./common/useFetchResources";
|
||||||
|
|
||||||
type extStates =
|
type extStates =
|
||||||
| "not-authenticated"
|
| "not-authenticated"
|
||||||
@ -334,6 +335,7 @@ function App() {
|
|||||||
const [hasSettingsChanged, setHasSettingsChanged] = useRecoilState(
|
const [hasSettingsChanged, setHasSettingsChanged] = useRecoilState(
|
||||||
hasSettingsChangedAtom
|
hasSettingsChangedAtom
|
||||||
);
|
);
|
||||||
|
const {downloadResource} = useFetchResources()
|
||||||
const holdRefExtState = useRef<extStates>("not-authenticated");
|
const holdRefExtState = useRef<extStates>("not-authenticated");
|
||||||
const isFocusedRef = useRef<boolean>(true);
|
const isFocusedRef = useRef<boolean>(true);
|
||||||
const { isShow, onCancel, onOk, show, message } = useModal();
|
const { isShow, onCancel, onOk, show, message } = useModal();
|
||||||
@ -1580,6 +1582,7 @@ function App() {
|
|||||||
setOpenSnackGlobal: setOpenSnack,
|
setOpenSnackGlobal: setOpenSnack,
|
||||||
infoSnackCustom: infoSnack,
|
infoSnackCustom: infoSnack,
|
||||||
setInfoSnackCustom: setInfoSnack,
|
setInfoSnackCustom: setInfoSnack,
|
||||||
|
downloadResource
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { atom } from 'recoil';
|
import { atom, selectorFamily } from 'recoil';
|
||||||
|
|
||||||
|
|
||||||
export const sortablePinnedAppsAtom = atom({
|
export const sortablePinnedAppsAtom = atom({
|
||||||
@ -93,4 +93,30 @@ export const promotionTimeIntervalAtom = atom({
|
|||||||
export const promotionsAtom = atom({
|
export const promotionsAtom = atom({
|
||||||
key: 'promotionsAtom',
|
key: 'promotionsAtom',
|
||||||
default: [],
|
default: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const resourceDownloadControllerAtom = atom({
|
||||||
|
key: 'resourceDownloadControllerAtom',
|
||||||
|
default: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const resourceKeySelector = selectorFamily({
|
||||||
|
key: 'resourceKeySelector',
|
||||||
|
get: (key) => ({ get }) => {
|
||||||
|
const resources = get(resourceDownloadControllerAtom);
|
||||||
|
return resources[key] || null; // Return the value for the key or null if not found
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const blobControllerAtom = atom({
|
||||||
|
key: 'blobControllerAtom',
|
||||||
|
default: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const blobKeySelector = selectorFamily({
|
||||||
|
key: 'blobKeySelector',
|
||||||
|
get: (key) => ({ get }) => {
|
||||||
|
const blobs = get(blobControllerAtom);
|
||||||
|
return blobs[key] || null; // Return the value for the key or null if not found
|
||||||
|
},
|
||||||
|
});
|
||||||
|
@ -1381,6 +1381,7 @@ export async function encryptAndPublishSymmetricKeyGroupChatCase(
|
|||||||
},
|
},
|
||||||
event.origin
|
event.origin
|
||||||
);
|
);
|
||||||
|
if (!previousData) {
|
||||||
try {
|
try {
|
||||||
sendChatGroup({
|
sendChatGroup({
|
||||||
groupId,
|
groupId,
|
||||||
@ -1391,6 +1392,7 @@ export async function encryptAndPublishSymmetricKeyGroupChatCase(
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
// error in sending chat message
|
// error in sending chat message
|
||||||
}
|
}
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
sendChatNotification(data, groupId, previousData, numberOfMembers);
|
sendChatNotification(data, groupId, previousData, numberOfMembers);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -99,7 +99,7 @@ import {
|
|||||||
import { getData, removeKeysAndLogout, storeData } from "./utils/chromeStorage";
|
import { getData, removeKeysAndLogout, storeData } from "./utils/chromeStorage";
|
||||||
// import {BackgroundFetch} from '@transistorsoft/capacitor-background-fetch';
|
// import {BackgroundFetch} from '@transistorsoft/capacitor-background-fetch';
|
||||||
|
|
||||||
|
export let groupSecretkeys = {}
|
||||||
|
|
||||||
export function cleanUrl(url) {
|
export function cleanUrl(url) {
|
||||||
return url?.replace(/^(https?:\/\/)?(www\.)?/, "");
|
return url?.replace(/^(https?:\/\/)?(www\.)?/, "");
|
||||||
@ -1728,6 +1728,7 @@ export async function decryptSingleForPublishes({
|
|||||||
return holdMessages;
|
return holdMessages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function decryptDirectFunc({ messages, involvingAddress }) {
|
export async function decryptDirectFunc({ messages, involvingAddress }) {
|
||||||
const senderPublicKey = await getPublicKey(involvingAddress);
|
const senderPublicKey = await getPublicKey(involvingAddress);
|
||||||
let holdMessages = [];
|
let holdMessages = [];
|
||||||
@ -3018,7 +3019,7 @@ function setupMessageListener() {
|
|||||||
// for announcement notification
|
// for announcement notification
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
}
|
}
|
||||||
|
groupSecretkeys = {}
|
||||||
const wallet = await getSaveWallet();
|
const wallet = await getSaveWallet();
|
||||||
const address = wallet.address0;
|
const address = wallet.address0;
|
||||||
const key1 = `tempPublish-${address}`;
|
const key1 = `tempPublish-${address}`;
|
||||||
|
124
src/common/useFetchResources.tsx
Normal file
124
src/common/useFetchResources.tsx
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { useRecoilState } from 'recoil';
|
||||||
|
import { resourceDownloadControllerAtom } from '../atoms/global';
|
||||||
|
import { getBaseApiReact } from '../App';
|
||||||
|
|
||||||
|
export const useFetchResources = () => {
|
||||||
|
const [resources, setResources] = useRecoilState(resourceDownloadControllerAtom);
|
||||||
|
|
||||||
|
const downloadResource = useCallback(({ service, name, identifier }, build) => {
|
||||||
|
setResources((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[`${service}-${name}-${identifier}`]: {
|
||||||
|
...(prev[`${service}-${name}-${identifier}`] || {}),
|
||||||
|
service,
|
||||||
|
name,
|
||||||
|
identifier,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
try {
|
||||||
|
let isCalling = false;
|
||||||
|
let percentLoaded = 0;
|
||||||
|
let timer = 24;
|
||||||
|
let calledFirstTime = false
|
||||||
|
|
||||||
|
const intervalId = setInterval(async () => {
|
||||||
|
if (isCalling) return;
|
||||||
|
isCalling = true;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let res
|
||||||
|
|
||||||
|
if(!build){
|
||||||
|
const urlFirstTime = `${getBaseApiReact()}/arbitrary/resource/status/${service}/${name}/${identifier}`;
|
||||||
|
const resCall = await fetch(urlFirstTime, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
res = await resCall.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if(build || (calledFirstTime === false && res?.status !== 'READY')){
|
||||||
|
const url = `${getBaseApiReact()}/arbitrary/resource/status/${service}/${name}/${identifier}?build=true`;
|
||||||
|
const resCall = await fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
res = await resCall.json();
|
||||||
|
|
||||||
|
}
|
||||||
|
calledFirstTime = true
|
||||||
|
isCalling = false;
|
||||||
|
|
||||||
|
if (res.localChunkCount) {
|
||||||
|
if (res.percentLoaded) {
|
||||||
|
if (res.percentLoaded === percentLoaded && res.percentLoaded !== 100) {
|
||||||
|
timer = timer - 5;
|
||||||
|
} else {
|
||||||
|
timer = 24;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timer < 0) {
|
||||||
|
timer = 24;
|
||||||
|
isCalling = true;
|
||||||
|
|
||||||
|
// Update Recoil state for refetching
|
||||||
|
setResources((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[`${service}-${name}-${identifier}`]: {
|
||||||
|
...(prev[`${service}-${name}-${identifier}`] || {}),
|
||||||
|
status: {
|
||||||
|
...res,
|
||||||
|
status: 'REFETCHING',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
isCalling = false;
|
||||||
|
downloadResource({ name, service, identifier }, true);
|
||||||
|
}, 25000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
percentLoaded = res.percentLoaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update Recoil state for progress
|
||||||
|
setResources((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[`${service}-${name}-${identifier}`]: {
|
||||||
|
...(prev[`${service}-${name}-${identifier}`] || {}),
|
||||||
|
status: res,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if progress is 100% and clear interval if true
|
||||||
|
if (res?.status === 'READY') {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
|
||||||
|
// Update Recoil state for completion
|
||||||
|
setResources((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[`${service}-${name}-${identifier}`]: {
|
||||||
|
...(prev[`${service}-${name}-${identifier}`] || {}),
|
||||||
|
status: res,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}, !calledFirstTime ? 100 :5000);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during resource fetch:', error);
|
||||||
|
}
|
||||||
|
}, [setResources]);
|
||||||
|
|
||||||
|
return { downloadResource };
|
||||||
|
};
|
@ -3,7 +3,7 @@ import { AppViewer } from './AppViewer';
|
|||||||
import Frame from 'react-frame-component';
|
import Frame from 'react-frame-component';
|
||||||
import { MyContext, isMobile } from '../../App';
|
import { MyContext, isMobile } from '../../App';
|
||||||
|
|
||||||
const AppViewerContainer = React.forwardRef(({ app, isSelected, hide, isDevMode }, ref) => {
|
const AppViewerContainer = React.forwardRef(({ app, isSelected, hide, isDevMode, customHeight }, ref) => {
|
||||||
const { rootHeight } = useContext(MyContext);
|
const { rootHeight } = useContext(MyContext);
|
||||||
|
|
||||||
|
|
||||||
@ -36,7 +36,7 @@ const AppViewerContainer = React.forwardRef(({ app, isSelected, hide, isDevMode
|
|||||||
}
|
}
|
||||||
style={{
|
style={{
|
||||||
display: (!isSelected || hide) && 'none',
|
display: (!isSelected || hide) && 'none',
|
||||||
height: !isMobile ? '100vh' : `calc(${rootHeight} - 60px - 45px)`,
|
height: customHeight ? customHeight : !isMobile ? '100vh' : `calc(${rootHeight} - 60px - 45px)`,
|
||||||
border: 'none',
|
border: 'none',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
|
@ -182,7 +182,7 @@ const UIQortalRequests = [
|
|||||||
'GET_WALLET_BALANCE', 'GET_USER_WALLET_INFO', 'GET_CROSSCHAIN_SERVER_INFO',
|
'GET_WALLET_BALANCE', 'GET_USER_WALLET_INFO', 'GET_CROSSCHAIN_SERVER_INFO',
|
||||||
'GET_TX_ACTIVITY_SUMMARY', 'GET_FOREIGN_FEE', 'UPDATE_FOREIGN_FEE',
|
'GET_TX_ACTIVITY_SUMMARY', 'GET_FOREIGN_FEE', 'UPDATE_FOREIGN_FEE',
|
||||||
'GET_SERVER_CONNECTION_HISTORY', 'SET_CURRENT_FOREIGN_SERVER',
|
'GET_SERVER_CONNECTION_HISTORY', 'SET_CURRENT_FOREIGN_SERVER',
|
||||||
'ADD_FOREIGN_SERVER', 'REMOVE_FOREIGN_SERVER', 'GET_DAY_SUMMARY', 'CREATE_TRADE_BUY_ORDER', 'CREATE_TRADE_SELL_ORDER', 'CANCEL_TRADE_SELL_ORDER', 'IS_USING_GATEWAY', 'ADMIN_ACTION', 'SIGN_TRANSACTION', 'OPEN_NEW_TAB', 'CREATE_AND_COPY_EMBED_LINK'
|
'ADD_FOREIGN_SERVER', 'REMOVE_FOREIGN_SERVER', 'GET_DAY_SUMMARY', 'CREATE_TRADE_BUY_ORDER', 'CREATE_TRADE_SELL_ORDER', 'CANCEL_TRADE_SELL_ORDER', 'IS_USING_GATEWAY', 'ADMIN_ACTION', 'SIGN_TRANSACTION', 'OPEN_NEW_TAB', 'CREATE_AND_COPY_EMBED_LINK', 'DECRYPT_QORTAL_GROUP_DATA'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
@ -481,7 +481,7 @@ isDOMContentLoaded: false
|
|||||||
} else if (
|
} else if (
|
||||||
event?.data?.action === 'PUBLISH_MULTIPLE_QDN_RESOURCES' ||
|
event?.data?.action === 'PUBLISH_MULTIPLE_QDN_RESOURCES' ||
|
||||||
event?.data?.action === 'PUBLISH_QDN_RESOURCE' ||
|
event?.data?.action === 'PUBLISH_QDN_RESOURCE' ||
|
||||||
event?.data?.action === 'ENCRYPT_DATA'
|
event?.data?.action === 'ENCRYPT_DATA' || event?.data?.action === 'ENCRYPT_DATA_WITH_SHARING_KEY' || 'ENCRYPT_QORTAL_GROUP_DATA'
|
||||||
|
|
||||||
) {
|
) {
|
||||||
let data;
|
let data;
|
||||||
|
@ -15,16 +15,18 @@ import { CustomizedSnackbars } from '../Snackbar/Snackbar'
|
|||||||
import { PUBLIC_NOTIFICATION_CODE_FIRST_SECRET_KEY } from '../../constants/codes'
|
import { PUBLIC_NOTIFICATION_CODE_FIRST_SECRET_KEY } from '../../constants/codes'
|
||||||
import { useMessageQueue } from '../../MessageQueueContext'
|
import { useMessageQueue } from '../../MessageQueueContext'
|
||||||
import { executeEvent } from '../../utils/events'
|
import { executeEvent } from '../../utils/events'
|
||||||
import { Box, ButtonBase, Typography } from '@mui/material'
|
import { Box, ButtonBase, Divider, Typography } from '@mui/material'
|
||||||
import ShortUniqueId from "short-unique-id";
|
import ShortUniqueId from "short-unique-id";
|
||||||
import { ReplyPreview } from './MessageItem'
|
import { ReplyPreview } from './MessageItem'
|
||||||
import { ExitIcon } from '../../assets/Icons/ExitIcon'
|
import { ExitIcon } from '../../assets/Icons/ExitIcon'
|
||||||
import { RESOURCE_TYPE_NUMBER_GROUP_CHAT_REACTIONS } from '../../constants/resourceTypes'
|
import { RESOURCE_TYPE_NUMBER_GROUP_CHAT_REACTIONS } from '../../constants/resourceTypes'
|
||||||
import { isExtMsg } from '../../background'
|
import { isExtMsg } from '../../background'
|
||||||
|
import AppViewerContainer from '../Apps/AppViewerContainer'
|
||||||
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
|
|
||||||
const uid = new ShortUniqueId({ length: 5 });
|
const uid = new ShortUniqueId({ length: 5 });
|
||||||
|
|
||||||
export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey, myAddress, handleNewEncryptionNotification, hide, handleSecretKeyCreationInProgress, triedToFetchSecretKey, myName, balance, getTimestampEnterChatParent}) => {
|
export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey, myAddress, handleNewEncryptionNotification, hide, handleSecretKeyCreationInProgress, triedToFetchSecretKey, myName, balance, getTimestampEnterChatParent, hideView}) => {
|
||||||
const [messages, setMessages] = useState([])
|
const [messages, setMessages] = useState([])
|
||||||
const [chatReferences, setChatReferences] = useState({})
|
const [chatReferences, setChatReferences] = useState({})
|
||||||
const [isSending, setIsSending] = useState(false)
|
const [isSending, setIsSending] = useState(false)
|
||||||
@ -36,7 +38,7 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
|
|||||||
const [isFocusedParent, setIsFocusedParent] = useState(false);
|
const [isFocusedParent, setIsFocusedParent] = useState(false);
|
||||||
const [replyMessage, setReplyMessage] = useState(null)
|
const [replyMessage, setReplyMessage] = useState(null)
|
||||||
const [onEditMessage, setOnEditMessage] = useState(null)
|
const [onEditMessage, setOnEditMessage] = useState(null)
|
||||||
|
const [isOpenQManager, setIsOpenQManager] = useState(null)
|
||||||
|
|
||||||
const [messageSize, setMessageSize] = useState(0)
|
const [messageSize, setMessageSize] = useState(0)
|
||||||
const hasInitializedWebsocket = useRef(false)
|
const hasInitializedWebsocket = useRef(false)
|
||||||
@ -143,7 +145,6 @@ const [messageSize, setMessageSize] = useState(0)
|
|||||||
messages?.forEach((message)=> {
|
messages?.forEach((message)=> {
|
||||||
try {
|
try {
|
||||||
const decodeMsg = atob(message.data);
|
const decodeMsg = atob(message.data);
|
||||||
|
|
||||||
if(decodeMsg === PUBLIC_NOTIFICATION_CODE_FIRST_SECRET_KEY){
|
if(decodeMsg === PUBLIC_NOTIFICATION_CODE_FIRST_SECRET_KEY){
|
||||||
handleSecretKeyCreationInProgress()
|
handleSecretKeyCreationInProgress()
|
||||||
return
|
return
|
||||||
@ -174,7 +175,6 @@ const [messageSize, setMessageSize] = useState(0)
|
|||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const decryptMessages = (encryptedMessages: any[], isInitiated: boolean )=> {
|
const decryptMessages = (encryptedMessages: any[], isInitiated: boolean )=> {
|
||||||
try {
|
try {
|
||||||
@ -729,6 +729,10 @@ const clearEditorContent = () => {
|
|||||||
resumeAllQueues()
|
resumeAllQueues()
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const openQManager = useCallback(()=> {
|
||||||
|
setIsOpenQManager(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
@ -741,7 +745,7 @@ const clearEditorContent = () => {
|
|||||||
left: hide && '-100000px',
|
left: hide && '-100000px',
|
||||||
}}>
|
}}>
|
||||||
|
|
||||||
<ChatList enableMentions onReply={onReply} onEdit={onEdit} chatId={selectedGroup} initialMessages={messages} myAddress={myAddress} tempMessages={tempMessages} handleReaction={handleReaction} chatReferences={chatReferences} tempChatReferences={tempChatReferences} members={members} myName={myName} selectedGroup={selectedGroup} />
|
<ChatList openQManager={openQManager} enableMentions onReply={onReply} onEdit={onEdit} chatId={selectedGroup} initialMessages={messages} myAddress={myAddress} tempMessages={tempMessages} handleReaction={handleReaction} chatReferences={chatReferences} tempChatReferences={tempChatReferences} members={members} myName={myName} selectedGroup={selectedGroup} />
|
||||||
|
|
||||||
|
|
||||||
<div style={{
|
<div style={{
|
||||||
@ -894,6 +898,55 @@ const clearEditorContent = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
{/* <button onClick={sendMessage}>send</button> */}
|
{/* <button onClick={sendMessage}>send</button> */}
|
||||||
</div>
|
</div>
|
||||||
|
{isOpenQManager !== null && (
|
||||||
|
<Box sx={{
|
||||||
|
position: 'fixed',
|
||||||
|
height: '600px',
|
||||||
|
|
||||||
|
maxHeight: '100vh',
|
||||||
|
width: '400px',
|
||||||
|
maxWidth: '100vw',
|
||||||
|
backgroundColor: '#27282c',
|
||||||
|
zIndex: 100,
|
||||||
|
bottom: 0,
|
||||||
|
right: 0,
|
||||||
|
overflow: 'hidden',
|
||||||
|
borderTopLeftRadius: '10px',
|
||||||
|
borderTopRightRadius: '10px',
|
||||||
|
display: hideView ? 'none' : isOpenQManager === true ? 'block' : 'none',
|
||||||
|
boxShadow: 4,
|
||||||
|
|
||||||
|
}}>
|
||||||
|
<Box sx={{
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
|
||||||
|
}}>
|
||||||
|
<Box sx={{
|
||||||
|
height: '40px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: '5px',
|
||||||
|
|
||||||
|
justifyContent: 'space-between'
|
||||||
|
}}>
|
||||||
|
<Typography>Q-Manager</Typography>
|
||||||
|
<ButtonBase onClick={()=> {
|
||||||
|
setIsOpenQManager(false)
|
||||||
|
}}><CloseIcon sx={{
|
||||||
|
color: 'white'
|
||||||
|
}} /></ButtonBase>
|
||||||
|
</Box>
|
||||||
|
<Divider />
|
||||||
|
<AppViewerContainer customHeight="560px" app={{
|
||||||
|
tabId: '5558588',
|
||||||
|
name: 'Q-Manager',
|
||||||
|
service: 'APP'
|
||||||
|
}} isSelected />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* <ChatContainerComp messages={formatMessages} /> */}
|
{/* <ChatContainerComp messages={formatMessages} /> */}
|
||||||
<LoadingSnackbar open={isLoading} info={{
|
<LoadingSnackbar open={isLoading} info={{
|
||||||
message: "Loading chat... please wait."
|
message: "Loading chat... please wait."
|
||||||
|
@ -27,6 +27,7 @@ export const ChatList = ({
|
|||||||
myName,
|
myName,
|
||||||
selectedGroup,
|
selectedGroup,
|
||||||
enableMentions,
|
enableMentions,
|
||||||
|
openQManager
|
||||||
}) => {
|
}) => {
|
||||||
const parentRef = useRef();
|
const parentRef = useRef();
|
||||||
const [messages, setMessages] = useState(initialMessages);
|
const [messages, setMessages] = useState(initialMessages);
|
||||||
@ -407,6 +408,7 @@ export const ChatList = ({
|
|||||||
</div>
|
</div>
|
||||||
{enableMentions && (
|
{enableMentions && (
|
||||||
<ChatOptions
|
<ChatOptions
|
||||||
|
openQManager={openQManager}
|
||||||
messages={messages}
|
messages={messages}
|
||||||
goToMessage={goToMessage}
|
goToMessage={goToMessage}
|
||||||
members={members}
|
members={members}
|
||||||
|
@ -12,6 +12,7 @@ import SearchIcon from "@mui/icons-material/Search";
|
|||||||
import { Spacer } from "../../common/Spacer";
|
import { Spacer } from "../../common/Spacer";
|
||||||
import AlternateEmailIcon from "@mui/icons-material/AlternateEmail";
|
import AlternateEmailIcon from "@mui/icons-material/AlternateEmail";
|
||||||
import CloseIcon from "@mui/icons-material/Close";
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
|
import InsertLinkIcon from '@mui/icons-material/InsertLink';
|
||||||
import {
|
import {
|
||||||
AppsSearchContainer,
|
AppsSearchContainer,
|
||||||
AppsSearchLeft,
|
AppsSearchLeft,
|
||||||
@ -42,7 +43,7 @@ const cache = new CellMeasurerCache({
|
|||||||
defaultHeight: 50,
|
defaultHeight: 50,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ChatOptions = ({ messages, goToMessage, members, myName, selectedGroup }) => {
|
export const ChatOptions = ({ messages, goToMessage, members, myName, selectedGroup, openQManager }) => {
|
||||||
const [mode, setMode] = useState("default");
|
const [mode, setMode] = useState("default");
|
||||||
const [searchValue, setSearchValue] = useState("");
|
const [searchValue, setSearchValue] = useState("");
|
||||||
const [selectedMember, setSelectedMember] = useState(0);
|
const [selectedMember, setSelectedMember] = useState(0);
|
||||||
@ -676,6 +677,16 @@ export const ChatOptions = ({ messages, goToMessage, members, myName, selectedGr
|
|||||||
}}>
|
}}>
|
||||||
<SearchIcon />
|
<SearchIcon />
|
||||||
</ButtonBase>
|
</ButtonBase>
|
||||||
|
<ButtonBase onClick={() => {
|
||||||
|
setMode("default")
|
||||||
|
setSearchValue('')
|
||||||
|
setSelectedMember(0)
|
||||||
|
openQManager()
|
||||||
|
}}>
|
||||||
|
<InsertLinkIcon sx={{
|
||||||
|
color: 'white'
|
||||||
|
}} />
|
||||||
|
</ButtonBase>
|
||||||
<ContextMenuMentions getTimestampMention={getTimestampMention} groupId={selectedGroup}>
|
<ContextMenuMentions getTimestampMention={getTimestampMention} groupId={selectedGroup}>
|
||||||
<ButtonBase onClick={() => {
|
<ButtonBase onClick={() => {
|
||||||
setMode("mentions")
|
setMode("mentions")
|
||||||
@ -686,7 +697,10 @@ export const ChatOptions = ({ messages, goToMessage, members, myName, selectedGr
|
|||||||
color: mentionList?.length > 0 && (!lastMentionTimestamp || lastMentionTimestamp < mentionList[0]?.timestamp) ? 'var(--unread)' : 'white'
|
color: mentionList?.length > 0 && (!lastMentionTimestamp || lastMentionTimestamp < mentionList[0]?.timestamp) ? 'var(--unread)' : 'white'
|
||||||
}} />
|
}} />
|
||||||
</ButtonBase>
|
</ButtonBase>
|
||||||
|
|
||||||
</ContextMenuMentions>
|
</ContextMenuMentions>
|
||||||
|
|
||||||
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
297
src/components/Embeds/AttachmentEmbed.tsx
Normal file
297
src/components/Embeds/AttachmentEmbed.tsx
Normal file
@ -0,0 +1,297 @@
|
|||||||
|
import React, { useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import { MyContext, getBaseApiReact } from "../../App";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardHeader,
|
||||||
|
Typography,
|
||||||
|
RadioGroup,
|
||||||
|
Radio,
|
||||||
|
FormControlLabel,
|
||||||
|
Button,
|
||||||
|
Box,
|
||||||
|
ButtonBase,
|
||||||
|
Divider,
|
||||||
|
Dialog,
|
||||||
|
IconButton,
|
||||||
|
CircularProgress,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { base64ToBlobUrl } from "../../utils/fileReading";
|
||||||
|
import { saveFileToDiskGeneric } from "../../utils/generateWallet/generateWallet";
|
||||||
|
import AttachmentIcon from '@mui/icons-material/Attachment';
|
||||||
|
import RefreshIcon from "@mui/icons-material/Refresh";
|
||||||
|
import OpenInNewIcon from "@mui/icons-material/OpenInNew";
|
||||||
|
import { CustomLoader } from "../../common/CustomLoader";
|
||||||
|
import { Spacer } from "../../common/Spacer";
|
||||||
|
import { FileAttachmentContainer, FileAttachmentFont } from "./Embed-styles";
|
||||||
|
import DownloadIcon from "@mui/icons-material/Download";
|
||||||
|
import SaveIcon from '@mui/icons-material/Save';
|
||||||
|
import { useSetRecoilState } from "recoil";
|
||||||
|
import { blobControllerAtom } from "../../atoms/global";
|
||||||
|
|
||||||
|
|
||||||
|
export const AttachmentCard = ({
|
||||||
|
resourceData,
|
||||||
|
resourceDetails,
|
||||||
|
owner,
|
||||||
|
refresh,
|
||||||
|
openExternal,
|
||||||
|
external,
|
||||||
|
isLoadingParent,
|
||||||
|
errorMsg,
|
||||||
|
encryptionType,
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
const [isOpen, setIsOpen] = useState(true);
|
||||||
|
const { downloadResource } = useContext(MyContext);
|
||||||
|
|
||||||
|
const saveToDisk = async ()=> {
|
||||||
|
const { name, service, identifier } = resourceData;
|
||||||
|
|
||||||
|
const url = `${getBaseApiReact()}/arbitrary/${service}/${name}/${identifier}`;
|
||||||
|
fetch(url)
|
||||||
|
.then(response => response.blob())
|
||||||
|
.then(async blob => {
|
||||||
|
await saveFileToDiskGeneric(blob, resourceData?.fileName)
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error("Error fetching the video:", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveToDiskEncrypted = async ()=> {
|
||||||
|
let blobUrl
|
||||||
|
try {
|
||||||
|
const { name, service, identifier,key } = resourceData;
|
||||||
|
|
||||||
|
const url = `${getBaseApiReact()}/arbitrary/${service}/${name}/${identifier}?encoding=base64`;
|
||||||
|
const res = await fetch(url)
|
||||||
|
const data = await res.text();
|
||||||
|
let decryptedData
|
||||||
|
try {
|
||||||
|
if(key && encryptionType === 'private'){
|
||||||
|
decryptedData = await window.sendMessage(
|
||||||
|
"DECRYPT_DATA_WITH_SHARING_KEY",
|
||||||
|
|
||||||
|
{
|
||||||
|
encryptedData: data,
|
||||||
|
key: decodeURIComponent(key),
|
||||||
|
}
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if(encryptionType === 'group'){
|
||||||
|
decryptedData = await window.sendMessage(
|
||||||
|
"DECRYPT_QORTAL_GROUP_DATA",
|
||||||
|
|
||||||
|
{
|
||||||
|
data64: data,
|
||||||
|
groupId: 683,
|
||||||
|
}
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error('Unable to decrypt')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!decryptedData || decryptedData?.error) throw new Error("Could not decrypt data");
|
||||||
|
blobUrl = base64ToBlobUrl(decryptedData, resourceData?.mimeType)
|
||||||
|
const response = await fetch(blobUrl);
|
||||||
|
const blob = await response.blob();
|
||||||
|
await saveFileToDiskGeneric(blob, resourceData?.fileName)
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
} finally {
|
||||||
|
if(blobUrl){
|
||||||
|
URL.revokeObjectURL(blobUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
backgroundColor: "#1F2023",
|
||||||
|
height: "250px",
|
||||||
|
// height: isOpen ? "auto" : "150px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
padding: "16px 16px 0px 16px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AttachmentIcon
|
||||||
|
sx={{
|
||||||
|
color: "white",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Typography>ATTACHMENT embed</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ButtonBase>
|
||||||
|
<RefreshIcon
|
||||||
|
onClick={refresh}
|
||||||
|
sx={{
|
||||||
|
fontSize: "24px",
|
||||||
|
color: "white",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ButtonBase>
|
||||||
|
{external && (
|
||||||
|
<ButtonBase>
|
||||||
|
<OpenInNewIcon
|
||||||
|
onClick={openExternal}
|
||||||
|
sx={{
|
||||||
|
fontSize: "24px",
|
||||||
|
color: "white",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ButtonBase>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
padding: "8px 16px 8px 16px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "12px",
|
||||||
|
color: "white",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Created by {owner}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "12px",
|
||||||
|
color: "cadetblue",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{encryptionType === 'private' ? "ENCRYPTED" : encryptionType === 'group' ? 'GROUP ENCRYPTED' : "Not encrypted"}
|
||||||
|
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Divider sx={{ borderColor: "rgb(255 255 255 / 10%)" }} />
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
width: "100%",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
|
||||||
|
{isLoadingParent && isOpen && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{" "}
|
||||||
|
<CustomLoader />{" "}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{errorMsg && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{" "}
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "14px",
|
||||||
|
color: "var(--unread)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{errorMsg}
|
||||||
|
</Typography>{" "}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<CardContent>
|
||||||
|
{resourceData?.fileName && (
|
||||||
|
<>
|
||||||
|
<Typography sx={{
|
||||||
|
fontSize: '14px'
|
||||||
|
}}>{resourceData?.fileName}</Typography>
|
||||||
|
<Spacer height="10px" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<ButtonBase sx={{
|
||||||
|
width: '90%',
|
||||||
|
maxWidth: '400px'
|
||||||
|
}} onClick={()=> {
|
||||||
|
if(resourceDetails?.status?.status === 'READY'){
|
||||||
|
if(encryptionType){
|
||||||
|
saveToDiskEncrypted()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
saveToDisk()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
downloadResource(resourceData)
|
||||||
|
}}>
|
||||||
|
|
||||||
|
<FileAttachmentContainer >
|
||||||
|
<Typography>{resourceDetails?.status?.status}</Typography>
|
||||||
|
{!resourceDetails && (
|
||||||
|
<>
|
||||||
|
<DownloadIcon />
|
||||||
|
<FileAttachmentFont>Download File</FileAttachmentFont>
|
||||||
|
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{resourceDetails && resourceDetails?.status?.status !== 'READY' && (
|
||||||
|
<>
|
||||||
|
<CircularProgress sx={{
|
||||||
|
color: 'white'
|
||||||
|
}} size={20} />
|
||||||
|
<FileAttachmentFont>Downloading: {resourceDetails?.status?.percentLoaded || '0'}%</FileAttachmentFont>
|
||||||
|
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{resourceDetails && resourceDetails?.status?.status === 'READY' && (
|
||||||
|
<>
|
||||||
|
<SaveIcon />
|
||||||
|
<FileAttachmentFont>Save to Disk</FileAttachmentFont>
|
||||||
|
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
</FileAttachmentContainer>
|
||||||
|
</ButtonBase>
|
||||||
|
|
||||||
|
</CardContent>
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
18
src/components/Embeds/Embed-styles.tsx
Normal file
18
src/components/Embeds/Embed-styles.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Box, Typography, styled } from "@mui/material";
|
||||||
|
|
||||||
|
export const FileAttachmentContainer = styled(Box)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: "5px 10px",
|
||||||
|
border: `1px solid ${theme.palette.text.primary}`,
|
||||||
|
width: "100%",
|
||||||
|
gap: '20px'
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const FileAttachmentFont = styled(Typography)(({ theme }) => ({
|
||||||
|
fontSize: "20px",
|
||||||
|
letterSpacing: 0,
|
||||||
|
fontWeight: 400,
|
||||||
|
userSelect: "none",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
}));
|
File diff suppressed because it is too large
Load Diff
264
src/components/Embeds/ImageEmbed.tsx
Normal file
264
src/components/Embeds/ImageEmbed.tsx
Normal file
@ -0,0 +1,264 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
Typography,
|
||||||
|
|
||||||
|
Box,
|
||||||
|
ButtonBase,
|
||||||
|
Divider,
|
||||||
|
Dialog,
|
||||||
|
IconButton,
|
||||||
|
|
||||||
|
} from "@mui/material";
|
||||||
|
|
||||||
|
import RefreshIcon from "@mui/icons-material/Refresh";
|
||||||
|
import OpenInNewIcon from "@mui/icons-material/OpenInNew";
|
||||||
|
import { CustomLoader } from "../../common/CustomLoader";
|
||||||
|
import ImageIcon from "@mui/icons-material/Image";
|
||||||
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
|
|
||||||
|
export const ImageCard = ({
|
||||||
|
image,
|
||||||
|
fetchImage,
|
||||||
|
owner,
|
||||||
|
refresh,
|
||||||
|
openExternal,
|
||||||
|
external,
|
||||||
|
isLoadingParent,
|
||||||
|
errorMsg,
|
||||||
|
encryptionType,
|
||||||
|
}) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(true);
|
||||||
|
const [height, setHeight] = useState('400px')
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
fetchImage();
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
useEffect(()=> {
|
||||||
|
if(errorMsg){
|
||||||
|
setHeight('300px')
|
||||||
|
}
|
||||||
|
}, [errorMsg])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
backgroundColor: "#1F2023",
|
||||||
|
height: height,
|
||||||
|
transition: "height 0.6s ease-in-out",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
padding: "16px 16px 0px 16px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ImageIcon
|
||||||
|
sx={{
|
||||||
|
color: "white",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Typography>IMAGE embed</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ButtonBase>
|
||||||
|
<RefreshIcon
|
||||||
|
onClick={refresh}
|
||||||
|
sx={{
|
||||||
|
fontSize: "24px",
|
||||||
|
color: "white",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ButtonBase>
|
||||||
|
{external && (
|
||||||
|
<ButtonBase>
|
||||||
|
<OpenInNewIcon
|
||||||
|
onClick={openExternal}
|
||||||
|
sx={{
|
||||||
|
fontSize: "24px",
|
||||||
|
color: "white",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ButtonBase>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
padding: "8px 16px 8px 16px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "12px",
|
||||||
|
color: "white",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Created by {owner}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "12px",
|
||||||
|
color: "cadetblue",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{encryptionType === 'private' ? "ENCRYPTED" : encryptionType === 'group' ? 'GROUP ENCRYPTED' : "Not encrypted"}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Divider sx={{ borderColor: "rgb(255 255 255 / 10%)" }} />
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
width: "100%",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
|
||||||
|
{isLoadingParent && isOpen && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{" "}
|
||||||
|
<CustomLoader />{" "}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{errorMsg && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{" "}
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "14px",
|
||||||
|
color: "var(--unread)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{errorMsg}
|
||||||
|
</Typography>{" "}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<CardContent>
|
||||||
|
<ImageViewer src={image} />
|
||||||
|
</CardContent>
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ImageViewer({ src, alt = "" }) {
|
||||||
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
|
|
||||||
|
const handleOpenFullscreen = () => setIsFullscreen(true);
|
||||||
|
const handleCloseFullscreen = () => setIsFullscreen(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Image in container */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
maxWidth: "100%", // Prevent horizontal overflow
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
onClick={handleOpenFullscreen}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={src}
|
||||||
|
alt={alt}
|
||||||
|
style={{
|
||||||
|
maxWidth: "100%",
|
||||||
|
maxHeight: "450px", // Adjust max height for small containers
|
||||||
|
objectFit: "contain", // Preserve aspect ratio
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Fullscreen Viewer */}
|
||||||
|
<Dialog
|
||||||
|
open={isFullscreen}
|
||||||
|
onClose={handleCloseFullscreen}
|
||||||
|
maxWidth="lg"
|
||||||
|
fullWidth
|
||||||
|
fullScreen
|
||||||
|
sx={{
|
||||||
|
"& .MuiDialog-paper": {
|
||||||
|
margin: 0,
|
||||||
|
maxWidth: "100%",
|
||||||
|
width: "100%",
|
||||||
|
height: "100vh",
|
||||||
|
overflow: "hidden", // Prevent scrollbars
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "relative",
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
backgroundColor: "#000", // Optional: dark background for fullscreen mode
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Close Button */}
|
||||||
|
<IconButton
|
||||||
|
onClick={handleCloseFullscreen}
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 8,
|
||||||
|
right: 8,
|
||||||
|
zIndex: 10,
|
||||||
|
color: "white",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
{/* Fullscreen Image */}
|
||||||
|
<img
|
||||||
|
src={src}
|
||||||
|
alt={alt}
|
||||||
|
style={{
|
||||||
|
maxWidth: "100%",
|
||||||
|
maxHeight: "100%",
|
||||||
|
objectFit: "contain", // Preserve aspect ratio
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
388
src/components/Embeds/PollEmbed.tsx
Normal file
388
src/components/Embeds/PollEmbed.tsx
Normal file
@ -0,0 +1,388 @@
|
|||||||
|
import React, { useContext, useEffect, useState } from "react";
|
||||||
|
import { MyContext } from "../../App";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardHeader,
|
||||||
|
Typography,
|
||||||
|
RadioGroup,
|
||||||
|
Radio,
|
||||||
|
FormControlLabel,
|
||||||
|
Button,
|
||||||
|
Box,
|
||||||
|
ButtonBase,
|
||||||
|
Divider,
|
||||||
|
|
||||||
|
} from "@mui/material";
|
||||||
|
import { getNameInfo } from "../Group/Group";
|
||||||
|
import PollIcon from "@mui/icons-material/Poll";
|
||||||
|
import { getFee } from "../../background";
|
||||||
|
import RefreshIcon from "@mui/icons-material/Refresh";
|
||||||
|
import { Spacer } from "../../common/Spacer";
|
||||||
|
import OpenInNewIcon from "@mui/icons-material/OpenInNew";
|
||||||
|
|
||||||
|
|
||||||
|
export const PollCard = ({
|
||||||
|
poll,
|
||||||
|
setInfoSnack,
|
||||||
|
setOpenSnack,
|
||||||
|
refresh,
|
||||||
|
openExternal,
|
||||||
|
external,
|
||||||
|
isLoadingParent,
|
||||||
|
errorMsg,
|
||||||
|
}) => {
|
||||||
|
const [selectedOption, setSelectedOption] = useState("");
|
||||||
|
const [ownerName, setOwnerName] = useState("");
|
||||||
|
const [showResults, setShowResults] = useState(false);
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const { show, userInfo } = useContext(MyContext);
|
||||||
|
const [isLoadingSubmit, setIsLoadingSubmit] = useState(false);
|
||||||
|
const handleVote = async () => {
|
||||||
|
const fee = await getFee("VOTE_ON_POLL");
|
||||||
|
|
||||||
|
await show({
|
||||||
|
message: `Do you accept this VOTE_ON_POLL transaction? POLLS are public!`,
|
||||||
|
publishFee: fee.fee + " QORT",
|
||||||
|
});
|
||||||
|
setIsLoadingSubmit(true);
|
||||||
|
|
||||||
|
window
|
||||||
|
.sendMessage(
|
||||||
|
"voteOnPoll",
|
||||||
|
{
|
||||||
|
pollName: poll?.info?.pollName,
|
||||||
|
optionIndex: +selectedOption,
|
||||||
|
},
|
||||||
|
60000
|
||||||
|
)
|
||||||
|
.then((response) => {
|
||||||
|
setIsLoadingSubmit(false);
|
||||||
|
if (response.error) {
|
||||||
|
setInfoSnack({
|
||||||
|
type: "error",
|
||||||
|
message: response?.error || "Unable to vote.",
|
||||||
|
});
|
||||||
|
setOpenSnack(true);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
setInfoSnack({
|
||||||
|
type: "success",
|
||||||
|
message:
|
||||||
|
"Successfully voted. Please wait a couple minutes for the network to propogate the changes.",
|
||||||
|
});
|
||||||
|
setOpenSnack(true);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
setIsLoadingSubmit(false);
|
||||||
|
setInfoSnack({
|
||||||
|
type: "error",
|
||||||
|
message: error?.message || "Unable to vote.",
|
||||||
|
});
|
||||||
|
setOpenSnack(true);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getName = async (owner) => {
|
||||||
|
try {
|
||||||
|
const res = await getNameInfo(owner);
|
||||||
|
if (res) {
|
||||||
|
setOwnerName(res);
|
||||||
|
}
|
||||||
|
} catch (error) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (poll?.info?.owner) {
|
||||||
|
getName(poll.info.owner);
|
||||||
|
}
|
||||||
|
}, [poll?.info?.owner]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
backgroundColor: "#1F2023",
|
||||||
|
height: isOpen ? "auto" : "150px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
padding: "16px 16px 0px 16px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PollIcon
|
||||||
|
sx={{
|
||||||
|
color: "white",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Typography>POLL embed</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ButtonBase>
|
||||||
|
<RefreshIcon
|
||||||
|
onClick={refresh}
|
||||||
|
sx={{
|
||||||
|
fontSize: "24px",
|
||||||
|
color: "white",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ButtonBase>
|
||||||
|
{external && (
|
||||||
|
<ButtonBase>
|
||||||
|
<OpenInNewIcon
|
||||||
|
onClick={openExternal}
|
||||||
|
sx={{
|
||||||
|
fontSize: "24px",
|
||||||
|
color: "white",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ButtonBase>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
padding: "8px 16px 8px 16px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "12px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Created by {ownerName || poll?.info?.owner}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Divider sx={{ borderColor: "rgb(255 255 255 / 10%)" }} />
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
width: "100%",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!isOpen && !errorMsg && (
|
||||||
|
<>
|
||||||
|
<Spacer height="5px" />
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="contained"
|
||||||
|
sx={{
|
||||||
|
backgroundColor: "var(--green)",
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Show poll
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{isLoadingParent && isOpen && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{" "}
|
||||||
|
<CustomLoader />{" "}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{errorMsg && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{" "}
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "14px",
|
||||||
|
color: "var(--unread)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{errorMsg}
|
||||||
|
</Typography>{" "}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: isOpen ? "block" : "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardHeader
|
||||||
|
title={poll?.info?.pollName}
|
||||||
|
subheader={poll?.info?.description}
|
||||||
|
sx={{
|
||||||
|
"& .MuiCardHeader-title": {
|
||||||
|
fontSize: "18px", // Custom font size for title
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<CardContent>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "18px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Options
|
||||||
|
</Typography>
|
||||||
|
<RadioGroup
|
||||||
|
value={selectedOption}
|
||||||
|
onChange={(e) => setSelectedOption(e.target.value)}
|
||||||
|
>
|
||||||
|
{poll?.info?.pollOptions?.map((option, index) => (
|
||||||
|
<FormControlLabel
|
||||||
|
key={index}
|
||||||
|
value={index}
|
||||||
|
control={
|
||||||
|
<Radio
|
||||||
|
sx={{
|
||||||
|
color: "white", // Unchecked color
|
||||||
|
"&.Mui-checked": {
|
||||||
|
color: "var(--green)", // Checked color
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={option?.optionName}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</RadioGroup>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "20px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
disabled={!selectedOption || isLoadingSubmit}
|
||||||
|
onClick={handleVote}
|
||||||
|
>
|
||||||
|
Vote
|
||||||
|
</Button>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "14px",
|
||||||
|
fontStyle: "italic",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{" "}
|
||||||
|
{`${poll?.votes?.totalVotes} ${
|
||||||
|
poll?.votes?.totalVotes === 1 ? " vote" : " votes"
|
||||||
|
}`}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Spacer height="10px" />
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "14px",
|
||||||
|
visibility: poll?.votes?.votes?.find(
|
||||||
|
(item) => item?.voterPublicKey === userInfo?.publicKey
|
||||||
|
)
|
||||||
|
? "visible"
|
||||||
|
: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
You've already voted.
|
||||||
|
</Typography>
|
||||||
|
<Spacer height="10px" />
|
||||||
|
{isLoadingSubmit && (
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "12px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Is processing transaction, please wait...
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
<ButtonBase
|
||||||
|
onClick={() => {
|
||||||
|
setShowResults((prev) => !prev);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{showResults ? "hide " : "show "} results
|
||||||
|
</ButtonBase>
|
||||||
|
</CardContent>
|
||||||
|
{showResults && <PollResults votes={poll?.votes} />}
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const PollResults = ({ votes }) => {
|
||||||
|
const maxVotes = Math.max(
|
||||||
|
...votes?.voteCounts?.map((option) => option.voteCount)
|
||||||
|
);
|
||||||
|
const options = votes?.voteCounts;
|
||||||
|
return (
|
||||||
|
<Box sx={{ width: "100%", p: 2 }}>
|
||||||
|
{options
|
||||||
|
.sort((a, b) => b.voteCount - a.voteCount) // Sort options by votes (highest first)
|
||||||
|
.map((option, index) => (
|
||||||
|
<Box key={index} sx={{ mb: 2 }}>
|
||||||
|
<Box sx={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
sx={{ fontWeight: index === 0 ? "bold" : "normal" }}
|
||||||
|
>
|
||||||
|
{`${index + 1}. ${option.optionName}`}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
sx={{ fontWeight: index === 0 ? "bold" : "normal" }}
|
||||||
|
>
|
||||||
|
{option.voteCount} votes
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
mt: 1,
|
||||||
|
height: 10,
|
||||||
|
backgroundColor: "#e0e0e0",
|
||||||
|
borderRadius: 5,
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: `${(option.voteCount / maxVotes) * 100}%`,
|
||||||
|
height: "100%",
|
||||||
|
backgroundColor: index === 0 ? "#3f51b5" : "#f50057",
|
||||||
|
transition: "width 0.3s ease-in-out",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
40
src/components/Embeds/embed-utils.ts
Normal file
40
src/components/Embeds/embed-utils.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
function decodeHTMLEntities(str) {
|
||||||
|
const txt = document.createElement("textarea");
|
||||||
|
txt.innerHTML = str;
|
||||||
|
return txt.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const parseQortalLink = (link) => {
|
||||||
|
const prefix = "qortal://use-embed/";
|
||||||
|
if (!link.startsWith(prefix)) {
|
||||||
|
throw new Error("Invalid link format");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode any HTML entities in the link
|
||||||
|
link = decodeHTMLEntities(link);
|
||||||
|
|
||||||
|
// Separate the type and query string
|
||||||
|
const [typePart, queryPart] = link.slice(prefix.length).split("?");
|
||||||
|
|
||||||
|
// Ensure only the type is parsed
|
||||||
|
const type = typePart.split("/")[0].toUpperCase();
|
||||||
|
|
||||||
|
const params = {};
|
||||||
|
if (queryPart) {
|
||||||
|
const queryPairs = queryPart.split("&");
|
||||||
|
|
||||||
|
queryPairs.forEach((pair) => {
|
||||||
|
const [key, value] = pair.split("=");
|
||||||
|
if (key && value) {
|
||||||
|
const decodedKey = decodeURIComponent(key.trim());
|
||||||
|
const decodedValue = value.trim().replace(
|
||||||
|
/<\/?[^>]+(>|$)/g,
|
||||||
|
"" // Remove any HTML tags
|
||||||
|
);
|
||||||
|
params[decodedKey] = decodedValue;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { type, ...params };
|
||||||
|
};
|
@ -118,6 +118,36 @@ import { formatEmailDate } from "./QMailMessages";
|
|||||||
// }
|
// }
|
||||||
// });
|
// });
|
||||||
|
|
||||||
|
export const getPublishesFromAdmins = async (admins: string[], groupId) => {
|
||||||
|
// const validApi = await findUsableApi();
|
||||||
|
const queryString = admins.map((name) => `name=${name}`).join("&");
|
||||||
|
const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT_PRIVATE&identifier=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 === `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];
|
||||||
|
};
|
||||||
interface GroupProps {
|
interface GroupProps {
|
||||||
myAddress: string;
|
myAddress: string;
|
||||||
isFocused: boolean;
|
isFocused: boolean;
|
||||||
@ -685,36 +715,7 @@ export const Group = ({
|
|||||||
// };
|
// };
|
||||||
// }, [checkGroupListFunc, myAddress]);
|
// }, [checkGroupListFunc, myAddress]);
|
||||||
|
|
||||||
const getPublishesFromAdmins = async (admins: string[]) => {
|
|
||||||
// const validApi = await findUsableApi();
|
|
||||||
const queryString = admins.map((name) => `name=${name}`).join("&");
|
|
||||||
const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT_PRIVATE&identifier=symmetric-qchat-group-${
|
|
||||||
selectedGroup?.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 === `symmetric-qchat-group-${selectedGroup?.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];
|
|
||||||
};
|
|
||||||
const getSecretKey = async (
|
const getSecretKey = async (
|
||||||
loadingGroupParam?: boolean,
|
loadingGroupParam?: boolean,
|
||||||
secretKeyToPublish?: boolean
|
secretKeyToPublish?: boolean
|
||||||
@ -763,7 +764,7 @@ export const Group = ({
|
|||||||
throw new Error("Network error");
|
throw new Error("Network error");
|
||||||
}
|
}
|
||||||
const publish =
|
const publish =
|
||||||
publishFromStorage || (await getPublishesFromAdmins(names));
|
publishFromStorage || (await getPublishesFromAdmins(names, selectedGroup?.groupId));
|
||||||
|
|
||||||
if (prevGroupId !== selectedGroupRef.current.groupId) {
|
if (prevGroupId !== selectedGroupRef.current.groupId) {
|
||||||
if (settimeoutForRefetchSecretKey.current) {
|
if (settimeoutForRefetchSecretKey.current) {
|
||||||
@ -791,12 +792,9 @@ export const Group = ({
|
|||||||
);
|
);
|
||||||
data = await res.text();
|
data = await res.text();
|
||||||
}
|
}
|
||||||
|
|
||||||
const decryptedKey: any = await decryptResource(data);
|
const decryptedKey: any = await decryptResource(data);
|
||||||
|
|
||||||
const dataint8Array = base64ToUint8Array(decryptedKey.data);
|
const dataint8Array = base64ToUint8Array(decryptedKey.data);
|
||||||
const decryptedKeyToObject = uint8ArrayToObject(dataint8Array);
|
const decryptedKeyToObject = uint8ArrayToObject(dataint8Array);
|
||||||
|
|
||||||
if (!validateSecretKey(decryptedKeyToObject))
|
if (!validateSecretKey(decryptedKeyToObject))
|
||||||
throw new Error("SecretKey is not valid");
|
throw new Error("SecretKey is not valid");
|
||||||
setSecretKeyDetails(publish);
|
setSecretKeyDetails(publish);
|
||||||
@ -2478,6 +2476,7 @@ export const Group = ({
|
|||||||
setNewEncryptionNotification
|
setNewEncryptionNotification
|
||||||
}
|
}
|
||||||
hide={groupSection !== "chat" || !secretKey}
|
hide={groupSection !== "chat" || !secretKey}
|
||||||
|
hideView={!(desktopViewMode === 'chat' && selectedGroup)}
|
||||||
handleSecretKeyCreationInProgress={
|
handleSecretKeyCreationInProgress={
|
||||||
handleSecretKeyCreationInProgress
|
handleSecretKeyCreationInProgress
|
||||||
}
|
}
|
||||||
|
@ -65,7 +65,7 @@ export const createSymmetricKeyAndNonce = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export const encryptDataGroup = ({ data64, publicKeys, privateKey, userPublicKey }: any) => {
|
export const encryptDataGroup = ({ data64, publicKeys, privateKey, userPublicKey, customSymmetricKey }: any) => {
|
||||||
|
|
||||||
let combinedPublicKeys = [...publicKeys, userPublicKey]
|
let combinedPublicKeys = [...publicKeys, userPublicKey]
|
||||||
const decodedPrivateKey = Base58.decode(privateKey)
|
const decodedPrivateKey = Base58.decode(privateKey)
|
||||||
@ -77,8 +77,16 @@ export const encryptDataGroup = ({ data64, publicKeys, privateKey, userPublicKey
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
// Generate a random symmetric key for the message.
|
// Generate a random symmetric key for the message.
|
||||||
const messageKey = new Uint8Array(32)
|
|
||||||
|
let messageKey
|
||||||
|
if(customSymmetricKey){
|
||||||
|
messageKey = base64ToUint8Array(customSymmetricKey)
|
||||||
|
} else {
|
||||||
|
messageKey = new Uint8Array(32)
|
||||||
crypto.getRandomValues(messageKey)
|
crypto.getRandomValues(messageKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!messageKey) throw new Error('Cannot create symmetric key')
|
||||||
const nonce = new Uint8Array(24)
|
const nonce = new Uint8Array(24)
|
||||||
crypto.getRandomValues(nonce)
|
crypto.getRandomValues(nonce)
|
||||||
// Encrypt the data with the symmetric key.
|
// Encrypt the data with the symmetric key.
|
||||||
@ -132,7 +140,7 @@ export const encryptDataGroup = ({ data64, publicKeys, privateKey, userPublicKey
|
|||||||
combinedData.set(countArray, combinedData.length - 4)
|
combinedData.set(countArray, combinedData.length - 4)
|
||||||
return uint8ArrayToBase64(combinedData)
|
return uint8ArrayToBase64(combinedData)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.log('error', error)
|
||||||
throw new Error("Error in encrypting data")
|
throw new Error("Error in encrypting data")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -272,6 +280,46 @@ export const decodeBase64ForUIChatMessages = (messages)=> {
|
|||||||
// Convert the decrypted Uint8Array back to a Base64 string
|
// Convert the decrypted Uint8Array back to a Base64 string
|
||||||
return uint8ArrayToBase64(decryptedData);
|
return uint8ArrayToBase64(decryptedData);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const decryptGroupEncryptionWithSharingKey = async ({ data64EncryptedData, key }: any) => {
|
||||||
|
|
||||||
|
const allCombined = base64ToUint8Array(data64EncryptedData)
|
||||||
|
const str = "qortalGroupEncryptedData"
|
||||||
|
const strEncoder = new TextEncoder()
|
||||||
|
const strUint8Array = strEncoder.encode(str)
|
||||||
|
// Extract the nonce
|
||||||
|
const nonceStartPosition = strUint8Array.length
|
||||||
|
const nonceEndPosition = nonceStartPosition + 24 // Nonce is 24 bytes
|
||||||
|
const nonce = allCombined.slice(nonceStartPosition, nonceEndPosition)
|
||||||
|
// Extract the shared keyNonce
|
||||||
|
const keyNonceStartPosition = nonceEndPosition
|
||||||
|
const keyNonceEndPosition = keyNonceStartPosition + 24 // Nonce is 24 bytes
|
||||||
|
const keyNonce = allCombined.slice(keyNonceStartPosition, keyNonceEndPosition)
|
||||||
|
// Extract the sender's public key
|
||||||
|
const senderPublicKeyStartPosition = keyNonceEndPosition
|
||||||
|
const senderPublicKeyEndPosition = senderPublicKeyStartPosition + 32 // Public keys are 32 bytes
|
||||||
|
|
||||||
|
// Calculate count first
|
||||||
|
const countStartPosition = allCombined.length - 4 // 4 bytes before the end, since count is stored in Uint32 (4 bytes)
|
||||||
|
const countArray = allCombined.slice(countStartPosition, countStartPosition + 4)
|
||||||
|
const count = new Uint32Array(countArray.buffer)[0]
|
||||||
|
// Then use count to calculate encryptedData
|
||||||
|
const encryptedDataStartPosition = senderPublicKeyEndPosition // start position of encryptedData
|
||||||
|
const encryptedDataEndPosition = allCombined.length - ((count * (32 + 16)) + 4)
|
||||||
|
const encryptedData = allCombined.slice(encryptedDataStartPosition, encryptedDataEndPosition)
|
||||||
|
const symmetricKey = base64ToUint8Array(key);
|
||||||
|
|
||||||
|
// Decrypt the data using the nonce and messageKey
|
||||||
|
const decryptedData = nacl.secretbox.open(encryptedData, nonce, symmetricKey)
|
||||||
|
|
||||||
|
|
||||||
|
// Check if decryption was successful
|
||||||
|
if (!decryptedData) {
|
||||||
|
throw new Error("Decryption failed");
|
||||||
|
}
|
||||||
|
// Convert the decrypted Uint8Array back to a Base64 string
|
||||||
|
return uint8ArrayToBase64(decryptedData);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { gateways, getApiKeyFromStorage } from "./background";
|
import { gateways, getApiKeyFromStorage } from "./background";
|
||||||
import { addForeignServer, addListItems, adminAction, cancelSellOrder, createAndCopyEmbedLink, createBuyOrder, createPoll, createSellOrder, decryptData, deleteListItems, deployAt, encryptData, getCrossChainServerInfo, getDaySummary, getForeignFee, getListItems, getServerConnectionHistory, getTxActivitySummary, getUserAccount, getUserWallet, getUserWalletInfo, getWalletBalance, joinGroup, openNewTab, publishMultipleQDNResources, publishQDNResource, removeForeignServer, saveFile, sendChatMessage, sendCoin, setCurrentForeignServer, signTransaction, updateForeignFee, voteOnPoll } from "./qortalRequests/get";
|
import { addForeignServer, addListItems, adminAction, cancelSellOrder, createAndCopyEmbedLink, createBuyOrder, createPoll, createSellOrder, decryptData, decryptDataWithSharingKey, decryptQortalGroupData, deleteListItems, deployAt, encryptData, encryptDataWithSharingKey, encryptQortalGroupData, getCrossChainServerInfo, getDaySummary, getForeignFee, getListItems, getServerConnectionHistory, getTxActivitySummary, getUserAccount, getUserWallet, getUserWalletInfo, getWalletBalance, joinGroup, openNewTab, publishMultipleQDNResources, publishQDNResource, removeForeignServer, saveFile, sendChatMessage, sendCoin, setCurrentForeignServer, signTransaction, updateForeignFee, voteOnPoll } from "./qortalRequests/get";
|
||||||
import { getData, storeData } from "./utils/chromeStorage";
|
import { getData, storeData } from "./utils/chromeStorage";
|
||||||
|
|
||||||
|
|
||||||
@ -113,6 +113,44 @@ export const isRunningGateway = async ()=> {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "ENCRYPT_QORTAL_GROUP_DATA": {
|
||||||
|
try {
|
||||||
|
const res = await encryptQortalGroupData(request.payload, event.source);
|
||||||
|
event.source.postMessage({
|
||||||
|
requestId: request.requestId,
|
||||||
|
action: request.action,
|
||||||
|
payload: res,
|
||||||
|
type: "backgroundMessageResponse",
|
||||||
|
}, event.origin);
|
||||||
|
} catch (error) {
|
||||||
|
event.source.postMessage({
|
||||||
|
requestId: request.requestId,
|
||||||
|
action: request.action,
|
||||||
|
error: error?.message,
|
||||||
|
type: "backgroundMessageResponse",
|
||||||
|
}, event.origin);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "DECRYPT_QORTAL_GROUP_DATA": {
|
||||||
|
try {
|
||||||
|
const res = await decryptQortalGroupData(request.payload, event.source);
|
||||||
|
event.source.postMessage({
|
||||||
|
requestId: request.requestId,
|
||||||
|
action: request.action,
|
||||||
|
payload: res,
|
||||||
|
type: "backgroundMessageResponse",
|
||||||
|
}, event.origin);
|
||||||
|
} catch (error) {
|
||||||
|
event.source.postMessage({
|
||||||
|
requestId: request.requestId,
|
||||||
|
action: request.action,
|
||||||
|
error: error?.message,
|
||||||
|
type: "backgroundMessageResponse",
|
||||||
|
}, event.origin);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case "DECRYPT_DATA": {
|
case "DECRYPT_DATA": {
|
||||||
try {
|
try {
|
||||||
@ -749,6 +787,46 @@ export const isRunningGateway = async ()=> {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "ENCRYPT_DATA_WITH_SHARING_KEY": {
|
||||||
|
try {
|
||||||
|
const res = await encryptDataWithSharingKey(request.payload, isFromExtension)
|
||||||
|
event.source.postMessage({
|
||||||
|
requestId: request.requestId,
|
||||||
|
action: request.action,
|
||||||
|
payload: res,
|
||||||
|
type: "backgroundMessageResponse",
|
||||||
|
}, event.origin);
|
||||||
|
} catch (error) {
|
||||||
|
event.source.postMessage({
|
||||||
|
requestId: request.requestId,
|
||||||
|
action: request.action,
|
||||||
|
error: error?.message,
|
||||||
|
type: "backgroundMessageResponse",
|
||||||
|
}, event.origin);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "DECRYPT_DATA_WITH_SHARING_KEY": {
|
||||||
|
try {
|
||||||
|
const res = await decryptDataWithSharingKey(request.payload, isFromExtension)
|
||||||
|
event.source.postMessage({
|
||||||
|
requestId: request.requestId,
|
||||||
|
action: request.action,
|
||||||
|
payload: res,
|
||||||
|
type: "backgroundMessageResponse",
|
||||||
|
}, event.origin);
|
||||||
|
} catch (error) {
|
||||||
|
event.source.postMessage({
|
||||||
|
requestId: request.requestId,
|
||||||
|
action: request.action,
|
||||||
|
error: error?.message,
|
||||||
|
type: "backgroundMessageResponse",
|
||||||
|
}, event.origin);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
|
@ -16,18 +16,25 @@ import {
|
|||||||
createBuyOrderTx,
|
createBuyOrderTx,
|
||||||
performPowTask,
|
performPowTask,
|
||||||
parseErrorResponse,
|
parseErrorResponse,
|
||||||
|
groupSecretkeys,
|
||||||
} from "../background";
|
} from "../background";
|
||||||
import { getNameInfo } from "../backgroundFunctions/encryption";
|
import { getNameInfo, uint8ArrayToObject } from "../backgroundFunctions/encryption";
|
||||||
import { showSaveFilePicker } from "../components/Apps/useQortalMessageListener";
|
import { showSaveFilePicker } from "../components/Apps/useQortalMessageListener";
|
||||||
import { extractComponents } from "../components/Chat/MessageDisplay";
|
import { extractComponents } from "../components/Chat/MessageDisplay";
|
||||||
|
import { decryptResource, getGroupAdmins, getPublishesFromAdmins, validateSecretKey } from "../components/Group/Group";
|
||||||
import { QORT_DECIMALS } from "../constants/constants";
|
import { QORT_DECIMALS } from "../constants/constants";
|
||||||
import Base58 from "../deps/Base58";
|
import Base58 from "../deps/Base58";
|
||||||
import nacl from "../deps/nacl-fast";
|
import nacl from "../deps/nacl-fast";
|
||||||
import {
|
import {
|
||||||
base64ToUint8Array,
|
base64ToUint8Array,
|
||||||
|
createSymmetricKeyAndNonce,
|
||||||
decryptDeprecatedSingle,
|
decryptDeprecatedSingle,
|
||||||
decryptGroupDataQortalRequest,
|
decryptGroupDataQortalRequest,
|
||||||
|
decryptGroupEncryptionWithSharingKey,
|
||||||
|
decryptSingle,
|
||||||
encryptDataGroup,
|
encryptDataGroup,
|
||||||
|
encryptSingle,
|
||||||
|
objectToBase64,
|
||||||
uint8ArrayStartsWith,
|
uint8ArrayStartsWith,
|
||||||
uint8ArrayToBase64,
|
uint8ArrayToBase64,
|
||||||
} from "../qdn/encryption/group-encryption";
|
} from "../qdn/encryption/group-encryption";
|
||||||
@ -391,6 +398,169 @@ export const encryptData = async (data, sender) => {
|
|||||||
throw new Error("Unable to encrypt");
|
throw new Error("Unable to encrypt");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const encryptQortalGroupData = async (data, sender) => {
|
||||||
|
let data64 = data.data64;
|
||||||
|
let groupId = data?.groupId
|
||||||
|
|
||||||
|
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(groupSecretkeys[groupId] && groupSecretkeys[groupId].secretKeyObject && groupSecretkeys[groupId]?.timestamp && (Date.now() - groupSecretkeys[groupId]?.timestamp) < 1200000){
|
||||||
|
secretKeyObject = groupSecretkeys[groupId].secretKeyObject
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!secretKeyObject){
|
||||||
|
const { names } =
|
||||||
|
await getGroupAdmins(groupId)
|
||||||
|
|
||||||
|
const publish =
|
||||||
|
await getPublishesFromAdmins(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[groupId] = {
|
||||||
|
secretKeyObject,
|
||||||
|
timestamp: Date.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const resGroupEncryptedResource = encryptSingle({
|
||||||
|
data64, secretKeyObject: secretKeyObject,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (resGroupEncryptedResource) {
|
||||||
|
return resGroupEncryptedResource;
|
||||||
|
} else {
|
||||||
|
throw new Error("Unable to encrypt");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const decryptQortalGroupData = async (data, sender) => {
|
||||||
|
let data64 = data.data64;
|
||||||
|
let groupId = data?.groupId
|
||||||
|
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(groupSecretkeys[groupId] && groupSecretkeys[groupId].secretKeyObject && groupSecretkeys[groupId]?.timestamp && (Date.now() - groupSecretkeys[groupId]?.timestamp) < 1200000){
|
||||||
|
secretKeyObject = groupSecretkeys[groupId].secretKeyObject
|
||||||
|
}
|
||||||
|
if(!secretKeyObject){
|
||||||
|
const { names } =
|
||||||
|
await getGroupAdmins(groupId)
|
||||||
|
|
||||||
|
const publish =
|
||||||
|
await getPublishesFromAdmins(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[groupId] = {
|
||||||
|
secretKeyObject,
|
||||||
|
timestamp: Date.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resGroupDecryptResource = decryptSingle({
|
||||||
|
data64, secretKeyObject: secretKeyObject, skipDecodeBase64: true
|
||||||
|
})
|
||||||
|
if (resGroupDecryptResource) {
|
||||||
|
return resGroupDecryptResource;
|
||||||
|
} else {
|
||||||
|
throw new Error("Unable to decrypt");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const encryptDataWithSharingKey = async (data, sender) => {
|
||||||
|
let data64 = data.data64;
|
||||||
|
let publicKeys = data.publicKeys || [];
|
||||||
|
if (data.fileId) {
|
||||||
|
data64 = await getFileFromContentScript(data.fileId);
|
||||||
|
}
|
||||||
|
if (!data64) {
|
||||||
|
throw new Error("Please include data to encrypt");
|
||||||
|
}
|
||||||
|
const symmetricKey = createSymmetricKeyAndNonce()
|
||||||
|
const dataObject = {
|
||||||
|
data: data64,
|
||||||
|
key:symmetricKey.messageKey
|
||||||
|
}
|
||||||
|
const dataObjectBase64 = await objectToBase64(dataObject)
|
||||||
|
|
||||||
|
const resKeyPair = await getKeyPair();
|
||||||
|
const parsedData = resKeyPair;
|
||||||
|
const privateKey = parsedData.privateKey;
|
||||||
|
const userPublicKey = parsedData.publicKey;
|
||||||
|
|
||||||
|
const encryptDataResponse = encryptDataGroup({
|
||||||
|
data64: dataObjectBase64,
|
||||||
|
publicKeys: publicKeys,
|
||||||
|
privateKey,
|
||||||
|
userPublicKey,
|
||||||
|
customSymmetricKey: symmetricKey.messageKey
|
||||||
|
});
|
||||||
|
if (encryptDataResponse) {
|
||||||
|
return encryptDataResponse;
|
||||||
|
} else {
|
||||||
|
throw new Error("Unable to encrypt");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const decryptDataWithSharingKey = async (data, sender) => {
|
||||||
|
const { encryptedData, key } = data;
|
||||||
|
|
||||||
|
|
||||||
|
if (!encryptedData) {
|
||||||
|
throw new Error("Please include data to decrypt");
|
||||||
|
}
|
||||||
|
const decryptedData = await decryptGroupEncryptionWithSharingKey({data64EncryptedData: encryptedData, key})
|
||||||
|
const base64ToObject = JSON.parse(atob(decryptedData))
|
||||||
|
if(!base64ToObject.data) throw new Error('No data in the encrypted resource')
|
||||||
|
return base64ToObject.data
|
||||||
|
};
|
||||||
|
|
||||||
export const decryptData = async (data) => {
|
export const decryptData = async (data) => {
|
||||||
const { encryptedData, publicKey } = data;
|
const { encryptedData, publicKey } = data;
|
||||||
|
|
||||||
@ -653,9 +823,7 @@ export const publishQDNResource = async (
|
|||||||
) {
|
) {
|
||||||
throw new Error("Encrypting data requires public keys");
|
throw new Error("Encrypting data requires public keys");
|
||||||
}
|
}
|
||||||
if (!data.encrypt && data.service.endsWith("_PRIVATE")) {
|
|
||||||
throw new Error("Only encrypted data can go into private services");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.encrypt) {
|
if (data.encrypt) {
|
||||||
try {
|
try {
|
||||||
@ -686,7 +854,7 @@ export const publishQDNResource = async (
|
|||||||
text1: "Do you give this application permission to publish to QDN?",
|
text1: "Do you give this application permission to publish to QDN?",
|
||||||
text2: `service: ${service}`,
|
text2: `service: ${service}`,
|
||||||
text3: `identifier: ${identifier || null}`,
|
text3: `identifier: ${identifier || null}`,
|
||||||
highlightedText: `isEncrypted: ${!!data.encrypt}`,
|
highlightedText: data?.externalEncrypt ? `App is externally encrypting the resource. Make sure you trust the app.` : `isEncrypted: ${!!data.encrypt}`,
|
||||||
fee: fee.fee,
|
fee: fee.fee,
|
||||||
},
|
},
|
||||||
isFromExtension
|
isFromExtension
|
||||||
@ -3071,7 +3239,17 @@ const missingFieldsFunc = (data, requiredFields)=> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const encode = (value) => encodeURIComponent(value.trim()); // Helper to encode values
|
const encode = (value) => encodeURIComponent(value.trim()); // Helper to encode values
|
||||||
|
const buildQueryParams = (data) => {
|
||||||
|
const allowedParams= ["name", "service", "identifier", "mimeType", "fileName", "encryptionType", "key"]
|
||||||
|
return Object.entries(data)
|
||||||
|
.map(([key, value]) => {
|
||||||
|
if (value === undefined || value === null || value === false || !allowedParams.includes(key)) return null; // Skip null, undefined, or false
|
||||||
|
if (typeof value === "boolean") return `${key}=${value}`; // Handle boolean values
|
||||||
|
return `${key}=${encode(value)}`; // Encode other values
|
||||||
|
})
|
||||||
|
.filter(Boolean) // Remove null values
|
||||||
|
.join("&"); // Join with `&`
|
||||||
|
};
|
||||||
export const createAndCopyEmbedLink = async (data, isFromExtension) => {
|
export const createAndCopyEmbedLink = async (data, isFromExtension) => {
|
||||||
const requiredFields = [
|
const requiredFields = [
|
||||||
"type",
|
"type",
|
||||||
@ -3103,53 +3281,33 @@ export const createAndCopyEmbedLink = async (data, isFromExtension) => {
|
|||||||
.filter(Boolean) // Remove null values
|
.filter(Boolean) // Remove null values
|
||||||
.join("&"); // Join with `&`
|
.join("&"); // Join with `&`
|
||||||
const link = `qortal://use-embed/POLL?${queryParams}`
|
const link = `qortal://use-embed/POLL?${queryParams}`
|
||||||
|
try {
|
||||||
navigator.clipboard.writeText(link)
|
await navigator.clipboard.writeText(link);
|
||||||
.then(() => {
|
} catch (error) {
|
||||||
executeEvent('openGlobalSnackBar', {
|
throw new Error('Failed to copy to clipboard.')
|
||||||
message: 'Copied link to clipboard',
|
}
|
||||||
type: 'info'
|
|
||||||
})
|
|
||||||
//success
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
executeEvent('openGlobalSnackBar', {
|
|
||||||
message: 'Failed to copy to clipboard',
|
|
||||||
type: 'error'
|
|
||||||
})
|
|
||||||
// error
|
|
||||||
});
|
|
||||||
return link;
|
return link;
|
||||||
}
|
}
|
||||||
case "IMAGE": {
|
case "IMAGE":
|
||||||
|
case "ATTACHMENT":
|
||||||
|
{
|
||||||
missingFieldsFunc(data, [
|
missingFieldsFunc(data, [
|
||||||
"type",
|
"type",
|
||||||
"name",
|
"name",
|
||||||
"service",
|
"service",
|
||||||
"identifier"
|
"identifier"
|
||||||
])
|
])
|
||||||
const queryParams = [
|
if(data?.encryptionType === 'private' && !data?.key){
|
||||||
`name=${encode(data.name)}`,
|
throw new Error('For an encrypted resource, you must provide the key to create the shared link')
|
||||||
`service=${encode(data.service)}`,
|
}
|
||||||
`identifier=${encode(data.identifier)}`,
|
const queryParams = buildQueryParams(data)
|
||||||
data.ref ? `ref=${encode(data.ref)}` : null, // Add only if ref exists
|
|
||||||
]
|
|
||||||
.filter(Boolean) // Remove null values
|
|
||||||
.join("&"); // Join with `&`
|
|
||||||
|
|
||||||
const link = `qortal://use-embed/IMAGE?${queryParams}`;
|
const link = `qortal://use-embed/${data.type}?${queryParams}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(link);
|
await navigator.clipboard.writeText(link);
|
||||||
executeEvent("openGlobalSnackBar", {
|
|
||||||
message: "Copied link to clipboard",
|
|
||||||
type: "info",
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
executeEvent("openGlobalSnackBar", {
|
throw new Error('Failed to copy to clipboard.')
|
||||||
message: "Failed to copy to clipboard",
|
|
||||||
type: "error",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return link;
|
return link;
|
||||||
|
@ -54,4 +54,14 @@ export const fileToBase64 = (file) => new Promise(async (resolve, reject) => {
|
|||||||
reject(error)
|
reject(error)
|
||||||
semaphore.release()
|
semaphore.release()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const base64ToBlobUrl = (base64, mimeType = "image/png") => {
|
||||||
|
const binary = atob(base64);
|
||||||
|
const array = [];
|
||||||
|
for (let i = 0; i < binary.length; i++) {
|
||||||
|
array.push(binary.charCodeAt(i));
|
||||||
|
}
|
||||||
|
const blob = new Blob([new Uint8Array(array)], { type: mimeType });
|
||||||
|
return URL.createObjectURL(blob);
|
||||||
|
};
|
@ -7,6 +7,7 @@ import * as WORDLISTS from './wordlists';
|
|||||||
import FileSaver from 'file-saver';
|
import FileSaver from 'file-saver';
|
||||||
|
|
||||||
import { Filesystem, Directory, Encoding } from '@capacitor/filesystem';
|
import { Filesystem, Directory, Encoding } from '@capacitor/filesystem';
|
||||||
|
import { mimeToExtensionMap } from '../memeTypes';
|
||||||
export function generateRandomSentence(template = 'adverb verb noun adjective noun adverb verb noun adjective noun adjective verbed adjective noun', maxWordLength = 0, capitalize = true) {
|
export function generateRandomSentence(template = 'adverb verb noun adjective noun adverb verb noun adjective noun adjective verbed adjective noun', maxWordLength = 0, capitalize = true) {
|
||||||
const partsOfSpeechMap = {
|
const partsOfSpeechMap = {
|
||||||
'noun': 'nouns',
|
'noun': 'nouns',
|
||||||
@ -85,6 +86,10 @@ export const createAccount = async(generatedSeedPhrase)=> {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasExtension = (filename) => {
|
||||||
|
return filename.includes(".") && filename.split(".").pop().length > 0;
|
||||||
|
};
|
||||||
|
|
||||||
export const saveFileToDisk = async (data, qortAddress) => {
|
export const saveFileToDisk = async (data, qortAddress) => {
|
||||||
|
|
||||||
const dataString = JSON.stringify(data);
|
const dataString = JSON.stringify(data);
|
||||||
@ -95,6 +100,19 @@ export const createAccount = async(generatedSeedPhrase)=> {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const saveFileToDiskGeneric = async (blob, filename) => {
|
||||||
|
const timestamp = new Date()
|
||||||
|
.toISOString()
|
||||||
|
.replace(/:/g, "-"); // Safe timestamp for filenames
|
||||||
|
|
||||||
|
const fileExtension = mimeToExtensionMap[blob.type]
|
||||||
|
let fileName = filename || "qortal_file_" + timestamp + "." + fileExtension;
|
||||||
|
fileName = hasExtension(fileName) ? fileName : fileName + "." + fileExtension;
|
||||||
|
|
||||||
|
await FileSaver.saveAs(blob, fileName);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
export const saveSeedPhraseToDisk = async (data) => {
|
export const saveSeedPhraseToDisk = async (data) => {
|
||||||
|
|
||||||
const blob = new Blob([data], { type: 'text/plain;charset=utf-8' })
|
const blob = new Blob([data], { type: 'text/plain;charset=utf-8' })
|
||||||
|
Loading…
x
Reference in New Issue
Block a user