Compare commits

..

27 Commits

Author SHA1 Message Date
93b8282a69 fix userlookup address 2025-06-23 14:31:45 +03:00
d2fc982852 added screen orientation qr 2025-06-22 08:50:29 +03:00
9113267dc2 fixes 2025-06-21 15:20:50 +03:00
fe63d9c0ff added fix to status 2025-06-21 06:31:56 +03:00
7e34886d15 fix getting list of groups where user is admin 2025-06-19 07:27:11 +03:00
b2b7820017 added txGroupId and chunk status 2025-06-18 22:27:24 +03:00
74d064f735 fix inviteToGroup error message 2025-06-17 14:48:15 +03:00
1482a8ed60 fix return of primary name 2025-06-16 21:06:39 +03:00
d9db4e5c27 fix sell order 2025-06-12 18:04:25 +03:00
15c1373cb0 fix blocked info at startup 2025-06-12 17:43:55 +03:00
2fbc48d676 block user changes 2025-06-11 15:14:12 +03:00
54a1bb636a add missing break 2025-06-06 14:44:36 +03:00
a22c48667b allow pasting of img and publish limit 2025-06-05 16:12:48 +03:00
4ad0fb7db3 add ability to enter html for public group chat 2025-05-29 21:19:57 +03:00
cfa4afc506 fixes 2025-05-28 18:55:03 +03:00
44a2675c87 updated to primary name logic 2025-05-27 00:30:12 +03:00
7be4a08a41 fix 2025-05-23 21:58:22 +03:00
20c51e0806 fix filename 2025-05-22 21:06:42 +03:00
0b9f32fd8c handle large uploads 2025-05-22 19:05:58 +03:00
0cfdd5cbc9 fix sort 2025-05-21 21:29:35 +03:00
21b3dd9d02 display images 2025-05-12 02:10:22 +03:00
93305b8dc4 fixes 2025-05-04 20:43:00 +03:00
8c98fcbcdf added qortal requests 2025-05-04 01:30:40 +03:00
3c00d40093 added qrs , modified blocks, 2025-04-16 18:58:04 +03:00
3e1e38838d fix bug when sending qort input not working triple tap 2025-04-07 10:20:07 +03:00
24f133acf5 fix send_coin return for qort 2025-04-06 08:06:57 +03:00
d2c79f52c4 added rebuild to sym keys 2025-04-06 05:46:09 +03:00
60 changed files with 4185 additions and 1002 deletions

View File

@ -14,6 +14,7 @@ dependencies {
implementation project(':capacitor-filesystem') implementation project(':capacitor-filesystem')
implementation project(':capacitor-local-notifications') implementation project(':capacitor-local-notifications')
implementation project(':capacitor-preferences') implementation project(':capacitor-preferences')
implementation project(':capacitor-screen-orientation')
implementation project(':capacitor-splash-screen') implementation project(':capacitor-splash-screen')
implementation project(':capawesome-capacitor-file-picker') implementation project(':capawesome-capacitor-file-picker')
implementation project(':evva-capacitor-secure-storage-plugin') implementation project(':evva-capacitor-secure-storage-plugin')

View File

@ -13,6 +13,7 @@
android:usesCleartextTraffic="true"> android:usesCleartextTraffic="true">
<activity <activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
android:screenOrientation="unspecified"
android:name=".MainActivity" android:name=".MainActivity"
android:label="@string/title_activity_main" android:label="@string/title_activity_main"
android:theme="@style/AppTheme.NoActionBarLaunch" android:theme="@style/AppTheme.NoActionBarLaunch"

View File

@ -4,6 +4,8 @@ import com.getcapacitor.BridgeActivity;
import com.github.Qortal.qortalMobile.NativeBcrypt; import com.github.Qortal.qortalMobile.NativeBcrypt;
import com.github.Qortal.qortalMobile.NativePOW; import com.github.Qortal.qortalMobile.NativePOW;
import android.os.Bundle; import android.os.Bundle;
import android.webkit.WebSettings;
import android.webkit.WebView;
public class MainActivity extends BridgeActivity { public class MainActivity extends BridgeActivity {
@Override @Override
@ -12,6 +14,9 @@ public class MainActivity extends BridgeActivity {
registerPlugin(NativePOW.class); registerPlugin(NativePOW.class);
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
// Enable mixed content mode for WebView
WebView webView = this.bridge.getWebView();
WebSettings webSettings = webView.getSettings();
webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
} }
} }

View File

@ -17,6 +17,9 @@ project(':capacitor-local-notifications').projectDir = new File('../node_modules
include ':capacitor-preferences' include ':capacitor-preferences'
project(':capacitor-preferences').projectDir = new File('../node_modules/@capacitor/preferences/android') project(':capacitor-preferences').projectDir = new File('../node_modules/@capacitor/preferences/android')
include ':capacitor-screen-orientation'
project(':capacitor-screen-orientation').projectDir = new File('../node_modules/@capacitor/screen-orientation/android')
include ':capacitor-splash-screen' include ':capacitor-splash-screen'
project(':capacitor-splash-screen').projectDir = new File('../node_modules/@capacitor/splash-screen/android') project(':capacitor-splash-screen').projectDir = new File('../node_modules/@capacitor/splash-screen/android')

View File

@ -19,7 +19,7 @@ const config: CapacitorConfig = {
"splashImmersive": true "splashImmersive": true
}, },
CapacitorHttp: { CapacitorHttp: {
enabled: true, enabled: false,
} }
} }
}; };

