Merge pull request #57 from Qortal/feature/large-files-and-names

Feature/large files and names
This commit is contained in:
Phillip 2025-05-31 22:13:13 +03:00 committed by GitHub
commit 345d192bb5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 508 additions and 164 deletions

View File

@ -766,6 +766,24 @@ function App() {
balanceSetInterval(); balanceSetInterval();
}); });
}; };
const refetchUserInfo = () => {
window
.sendMessage('userInfo')
.then((response) => {
if (response && !response.error) {
setUserInfo(response);
}
})
.catch((error) => {
console.error('Failed to get user info:', error);
});
};
const getBalanceAndUserInfoFunc = () => {
getBalanceFunc();
refetchUserInfo();
};
const getLtcBalanceFunc = () => { const getLtcBalanceFunc = () => {
setLtcBalanceLoading(true); setLtcBalanceLoading(true);
window window
@ -1503,7 +1521,7 @@ function App() {
</TextP> </TextP>
<RefreshIcon <RefreshIcon
onClick={getBalanceFunc} onClick={getBalanceAndUserInfoFunc}
sx={{ sx={{
fontSize: '16px', fontSize: '16px',
cursor: 'pointer', cursor: 'pointer',

View File

@ -1506,6 +1506,7 @@ export async function publishOnQDNCase(request, event) {
try { try {
const { const {
data, data,
name = '',
identifier, identifier,
service, service,
title, title,
@ -1521,6 +1522,7 @@ export async function publishOnQDNCase(request, event) {
const response = await publishOnQDN({ const response = await publishOnQDN({
data, data,
name,
identifier, identifier,
service, service,
title, title,

View File

@ -805,21 +805,22 @@ export async function getNameInfo() {
const wallet = await getSaveWallet(); const wallet = await getSaveWallet();
const address = wallet.address0; const address = wallet.address0;
const validApi = await getBaseApi(); const validApi = await getBaseApi();
const response = await fetch(validApi + '/names/address/' + address); const response = await fetch(validApi + '/names/primary/' + address);
const nameData = await response.json(); const nameData = await response.json();
if (nameData?.length > 0) { if (nameData?.name) {
return nameData[0].name; return nameData.name;
} else { } else {
return ''; return '';
} }
} }
export async function getNameInfoForOthers(address) { export async function getNameInfoForOthers(address) {
if (!address) return '';
const validApi = await getBaseApi(); const validApi = await getBaseApi();
const response = await fetch(validApi + '/names/address/' + address); const response = await fetch(validApi + '/names/primary/' + address);
const nameData = await response.json(); const nameData = await response.json();
if (nameData?.length > 0) { if (nameData?.name) {
return nameData[0].name; return nameData?.name;
} else { } else {
return ''; return '';
} }

View File

@ -1,4 +1,4 @@
import React, { useContext, useEffect, useState } from 'react'; import React, { useCallback, useContext, useEffect, useState } from 'react';
import { import {
AppLibrarySubTitle, AppLibrarySubTitle,
AppPublishTagsContainer, AppPublishTagsContainer,
@ -25,6 +25,7 @@ import { CustomizedSnackbars } from '../Snackbar/Snackbar';
import { getFee } from '../../background/background.ts'; import { getFee } from '../../background/background.ts';
import { fileToBase64 } from '../../utils/fileReading'; import { fileToBase64 } from '../../utils/fileReading';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useSortedMyNames } from '../../hooks/useSortedMyNames';
const CustomSelect = styled(Select)({ const CustomSelect = styled(Select)({
border: '0.5px solid var(--50-white, #FFFFFF80)', border: '0.5px solid var(--50-white, #FFFFFF80)',
@ -58,7 +59,8 @@ const CustomMenuItem = styled(MenuItem)({
// }, // },
}); });
export const AppPublish = ({ names, categories }) => { export const AppPublish = ({ categories, myAddress, myName }) => {
const [names, setNames] = useState([]);
const [name, setName] = useState(''); const [name, setName] = useState('');
const [title, setTitle] = useState(''); const [title, setTitle] = useState('');
const [description, setDescription] = useState(''); const [description, setDescription] = useState('');
@ -148,6 +150,27 @@ export const AppPublish = ({ names, categories }) => {
getQapp(name, appType); getQapp(name, appType);
}, [name, appType]); }, [name, appType]);
const getNames = useCallback(async () => {
if (!myAddress) return;
try {
setIsLoading('Loading names');
const res = await fetch(
`${getBaseApiReact()}/names/address/${myAddress}?limit=0`
);
const data = await res.json();
setNames(data?.map((item) => item.name));
} catch (error) {
console.error(error);
} finally {
setIsLoading('');
}
}, [myAddress]);
useEffect(() => {
getNames();
}, [getNames]);
const mySortedNames = useSortedMyNames(names, myName);
const publishApp = async () => { const publishApp = async () => {
try { try {
const data = { const data = {
@ -194,13 +217,13 @@ export const AppPublish = ({ names, categories }) => {
postProcess: 'capitalizeFirstChar', postProcess: 'capitalizeFirstChar',
}) })
); );
const fileBase64 = await fileToBase64(file);
await new Promise((res, rej) => { await new Promise((res, rej) => {
window window
.sendMessage('publishOnQDN', { .sendMessage('publishOnQDN', {
data: fileBase64, data: file,
service: appType, service: appType,
title, title,
name,
description, description,
category, category,
tag1, tag1,
@ -317,7 +340,7 @@ export const AppPublish = ({ names, categories }) => {
</em> </em>
{/* This is the placeholder item */} {/* This is the placeholder item */}
</CustomMenuItem> </CustomMenuItem>
{names.map((name) => { {mySortedNames.map((name) => {
return <CustomMenuItem value={name}>{name}</CustomMenuItem>; return <CustomMenuItem value={name}>{name}</CustomMenuItem>;
})} })}
</CustomSelect> </CustomSelect>

View File

@ -94,7 +94,7 @@ export const AppViewer = forwardRef<HTMLIFrameElement, AppViewerProps>(
useEffect(() => { useEffect(() => {
const iframe = iframeRef?.current; const iframe = iframeRef?.current;
if (!iframe) return; if (!iframe || !iframe?.src) return;
try { try {
const targetOrigin = new URL(iframe.src).origin; const targetOrigin = new URL(iframe.src).origin;
@ -109,7 +109,7 @@ export const AppViewer = forwardRef<HTMLIFrameElement, AppViewerProps>(
useEffect(() => { useEffect(() => {
const iframe = iframeRef?.current; const iframe = iframeRef?.current;
if (!iframe) return; if (!iframe || !iframe?.src) return;
try { try {
const targetOrigin = new URL(iframe.src).origin; const targetOrigin = new URL(iframe.src).origin;

View File

@ -40,6 +40,7 @@ export const AppsDesktop = ({
hasUnreadGroups, hasUnreadGroups,
setDesktopViewMode, setDesktopViewMode,
desktopViewMode, desktopViewMode,
myAddress,
}) => { }) => {
const [availableQapps, setAvailableQapps] = useState([]); const [availableQapps, setAvailableQapps] = useState([]);
const [selectedAppInfo, setSelectedAppInfo] = useState(null); const [selectedAppInfo, setSelectedAppInfo] = useState(null);
@ -485,6 +486,7 @@ export const AppsDesktop = ({
setMode={setMode} setMode={setMode}
myApp={myApp} myApp={myApp}
myWebsite={myWebsite} myWebsite={myWebsite}
myAddress={myAddress}
/> />
</Box> </Box>
)} )}
@ -515,7 +517,11 @@ export const AppsDesktop = ({
/> />
{mode === 'publish' && !selectedTab && ( {mode === 'publish' && !selectedTab && (
<AppPublish names={myName ? [myName] : []} categories={categories} /> <AppPublish
categories={categories}
myAddress={myAddress}
myName={myName}
/>
)} )}
{tabs.map((tab) => { {tabs.map((tab) => {
@ -552,6 +558,7 @@ export const AppsDesktop = ({
myApp={myApp} myApp={myApp}
myName={myName} myName={myName}
myWebsite={myWebsite} myWebsite={myWebsite}
myAddress={myAddress}
setMode={setMode} setMode={setMode}
/> />
</Box> </Box>

View File

@ -32,17 +32,9 @@ export const AppsDevModeNavBar = () => {
'tutorial', 'tutorial',
]); ]);
const theme = useTheme(); const theme = useTheme();
const [isNewTabWindow, setIsNewTabWindow] = useState(false); const [isNewTabWindow, setIsNewTabWindow] = useState(false);
const tabsRef = useRef(null); const tabsRef = useRef(null);
const [anchorEl, setAnchorEl] = useState(null);
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
useEffect(() => { useEffect(() => {
// Scroll to the last tab whenever the tabs array changes (e.g., when a new tab is added) // Scroll to the last tab whenever the tabs array changes (e.g., when a new tab is added)

View File

@ -22,6 +22,7 @@ export const AppsHomeDesktop = ({
myWebsite, myWebsite,
availableQapps, availableQapps,
myName, myName,
myAddress,
}) => { }) => {
const [qortalUrl, setQortalUrl] = useState(''); const [qortalUrl, setQortalUrl] = useState('');
const theme = useTheme(); const theme = useTheme();
@ -157,7 +158,7 @@ export const AppsHomeDesktop = ({
</AppCircleContainer> </AppCircleContainer>
</ButtonBase> </ButtonBase>
<AppsPrivate myName={myName} /> <AppsPrivate myName={myName} myAddress={myAddress} />
<SortablePinnedApps <SortablePinnedApps
isDesktop={true} isDesktop={true}

View File

@ -1,4 +1,10 @@
import React, { useContext, useMemo, useState } from 'react'; import React, {
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from 'react';
import { import {
Box, Box,
Button, Button,
@ -31,16 +37,20 @@ import {
} from './Apps-styles'; } from './Apps-styles';
import AddIcon from '@mui/icons-material/Add'; import AddIcon from '@mui/icons-material/Add';
import ImageUploader from '../../common/ImageUploader'; import ImageUploader from '../../common/ImageUploader';
import { QORTAL_APP_CONTEXT } from '../../App'; import { getBaseApiReact, QORTAL_APP_CONTEXT } from '../../App';
import { fileToBase64 } from '../../utils/fileReading'; import { fileToBase64 } from '../../utils/fileReading';
import { objectToBase64 } from '../../qdn/encryption/group-encryption'; import { objectToBase64 } from '../../qdn/encryption/group-encryption';
import { getFee } from '../../background/background.ts'; import { getFee } from '../../background/background.ts';
import { useAtom } from 'jotai'; import { useAtom } from 'jotai';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useSortedMyNames } from '../../hooks/useSortedMyNames';
const maxFileSize = 50 * 1024 * 1024; // 50MB const maxFileSize = 50 * 1024 * 1024; // 50MB
export const AppsPrivate = ({ myName }) => { export const AppsPrivate = ({ myName, myAddress }) => {
const [names, setNames] = useState([]);
const [name, setName] = useState(0);
const { openApp } = useHandlePrivateApps(); const { openApp } = useHandlePrivateApps();
const [file, setFile] = useState(null); const [file, setFile] = useState(null);
const [logo, setLogo] = useState(null); const [logo, setLogo] = useState(null);
@ -90,6 +100,8 @@ export const AppsPrivate = ({ myName }) => {
name: '', name: '',
}); });
const mySortedNames = useSortedMyNames(names, myName);
const { getRootProps, getInputProps } = useDropzone({ const { getRootProps, getInputProps } = useDropzone({
accept: { accept: {
'application/zip': ['.zip'], // Only accept zip files 'application/zip': ['.zip'], // Only accept zip files
@ -210,6 +222,8 @@ export const AppsPrivate = ({ myName }) => {
data: decryptedData, data: decryptedData,
identifier: newPrivateAppValues?.identifier, identifier: newPrivateAppValues?.identifier,
service: newPrivateAppValues?.service, service: newPrivateAppValues?.service,
uploadType: 'base64',
name,
}) })
.then((response) => { .then((response) => {
if (!response?.error) { if (!response?.error) {
@ -232,7 +246,7 @@ export const AppsPrivate = ({ myName }) => {
{ {
identifier: newPrivateAppValues?.identifier, identifier: newPrivateAppValues?.identifier,
service: newPrivateAppValues?.service, service: newPrivateAppValues?.service,
name: myName, name,
groupId: selectedGroup, groupId: selectedGroup,
}, },
true true
@ -262,6 +276,22 @@ export const AppsPrivate = ({ myName }) => {
}; };
} }
const getNames = useCallback(async () => {
if (!myAddress) return;
try {
const res = await fetch(
`${getBaseApiReact()}/names/address/${myAddress}?limit=0`
);
const data = await res.json();
setNames(data?.map((item) => item.name));
} catch (error) {
console.error(error);
}
}, [myAddress]);
useEffect(() => {
getNames();
}, [getNames]);
return ( return (
<> <>
<ButtonBase <ButtonBase
@ -558,7 +588,34 @@ export const AppsPrivate = ({ myName }) => {
postProcess: 'capitalizeFirstChar', postProcess: 'capitalizeFirstChar',
})} })}
</PublishQAppChoseFile> </PublishQAppChoseFile>
<Spacer height="20px" />
<Box
sx={{
display: 'flex',
flexDirection: 'column',
gap: '5px',
}}
>
<Label>Select a Qortal name</Label>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={name}
label="Groups where you are an admin"
onChange={(e) => setName(e.target.value)}
>
<MenuItem value={0}>No name selected</MenuItem>
{mySortedNames.map((name) => {
return (
<MenuItem key={name} value={name}>
{name}
</MenuItem>
);
})}
</Select>
</Box>
<Spacer height="20px" /> <Spacer height="20px" />
<Box <Box

View File

@ -866,6 +866,7 @@ export const ChatGroup = ({
data: 'RA==', data: 'RA==',
identifier: onEditMessage?.images[0]?.identifier, identifier: onEditMessage?.images[0]?.identifier,
service: onEditMessage?.images[0]?.service, service: onEditMessage?.images[0]?.service,
uploadType: 'base64',
}); });
} }

View File

@ -35,6 +35,7 @@ import { convert } from 'html-to-text';
import { generateHTML } from '@tiptap/react'; import { generateHTML } from '@tiptap/react';
import ErrorBoundary from '../../common/ErrorBoundary'; import ErrorBoundary from '../../common/ErrorBoundary';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { isHtmlString } from '../../utils/chat';
const extractTextFromHTML = (htmlString = '') => { const extractTextFromHTML = (htmlString = '') => {
return convert(htmlString, { return convert(htmlString, {
@ -76,13 +77,16 @@ export const ChatOptions = ({
return untransformedMessages?.map((item) => { return untransformedMessages?.map((item) => {
if (item?.messageText) { if (item?.messageText) {
let transformedMessage = item?.messageText; let transformedMessage = item?.messageText;
const isHtml = isHtmlString(item?.messageText);
try { try {
transformedMessage = generateHTML(item?.messageText, [ transformedMessage = isHtml
StarterKit, ? item?.messageText
Underline, : generateHTML(item?.messageText, [
Highlight, StarterKit,
Mention, Underline,
]); Highlight,
Mention,
]);
return { return {
...item, ...item,
messageText: transformedMessage, messageText: transformedMessage,

View File

@ -109,6 +109,7 @@ export const GroupAvatar = ({
data: avatarBase64, data: avatarBase64,
identifier: `qortal_group_avatar_${groupId}`, identifier: `qortal_group_avatar_${groupId}`,
service: 'THUMBNAIL', service: 'THUMBNAIL',
uploadType: 'base64',
}) })
.then((response) => { .then((response) => {
if (!response?.error) { if (!response?.error) {

View File

@ -47,7 +47,11 @@ import level8Img from '../../assets/badges/level-8.png';
import level9Img from '../../assets/badges/level-9.png'; import level9Img from '../../assets/badges/level-9.png';
import level10Img from '../../assets/badges/level-10.png'; import level10Img from '../../assets/badges/level-10.png';
import { Embed } from '../Embeds/Embed'; import { Embed } from '../Embeds/Embed';
import { buildImageEmbedLink, messageHasImage } from '../../utils/chat'; import {
buildImageEmbedLink,
isHtmlString,
messageHasImage,
} from '../../utils/chat';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const getBadgeImg = (level) => { const getBadgeImg = (level) => {
@ -135,6 +139,8 @@ export const MessageItem = memo(
const htmlText = useMemo(() => { const htmlText = useMemo(() => {
if (message?.messageText) { if (message?.messageText) {
const isHtml = isHtmlString(message?.messageText);
if (isHtml) return message?.messageText;
return generateHTML(message?.messageText, [ return generateHTML(message?.messageText, [
StarterKit, StarterKit,
Underline, Underline,
@ -147,6 +153,8 @@ export const MessageItem = memo(
const htmlReply = useMemo(() => { const htmlReply = useMemo(() => {
if (reply?.messageText) { if (reply?.messageText) {
const isHtml = isHtmlString(reply?.messageText);
if (isHtml) return reply?.messageText;
return generateHTML(reply?.messageText, [ return generateHTML(reply?.messageText, [
StarterKit, StarterKit,
Underline, Underline,
@ -628,6 +636,18 @@ export const ReplyPreview = ({ message, isEdit = false }) => {
'tutorial', 'tutorial',
]); ]);
const replyMessageText = useMemo(() => {
const isHtml = isHtmlString(message?.messageText);
if (isHtml) return message?.messageText;
return generateHTML(message?.messageText, [
StarterKit,
Underline,
Highlight,
Mention,
TextStyle,
]);
}, [message?.messageText]);
return ( return (
<Box <Box
sx={{ sx={{
@ -673,15 +693,7 @@ export const ReplyPreview = ({ message, isEdit = false }) => {
)} )}
{message?.messageText && ( {message?.messageText && (
<MessageDisplay <MessageDisplay htmlContent={replyMessageText} />
htmlContent={generateHTML(message?.messageText, [
StarterKit,
Underline,
Highlight,
Mention,
TextStyle,
])}
/>
)} )}
{message?.decryptedData?.type === 'notification' ? ( {message?.decryptedData?.type === 'notification' ? (

View File

@ -265,11 +265,11 @@ export const getDataPublishesFunc = async (groupId, type) => {
}; };
export async function getNameInfo(address: string) { export async function getNameInfo(address: string) {
const response = await fetch(`${getBaseApiReact()}/names/address/` + address); const response = await fetch(`${getBaseApiReact()}/names/primary/` + address);
const nameData = await response.json(); const nameData = await response.json();
if (nameData?.length > 0) { if (nameData?.name) {
return nameData[0]?.name; return nameData?.name;
} else { } else {
return ''; return '';
} }
@ -523,7 +523,7 @@ export const Group = ({
}); });
}); });
} catch (error) { } catch (error) {
console.log('error', error); console.error(error);
} }
}, [setMutedGroups]); }, [setMutedGroups]);
@ -2357,6 +2357,7 @@ export const Group = ({
hasUnreadDirects={directChatHasUnread} hasUnreadDirects={directChatHasUnread}
show={desktopViewMode === 'apps'} show={desktopViewMode === 'apps'}
myName={userInfo?.name} myName={userInfo?.name}
myAddress={userInfo?.address}
isGroups={isOpenSideViewGroups} isGroups={isOpenSideViewGroups}
isDirects={isOpenSideViewDirects} isDirects={isOpenSideViewDirects}
hasUnreadGroups={groupChatHasUnread || groupsAnnHasUnread} hasUnreadGroups={groupChatHasUnread || groupsAnnHasUnread}

View File

@ -229,6 +229,7 @@ export const ListOfGroupPromotions = () => {
data: data, data: data,
identifier: identifier, identifier: identifier,
service: 'DOCUMENT', service: 'DOCUMENT',
uploadType: 'base64',
}) })
.then((response) => { .then((response) => {
if (!response?.error) { if (!response?.error) {

View File

@ -104,6 +104,7 @@ export const MainAvatar = ({ myName, balance, setOpenSnack, setInfoSnack }) => {
data: avatarBase64, data: avatarBase64,
identifier: 'qortal_avatar', identifier: 'qortal_avatar',
service: 'THUMBNAIL', service: 'THUMBNAIL',
uploadType: 'base64',
}) })
.then((response) => { .then((response) => {
if (!response?.error) { if (!response?.error) {

View File

@ -87,14 +87,14 @@ export const Minting = ({ setIsOpenMinting, myAddress, show }) => {
const getName = async (address) => { const getName = async (address) => {
try { try {
const response = await fetch( const response = await fetch(
`${getBaseApiReact()}/names/address/${address}` `${getBaseApiReact()}/names/primary/${address}`
); );
const nameData = await response.json(); const nameData = await response.json();
if (nameData?.length > 0) { if (nameData?.name) {
setNames((prev) => { setNames((prev) => {
return { return {
...prev, ...prev,
[address]: nameData[0].name, [address]: nameData?.name,
}; };
}); });
} else { } else {

View File

@ -171,6 +171,7 @@ export const Save = ({ isDesktop, disableWidth, myName }) => {
data: encryptData, data: encryptData,
identifier: 'ext_saved_settings', identifier: 'ext_saved_settings',
service: 'DOCUMENT_PRIVATE', service: 'DOCUMENT_PRIVATE',
uploadType: 'base64',
}) })
.then((response) => { .then((response) => {
if (!response?.error) { if (!response?.error) {

View File

@ -26,15 +26,24 @@ export async function getNameInfo() {
const wallet = await getSaveWallet(); const wallet = await getSaveWallet();
const address = wallet.address0; const address = wallet.address0;
const validApi = await getBaseApi(); const validApi = await getBaseApi();
const response = await fetch(validApi + '/names/address/' + address); const response = await fetch(validApi + '/names/primary/' + address);
const nameData = await response.json(); const nameData = await response.json();
if (nameData?.length > 0) { if (nameData?.name) {
return nameData[0].name; return nameData?.name;
} else { } else {
return ''; return '';
} }
} }
export async function getAllUserNames() {
const wallet = await getSaveWallet();
const address = wallet.address0;
const validApi = await getBaseApi();
const response = await fetch(validApi + '/names/address/' + address);
const nameData = await response.json();
return nameData.map((item) => item.name);
}
async function getKeyPair() { async function getKeyPair() {
const res = await getData<any>('keyPair').catch(() => null); const res = await getData<any>('keyPair').catch(() => null);
if (res) { if (res) {
@ -134,12 +143,12 @@ export const encryptAndPublishSymmetricKeyGroupChat = async ({
if (encryptedData) { if (encryptedData) {
const registeredName = await getNameInfo(); const registeredName = await getNameInfo();
const data = await publishData({ const data = await publishData({
registeredName, data: encryptedData,
file: encryptedData, file: encryptedData,
service: 'DOCUMENT_PRIVATE',
identifier: `symmetric-qchat-group-${groupId}`, identifier: `symmetric-qchat-group-${groupId}`,
uploadType: 'file', registeredName,
isBase64: true, service: 'DOCUMENT_PRIVATE',
uploadType: 'base64',
withFee: true, withFee: true,
}); });
return { return {
@ -202,12 +211,12 @@ export const encryptAndPublishSymmetricKeyGroupChatForAdmins = async ({
if (encryptedData) { if (encryptedData) {
const registeredName = await getNameInfo(); const registeredName = await getNameInfo();
const data = await publishData({ const data = await publishData({
registeredName, data: encryptedData,
file: encryptedData, file: encryptedData,
service: 'DOCUMENT_PRIVATE',
identifier: `admins-symmetric-qchat-group-${groupId}`, identifier: `admins-symmetric-qchat-group-${groupId}`,
uploadType: 'file', registeredName,
isBase64: true, service: 'DOCUMENT_PRIVATE',
uploadType: 'base64',
withFee: true, withFee: true,
}); });
return { return {
@ -240,12 +249,12 @@ export const publishGroupEncryptedResource = async ({
}) })
); );
const data = await publishData({ const data = await publishData({
registeredName, data: encryptedData,
file: encryptedData, file: encryptedData,
service: 'DOCUMENT',
identifier, identifier,
uploadType: 'file', registeredName,
isBase64: true, service: 'DOCUMENT',
uploadType: 'base64',
withFee: true, withFee: true,
}); });
return data; return data;
@ -262,21 +271,22 @@ export const publishGroupEncryptedResource = async ({
}; };
export const publishOnQDN = async ({ export const publishOnQDN = async ({
data,
identifier,
service,
title,
description,
category, category,
data,
description,
identifier,
name,
service,
tag1, tag1,
tag2, tag2,
tag3, tag3,
tag4, tag4,
tag5, tag5,
uploadType = 'file', title,
uploadType = 'base64',
}) => { }) => {
if (data && service) { if (data && service) {
const registeredName = await getNameInfo(); const registeredName = name || (await getNameInfo());
if (!registeredName) if (!registeredName)
throw new Error( throw new Error(
i18n.t('core:message.generic.name_publish', { i18n.t('core:message.generic.name_publish', {
@ -286,11 +296,11 @@ export const publishOnQDN = async ({
const res = await publishData({ const res = await publishData({
registeredName, registeredName,
data,
file: data, file: data,
service, service,
identifier, identifier,
uploadType, uploadType,
isBase64: true,
withFee: true, withFee: true,
title, title,
description, description,

View File

@ -256,6 +256,7 @@ export const listOfAllQortalRequests = [
'UPDATE_GROUP', 'UPDATE_GROUP',
'UPDATE_NAME', 'UPDATE_NAME',
'VOTE_ON_POLL', 'VOTE_ON_POLL',
'GET_PRIMARY_NAME',
]; ];
export const UIQortalRequests = [ export const UIQortalRequests = [
@ -319,6 +320,7 @@ export const UIQortalRequests = [
'UPDATE_GROUP', 'UPDATE_GROUP',
'UPDATE_NAME', 'UPDATE_NAME',
'VOTE_ON_POLL', 'VOTE_ON_POLL',
'GET_PRIMARY_NAME',
]; ];
async function retrieveFileFromIndexedDB(fileId) { async function retrieveFileFromIndexedDB(fileId) {
@ -615,13 +617,22 @@ export const useQortalMessageListener = (
); );
} else if (event?.data?.action === 'SAVE_FILE') { } else if (event?.data?.action === 'SAVE_FILE') {
try { try {
const res = await saveFile(event.data, null, true, { await saveFile(event.data, null, true, {
openSnackGlobal, openSnackGlobal,
setOpenSnackGlobal, setOpenSnackGlobal,
infoSnackCustom, infoSnackCustom,
setInfoSnackCustom, setInfoSnackCustom,
}); });
} catch (error) {} event.ports[0].postMessage({
result: true,
error: null,
});
} catch (error) {
event.ports[0].postMessage({
result: null,
error: error?.message || 'Failed to save file',
});
}
} 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' ||

View File

@ -0,0 +1,11 @@
import { useMemo } from 'react';
export function useSortedMyNames(names, myName) {
return useMemo(() => {
return [...names].sort((a, b) => {
if (a === myName) return -1;
if (b === myName) return 1;
return 0;
});
}, [names, myName]);
}

View File

@ -34,6 +34,44 @@ async function reusablePost(endpoint, _body) {
return data; return data;
} }
async function reusablePostStream(endpoint, _body) {
const url = await createEndpoint(endpoint);
const headers = {};
const response = await fetch(url, {
method: 'POST',
headers,
body: _body,
});
return response; // return the actual response so calling code can use response.ok
}
async function uploadChunkWithRetry(endpoint, formData, index, maxRetries = 3) {
let attempt = 0;
while (attempt < maxRetries) {
try {
const response = await reusablePostStream(endpoint, formData);
if (!response.ok) {
const errorText = await response.text();
throw new Error(errorText);
}
return; // Success
} catch (err) {
attempt++;
console.warn(
`Chunk ${index} failed (attempt ${attempt}): ${err.message}`
);
if (attempt >= maxRetries) {
throw new Error(`Chunk ${index} failed after ${maxRetries} attempts`);
}
// Wait 10 seconds before next retry
await new Promise((res) => setTimeout(res, 10_000));
}
}
}
async function getKeyPair() { async function getKeyPair() {
const res = await getData<any>('keyPair').catch(() => null); const res = await getData<any>('keyPair').catch(() => null);
if (res) { if (res) {
@ -44,23 +82,22 @@ async function getKeyPair() {
} }
export const publishData = async ({ export const publishData = async ({
registeredName,
file,
service,
identifier,
uploadType,
isBase64,
filename,
withFee,
title,
description,
category, category,
data,
description,
feeAmount,
filename,
identifier,
registeredName,
service,
tag1, tag1,
tag2, tag2,
tag3, tag3,
tag4, tag4,
tag5, tag5,
feeAmount, title,
uploadType,
withFee,
}: any) => { }: any) => {
const validateName = async (receiverName: string) => { const validateName = async (receiverName: string) => {
return await reusableGet(`/names/${receiverName}`); return await reusableGet(`/names/${receiverName}`);
@ -186,7 +223,8 @@ export const publishData = async ({
} }
} }
let transactionBytes = await uploadData(registeredName, file, fee); let transactionBytes = await uploadData(registeredName, data, fee);
if (!transactionBytes || transactionBytes.error) { if (!transactionBytes || transactionBytes.error) {
throw new Error(transactionBytes?.message || 'Error when uploading'); throw new Error(transactionBytes?.message || 'Error when uploading');
} else if (transactionBytes.includes('Error 500 Internal Server Error')) { } else if (transactionBytes.includes('Error 500 Internal Server Error')) {
@ -206,79 +244,119 @@ export const publishData = async ({
return signAndProcessRes; return signAndProcessRes;
}; };
const uploadData = async (registeredName: string, file: any, fee: number) => { const uploadData = async (registeredName: string, data: any, fee: number) => {
let postBody = ''; let postBody = '';
let urlSuffix = ''; let urlSuffix = '';
if (file != null) { if (data != null) {
// If we're sending zipped data, make sure to use the /zip version of the POST /arbitrary/* API if (uploadType === 'base64') {
if (uploadType === 'zip') {
urlSuffix = '/zip';
}
// If we're sending file data, use the /base64 version of the POST /arbitrary/* API
else if (uploadType === 'file') {
urlSuffix = '/base64'; urlSuffix = '/base64';
} }
// Base64 encode the file to work around compatibility issues between javascript and java byte arrays if (uploadType === 'base64') {
if (isBase64) { postBody = data;
postBody = file;
}
if (!isBase64) {
let fileBuffer = new Uint8Array(await file.arrayBuffer());
postBody = Buffer.from(fileBuffer).toString('base64');
} }
} else {
throw new Error('No data provided');
} }
let uploadDataUrl = `/arbitrary/${service}/${registeredName}${urlSuffix}`; let uploadDataUrl = `/arbitrary/${service}/${registeredName}`;
let paramQueries = '';
if (identifier?.trim().length > 0) { if (identifier?.trim().length > 0) {
uploadDataUrl = `/arbitrary/${service}/${registeredName}/${identifier}${urlSuffix}`; uploadDataUrl = `/arbitrary/${service}/${registeredName}/${identifier}`;
} }
uploadDataUrl = uploadDataUrl + `?fee=${fee}`; paramQueries = paramQueries + `?fee=${fee}`;
if (filename != null && filename != 'undefined') { if (filename != null && filename != 'undefined') {
uploadDataUrl = paramQueries = paramQueries + '&filename=' + encodeURIComponent(filename);
uploadDataUrl + '&filename=' + encodeURIComponent(filename);
} }
if (title != null && title != 'undefined') { if (title != null && title != 'undefined') {
uploadDataUrl = uploadDataUrl + '&title=' + encodeURIComponent(title); paramQueries = paramQueries + '&title=' + encodeURIComponent(title);
} }
if (description != null && description != 'undefined') { if (description != null && description != 'undefined') {
uploadDataUrl = paramQueries =
uploadDataUrl + '&description=' + encodeURIComponent(description); paramQueries + '&description=' + encodeURIComponent(description);
} }
if (category != null && category != 'undefined') { if (category != null && category != 'undefined') {
uploadDataUrl = paramQueries = paramQueries + '&category=' + encodeURIComponent(category);
uploadDataUrl + '&category=' + encodeURIComponent(category);
} }
if (tag1 != null && tag1 != 'undefined') { if (tag1 != null && tag1 != 'undefined') {
uploadDataUrl = uploadDataUrl + '&tags=' + encodeURIComponent(tag1); paramQueries = paramQueries + '&tags=' + encodeURIComponent(tag1);
} }
if (tag2 != null && tag2 != 'undefined') { if (tag2 != null && tag2 != 'undefined') {
uploadDataUrl = uploadDataUrl + '&tags=' + encodeURIComponent(tag2); paramQueries = paramQueries + '&tags=' + encodeURIComponent(tag2);
} }
if (tag3 != null && tag3 != 'undefined') { if (tag3 != null && tag3 != 'undefined') {
uploadDataUrl = uploadDataUrl + '&tags=' + encodeURIComponent(tag3); paramQueries = paramQueries + '&tags=' + encodeURIComponent(tag3);
} }
if (tag4 != null && tag4 != 'undefined') { if (tag4 != null && tag4 != 'undefined') {
uploadDataUrl = uploadDataUrl + '&tags=' + encodeURIComponent(tag4); paramQueries = paramQueries + '&tags=' + encodeURIComponent(tag4);
} }
if (tag5 != null && tag5 != 'undefined') { if (tag5 != null && tag5 != 'undefined') {
uploadDataUrl = uploadDataUrl + '&tags=' + encodeURIComponent(tag5); paramQueries = paramQueries + '&tags=' + encodeURIComponent(tag5);
}
if (uploadType === 'zip') {
paramQueries = paramQueries + '&isZip=' + true;
} }
return await reusablePost(uploadDataUrl, postBody); if (uploadType === 'base64') {
if (urlSuffix) {
uploadDataUrl = uploadDataUrl + urlSuffix;
}
uploadDataUrl = uploadDataUrl + paramQueries;
return await reusablePost(uploadDataUrl, postBody);
}
const file = data;
const urlCheck = `/arbitrary/check/tmp?totalSize=${file.size}`;
const checkEndpoint = await createEndpoint(urlCheck);
const checkRes = await fetch(checkEndpoint);
if (!checkRes.ok) {
throw new Error('Not enough space on your hard drive');
}
const chunkUrl = uploadDataUrl + `/chunk`;
const chunkSize = 5 * 1024 * 1024; // 5MB
const totalChunks = Math.ceil(file.size / chunkSize);
for (let index = 0; index < totalChunks; index++) {
const start = index * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
const formData = new FormData();
formData.append('chunk', chunk, file.name); // Optional: include filename
formData.append('index', index);
await uploadChunkWithRetry(chunkUrl, formData, index);
}
const finalizeUrl = uploadDataUrl + `/finalize` + paramQueries;
const finalizeEndpoint = await createEndpoint(finalizeUrl);
const response = await fetch(finalizeEndpoint, {
method: 'POST',
headers: {},
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Finalize failed: ${errorText}`);
}
const result = await response.text(); // Base58-encoded unsigned transaction
return result;
}; };
try { try {

View File

@ -38,7 +38,11 @@ import {
getPublicKey, getPublicKey,
transferAsset, transferAsset,
} from '../background/background.ts'; } from '../background/background.ts';
import { getNameInfo, uint8ArrayToObject } from '../encryption/encryption.ts'; import {
getAllUserNames,
getNameInfo,
uint8ArrayToObject,
} from '../encryption/encryption.ts';
import { showSaveFilePicker } from '../hooks/useQortalMessageListener.tsx'; import { showSaveFilePicker } from '../hooks/useQortalMessageListener.tsx';
import { getPublishesFromAdminsAdminSpace } from '../components/Chat/AdminSpaceInner.tsx'; import { getPublishesFromAdminsAdminSpace } from '../components/Chat/AdminSpaceInner.tsx';
import { extractComponents } from '../components/Chat/MessageDisplay.tsx'; import { extractComponents } from '../components/Chat/MessageDisplay.tsx';
@ -1315,7 +1319,7 @@ export const publishQDNResource = async (
if (appFee && appFee > 0 && appFeeRecipient) { if (appFee && appFee > 0 && appFeeRecipient) {
hasAppFee = true; hasAppFee = true;
} }
const registeredName = await getNameInfo(); const registeredName = data?.name || (await getNameInfo());
const name = registeredName; const name = registeredName;
if (!name) { if (!name) {
throw new Error( throw new Error(
@ -1331,7 +1335,7 @@ export const publishQDNResource = async (
const title = data.title; const title = data.title;
const description = data.description; const description = data.description;
const category = data.category; const category = data.category;
const file = data?.file || data?.blob;
const tags = data?.tags || []; const tags = data?.tags || [];
const result = {}; const result = {};
@ -1346,9 +1350,7 @@ export const publishQDNResource = async (
if (data.identifier == null) { if (data.identifier == null) {
identifier = 'default'; identifier = 'default';
} }
if (data?.file || data?.blob) {
data64 = await fileToBase64(data?.file || data?.blob);
}
if ( if (
data.encrypt && data.encrypt &&
(!data.publicKeys || (!data.publicKeys ||
@ -1367,6 +1369,9 @@ export const publishQDNResource = async (
const parsedData = resKeyPair; const parsedData = resKeyPair;
const privateKey = parsedData.privateKey; const privateKey = parsedData.privateKey;
const userPublicKey = parsedData.publicKey; const userPublicKey = parsedData.publicKey;
if (data?.file || data?.blob) {
data64 = await fileToBase64(data?.file || data?.blob);
}
const encryptDataResponse = encryptDataGroup({ const encryptDataResponse = encryptDataGroup({
data64, data64,
publicKeys: data.publicKeys, publicKeys: data.publicKeys,
@ -1410,6 +1415,7 @@ export const publishQDNResource = async (
}), }),
text2: `service: ${service}`, text2: `service: ${service}`,
text3: `identifier: ${identifier || null}`, text3: `identifier: ${identifier || null}`,
text4: `name: ${registeredName}`,
fee: fee.fee, fee: fee.fee,
...handleDynamicValues, ...handleDynamicValues,
}, },
@ -1420,11 +1426,10 @@ export const publishQDNResource = async (
try { try {
const resPublish = await publishData({ const resPublish = await publishData({
registeredName: encodeURIComponent(name), registeredName: encodeURIComponent(name),
file: data64, data: data64 ? data64 : file,
service: service, service: service,
identifier: encodeURIComponent(identifier), identifier: encodeURIComponent(identifier),
uploadType: 'file', uploadType: data64 ? 'base64' : 'file',
isBase64: true,
filename: filename, filename: filename,
title, title,
description, description,
@ -1558,6 +1563,7 @@ export const publishMultipleQDNResources = async (
const fee = await getFee('ARBITRARY'); const fee = await getFee('ARBITRARY');
const registeredName = await getNameInfo(); const registeredName = await getNameInfo();
const name = registeredName; const name = registeredName;
if (!name) { if (!name) {
@ -1568,6 +1574,14 @@ export const publishMultipleQDNResources = async (
); );
} }
const userNames = await getAllUserNames();
data.resources?.forEach((item) => {
if (item?.name && !userNames?.includes(item.name))
throw new Error(
`The name ${item.name}, does not belong to the publisher.`
);
});
const appFee = data?.appFee ? +data.appFee : undefined; const appFee = data?.appFee ? +data.appFee : undefined;
const appFeeRecipient = data?.appFeeRecipient; const appFeeRecipient = data?.appFeeRecipient;
let hasAppFee = false; let hasAppFee = false;
@ -1638,7 +1652,7 @@ export const publishMultipleQDNResources = async (
<div class="resource-detail"><span>Service:</span> ${ <div class="resource-detail"><span>Service:</span> ${
resource.service resource.service
}</div> }</div>
<div class="resource-detail"><span>Name:</span> ${name}</div> <div class="resource-detail"><span>Name:</span> ${resource?.name || name}</div>
<div class="resource-detail"><span>Identifier:</span> ${ <div class="resource-detail"><span>Identifier:</span> ${
resource.identifier resource.identifier
}</div> }</div>
@ -1695,6 +1709,7 @@ export const publishMultipleQDNResources = async (
reason: errorMsg, reason: errorMsg,
identifier: resource.identifier, identifier: resource.identifier,
service: resource.service, service: resource.service,
name: resource?.name || name,
}); });
continue; continue;
} }
@ -1709,12 +1724,13 @@ export const publishMultipleQDNResources = async (
reason: errorMsg, reason: errorMsg,
identifier: resource.identifier, identifier: resource.identifier,
service: resource.service, service: resource.service,
name: resource?.name || name,
}); });
continue; continue;
} }
const service = resource.service; const service = resource.service;
let identifier = resource.identifier; let identifier = resource.identifier;
let data64 = resource?.data64 || resource?.base64; let rawData = resource?.data64 || resource?.base64;
const filename = resource.filename; const filename = resource.filename;
const title = resource.title; const title = resource.title;
const description = resource.description; const description = resource.description;
@ -1741,26 +1757,31 @@ export const publishMultipleQDNResources = async (
reason: errorMsg, reason: errorMsg,
identifier: resource.identifier, identifier: resource.identifier,
service: resource.service, service: resource.service,
name: resource?.name || name,
}); });
continue; continue;
} }
if (resource.file) { if (resource.file) {
data64 = await fileToBase64(resource.file); rawData = resource.file;
} }
if (resourceEncrypt) { if (resourceEncrypt) {
try { try {
if (resource?.file) {
rawData = await fileToBase64(resource.file);
}
const resKeyPair = await getKeyPair(); const resKeyPair = await getKeyPair();
const parsedData = resKeyPair; const parsedData = resKeyPair;
const privateKey = parsedData.privateKey; const privateKey = parsedData.privateKey;
const userPublicKey = parsedData.publicKey; const userPublicKey = parsedData.publicKey;
const encryptDataResponse = encryptDataGroup({ const encryptDataResponse = encryptDataGroup({
data64, data64: rawData,
publicKeys: data.publicKeys, publicKeys: data.publicKeys,
privateKey, privateKey,
userPublicKey, userPublicKey,
}); });
if (encryptDataResponse) { if (encryptDataResponse) {
data64 = encryptDataResponse; rawData = encryptDataResponse;
} }
} catch (error) { } catch (error) {
const errorMsg = const errorMsg =
@ -1772,32 +1793,36 @@ export const publishMultipleQDNResources = async (
reason: errorMsg, reason: errorMsg,
identifier: resource.identifier, identifier: resource.identifier,
service: resource.service, service: resource.service,
name: resource?.name || name,
}); });
continue; continue;
} }
} }
try { try {
const dataType =
resource?.base64 || resource?.data64 || resourceEncrypt
? 'base64'
: 'file';
await retryTransaction( await retryTransaction(
publishData, publishData,
[ [
{ {
registeredName: encodeURIComponent(name), apiVersion: 2,
file: data64,
service: service,
identifier: encodeURIComponent(identifier),
uploadType: 'file',
isBase64: true,
filename: filename,
title,
description,
category, category,
data: rawData,
description,
filename: filename,
identifier: encodeURIComponent(identifier),
registeredName: encodeURIComponent(resource?.name || name),
service: service,
tag1, tag1,
tag2, tag2,
tag3, tag3,
tag4, tag4,
tag5, tag5,
apiVersion: 2, title,
uploadType: dataType,
withFee: true, withFee: true,
}, },
], ],
@ -1818,6 +1843,7 @@ export const publishMultipleQDNResources = async (
reason: errorMsg, reason: errorMsg,
identifier: resource.identifier, identifier: resource.identifier,
service: resource.service, service: resource.service,
name: resource?.name || name,
}); });
} }
} catch (error) { } catch (error) {
@ -1829,6 +1855,7 @@ export const publishMultipleQDNResources = async (
}), }),
identifier: resource.identifier, identifier: resource.identifier,
service: resource.service, service: resource.service,
name: resource?.name || name,
}); });
} }
} }
@ -2324,6 +2351,44 @@ export const joinGroup = async (data, isFromExtension) => {
export const saveFile = async (data, sender, isFromExtension, snackMethods) => { export const saveFile = async (data, sender, isFromExtension, snackMethods) => {
try { try {
if (!data?.filename) throw new Error('Missing filename');
if (data?.location) {
const requiredFieldsLocation = ['service', 'name'];
const missingFieldsLocation: string[] = [];
requiredFieldsLocation.forEach((field) => {
if (!data?.location[field]) {
missingFieldsLocation.push(field);
}
});
if (missingFieldsLocation.length > 0) {
const missingFieldsString = missingFieldsLocation.join(', ');
const errorMsg = `Missing fields: ${missingFieldsString}`;
throw new Error(errorMsg);
}
const resPermission = await getUserPermission(
{
text1: 'Would you like to download:',
highlightedText: `${data?.filename}`,
},
isFromExtension
);
const { accepted } = resPermission;
if (!accepted) throw new Error('User declined to save file');
const a = document.createElement('a');
let locationUrl = `/arbitrary/${data.location.service}/${data.location.name}`;
if (data.location.identifier) {
locationUrl = locationUrl + `/${data.location.identifier}`;
}
const endpoint = await createEndpoint(
locationUrl + `?attachment=true&attachmentFilename=${data?.filename}`
);
a.href = endpoint;
a.download = data.filename;
document.body.appendChild(a);
a.click();
a.remove();
return true;
}
const requiredFields = ['filename', 'blob']; const requiredFields = ['filename', 'blob'];
const missingFields: string[] = []; const missingFields: string[] = [];
requiredFields.forEach((field) => { requiredFields.forEach((field) => {
@ -2341,6 +2406,8 @@ export const saveFile = async (data, sender, isFromExtension, snackMethods) => {
} }
const filename = data.filename; const filename = data.filename;
const blob = data.blob; const blob = data.blob;
const mimeType = blob.type || data.mimeType;
const resPermission = await getUserPermission( const resPermission = await getUserPermission(
{ {
text1: i18n.t('question:download_file', { text1: i18n.t('question:download_file', {
@ -2351,6 +2418,17 @@ export const saveFile = async (data, sender, isFromExtension, snackMethods) => {
isFromExtension isFromExtension
); );
const { accepted } = resPermission; const { accepted } = resPermission;
if (!accepted) throw new Error('User declined to save file'); // TODO translate
showSaveFilePicker(
{
filename,
mimeType,
blob,
},
snackMethods
);
return true;
if (accepted) { if (accepted) {
const mimeType = blob.type || data.mimeType; const mimeType = blob.type || data.mimeType;
@ -5396,11 +5474,10 @@ export const updateNameRequest = async (data, isFromExtension) => {
const fee = await getFee('UPDATE_NAME'); const fee = await getFee('UPDATE_NAME');
const resPermission = await getUserPermission( const resPermission = await getUserPermission(
{ {
text1: i18n.t('question:permission.register_name', { text1: `Do you give this application permission to update this name?`, // TODO translate
postProcess: 'capitalizeFirstChar', text2: `previous name: ${oldName}`,
}), text3: `new name: ${newName}`,
highlightedText: data.newName, text4: data?.description,
text2: data?.description,
fee: fee.fee, fee: fee.fee,
}, },
isFromExtension isFromExtension
@ -6012,7 +6089,7 @@ export const createGroupRequest = async (data, isFromExtension) => {
]; ];
const missingFields: string[] = []; const missingFields: string[] = [];
requiredFields.forEach((field) => { requiredFields.forEach((field) => {
if (data[field] !== undefined && data[field] !== null) { if (data[field] === undefined || data[field] === null) {
missingFields.push(field); missingFields.push(field);
} }
}); });
@ -6076,7 +6153,7 @@ export const updateGroupRequest = async (data, isFromExtension) => {
]; ];
const missingFields: string[] = []; const missingFields: string[] = [];
requiredFields.forEach((field) => { requiredFields.forEach((field) => {
if (data[field] !== undefined && data[field] !== null) { if (data[field] === undefined || data[field] === null) {
missingFields.push(field); missingFields.push(field);
} }
}); });
@ -6250,7 +6327,7 @@ export const sellNameRequest = async (data, isFromExtension) => {
const requiredFields = ['salePrice', 'nameForSale']; const requiredFields = ['salePrice', 'nameForSale'];
const missingFields: string[] = []; const missingFields: string[] = [];
requiredFields.forEach((field) => { requiredFields.forEach((field) => {
if (data[field] !== undefined && data[field] !== null) { if (data[field] === undefined || data[field] === null) {
missingFields.push(field); missingFields.push(field);
} }
}); });
@ -6320,7 +6397,7 @@ export const cancelSellNameRequest = async (data, isFromExtension) => {
const requiredFields = ['nameForSale']; const requiredFields = ['nameForSale'];
const missingFields: string[] = []; const missingFields: string[] = [];
requiredFields.forEach((field) => { requiredFields.forEach((field) => {
if (data[field] !== undefined && data[field] !== null) { if (data[field] === undefined || data[field] === null) {
missingFields.push(field); missingFields.push(field);
} }
}); });
@ -6377,7 +6454,7 @@ export const buyNameRequest = async (data, isFromExtension) => {
const requiredFields = ['nameForSale']; const requiredFields = ['nameForSale'];
const missingFields: string[] = []; const missingFields: string[] = [];
requiredFields.forEach((field) => { requiredFields.forEach((field) => {
if (data[field] !== undefined && data[field] !== null) { if (data[field] === undefined || data[field] === null) {
missingFields.push(field); missingFields.push(field);
} }
}); });
@ -6908,12 +6985,11 @@ export const multiPaymentWithPrivateData = async (data, isFromExtension) => {
[ [
{ {
registeredName: encodeURIComponent(name), registeredName: encodeURIComponent(name),
file: encryptDataResponse, data: encryptDataResponse,
service: transaction.service, service: transaction.service,
identifier: encodeURIComponent(transaction.identifier), identifier: encodeURIComponent(transaction.identifier),
uploadType: 'file', uploadType: 'base64',
description: transaction?.description, description: transaction?.description,
isBase64: true,
apiVersion: 2, apiVersion: 2,
withFee: true, withFee: true,
}, },

View File

@ -1,4 +1,8 @@
import { gateways, getApiKeyFromStorage } from '../background/background.ts'; import {
gateways,
getApiKeyFromStorage,
getNameInfoForOthers,
} from '../background/background.ts';
import { listOfAllQortalRequests } from '../hooks/useQortalMessageListener.tsx'; import { listOfAllQortalRequests } from '../hooks/useQortalMessageListener.tsx';
import { import {
addForeignServer, addForeignServer,
@ -1932,6 +1936,33 @@ function setupMessageListenerQortalRequest() {
break; break;
} }
case 'GET_PRIMARY_NAME': {
try {
const res = await getNameInfoForOthers(request.payload?.address);
const resData = res ? res : null;
event.source.postMessage(
{
requestId: request.requestId,
action: request.action,
payload: resData,
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;
} }

View File

@ -20,3 +20,7 @@ export const messageHasImage = (message) => {
message.images[0]?.service message.images[0]?.service
); );
}; };
export function isHtmlString(value) {
return typeof value === 'string' && /<[^>]+>/.test(value.trim());
}

View File

@ -44,14 +44,14 @@ export function sortArrayByTimestampAndGroupName(array) {
// Both have timestamp, sort by timestamp descending // Both have timestamp, sort by timestamp descending
return b.timestamp - a.timestamp; return b.timestamp - a.timestamp;
} else if (a.timestamp) { } else if (a.timestamp) {
// Only `a` has timestamp, it comes first
return -1; return -1;
} else if (b.timestamp) { } else if (b.timestamp) {
// Only `b` has timestamp, it comes first
return 1; return 1;
} else { } else {
// Neither has timestamp, sort alphabetically by groupName // Neither has timestamp, sort alphabetically by groupName (with fallback)
return a.groupName.localeCompare(b.groupName); const nameA = a.groupName || '';
const nameB = b.groupName || '';
return nameA.localeCompare(nameB);
} }
}); });
} }