14
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "qortal-go", "name": "qortal-go",
"version": "0.5.2", "version": "0.5.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "qortal-go", "name": "qortal-go",
"version": "0.5.2", "version": "0.5.3",
"dependencies": { "dependencies": {
"@capacitor/android": "^6.1.2", "@capacitor/android": "^6.1.2",
"@capacitor/app": "^6.0.1", "@capacitor/app": "^6.0.1",
@ -16,6 +16,7 @@
"@capacitor/filesystem": "^6.0.1", "@capacitor/filesystem": "^6.0.1",
"@capacitor/local-notifications": "^6.1.0", "@capacitor/local-notifications": "^6.1.0",
"@capacitor/preferences": "^6.0.3", "@capacitor/preferences": "^6.0.3",
"@capacitor/screen-orientation": "^6.0.3",
"@capacitor/splash-screen": "^6.0.2", "@capacitor/splash-screen": "^6.0.2",
"@capawesome/capacitor-file-picker": "^6.1.0", "@capawesome/capacitor-file-picker": "^6.1.0",
"@chatscope/chat-ui-kit-react": "^2.0.3", "@chatscope/chat-ui-kit-react": "^2.0.3",
@ -1700,6 +1701,15 @@
"@capacitor/core": "^6.0.0" "@capacitor/core": "^6.0.0"
} }
}, },
"node_modules/@capacitor/screen-orientation": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/@capacitor/screen-orientation/-/screen-orientation-6.0.3.tgz",
"integrity": "sha512-5R+tf+twRNnkZFGSWsQkEBz1MFyP1kzZDyqOA9rtXJlTQYNcFJWouSXEuNa+Ba6i6nEi4X83BuXVzEFJ7zDrgQ==",
"license": "MIT",
"peerDependencies": {
"@capacitor/core": "^6.0.0"
}
},
"node_modules/@capacitor/splash-screen": { "node_modules/@capacitor/splash-screen": {
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/@capacitor/splash-screen/-/splash-screen-6.0.2.tgz", "resolved": "https://registry.npmjs.org/@capacitor/splash-screen/-/splash-screen-6.0.2.tgz",

View File

@ -20,6 +20,7 @@
"@capacitor/filesystem": "^6.0.1", "@capacitor/filesystem": "^6.0.1",
"@capacitor/local-notifications": "^6.1.0", "@capacitor/local-notifications": "^6.1.0",
"@capacitor/preferences": "^6.0.3", "@capacitor/preferences": "^6.0.3",
"@capacitor/screen-orientation": "^6.0.3",
"@capacitor/splash-screen": "^6.0.2", "@capacitor/splash-screen": "^6.0.2",
"@capawesome/capacitor-file-picker": "^6.1.0", "@capawesome/capacitor-file-picker": "^6.1.0",
"@chatscope/chat-ui-kit-react": "^2.0.3", "@chatscope/chat-ui-kit-react": "^2.0.3",

View File

@ -21,14 +21,18 @@ import {
DialogContentText, DialogContentText,
DialogTitle, DialogTitle,
Divider, Divider,
FormControlLabel,
Input, Input,
InputLabel, InputLabel,
Popover, Popover,
Tooltip, Tooltip,
Typography, Typography,
} from "@mui/material"; } from "@mui/material";
import { ScreenOrientation } from '@capacitor/screen-orientation';
import { decryptStoredWallet } from "./utils/decryptWallet"; import { decryptStoredWallet } from "./utils/decryptWallet";
import AccountBalanceWalletIcon from '@mui/icons-material/AccountBalanceWallet'; import AccountBalanceWalletIcon from '@mui/icons-material/AccountBalanceWallet';
import PriorityHighIcon from '@mui/icons-material/PriorityHigh';
import { JsonView, allExpanded, darkStyles } from 'react-json-view-lite'; import { JsonView, allExpanded, darkStyles } from 'react-json-view-lite';
import 'react-json-view-lite/dist/index.css'; import 'react-json-view-lite/dist/index.css';
@ -127,6 +131,7 @@ import {
isUsingImportExportSettingsAtom, isUsingImportExportSettingsAtom,
lastEnteredGroupIdAtom, lastEnteredGroupIdAtom,
mailsAtom, mailsAtom,
myGroupsWhereIAmAdminAtom,
oldPinnedAppsAtom, oldPinnedAppsAtom,
qMailLastEnteredTimestampAtom, qMailLastEnteredTimestampAtom,
settingsLocalLastUpdatedAtom, settingsLocalLastUpdatedAtom,
@ -153,6 +158,7 @@ import { BuyQortInformation } from "./components/BuyQortInformation";
import { InstallPWA } from "./components/InstallPWA"; import { InstallPWA } from "./components/InstallPWA";
import { QortPayment } from "./components/QortPayment"; import { QortPayment } from "./components/QortPayment";
import { PdfViewer } from "./common/PdfViewer"; import { PdfViewer } from "./common/PdfViewer";
import { DownloadWallet } from "./components/Auth/DownloadWallet";
type extStates = type extStates =
@ -439,7 +445,7 @@ function App() {
const { isShow, onCancel, onOk, show, message } = useModal(); const { isShow, onCancel, onOk, show, message } = useModal();
const {isUserBlocked, const {isUserBlocked,
addToBlockList, addToBlockList,
removeBlockFromList, getAllBlockedUsers} = useBlockedAddresses() removeBlockFromList, getAllBlockedUsers} = useBlockedAddresses(extState === 'authenticated')
const { const {
isShow: isShowUnsavedChanges, isShow: isShowUnsavedChanges,
onCancel: onCancelUnsavedChanges, onCancel: onCancelUnsavedChanges,
@ -486,6 +492,8 @@ function App() {
url: "http://127.0.0.1:12391", url: "http://127.0.0.1:12391",
}); });
const [useLocalNode, setUseLocalNode] = useState(false); const [useLocalNode, setUseLocalNode] = useState(false);
const [confirmRequestRead, setConfirmRequestRead] = useState(false);
const [isSettingsOpen, setIsSettingsOpen] = useState(false); const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const [showSeed, setShowSeed] = useState(false) const [showSeed, setShowSeed] = useState(false)
const [creationStep, setCreationStep] = useState(1) const [creationStep, setCreationStep] = useState(1)
@ -512,6 +520,15 @@ function App() {
} }
} }
useEffect(() => {
try {
ScreenOrientation.lock({ orientation: 'portrait' });
} catch (error) {
console.error(error)
}
}, []);
useEffect(()=> { useEffect(()=> {
if(!shownTutorialsInitiated) return if(!shownTutorialsInitiated) return
if(extState === 'not-authenticated'){ if(extState === 'not-authenticated'){
@ -558,6 +575,9 @@ function App() {
const resetAtomQMailLastEnteredTimestampAtom = useResetRecoilState(qMailLastEnteredTimestampAtom) const resetAtomQMailLastEnteredTimestampAtom = useResetRecoilState(qMailLastEnteredTimestampAtom)
const resetAtomMailsAtom = useResetRecoilState(mailsAtom) const resetAtomMailsAtom = useResetRecoilState(mailsAtom)
const resetLastEnteredGroupIdAtom = useResetRecoilState(lastEnteredGroupIdAtom) const resetLastEnteredGroupIdAtom = useResetRecoilState(lastEnteredGroupIdAtom)
const resetMyGroupsWhereIAmAdminAtom = useResetRecoilState(
myGroupsWhereIAmAdminAtom
);
const resetAllRecoil = () => { const resetAllRecoil = () => {
resetAtomSortablePinnedAppsAtom(); resetAtomSortablePinnedAppsAtom();
resetAtomCanSaveSettingToQdnAtom(); resetAtomCanSaveSettingToQdnAtom();
@ -569,6 +589,7 @@ function App() {
resetAtomMailsAtom() resetAtomMailsAtom()
resetGroupPropertiesAtom() resetGroupPropertiesAtom()
resetLastEnteredGroupIdAtom() resetLastEnteredGroupIdAtom()
resetMyGroupsWhereIAmAdminAtom()
}; };
useEffect(() => { useEffect(() => {
if (!isMobile) return; if (!isMobile) return;
@ -850,6 +871,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
@ -878,6 +917,8 @@ function App() {
if(message?.payload?.checkbox1){ if(message?.payload?.checkbox1){
qortalRequestCheckbox1Ref.current = message?.payload?.checkbox1?.value || false qortalRequestCheckbox1Ref.current = message?.payload?.checkbox1?.value || false
} }
setConfirmRequestRead(false)
await showQortalRequestExtension(message?.payload); await showQortalRequestExtension(message?.payload);
if (qortalRequestCheckbox1Ref.current) { if (qortalRequestCheckbox1Ref.current) {
event.source.postMessage( event.source.postMessage(
@ -1632,7 +1673,7 @@ function App() {
{balance?.toFixed(2)} QORT {balance?.toFixed(2)} QORT
</TextP> </TextP>
<RefreshIcon <RefreshIcon
onClick={getBalanceFunc} onClick={getBalanceAndUserInfoFunc}
sx={{ sx={{
fontSize: "16px", fontSize: "16px",
color: "white", color: "white",
@ -2578,87 +2619,14 @@ function App() {
)} )}
{extState === "download-wallet" && ( {extState === "download-wallet" && (
<> <>
<Spacer height="22px" /> <DownloadWallet
<Box returnToMain={returnToMain}
sx={{ setIsLoading={setIsLoading}
display: "flex", showInfo={showInfo}
width: "100%", rawWallet={rawWallet}
justifyContent: "flex-start", setWalletToBeDownloaded={setWalletToBeDownloaded}
paddingLeft: "22px", walletToBeDownloaded={walletToBeDownloaded}
boxSizing: "border-box", />
}}
>
<img
style={{
cursor: "pointer",
}}
onClick={returnToMain}
src={Return}
/>
</Box>
<Spacer height="10px" />
<div
className="image-container"
style={{
width: "136px",
height: "154px",
}}
>
<img src={Logo1Dark} className="base-image" />
</div>
<Spacer height="35px" />
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
}}
>
<TextP
sx={{
textAlign: "start",
lineHeight: "24px",
fontSize: "20px",
fontWeight: 600,
}}
>
Download Account
</TextP>
</Box>
<Spacer height="35px" />
{!walletToBeDownloaded && (
<>
<CustomLabel htmlFor="standard-adornment-password">
Confirm Wallet Password
</CustomLabel>
<Spacer height="5px" />
<PasswordField
id="standard-adornment-password"
value={walletToBeDownloadedPassword}
onChange={(e) =>
setWalletToBeDownloadedPassword(e.target.value)
}
/>
<Spacer height="20px" />
<CustomButton onClick={confirmPasswordToDownload}>
Confirm password
</CustomButton>
<ErrorText>{walletToBeDownloadedError}</ErrorText>
</>
)}
{walletToBeDownloaded && (
<>
<CustomButton onClick={async ()=> {
await saveFileToDiskFunc()
await showInfo({
message: isNative ? `Your account file was saved to internal storage, in the document folder. Keep that file secure.` : `Your account file was downloaded by your browser. Keep that file secure.` ,
})
}}>
Download account
</CustomButton>
</>
)}
</> </>
)} )}
{extState === "create-wallet" && ( {extState === "create-wallet" && (
@ -3110,7 +3078,7 @@ await showInfo({
> >
<CountdownCircleTimer <CountdownCircleTimer
isPlaying isPlaying
duration={30} duration={60}
colors={["#004777", "#F7B801", "#A30000", "#A30000"]} colors={["#004777", "#F7B801", "#A30000", "#A30000"]}
colorsTime={[7, 5, 2, 0]} colorsTime={[7, 5, 2, 0]}
onComplete={() => { onComplete={() => {
@ -3191,12 +3159,14 @@ await showInfo({
> >
{messageQortalRequestExtension?.text3} {messageQortalRequestExtension?.text3}
</TextP> </TextP>
<Spacer height="15px" />
</Box> </Box>
<Spacer height="15px" />
</> </>
)} )}
{messageQortalRequestExtension?.text4 && ( {messageQortalRequestExtension?.text4 && (
<>
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
@ -3214,6 +3184,8 @@ await showInfo({
{messageQortalRequestExtension?.text4} {messageQortalRequestExtension?.text4}
</TextP> </TextP>
</Box> </Box>
<Spacer height="15px" />
</>
)} )}
{messageQortalRequestExtension?.html && ( {messageQortalRequestExtension?.html && (
@ -3341,6 +3313,35 @@ await showInfo({
</Typography> </Typography>
</Box> </Box>
)} )}
{messageQortalRequestExtension?.confirmCheckbox && (
<FormControlLabel
control={
<Checkbox
onChange={(e) => setConfirmRequestRead(e.target.checked)}
checked={confirmRequestRead}
edge="start"
tabIndex={-1}
disableRipple
sx={{
"&.Mui-checked": {
color: "white",
},
"& .MuiSvgIcon-root": {
color: "white",
},
}}
/>
}
label={
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Typography sx={{ fontSize: "14px" }}>
I have read this request
</Typography>
<PriorityHighIcon color="warning" />
</Box>
}
/>
)}
<Spacer height="29px" /> <Spacer height="29px" />
<Box <Box
@ -3350,13 +3351,21 @@ await showInfo({
gap: "14px", gap: "14px",
}} }}
> >
<CustomButtonAccept <CustomButtonAccept
color="black" color="black"
bgColor="var(--green)" bgColor="var(--green)"
sx={{ sx={{
minWidth: "102px", minWidth: "102px",
opacity: messageQortalRequestExtension?.confirmCheckbox && !confirmRequestRead ? 0.1 : 0.7,
cursor: messageQortalRequestExtension?.confirmCheckbox && !confirmRequestRead ? 'default' : 'pointer',
"&:hover": {
opacity: messageQortalRequestExtension?.confirmCheckbox && !confirmRequestRead ? 0.1 : 1,
}
}}
onClick={() => {
if(messageQortalRequestExtension?.confirmCheckbox && !confirmRequestRead) return
onOkQortalRequestExtension("accepted")
}} }}
onClick={() => onOkQortalRequestExtension("accepted")}
> >
accept accept
</CustomButtonAccept> </CustomButtonAccept>

View File

@ -41,6 +41,14 @@ export const sortablePinnedAppsAtom = atom({
{ {
name: 'Q-Wallets', name: 'Q-Wallets',
service: 'APP' service: 'APP'
},
{
name: 'Q-Search',
service: 'APP'
},
{
name: 'Q-Nodecontrol',
service: 'APP'
} }
], ],
}); });
@ -180,3 +188,8 @@ export const lastPaymentSeenTimestampAtom = atom<null | number>({
key: 'lastPaymentSeenTimestampAtom', key: 'lastPaymentSeenTimestampAtom',
default: null, default: null,
}); });
export const isOpenBlockedModalAtom = atom({
key: 'isOpenBlockedModalAtom',
default: false,
});

View File

@ -358,11 +358,12 @@ export async function sendCoinCase(request, event) {
export async function inviteToGroupCase(request, event) { export async function inviteToGroupCase(request, event) {
try { try {
const { groupId, qortalAddress, inviteTime } = request.payload; const { groupId, qortalAddress, inviteTime, txGroupId = 0 } = request.payload;
const response = await inviteToGroup({ const response = await inviteToGroup({
groupId, groupId,
qortalAddress, qortalAddress,
inviteTime, inviteTime,
txGroupId
}); });
event.source.postMessage( event.source.postMessage(
@ -483,8 +484,8 @@ export async function createGroupCase(request, event) {
export async function cancelInvitationToGroupCase(request, event) { export async function cancelInvitationToGroupCase(request, event) {
try { try {
const { groupId, qortalAddress } = request.payload; const { groupId, qortalAddress,txGroupId = 0 } = request.payload;
const response = await cancelInvitationToGroup({ groupId, qortalAddress }); const response = await cancelInvitationToGroup({ groupId, qortalAddress, txGroupId });
event.source.postMessage( event.source.postMessage(
{ {
@ -564,11 +565,12 @@ export async function joinGroupCase(request, event) {
export async function kickFromGroupCase(request, event) { export async function kickFromGroupCase(request, event) {
try { try {
const { groupId, qortalAddress, rBanReason } = request.payload; const { groupId, qortalAddress, rBanReason, txGroupId = 0 } = request.payload;
const response = await kickFromGroup({ const response = await kickFromGroup({
groupId, groupId,
qortalAddress, qortalAddress,
rBanReason, rBanReason,
txGroupId
}); });
event.source.postMessage( event.source.postMessage(
@ -595,12 +597,13 @@ export async function kickFromGroupCase(request, event) {
export async function banFromGroupCase(request, event) { export async function banFromGroupCase(request, event) {
try { try {
const { groupId, qortalAddress, rBanReason, rBanTime } = request.payload; const { groupId, qortalAddress, rBanReason, rBanTime, txGroupId = 0 } = request.payload;
const response = await banFromGroup({ const response = await banFromGroup({
groupId, groupId,
qortalAddress, qortalAddress,
rBanReason, rBanReason,
rBanTime, rBanTime,
txGroupId
}); });
event.source.postMessage( event.source.postMessage(
@ -734,8 +737,8 @@ export async function getUserSettingsCase(request, event) {
export async function cancelBanCase(request, event) { export async function cancelBanCase(request, event) {
try { try {
const { groupId, qortalAddress } = request.payload; const { groupId, qortalAddress, txGroupId = 0 } = request.payload;
const response = await cancelBan({ groupId, qortalAddress }); const response = await cancelBan({ groupId, qortalAddress, txGroupId });
event.source.postMessage( event.source.postMessage(
{ {
@ -788,8 +791,8 @@ export async function registerNameCase(request, event) {
export async function makeAdminCase(request, event) { export async function makeAdminCase(request, event) {
try { try {
const { groupId, qortalAddress } = request.payload; const { groupId, qortalAddress,txGroupId = 0 } = request.payload;
const response = await makeAdmin({ groupId, qortalAddress }); const response = await makeAdmin({ groupId, qortalAddress, txGroupId });
event.source.postMessage( event.source.postMessage(
{ {
@ -815,8 +818,8 @@ export async function makeAdminCase(request, event) {
export async function removeAdminCase(request, event) { export async function removeAdminCase(request, event) {
try { try {
const { groupId, qortalAddress } = request.payload; const { groupId, qortalAddress, txGroupId = 0 } = request.payload;
const response = await removeAdmin({ groupId, qortalAddress }); const response = await removeAdmin({ groupId, qortalAddress, txGroupId });
event.source.postMessage( event.source.postMessage(
{ {
@ -1329,6 +1332,7 @@ export async function publishOnQDNCase(request, event) {
try { try {
const { const {
data, data,
name = "",
identifier, identifier,
service, service,
title, title,
@ -1346,6 +1350,7 @@ export async function publishOnQDNCase(request, event) {
identifier, identifier,
service, service,
title, title,
name,
description, description,
category, category,
tag1, tag1,

View File

@ -795,33 +795,35 @@ 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 '';
} }
} }
export async function getAddressInfo(address) { export async function getAddressInfo(address) {
const validApi = await getBaseApi(); const validApi = await getBaseApi();
const response = await fetch(validApi + "/addresses/" + address); const response = await fetch(validApi + "/addresses/" + address);
const data = await response.json(); const data = await response.json();
if (!response?.ok && data?.error !== 124) if (!response?.ok && data?.error !== 124)
throw new Error("Cannot fetch address info"); throw new Error("Cannot retrieve address info");
if (data?.error === 124) { if (data?.error === 124) {
return { return {
address, address,
@ -928,6 +930,59 @@ export async function getBalanceInfo() {
const data = await response.json(); const data = await response.json();
return data; return data;
} }
export async function getAssetBalanceInfo(assetId: number) {
const wallet = await getSaveWallet();
const address = wallet.address0;
const validApi = await getBaseApi();
const response = await fetch(validApi + `/assets/balances?address=${address}&assetid=${assetId}&ordering=ASSET_BALANCE_ACCOUNT&limit=1`);
if (!response?.ok) throw new Error("Cannot fetch asset balance");
const data = await response.json();
return +data?.[0]?.balance
}
export async function getAssetInfo(assetId: number) {
const validApi = await getBaseApi();
const response = await fetch(validApi + `/assets/info?assetId=${assetId}`);
if (!response?.ok) throw new Error("Cannot fetch asset info");
const data = await response.json();
return data
}
export async function transferAsset({
amount,
recipient,
assetId,
}) {
const lastReference = await getLastRef();
const resKeyPair = await getKeyPair();
const parsedData = resKeyPair;
const uint8PrivateKey = Base58.decode(parsedData.privateKey);
const uint8PublicKey = Base58.decode(parsedData.publicKey);
const keyPair = {
privateKey: uint8PrivateKey,
publicKey: uint8PublicKey,
};
const feeres = await getFee("TRANSFER_ASSET");
const tx = await createTransaction(12, keyPair, {
fee: feeres.fee,
recipient: recipient,
amount: amount,
assetId: assetId,
lastReference: lastReference,
});
const signedBytes = Base58.encode(tx.signedBytes);
const res = await processTransactionVersion2(signedBytes);
if (!res?.signature)
throw new Error(res?.message || "Transaction was not able to be processed");
return res;
}
export async function getLTCBalance() { export async function getLTCBalance() {
const wallet = await getSaveWallet(); const wallet = await getSaveWallet();
let _url = `${buyTradeNodeBaseUrl}/crosschain/ltc/walletbalance`; let _url = `${buyTradeNodeBaseUrl}/crosschain/ltc/walletbalance`;
@ -1979,7 +2034,7 @@ export async function joinGroup({ groupId }) {
return res; return res;
} }
export async function cancelInvitationToGroup({ groupId, qortalAddress }) { export async function cancelInvitationToGroup({ groupId, qortalAddress, txGroupId = 0 }) {
const lastReference = await getLastRef(); const lastReference = await getLastRef();
const resKeyPair = await getKeyPair(); const resKeyPair = await getKeyPair();
const parsedData = resKeyPair; const parsedData = resKeyPair;
@ -1996,6 +2051,7 @@ export async function cancelInvitationToGroup({ groupId, qortalAddress }) {
recipient: qortalAddress, recipient: qortalAddress,
rGroupId: groupId, rGroupId: groupId,
lastReference: lastReference, lastReference: lastReference,
groupID: txGroupId
}); });
const signedBytes = Base58.encode(tx.signedBytes); const signedBytes = Base58.encode(tx.signedBytes);
@ -2006,7 +2062,7 @@ export async function cancelInvitationToGroup({ groupId, qortalAddress }) {
return res; return res;
} }
export async function cancelBan({ groupId, qortalAddress }) { export async function cancelBan({ groupId, qortalAddress, txGroupId = 0 }) {
const lastReference = await getLastRef(); const lastReference = await getLastRef();
const resKeyPair = await getKeyPair(); const resKeyPair = await getKeyPair();
const parsedData = resKeyPair; const parsedData = resKeyPair;
@ -2023,6 +2079,7 @@ export async function cancelBan({ groupId, qortalAddress }) {
recipient: qortalAddress, recipient: qortalAddress,
rGroupId: groupId, rGroupId: groupId,
lastReference: lastReference, lastReference: lastReference,
groupID: txGroupId
}); });
const signedBytes = Base58.encode(tx.signedBytes); const signedBytes = Base58.encode(tx.signedBytes);
@ -2087,7 +2144,7 @@ export async function updateName({ newName, oldName, description }) {
return res; return res;
} }
export async function makeAdmin({ groupId, qortalAddress }) { export async function makeAdmin({ groupId, qortalAddress, txGroupId = 0 }) {
const lastReference = await getLastRef(); const lastReference = await getLastRef();
const resKeyPair = await getKeyPair(); const resKeyPair = await getKeyPair();
const parsedData = resKeyPair; const parsedData = resKeyPair;
@ -2104,6 +2161,7 @@ export async function makeAdmin({ groupId, qortalAddress }) {
recipient: qortalAddress, recipient: qortalAddress,
rGroupId: groupId, rGroupId: groupId,
lastReference: lastReference, lastReference: lastReference,
groupID: txGroupId
}); });
const signedBytes = Base58.encode(tx.signedBytes); const signedBytes = Base58.encode(tx.signedBytes);
@ -2114,7 +2172,7 @@ export async function makeAdmin({ groupId, qortalAddress }) {
return res; return res;
} }
export async function removeAdmin({ groupId, qortalAddress }) { export async function removeAdmin({ groupId, qortalAddress, txGroupId = 0 }) {
const lastReference = await getLastRef(); const lastReference = await getLastRef();
const resKeyPair = await getKeyPair(); const resKeyPair = await getKeyPair();
const parsedData = resKeyPair; const parsedData = resKeyPair;
@ -2131,6 +2189,7 @@ export async function removeAdmin({ groupId, qortalAddress }) {
recipient: qortalAddress, recipient: qortalAddress,
rGroupId: groupId, rGroupId: groupId,
lastReference: lastReference, lastReference: lastReference,
groupID: txGroupId
}); });
const signedBytes = Base58.encode(tx.signedBytes); const signedBytes = Base58.encode(tx.signedBytes);
@ -2146,6 +2205,7 @@ export async function banFromGroup({
qortalAddress, qortalAddress,
rBanReason = "", rBanReason = "",
rBanTime, rBanTime,
txGroupId = 0
}) { }) {
const lastReference = await getLastRef(); const lastReference = await getLastRef();
const resKeyPair = await getKeyPair(); const resKeyPair = await getKeyPair();
@ -2165,6 +2225,7 @@ export async function banFromGroup({
rBanReason: rBanReason, rBanReason: rBanReason,
rBanTime, rBanTime,
lastReference: lastReference, lastReference: lastReference,
groupID: txGroupId
}); });
const signedBytes = Base58.encode(tx.signedBytes); const signedBytes = Base58.encode(tx.signedBytes);
@ -2179,6 +2240,7 @@ export async function kickFromGroup({
groupId, groupId,
qortalAddress, qortalAddress,
rBanReason = "", rBanReason = "",
txGroupId = 0
}) { }) {
const lastReference = await getLastRef(); const lastReference = await getLastRef();
const resKeyPair = await getKeyPair(); const resKeyPair = await getKeyPair();
@ -2197,6 +2259,7 @@ export async function kickFromGroup({
rGroupId: groupId, rGroupId: groupId,
rBanReason: rBanReason, rBanReason: rBanReason,
lastReference: lastReference, lastReference: lastReference,
groupID: txGroupId
}); });
const signedBytes = Base58.encode(tx.signedBytes); const signedBytes = Base58.encode(tx.signedBytes);
@ -2247,7 +2310,153 @@ export async function createGroup({
if (!res?.signature) throw new Error(res?.message || "Transaction was not able to be processed"); if (!res?.signature) throw new Error(res?.message || "Transaction was not able to be processed");
return res; return res;
} }
export async function inviteToGroup({ groupId, qortalAddress, inviteTime }) {
export async function sellName({
name,
sellPrice
}) {
const wallet = await getSaveWallet();
const address = wallet.address0;
if (!address) throw new Error("Cannot find user");
const lastReference = await getLastRef();
const feeres = await getFee("SELL_NAME");
const resKeyPair = await getKeyPair();
const parsedData = resKeyPair;
const uint8PrivateKey = Base58.decode(parsedData.privateKey);
const uint8PublicKey = Base58.decode(parsedData.publicKey);
const keyPair = {
privateKey: uint8PrivateKey,
publicKey: uint8PublicKey,
};
const tx = await createTransaction(5, keyPair, {
fee: feeres.fee,
name,
sellPrice: sellPrice,
lastReference: lastReference,
});
const signedBytes = Base58.encode(tx.signedBytes);
const res = await processTransactionVersion2(signedBytes);
if (!res?.signature)
throw new Error(res?.message || "Transaction was not able to be processed");
return res;
}
export async function cancelSellName({
name
}) {
const wallet = await getSaveWallet();
const address = wallet.address0;
if (!address) throw new Error("Cannot find user");
const lastReference = await getLastRef();
const feeres = await getFee("SELL_NAME");
const resKeyPair = await getKeyPair();
const parsedData = resKeyPair;
const uint8PrivateKey = Base58.decode(parsedData.privateKey);
const uint8PublicKey = Base58.decode(parsedData.publicKey);
const keyPair = {
privateKey: uint8PrivateKey,
publicKey: uint8PublicKey,
};
const tx = await createTransaction(6, keyPair, {
fee: feeres.fee,
name,
lastReference: lastReference,
});
const signedBytes = Base58.encode(tx.signedBytes);
const res = await processTransactionVersion2(signedBytes);
if (!res?.signature)
throw new Error(res?.message || "Transaction was not able to be processed");
return res;
}
export async function buyName({
name,
sellerAddress,
sellPrice
}) {
const wallet = await getSaveWallet();
const address = wallet.address0;
if (!address) throw new Error("Cannot find user");
const lastReference = await getLastRef();
const feeres = await getFee("BUY_NAME");
const resKeyPair = await getKeyPair();
const parsedData = resKeyPair;
const uint8PrivateKey = Base58.decode(parsedData.privateKey);
const uint8PublicKey = Base58.decode(parsedData.publicKey);
const keyPair = {
privateKey: uint8PrivateKey,
publicKey: uint8PublicKey,
};
const tx = await createTransaction(7, keyPair, {
fee: feeres.fee,
name,
sellPrice,
recipient: sellerAddress,
lastReference: lastReference,
});
const signedBytes = Base58.encode(tx.signedBytes);
const res = await processTransactionVersion2(signedBytes);
if (!res?.signature)
throw new Error(res?.message || "Transaction was not able to be processed");
return res;
}
export async function updateGroup({
groupId,
newOwner,
newIsOpen,
newDescription,
newApprovalThreshold,
newMinimumBlockDelay,
newMaximumBlockDelay,
txGroupId = 0
}) {
const wallet = await getSaveWallet();
const address = wallet.address0;
if (!address) throw new Error("Cannot find user");
const lastReference = await getLastRef();
const feeres = await getFee("UPDATE_GROUP");
const resKeyPair = await getKeyPair();
const parsedData = resKeyPair;
const uint8PrivateKey = Base58.decode(parsedData.privateKey);
const uint8PublicKey = Base58.decode(parsedData.publicKey);
const keyPair = {
privateKey: uint8PrivateKey,
publicKey: uint8PublicKey,
};
const tx = await createTransaction(23, keyPair, {
fee: feeres.fee,
_groupId: groupId,
newOwner,
newIsOpen,
newDescription,
newApprovalThreshold,
newMinimumBlockDelay,
newMaximumBlockDelay,
lastReference: lastReference,
groupID: txGroupId
});
const signedBytes = Base58.encode(tx.signedBytes);
const res = await processTransactionVersion2(signedBytes);
if (!res?.signature)
throw new Error(res?.message || "Transaction was not able to be processed");
return res;
}
export async function inviteToGroup({ groupId, qortalAddress, inviteTime, txGroupId = 0 }) {
const address = await getNameOrAddress(qortalAddress); const address = await getNameOrAddress(qortalAddress);
if (!address) throw new Error("Cannot find user"); if (!address) throw new Error("Cannot find user");
const lastReference = await getLastRef(); const lastReference = await getLastRef();
@ -2267,13 +2476,14 @@ export async function inviteToGroup({ groupId, qortalAddress, inviteTime }) {
rGroupId: groupId, rGroupId: groupId,
rInviteTime: inviteTime, rInviteTime: inviteTime,
lastReference: lastReference, lastReference: lastReference,
groupID: txGroupId
}); });
const signedBytes = Base58.encode(tx.signedBytes); const signedBytes = Base58.encode(tx.signedBytes);
const res = await processTransactionVersion2(signedBytes); const res = await processTransactionVersion2(signedBytes);
if (!res?.signature) if (!res?.signature)
throw new Error("Transaction was not able to be processed"); throw new Error(res?.message || "Transaction was not able to be processed");
return res; return res;
} }
@ -2988,6 +3198,7 @@ function setupMessageListener() {
break; break;
case "updateThreadActivity": case "updateThreadActivity":
updateThreadActivityCase(request, event); updateThreadActivityCase(request, event);
break;
case "decryptGroupEncryption": case "decryptGroupEncryption":
decryptGroupEncryptionCase(request, event); decryptGroupEncryptionCase(request, event);
break; break;

View File

@ -47,18 +47,29 @@ async function getSaveWallet() {
throw new Error("No wallet saved"); throw new Error("No wallet saved");
} }
} }
export async function getNameInfo() { 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) {
@ -151,7 +162,7 @@ async function getKeyPair() {
if(encryptedData){ if(encryptedData){
const registeredName = await getNameInfo() const registeredName = await getNameInfo()
const data = await publishData({ const data = await publishData({
registeredName, file: encryptedData, service: 'DOCUMENT_PRIVATE', identifier: `admins-symmetric-qchat-group-${groupId}`, uploadType: 'file', isBase64: true, withFee: true registeredName, data: encryptedData, service: 'DOCUMENT_PRIVATE', identifier: `admins-symmetric-qchat-group-${groupId}`, uploadType: 'base64', withFee: true
}) })
return { return {
data, data,
@ -202,7 +213,7 @@ export const encryptAndPublishSymmetricKeyGroupChat = async ({groupId, previousD
if(encryptedData){ if(encryptedData){
const registeredName = await getNameInfo() const registeredName = await getNameInfo()
const data = await publishData({ const data = await publishData({
registeredName, file: encryptedData, service: 'DOCUMENT_PRIVATE', identifier: `symmetric-qchat-group-${groupId}`, uploadType: 'file', isBase64: true, withFee: true registeredName, data: encryptedData, service: 'DOCUMENT_PRIVATE', identifier: `symmetric-qchat-group-${groupId}`, uploadType: 'base64', withFee: true
}) })
return { return {
data, data,
@ -223,7 +234,7 @@ export const publishGroupEncryptedResource = async ({encryptedData, identifier})
const registeredName = await getNameInfo() const registeredName = await getNameInfo()
if(!registeredName) throw new Error('You need a name to publish') if(!registeredName) throw new Error('You need a name to publish')
const data = await publishData({ const data = await publishData({
registeredName, file: encryptedData, service: 'DOCUMENT', identifier, uploadType: 'file', isBase64: true, withFee: true registeredName, data: encryptedData, service: 'DOCUMENT', identifier, uploadType: 'base64', withFee: true
}) })
return data return data
@ -242,15 +253,16 @@ export const publishOnQDN = async ({data, identifier, service, title,
tag3, tag3,
tag4, tag4,
tag5, tag5,
name,
uploadType = 'file' uploadType = 'file'
}) => { }) => {
if(data && service){ if(data && service){
const registeredName = await getNameInfo() const registeredName = name || await getNameInfo()
if(!registeredName) throw new Error('You need a name to publish') if(!registeredName) throw new Error('You need a name to publish')
const res = await publishData({ const res = await publishData({
registeredName, file: data, service, identifier, uploadType, isBase64: true, withFee: true, title, registeredName, data, service, identifier, uploadType, withFee: true, title,
description, description,
category, category,
tag1, tag1,

View File

@ -130,12 +130,17 @@ export const BoundedNumericTextField = ({
...props?.InputProps, ...props?.InputProps,
endAdornment: addIconButtons ? ( endAdornment: addIconButtons ? (
<InputAdornment position="end"> <InputAdornment position="end">
<IconButton size="small" onClick={() => changeValueWithIncDecButton(1)}> <IconButton size="small" onClick={() =>
changeValueWithIncDecButton(1)
} onTouchStart={(e)=> e.stopPropagation()}>
<AddIcon sx={{ <AddIcon sx={{
color: 'white' color: 'white'
}} />{" "} }} />{" "}
</IconButton> </IconButton>
<IconButton size="small" onClick={() => changeValueWithIncDecButton(-1)}> <IconButton onTouchStart={(e)=> e.stopPropagation()} size="small" onClick={() =>
changeValueWithIncDecButton(-1)
}>
<RemoveIcon sx={{ <RemoveIcon sx={{
color: 'white' color: 'white'
}} />{" "} }} />{" "}

View File

@ -48,7 +48,7 @@ export const useModal = () => {
const onCancel = () => { const onCancel = () => {
const { reject } = promiseConfig.current; const { reject } = promiseConfig.current;
hide(); hide();
reject(); reject('Declined');
setMessage({ setMessage({
publishFee: "", publishFee: "",
message: "" message: ""

View File

@ -1,4 +1,4 @@
import React, { useContext, useEffect, useMemo, useState } from "react"; import React, { useCallback, useContext, useEffect, useMemo, useState } from "react";
import { import {
AppCircle, AppCircle,
AppCircleContainer, AppCircleContainer,
@ -49,6 +49,7 @@ import { LoadingSnackbar } from "../Snackbar/LoadingSnackbar";
import { CustomizedSnackbars } from "../Snackbar/Snackbar"; import { CustomizedSnackbars } from "../Snackbar/Snackbar";
import { getFee } from "../../background"; import { getFee } from "../../background";
import { fileToBase64 } from "../../utils/fileReading"; import { fileToBase64 } from "../../utils/fileReading";
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)",
@ -82,7 +83,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("");
@ -99,6 +101,8 @@ export const AppPublish = ({ names, categories }) => {
const [openSnack, setOpenSnack] = useState(false); const [openSnack, setOpenSnack] = useState(false);
const [infoSnack, setInfoSnack] = useState(null); const [infoSnack, setInfoSnack] = useState(null);
const [isLoading, setIsLoading] = useState(""); const [isLoading, setIsLoading] = useState("");
const mySortedNames = useSortedMyNames(names, myName);
const maxFileSize = appType === "APP" ? 50 * 1024 * 1024 : 400 * 1024 * 1024; // 50MB or 400MB const maxFileSize = appType === "APP" ? 50 * 1024 * 1024 : 400 * 1024 * 1024; // 50MB or 400MB
const { getRootProps, getInputProps } = useDropzone({ const { getRootProps, getInputProps } = useDropzone({
accept: { accept: {
@ -162,6 +166,25 @@ 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 publishApp = async () => { const publishApp = async () => {
try { try {
const data = { const data = {
@ -199,10 +222,10 @@ export const AppPublish = ({ names, categories }) => {
publishFee: fee.fee + " QORT", publishFee: fee.fee + " QORT",
}); });
setIsLoading("Publishing... Please wait."); setIsLoading("Publishing... Please wait.");
const fileBase64 = await fileToBase64(file);
await new Promise((res, rej) => { await new Promise((res, rej) => {
window.sendMessage("publishOnQDN", { window.sendMessage("publishOnQDN", {
data: fileBase64, data: file,
service: appType, service: appType,
title, title,
description, description,
@ -213,6 +236,7 @@ export const AppPublish = ({ names, categories }) => {
tag4, tag4,
tag5, tag5,
uploadType: "zip", uploadType: "zip",
name
}) })
.then((response) => { .then((response) => {
if (!response?.error) { if (!response?.error) {
@ -287,7 +311,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

@ -1,4 +1,4 @@
import React, { useContext, useEffect, useMemo, useState } from "react"; import React, { useCallback, useContext, useEffect, useMemo, useState } from "react";
import { Avatar, Box, } from "@mui/material"; import { Avatar, Box, } from "@mui/material";
import { Add } from "@mui/icons-material"; import { Add } from "@mui/icons-material";
@ -100,6 +100,57 @@ export const AppViewer = React.forwardRef(({ app , hide, isDevMode, skipAuth}, i
}; };
}, [app, path]); }, [app, path]);
const receiveChunksFunc = useCallback(
(e) => {
const iframe = iframeRef?.current;
if (!iframe || !iframe?.src) return;
if (app?.tabId !== e.detail?.tabId) return;
const publishLocation = e.detail?.publishLocation;
const chunksSubmitted = e.detail?.chunksSubmitted;
const totalChunks = e.detail?.totalChunks;
const retry = e.detail?.retry;
const filename = e.detail?.filename;
try {
if (publishLocation === undefined || publishLocation === null) return;
const dataToBeSent = {};
if (chunksSubmitted !== undefined && chunksSubmitted !== null) {
dataToBeSent.chunks = chunksSubmitted;
}
if (totalChunks !== undefined && totalChunks !== null) {
dataToBeSent.totalChunks = totalChunks;
}
if (retry !== undefined && retry !== null) {
dataToBeSent.retry = retry;
}
if (filename !== undefined && filename !== null) {
dataToBeSent.filename = filename;
}
const targetOrigin = new URL(iframe.src).origin;
iframe.contentWindow?.postMessage(
{
action: 'PUBLISH_STATUS',
publishLocation,
...dataToBeSent,
requestedHandler: 'UI',
processed: e.detail?.processed || false,
},
targetOrigin
);
} catch (err) {
console.error('Failed to send theme change to iframe:', err);
}
},
[iframeRef, app?.tabId]
);
useEffect(() => {
subscribeToEvent('receiveChunks', receiveChunksFunc);
return () => {
unsubscribeFromEvent('receiveChunks', receiveChunksFunc);
};
}, [receiveChunksFunc]);
// Function to navigate back in iframe // Function to navigate back in iframe
const navigateBackInIframe = async () => { const navigateBackInIframe = async () => {
if (iframeRef.current && iframeRef.current.contentWindow && history?.currentIndex > 0) { if (iframeRef.current && iframeRef.current.contentWindow && history?.currentIndex > 0) {
@ -194,7 +245,7 @@ export const AppViewer = React.forwardRef(({ app , hide, isDevMode, skipAuth}, i
height: !isMobile ? '100vh' : `calc(${rootHeight} - 60px - 45px )`, height: !isMobile ? '100vh' : `calc(${rootHeight} - 60px - 45px )`,
border: 'none', border: 'none',
width: '100%' width: '100%'
}} id="browser-iframe" src={defaultUrl} sandbox="allow-scripts allow-same-origin allow-forms allow-downloads allow-modals" }} id="browser-iframe" src={defaultUrl} sandbox="allow-scripts allow-same-origin allow-forms allow-downloads allow-modals allow-orientation-lock"
allow="fullscreen; clipboard-read; clipboard-write"> allow="fullscreen; clipboard-read; clipboard-write">
</iframe> </iframe>

View File

@ -17,7 +17,7 @@ import { AppsLibrary } from "./AppsLibrary";
const uid = new ShortUniqueId({ length: 8 }); const uid = new ShortUniqueId({ length: 8 });
export const Apps = ({ mode, setMode, show , myName}) => { export const Apps = ({ mode, setMode, show , myName, myAddress}) => {
const [availableQapps, setAvailableQapps] = useState([]); const [availableQapps, setAvailableQapps] = useState([]);
const [selectedAppInfo, setSelectedAppInfo] = useState(null); const [selectedAppInfo, setSelectedAppInfo] = useState(null);
const [selectedCategory, setSelectedCategory] = useState(null) const [selectedCategory, setSelectedCategory] = useState(null)
@ -298,7 +298,7 @@ export const Apps = ({ mode, setMode, show , myName}) => {
> >
{mode !== "viewer" && !selectedTab && <Spacer height="30px" />} {mode !== "viewer" && !selectedTab && <Spacer height="30px" />}
{mode === "home" && ( {mode === "home" && (
<AppsHome myName={myName} availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} /> <AppsHome myName={myName} availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} myAddress={myAddress} />
)} )}
<AppsLibrary <AppsLibrary
@ -314,7 +314,7 @@ export const Apps = ({ mode, setMode, show , myName}) => {
{mode === "appInfo" && !selectedTab && <AppInfo app={selectedAppInfo} myName={myName} />} {mode === "appInfo" && !selectedTab && <AppInfo app={selectedAppInfo} myName={myName} />}
{mode === "appInfo-from-category" && !selectedTab && <AppInfo app={selectedAppInfo} myName={myName} />} {mode === "appInfo-from-category" && !selectedTab && <AppInfo app={selectedAppInfo} myName={myName} />}
<AppsCategory availableQapps={availableQapps} isShow={mode === 'category' && !selectedTab} category={selectedCategory} myName={myName} /> <AppsCategory availableQapps={availableQapps} isShow={mode === 'category' && !selectedTab} category={selectedCategory} myName={myName} />
{mode === "publish" && !selectedTab && <AppPublish names={myName ? [myName] : []} categories={categories} />} {mode === "publish" && !selectedTab && <AppPublish categories={categories} myAddress={myAddress} myName={myName} />}
{tabs.map((tab) => { {tabs.map((tab) => {
if (!iframeRefs.current[tab.tabId]) { if (!iframeRefs.current[tab.tabId]) {
@ -335,7 +335,7 @@ export const Apps = ({ mode, setMode, show , myName}) => {
{isNewTabWindow && mode === "viewer" && ( {isNewTabWindow && mode === "viewer" && (
<> <>
<Spacer height="30px" /> <Spacer height="30px" />
<AppsHome myName={myName} availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} /> <AppsHome myName={myName} availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} myAddress={myAddress} />
</> </>
)} )}
{mode !== "viewer" && !selectedTab && <Spacer height="180px" />} {mode !== "viewer" && !selectedTab && <Spacer height="180px" />}

View File

@ -41,7 +41,9 @@ const officialAppList = [
"q-trade", "q-trade",
"q-support", "q-support",
"q-manager", "q-manager",
"q-wallets" "q-wallets",
"q-search",
"q-nodecontrol"
]; ];
const ScrollerStyled = styled('div')({ const ScrollerStyled = styled('div')({

View File

@ -47,7 +47,9 @@ const officialAppList = [
"q-fund", "q-fund",
"q-shop", "q-shop",
"q-manager", "q-manager",
"q-wallets" "q-wallets",
"q-search",
"q-nodecontrol"
]; ];
const ScrollerStyled = styled("div")({ const ScrollerStyled = styled("div")({

View File

@ -20,7 +20,7 @@ import HelpIcon from '@mui/icons-material/Help';
import { useHandleTutorials } from "../Tutorials/useHandleTutorials"; import { useHandleTutorials } from "../Tutorials/useHandleTutorials";
import { AppsPrivate } from "./AppsPrivate"; import { AppsPrivate } from "./AppsPrivate";
export const AppsHome = ({ setMode, myApp, myWebsite, availableQapps, myName }) => { export const AppsHome = ({ setMode, myApp, myWebsite, availableQapps, myName, myAddress }) => {
const [qortalUrl, setQortalUrl] = useState('') const [qortalUrl, setQortalUrl] = useState('')
const { showTutorial } = useContext(GlobalContext); const { showTutorial } = useContext(GlobalContext);
@ -146,7 +146,7 @@ export const AppsHome = ({ setMode, myApp, myWebsite, availableQapps, myName }
<AppCircleLabel>Library</AppCircleLabel> <AppCircleLabel>Library</AppCircleLabel>
</AppCircleContainer> </AppCircleContainer>
</ButtonBase> </ButtonBase>
<AppsPrivate myName={myName} /> <AppsPrivate myName={myName} myAddress={myAddress} />
<SortablePinnedApps availableQapps={availableQapps} myWebsite={myWebsite} myApp={myApp} /> <SortablePinnedApps availableQapps={availableQapps} myWebsite={myWebsite} myApp={myApp} />

View File

@ -45,7 +45,9 @@ const officialAppList = [
"q-support", "q-support",
"q-manager", "q-manager",
"q-mintership", "q-mintership",
"q-wallets" "q-wallets",
"q-search",
"q-nodecontrol"
]; ];
const ScrollerStyled = styled('div')({ const ScrollerStyled = styled('div')({

View File

@ -56,7 +56,9 @@ const officialAppList = [
"q-shop", "q-shop",
"q-manager", "q-manager",
"q-mintership", "q-mintership",
"q-wallets" "q-wallets",
"q-search",
"q-nodecontrol"
]; ];
const ScrollerStyled = styled("div")({ const ScrollerStyled = styled("div")({

View File

@ -1,4 +1,4 @@
import React, { useContext, useMemo, useState } from "react"; import React, { useCallback, useContext, useEffect, useMemo, useState } from "react";
import { import {
Avatar, Avatar,
Box, Box,
@ -30,15 +30,18 @@ import {
PublishQAppInfo, PublishQAppInfo,
} from "./Apps-styles"; } from "./Apps-styles";
import ImageUploader from "../../common/ImageUploader"; import ImageUploader from "../../common/ImageUploader";
import { isMobile, MyContext } from "../../App"; import { getBaseApiReact, isMobile, MyContext } 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"; import { getFee } from "../../background";
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 { openApp } = useHandlePrivateApps(); const { openApp } = useHandlePrivateApps();
const [names, setNames] = useState([]);
const [name, setName] = useState(0);
const [file, setFile] = useState(null); const [file, setFile] = useState(null);
const [logo, setLogo] = useState(null); const [logo, setLogo] = useState(null);
const [qortalUrl, setQortalUrl] = useState(""); const [qortalUrl, setQortalUrl] = useState("");
@ -48,6 +51,7 @@ export const AppsPrivate = ({myName}) => {
const [myGroupsWhereIAmAdminFromGlobal] = useRecoilState( const [myGroupsWhereIAmAdminFromGlobal] = useRecoilState(
myGroupsWhereIAmAdminAtom myGroupsWhereIAmAdminAtom
); );
const mySortedNames = useSortedMyNames(names, myName);
const myGroupsWhereIAmAdmin = useMemo(()=> { const myGroupsWhereIAmAdmin = useMemo(()=> {
return myGroupsWhereIAmAdminFromGlobal?.filter((group)=> groupsProperties[group?.groupId]?.isOpen === false) return myGroupsWhereIAmAdminFromGlobal?.filter((group)=> groupsProperties[group?.groupId]?.isOpen === false)
@ -165,6 +169,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) {
@ -181,7 +187,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
@ -196,6 +202,24 @@ 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(() => {
if (isOpenPrivateModal) {
getNames();
}
}, [getNames, isOpenPrivateModal]);
const handleChange = (event: React.SyntheticEvent, newValue: number) => { const handleChange = (event: React.SyntheticEvent, newValue: number) => {
setValueTabPrivateApp(newValue); setValueTabPrivateApp(newValue);
}; };
@ -432,6 +456,34 @@ export const AppsPrivate = ({myName}) => {
{file ? "Change" : "Choose"} File {file ? "Change" : "Choose"} File
</PublishQAppChoseFile> </PublishQAppChoseFile>
<Spacer height="20px" /> <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" />
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",

View File

@ -10,9 +10,99 @@ import { MyContext } from '../../App';
import FileSaver from 'file-saver'; import FileSaver from 'file-saver';
import { Capacitor } from '@capacitor/core'; import { Capacitor } from '@capacitor/core';
import { createEndpoint } from '../../background';
import { uint8ArrayToBase64 } from '../../backgroundFunctions/encryption';
export const isNative = Capacitor.isNativePlatform(); export const isNative = Capacitor.isNativePlatform();
export const saveFileInChunksFromUrl = async (
location,
) => {
let fileName = location.filename
let locationUrl = `/arbitrary/${location.service}/${location.name}`;
if (location.identifier) {
locationUrl = locationUrl + `/${location.identifier}`;
}
const endpoint = await createEndpoint(
locationUrl +
`?attachment=true&attachmentFilename=${location?.filename}`
);
const response = await fetch(endpoint);
if (!response.ok || !response.body) {
throw new Error('Failed to fetch file or no readable stream');
}
const contentType = response.headers.get('Content-Type') || 'application/octet-stream';
const base64Prefix = `data:${contentType};base64,`;
const getExtensionFromFileName = (name: string): string => {
const lastDotIndex = name.lastIndexOf('.');
return lastDotIndex !== -1 ? name.substring(lastDotIndex) : '';
};
const existingExtension = getExtensionFromFileName(fileName);
if (existingExtension) {
fileName = fileName.substring(0, fileName.lastIndexOf('.'));
}
const mimeTypeToExtension = (mimeType: string): string => {
return mimeToExtensionMap[mimeType] || existingExtension || '';
};
const extension = mimeTypeToExtension(contentType);
const fullFileName = `${fileName}_${Date.now()}${extension}`;
const reader = response.body.getReader();
let isFirstChunk = true;
let done = false;
let buffer = new Uint8Array(0);
const preferredChunkSize = 1024 * 1024; // 1MB
while (!done) {
const result = await reader.read();
done = result.done;
if (result.value) {
// Combine new value with existing buffer
const newBuffer = new Uint8Array(buffer.length + result.value.length);
newBuffer.set(buffer);
newBuffer.set(result.value, buffer.length);
buffer = newBuffer;
// While we have enough data, process 1MB chunks
while (buffer.length >= preferredChunkSize) {
const chunk = buffer.slice(0, preferredChunkSize);
buffer = buffer.slice(preferredChunkSize);
const base64Chunk = uint8ArrayToBase64(chunk);
await Filesystem.writeFile({
path: fullFileName,
data: isFirstChunk ? base64Prefix + base64Chunk : base64Chunk,
directory: Directory.Documents,
recursive: true,
append: !isFirstChunk,
});
isFirstChunk = false;
}
}
}
// Write remaining buffer (if any)
if (buffer.length > 0) {
const base64Chunk = uint8ArrayToBase64(buffer);
await Filesystem.writeFile({
path: fullFileName,
data: isFirstChunk ? base64Prefix + base64Chunk : base64Chunk,
directory: Directory.Documents,
recursive: true,
append: !isFirstChunk,
});
}
};
export const saveFileInChunks = async ( export const saveFileInChunks = async (
blob: Blob, blob: Blob,
@ -255,7 +345,15 @@ export function openIndexedDB() {
'GET_NODE_INFO', 'GET_NODE_INFO',
'GET_NODE_STATUS', 'GET_NODE_STATUS',
'GET_ARRR_SYNC_STATUS', 'GET_ARRR_SYNC_STATUS',
'SHOW_PDF_READER' 'SHOW_PDF_READER',
'UPDATE_GROUP',
'SELL_NAME',
'CANCEL_SELL_NAME',
'BUY_NAME', 'MULTI_ASSET_PAYMENT_WITH_PRIVATE_DATA',
'TRANSFER_ASSET',
'SIGN_FOREIGN_FEES',
'GET_PRIMARY_NAME',
'SCREEN-ORIENTATION'
] ]
@ -269,7 +367,13 @@ const UIQortalRequests = [
'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', 'ADD_FOREIGN_SERVER', 'REMOVE_FOREIGN_SERVER', 'GET_DAY_SUMMARY', 'CREATE_TRADE_BUY_ORDER',
'CREATE_TRADE_SELL_ORDER', 'CANCEL_TRADE_SELL_ORDER', 'IS_USING_PUBLIC_NODE', 'SIGN_TRANSACTION', 'ADMIN_ACTION', 'OPEN_NEW_TAB', 'CREATE_AND_COPY_EMBED_LINK', 'DECRYPT_QORTAL_GROUP_DATA', 'DECRYPT_DATA_WITH_SHARING_KEY', 'DELETE_HOSTED_DATA', 'GET_HOSTED_DATA', 'SHOW_ACTIONS', 'REGISTER_NAME', 'UPDATE_NAME', 'LEAVE_GROUP', 'INVITE_TO_GROUP', 'KICK_FROM_GROUP', 'BAN_FROM_GROUP', 'CANCEL_GROUP_BAN', 'ADD_GROUP_ADMIN', 'REMOVE_GROUP_ADMIN','DECRYPT_AESGCM', 'CANCEL_GROUP_INVITE', 'CREATE_GROUP', 'GET_USER_WALLET_TRANSACTIONS', 'GET_NODE_INFO', 'CREATE_TRADE_SELL_ORDER', 'CANCEL_TRADE_SELL_ORDER', 'IS_USING_PUBLIC_NODE', 'SIGN_TRANSACTION', 'ADMIN_ACTION', 'OPEN_NEW_TAB', 'CREATE_AND_COPY_EMBED_LINK', 'DECRYPT_QORTAL_GROUP_DATA', 'DECRYPT_DATA_WITH_SHARING_KEY', 'DELETE_HOSTED_DATA', 'GET_HOSTED_DATA', 'SHOW_ACTIONS', 'REGISTER_NAME', 'UPDATE_NAME', 'LEAVE_GROUP', 'INVITE_TO_GROUP', 'KICK_FROM_GROUP', 'BAN_FROM_GROUP', 'CANCEL_GROUP_BAN', 'ADD_GROUP_ADMIN', 'REMOVE_GROUP_ADMIN','DECRYPT_AESGCM', 'CANCEL_GROUP_INVITE', 'CREATE_GROUP', 'GET_USER_WALLET_TRANSACTIONS', 'GET_NODE_INFO',
'GET_NODE_STATUS', 'GET_ARRR_SYNC_STATUS', 'SHOW_PDF_READER' 'GET_NODE_STATUS', 'GET_ARRR_SYNC_STATUS', 'SHOW_PDF_READER', 'UPDATE_GROUP', 'SELL_NAME',
'CANCEL_SELL_NAME',
'BUY_NAME', 'MULTI_ASSET_PAYMENT_WITH_PRIVATE_DATA',
'TRANSFER_ASSET',
'SIGN_FOREIGN_FEES',
'GET_PRIMARY_NAME',
'SCREEN_ORIENTATION'
]; ];
@ -542,8 +646,18 @@ isDOMContentLoaded: false
if (event?.data?.requestedHandler !== 'UI') return; if (event?.data?.requestedHandler !== 'UI') return;
const sendMessageToRuntime = (message, eventPort) => { const sendMessageToRuntime = (message, eventPort) => {
window.sendMessage(message.action, message.payload, 300000, message.isExtension, { let timeout: number = 300000;
name: appName, service: appService if (
message?.action === 'PUBLISH_MULTIPLE_QDN_RESOURCES' &&
message?.payload?.resources?.length > 0
) {
timeout = message?.payload?.resources?.length * 1200000;
} else if (message?.action === 'PUBLISH_QDN_RESOURCE') {
timeout = 1200000;
}
window.sendMessage(message.action, message.payload, timeout, message.isExtension, {
name: appName, service: appService, tabId,
}, skipAuth) }, skipAuth)
.then((response) => { .then((response) => {
if (response.error) { if (response.error) {
@ -551,7 +665,7 @@ isDOMContentLoaded: false
result: null, result: null,
error: { error: {
error: response.error, error: response.error,
message: typeof response?.error === 'string' ? response.error : 'An error has occurred' message: typeof response?.error === 'string' ? response?.error : typeof response?.message === 'string' ? response?.message : 'An error has occurred'
}, },
}); });
} else { } else {
@ -576,38 +690,26 @@ isDOMContentLoaded: false
} 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,
});
event.ports[0].postMessage({
result: true,
error: null,
}); });
} catch (error) { } 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_QDN_RESOURCE' ||
event?.data?.action === 'ENCRYPT_DATA' || event?.data?.action === 'ENCRYPT_DATA_WITH_SHARING_KEY' || event?.data?.action === 'ENCRYPT_QORTAL_GROUP_DATA' event?.data?.action === 'ENCRYPT_DATA' || event?.data?.action === 'ENCRYPT_DATA_WITH_SHARING_KEY' || event?.data?.action === 'ENCRYPT_QORTAL_GROUP_DATA'
) { ) {
if (
event?.data?.action === 'PUBLISH_MULTIPLE_QDN_RESOURCES' ||
event?.data?.action === 'PUBLISH_QDN_RESOURCE'
){
try {
checkMobileSizeConstraints(event.data)
} catch (error) {
event.ports[0].postMessage({
result: null,
error: error?.message,
});
return;
}
}
let data; let data;
try { try {
data = await storeFilesInIndexedDB(event.data); data = await storeFilesInIndexedDB(event.data);
@ -630,6 +732,29 @@ isDOMContentLoaded: false
error: 'Failed to prepare data for publishing', error: 'Failed to prepare data for publishing',
}); });
} }
} else if (
event?.data?.action === 'PUBLISH_MULTIPLE_QDN_RESOURCES' ||
event?.data?.action === 'PUBLISH_QDN_RESOURCE'
) {
const data = event.data;
if (data) {
sendMessageToRuntime(
{
action: event.data.action,
type: 'qortalRequest',
payload: data,
isExtension: true,
},
event.ports[0]
);
} else {
event.ports[0].postMessage({
result: null,
error: 'Failed to prepare data for publishing',
});
}
} else if(event?.data?.action === 'LINK_TO_QDN_RESOURCE' || } else if(event?.data?.action === 'LINK_TO_QDN_RESOURCE' ||
event?.data?.action === 'QDN_RESOURCE_DISPLAYED'){ event?.data?.action === 'QDN_RESOURCE_DISPLAYED'){
const pathUrl = event?.data?.path != null ? (event?.data?.path.startsWith('/') ? '' : '/') + event?.data?.path : null const pathUrl = event?.data?.path != null ? (event?.data?.path.startsWith('/') ? '' : '/') + event?.data?.path : null
@ -687,7 +812,7 @@ isDOMContentLoaded: false
}; };
}, [appName, appService]); // Empty dependency array to run once when the component mounts }, [appName, appService, tabId]); // Empty dependency array to run once when the component mounts

View File

@ -0,0 +1,248 @@
import {
Box,
Checkbox,
FormControlLabel,
Typography,
useTheme,
} from '@mui/material';
import { Spacer } from '../../common/Spacer';
import { PasswordField } from '../PasswordField/PasswordField';
import { ErrorText } from '../ErrorText/ErrorText';
import Logo1Dark from '../../assets/svgs/Logo1Dark.svg';
import { saveFileToDisk } from '../../utils/generateWallet/generateWallet';
import { useState } from 'react';
import { decryptStoredWallet } from '../../utils/decryptWallet';
import PhraseWallet from '../../utils/generateWallet/phrase-wallet';
import { crypto, walletVersion } from '../../constants/decryptWallet';
import Return from "../../assets/svgs/Return.svg";
import { CustomButton, CustomLabel, TextP } from '../../App-styles';
export const DownloadWallet = ({
returnToMain,
setIsLoading,
showInfo,
rawWallet,
setWalletToBeDownloaded,
walletToBeDownloaded,
}) => {
const [walletToBeDownloadedPassword, setWalletToBeDownloadedPassword] =
useState<string>('');
const [newPassword, setNewPassword] = useState<string>('');
const [keepCurrentPassword, setKeepCurrentPassword] = useState<boolean>(true);
const theme = useTheme();
const [walletToBeDownloadedError, setWalletToBeDownloadedError] =
useState<string>('');
const saveFileToDiskFunc = async () => {
try {
await saveFileToDisk(
walletToBeDownloaded.wallet,
walletToBeDownloaded.qortAddress
);
} catch (error: any) {
setWalletToBeDownloadedError(error?.message);
}
};
const saveWalletFunc = async (password: string, newPassword) => {
let wallet = structuredClone(rawWallet);
const res = await decryptStoredWallet(password, wallet);
const wallet2 = new PhraseWallet(res, wallet?.version || walletVersion);
const passwordToUse = newPassword || password;
wallet = await wallet2.generateSaveWalletData(
passwordToUse,
crypto.kdfThreads,
() => {}
);
setWalletToBeDownloaded({
wallet,
qortAddress: rawWallet.address0,
});
return {
wallet,
qortAddress: rawWallet.address0,
};
};
const confirmPasswordToDownload = async () => {
try {
setWalletToBeDownloadedError('');
if (!keepCurrentPassword && !newPassword) {
setWalletToBeDownloadedError(
'Please enter a new password'
);
return;
}
if (!walletToBeDownloadedPassword) {
setWalletToBeDownloadedError(
'Please enter your password'
);
return;
}
setIsLoading(true);
await new Promise<void>((res) => {
setTimeout(() => {
res();
}, 250);
});
const newPasswordForWallet = !keepCurrentPassword ? newPassword : null;
const res = await saveWalletFunc(
walletToBeDownloadedPassword,
newPasswordForWallet
);
} catch (error: any) {
setWalletToBeDownloadedError(error?.message);
} finally {
setIsLoading(false);
}
};
return (
<>
<Spacer height="22px" />
<Box
sx={{
boxSizing: 'border-box',
display: 'flex',
justifyContent: 'flex-start',
maxWidth: '700px',
paddingLeft: '22px',
width: '100%',
}}
>
<img
style={{
cursor: "pointer",
height: '24px'
}}
onClick={returnToMain}
src={Return}
/>
</Box>
<Spacer height="10px" />
<div
className="image-container"
style={{
width: '136px',
height: '154px',
}}
>
<img src={Logo1Dark} className="base-image" />
</div>
<Spacer height="35px" />
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
}}
>
<TextP
sx={{
textAlign: 'start',
lineHeight: '24px',
fontSize: '20px',
fontWeight: 600,
}}
>
Download account
</TextP>
</Box>
<Spacer height="35px" />
{!walletToBeDownloaded && (
<>
<CustomLabel htmlFor="standard-adornment-password">
Confirm password
</CustomLabel>
<Spacer height="5px" />
<PasswordField
id="standard-adornment-password"
value={walletToBeDownloadedPassword}
onChange={(e) => setWalletToBeDownloadedPassword(e.target.value)}
/>
<Spacer height="20px" />
<FormControlLabel
sx={{
margin: 0,
}}
control={
<Checkbox
onChange={(e) => setKeepCurrentPassword(e.target.checked)}
checked={keepCurrentPassword}
edge="start"
tabIndex={-1}
disableRipple
sx={{
'&.Mui-checked': {
color: theme.palette.text.secondary,
},
'& .MuiSvgIcon-root': {
color: theme.palette.text.secondary,
},
}}
/>
}
label={
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Typography sx={{ fontSize: '14px' }}>
Keep current password
</Typography>
</Box>
}
/>
<Spacer height="20px" />
{!keepCurrentPassword && (
<>
<CustomLabel htmlFor="standard-adornment-password">
New password
</CustomLabel>
<Spacer height="5px" />
<PasswordField
id="standard-adornment-password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
/>
<Spacer height="20px" />
</>
)}
<CustomButton onClick={confirmPasswordToDownload}>
Confirm wallet password
</CustomButton>
<ErrorText>{walletToBeDownloadedError}</ErrorText>
</>
)}
{walletToBeDownloaded && (
<>
<CustomButton
onClick={async () => {
await saveFileToDiskFunc();
await showInfo({
message: 'Keep your account file secure',
});
}}
>
Download account
</CustomButton>
</>
)}
</>
);
};

View File

@ -79,7 +79,7 @@ export const AdminSpaceInner = ({
const res = await fetch( const res = await fetch(
`${getBaseApiReact()}/arbitrary/DOCUMENT_PRIVATE/${ `${getBaseApiReact()}/arbitrary/DOCUMENT_PRIVATE/${
getLatestPublish.name getLatestPublish.name
}/${getLatestPublish.identifier}?encoding=base64` }/${getLatestPublish.identifier}?encoding=base64&rebuild=true`
); );
data = await res.text(); data = await res.text();

View File

@ -15,12 +15,12 @@ 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, subscribeToEvent, unsubscribeFromEvent } from '../../utils/events' import { executeEvent, subscribeToEvent, unsubscribeFromEvent } from '../../utils/events'
import { Box, ButtonBase, Divider, Typography } from '@mui/material' import { Box, ButtonBase, Divider, IconButton, Tooltip, 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 { getFee, isExtMsg } from '../../background'
import MentionList from './MentionList' import MentionList from './MentionList'
import { ChatOptions } from './ChatOptions' import { ChatOptions } from './ChatOptions'
import { isFocusedParentGroupAtom } from '../../atoms/global' import { isFocusedParentGroupAtom } from '../../atoms/global'
@ -28,6 +28,10 @@ import { useRecoilState } from 'recoil'
import AppViewerContainer from '../Apps/AppViewerContainer' import AppViewerContainer from '../Apps/AppViewerContainer'
import CloseIcon from "@mui/icons-material/Close"; import CloseIcon from "@mui/icons-material/Close";
import { throttle } from 'lodash' import { throttle } from 'lodash'
import ImageIcon from '@mui/icons-material/Image';
import { messageHasImage } from '../../utils/chat'
const uidImages = new ShortUniqueId({ length: 12 });
const uid = new ShortUniqueId({ length: 5 }); const uid = new ShortUniqueId({ length: 5 });
@ -55,8 +59,9 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
const editorRef = useRef(null); const editorRef = useRef(null);
const { queueChats, addToQueue, processWithNewMessages } = useMessageQueue(); const { queueChats, addToQueue, processWithNewMessages } = useMessageQueue();
const handleUpdateRef = useRef(null); const handleUpdateRef = useRef(null);
const {isUserBlocked} = useContext(MyContext) const {isUserBlocked, show} = useContext(MyContext)
const [chatImagesToSave, setChatImagesToSave] = useState([]);
const [isDeleteImage, setIsDeleteImage] = useState(false);
const lastReadTimestamp = useRef(null) const lastReadTimestamp = useRef(null)
@ -624,6 +629,8 @@ if(isFocusedParent === false){
setReplyMessage(null) setReplyMessage(null)
setOnEditMessage(null) setOnEditMessage(null)
clearEditorContent() clearEditorContent()
setIsDeleteImage(false);
setChatImagesToSave([]);
} }
}, [isFocusedParent]) }, [isFocusedParent])
const clearEditorContent = () => { const clearEditorContent = () => {
@ -644,88 +651,193 @@ const clearEditorContent = () => {
const sendMessage = async ()=> { const sendMessage = async () => {
try { try {
if(messageSize > 4000) return if (messageSize > 4000) return; // TODO magic number
if(isPrivate === null) throw new Error('Unable to determine if group is private') if (isPrivate === null)
if(isSending) return throw new Error(
if(+balance < 4) throw new Error('You need at least 4 QORT to send a message') "Onable to determine if group is private"
pauseAllQueues() );
if (isSending) return;
if (+balance < 4)
// TODO magic number
throw new Error(
"You need at least 4 QORT to send a message"
);
pauseAllQueues();
if (editorRef.current) { if (editorRef.current) {
const htmlContent = editorRef.current.getHTML(); let htmlContent = editorRef.current.getHTML();
const deleteImage =
if(!htmlContent?.trim() || htmlContent?.trim() === '<p></p>') return onEditMessage && isDeleteImage && messageHasImage(onEditMessage);
setIsSending(true) const hasImage =
const message = isPrivate === false ? editorRef.current.getJSON() : htmlContent chatImagesToSave?.length > 0 || onEditMessage?.images?.length > 0;
const secretKeyObject = await getSecretKey(false, true) if (
(!htmlContent?.trim() || htmlContent?.trim() === '<p></p>') &&
!hasImage &&
!deleteImage
)
return;
if (htmlContent?.trim() === '<p></p>') {
htmlContent = null;
}
setIsSending(true);
const message =
isPrivate === false
? !htmlContent
? '<p></p>'
: editorRef.current.getJSON()
: htmlContent;
const secretKeyObject = await getSecretKey(false, true);
let repliedTo = replyMessage?.signature let repliedTo = replyMessage?.signature;
if (replyMessage?.chatReference) { if (replyMessage?.chatReference) {
repliedTo = replyMessage?.chatReference repliedTo = replyMessage?.chatReference;
} }
let chatReference = onEditMessage?.signature
const publicData = isPrivate ? {} : { const chatReference = onEditMessage?.signature;
isEdited : chatReference ? true : false,
}
const otherData = {
repliedTo,
...(onEditMessage?.decryptedData || {}),
type: chatReference ? 'edit' : '',
specialId: uid.rnd(),
...publicData
}
const objectMessage = {
...(otherData || {}),
[isPrivate ? 'message' : 'messageText']: message,
version: 3
}
const message64: any = await objectToBase64(objectMessage)
const encryptSingle = isPrivate === false ? JSON.stringify(objectMessage) : await encryptChatMessage(message64, secretKeyObject)
// const res = await sendChatGroup({groupId: selectedGroup,messageText: encryptSingle})
const sendMessageFunc = async () => {
return await sendChatGroup({groupId: selectedGroup,messageText: encryptSingle, chatReference})
};
// Add the function to the queue const publicData = isPrivate
const messageObj = { ? {}
message: { : {
text: htmlContent, isEdited: chatReference ? true : false,
timestamp: Date.now(), };
senderName: myName,
sender: myAddress, interface ImageToPublish {
...(otherData || {}) service: string;
}, identifier: string;
chatReference name: string;
} base64: string;
addToQueue(sendMessageFunc, messageObj, 'chat', }
selectedGroup );
setTimeout(() => { const imagesToPublish: ImageToPublish[] = [];
executeEvent("sent-new-message-group", {})
}, 150); if (deleteImage) {
clearEditorContent() const fee = await getFee('ARBITRARY');
setReplyMessage(null) await show({
setOnEditMessage(null) publishFee: fee.fee + ' QORT',
message: "Would you like to delete your previous chat image?",
});
// TODO magic string
await window.sendMessage('publishOnQDN', {
data: 'RA==',
identifier: onEditMessage?.images[0]?.identifier,
service: onEditMessage?.images[0]?.service,
uploadType: 'base64',
});
}
if (chatImagesToSave?.length > 0) {
const imageToSave = chatImagesToSave[0];
const base64ToSave = isPrivate
? await encryptChatMessage(imageToSave, secretKeyObject)
: imageToSave;
// 1 represents public group, 0 is private
const identifier = `grp-q-manager_${isPrivate ? 0 : 1}_group_${selectedGroup}_${uidImages.rnd()}`;
imagesToPublish.push({
service: 'IMAGE',
identifier,
name: myName,
base64: base64ToSave,
});
const res = await window.sendMessage(
'PUBLISH_MULTIPLE_QDN_RESOURCES',
{
resources: imagesToPublish,
},
240000,
true
);
if (res?.error)
throw new Error(
"Unable to publish image"
);
}
const images =
imagesToPublish?.length > 0
? imagesToPublish.map((item) => {
return {
name: item.name,
identifier: item.identifier,
service: item.service,
timestamp: Date.now(),
};
})
: chatReference
? isDeleteImage
? []
: onEditMessage?.images || []
: [];
const otherData = {
repliedTo,
...(onEditMessage?.decryptedData || {}),
type: chatReference ? 'edit' : '',
specialId: uid.rnd(),
images: images,
...publicData,
};
const objectMessage = {
...(otherData || {}),
[isPrivate ? 'message' : 'messageText']: message,
version: 3,
};
const message64: any = await objectToBase64(objectMessage);
const encryptSingle =
isPrivate === false
? JSON.stringify(objectMessage)
: await encryptChatMessage(message64, secretKeyObject);
const sendMessageFunc = async () => {
return await sendChatGroup({
groupId: selectedGroup,
messageText: encryptSingle,
chatReference,
});
};
// Add the function to the queue
const messageObj = {
message: {
text: htmlContent,
timestamp: Date.now(),
senderName: myName,
sender: myAddress,
...(otherData || {}),
},
chatReference,
};
addToQueue(sendMessageFunc, messageObj, 'chat', selectedGroup);
setTimeout(() => {
executeEvent('sent-new-message-group', {});
}, 150);
clearEditorContent();
setReplyMessage(null);
setOnEditMessage(null);
setIsDeleteImage(false);
setChatImagesToSave([]);
} }
// send chat message // send chat message
} catch (error) { } catch (error) {
const errorMsg = error?.message || error const errorMsg = error?.message || error;
setInfoSnack({ setInfoSnack({
type: "error", type: 'error',
message: errorMsg, message: errorMsg,
}); });
setOpenSnack(true); setOpenSnack(true);
console.error(error) console.error(error);
} finally { } finally {
setIsSending(false) setIsSending(false);
resumeAllQueues() resumeAllQueues();
} }
} };
useEffect(() => { useEffect(() => {
if (hide) { if (hide) {
@ -742,7 +854,8 @@ const sendMessage = async ()=> {
setReplyMessage(message) setReplyMessage(message)
setOnEditMessage(null) setOnEditMessage(null)
setIsFocusedParent(true); setIsFocusedParent(true);
setIsDeleteImage(false);
setChatImagesToSave([]);
setTimeout(() => { setTimeout(() => {
editorRef?.current?.chain().focus() editorRef?.current?.chain().focus()
@ -755,7 +868,7 @@ const sendMessage = async ()=> {
setReplyMessage(null) setReplyMessage(null)
setIsFocusedParent(true); setIsFocusedParent(true);
setTimeout(() => { setTimeout(() => {
editorRef.current.chain().focus().setContent(message?.messageText || message?.text).run(); editorRef?.current?.chain().focus().setContent(message?.messageText || message?.text || '<p></p>').run();
}, 250); }, 250);
}, []) }, [])
@ -824,6 +937,24 @@ const sendMessage = async ()=> {
resumeAllQueues() resumeAllQueues()
} }
}, [isPrivate]) }, [isPrivate])
const insertImage = useCallback(
(img) => {
if (
chatImagesToSave?.length > 0 ||
(messageHasImage(onEditMessage) && !isDeleteImage)
) {
setInfoSnack({
type: 'error',
message: 'This message already has an image',
});
setOpenSnack(true);
return;
}
setChatImagesToSave((prev) => [...prev, img]);
},
[chatImagesToSave, onEditMessage?.images, isDeleteImage]
);
return ( return (
<div style={{ <div style={{
@ -864,6 +995,117 @@ const sendMessage = async ()=> {
overflow: !isMobile && "auto", overflow: !isMobile && "auto",
flexShrink: 0 flexShrink: 0
}}> }}>
<Box
sx={{
alignItems: 'flex-start',
display: 'flex',
width: '100%',
gap: '10px',
flexWrap: 'wrap',
}}
>
{!isDeleteImage &&
onEditMessage &&
messageHasImage(onEditMessage) &&
onEditMessage?.images?.map((_, index) => (
<div
key={index}
style={{
position: 'relative',
height: '50px',
width: '50px',
}}
>
<ImageIcon
sx={{
height: '100%',
width: '100%',
borderRadius: '3px',
color:'white'
}}
/>
<Tooltip title="Delete image">
<IconButton
onClick={() => setIsDeleteImage(true)}
size="small"
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
backgroundColor: (theme) =>
theme.palette.background.paper,
color: (theme) => theme.palette.text.primary,
borderRadius: '50%',
opacity: 0,
transition: 'opacity 0.2s',
boxShadow: (theme) => theme.shadows[2],
'&:hover': {
backgroundColor: (theme) =>
theme.palette.background.default,
opacity: 1,
},
pointerEvents: 'auto',
}}
>
<CloseIcon fontSize="small" />
</IconButton>
</Tooltip>
</div>
))}
{chatImagesToSave.map((imgBase64, index) => (
<div
key={index}
style={{
position: 'relative',
height: '50px',
width: '50px',
}}
>
<img
src={`data:image/webp;base64,${imgBase64}`}
style={{
height: '100%',
width: '100%',
objectFit: 'contain',
borderRadius: '3px',
}}
/>
<Tooltip title="Remove image">
<IconButton
onClick={() =>
setChatImagesToSave((prev) =>
prev.filter((_, i) => i !== index)
)
}
size="small"
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
backgroundColor: (theme) =>
theme.palette.background.paper,
color: (theme) => theme.palette.text.primary,
borderRadius: '50%',
opacity: 0,
transition: 'opacity 0.2s',
boxShadow: (theme) => theme.shadows[2],
'&:hover': {
backgroundColor: (theme) =>
theme.palette.background.default,
opacity: 1,
},
pointerEvents: 'auto',
}}
>
<CloseIcon fontSize="small" />
</IconButton>
</Tooltip>
</div>
))}
</Box>
{replyMessage && ( {replyMessage && (
<Box sx={{ <Box sx={{
display: 'flex', display: 'flex',
@ -895,7 +1137,7 @@ const sendMessage = async ()=> {
}}> }}>
<Tiptap isReply={onEditMessage || replyMessage} enableMentions setEditorRef={setEditorRef} onEnter={sendMessage} isChat disableEnter={isMobile ? true : false} isFocusedParent={isFocusedParent} setIsFocusedParent={setIsFocusedParent} membersWithNames={members} /> <Tiptap isReply={onEditMessage || replyMessage} enableMentions setEditorRef={setEditorRef} onEnter={sendMessage} isChat disableEnter={isMobile ? true : false} isFocusedParent={isFocusedParent} setIsFocusedParent={setIsFocusedParent} membersWithNames={members} insertImage={insertImage} />

View File

@ -261,17 +261,18 @@ export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onR
if (chatReferences?.[message.signature]) { if (chatReferences?.[message.signature]) {
reactions = chatReferences[message.signature]?.reactions || null; reactions = chatReferences[message.signature]?.reactions || null;
if (chatReferences[message.signature]?.edit?.message && message?.text) { if (chatReferences[message.signature]?.edit) {
message.text = chatReferences[message.signature]?.edit?.message; message.text =
message.isEdit = true chatReferences[message.signature]?.edit?.message;
message.editTimestamp = chatReferences[message.signature]?.edit?.timestamp message.messageText =
} chatReferences[message.signature]?.edit?.messageText;
if (chatReferences[message.signature]?.edit?.messageText && message?.messageText) { message.images =
message.messageText = chatReferences[message.signature]?.edit?.messageText; chatReferences[message.signature]?.edit?.images;
message.isEdit = true
message.editTimestamp = chatReferences[message.signature]?.edit?.timestamp message.isEdit = true;
message.editTimestamp =
chatReferences[message.signature]?.edit?.timestamp;
} }
} }
// Check if message is updating // Check if message is updating

View File

@ -39,6 +39,7 @@ import StarterKit from "@tiptap/starter-kit";
import Underline from "@tiptap/extension-underline"; import Underline from "@tiptap/extension-underline";
import { generateHTML } from "@tiptap/react"; import { generateHTML } from "@tiptap/react";
import ErrorBoundary from "../../common/ErrorBoundary"; import ErrorBoundary from "../../common/ErrorBoundary";
import { isHtmlString } from "../../utils/chat";
const extractTextFromHTML = (htmlString = '') => { const extractTextFromHTML = (htmlString = '') => {
return convert(htmlString, { return convert(htmlString, {
@ -59,27 +60,30 @@ export const ChatOptions = ({ messages : untransformedMessages, goToMessage, mem
const parentRefMentions = useRef(); const parentRefMentions = useRef();
const [lastMentionTimestamp, setLastMentionTimestamp] = useState(null) const [lastMentionTimestamp, setLastMentionTimestamp] = useState(null)
const [debouncedValue, setDebouncedValue] = useState(""); // Debounced value const [debouncedValue, setDebouncedValue] = useState(""); // Debounced value
const messages = useMemo(()=> { const messages = useMemo(() => {
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,
return { Mention,
...item, ]);
messageText: transformedMessage return {
} ...item,
messageText: transformedMessage,
};
} catch (error) { } catch (error) {
// error console.log(error);
} }
} else return item } else return item;
}) });
}, [untransformedMessages]) }, [untransformedMessages]);
const getTimestampMention = async () => { const getTimestampMention = async () => {
try { try {

View File

@ -66,7 +66,7 @@ export const CreateCommonSecret = ({groupId, secretKey, isOwner, myAddress, sec
const res = await fetch( const res = await fetch(
`${getBaseApiReact()}/arbitrary/DOCUMENT_PRIVATE/${publish.name}/${ `${getBaseApiReact()}/arbitrary/DOCUMENT_PRIVATE/${publish.name}/${
publish.identifier publish.identifier
}?encoding=base64` }?encoding=base64&rebuild=true`
); );
const data = await res.text(); const data = await res.text();

View File

@ -33,6 +33,9 @@ import level7Img from "../../assets/badges/level-7.png"
import level8Img from "../../assets/badges/level-8.png" 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 { buildImageEmbedLink, isHtmlString, messageHasImage } from "../../utils/chat";
import CommentsDisabledIcon from '@mui/icons-material/CommentsDisabled';
const getBadgeImg = (level)=> { const getBadgeImg = (level)=> {
switch(level?.toString()){ switch(level?.toString()){
@ -102,35 +105,33 @@ useEffect(()=> {
getInfo() getInfo()
}, [message?.sender, getIndividualUserInfo]) }, [message?.sender, getIndividualUserInfo])
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,
Highlight, Highlight,
Mention, Mention,
TextStyle TextStyle,
]) ]);
} }
}, [message?.editTimestamp]);
}, [message?.editTimestamp])
const htmlReply = useMemo(() => {
if (reply?.messageText) {
const htmlReply = useMemo(()=> { const isHtml = isHtmlString(reply?.messageText);
if (isHtml) return reply?.messageText;
if(reply?.messageText){
return generateHTML(reply?.messageText, [ return generateHTML(reply?.messageText, [
StarterKit, StarterKit,
Underline, Underline,
Highlight, Highlight,
Mention, Mention,
TextStyle TextStyle,
]) ]);
} }
}, [reply?.editTimestamp]);
}, [reply?.editTimestamp])
const userAvatarUrl = useMemo(()=> { const userAvatarUrl = useMemo(()=> {
return message?.senderName ? `${getBaseApiReact()}/arbitrary/THUMBNAIL/${ return message?.senderName ? `${getBaseApiReact()}/arbitrary/THUMBNAIL/${
@ -142,6 +143,13 @@ const onSeenFunc = useCallback(()=> {
onSeen(message.id); onSeen(message.id);
}, [message?.id]) }, [message?.id])
const hasNoMessage =
(!message.decryptedData?.data?.message ||
message.decryptedData?.data?.message === '<p></p>') &&
(message?.images || [])?.length === 0 &&
(!message?.messageText || message?.messageText === '<p></p>') &&
(!message?.text || message?.text === '<p></p>');
return ( return (
<MessageWragger lastMessage={lastSignature === message?.signature} isLast={isLast} onSeen={onSeenFunc}> <MessageWragger lastMessage={lastSignature === message?.signature} isLast={isLast} onSeen={onSeenFunc}>
{message?.divide && ( {message?.divide && (
@ -335,7 +343,7 @@ const onSeenFunc = useCallback(()=> {
</Box> </Box>
</> </>
)} )}
{message?.messageText && ( {htmlText && !hasNoMessage && (
<MessageDisplay <MessageDisplay
htmlContent={htmlText} htmlContent={htmlText}
setMobileViewModeKeepOpen={setMobileViewModeKeepOpen} setMobileViewModeKeepOpen={setMobileViewModeKeepOpen}
@ -343,9 +351,30 @@ const onSeenFunc = useCallback(()=> {
)} )}
{message?.decryptedData?.type === "notification" ? ( {message?.decryptedData?.type === "notification" ? (
<MessageDisplay htmlContent={message.decryptedData?.data?.message} /> <MessageDisplay htmlContent={message.decryptedData?.data?.message} />
) : ( ) : hasNoMessage ? null : (
<MessageDisplay setMobileViewModeKeepOpen={setMobileViewModeKeepOpen} htmlContent={message.text} /> <MessageDisplay setMobileViewModeKeepOpen={setMobileViewModeKeepOpen} htmlContent={message.text} />
)} )}
{hasNoMessage && (
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: '10px',
}}
>
<CommentsDisabledIcon sx={{
color: 'white'
}} />
<Typography sx={{
color: 'white'
}}>
No Message
</Typography>
</Box>
)}
{message?.images && messageHasImage(message) && (
<Embed embedLink={buildImageEmbedLink(message.images[0])} />
)}
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
@ -519,6 +548,19 @@ const onSeenFunc = useCallback(()=> {
export const ReplyPreview = ({message, isEdit})=> { export const ReplyPreview = ({message, isEdit})=> {
const replyMessageText = useMemo(() => {
if (!message?.messageText) return null;
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={{
@ -553,15 +595,9 @@ export const ReplyPreview = ({message, isEdit})=> {
}}>Replied to {message?.senderName || message?.senderAddress}</Typography> }}>Replied to {message?.senderName || message?.senderAddress}</Typography>
)} )}
{message?.messageText && ( {replyMessageText && (
<MessageDisplay <MessageDisplay
htmlContent={generateHTML(message?.messageText, [ htmlContent={replyMessageText}
StarterKit,
Underline,
Highlight,
Mention,
TextStyle
])}
/> />
)} )}
{message?.decryptedData?.type === "notification" ? ( {message?.decryptedData?.type === "notification" ? (

View File

@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useRef, useState } from "react"; import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { EditorProvider, useCurrentEditor, useEditor } from "@tiptap/react"; import { EditorProvider, useCurrentEditor, useEditor } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit"; import StarterKit from "@tiptap/starter-kit";
import { Color } from "@tiptap/extension-color"; import { Color } from "@tiptap/extension-color";
@ -34,6 +34,7 @@ import ListItemButton from '@mui/material/ListItemButton';
import ListItemText from '@mui/material/ListItemText'; import ListItemText from '@mui/material/ListItemText';
import { ReactRenderer } from '@tiptap/react' import { ReactRenderer } from '@tiptap/react'
import MentionList from './MentionList.jsx' import MentionList from './MentionList.jsx'
import { fileToBase64 } from "../../utils/fileReading/index.js";
function textMatcher(doc, from) { function textMatcher(doc, from) {
const textBeforeCursor = doc.textBetween(0, from, ' ', ' '); const textBeforeCursor = doc.textBetween(0, from, ' ', ' ');
@ -110,13 +111,13 @@ const MenuBar = ({ setEditorRef, isChat }) => {
}; };
useEffect(() => { useEffect(() => {
if (editor) { if (editor && !isChat) {
editor.view.dom.addEventListener("paste", handlePaste); editor.view.dom.addEventListener("paste", handlePaste);
return () => { return () => {
editor.view.dom.removeEventListener("paste", handlePaste); editor.view.dom.removeEventListener("paste", handlePaste);
}; };
} }
}, [editor]); }, [editor, isChat]);
return ( return (
<div className="control-group"> <div className="control-group">
@ -299,7 +300,8 @@ export default ({
customEditorHeight, customEditorHeight,
membersWithNames, membersWithNames,
enableMentions, enableMentions,
isReply isReply,
insertImage,
}) => { }) => {
const extensionsFiltered = isChat const extensionsFiltered = isChat
@ -329,7 +331,35 @@ export default ({
}, [membersWithNames]) }, [membersWithNames])
const handleImageUpload = useCallback(async (file) => {
try {
if (!file.type.includes('image')) return;
let compressedFile = file;
if (file.type !== 'image/gif') {
await new Promise<void>((resolve) => {
new Compressor(file, {
quality: 0.6,
maxWidth: 1200,
mimeType: 'image/webp',
success(result) {
compressedFile = result;
resolve();
},
error(err) {
console.error('Image compression error:', err);
},
});
});
}
if (compressedFile) {
const toBase64 = await fileToBase64(compressedFile);
insertImage(toBase64);
}
} catch (error) {
console.error(error);
}
}, [insertImage]);
const usersRef = useRef([]); const usersRef = useRef([]);
@ -470,6 +500,25 @@ export default ({
} }
return false; return false;
}, },
handlePaste(view, event) {
if(!handleImageUpload) return
if (!isChat) return;
const items = event.clipboardData?.items;
if (!items) return false;
for (const item of items) {
if (item.type.startsWith('image/')) {
const file = item.getAsFile();
if (file) {
event.preventDefault(); // Block the default paste
handleImageUpload(file); // Custom handler
return true; // Let ProseMirror know we handled it
}
}
}
return false; // fallback to default behavior otherwise
},
}} }}
/> />
</div> </div>

View File

@ -1,10 +1,9 @@
import React, { useCallback, useEffect, useRef } from "react"; import React, { useCallback, useEffect, useRef } from "react";
import { getBaseApiReact } from "../../App";
import { truncate } from "lodash";
export const useBlockedAddresses = () => {
export const useBlockedAddresses = (isAuthenticated: boolean) => {
const userBlockedRef = useRef({}) const userBlockedRef = useRef({})
const userNamesBlockedRef = useRef({}) const userNamesBlockedRef = useRef({})
@ -19,7 +18,7 @@ export const useBlockedAddresses = () => {
const isUserBlocked = useCallback((address, name)=> { const isUserBlocked = useCallback((address, name)=> {
try { try {
if(!address) return false if(!address) return false
if(userBlockedRef.current[address] || userNamesBlockedRef.current[name]) return true if(userBlockedRef.current[address]) return true
return false return false
@ -29,6 +28,9 @@ export const useBlockedAddresses = () => {
}, []) }, [])
useEffect(()=> { useEffect(()=> {
if (!isAuthenticated) return;
userBlockedRef.current = {};
userNamesBlockedRef.current = {};
const fetchBlockedList = async ()=> { const fetchBlockedList = async ()=> {
try { try {
const response = await new Promise((res, rej) => { const response = await new Promise((res, rej) => {
@ -87,46 +89,16 @@ export const useBlockedAddresses = () => {
} }
} }
fetchBlockedList() fetchBlockedList()
}, []) }, [isAuthenticated])
const removeBlockFromList = useCallback(async (address, name)=> { const removeBlockFromList = useCallback(async (address, name)=> {
await new Promise((res, rej) => { if(name){
window.sendMessage("listActions", {
type: 'remove',
items: name ? [name] : [address],
listName: name ? 'blockedNames' : 'blockedAddresses'
})
.then((response) => {
if (response.error) {
rej(response?.message);
return;
} else {
if(!name){
const copyObject = {...userBlockedRef.current}
delete copyObject[address]
userBlockedRef.current = copyObject
} else {
const copyObject = {...userNamesBlockedRef.current}
delete copyObject[name]
userNamesBlockedRef.current = copyObject
}
res(response);
}
})
.catch((error) => {
console.error("Failed qortalRequest", error);
});
})
if(name && userBlockedRef.current[address]){
await new Promise((res, rej) => { await new Promise((res, rej) => {
window.sendMessage("listActions", { window.sendMessage("listActions", {
type: 'remove', type: 'remove',
items: !name ? [name] : [address], items: [name] ,
listName: !name ? 'blockedNames' : 'blockedAddresses' listName: 'blockedNames'
}) })
.then((response) => { .then((response) => {
@ -134,9 +106,12 @@ export const useBlockedAddresses = () => {
rej(response?.message); rej(response?.message);
return; return;
} else { } else {
const copyObject = {...userBlockedRef.current}
delete copyObject[address] const copyObject = {...userNamesBlockedRef.current}
userBlockedRef.current = copyObject delete copyObject[name]
userNamesBlockedRef.current = copyObject
res(response); res(response);
} }
}) })
@ -145,42 +120,95 @@ export const useBlockedAddresses = () => {
}); });
}) })
} }
if(address){
await new Promise((res, rej) => {
window.sendMessage("listActions", {
type: 'remove',
items: [address],
listName: 'blockedAddresses'
})
.then((response) => {
if (response.error) {
rej(response?.message);
return;
} else {
const copyObject = {...userBlockedRef.current}
delete copyObject[address]
userBlockedRef.current = copyObject
res(response);
}
})
.catch((error) => {
console.error("Failed qortalRequest", error);
});
})
}
}, []) }, [])
const addToBlockList = useCallback(async (address, name)=> { const addToBlockList = useCallback(async (address, name)=> {
await new Promise((res, rej) => { if(name){
window.sendMessage("listActions", { await new Promise((res, rej) => {
window.sendMessage("listActions", {
type: 'add',
items: name ? [name] : [address],
listName: name ? 'blockedNames' : 'blockedAddresses'
})
.then((response) => {
if (response.error) {
rej(response?.message);
return;
} else {
if(name){
const copyObject = {...userNamesBlockedRef.current}
copyObject[name] = true
userNamesBlockedRef.current = copyObject
}else {
const copyObject = {...userBlockedRef.current}
copyObject[address] = true
userBlockedRef.current = copyObject
}
res(response); type: 'add',
} items: [name],
listName: 'blockedNames'
})
.then((response) => {
if (response.error) {
rej(response?.message);
return;
} else {
const copyObject = {...userNamesBlockedRef.current}
copyObject[name] = true
userNamesBlockedRef.current = copyObject
res(response);
}
})
.catch((error) => {
console.error("Failed qortalRequest", error);
});
}) })
.catch((error) => { }
console.error("Failed qortalRequest", error); if(address){
}); await new Promise((res, rej) => {
}) window.sendMessage("listActions", {
type: 'add',
items: [address],
listName: 'blockedAddresses'
})
.then((response) => {
if (response.error) {
rej(response?.message);
return;
} else {
const copyObject = {...userBlockedRef.current}
copyObject[address] = true
userBlockedRef.current = copyObject
res(response);
}
})
.catch((error) => {
console.error("Failed qortalRequest", error);
});
})
}
}, []) }, [])
return { return {

View File

@ -52,6 +52,8 @@ export const ImageCard = ({
backgroundColor: "#1F2023", backgroundColor: "#1F2023",
height: height, height: height,
transition: "height 0.6s ease-in-out", transition: "height 0.6s ease-in-out",
display: 'flex',
flexDirection: 'column',
}} }}
> >
<Box <Box
@ -170,8 +172,18 @@ export const ImageCard = ({
)} )}
</Box> </Box>
<Box> <Box
<CardContent> sx={{
maxHeight: '100%',
flexGrow: 1,
overflow: 'hidden',
}}
>
<CardContent
sx={{
height: '100%',
}}
>
<ImageViewer src={image} /> <ImageViewer src={image} />
</CardContent> </CardContent>
</Box> </Box>
@ -203,6 +215,7 @@ export const ImageCard = ({
display: "flex", display: "flex",
justifyContent: "center", justifyContent: "center",
cursor: "pointer", cursor: "pointer",
height: '100%',
}} }}
onClick={handleOpenFullscreen} onClick={handleOpenFullscreen}
> >
@ -239,6 +252,9 @@ export const ImageCard = ({
position: "relative", position: "relative",
width: "100%", width: "100%",
height: "100%", height: "100%",
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
backgroundColor: "#000", backgroundColor: "#000",
}} }}
> >

View File

@ -6,19 +6,30 @@ import {
DialogContent, DialogContent,
DialogContentText, DialogContentText,
DialogTitle, DialogTitle,
IconButton,
TextField, TextField,
Typography, Typography,
} from "@mui/material"; } from "@mui/material";
import React, { useContext, useEffect, useState } from "react"; import React, { useContext, useEffect, useState } from "react";
import { MyContext } from "../../App"; import { getBaseApiReact, MyContext } from "../../App";
import { Spacer } from "../../common/Spacer"; import { Spacer } from "../../common/Spacer";
import { executeEvent } from "../../utils/events"; import { executeEvent, subscribeToEvent, unsubscribeFromEvent } from "../../utils/events";
import { validateAddress } from "../../utils/validateAddress";
import { getNameInfo, requestQueueMemberNames } from "./Group";
import { useModal } from "../../common/useModal";
import { useRecoilState } from "recoil";
import { isOpenBlockedModalAtom } from "../../atoms/global";
import CloseIcon from '@mui/icons-material/Close';
export const BlockedUsersModal = ({ close }) => { import InfoIcon from '@mui/icons-material/Info';
export const BlockedUsersModal = () => {
const [isOpenBlockedModal, setIsOpenBlockedModal] = useRecoilState(isOpenBlockedModalAtom)
const [hasChanged, setHasChanged] = useState(false); const [hasChanged, setHasChanged] = useState(false);
const [value, setValue] = useState(""); const [value, setValue] = useState("");
const [addressesWithNames, setAddressesWithNames] = useState({})
const { getAllBlockedUsers, removeBlockFromList, addToBlockList } = useContext(MyContext); const { isShow, onCancel, onOk, show, message } = useModal();
const { getAllBlockedUsers, removeBlockFromList, addToBlockList, setOpenSnackGlobal, setInfoSnackCustom } =
useContext(MyContext);
const [blockedUsers, setBlockedUsers] = useState({ const [blockedUsers, setBlockedUsers] = useState({
addresses: {}, addresses: {},
names: {}, names: {},
@ -28,60 +39,162 @@ export const BlockedUsersModal = ({ close }) => {
}; };
useEffect(() => { useEffect(() => {
if(!isOpenBlockedModal) return
fetchBlockedUsers(); fetchBlockedUsers();
}, []); }, [isOpenBlockedModal]);
const getNames = async () => {
// const validApi = await findUsableApi();
const addresses = Object.keys(blockedUsers?.addresses)
const addressNames = {}
const getMemNames = addresses.map(async (address) => {
const name = await requestQueueMemberNames.enqueue(() => {
return getNameInfo(address);
});
if (name) {
addressNames[address] = name
}
return true;
});
await Promise.all(getMemNames);
setAddressesWithNames(addressNames)
};
const blockUser = async (e, user?: string) => {
try {
const valUser = user || value
if (!valUser) return;
const isAddress = validateAddress(valUser);
let userName = null;
let userAddress = null;
if (isAddress) {
userAddress = valUser;
const name = await getNameInfo(valUser);
if (name) {
userName = name;
}
}
if (!isAddress) {
const response = await fetch(`${getBaseApiReact()}/names/${valUser}`);
const data = await response.json();
if (!data?.owner) throw new Error("Name does not exist");
if (data?.owner) {
userAddress = data.owner;
userName = valUser;
}
}
if(!userName){
await addToBlockList(userAddress, null);
fetchBlockedUsers();
setHasChanged(true);
executeEvent('updateChatMessagesWithBlocks', true)
setValue('')
return
}
const responseModal = await show({
userName,
userAddress,
});
if (responseModal === "both") {
await addToBlockList(userAddress, userName);
} else if (responseModal === "address") {
await addToBlockList(userAddress, null);
} else if (responseModal === "name") {
await addToBlockList(null, userName);
}
fetchBlockedUsers();
setHasChanged(true);
setValue('')
if(user){
setIsOpenBlockedModal(false)
}
if(responseModal === 'both' || responseModal === 'address'){
executeEvent('updateChatMessagesWithBlocks', true)
}
} catch (error) {
setOpenSnackGlobal(true);
setInfoSnackCustom({
type: "error",
message: error?.message || "Unable to block user",
});
}
};
const blockUserFromOutsideModalFunc = (e) => {
const user = e.detail?.user;
setIsOpenBlockedModal(true)
blockUser(null, user)
};
useEffect(() => {
subscribeToEvent("blockUserFromOutside", blockUserFromOutsideModalFunc);
return () => {
unsubscribeFromEvent("blockUserFromOutside", blockUserFromOutsideModalFunc);
};
}, []);
return ( return (
<Dialog <Dialog
open={true} open={isOpenBlockedModal}
aria-labelledby="alert-dialog-title" aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description" aria-describedby="alert-dialog-description"
> >
<DialogTitle>Blocked Users</DialogTitle> <DialogTitle>Blocked Users</DialogTitle>
<DialogContent sx={{ <DialogContent
padding: '20px' sx={{
}}> padding: "20px",
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "10px",
}} }}
> >
<TextField <Box
placeholder="Name" sx={{
value={value} display: "flex",
onChange={(e) => { alignItems: "center",
setValue(e.target.value); gap: "10px",
}} }}
/> >
<Button variant="contained" onClick={async ()=> { <TextField
try { placeholder="Name or address"
if(!value) return value={value}
await addToBlockList(undefined, value) onChange={(e) => {
fetchBlockedUsers() setValue(e.target.value);
setHasChanged(true) }}
} catch (error) { />
console.error(error) <Button
} sx={{
}}>Block</Button> flexShrink: 0,
</Box> }}
variant="contained"
onClick={blockUser}
>
Block
</Button>
</Box>
{Object.entries(blockedUsers?.addresses).length > 0 && ( {Object.entries(blockedUsers?.addresses).length > 0 && (
<> <>
<Spacer height="20px" /> <Spacer height="20px" />
<DialogContentText id="alert-dialog-description"> <DialogContentText id="alert-dialog-description">
Blocked Users for Chat ( addresses ) Blocked addresses- blocks processing of txs
</DialogContentText> </DialogContentText>
<Spacer height="10px" /> <Spacer height="10px" />
<Button variant="contained" size="small" onClick={getNames}>Fetch names</Button>
<Spacer height="10px" />
</> </>
)} )}
<Box sx={{ <Box
display: 'flex', sx={{
flexDirection: 'column', display: "flex",
gap: '10px' flexDirection: "column",
}}> gap: "10px",
}}
>
{Object.entries(blockedUsers?.addresses || {})?.map( {Object.entries(blockedUsers?.addresses || {})?.map(
([key, value]) => { ([key, value]) => {
return ( return (
@ -90,18 +203,22 @@ export const BlockedUsersModal = ({ close }) => {
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
gap: "10px", gap: "10px",
width: '100%', width: "100%",
justifyContent: 'space-between' justifyContent: "space-between",
}} }}
> >
<Typography>{key}</Typography> <Typography>{addressesWithNames[key] || key}</Typography>
<Button <Button
sx={{
flexShrink: 0,
}}
size="small"
variant="contained" variant="contained"
onClick={async () => { onClick={async () => {
try { try {
await removeBlockFromList(key, undefined); await removeBlockFromList(key, undefined);
setHasChanged(true); setHasChanged(true);
setValue('') setValue("");
fetchBlockedUsers(); fetchBlockedUsers();
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@ -119,17 +236,19 @@ export const BlockedUsersModal = ({ close }) => {
<> <>
<Spacer height="20px" /> <Spacer height="20px" />
<DialogContentText id="alert-dialog-description"> <DialogContentText id="alert-dialog-description">
Blocked Users for QDN and Chat (names) Blocked names for QDN
</DialogContentText> </DialogContentText>
<Spacer height="10px" /> <Spacer height="10px" />
</> </>
)} )}
<Box sx={{ <Box
display: 'flex', sx={{
flexDirection: 'column', display: "flex",
gap: '10px' flexDirection: "column",
}}> gap: "10px",
}}
>
{Object.entries(blockedUsers?.names || {})?.map(([key, value]) => { {Object.entries(blockedUsers?.names || {})?.map(([key, value]) => {
return ( return (
<Box <Box
@ -137,12 +256,16 @@ export const BlockedUsersModal = ({ close }) => {
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
gap: "10px", gap: "10px",
width: '100%', width: "100%",
justifyContent: 'space-between' justifyContent: "space-between",
}} }}
> >
<Typography>{key}</Typography> <Typography>{key}</Typography>
<Button <Button
size="small"
sx={{
flexShrink: 0,
}}
variant="contained" variant="contained"
onClick={async () => { onClick={async () => {
try { try {
@ -175,16 +298,78 @@ export const BlockedUsersModal = ({ close }) => {
}, },
}} }}
variant="contained" variant="contained"
onClick={()=> { onClick={() => {
if(hasChanged){ if (hasChanged) {
executeEvent('updateChatMessagesWithBlocks', true) executeEvent("updateChatMessagesWithBlocks", true);
} }
close() setIsOpenBlockedModal(false);
}} }}
> >
close close
</Button> </Button>
</DialogActions> </DialogActions>
<Dialog
open={isShow}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">
{"Decide what to block"}
</DialogTitle>
<IconButton
onClick={onCancel}
sx={{
position: 'absolute',
right: 8,
top: 8,
color: 'white',
}}
>
<CloseIcon />
</IconButton>
<DialogContent>
<DialogContentText id="alert-dialog-description">
Blocking {message?.userName || message?.userAddress}
</DialogContentText>
<Box sx={{
display: 'flex',
alignItems: 'center',
gap: '10px',
marginTop: '20px'
}}>
<InfoIcon sx={{
color: 'fff'
}}/> <Typography>Choose "block txs" or "all" to block chat messages </Typography>
</Box>
</DialogContent>
<DialogActions>
<Button
variant="contained"
onClick={() => {
onOk("address");
}}
>
Block txs
</Button>
<Button
variant="contained"
onClick={() => {
onOk("name");
}}
>
Block QDN data
</Button>
<Button
variant="contained"
onClick={() => {
onOk("both");
}}
>
Block All
</Button>
</DialogActions>
</Dialog>
</Dialog> </Dialog>
); );
}; };

View File

@ -19,7 +19,8 @@ import React, {
useRef, useRef,
useState, useState,
} from "react"; } from "react";
import BlockIcon from '@mui/icons-material/Block'; import PersonOffIcon from '@mui/icons-material/PersonOff';
import { WalletsAppWrapper } from "./WalletsAppWrapper"; import { WalletsAppWrapper } from "./WalletsAppWrapper";
import SettingsIcon from "@mui/icons-material/Settings"; import SettingsIcon from "@mui/icons-material/Settings";
@ -66,7 +67,7 @@ import HomeIcon from "@mui/icons-material/Home";
import CloseIcon from "@mui/icons-material/Close"; import CloseIcon from "@mui/icons-material/Close";
import { ThingsToDoInitial } from "./ThingsToDoInitial"; import { ThingsToDoInitial } from "./ThingsToDoInitial";
import { GroupJoinRequests } from "./GroupJoinRequests"; import { GroupJoinRequests, requestQueueGroupJoinRequests } from "./GroupJoinRequests";
import { GroupForum } from "../Chat/GroupForum"; import { GroupForum } from "../Chat/GroupForum";
import { GroupInvites } from "./GroupInvites"; import { GroupInvites } from "./GroupInvites";
import { import {
@ -99,7 +100,7 @@ import { formatEmailDate } from "./QMailMessages";
import { useHandleMobileNativeBack } from "../../hooks/useHandleMobileNativeBack"; import { useHandleMobileNativeBack } from "../../hooks/useHandleMobileNativeBack";
import { AdminSpace } from "../Chat/AdminSpace"; import { AdminSpace } from "../Chat/AdminSpace";
import { useRecoilState, useSetRecoilState } from "recoil"; import { useRecoilState, useSetRecoilState } from "recoil";
import { addressInfoControllerAtom, groupsPropertiesAtom, lastEnteredGroupIdAtom, selectedGroupIdAtom } from "../../atoms/global"; import { addressInfoControllerAtom, groupsPropertiesAtom, isOpenBlockedModalAtom, lastEnteredGroupIdAtom, myGroupsWhereIAmAdminAtom, selectedGroupIdAtom } from "../../atoms/global";
import { sortArrayByTimestampAndGroupName } from "../../utils/time"; import { sortArrayByTimestampAndGroupName } from "../../utils/time";
import { BlockedUsersModal } from "./BlockedUsersModal"; import { BlockedUsersModal } from "./BlockedUsersModal";
import { GlobalTouchMenu } from "../GlobalTouchMenu"; import { GlobalTouchMenu } from "../GlobalTouchMenu";
@ -329,16 +330,17 @@ 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 '';
} }
} }
export const getGroupAdmins = async (groupNumber: number) => { export const getGroupAdmins = async (groupNumber: number) => {
// const validApi = await findUsableApi(); // const validApi = await findUsableApi();
@ -470,6 +472,9 @@ export const Group = ({
const { setMemberGroups, memberGroups, rootHeight, isRunningPublicNode } = useContext(MyContext); const { setMemberGroups, memberGroups, rootHeight, isRunningPublicNode } = useContext(MyContext);
const lastGroupNotification = useRef<null | number>(null); const lastGroupNotification = useRef<null | number>(null);
const [timestampEnterData, setTimestampEnterData] = useState({}); const [timestampEnterData, setTimestampEnterData] = useState({});
const groupsPropertiesRef = useRef({});
const setMyGroupsWhereIAmAdmin = useSetRecoilState(myGroupsWhereIAmAdminAtom);
const [chatMode, setChatMode] = useState("groups"); const [chatMode, setChatMode] = useState("groups");
const [newChat, setNewChat] = useState(false); const [newChat, setNewChat] = useState(false);
const [openSnack, setOpenSnack] = React.useState(false); const [openSnack, setOpenSnack] = React.useState(false);
@ -483,6 +488,8 @@ export const Group = ({
const [groupAnnouncements, setGroupAnnouncements] = React.useState({}); const [groupAnnouncements, setGroupAnnouncements] = React.useState({});
const [defaultThread, setDefaultThread] = React.useState(null); const [defaultThread, setDefaultThread] = React.useState(null);
const [isOpenDrawer, setIsOpenDrawer] = React.useState(false); const [isOpenDrawer, setIsOpenDrawer] = React.useState(false);
const setIsOpenBlockedUserModal = useSetRecoilState(isOpenBlockedModalAtom)
const [hideCommonKeyPopup, setHideCommonKeyPopup] = React.useState(false); const [hideCommonKeyPopup, setHideCommonKeyPopup] = React.useState(false);
const [isLoadingGroupMessage, setIsLoadingGroupMessage] = React.useState(""); const [isLoadingGroupMessage, setIsLoadingGroupMessage] = React.useState("");
const [drawerMode, setDrawerMode] = React.useState("groups"); const [drawerMode, setDrawerMode] = React.useState("groups");
@ -507,7 +514,6 @@ export const Group = ({
const [isForceShowCreationKeyPopup, setIsForceShowCreationKeyPopup] = useState(false) const [isForceShowCreationKeyPopup, setIsForceShowCreationKeyPopup] = useState(false)
const [groupsProperties, setGroupsProperties] = useRecoilState(groupsPropertiesAtom) const [groupsProperties, setGroupsProperties] = useRecoilState(groupsPropertiesAtom)
const setUserInfoForLevels = useSetRecoilState(addressInfoControllerAtom); const setUserInfoForLevels = useSetRecoilState(addressInfoControllerAtom);
const [isOpenBlockedUserModal, setIsOpenBlockedUserModal] = React.useState(false);
const setLastEnteredGroupIdAtom = useSetRecoilState(lastEnteredGroupIdAtom) const setLastEnteredGroupIdAtom = useSetRecoilState(lastEnteredGroupIdAtom)
const isPrivate = useMemo(()=> { const isPrivate = useMemo(()=> {
if(selectedGroup?.groupId === '0') return false if(selectedGroup?.groupId === '0') return false
@ -533,6 +539,9 @@ export const Group = ({
useEffect(()=> { useEffect(()=> {
timestampEnterDataRef.current = timestampEnterData timestampEnterDataRef.current = timestampEnterData
}, [timestampEnterData]) }, [timestampEnterData])
useEffect(() => {
groupsPropertiesRef.current = groupsProperties;
}, [groupsProperties]);
useEffect(() => { useEffect(() => {
isFocusedRef.current = isFocused; isFocusedRef.current = isFocused;
@ -572,7 +581,7 @@ export const Group = ({
}); });
} catch (error) { } catch (error) {
console.log("error", error); console.error(error);
} }
}; };
@ -839,7 +848,7 @@ export const Group = ({
const res = await fetch( const res = await fetch(
`${getBaseApiReact()}/arbitrary/DOCUMENT_PRIVATE/${publish.name}/${ `${getBaseApiReact()}/arbitrary/DOCUMENT_PRIVATE/${publish.name}/${
publish.identifier publish.identifier
}?encoding=base64` }?encoding=base64&rebuild=true`
); );
data = await res.text(); data = await res.text();
} }
@ -985,15 +994,50 @@ export const Group = ({
} }
}, []) }, [])
const getGroupsWhereIAmAMember = useCallback(async (groups) => {
try {
let groupsAsAdmin = [];
const getAllGroupsAsAdmin = groups
.filter((item) => item.groupId !== '0')
.map(async (group) => {
const isAdminResponse = await requestQueueGroupJoinRequests.enqueue(
() => {
return fetch(
`${getBaseApiReact()}/groups/members/${group.groupId}?limit=0&onlyAdmins=true`
);
}
);
const isAdminData = await isAdminResponse.json();
useEffect(()=> { const findMyself = isAdminData?.members?.find(
if(!myAddress) return (member) => member.member === myAddress
if(areKeysEqual(groups?.map((grp)=> grp?.groupId), Object.keys(groupsProperties))){ );
} else {
getGroupsProperties(myAddress) if (findMyself) {
groupsAsAdmin.push(group);
}
return true;
});
await Promise.all(getAllGroupsAsAdmin);
setMyGroupsWhereIAmAdmin(groupsAsAdmin);
} catch (error) {
console.error();
} }
}, [groups, myAddress]) }, []);
useEffect(() => {
if (!myAddress) return;
if (
!areKeysEqual(
groups?.map((grp) => grp?.groupId),
Object.keys(groupsPropertiesRef.current)
)
) {
getGroupsProperties(myAddress);
getGroupsWhereIAmAMember(groups);
}
}, [groups, myAddress]);
useEffect(() => { useEffect(() => {
// Handler function for incoming messages // Handler function for incoming messages
@ -1905,6 +1949,7 @@ export const Group = ({
width: "100%", width: "100%",
justifyContent: "center", justifyContent: "center",
padding: "10px", padding: "10px",
gap: '10px'
}} }}
> >
<CustomButton <CustomButton
@ -1922,6 +1967,23 @@ export const Group = ({
/> />
New Chat New Chat
</CustomButton> </CustomButton>
{!isRunningPublicNode && (
<CustomButton
onClick={() => {
setIsOpenBlockedUserModal(true);
}}
sx={{
minWidth: 'unset',
padding: '10px',
}}
>
<PersonOffIcon
sx={{
color: 'white',
}}
/>
</CustomButton>
)}
</div> </div>
</div> </div>
); );
@ -2159,7 +2221,7 @@ export const Group = ({
padding: '10px' padding: '10px'
}} }}
> >
<BlockIcon <PersonOffIcon
sx={{ sx={{
color: "white", color: "white",
}} }}
@ -2656,11 +2718,9 @@ export const Group = ({
)} )}
</> </>
)} )}
{isOpenBlockedUserModal && (
<BlockedUsersModal close={()=> { <BlockedUsersModal />
setIsOpenBlockedUserModal(false)
}} />
)}
{selectedDirect && !newChat && ( {selectedDirect && !newChat && (
<> <>
<Box <Box
@ -2756,7 +2816,7 @@ export const Group = ({
/> />
)} )}
{isMobile && ( {isMobile && (
<Apps mode={appsMode} setMode={setAppsMode} show={mobileViewMode === "apps"} myName={userInfo?.name} /> <Apps mode={appsMode} setMode={setAppsMode} show={mobileViewMode === "apps"} myName={userInfo?.name} myAddress={userInfo?.address} />
)} )}
{!isMobile && ( {!isMobile && (
<AppsDesktop toggleSideViewGroups={toggleSideViewGroups} toggleSideViewDirects={toggleSideViewDirects} goToHome={goToHome} mode={appsMode} setMode={setAppsMode} setDesktopSideView={setDesktopSideView} hasUnreadDirects={directChatHasUnread} show={desktopViewMode === "apps"} myName={userInfo?.name} isGroups={isOpenSideViewGroups} <AppsDesktop toggleSideViewGroups={toggleSideViewGroups} toggleSideViewDirects={toggleSideViewDirects} goToHome={goToHome} mode={appsMode} setMode={setAppsMode} setDesktopSideView={setDesktopSideView} hasUnreadDirects={directChatHasUnread} show={desktopViewMode === "apps"} myName={userInfo?.name} isGroups={isOpenSideViewGroups}

View File

@ -17,7 +17,7 @@ import { CustomLoader } from "../../common/CustomLoader";
import { getBaseApi } from "../../background"; import { getBaseApi } from "../../background";
import { MyContext, getBaseApiReact, isMobile } from "../../App"; import { MyContext, getBaseApiReact, isMobile } from "../../App";
import { myGroupsWhereIAmAdminAtom } from "../../atoms/global"; import { myGroupsWhereIAmAdminAtom } from "../../atoms/global";
import { useSetRecoilState } from "recoil"; import { useRecoilState, useSetRecoilState } from "recoil";
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ExpandLessIcon from '@mui/icons-material/ExpandLess'; import ExpandLessIcon from '@mui/icons-material/ExpandLess';
export const requestQueueGroupJoinRequests = new RequestQueueWithPromise(2) export const requestQueueGroupJoinRequests = new RequestQueueWithPromise(2)
@ -28,66 +28,44 @@ export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, get
const [groupsWithJoinRequests, setGroupsWithJoinRequests] = React.useState([]) const [groupsWithJoinRequests, setGroupsWithJoinRequests] = React.useState([])
const [loading, setLoading] = React.useState(true) const [loading, setLoading] = React.useState(true)
const {txList, setTxList} = React.useContext(MyContext) const {txList, setTxList} = React.useContext(MyContext)
const setMyGroupsWhereIAmAdmin = useSetRecoilState( const [myGroupsWhereIAmAdmin] = useRecoilState(myGroupsWhereIAmAdminAtom);
myGroupsWhereIAmAdminAtom
);
const getJoinRequests = async ()=> {
const getJoinRequests = async () => {
try { try {
setLoading(true) setLoading(true);
const res = await Promise.all(
let groupsAsAdmin = [] myGroupsWhereIAmAdmin.map(async (group) => {
const getAllGroupsAsAdmin = groups.filter((item)=> item.groupId !== '0').map(async (group)=> { const joinRequestResponse =
await requestQueueGroupJoinRequests.enqueue(() => {
const isAdminResponse = await requestQueueGroupJoinRequests.enqueue(()=> { return fetch(
return fetch( `${getBaseApiReact()}/groups/joinrequests/${group.groupId}`
`${getBaseApiReact()}/groups/members/${group.groupId}?limit=0&onlyAdmins=true` );
); });
})
const isAdminData = await isAdminResponse.json()
const findMyself = isAdminData?.members?.find((member)=> member.member === myAddress) const joinRequestData = await joinRequestResponse.json();
return {
if(findMyself){ group,
groupsAsAdmin.push(group) data: joinRequestData,
} };
return true })
}) );
setGroupsWithJoinRequests(res);
await Promise.all(getAllGroupsAsAdmin)
setMyGroupsWhereIAmAdmin(groupsAsAdmin)
const res = await Promise.all(groupsAsAdmin.map(async (group)=> {
const joinRequestResponse = await requestQueueGroupJoinRequests.enqueue(()=> {
return fetch(
`${getBaseApiReact()}/groups/joinrequests/${group.groupId}`
);
})
const joinRequestData = await joinRequestResponse.json()
return {
group,
data: joinRequestData
}
}))
setGroupsWithJoinRequests(res)
} catch (error) { } catch (error) {
console.log(error);
} finally { } finally {
setLoading(false) setLoading(false);
} }
} };
React.useEffect(() => { React.useEffect(() => {
if (myAddress && groups.length > 0) { if (myAddress && myGroupsWhereIAmAdmin.length > 0) {
getJoinRequests() getJoinRequests();
} else { } else {
setLoading(false) setLoading(false);
} }
}, [myAddress, groups]); }, [myAddress, myGroupsWhereIAmAdmin]);
const filteredJoinRequests = React.useMemo(()=> { const filteredJoinRequests = React.useMemo(()=> {
return groupsWithJoinRequests.map((group)=> { return groupsWithJoinRequests.map((group)=> {

View File

@ -226,6 +226,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

@ -54,35 +54,19 @@ export const NewUsersCTA = ({ balance }) => {
textDecoration: "underline", textDecoration: "underline",
}} }}
onClick={() => { onClick={() => {
if (chrome && chrome.tabs) { window.open("https://link.qortal.dev/support", '_system')
chrome.tabs.create({ url: "https://link.qortal.dev/telegram-invite" }, (tab) => {
if (chrome.runtime.lastError) {
console.error("Error opening tab:", chrome.runtime.lastError);
} else {
console.log("Tab opened successfully:", tab);
}
});
}
}} }}
> >
Telegram Nextcloud
</ButtonBase> </ButtonBase>
<ButtonBase <ButtonBase
sx={{ sx={{
textDecoration: "underline", textDecoration: "underline",
}} }}
onClick={() => { onClick={() => {
if (chrome && chrome.tabs) { window.open("https://link.qortal.dev/discord-invite", '_system')
chrome.tabs.create({ url: "https://link.qortal.dev/discord-invite" }, (tab) => {
if (chrome.runtime.lastError) {
console.error("Error opening tab:", chrome.runtime.lastError);
} else {
console.log("Tab opened successfully:", tab);
}
});
}
}} }}
> >
Discord Discord
</ButtonBase> </ButtonBase>

View File

@ -67,6 +67,7 @@ const [isLoading, setIsLoading] = useState(false)
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

@ -89,14 +89,14 @@ export const Minting = ({
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 {
@ -108,7 +108,7 @@ export const Minting = ({
}); });
} }
} catch (error) { } catch (error) {
// error console.log(error);
} }
}; };

View File

@ -155,6 +155,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

@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useState } from "react"; import React, { useCallback, useEffect, useMemo, useState } from "react";
import { DrawerUserLookup } from "../Drawer/DrawerUserLookup"; import { DrawerUserLookup } from "../Drawer/DrawerUserLookup";
import { import {
Avatar, Avatar,
@ -16,6 +16,7 @@ import {
Typography, Typography,
Table, Table,
CircularProgress, CircularProgress,
Autocomplete,
} from "@mui/material"; } from "@mui/material";
import { getAddressInfo, getNameOrAddress } from "../../background"; import { getAddressInfo, getNameOrAddress } from "../../background";
import { getBaseApiReact } from "../../App"; import { getBaseApiReact } from "../../App";
@ -26,6 +27,8 @@ import { formatTimestamp } from "../../utils/time";
import CloseFullscreenIcon from '@mui/icons-material/CloseFullscreen'; import CloseFullscreenIcon from '@mui/icons-material/CloseFullscreen';
import SearchIcon from '@mui/icons-material/Search'; import SearchIcon from '@mui/icons-material/Search';
import { executeEvent, subscribeToEvent, unsubscribeFromEvent } from "../../utils/events"; import { executeEvent, subscribeToEvent, unsubscribeFromEvent } from "../../utils/events";
import { useNameSearch } from "../../hooks/useNameSearch";
import { validateAddress } from "../../utils/validateAddress";
function formatAddress(str) { function formatAddress(str) {
if (str.length <= 12) return str; if (str.length <= 12) return str;
@ -38,6 +41,13 @@ function formatAddress(str) {
export const UserLookup = ({ isOpenDrawerLookup, setIsOpenDrawerLookup }) => { export const UserLookup = ({ isOpenDrawerLookup, setIsOpenDrawerLookup }) => {
const [nameOrAddress, setNameOrAddress] = useState(""); const [nameOrAddress, setNameOrAddress] = useState("");
const [inputValue, setInputValue] = useState('');
const { results, isLoading } = useNameSearch(inputValue);
const options = useMemo(() => {
const isAddress = validateAddress(inputValue);
if (isAddress) return [inputValue];
return results?.map((item) => item.name);
}, [results, inputValue]);
const [errorMessage, setErrorMessage] = useState(""); const [errorMessage, setErrorMessage] = useState("");
const [addressInfo, setAddressInfo] = useState(null); const [addressInfo, setAddressInfo] = useState(null);
const [isLoadingUser, setIsLoadingUser] = useState(false); const [isLoadingUser, setIsLoadingUser] = useState(false);
@ -58,7 +68,10 @@ export const UserLookup = ({ isOpenDrawerLookup, setIsOpenDrawerLookup }) => {
if (!addressInfoRes?.publicKey) { if (!addressInfoRes?.publicKey) {
throw new Error("Address does not exist on blockchain"); throw new Error("Address does not exist on blockchain");
} }
const name = await getNameInfo(owner); const isAddress = validateAddress(messageAddressOrName);
const name = !isAddress
? messageAddressOrName
: await getNameInfo(owner);
const balanceRes = await fetch( const balanceRes = await fetch(
`${getBaseApiReact()}/addresses/balance/${owner}` `${getBaseApiReact()}/addresses/balance/${owner}`
); );
@ -106,6 +119,7 @@ export const UserLookup = ({ isOpenDrawerLookup, setIsOpenDrawerLookup }) => {
setIsOpenDrawerLookup(false) setIsOpenDrawerLookup(false)
setNameOrAddress('') setNameOrAddress('')
setErrorMessage('') setErrorMessage('')
setInputValue('');
setPayments([]) setPayments([])
setIsLoadingUser(false) setIsLoadingUser(false)
setIsLoadingPayments(false) setIsLoadingPayments(false)
@ -134,27 +148,66 @@ export const UserLookup = ({ isOpenDrawerLookup, setIsOpenDrawerLookup }) => {
flexShrink: 0, flexShrink: 0,
}} }}
> >
<TextField <Autocomplete
autoFocus
value={nameOrAddress} value={nameOrAddress}
onChange={(e) => setNameOrAddress(e.target.value)} onChange={(event: any, newValue: string | null) => {
size="small" if (!newValue) {
placeholder="Address or Name" setNameOrAddress('');
autoComplete="off" return;
onKeyDown={(e) => {
if (e.key === "Enter" && nameOrAddress) {
lookupFunc();
} }
setNameOrAddress(newValue);
lookupFunc(newValue);
}} }}
inputValue={inputValue}
onInputChange={(event, newInputValue) => {
setInputValue(newInputValue);
}}
id="controllable-states-demo"
loading={isLoading}
options={options}
sx={{ width: 300 }}
size="small"
renderInput={(params) => (
<TextField
autoFocus
autoComplete="off"
{...params}
label="Address or Name"
onKeyDown={(e) => {
if (e.key === 'Enter' && inputValue) {
lookupFunc(inputValue);
}
}}
sx={{
'& .MuiOutlinedInput-root': {
'& fieldset': {
borderColor: 'white',
},
'&:hover fieldset': {
borderColor: 'white',
},
'&.Mui-focused fieldset': {
borderColor: 'white',
},
'& input': {
color: 'white',
},
},
'& .MuiInputLabel-root': {
color: 'white',
},
'& .MuiInputLabel-root.Mui-focused': {
color: 'white',
},
'& .MuiAutocomplete-endAdornment svg': {
color: 'white',
},
}}
/>
)}
/> />
<ButtonBase onClick={()=> {
lookupFunc();
}} >
<SearchIcon sx={{
color: 'white',
marginRight: '20px'
}} />
</ButtonBase>
<ButtonBase sx={{ <ButtonBase sx={{
marginLeft: 'auto', marginLeft: 'auto',

View File

@ -167,12 +167,9 @@ useEffect(()=> {
onClick={async () => { onClick={async () => {
try { try {
setIsLoading(true) setIsLoading(true)
if(isAlreadyBlocked === true){ executeEvent("blockUserFromOutside", {
await removeBlockFromList(address, name) user: address
} else if(isAlreadyBlocked === false) { })
await addToBlockList(address, name)
}
executeEvent('updateChatMessagesWithBlocks', true)
} catch (error) { } catch (error) {
console.error(error) console.error(error)
} finally { } finally {

View File

@ -173,3 +173,6 @@ const STATIC_BCRYPT_SALT = `$${BCRYPT_VERSION}$${BCRYPT_ROUNDS}$IxVE941tXVUD4cW0
const KDF_THREADS = 16 const KDF_THREADS = 16
export { TX_TYPES, ERROR_CODES, QORT_DECIMALS, PROXY_URL, STATIC_SALT, ADDRESS_VERSION, KDF_THREADS, STATIC_BCRYPT_SALT, CHAT_REFERENCE_FEATURE_TRIGGER_TIMESTAMP, DYNAMIC_FEE_TIMESTAMP } export { TX_TYPES, ERROR_CODES, QORT_DECIMALS, PROXY_URL, STATIC_SALT, ADDRESS_VERSION, KDF_THREADS, STATIC_BCRYPT_SALT, CHAT_REFERENCE_FEATURE_TRIGGER_TIMESTAMP, DYNAMIC_FEE_TIMESTAMP }
export const MAX_SIZE_PUBLIC_NODE = 500 * 1024 * 1024; // 500mb
export const MAX_SIZE_PUBLISH = 2000 * 1024 * 1024; // 2GB

View File

@ -0,0 +1,55 @@
import { useCallback, useEffect, useState } from 'react';
import { getBaseApiReact } from '../App';
interface NameListItem {
name: string;
address: string;
}
export const useNameSearch = (value: string, limit = 20) => {
const [nameList, setNameList] = useState<NameListItem[]>([]);
const [isLoading, setIsLoading] = useState(false);
const checkIfNameExisits = useCallback(
async (name: string, listLimit: number) => {
try {
if (!name) {
setNameList([]);
return;
}
const res = await fetch(
`${getBaseApiReact()}/names/search?query=${name}&prefix=true&limit=${listLimit}`
);
const data = await res.json();
setNameList(
data?.map((item: any) => {
return {
name: item.name,
address: item.owner,
};
})
);
} catch (error) {
console.error(error);
} finally {
setIsLoading(false);
}
},
[]
);
// Debounce logic
useEffect(() => {
setIsLoading(true);
const handler = setTimeout(() => {
checkIfNameExisits(value, limit);
}, 500);
// Cleanup timeout if searchValue changes before the timeout completes
return () => {
clearTimeout(handler);
};
}, [value, limit, checkIfNameExisits]);
return {
isLoading,
results: nameList,
};
};

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

@ -24,7 +24,7 @@ window.addEventListener("message", (event) => {
} }
}); });
export const sendMessageBackground = (action, data = {}, timeout = 180000, isExtension, appInfo, skipAuth) => { export const sendMessageBackground = (action, data = {}, timeout = 600000, isExtension, appInfo, skipAuth) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const requestId = generateRequestId(); // Unique ID for each request const requestId = generateRequestId(); // Unique ID for each request
callbackMap.set(requestId, { resolve, reject }); // Store both resolve and reject callbacks callbackMap.set(requestId, { resolve, reject }); // Store both resolve and reject callbacks

View File

@ -1,265 +1,474 @@
// @ts-nocheck // @ts-nocheck
import { Buffer } from "buffer" import { Buffer } from 'buffer';
import Base58 from "../../deps/Base58" import Base58 from '../../deps/Base58';
import nacl from "../../deps/nacl-fast" import nacl from '../../deps/nacl-fast';
import utils from "../../utils/utils" import utils from '../../utils/utils';
import { createEndpoint, getBaseApi } from "../../background"; import { createEndpoint, getBaseApi } from '../../background';
import { getData } from "../../utils/chromeStorage"; import { getData } from '../../utils/chromeStorage';
import { executeEvent } from '../../utils/events';
export async function reusableGet(endpoint){ export async function reusableGet(endpoint) {
const validApi = await getBaseApi(); const validApi = await getBaseApi();
const response = await fetch(validApi + endpoint); const response = await fetch(validApi + endpoint);
const data = await response.json(); const data = await response.json();
return data return data;
} }
async function reusablePost(endpoint, _body){ async function reusablePost(endpoint, _body) {
// const validApi = await findUsableApi(); // const validApi = await findUsableApi();
const url = await createEndpoint(endpoint) const url = await createEndpoint(endpoint);
const response = await fetch(url, { const response = await fetch(url, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json',
}, },
body: _body body: _body,
}); });
let data if (!response.ok) {
const errorText = await response.text();
throw new Error(errorText);
}
let data;
try { try {
data = await response.clone().json() data = await response.clone().json();
} catch (e) { } catch (e) {
data = await response.text() data = await response.text();
} }
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, 25_000));
}
} }
}
async function resuablePostRetry(
endpoint,
body,
maxRetries = 3,
appInfo,
resourceInfo
) {
let attempt = 0;
while (attempt < maxRetries) {
try {
const response = await reusablePost(endpoint, body);
return response;
} catch (err) {
attempt++;
if (attempt >= maxRetries) {
throw new Error(
err instanceof Error
? err?.message || `Failed to make request`
: `Failed to make request`
);
}
if (appInfo?.tabId && resourceInfo) {
executeEvent('receiveChunks', {
tabId: appInfo.tabId,
publishLocation: {
name: resourceInfo?.name,
identifier: resourceInfo?.identifier,
service: resourceInfo?.service,
},
retry: true,
});
}
// Wait 10 seconds before next retry
await new Promise((res) => setTimeout(res, 25_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) {
return res return res;
} else { } else {
throw new Error("Wallet not authenticated"); throw new Error('Wallet not authenticated');
}
} }
}
export const publishData = async ({ export const publishData = async ({
registeredName, registeredName,
file, data,
service, service,
identifier, identifier,
uploadType, uploadType,
isBase64, filename,
filename, withFee,
withFee, title,
title, description,
description, category,
category, tag1,
tag1, tag2,
tag2, tag3,
tag3, tag4,
tag4, tag5,
tag5, feeAmount,
feeAmount appInfo
}: any) => { }: any) => {
const validateName = async (receiverName: string) => {
const validateName = async (receiverName: string) => { return await reusableGet(`/names/${receiverName}`);
return await reusableGet(`/names/${receiverName}`) };
}
const convertBytesForSigning = async (transactionBytesBase58: string) => { const convertBytesForSigning = async (transactionBytesBase58: string) => {
return await reusablePost('/transactions/convert', transactionBytesBase58) return await resuablePostRetry(
} '/transactions/convert',
transactionBytesBase58,
3,
appInfo,
{ identifier, name: registeredName, service }
);
};
const getArbitraryFee = async () => { const getArbitraryFee = async () => {
const timestamp = Date.now() const timestamp = Date.now();
let fee = await reusableGet(`/transactions/unitfee?txType=ARBITRARY&timestamp=${timestamp}`) let fee = await reusableGet(
`/transactions/unitfee?txType=ARBITRARY&timestamp=${timestamp}`
);
return { return {
timestamp, timestamp,
fee: Number(fee), fee: Number(fee),
feeToShow: (Number(fee) / 1e8).toFixed(8) feeToShow: (Number(fee) / 1e8).toFixed(8),
} };
} };
const signArbitraryWithFee = (arbitraryBytesBase58, arbitraryBytesForSigningBase58, keyPair) => { const signArbitraryWithFee = (
if (!arbitraryBytesBase58) { arbitraryBytesBase58,
throw new Error('ArbitraryBytesBase58 not defined') arbitraryBytesForSigningBase58,
} keyPair
) => {
if (!keyPair) { if (!arbitraryBytesBase58) {
throw new Error('keyPair not defined') throw new Error('ArbitraryBytesBase58 not defined');
}
const arbitraryBytes = Base58.decode(arbitraryBytesBase58)
const _arbitraryBytesBuffer = Object.keys(arbitraryBytes).map(function (key) { return arbitraryBytes[key]; })
const arbitraryBytesBuffer = new Uint8Array(_arbitraryBytesBuffer)
const arbitraryBytesForSigning = Base58.decode(arbitraryBytesForSigningBase58)
const _arbitraryBytesForSigningBuffer = Object.keys(arbitraryBytesForSigning).map(function (key) { return arbitraryBytesForSigning[key]; })
const arbitraryBytesForSigningBuffer = new Uint8Array(_arbitraryBytesForSigningBuffer)
const signature = nacl.sign.detached(arbitraryBytesForSigningBuffer, keyPair.privateKey)
return utils.appendBuffer(arbitraryBytesBuffer, signature)
} }
const processTransactionVersion2 = async (bytes) => { if (!keyPair) {
throw new Error('keyPair not defined');
}
return await reusablePost('/transactions/process?apiVersion=2', Base58.encode(bytes)) const arbitraryBytes = Base58.decode(arbitraryBytesBase58);
} const _arbitraryBytesBuffer = Object.keys(arbitraryBytes).map(
function (key) {
return arbitraryBytes[key];
}
);
const arbitraryBytesBuffer = new Uint8Array(_arbitraryBytesBuffer);
const arbitraryBytesForSigning = Base58.decode(
arbitraryBytesForSigningBase58
);
const _arbitraryBytesForSigningBuffer = Object.keys(
arbitraryBytesForSigning
).map(function (key) {
return arbitraryBytesForSigning[key];
});
const arbitraryBytesForSigningBuffer = new Uint8Array(
_arbitraryBytesForSigningBuffer
);
const signature = nacl.sign.detached(
arbitraryBytesForSigningBuffer,
keyPair.privateKey
);
const signAndProcessWithFee = async (transactionBytesBase58: string) => { return utils.appendBuffer(arbitraryBytesBuffer, signature);
let convertedBytesBase58 = await convertBytesForSigning( };
transactionBytesBase58
)
if (convertedBytesBase58.error) { const processTransactionVersion2 = async (bytes) => {
throw new Error('Error when signing') return await resuablePostRetry(
} '/transactions/process?apiVersion=2',
Base58.encode(bytes),
3,
appInfo,
{ identifier, name: registeredName, service }
);
};
const signAndProcessWithFee = async (transactionBytesBase58: string) => {
let convertedBytesBase58 = await convertBytesForSigning(
transactionBytesBase58
);
const resKeyPair = await getKeyPair() if (convertedBytesBase58.error) {
const parsedData = resKeyPair throw new Error('Error when signing');
const uint8PrivateKey = Base58.decode(parsedData.privateKey); }
const uint8PublicKey = Base58.decode(parsedData.publicKey);
const keyPair = {
privateKey: uint8PrivateKey,
publicKey: uint8PublicKey
};
let signedArbitraryBytes = signArbitraryWithFee(transactionBytesBase58, convertedBytesBase58, keyPair) const resKeyPair = await getKeyPair();
const response = await processTransactionVersion2(signedArbitraryBytes) const parsedData = resKeyPair;
const uint8PrivateKey = Base58.decode(parsedData.privateKey);
const uint8PublicKey = Base58.decode(parsedData.publicKey);
const keyPair = {
privateKey: uint8PrivateKey,
publicKey: uint8PublicKey,
};
let myResponse = { error: '' } let signedArbitraryBytes = signArbitraryWithFee(
transactionBytesBase58,
convertedBytesBase58,
keyPair
);
const response = await processTransactionVersion2(signedArbitraryBytes);
if (response === false) { let myResponse = { error: '' };
throw new Error('Error when signing')
} else {
myResponse = response
}
return myResponse if (response === false) {
} throw new Error('Error when signing');
} else {
myResponse = response;
}
if (appInfo?.tabId) {
executeEvent('receiveChunks', {
tabId: appInfo.tabId,
publishLocation: {
name: registeredName,
identifier,
service,
},
processed: true,
});
}
return myResponse;
};
const validate = async () => { const validate = async () => {
let validNameRes = await validateName(registeredName) let validNameRes = await validateName(registeredName);
if (validNameRes.error) { if (validNameRes.error) {
throw new Error('Name not found') throw new Error('Name not found');
} }
let fee = null let fee = null;
if (withFee && feeAmount) { if (withFee && feeAmount) {
fee = feeAmount fee = feeAmount;
} else if (withFee) { } else if (withFee) {
const res = await getArbitraryFee() const res = await getArbitraryFee();
if (res.fee) { if (res.fee) {
fee = res.fee fee = res.fee;
} else { } else {
throw new Error('unable to get fee') throw new Error('unable to get fee');
} }
} }
let transactionBytes = await uploadData(registeredName, file, fee)
if (!transactionBytes || transactionBytes.error) {
throw new Error(transactionBytes?.message || 'Error when uploading')
} else if (transactionBytes.includes('Error 500 Internal Server Error')) {
throw new Error('Error when uploading')
}
let signAndProcessRes let transactionBytes = await uploadData(registeredName, data, fee);
if (!transactionBytes || transactionBytes.error) {
throw new Error(transactionBytes?.message || 'Error when uploading');
} else if (transactionBytes.includes('Error 500 Internal Server Error')) {
throw new Error('Error when uploading');
}
if (withFee) { let signAndProcessRes;
signAndProcessRes = await signAndProcessWithFee(transactionBytes)
}
if (signAndProcessRes?.error) { if (withFee) {
throw new Error('Error when signing') signAndProcessRes = await signAndProcessWithFee(transactionBytes);
} }
return signAndProcessRes if (signAndProcessRes?.error) {
} throw new Error('Error when signing');
}
const uploadData = async (registeredName: string, file:any, fee: number) => { return signAndProcessRes;
};
let postBody = '' const uploadData = async (registeredName: string, data: any, fee: number) => {
let urlSuffix = '' let postBody = '';
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 = '/base64';
urlSuffix = '/zip' }
}
// If we're sending file data, use the /base64 version of the POST /arbitrary/* API if (uploadType === 'base64') {
else if (uploadType === 'file') { postBody = data;
urlSuffix = '/base64' }
} } else {
throw new Error('No data provided');
}
// Base64 encode the file to work around compatibility issues between javascript and java byte arrays let uploadDataUrl = `/arbitrary/${service}/${registeredName}`;
if (isBase64) { let paramQueries = '';
postBody = file if (identifier?.trim().length > 0) {
} uploadDataUrl = `/arbitrary/${service}/${registeredName}/${identifier}`;
}
if (!isBase64) { paramQueries = paramQueries + `?fee=${fee}`;
let fileBuffer = new Uint8Array(await file.arrayBuffer())
postBody = Buffer.from(fileBuffer).toString("base64")
}
} if (filename != null && filename != 'undefined') {
paramQueries = paramQueries + '&filename=' + encodeURIComponent(filename);
let uploadDataUrl = `/arbitrary/${service}/${registeredName}${urlSuffix}` }
if (identifier?.trim().length > 0) {
uploadDataUrl = `/arbitrary/${service}/${registeredName}/${identifier}${urlSuffix}`
}
uploadDataUrl = uploadDataUrl + `?fee=${fee}`
if (filename != null && filename != 'undefined') { if (title != null && title != 'undefined') {
uploadDataUrl = uploadDataUrl + '&filename=' + encodeURIComponent(filename) paramQueries = paramQueries + '&title=' + encodeURIComponent(title);
} }
if (title != null && title != 'undefined') { if (description != null && description != 'undefined') {
uploadDataUrl = uploadDataUrl + '&title=' + encodeURIComponent(title) paramQueries =
} paramQueries + '&description=' + encodeURIComponent(description);
}
if (description != null && description != 'undefined') { if (category != null && category != 'undefined') {
uploadDataUrl = uploadDataUrl + '&description=' + encodeURIComponent(description) paramQueries = paramQueries + '&category=' + encodeURIComponent(category);
} }
if (category != null && category != 'undefined') { if (tag1 != null && tag1 != 'undefined') {
uploadDataUrl = uploadDataUrl + '&category=' + encodeURIComponent(category) paramQueries = paramQueries + '&tags=' + encodeURIComponent(tag1);
} }
if (tag1 != null && tag1 != 'undefined') { if (tag2 != null && tag2 != 'undefined') {
uploadDataUrl = uploadDataUrl + '&tags=' + encodeURIComponent(tag1) paramQueries = paramQueries + '&tags=' + encodeURIComponent(tag2);
} }
if (tag2 != null && tag2 != 'undefined') { if (tag3 != null && tag3 != 'undefined') {
uploadDataUrl = uploadDataUrl + '&tags=' + encodeURIComponent(tag2) paramQueries = paramQueries + '&tags=' + encodeURIComponent(tag3);
} }
if (tag3 != null && tag3 != 'undefined') { if (tag4 != null && tag4 != 'undefined') {
uploadDataUrl = uploadDataUrl + '&tags=' + encodeURIComponent(tag3) paramQueries = paramQueries + '&tags=' + encodeURIComponent(tag4);
} }
if (tag4 != null && tag4 != 'undefined') { if (tag5 != null && tag5 != 'undefined') {
uploadDataUrl = uploadDataUrl + '&tags=' + encodeURIComponent(tag4) paramQueries = paramQueries + '&tags=' + encodeURIComponent(tag5);
} }
if (uploadType === 'zip') {
paramQueries = paramQueries + '&isZip=' + true;
}
if (tag5 != null && tag5 != 'undefined') { if (uploadType === 'base64') {
uploadDataUrl = uploadDataUrl + '&tags=' + encodeURIComponent(tag5) if (urlSuffix) {
} uploadDataUrl = uploadDataUrl + urlSuffix;
}
uploadDataUrl = uploadDataUrl + paramQueries;
if (appInfo?.tabId) {
executeEvent('receiveChunks', {
tabId: appInfo.tabId,
publishLocation: {
name: registeredName,
identifier,
service,
},
chunksSubmitted: 1,
totalChunks: 1,
processed: false,
filename: filename || title || `${service}-${identifier || ''}`,
});
}
return await resuablePostRetry(uploadDataUrl, postBody, 3, appInfo, {
identifier,
name: registeredName,
service,
});
}
return await reusablePost(uploadDataUrl, postBody) const file = data;
const urlCheck = `/arbitrary/check/tmp?totalSize=${file.size}`;
}
try { const checkEndpoint = await createEndpoint(urlCheck);
return await validate() const checkRes = await fetch(checkEndpoint);
} catch (error: any) { if (!checkRes.ok) {
throw new Error(error?.message) 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);
if (appInfo?.tabId) {
executeEvent('receiveChunks', {
tabId: appInfo.tabId,
publishLocation: {
name: registeredName,
identifier,
service,
},
chunksSubmitted: 0,
totalChunks,
processed: false,
filename:
file?.name || filename || title || `${service}-${identifier || ''}`,
});
}
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);
if (appInfo?.tabId) {
executeEvent('receiveChunks', {
tabId: appInfo.tabId,
publishLocation: {
name: registeredName,
identifier,
service,
},
chunksSubmitted: index + 1,
totalChunks,
});
}
}
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 {
return await validate();
} catch (error: any) {
throw new Error(error?.message);
}
};

View File

@ -1,9 +1,10 @@
import { gateways, getApiKeyFromStorage } from "./background"; import { gateways, getApiKeyFromStorage, getNameInfoForOthers } from "./background";
import { listOfAllQortalRequests } from "./components/Apps/useQortalMessageListener"; import { listOfAllQortalRequests } from "./components/Apps/useQortalMessageListener";
import { addForeignServer, addGroupAdminRequest, addListItems, adminAction, banFromGroupRequest, cancelGroupBanRequest, cancelGroupInviteRequest, cancelSellOrder, createAndCopyEmbedLink, createBuyOrder, createGroupRequest, createPoll, decryptAESGCMRequest, decryptData, decryptDataWithSharingKey, decryptQortalGroupData, deleteHostedData, deleteListItems, deployAt, encryptData, encryptDataWithSharingKey, encryptQortalGroupData, getArrrSyncStatus, getCrossChainServerInfo, getDaySummary, getForeignFee, getHostedData, getListItems, getNodeInfo, getNodeStatus, getServerConnectionHistory, getTxActivitySummary, getUserAccount, getUserWallet, getUserWalletInfo, getUserWalletTransactions, getWalletBalance, inviteToGroupRequest, joinGroup, kickFromGroupRequest, leaveGroupRequest, openNewTab, publishMultipleQDNResources, publishQDNResource, registerNameRequest, removeForeignServer, removeGroupAdminRequest, saveFile, sendChatMessage, sendCoin, setCurrentForeignServer, signTransaction, updateForeignFee, updateNameRequest, voteOnPoll } from "./qortalRequests/get"; import { addForeignServer, addGroupAdminRequest, addListItems, adminAction, banFromGroupRequest, buyNameRequest, cancelGroupBanRequest, cancelGroupInviteRequest, cancelSellNameRequest, cancelSellOrder, createAndCopyEmbedLink, createBuyOrder, createGroupRequest, createPoll, createSellOrder, decryptAESGCMRequest, decryptData, decryptDataWithSharingKey, decryptQortalGroupData, deleteHostedData, deleteListItems, deployAt, encryptData, encryptDataWithSharingKey, encryptQortalGroupData, getArrrSyncStatus, getCrossChainServerInfo, getDaySummary, getForeignFee, getHostedData, getListItems, getNodeInfo, getNodeStatus, getServerConnectionHistory, getTxActivitySummary, getUserAccount, getUserWallet, getUserWalletInfo, getUserWalletTransactions, getWalletBalance, inviteToGroupRequest, joinGroup, kickFromGroupRequest, leaveGroupRequest, multiPaymentWithPrivateData, openNewTab, publishMultipleQDNResources, publishQDNResource, registerNameRequest, removeForeignServer, removeGroupAdminRequest, saveFile, sellNameRequest, sendChatMessage, sendCoin, setCurrentForeignServer, signForeignFees, signTransaction, transferAssetRequest, updateForeignFee, updateGroupRequest, updateNameRequest, voteOnPoll } from "./qortalRequests/get";
import { getData, storeData } from "./utils/chromeStorage"; import { getData, storeData } from "./utils/chromeStorage";
import { executeEvent } from "./utils/events"; import { executeEvent } from "./utils/events";
import { ScreenOrientation } from '@capacitor/screen-orientation';
function getLocalStorage(key) { function getLocalStorage(key) {
@ -200,7 +201,7 @@ export const isRunningGateway = async ()=> {
case "PUBLISH_QDN_RESOURCE": { case "PUBLISH_QDN_RESOURCE": {
try { try {
const res = await publishQDNResource(request.payload, event.source, isFromExtension); const res = await publishQDNResource(request.payload, event.source, isFromExtension, appInfo);
event.source.postMessage({ event.source.postMessage({
requestId: request.requestId, requestId: request.requestId,
action: request.action, action: request.action,
@ -220,7 +221,7 @@ export const isRunningGateway = async ()=> {
case "PUBLISH_MULTIPLE_QDN_RESOURCES": { case "PUBLISH_MULTIPLE_QDN_RESOURCES": {
try { try {
const res = await publishMultipleQDNResources(request.payload, event.source, isFromExtension); const res = await publishMultipleQDNResources(request.payload, event.source, isFromExtension, appInfo);
event.source.postMessage({ event.source.postMessage({
requestId: request.requestId, requestId: request.requestId,
action: request.action, action: request.action,
@ -462,7 +463,7 @@ export const isRunningGateway = async ()=> {
case "UPDATE_FOREIGN_FEE": { case "UPDATE_FOREIGN_FEE": {
try { try {
const res = await updateForeignFee(request.payload); const res = await updateForeignFee(request.payload, isFromExtension);
event.source.postMessage({ event.source.postMessage({
requestId: request.requestId, requestId: request.requestId,
action: request.action, action: request.action,
@ -502,7 +503,7 @@ export const isRunningGateway = async ()=> {
case "SET_CURRENT_FOREIGN_SERVER": { case "SET_CURRENT_FOREIGN_SERVER": {
try { try {
const res = await setCurrentForeignServer(request.payload); const res = await setCurrentForeignServer(request.payload, isFromExtension);
event.source.postMessage({ event.source.postMessage({
requestId: request.requestId, requestId: request.requestId,
action: request.action, action: request.action,
@ -522,7 +523,7 @@ export const isRunningGateway = async ()=> {
case "ADD_FOREIGN_SERVER": { case "ADD_FOREIGN_SERVER": {
try { try {
const res = await addForeignServer(request.payload); const res = await addForeignServer(request.payload, isFromExtension);
event.source.postMessage({ event.source.postMessage({
requestId: request.requestId, requestId: request.requestId,
action: request.action, action: request.action,
@ -542,7 +543,7 @@ export const isRunningGateway = async ()=> {
case "REMOVE_FOREIGN_SERVER": { case "REMOVE_FOREIGN_SERVER": {
try { try {
const res = await removeForeignServer(request.payload); const res = await removeForeignServer(request.payload, isFromExtension);
event.source.postMessage({ event.source.postMessage({
requestId: request.requestId, requestId: request.requestId,
action: request.action, action: request.action,
@ -620,6 +621,32 @@ export const isRunningGateway = async ()=> {
break; break;
} }
case 'CREATE_TRADE_SELL_ORDER': {
try {
const res = await createSellOrder(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 "CANCEL_TRADE_SELL_ORDER": { case "CANCEL_TRADE_SELL_ORDER": {
try { try {
const res = await cancelSellOrder(request.payload, isFromExtension); const res = await cancelSellOrder(request.payload, isFromExtension);
@ -1206,6 +1233,206 @@ export const isRunningGateway = async ()=> {
} }
break; break;
} }
case "UPDATE_GROUP" : {
try {
const res = await updateGroupRequest(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 "BUY_NAME": {
try {
const res = await buyNameRequest(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 "SELL_NAME": {
try {
const res = await sellNameRequest(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 "CANCEL_SELL_NAME": {
try {
const res = await cancelSellNameRequest(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 "MULTI_ASSET_PAYMENT_WITH_PRIVATE_DATA" : {
try {
const res = await multiPaymentWithPrivateData(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 "TRANSFER_ASSET" : {
try {
const res = await transferAssetRequest(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 'SIGN_FOREIGN_FEES': {
try {
const res = await signForeignFees(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 'GET_PRIMARY_NAME': {
try {
const res = await getNameInfoForOthers(request.payload?.address);
const resData = res ? res : "";
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;
}
case 'SCREEN_ORIENTATION': {
try {
const mode = request.payload?.mode
if(mode === 'unlock'){
await ScreenOrientation.unlock();
} else {
await ScreenOrientation.lock({ orientation: mode });
}
event.source.postMessage(
{
requestId: request.requestId,
action: request.action,
payload: true,
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;
} }

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,45 @@
// @ts-nocheck
import { QORT_DECIMALS } from "../constants/constants"
import TransactionBase from "./TransactionBase"
export default class BuyNameTransacion extends TransactionBase {
constructor() {
super()
this.type = 7
}
set fee(fee) {
this._fee = fee * QORT_DECIMALS
this._feeBytes = this.constructor.utils.int64ToBytes(this._fee)
}
set name(name) {
this.nameText = name
this._nameBytes = this.constructor.utils.stringtoUTF8Array(name)
this._nameLength = this.constructor.utils.int32ToBytes(this._nameBytes.length)
}
set sellPrice(sellPrice) {
this._sellPrice = sellPrice * QORT_DECIMALS
this._sellPriceBytes = this.constructor.utils.int64ToBytes(this._sellPrice)
}
set recipient(recipient) {
this._recipient = recipient instanceof Uint8Array ? recipient : this.constructor.Base58.decode(recipient)
this.theRecipient = recipient
}
get params() {
const params = super.params
params.push(
this._nameLength,
this._nameBytes,
this._sellPriceBytes,
this._recipient,
this._feeBytes
)
return params
}
}

View File

@ -0,0 +1,33 @@
// @ts-nocheck
import { QORT_DECIMALS } from "../constants/constants"
import TransactionBase from "./TransactionBase"
export default class CancelSellNameTransacion extends TransactionBase {
constructor() {
super()
this.type = 6
}
set fee(fee) {
this._fee = fee * QORT_DECIMALS
this._feeBytes = this.constructor.utils.int64ToBytes(this._fee)
}
set name(name) {
this.nameText = name
this._nameBytes = this.constructor.utils.stringtoUTF8Array(name)
this._nameLength = this.constructor.utils.int32ToBytes(this._nameBytes.length)
}
get params() {
const params = super.params
params.push(
this._nameLength,
this._nameBytes,
this._feeBytes
)
return params
}
}

View File

@ -0,0 +1,40 @@
// @ts-nocheck
import { QORT_DECIMALS } from "../constants/constants"
import TransactionBase from "./TransactionBase"
export default class SellNameTransacion extends TransactionBase {
constructor() {
super()
this.type = 5
}
set fee(fee) {
this._fee = fee * QORT_DECIMALS
this._feeBytes = this.constructor.utils.int64ToBytes(this._fee)
}
set name(name) {
this.nameText = name
this._nameBytes = this.constructor.utils.stringtoUTF8Array(name)
this._nameLength = this.constructor.utils.int32ToBytes(this._nameBytes.length)
}
set sellPrice(sellPrice) {
this.showSellPrice = sellPrice
this._sellPrice = sellPrice * QORT_DECIMALS
this._sellPriceBytes = this.constructor.utils.int64ToBytes(this._sellPrice)
}
get params() {
const params = super.params
params.push(
this._nameLength,
this._nameBytes,
this._sellPriceBytes,
this._feeBytes
)
return params
}
}

View File

@ -0,0 +1,35 @@
// @ts-nocheck
import { QORT_DECIMALS } from '../constants/constants'
import TransactionBase from './TransactionBase'
export default class TransferAssetTransaction extends TransactionBase {
constructor() {
super()
this.type = 12
}
set recipient(recipient) {
this._recipient = recipient instanceof Uint8Array ? recipient : this.constructor.Base58.decode(recipient)
}
set amount(amount) {
this._amount = Math.round(amount * QORT_DECIMALS)
this._amountBytes = this.constructor.utils.int64ToBytes(this._amount)
}
set assetId(assetId) {
this._assetId = this.constructor.utils.int64ToBytes(assetId)
}
get params() {
const params = super.params
params.push(
this._recipient,
this._assetId,
this._amountBytes,
this._feeBytes
)
return params
}
}

View File

@ -0,0 +1,62 @@
// @ts-nocheck
import { QORT_DECIMALS } from "../constants/constants";
import TransactionBase from "./TransactionBase";
export default class UpdateGroupTransaction extends TransactionBase {
constructor() {
super()
this.type = 23
}
set fee(fee) {
this._fee = fee * QORT_DECIMALS
this._feeBytes = this.constructor.utils.int64ToBytes(this._fee)
}
set newOwner(newOwner) {
this._newOwner = newOwner instanceof Uint8Array ? newOwner : this.constructor.Base58.decode(newOwner)
}
set newIsOpen(newIsOpen) {
this._rGroupType = new Uint8Array(1)
this._rGroupType[0] = newIsOpen
}
set newDescription(newDescription) {
this._rGroupDescBytes = this.constructor.utils.stringtoUTF8Array(newDescription.toLocaleLowerCase())
this._rGroupDescLength = this.constructor.utils.int32ToBytes(this._rGroupDescBytes.length)
}
set newApprovalThreshold(newApprovalThreshold) {
this._rGroupApprovalThreshold = new Uint8Array(1)
this._rGroupApprovalThreshold[0] = newApprovalThreshold;
}
set newMinimumBlockDelay(newMinimumBlockDelay) {
this._rGroupMinimumBlockDelayBytes = this.constructor.utils.int32ToBytes(newMinimumBlockDelay)
}
set newMaximumBlockDelay(newMaximumBlockDelay) {
this._rGroupMaximumBlockDelayBytes = this.constructor.utils.int32ToBytes(newMaximumBlockDelay)
}
set _groupId(_groupId){
this._groupBytes = this.constructor.utils.int32ToBytes(_groupId)
}
get params() {
const params = super.params
params.push(
this._groupBytes,
this._newOwner,
this._rGroupDescLength,
this._rGroupDescBytes,
this._rGroupType,
this._rGroupApprovalThreshold,
this._rGroupMinimumBlockDelayBytes,
this._rGroupMaximumBlockDelayBytes,
this._feeBytes
)
return params
}
}

View File

@ -20,17 +20,27 @@ import DeployAtTransaction from './DeployAtTransaction.js'
import RewardShareTransaction from './RewardShareTransaction.js' import RewardShareTransaction from './RewardShareTransaction.js'
import RemoveRewardShareTransaction from './RemoveRewardShareTransaction.js' import RemoveRewardShareTransaction from './RemoveRewardShareTransaction.js'
import UpdateNameTransaction from './UpdateNameTransaction.js' import UpdateNameTransaction from './UpdateNameTransaction.js'
import UpdateGroupTransaction from './UpdateGroupTransaction.js'
import SellNameTransacion from './SellNameTransacion.js'
import CancelSellNameTransacion from './CancelSellNameTransacion.js'
import BuyNameTransacion from './BuyNameTransacion.js'
import TransferAssetTransaction from './TransferAssetTransaction.js'
export const transactionTypes = { export const transactionTypes = {
3: RegisterNameTransaction, 3: RegisterNameTransaction,
4: UpdateNameTransaction, 4: UpdateNameTransaction,
2: PaymentTransaction, 2: PaymentTransaction,
5: SellNameTransacion,
6: CancelSellNameTransacion,
7: BuyNameTransacion,
8: CreatePollTransaction, 8: CreatePollTransaction,
9: VoteOnPollTransaction, 9: VoteOnPollTransaction,
12: TransferAssetTransaction,
16: DeployAtTransaction, 16: DeployAtTransaction,
18: ChatTransaction, 18: ChatTransaction,
181: GroupChatTransaction, 181: GroupChatTransaction,
22: CreateGroupTransaction, 22: CreateGroupTransaction,
23: UpdateGroupTransaction,
24: AddGroupAdminTransaction, 24: AddGroupAdminTransaction,
25: RemoveGroupAdminTransaction, 25: RemoveGroupAdminTransaction,
26: GroupBanTransaction, 26: GroupBanTransaction,

26
src/utils/chat.ts Normal file
View File

@ -0,0 +1,26 @@
export function buildImageEmbedLink(image?: {
name?: string;
identifier?: string;
service?: string;
timestamp?: number;
}): string | null {
if (!image?.name || !image.identifier || !image.service) return null;
const base = `qortal://use-embed/IMAGE?name=${image.name}&identifier=${image.identifier}&service=${image.service}&mimeType=image%2Fpng&timestamp=${image?.timestamp || ''}`;
const isEncrypted = image.identifier.startsWith('grp-q-manager_0');
return isEncrypted ? `${base}&encryptionType=group` : base;
}
export const messageHasImage = (message) => {
return (
Array.isArray(message?.images) &&
message.images[0]?.identifier &&
message.images[0]?.name &&
message.images[0]?.service
);
};
export function isHtmlString(value) {
return typeof value === 'string' && /<[^>]+>/.test(value.trim());
}

View File

@ -13,4 +13,19 @@ export function decodeIfEncoded(input) {
// Return input as-is if not URI-encoded // Return input as-is if not URI-encoded
return input; return input;
} }
export const isValidBase64 = (str: string): boolean => {
if (typeof str !== "string" || str.length % 4 !== 0) return false;
const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;
return base64Regex.test(str);
};
export const isValidBase64WithDecode = (str: string): boolean => {
try {
return isValidBase64(str) && Boolean(atob(str));
} catch {
return false;
}
};

View File

@ -43,14 +43,15 @@ export function formatTimestamp(timestamp: number): string {
// 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);
} }
}); });
} }