mirror of
https://github.com/Qortal/qortal-mobile.git
synced 2025-07-04 20:51:22 +00:00
Compare commits
29 Commits
v0.5.4-pre
...
feature/in
Author | SHA1 | Date | |
---|---|---|---|
e9bb8b465c | |||
12e395424a | |||
93b8282a69 | |||
d2fc982852 | |||
9113267dc2 | |||
fe63d9c0ff | |||
7e34886d15 | |||
b2b7820017 | |||
74d064f735 | |||
1482a8ed60 | |||
d9db4e5c27 | |||
15c1373cb0 | |||
2fbc48d676 | |||
54a1bb636a | |||
a22c48667b | |||
4ad0fb7db3 | |||
cfa4afc506 | |||
44a2675c87 | |||
7be4a08a41 | |||
20c51e0806 | |||
0b9f32fd8c | |||
0cfdd5cbc9 | |||
21b3dd9d02 | |||
93305b8dc4 | |||
8c98fcbcdf | |||
3c00d40093 | |||
3e1e38838d | |||
24f133acf5 | |||
d2c79f52c4 |
@ -7,8 +7,8 @@ android {
|
||||
applicationId "com.github.Qortal.qortalMobile"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 44
|
||||
versionName "0.5.3"
|
||||
versionCode 45
|
||||
versionName "0.5.4"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
|
@ -14,6 +14,7 @@ dependencies {
|
||||
implementation project(':capacitor-filesystem')
|
||||
implementation project(':capacitor-local-notifications')
|
||||
implementation project(':capacitor-preferences')
|
||||
implementation project(':capacitor-screen-orientation')
|
||||
implementation project(':capacitor-splash-screen')
|
||||
implementation project(':capawesome-capacitor-file-picker')
|
||||
implementation project(':evva-capacitor-secure-storage-plugin')
|
||||
|
@ -13,6 +13,7 @@
|
||||
android:usesCleartextTraffic="true">
|
||||
<activity
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
|
||||
android:screenOrientation="unspecified"
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/title_activity_main"
|
||||
android:theme="@style/AppTheme.NoActionBarLaunch"
|
||||
|
@ -4,6 +4,8 @@ import com.getcapacitor.BridgeActivity;
|
||||
import com.github.Qortal.qortalMobile.NativeBcrypt;
|
||||
import com.github.Qortal.qortalMobile.NativePOW;
|
||||
import android.os.Bundle;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebView;
|
||||
|
||||
public class MainActivity extends BridgeActivity {
|
||||
@Override
|
||||
@ -12,6 +14,9 @@ public class MainActivity extends BridgeActivity {
|
||||
registerPlugin(NativePOW.class);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,9 @@ project(':capacitor-local-notifications').projectDir = new File('../node_modules
|
||||
include ':capacitor-preferences'
|
||||
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'
|
||||
project(':capacitor-splash-screen').projectDir = new File('../node_modules/@capacitor/splash-screen/android')
|
||||
|
||||
|
@ -19,7 +19,7 @@ const config: CapacitorConfig = {
|
||||
"splashImmersive": true
|
||||
},
|
||||
CapacitorHttp: {
|
||||
enabled: true,
|
||||
enabled: false,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "0.5.3",
|
||||
"apkUrl": "https://github.com/Qortal/qortal-mobile/releases/download/v0.5.3/qortal.apk"
|
||||
"version": "0.5.4",
|
||||
"apkUrl": "https://github.com/Qortal/qortal-mobile/releases/download/v0.5.4/qortal.apk"
|
||||
}
|
||||
|
14
package-lock.json
generated
14
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "qortal-go",
|
||||
"version": "0.5.2",
|
||||
"version": "0.5.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "qortal-go",
|
||||
"version": "0.5.2",
|
||||
"version": "0.5.4",
|
||||
"dependencies": {
|
||||
"@capacitor/android": "^6.1.2",
|
||||
"@capacitor/app": "^6.0.1",
|
||||
@ -16,6 +16,7 @@
|
||||
"@capacitor/filesystem": "^6.0.1",
|
||||
"@capacitor/local-notifications": "^6.1.0",
|
||||
"@capacitor/preferences": "^6.0.3",
|
||||
"@capacitor/screen-orientation": "^6.0.3",
|
||||
"@capacitor/splash-screen": "^6.0.2",
|
||||
"@capawesome/capacitor-file-picker": "^6.1.0",
|
||||
"@chatscope/chat-ui-kit-react": "^2.0.3",
|
||||
@ -1700,6 +1701,15 @@
|
||||
"@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": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/splash-screen/-/splash-screen-6.0.2.tgz",
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "qortal-go",
|
||||
"private": true,
|
||||
"version": "0.5.3",
|
||||
"version": "0.5.4",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@ -20,6 +20,7 @@
|
||||
"@capacitor/filesystem": "^6.0.1",
|
||||
"@capacitor/local-notifications": "^6.1.0",
|
||||
"@capacitor/preferences": "^6.0.3",
|
||||
"@capacitor/screen-orientation": "^6.0.3",
|
||||
"@capacitor/splash-screen": "^6.0.2",
|
||||
"@capawesome/capacitor-file-picker": "^6.1.0",
|
||||
"@chatscope/chat-ui-kit-react": "^2.0.3",
|
||||
|
179
src/App.tsx
179
src/App.tsx
@ -21,14 +21,18 @@ import {
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
Divider,
|
||||
FormControlLabel,
|
||||
Input,
|
||||
InputLabel,
|
||||
Popover,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { ScreenOrientation } from '@capacitor/screen-orientation';
|
||||
|
||||
import { decryptStoredWallet } from "./utils/decryptWallet";
|
||||
import AccountBalanceWalletIcon from '@mui/icons-material/AccountBalanceWallet';
|
||||
import PriorityHighIcon from '@mui/icons-material/PriorityHigh';
|
||||
|
||||
import { JsonView, allExpanded, darkStyles } from 'react-json-view-lite';
|
||||
import 'react-json-view-lite/dist/index.css';
|
||||
@ -127,6 +131,7 @@ import {
|
||||
isUsingImportExportSettingsAtom,
|
||||
lastEnteredGroupIdAtom,
|
||||
mailsAtom,
|
||||
myGroupsWhereIAmAdminAtom,
|
||||
oldPinnedAppsAtom,
|
||||
qMailLastEnteredTimestampAtom,
|
||||
settingsLocalLastUpdatedAtom,
|
||||
@ -153,6 +158,7 @@ import { BuyQortInformation } from "./components/BuyQortInformation";
|
||||
import { InstallPWA } from "./components/InstallPWA";
|
||||
import { QortPayment } from "./components/QortPayment";
|
||||
import { PdfViewer } from "./common/PdfViewer";
|
||||
import { DownloadWallet } from "./components/Auth/DownloadWallet";
|
||||
|
||||
|
||||
type extStates =
|
||||
@ -439,7 +445,7 @@ function App() {
|
||||
const { isShow, onCancel, onOk, show, message } = useModal();
|
||||
const {isUserBlocked,
|
||||
addToBlockList,
|
||||
removeBlockFromList, getAllBlockedUsers} = useBlockedAddresses()
|
||||
removeBlockFromList, getAllBlockedUsers} = useBlockedAddresses(extState === 'authenticated')
|
||||
const {
|
||||
isShow: isShowUnsavedChanges,
|
||||
onCancel: onCancelUnsavedChanges,
|
||||
@ -486,6 +492,8 @@ function App() {
|
||||
url: "http://127.0.0.1:12391",
|
||||
});
|
||||
const [useLocalNode, setUseLocalNode] = useState(false);
|
||||
const [confirmRequestRead, setConfirmRequestRead] = useState(false);
|
||||
|
||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||
const [showSeed, setShowSeed] = useState(false)
|
||||
const [creationStep, setCreationStep] = useState(1)
|
||||
@ -512,6 +520,15 @@ function App() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
ScreenOrientation.lock({ orientation: 'portrait' });
|
||||
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}, []);
|
||||
useEffect(()=> {
|
||||
if(!shownTutorialsInitiated) return
|
||||
if(extState === 'not-authenticated'){
|
||||
@ -558,6 +575,9 @@ function App() {
|
||||
const resetAtomQMailLastEnteredTimestampAtom = useResetRecoilState(qMailLastEnteredTimestampAtom)
|
||||
const resetAtomMailsAtom = useResetRecoilState(mailsAtom)
|
||||
const resetLastEnteredGroupIdAtom = useResetRecoilState(lastEnteredGroupIdAtom)
|
||||
const resetMyGroupsWhereIAmAdminAtom = useResetRecoilState(
|
||||
myGroupsWhereIAmAdminAtom
|
||||
);
|
||||
const resetAllRecoil = () => {
|
||||
resetAtomSortablePinnedAppsAtom();
|
||||
resetAtomCanSaveSettingToQdnAtom();
|
||||
@ -569,6 +589,7 @@ function App() {
|
||||
resetAtomMailsAtom()
|
||||
resetGroupPropertiesAtom()
|
||||
resetLastEnteredGroupIdAtom()
|
||||
resetMyGroupsWhereIAmAdminAtom()
|
||||
};
|
||||
useEffect(() => {
|
||||
if (!isMobile) return;
|
||||
@ -850,6 +871,24 @@ function App() {
|
||||
});
|
||||
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 = () => {
|
||||
setLtcBalanceLoading(true);
|
||||
window
|
||||
@ -878,6 +917,8 @@ function App() {
|
||||
if(message?.payload?.checkbox1){
|
||||
qortalRequestCheckbox1Ref.current = message?.payload?.checkbox1?.value || false
|
||||
}
|
||||
setConfirmRequestRead(false)
|
||||
|
||||
await showQortalRequestExtension(message?.payload);
|
||||
if (qortalRequestCheckbox1Ref.current) {
|
||||
event.source.postMessage(
|
||||
@ -1632,7 +1673,7 @@ function App() {
|
||||
{balance?.toFixed(2)} QORT
|
||||
</TextP>
|
||||
<RefreshIcon
|
||||
onClick={getBalanceFunc}
|
||||
onClick={getBalanceAndUserInfoFunc}
|
||||
sx={{
|
||||
fontSize: "16px",
|
||||
color: "white",
|
||||
@ -2578,87 +2619,14 @@ function App() {
|
||||
)}
|
||||
{extState === "download-wallet" && (
|
||||
<>
|
||||
<Spacer height="22px" />
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
justifyContent: "flex-start",
|
||||
paddingLeft: "22px",
|
||||
boxSizing: "border-box",
|
||||
}}
|
||||
>
|
||||
<img
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={returnToMain}
|
||||
src={Return}
|
||||
<DownloadWallet
|
||||
returnToMain={returnToMain}
|
||||
setIsLoading={setIsLoading}
|
||||
showInfo={showInfo}
|
||||
rawWallet={rawWallet}
|
||||
setWalletToBeDownloaded={setWalletToBeDownloaded}
|
||||
walletToBeDownloaded={walletToBeDownloaded}
|
||||
/>
|
||||
</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" && (
|
||||
@ -3110,7 +3078,7 @@ await showInfo({
|
||||
>
|
||||
<CountdownCircleTimer
|
||||
isPlaying
|
||||
duration={30}
|
||||
duration={60}
|
||||
colors={["#004777", "#F7B801", "#A30000", "#A30000"]}
|
||||
colorsTime={[7, 5, 2, 0]}
|
||||
onComplete={() => {
|
||||
@ -3191,12 +3159,14 @@ await showInfo({
|
||||
>
|
||||
{messageQortalRequestExtension?.text3}
|
||||
</TextP>
|
||||
<Spacer height="15px" />
|
||||
|
||||
</Box>
|
||||
<Spacer height="15px" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{messageQortalRequestExtension?.text4 && (
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
@ -3214,6 +3184,8 @@ await showInfo({
|
||||
{messageQortalRequestExtension?.text4}
|
||||
</TextP>
|
||||
</Box>
|
||||
<Spacer height="15px" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{messageQortalRequestExtension?.html && (
|
||||
@ -3341,6 +3313,35 @@ await showInfo({
|
||||
</Typography>
|
||||
</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" />
|
||||
<Box
|
||||
@ -3355,8 +3356,16 @@ await showInfo({
|
||||
bgColor="var(--green)"
|
||||
sx={{
|
||||
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
|
||||
</CustomButtonAccept>
|
||||
|
@ -28,7 +28,7 @@ import { GlobalContext } from "../App";
|
||||
import Tooltip, { TooltipProps, tooltipClasses } from '@mui/material/Tooltip';
|
||||
|
||||
export const manifestData = {
|
||||
version: "0.5.3",
|
||||
version: "0.5.4",
|
||||
};
|
||||
|
||||
|
||||
|
@ -41,6 +41,14 @@ export const sortablePinnedAppsAtom = atom({
|
||||
{
|
||||
name: 'Q-Wallets',
|
||||
service: 'APP'
|
||||
},
|
||||
{
|
||||
name: 'Q-Search',
|
||||
service: 'APP'
|
||||
},
|
||||
{
|
||||
name: 'Q-Nodecontrol',
|
||||
service: 'APP'
|
||||
}
|
||||
],
|
||||
});
|
||||
@ -180,3 +188,8 @@ export const lastPaymentSeenTimestampAtom = atom<null | number>({
|
||||
key: 'lastPaymentSeenTimestampAtom',
|
||||
default: null,
|
||||
});
|
||||
|
||||
export const isOpenBlockedModalAtom = atom({
|
||||
key: 'isOpenBlockedModalAtom',
|
||||
default: false,
|
||||
});
|
@ -358,11 +358,12 @@ export async function sendCoinCase(request, event) {
|
||||
|
||||
export async function inviteToGroupCase(request, event) {
|
||||
try {
|
||||
const { groupId, qortalAddress, inviteTime } = request.payload;
|
||||
const { groupId, qortalAddress, inviteTime, txGroupId = 0 } = request.payload;
|
||||
const response = await inviteToGroup({
|
||||
groupId,
|
||||
qortalAddress,
|
||||
inviteTime,
|
||||
txGroupId
|
||||
});
|
||||
|
||||
event.source.postMessage(
|
||||
@ -483,8 +484,8 @@ export async function createGroupCase(request, event) {
|
||||
|
||||
export async function cancelInvitationToGroupCase(request, event) {
|
||||
try {
|
||||
const { groupId, qortalAddress } = request.payload;
|
||||
const response = await cancelInvitationToGroup({ groupId, qortalAddress });
|
||||
const { groupId, qortalAddress,txGroupId = 0 } = request.payload;
|
||||
const response = await cancelInvitationToGroup({ groupId, qortalAddress, txGroupId });
|
||||
|
||||
event.source.postMessage(
|
||||
{
|
||||
@ -564,11 +565,12 @@ export async function joinGroupCase(request, event) {
|
||||
|
||||
export async function kickFromGroupCase(request, event) {
|
||||
try {
|
||||
const { groupId, qortalAddress, rBanReason } = request.payload;
|
||||
const { groupId, qortalAddress, rBanReason, txGroupId = 0 } = request.payload;
|
||||
const response = await kickFromGroup({
|
||||
groupId,
|
||||
qortalAddress,
|
||||
rBanReason,
|
||||
txGroupId
|
||||
});
|
||||
|
||||
event.source.postMessage(
|
||||
@ -595,12 +597,13 @@ export async function kickFromGroupCase(request, event) {
|
||||
|
||||
export async function banFromGroupCase(request, event) {
|
||||
try {
|
||||
const { groupId, qortalAddress, rBanReason, rBanTime } = request.payload;
|
||||
const { groupId, qortalAddress, rBanReason, rBanTime, txGroupId = 0 } = request.payload;
|
||||
const response = await banFromGroup({
|
||||
groupId,
|
||||
qortalAddress,
|
||||
rBanReason,
|
||||
rBanTime,
|
||||
txGroupId
|
||||
});
|
||||
|
||||
event.source.postMessage(
|
||||
@ -734,8 +737,8 @@ export async function getUserSettingsCase(request, event) {
|
||||
|
||||
export async function cancelBanCase(request, event) {
|
||||
try {
|
||||
const { groupId, qortalAddress } = request.payload;
|
||||
const response = await cancelBan({ groupId, qortalAddress });
|
||||
const { groupId, qortalAddress, txGroupId = 0 } = request.payload;
|
||||
const response = await cancelBan({ groupId, qortalAddress, txGroupId });
|
||||
|
||||
event.source.postMessage(
|
||||
{
|
||||
@ -788,8 +791,8 @@ export async function registerNameCase(request, event) {
|
||||
|
||||
export async function makeAdminCase(request, event) {
|
||||
try {
|
||||
const { groupId, qortalAddress } = request.payload;
|
||||
const response = await makeAdmin({ groupId, qortalAddress });
|
||||
const { groupId, qortalAddress,txGroupId = 0 } = request.payload;
|
||||
const response = await makeAdmin({ groupId, qortalAddress, txGroupId });
|
||||
|
||||
event.source.postMessage(
|
||||
{
|
||||
@ -815,8 +818,8 @@ export async function makeAdminCase(request, event) {
|
||||
|
||||
export async function removeAdminCase(request, event) {
|
||||
try {
|
||||
const { groupId, qortalAddress } = request.payload;
|
||||
const response = await removeAdmin({ groupId, qortalAddress });
|
||||
const { groupId, qortalAddress, txGroupId = 0 } = request.payload;
|
||||
const response = await removeAdmin({ groupId, qortalAddress, txGroupId });
|
||||
|
||||
event.source.postMessage(
|
||||
{
|
||||
@ -1329,6 +1332,7 @@ export async function publishOnQDNCase(request, event) {
|
||||
try {
|
||||
const {
|
||||
data,
|
||||
name = "",
|
||||
identifier,
|
||||
service,
|
||||
title,
|
||||
@ -1346,6 +1350,7 @@ export async function publishOnQDNCase(request, event) {
|
||||
identifier,
|
||||
service,
|
||||
title,
|
||||
name,
|
||||
description,
|
||||
category,
|
||||
tag1,
|
||||
|
@ -795,33 +795,35 @@ export async function getNameInfo() {
|
||||
const wallet = await getSaveWallet();
|
||||
const address = wallet.address0;
|
||||
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();
|
||||
if (nameData?.length > 0) {
|
||||
return nameData[0].name;
|
||||
if (nameData?.name) {
|
||||
return nameData.name;
|
||||
} else {
|
||||
return "";
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export async function getNameInfoForOthers(address) {
|
||||
if (!address) return '';
|
||||
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();
|
||||
if (nameData?.length > 0) {
|
||||
return nameData[0].name;
|
||||
if (nameData?.name) {
|
||||
return nameData?.name;
|
||||
} else {
|
||||
return "";
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function getAddressInfo(address) {
|
||||
const validApi = await getBaseApi();
|
||||
const response = await fetch(validApi + "/addresses/" + address);
|
||||
const data = await response.json();
|
||||
|
||||
if (!response?.ok && data?.error !== 124)
|
||||
throw new Error("Cannot fetch address info");
|
||||
throw new Error("Cannot retrieve address info");
|
||||
if (data?.error === 124) {
|
||||
return {
|
||||
address,
|
||||
@ -928,6 +930,59 @@ export async function getBalanceInfo() {
|
||||
const data = await response.json();
|
||||
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() {
|
||||
const wallet = await getSaveWallet();
|
||||
let _url = `${buyTradeNodeBaseUrl}/crosschain/ltc/walletbalance`;
|
||||
@ -1979,7 +2034,7 @@ export async function joinGroup({ groupId }) {
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function cancelInvitationToGroup({ groupId, qortalAddress }) {
|
||||
export async function cancelInvitationToGroup({ groupId, qortalAddress, txGroupId = 0 }) {
|
||||
const lastReference = await getLastRef();
|
||||
const resKeyPair = await getKeyPair();
|
||||
const parsedData = resKeyPair;
|
||||
@ -1996,6 +2051,7 @@ export async function cancelInvitationToGroup({ groupId, qortalAddress }) {
|
||||
recipient: qortalAddress,
|
||||
rGroupId: groupId,
|
||||
lastReference: lastReference,
|
||||
groupID: txGroupId
|
||||
});
|
||||
|
||||
const signedBytes = Base58.encode(tx.signedBytes);
|
||||
@ -2006,7 +2062,7 @@ export async function cancelInvitationToGroup({ groupId, qortalAddress }) {
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function cancelBan({ groupId, qortalAddress }) {
|
||||
export async function cancelBan({ groupId, qortalAddress, txGroupId = 0 }) {
|
||||
const lastReference = await getLastRef();
|
||||
const resKeyPair = await getKeyPair();
|
||||
const parsedData = resKeyPair;
|
||||
@ -2023,6 +2079,7 @@ export async function cancelBan({ groupId, qortalAddress }) {
|
||||
recipient: qortalAddress,
|
||||
rGroupId: groupId,
|
||||
lastReference: lastReference,
|
||||
groupID: txGroupId
|
||||
});
|
||||
|
||||
const signedBytes = Base58.encode(tx.signedBytes);
|
||||
@ -2087,7 +2144,7 @@ export async function updateName({ newName, oldName, description }) {
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function makeAdmin({ groupId, qortalAddress }) {
|
||||
export async function makeAdmin({ groupId, qortalAddress, txGroupId = 0 }) {
|
||||
const lastReference = await getLastRef();
|
||||
const resKeyPair = await getKeyPair();
|
||||
const parsedData = resKeyPair;
|
||||
@ -2104,6 +2161,7 @@ export async function makeAdmin({ groupId, qortalAddress }) {
|
||||
recipient: qortalAddress,
|
||||
rGroupId: groupId,
|
||||
lastReference: lastReference,
|
||||
groupID: txGroupId
|
||||
});
|
||||
|
||||
const signedBytes = Base58.encode(tx.signedBytes);
|
||||
@ -2114,7 +2172,7 @@ export async function makeAdmin({ groupId, qortalAddress }) {
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function removeAdmin({ groupId, qortalAddress }) {
|
||||
export async function removeAdmin({ groupId, qortalAddress, txGroupId = 0 }) {
|
||||
const lastReference = await getLastRef();
|
||||
const resKeyPair = await getKeyPair();
|
||||
const parsedData = resKeyPair;
|
||||
@ -2131,6 +2189,7 @@ export async function removeAdmin({ groupId, qortalAddress }) {
|
||||
recipient: qortalAddress,
|
||||
rGroupId: groupId,
|
||||
lastReference: lastReference,
|
||||
groupID: txGroupId
|
||||
});
|
||||
|
||||
const signedBytes = Base58.encode(tx.signedBytes);
|
||||
@ -2146,6 +2205,7 @@ export async function banFromGroup({
|
||||
qortalAddress,
|
||||
rBanReason = "",
|
||||
rBanTime,
|
||||
txGroupId = 0
|
||||
}) {
|
||||
const lastReference = await getLastRef();
|
||||
const resKeyPair = await getKeyPair();
|
||||
@ -2165,6 +2225,7 @@ export async function banFromGroup({
|
||||
rBanReason: rBanReason,
|
||||
rBanTime,
|
||||
lastReference: lastReference,
|
||||
groupID: txGroupId
|
||||
});
|
||||
|
||||
const signedBytes = Base58.encode(tx.signedBytes);
|
||||
@ -2179,6 +2240,7 @@ export async function kickFromGroup({
|
||||
groupId,
|
||||
qortalAddress,
|
||||
rBanReason = "",
|
||||
txGroupId = 0
|
||||
}) {
|
||||
const lastReference = await getLastRef();
|
||||
const resKeyPair = await getKeyPair();
|
||||
@ -2197,6 +2259,7 @@ export async function kickFromGroup({
|
||||
rGroupId: groupId,
|
||||
rBanReason: rBanReason,
|
||||
lastReference: lastReference,
|
||||
groupID: txGroupId
|
||||
});
|
||||
|
||||
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");
|
||||
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);
|
||||
if (!address) throw new Error("Cannot find user");
|
||||
const lastReference = await getLastRef();
|
||||
@ -2267,13 +2476,14 @@ export async function inviteToGroup({ groupId, qortalAddress, inviteTime }) {
|
||||
rGroupId: groupId,
|
||||
rInviteTime: inviteTime,
|
||||
lastReference: lastReference,
|
||||
groupID: txGroupId
|
||||
});
|
||||
|
||||
const signedBytes = Base58.encode(tx.signedBytes);
|
||||
|
||||
const res = await processTransactionVersion2(signedBytes);
|
||||
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;
|
||||
}
|
||||
|
||||
@ -2988,6 +3198,7 @@ function setupMessageListener() {
|
||||
break;
|
||||
case "updateThreadActivity":
|
||||
updateThreadActivityCase(request, event);
|
||||
break;
|
||||
case "decryptGroupEncryption":
|
||||
decryptGroupEncryptionCase(request, event);
|
||||
break;
|
||||
|
@ -50,15 +50,26 @@ async function getSaveWallet() {
|
||||
export async function getNameInfo() {
|
||||
const wallet = await getSaveWallet();
|
||||
const address = wallet.address0;
|
||||
const validApi = await getBaseApi()
|
||||
const response = await fetch(validApi + "/names/address/" + address);
|
||||
const validApi = await getBaseApi();
|
||||
const response = await fetch(validApi + '/names/primary/' + address);
|
||||
const nameData = await response.json();
|
||||
if (nameData?.length > 0) {
|
||||
return nameData[0].name;
|
||||
if (nameData?.name) {
|
||||
return nameData?.name;
|
||||
} 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() {
|
||||
const res = await getData<any>("keyPair").catch(() => null);
|
||||
if (res) {
|
||||
@ -151,7 +162,7 @@ async function getKeyPair() {
|
||||
if(encryptedData){
|
||||
const registeredName = await getNameInfo()
|
||||
const data = await publishData({
|
||||
registeredName, file: encryptedData, service: 'DOCUMENT_PRIVATE', identifier: `admins-symmetric-qchat-group-${groupId}`, uploadType: 'file', isBase64: true, withFee: true
|
||||
registeredName, data: encryptedData, service: 'DOCUMENT_PRIVATE', identifier: `admins-symmetric-qchat-group-${groupId}`, uploadType: 'base64', withFee: true
|
||||
})
|
||||
return {
|
||||
data,
|
||||
@ -202,7 +213,7 @@ export const encryptAndPublishSymmetricKeyGroupChat = async ({groupId, previousD
|
||||
if(encryptedData){
|
||||
const registeredName = await getNameInfo()
|
||||
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 {
|
||||
data,
|
||||
@ -223,7 +234,7 @@ export const publishGroupEncryptedResource = async ({encryptedData, identifier})
|
||||
const registeredName = await getNameInfo()
|
||||
if(!registeredName) throw new Error('You need a name to publish')
|
||||
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
|
||||
|
||||
@ -242,15 +253,16 @@ export const publishOnQDN = async ({data, identifier, service, title,
|
||||
tag3,
|
||||
tag4,
|
||||
tag5,
|
||||
name,
|
||||
uploadType = 'file'
|
||||
}) => {
|
||||
|
||||
if(data && service){
|
||||
const registeredName = await getNameInfo()
|
||||
const registeredName = name || await getNameInfo()
|
||||
if(!registeredName) throw new Error('You need a name to publish')
|
||||
|
||||
const res = await publishData({
|
||||
registeredName, file: data, service, identifier, uploadType, isBase64: true, withFee: true, title,
|
||||
registeredName, data, service, identifier, uploadType, withFee: true, title,
|
||||
description,
|
||||
category,
|
||||
tag1,
|
||||
|
@ -130,12 +130,17 @@ export const BoundedNumericTextField = ({
|
||||
...props?.InputProps,
|
||||
endAdornment: addIconButtons ? (
|
||||
<InputAdornment position="end">
|
||||
<IconButton size="small" onClick={() => changeValueWithIncDecButton(1)}>
|
||||
<IconButton size="small" onClick={() =>
|
||||
changeValueWithIncDecButton(1)
|
||||
|
||||
} onTouchStart={(e)=> e.stopPropagation()}>
|
||||
<AddIcon sx={{
|
||||
color: 'white'
|
||||
}} />{" "}
|
||||
</IconButton>
|
||||
<IconButton size="small" onClick={() => changeValueWithIncDecButton(-1)}>
|
||||
<IconButton onTouchStart={(e)=> e.stopPropagation()} size="small" onClick={() =>
|
||||
changeValueWithIncDecButton(-1)
|
||||
}>
|
||||
<RemoveIcon sx={{
|
||||
color: 'white'
|
||||
}} />{" "}
|
||||
|
@ -48,7 +48,7 @@ export const useModal = () => {
|
||||
const onCancel = () => {
|
||||
const { reject } = promiseConfig.current;
|
||||
hide();
|
||||
reject();
|
||||
reject('Declined');
|
||||
setMessage({
|
||||
publishFee: "",
|
||||
message: ""
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useContext, useEffect, useMemo, useState } from "react";
|
||||
import React, { useCallback, useContext, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
AppCircle,
|
||||
AppCircleContainer,
|
||||
@ -49,6 +49,7 @@ import { LoadingSnackbar } from "../Snackbar/LoadingSnackbar";
|
||||
import { CustomizedSnackbars } from "../Snackbar/Snackbar";
|
||||
import { getFee } from "../../background";
|
||||
import { fileToBase64 } from "../../utils/fileReading";
|
||||
import { useSortedMyNames } from "../../hooks/useSortedMyNames";
|
||||
|
||||
const CustomSelect = styled(Select)({
|
||||
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 [title, setTitle] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
@ -99,6 +101,8 @@ export const AppPublish = ({ names, categories }) => {
|
||||
const [openSnack, setOpenSnack] = useState(false);
|
||||
const [infoSnack, setInfoSnack] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState("");
|
||||
const mySortedNames = useSortedMyNames(names, myName);
|
||||
|
||||
const maxFileSize = appType === "APP" ? 50 * 1024 * 1024 : 400 * 1024 * 1024; // 50MB or 400MB
|
||||
const { getRootProps, getInputProps } = useDropzone({
|
||||
accept: {
|
||||
@ -162,6 +166,25 @@ export const AppPublish = ({ names, categories }) => {
|
||||
getQapp(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 () => {
|
||||
try {
|
||||
const data = {
|
||||
@ -199,10 +222,10 @@ export const AppPublish = ({ names, categories }) => {
|
||||
publishFee: fee.fee + " QORT",
|
||||
});
|
||||
setIsLoading("Publishing... Please wait.");
|
||||
const fileBase64 = await fileToBase64(file);
|
||||
|
||||
await new Promise((res, rej) => {
|
||||
window.sendMessage("publishOnQDN", {
|
||||
data: fileBase64,
|
||||
data: file,
|
||||
service: appType,
|
||||
title,
|
||||
description,
|
||||
@ -213,6 +236,7 @@ export const AppPublish = ({ names, categories }) => {
|
||||
tag4,
|
||||
tag5,
|
||||
uploadType: "zip",
|
||||
name
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response?.error) {
|
||||
@ -287,7 +311,7 @@ export const AppPublish = ({ names, categories }) => {
|
||||
</em>{" "}
|
||||
{/* This is the placeholder item */}
|
||||
</CustomMenuItem>
|
||||
{names.map((name) => {
|
||||
{mySortedNames.map((name) => {
|
||||
return <CustomMenuItem value={name}>{name}</CustomMenuItem>;
|
||||
})}
|
||||
</CustomSelect>
|
||||
|
@ -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 { Add } from "@mui/icons-material";
|
||||
@ -100,6 +100,57 @@ export const AppViewer = React.forwardRef(({ app , hide, isDevMode, skipAuth}, i
|
||||
};
|
||||
}, [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
|
||||
const navigateBackInIframe = async () => {
|
||||
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 )`,
|
||||
border: 'none',
|
||||
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">
|
||||
|
||||
</iframe>
|
||||
|
@ -17,7 +17,7 @@ import { AppsLibrary } from "./AppsLibrary";
|
||||
|
||||
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 [selectedAppInfo, setSelectedAppInfo] = 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 === "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
|
||||
@ -314,7 +314,7 @@ export const Apps = ({ mode, setMode, show , myName}) => {
|
||||
{mode === "appInfo" && !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} />
|
||||
{mode === "publish" && !selectedTab && <AppPublish names={myName ? [myName] : []} categories={categories} />}
|
||||
{mode === "publish" && !selectedTab && <AppPublish categories={categories} myAddress={myAddress} myName={myName} />}
|
||||
|
||||
{tabs.map((tab) => {
|
||||
if (!iframeRefs.current[tab.tabId]) {
|
||||
@ -335,7 +335,7 @@ export const Apps = ({ mode, setMode, show , myName}) => {
|
||||
{isNewTabWindow && mode === "viewer" && (
|
||||
<>
|
||||
<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" />}
|
||||
|
@ -41,7 +41,9 @@ const officialAppList = [
|
||||
"q-trade",
|
||||
"q-support",
|
||||
"q-manager",
|
||||
"q-wallets"
|
||||
"q-wallets",
|
||||
"q-search",
|
||||
"q-nodecontrol"
|
||||
];
|
||||
|
||||
const ScrollerStyled = styled('div')({
|
||||
|
@ -47,7 +47,9 @@ const officialAppList = [
|
||||
"q-fund",
|
||||
"q-shop",
|
||||
"q-manager",
|
||||
"q-wallets"
|
||||
"q-wallets",
|
||||
"q-search",
|
||||
"q-nodecontrol"
|
||||
];
|
||||
|
||||
const ScrollerStyled = styled("div")({
|
||||
|
@ -20,7 +20,7 @@ import HelpIcon from '@mui/icons-material/Help';
|
||||
import { useHandleTutorials } from "../Tutorials/useHandleTutorials";
|
||||
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 { showTutorial } = useContext(GlobalContext);
|
||||
|
||||
@ -146,7 +146,7 @@ export const AppsHome = ({ setMode, myApp, myWebsite, availableQapps, myName }
|
||||
<AppCircleLabel>Library</AppCircleLabel>
|
||||
</AppCircleContainer>
|
||||
</ButtonBase>
|
||||
<AppsPrivate myName={myName} />
|
||||
<AppsPrivate myName={myName} myAddress={myAddress} />
|
||||
|
||||
<SortablePinnedApps availableQapps={availableQapps} myWebsite={myWebsite} myApp={myApp} />
|
||||
|
||||
|
@ -45,7 +45,9 @@ const officialAppList = [
|
||||
"q-support",
|
||||
"q-manager",
|
||||
"q-mintership",
|
||||
"q-wallets"
|
||||
"q-wallets",
|
||||
"q-search",
|
||||
"q-nodecontrol"
|
||||
];
|
||||
|
||||
const ScrollerStyled = styled('div')({
|
||||
|
@ -56,7 +56,9 @@ const officialAppList = [
|
||||
"q-shop",
|
||||
"q-manager",
|
||||
"q-mintership",
|
||||
"q-wallets"
|
||||
"q-wallets",
|
||||
"q-search",
|
||||
"q-nodecontrol"
|
||||
];
|
||||
|
||||
const ScrollerStyled = styled("div")({
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useContext, useMemo, useState } from "react";
|
||||
import React, { useCallback, useContext, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
@ -30,15 +30,18 @@ import {
|
||||
PublishQAppInfo,
|
||||
} from "./Apps-styles";
|
||||
import ImageUploader from "../../common/ImageUploader";
|
||||
import { isMobile, MyContext } from "../../App";
|
||||
import { getBaseApiReact, isMobile, MyContext } from "../../App";
|
||||
import { fileToBase64 } from "../../utils/fileReading";
|
||||
import { objectToBase64 } from "../../qdn/encryption/group-encryption";
|
||||
import { getFee } from "../../background";
|
||||
import { useSortedMyNames } from "../../hooks/useSortedMyNames";
|
||||
|
||||
const maxFileSize = 50 * 1024 * 1024; // 50MB
|
||||
|
||||
export const AppsPrivate = ({myName}) => {
|
||||
export const AppsPrivate = ({myName, myAddress}) => {
|
||||
const { openApp } = useHandlePrivateApps();
|
||||
const [names, setNames] = useState([]);
|
||||
const [name, setName] = useState(0);
|
||||
const [file, setFile] = useState(null);
|
||||
const [logo, setLogo] = useState(null);
|
||||
const [qortalUrl, setQortalUrl] = useState("");
|
||||
@ -48,6 +51,7 @@ export const AppsPrivate = ({myName}) => {
|
||||
const [myGroupsWhereIAmAdminFromGlobal] = useRecoilState(
|
||||
myGroupsWhereIAmAdminAtom
|
||||
);
|
||||
const mySortedNames = useSortedMyNames(names, myName);
|
||||
|
||||
const myGroupsWhereIAmAdmin = useMemo(()=> {
|
||||
return myGroupsWhereIAmAdminFromGlobal?.filter((group)=> groupsProperties[group?.groupId]?.isOpen === false)
|
||||
@ -165,6 +169,8 @@ export const AppsPrivate = ({myName}) => {
|
||||
data: decryptedData,
|
||||
identifier: newPrivateAppValues?.identifier,
|
||||
service: newPrivateAppValues?.service,
|
||||
uploadType: 'base64',
|
||||
name,
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response?.error) {
|
||||
@ -181,7 +187,7 @@ export const AppsPrivate = ({myName}) => {
|
||||
{
|
||||
identifier: newPrivateAppValues?.identifier,
|
||||
service: newPrivateAppValues?.service,
|
||||
name: myName,
|
||||
name,
|
||||
groupId: selectedGroup,
|
||||
},
|
||||
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) => {
|
||||
setValueTabPrivateApp(newValue);
|
||||
};
|
||||
@ -432,6 +456,34 @@ export const AppsPrivate = ({myName}) => {
|
||||
{file ? "Change" : "Choose"} File
|
||||
</PublishQAppChoseFile>
|
||||
<Spacer height="20px" />
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '5px',
|
||||
}}
|
||||
>
|
||||
<Label>Select a Qortal name</Label>
|
||||
|
||||
<Select
|
||||
labelId="demo-simple-select-label"
|
||||
id="demo-simple-select"
|
||||
value={name}
|
||||
label="Groups where you are an admin"
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
>
|
||||
<MenuItem value={0}>No name selected</MenuItem>
|
||||
{mySortedNames.map((name) => {
|
||||
return (
|
||||
<MenuItem key={name} value={name}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</Box>
|
||||
<Spacer height="20px" />
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
|
@ -10,9 +10,99 @@ import { MyContext } from '../../App';
|
||||
import FileSaver from 'file-saver';
|
||||
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import { createEndpoint } from '../../background';
|
||||
import { uint8ArrayToBase64 } from '../../backgroundFunctions/encryption';
|
||||
|
||||
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 (
|
||||
blob: Blob,
|
||||
@ -255,7 +345,15 @@ export function openIndexedDB() {
|
||||
'GET_NODE_INFO',
|
||||
'GET_NODE_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',
|
||||
'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',
|
||||
'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;
|
||||
|
||||
const sendMessageToRuntime = (message, eventPort) => {
|
||||
window.sendMessage(message.action, message.payload, 300000, message.isExtension, {
|
||||
name: appName, service: appService
|
||||
let timeout: number = 300000;
|
||||
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)
|
||||
.then((response) => {
|
||||
if (response.error) {
|
||||
@ -551,7 +665,7 @@ isDOMContentLoaded: false
|
||||
result: null,
|
||||
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 {
|
||||
@ -576,38 +690,26 @@ isDOMContentLoaded: false
|
||||
} else if(event?.data?.action === 'SAVE_FILE'
|
||||
){
|
||||
try {
|
||||
const res = await saveFile( event.data, null, true, {
|
||||
await saveFile(event.data, null, true, {
|
||||
openSnackGlobal,
|
||||
setOpenSnackGlobal,
|
||||
infoSnackCustom,
|
||||
setInfoSnackCustom
|
||||
setInfoSnackCustom,
|
||||
});
|
||||
event.ports[0].postMessage({
|
||||
result: true,
|
||||
error: null,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
|
||||
}
|
||||
} 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'
|
||||
|
||||
) {
|
||||
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,
|
||||
error: error?.message || 'Failed to save file',
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
event?.data?.action === 'ENCRYPT_DATA' || event?.data?.action === 'ENCRYPT_DATA_WITH_SHARING_KEY' || event?.data?.action === 'ENCRYPT_QORTAL_GROUP_DATA'
|
||||
|
||||
) {
|
||||
let data;
|
||||
try {
|
||||
data = await storeFilesInIndexedDB(event.data);
|
||||
@ -630,6 +732,29 @@ isDOMContentLoaded: false
|
||||
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' ||
|
||||
event?.data?.action === 'QDN_RESOURCE_DISPLAYED'){
|
||||
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
|
||||
|
||||
|
||||
|
||||
|
248
src/components/Auth/DownloadWallet.tsx
Normal file
248
src/components/Auth/DownloadWallet.tsx
Normal 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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -79,7 +79,7 @@ export const AdminSpaceInner = ({
|
||||
const res = await fetch(
|
||||
`${getBaseApiReact()}/arbitrary/DOCUMENT_PRIVATE/${
|
||||
getLatestPublish.name
|
||||
}/${getLatestPublish.identifier}?encoding=base64`
|
||||
}/${getLatestPublish.identifier}?encoding=base64&rebuild=true`
|
||||
);
|
||||
data = await res.text();
|
||||
|
||||
|
@ -15,12 +15,12 @@ import { CustomizedSnackbars } from '../Snackbar/Snackbar'
|
||||
import { PUBLIC_NOTIFICATION_CODE_FIRST_SECRET_KEY } from '../../constants/codes'
|
||||
import { useMessageQueue } from '../../MessageQueueContext'
|
||||
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 { ReplyPreview } from './MessageItem'
|
||||
import { ExitIcon } from '../../assets/Icons/ExitIcon'
|
||||
import { RESOURCE_TYPE_NUMBER_GROUP_CHAT_REACTIONS } from '../../constants/resourceTypes'
|
||||
import { isExtMsg } from '../../background'
|
||||
import { getFee, isExtMsg } from '../../background'
|
||||
import MentionList from './MentionList'
|
||||
import { ChatOptions } from './ChatOptions'
|
||||
import { isFocusedParentGroupAtom } from '../../atoms/global'
|
||||
@ -28,6 +28,10 @@ import { useRecoilState } from 'recoil'
|
||||
import AppViewerContainer from '../Apps/AppViewerContainer'
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
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 });
|
||||
|
||||
@ -55,8 +59,9 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
|
||||
const editorRef = useRef(null);
|
||||
const { queueChats, addToQueue, processWithNewMessages } = useMessageQueue();
|
||||
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)
|
||||
|
||||
@ -624,6 +629,8 @@ if(isFocusedParent === false){
|
||||
setReplyMessage(null)
|
||||
setOnEditMessage(null)
|
||||
clearEditorContent()
|
||||
setIsDeleteImage(false);
|
||||
setChatImagesToSave([]);
|
||||
}
|
||||
}, [isFocusedParent])
|
||||
const clearEditorContent = () => {
|
||||
@ -646,50 +653,154 @@ const clearEditorContent = () => {
|
||||
|
||||
const sendMessage = async () => {
|
||||
try {
|
||||
if(messageSize > 4000) return
|
||||
if(isPrivate === null) throw new Error('Unable to determine if group is private')
|
||||
if(isSending) return
|
||||
if(+balance < 4) throw new Error('You need at least 4 QORT to send a message')
|
||||
pauseAllQueues()
|
||||
if (messageSize > 4000) return; // TODO magic number
|
||||
if (isPrivate === null)
|
||||
throw new Error(
|
||||
"Onable to determine if group is private"
|
||||
);
|
||||
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) {
|
||||
const htmlContent = editorRef.current.getHTML();
|
||||
let htmlContent = editorRef.current.getHTML();
|
||||
const deleteImage =
|
||||
onEditMessage && isDeleteImage && messageHasImage(onEditMessage);
|
||||
|
||||
if(!htmlContent?.trim() || htmlContent?.trim() === '<p></p>') return
|
||||
const hasImage =
|
||||
chatImagesToSave?.length > 0 || onEditMessage?.images?.length > 0;
|
||||
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);
|
||||
|
||||
|
||||
setIsSending(true)
|
||||
const message = isPrivate === false ? editorRef.current.getJSON() : htmlContent
|
||||
const secretKeyObject = await getSecretKey(false, true)
|
||||
|
||||
let repliedTo = replyMessage?.signature
|
||||
let repliedTo = replyMessage?.signature;
|
||||
|
||||
if (replyMessage?.chatReference) {
|
||||
repliedTo = replyMessage?.chatReference
|
||||
repliedTo = replyMessage?.chatReference;
|
||||
}
|
||||
let chatReference = onEditMessage?.signature
|
||||
|
||||
const publicData = isPrivate ? {} : {
|
||||
const chatReference = onEditMessage?.signature;
|
||||
|
||||
const publicData = isPrivate
|
||||
? {}
|
||||
: {
|
||||
isEdited: chatReference ? true : false,
|
||||
};
|
||||
|
||||
interface ImageToPublish {
|
||||
service: string;
|
||||
identifier: string;
|
||||
name: string;
|
||||
base64: string;
|
||||
}
|
||||
|
||||
const imagesToPublish: ImageToPublish[] = [];
|
||||
|
||||
if (deleteImage) {
|
||||
const fee = await getFee('ARBITRARY');
|
||||
await show({
|
||||
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(),
|
||||
...publicData
|
||||
}
|
||||
images: images,
|
||||
...publicData,
|
||||
};
|
||||
const objectMessage = {
|
||||
...(otherData || {}),
|
||||
[isPrivate ? 'message' : 'messageText']: message,
|
||||
version: 3
|
||||
}
|
||||
const message64: any = await objectToBase64(objectMessage)
|
||||
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 encryptSingle =
|
||||
isPrivate === false
|
||||
? JSON.stringify(objectMessage)
|
||||
: await encryptChatMessage(message64, secretKeyObject);
|
||||
|
||||
const sendMessageFunc = async () => {
|
||||
return await sendChatGroup({groupId: selectedGroup,messageText: encryptSingle, chatReference})
|
||||
return await sendChatGroup({
|
||||
groupId: selectedGroup,
|
||||
messageText: encryptSingle,
|
||||
chatReference,
|
||||
});
|
||||
};
|
||||
|
||||
// Add the function to the queue
|
||||
@ -699,33 +810,34 @@ const sendMessage = async ()=> {
|
||||
timestamp: Date.now(),
|
||||
senderName: myName,
|
||||
sender: myAddress,
|
||||
...(otherData || {})
|
||||
...(otherData || {}),
|
||||
},
|
||||
chatReference
|
||||
}
|
||||
addToQueue(sendMessageFunc, messageObj, 'chat',
|
||||
selectedGroup );
|
||||
chatReference,
|
||||
};
|
||||
addToQueue(sendMessageFunc, messageObj, 'chat', selectedGroup);
|
||||
setTimeout(() => {
|
||||
executeEvent("sent-new-message-group", {})
|
||||
executeEvent('sent-new-message-group', {});
|
||||
}, 150);
|
||||
clearEditorContent()
|
||||
setReplyMessage(null)
|
||||
setOnEditMessage(null)
|
||||
clearEditorContent();
|
||||
setReplyMessage(null);
|
||||
setOnEditMessage(null);
|
||||
setIsDeleteImage(false);
|
||||
setChatImagesToSave([]);
|
||||
}
|
||||
// send chat message
|
||||
} catch (error) {
|
||||
const errorMsg = error?.message || error
|
||||
const errorMsg = error?.message || error;
|
||||
setInfoSnack({
|
||||
type: "error",
|
||||
type: 'error',
|
||||
message: errorMsg,
|
||||
});
|
||||
setOpenSnack(true);
|
||||
console.error(error)
|
||||
console.error(error);
|
||||
} finally {
|
||||
setIsSending(false)
|
||||
resumeAllQueues()
|
||||
}
|
||||
setIsSending(false);
|
||||
resumeAllQueues();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (hide) {
|
||||
@ -742,7 +854,8 @@ const sendMessage = async ()=> {
|
||||
setReplyMessage(message)
|
||||
setOnEditMessage(null)
|
||||
setIsFocusedParent(true);
|
||||
|
||||
setIsDeleteImage(false);
|
||||
setChatImagesToSave([]);
|
||||
setTimeout(() => {
|
||||
editorRef?.current?.chain().focus()
|
||||
|
||||
@ -755,7 +868,7 @@ const sendMessage = async ()=> {
|
||||
setReplyMessage(null)
|
||||
setIsFocusedParent(true);
|
||||
setTimeout(() => {
|
||||
editorRef.current.chain().focus().setContent(message?.messageText || message?.text).run();
|
||||
editorRef?.current?.chain().focus().setContent(message?.messageText || message?.text || '<p></p>').run();
|
||||
}, 250);
|
||||
}, [])
|
||||
|
||||
@ -825,6 +938,24 @@ const sendMessage = async ()=> {
|
||||
}
|
||||
}, [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 (
|
||||
<div style={{
|
||||
height: isMobile ? '100%' : '100%',
|
||||
@ -864,6 +995,117 @@ const sendMessage = async ()=> {
|
||||
overflow: !isMobile && "auto",
|
||||
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 && (
|
||||
<Box sx={{
|
||||
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} />
|
||||
|
||||
|
||||
|
||||
|
@ -261,17 +261,18 @@ export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onR
|
||||
if (chatReferences?.[message.signature]) {
|
||||
reactions = chatReferences[message.signature]?.reactions || null;
|
||||
|
||||
if (chatReferences[message.signature]?.edit?.message && message?.text) {
|
||||
message.text = chatReferences[message.signature]?.edit?.message;
|
||||
message.isEdit = true
|
||||
message.editTimestamp = chatReferences[message.signature]?.edit?.timestamp
|
||||
}
|
||||
if (chatReferences[message.signature]?.edit?.messageText && message?.messageText) {
|
||||
message.messageText = chatReferences[message.signature]?.edit?.messageText;
|
||||
message.isEdit = true
|
||||
message.editTimestamp = chatReferences[message.signature]?.edit?.timestamp
|
||||
}
|
||||
if (chatReferences[message.signature]?.edit) {
|
||||
message.text =
|
||||
chatReferences[message.signature]?.edit?.message;
|
||||
message.messageText =
|
||||
chatReferences[message.signature]?.edit?.messageText;
|
||||
message.images =
|
||||
chatReferences[message.signature]?.edit?.images;
|
||||
|
||||
message.isEdit = true;
|
||||
message.editTimestamp =
|
||||
chatReferences[message.signature]?.edit?.timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if message is updating
|
||||
|
@ -39,6 +39,7 @@ import StarterKit from "@tiptap/starter-kit";
|
||||
import Underline from "@tiptap/extension-underline";
|
||||
import { generateHTML } from "@tiptap/react";
|
||||
import ErrorBoundary from "../../common/ErrorBoundary";
|
||||
import { isHtmlString } from "../../utils/chat";
|
||||
|
||||
const extractTextFromHTML = (htmlString = '') => {
|
||||
return convert(htmlString, {
|
||||
@ -62,24 +63,27 @@ export const ChatOptions = ({ messages : untransformedMessages, goToMessage, mem
|
||||
const messages = useMemo(() => {
|
||||
return untransformedMessages?.map((item) => {
|
||||
if (item?.messageText) {
|
||||
let transformedMessage = item?.messageText
|
||||
let transformedMessage = item?.messageText;
|
||||
const isHtml = isHtmlString(item?.messageText);
|
||||
try {
|
||||
transformedMessage = generateHTML(item?.messageText, [
|
||||
transformedMessage = isHtml
|
||||
? item?.messageText
|
||||
: generateHTML(item?.messageText, [
|
||||
StarterKit,
|
||||
Underline,
|
||||
Highlight,
|
||||
Mention
|
||||
])
|
||||
Mention,
|
||||
]);
|
||||
return {
|
||||
...item,
|
||||
messageText: transformedMessage
|
||||
}
|
||||
messageText: transformedMessage,
|
||||
};
|
||||
} catch (error) {
|
||||
// error
|
||||
console.log(error);
|
||||
}
|
||||
} else return item
|
||||
})
|
||||
}, [untransformedMessages])
|
||||
} else return item;
|
||||
});
|
||||
}, [untransformedMessages]);
|
||||
|
||||
const getTimestampMention = async () => {
|
||||
try {
|
||||
|
@ -66,7 +66,7 @@ export const CreateCommonSecret = ({groupId, secretKey, isOwner, myAddress, sec
|
||||
const res = await fetch(
|
||||
`${getBaseApiReact()}/arbitrary/DOCUMENT_PRIVATE/${publish.name}/${
|
||||
publish.identifier
|
||||
}?encoding=base64`
|
||||
}?encoding=base64&rebuild=true`
|
||||
);
|
||||
const data = await res.text();
|
||||
|
||||
|
@ -33,6 +33,9 @@ import level7Img from "../../assets/badges/level-7.png"
|
||||
import level8Img from "../../assets/badges/level-8.png"
|
||||
import level9Img from "../../assets/badges/level-9.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)=> {
|
||||
switch(level?.toString()){
|
||||
@ -103,34 +106,32 @@ useEffect(()=> {
|
||||
}, [message?.sender, getIndividualUserInfo])
|
||||
|
||||
const htmlText = useMemo(() => {
|
||||
|
||||
if (message?.messageText) {
|
||||
const isHtml = isHtmlString(message?.messageText);
|
||||
if (isHtml) return message?.messageText;
|
||||
return generateHTML(message?.messageText, [
|
||||
StarterKit,
|
||||
Underline,
|
||||
Highlight,
|
||||
Mention,
|
||||
TextStyle
|
||||
])
|
||||
TextStyle,
|
||||
]);
|
||||
}
|
||||
|
||||
}, [message?.editTimestamp])
|
||||
|
||||
|
||||
}, [message?.editTimestamp]);
|
||||
|
||||
const htmlReply = useMemo(() => {
|
||||
|
||||
if (reply?.messageText) {
|
||||
const isHtml = isHtmlString(reply?.messageText);
|
||||
if (isHtml) return reply?.messageText;
|
||||
return generateHTML(reply?.messageText, [
|
||||
StarterKit,
|
||||
Underline,
|
||||
Highlight,
|
||||
Mention,
|
||||
TextStyle
|
||||
])
|
||||
TextStyle,
|
||||
]);
|
||||
}
|
||||
|
||||
}, [reply?.editTimestamp])
|
||||
}, [reply?.editTimestamp]);
|
||||
|
||||
const userAvatarUrl = useMemo(()=> {
|
||||
return message?.senderName ? `${getBaseApiReact()}/arbitrary/THUMBNAIL/${
|
||||
@ -142,6 +143,13 @@ const onSeenFunc = useCallback(()=> {
|
||||
onSeen(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 (
|
||||
<MessageWragger lastMessage={lastSignature === message?.signature} isLast={isLast} onSeen={onSeenFunc}>
|
||||
{message?.divide && (
|
||||
@ -335,7 +343,7 @@ const onSeenFunc = useCallback(()=> {
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
{message?.messageText && (
|
||||
{htmlText && !hasNoMessage && (
|
||||
<MessageDisplay
|
||||
htmlContent={htmlText}
|
||||
setMobileViewModeKeepOpen={setMobileViewModeKeepOpen}
|
||||
@ -343,9 +351,30 @@ const onSeenFunc = useCallback(()=> {
|
||||
)}
|
||||
{message?.decryptedData?.type === "notification" ? (
|
||||
<MessageDisplay htmlContent={message.decryptedData?.data?.message} />
|
||||
) : (
|
||||
) : hasNoMessage ? null : (
|
||||
<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
|
||||
sx={{
|
||||
display: "flex",
|
||||
@ -519,6 +548,19 @@ const onSeenFunc = useCallback(()=> {
|
||||
|
||||
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 (
|
||||
<Box
|
||||
sx={{
|
||||
@ -553,15 +595,9 @@ export const ReplyPreview = ({message, isEdit})=> {
|
||||
}}>Replied to {message?.senderName || message?.senderAddress}</Typography>
|
||||
)}
|
||||
|
||||
{message?.messageText && (
|
||||
{replyMessageText && (
|
||||
<MessageDisplay
|
||||
htmlContent={generateHTML(message?.messageText, [
|
||||
StarterKit,
|
||||
Underline,
|
||||
Highlight,
|
||||
Mention,
|
||||
TextStyle
|
||||
])}
|
||||
htmlContent={replyMessageText}
|
||||
/>
|
||||
)}
|
||||
{message?.decryptedData?.type === "notification" ? (
|
||||
|
@ -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 StarterKit from "@tiptap/starter-kit";
|
||||
import { Color } from "@tiptap/extension-color";
|
||||
@ -34,6 +34,7 @@ import ListItemButton from '@mui/material/ListItemButton';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import { ReactRenderer } from '@tiptap/react'
|
||||
import MentionList from './MentionList.jsx'
|
||||
import { fileToBase64 } from "../../utils/fileReading/index.js";
|
||||
|
||||
function textMatcher(doc, from) {
|
||||
const textBeforeCursor = doc.textBetween(0, from, ' ', ' ');
|
||||
@ -110,13 +111,13 @@ const MenuBar = ({ setEditorRef, isChat }) => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (editor) {
|
||||
if (editor && !isChat) {
|
||||
editor.view.dom.addEventListener("paste", handlePaste);
|
||||
return () => {
|
||||
editor.view.dom.removeEventListener("paste", handlePaste);
|
||||
};
|
||||
}
|
||||
}, [editor]);
|
||||
}, [editor, isChat]);
|
||||
|
||||
return (
|
||||
<div className="control-group">
|
||||
@ -299,7 +300,8 @@ export default ({
|
||||
customEditorHeight,
|
||||
membersWithNames,
|
||||
enableMentions,
|
||||
isReply
|
||||
isReply,
|
||||
insertImage,
|
||||
}) => {
|
||||
|
||||
const extensionsFiltered = isChat
|
||||
@ -329,7 +331,35 @@ export default ({
|
||||
}, [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([]);
|
||||
@ -470,6 +500,25 @@ export default ({
|
||||
}
|
||||
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>
|
||||
|
@ -1,10 +1,9 @@
|
||||
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 userNamesBlockedRef = useRef({})
|
||||
|
||||
@ -19,7 +18,7 @@ export const useBlockedAddresses = () => {
|
||||
const isUserBlocked = useCallback((address, name)=> {
|
||||
try {
|
||||
if(!address) return false
|
||||
if(userBlockedRef.current[address] || userNamesBlockedRef.current[name]) return true
|
||||
if(userBlockedRef.current[address]) return true
|
||||
return false
|
||||
|
||||
|
||||
@ -29,6 +28,9 @@ export const useBlockedAddresses = () => {
|
||||
}, [])
|
||||
|
||||
useEffect(()=> {
|
||||
if (!isAuthenticated) return;
|
||||
userBlockedRef.current = {};
|
||||
userNamesBlockedRef.current = {};
|
||||
const fetchBlockedList = async ()=> {
|
||||
try {
|
||||
const response = await new Promise((res, rej) => {
|
||||
@ -87,15 +89,16 @@ export const useBlockedAddresses = () => {
|
||||
}
|
||||
}
|
||||
fetchBlockedList()
|
||||
}, [])
|
||||
}, [isAuthenticated])
|
||||
|
||||
const removeBlockFromList = useCallback(async (address, name)=> {
|
||||
if(name){
|
||||
await new Promise((res, rej) => {
|
||||
window.sendMessage("listActions", {
|
||||
|
||||
type: 'remove',
|
||||
items: name ? [name] : [address],
|
||||
listName: name ? 'blockedNames' : 'blockedAddresses'
|
||||
items: [name] ,
|
||||
listName: 'blockedNames'
|
||||
|
||||
})
|
||||
.then((response) => {
|
||||
@ -103,15 +106,11 @@ export const useBlockedAddresses = () => {
|
||||
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);
|
||||
}
|
||||
@ -120,13 +119,15 @@ export const useBlockedAddresses = () => {
|
||||
console.error("Failed qortalRequest", error);
|
||||
});
|
||||
})
|
||||
if(name && userBlockedRef.current[address]){
|
||||
}
|
||||
|
||||
if(address){
|
||||
await new Promise((res, rej) => {
|
||||
window.sendMessage("listActions", {
|
||||
|
||||
type: 'remove',
|
||||
items: !name ? [name] : [address],
|
||||
listName: !name ? 'blockedNames' : 'blockedAddresses'
|
||||
items: [address],
|
||||
listName: 'blockedAddresses'
|
||||
|
||||
})
|
||||
.then((response) => {
|
||||
@ -134,9 +135,12 @@ export const useBlockedAddresses = () => {
|
||||
rej(response?.message);
|
||||
return;
|
||||
} else {
|
||||
|
||||
const copyObject = {...userBlockedRef.current}
|
||||
delete copyObject[address]
|
||||
userBlockedRef.current = copyObject
|
||||
|
||||
|
||||
res(response);
|
||||
}
|
||||
})
|
||||
@ -146,15 +150,17 @@ export const useBlockedAddresses = () => {
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
}, [])
|
||||
|
||||
const addToBlockList = useCallback(async (address, name)=> {
|
||||
if(name){
|
||||
await new Promise((res, rej) => {
|
||||
window.sendMessage("listActions", {
|
||||
|
||||
type: 'add',
|
||||
items: name ? [name] : [address],
|
||||
listName: name ? 'blockedNames' : 'blockedAddresses'
|
||||
items: [name],
|
||||
listName: 'blockedNames'
|
||||
|
||||
})
|
||||
.then((response) => {
|
||||
@ -162,17 +168,10 @@ export const useBlockedAddresses = () => {
|
||||
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);
|
||||
}
|
||||
@ -181,6 +180,35 @@ export const useBlockedAddresses = () => {
|
||||
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 {
|
||||
|
@ -52,6 +52,8 @@ export const ImageCard = ({
|
||||
backgroundColor: "#1F2023",
|
||||
height: height,
|
||||
transition: "height 0.6s ease-in-out",
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
@ -170,8 +172,18 @@ export const ImageCard = ({
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<CardContent>
|
||||
<Box
|
||||
sx={{
|
||||
maxHeight: '100%',
|
||||
flexGrow: 1,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<CardContent
|
||||
sx={{
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<ImageViewer src={image} />
|
||||
</CardContent>
|
||||
</Box>
|
||||
@ -203,6 +215,7 @@ export const ImageCard = ({
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
cursor: "pointer",
|
||||
height: '100%',
|
||||
}}
|
||||
onClick={handleOpenFullscreen}
|
||||
>
|
||||
@ -239,6 +252,9 @@ export const ImageCard = ({
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: "#000",
|
||||
}}
|
||||
>
|
||||
|
@ -6,19 +6,30 @@ import {
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
IconButton,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import { MyContext } from "../../App";
|
||||
import { getBaseApiReact, MyContext } from "../../App";
|
||||
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 [value, setValue] = useState("");
|
||||
|
||||
const { getAllBlockedUsers, removeBlockFromList, addToBlockList } = useContext(MyContext);
|
||||
const [addressesWithNames, setAddressesWithNames] = useState({})
|
||||
const { isShow, onCancel, onOk, show, message } = useModal();
|
||||
const { getAllBlockedUsers, removeBlockFromList, addToBlockList, setOpenSnackGlobal, setInfoSnackCustom } =
|
||||
useContext(MyContext);
|
||||
const [blockedUsers, setBlockedUsers] = useState({
|
||||
addresses: {},
|
||||
names: {},
|
||||
@ -28,20 +39,119 @@ export const BlockedUsersModal = ({ close }) => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if(!isOpenBlockedModal) return
|
||||
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 (
|
||||
<Dialog
|
||||
open={true}
|
||||
open={isOpenBlockedModal}
|
||||
aria-labelledby="alert-dialog-title"
|
||||
aria-describedby="alert-dialog-description"
|
||||
>
|
||||
<DialogTitle>Blocked Users</DialogTitle>
|
||||
<DialogContent sx={{
|
||||
padding: '20px'
|
||||
}}>
|
||||
<DialogContent
|
||||
sx={{
|
||||
padding: "20px",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
@ -49,39 +159,42 @@ export const BlockedUsersModal = ({ close }) => {
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
placeholder="Name"
|
||||
placeholder="Name or address"
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
setValue(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<Button variant="contained" onClick={async ()=> {
|
||||
try {
|
||||
if(!value) return
|
||||
await addToBlockList(undefined, value)
|
||||
fetchBlockedUsers()
|
||||
setHasChanged(true)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}}>Block</Button>
|
||||
<Button
|
||||
sx={{
|
||||
flexShrink: 0,
|
||||
}}
|
||||
variant="contained"
|
||||
onClick={blockUser}
|
||||
>
|
||||
Block
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{Object.entries(blockedUsers?.addresses).length > 0 && (
|
||||
<>
|
||||
<Spacer height="20px" />
|
||||
<DialogContentText id="alert-dialog-description">
|
||||
Blocked Users for Chat ( addresses )
|
||||
Blocked addresses- blocks processing of txs
|
||||
</DialogContentText>
|
||||
<Spacer height="10px" />
|
||||
<Button variant="contained" size="small" onClick={getNames}>Fetch names</Button>
|
||||
<Spacer height="10px" />
|
||||
</>
|
||||
)}
|
||||
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '10px'
|
||||
}}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "10px",
|
||||
}}
|
||||
>
|
||||
{Object.entries(blockedUsers?.addresses || {})?.map(
|
||||
([key, value]) => {
|
||||
return (
|
||||
@ -90,18 +203,22 @@ export const BlockedUsersModal = ({ close }) => {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "10px",
|
||||
width: '100%',
|
||||
justifyContent: 'space-between'
|
||||
width: "100%",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Typography>{key}</Typography>
|
||||
<Typography>{addressesWithNames[key] || key}</Typography>
|
||||
<Button
|
||||
sx={{
|
||||
flexShrink: 0,
|
||||
}}
|
||||
size="small"
|
||||
variant="contained"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await removeBlockFromList(key, undefined);
|
||||
setHasChanged(true);
|
||||
setValue('')
|
||||
setValue("");
|
||||
fetchBlockedUsers();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@ -119,17 +236,19 @@ export const BlockedUsersModal = ({ close }) => {
|
||||
<>
|
||||
<Spacer height="20px" />
|
||||
<DialogContentText id="alert-dialog-description">
|
||||
Blocked Users for QDN and Chat (names)
|
||||
Blocked names for QDN
|
||||
</DialogContentText>
|
||||
<Spacer height="10px" />
|
||||
</>
|
||||
)}
|
||||
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '10px'
|
||||
}}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "10px",
|
||||
}}
|
||||
>
|
||||
{Object.entries(blockedUsers?.names || {})?.map(([key, value]) => {
|
||||
return (
|
||||
<Box
|
||||
@ -137,12 +256,16 @@ export const BlockedUsersModal = ({ close }) => {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "10px",
|
||||
width: '100%',
|
||||
justifyContent: 'space-between'
|
||||
width: "100%",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Typography>{key}</Typography>
|
||||
<Button
|
||||
size="small"
|
||||
sx={{
|
||||
flexShrink: 0,
|
||||
}}
|
||||
variant="contained"
|
||||
onClick={async () => {
|
||||
try {
|
||||
@ -177,14 +300,76 @@ export const BlockedUsersModal = ({ close }) => {
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
if (hasChanged) {
|
||||
executeEvent('updateChatMessagesWithBlocks', true)
|
||||
executeEvent("updateChatMessagesWithBlocks", true);
|
||||
}
|
||||
close()
|
||||
setIsOpenBlockedModal(false);
|
||||
}}
|
||||
>
|
||||
close
|
||||
</Button>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
@ -19,7 +19,8 @@ import React, {
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import BlockIcon from '@mui/icons-material/Block';
|
||||
import PersonOffIcon from '@mui/icons-material/PersonOff';
|
||||
|
||||
import { WalletsAppWrapper } from "./WalletsAppWrapper";
|
||||
|
||||
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 { ThingsToDoInitial } from "./ThingsToDoInitial";
|
||||
import { GroupJoinRequests } from "./GroupJoinRequests";
|
||||
import { GroupJoinRequests, requestQueueGroupJoinRequests } from "./GroupJoinRequests";
|
||||
import { GroupForum } from "../Chat/GroupForum";
|
||||
import { GroupInvites } from "./GroupInvites";
|
||||
import {
|
||||
@ -99,7 +100,7 @@ import { formatEmailDate } from "./QMailMessages";
|
||||
import { useHandleMobileNativeBack } from "../../hooks/useHandleMobileNativeBack";
|
||||
import { AdminSpace } from "../Chat/AdminSpace";
|
||||
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 { BlockedUsersModal } from "./BlockedUsersModal";
|
||||
import { GlobalTouchMenu } from "../GlobalTouchMenu";
|
||||
@ -329,16 +330,17 @@ export const getDataPublishesFunc = async (groupId, type) => {
|
||||
};
|
||||
|
||||
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();
|
||||
|
||||
if (nameData?.length > 0) {
|
||||
return nameData[0]?.name;
|
||||
if (nameData?.name) {
|
||||
return nameData?.name;
|
||||
} else {
|
||||
return "";
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const getGroupAdmins = async (groupNumber: number) => {
|
||||
// const validApi = await findUsableApi();
|
||||
|
||||
@ -470,6 +472,9 @@ export const Group = ({
|
||||
const { setMemberGroups, memberGroups, rootHeight, isRunningPublicNode } = useContext(MyContext);
|
||||
const lastGroupNotification = useRef<null | number>(null);
|
||||
const [timestampEnterData, setTimestampEnterData] = useState({});
|
||||
const groupsPropertiesRef = useRef({});
|
||||
const setMyGroupsWhereIAmAdmin = useSetRecoilState(myGroupsWhereIAmAdminAtom);
|
||||
|
||||
const [chatMode, setChatMode] = useState("groups");
|
||||
const [newChat, setNewChat] = useState(false);
|
||||
const [openSnack, setOpenSnack] = React.useState(false);
|
||||
@ -483,6 +488,8 @@ export const Group = ({
|
||||
const [groupAnnouncements, setGroupAnnouncements] = React.useState({});
|
||||
const [defaultThread, setDefaultThread] = React.useState(null);
|
||||
const [isOpenDrawer, setIsOpenDrawer] = React.useState(false);
|
||||
const setIsOpenBlockedUserModal = useSetRecoilState(isOpenBlockedModalAtom)
|
||||
|
||||
const [hideCommonKeyPopup, setHideCommonKeyPopup] = React.useState(false);
|
||||
const [isLoadingGroupMessage, setIsLoadingGroupMessage] = React.useState("");
|
||||
const [drawerMode, setDrawerMode] = React.useState("groups");
|
||||
@ -507,7 +514,6 @@ export const Group = ({
|
||||
const [isForceShowCreationKeyPopup, setIsForceShowCreationKeyPopup] = useState(false)
|
||||
const [groupsProperties, setGroupsProperties] = useRecoilState(groupsPropertiesAtom)
|
||||
const setUserInfoForLevels = useSetRecoilState(addressInfoControllerAtom);
|
||||
const [isOpenBlockedUserModal, setIsOpenBlockedUserModal] = React.useState(false);
|
||||
const setLastEnteredGroupIdAtom = useSetRecoilState(lastEnteredGroupIdAtom)
|
||||
const isPrivate = useMemo(()=> {
|
||||
if(selectedGroup?.groupId === '0') return false
|
||||
@ -533,6 +539,9 @@ export const Group = ({
|
||||
useEffect(()=> {
|
||||
timestampEnterDataRef.current = timestampEnterData
|
||||
}, [timestampEnterData])
|
||||
useEffect(() => {
|
||||
groupsPropertiesRef.current = groupsProperties;
|
||||
}, [groupsProperties]);
|
||||
|
||||
useEffect(() => {
|
||||
isFocusedRef.current = isFocused;
|
||||
@ -572,7 +581,7 @@ export const Group = ({
|
||||
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
@ -839,7 +848,7 @@ export const Group = ({
|
||||
const res = await fetch(
|
||||
`${getBaseApiReact()}/arbitrary/DOCUMENT_PRIVATE/${publish.name}/${
|
||||
publish.identifier
|
||||
}?encoding=base64`
|
||||
}?encoding=base64&rebuild=true`
|
||||
);
|
||||
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();
|
||||
|
||||
const findMyself = isAdminData?.members?.find(
|
||||
(member) => member.member === myAddress
|
||||
);
|
||||
|
||||
if (findMyself) {
|
||||
groupsAsAdmin.push(group);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
await Promise.all(getAllGroupsAsAdmin);
|
||||
setMyGroupsWhereIAmAdmin(groupsAsAdmin);
|
||||
} catch (error) {
|
||||
console.error();
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if(!myAddress) return
|
||||
if(areKeysEqual(groups?.map((grp)=> grp?.groupId), Object.keys(groupsProperties))){
|
||||
} else {
|
||||
getGroupsProperties(myAddress)
|
||||
if (!myAddress) return;
|
||||
if (
|
||||
!areKeysEqual(
|
||||
groups?.map((grp) => grp?.groupId),
|
||||
Object.keys(groupsPropertiesRef.current)
|
||||
)
|
||||
) {
|
||||
getGroupsProperties(myAddress);
|
||||
getGroupsWhereIAmAMember(groups);
|
||||
}
|
||||
}, [groups, myAddress])
|
||||
|
||||
}, [groups, myAddress]);
|
||||
|
||||
useEffect(() => {
|
||||
// Handler function for incoming messages
|
||||
@ -1905,6 +1949,7 @@ export const Group = ({
|
||||
width: "100%",
|
||||
justifyContent: "center",
|
||||
padding: "10px",
|
||||
gap: '10px'
|
||||
}}
|
||||
>
|
||||
<CustomButton
|
||||
@ -1922,6 +1967,23 @@ export const Group = ({
|
||||
/>
|
||||
New Chat
|
||||
</CustomButton>
|
||||
{!isRunningPublicNode && (
|
||||
<CustomButton
|
||||
onClick={() => {
|
||||
setIsOpenBlockedUserModal(true);
|
||||
}}
|
||||
sx={{
|
||||
minWidth: 'unset',
|
||||
padding: '10px',
|
||||
}}
|
||||
>
|
||||
<PersonOffIcon
|
||||
sx={{
|
||||
color: 'white',
|
||||
}}
|
||||
/>
|
||||
</CustomButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -2159,7 +2221,7 @@ export const Group = ({
|
||||
padding: '10px'
|
||||
}}
|
||||
>
|
||||
<BlockIcon
|
||||
<PersonOffIcon
|
||||
sx={{
|
||||
color: "white",
|
||||
}}
|
||||
@ -2656,11 +2718,9 @@ export const Group = ({
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{isOpenBlockedUserModal && (
|
||||
<BlockedUsersModal close={()=> {
|
||||
setIsOpenBlockedUserModal(false)
|
||||
}} />
|
||||
)}
|
||||
|
||||
<BlockedUsersModal />
|
||||
|
||||
{selectedDirect && !newChat && (
|
||||
<>
|
||||
<Box
|
||||
@ -2756,7 +2816,7 @@ export const Group = ({
|
||||
/>
|
||||
)}
|
||||
{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 && (
|
||||
<AppsDesktop toggleSideViewGroups={toggleSideViewGroups} toggleSideViewDirects={toggleSideViewDirects} goToHome={goToHome} mode={appsMode} setMode={setAppsMode} setDesktopSideView={setDesktopSideView} hasUnreadDirects={directChatHasUnread} show={desktopViewMode === "apps"} myName={userInfo?.name} isGroups={isOpenSideViewGroups}
|
||||
|
@ -17,7 +17,7 @@ import { CustomLoader } from "../../common/CustomLoader";
|
||||
import { getBaseApi } from "../../background";
|
||||
import { MyContext, getBaseApiReact, isMobile } from "../../App";
|
||||
import { myGroupsWhereIAmAdminAtom } from "../../atoms/global";
|
||||
import { useSetRecoilState } from "recoil";
|
||||
import { useRecoilState, useSetRecoilState } from "recoil";
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
||||
export const requestQueueGroupJoinRequests = new RequestQueueWithPromise(2)
|
||||
@ -28,66 +28,44 @@ export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, get
|
||||
const [groupsWithJoinRequests, setGroupsWithJoinRequests] = React.useState([])
|
||||
const [loading, setLoading] = React.useState(true)
|
||||
const {txList, setTxList} = React.useContext(MyContext)
|
||||
const setMyGroupsWhereIAmAdmin = useSetRecoilState(
|
||||
myGroupsWhereIAmAdminAtom
|
||||
);
|
||||
const [myGroupsWhereIAmAdmin] = useRecoilState(myGroupsWhereIAmAdminAtom);
|
||||
|
||||
|
||||
|
||||
const getJoinRequests = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
|
||||
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()
|
||||
|
||||
|
||||
const findMyself = isAdminData?.members?.find((member)=> member.member === myAddress)
|
||||
|
||||
if(findMyself){
|
||||
groupsAsAdmin.push(group)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
|
||||
await Promise.all(getAllGroupsAsAdmin)
|
||||
setMyGroupsWhereIAmAdmin(groupsAsAdmin)
|
||||
const res = await Promise.all(groupsAsAdmin.map(async (group)=> {
|
||||
|
||||
const joinRequestResponse = await requestQueueGroupJoinRequests.enqueue(()=> {
|
||||
setLoading(true);
|
||||
const res = await Promise.all(
|
||||
myGroupsWhereIAmAdmin.map(async (group) => {
|
||||
const joinRequestResponse =
|
||||
await requestQueueGroupJoinRequests.enqueue(() => {
|
||||
return fetch(
|
||||
`${getBaseApiReact()}/groups/joinrequests/${group.groupId}`
|
||||
);
|
||||
})
|
||||
});
|
||||
|
||||
const joinRequestData = await joinRequestResponse.json()
|
||||
const joinRequestData = await joinRequestResponse.json();
|
||||
return {
|
||||
group,
|
||||
data: joinRequestData
|
||||
}
|
||||
}))
|
||||
setGroupsWithJoinRequests(res)
|
||||
data: joinRequestData,
|
||||
};
|
||||
})
|
||||
);
|
||||
setGroupsWithJoinRequests(res);
|
||||
} catch (error) {
|
||||
|
||||
console.log(error);
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (myAddress && groups.length > 0) {
|
||||
getJoinRequests()
|
||||
if (myAddress && myGroupsWhereIAmAdmin.length > 0) {
|
||||
getJoinRequests();
|
||||
} else {
|
||||
setLoading(false)
|
||||
setLoading(false);
|
||||
}
|
||||
}, [myAddress, groups]);
|
||||
}, [myAddress, myGroupsWhereIAmAdmin]);
|
||||
|
||||
const filteredJoinRequests = React.useMemo(()=> {
|
||||
return groupsWithJoinRequests.map((group)=> {
|
||||
|
@ -226,6 +226,7 @@ export const ListOfGroupPromotions = () => {
|
||||
data: data,
|
||||
identifier: identifier,
|
||||
service: "DOCUMENT",
|
||||
uploadType: 'base64',
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response?.error) {
|
||||
|
@ -54,35 +54,19 @@ export const NewUsersCTA = ({ balance }) => {
|
||||
textDecoration: "underline",
|
||||
}}
|
||||
onClick={() => {
|
||||
if (chrome && chrome.tabs) {
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.open("https://link.qortal.dev/support", '_system')
|
||||
}}
|
||||
>
|
||||
Telegram
|
||||
Nextcloud
|
||||
</ButtonBase>
|
||||
<ButtonBase
|
||||
sx={{
|
||||
textDecoration: "underline",
|
||||
}}
|
||||
onClick={() => {
|
||||
if (chrome && chrome.tabs) {
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
window.open("https://link.qortal.dev/discord-invite", '_system')
|
||||
}}
|
||||
|
||||
>
|
||||
Discord
|
||||
</ButtonBase>
|
||||
|
@ -67,6 +67,7 @@ const [isLoading, setIsLoading] = useState(false)
|
||||
data: avatarBase64,
|
||||
identifier: "qortal_avatar",
|
||||
service: "THUMBNAIL",
|
||||
uploadType: 'base64',
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response?.error) {
|
||||
|
@ -89,14 +89,14 @@ export const Minting = ({
|
||||
const getName = async (address) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${getBaseApiReact()}/names/address/${address}`
|
||||
`${getBaseApiReact()}/names/primary/${address}`
|
||||
);
|
||||
const nameData = await response.json();
|
||||
if (nameData?.length > 0) {
|
||||
if (nameData?.name) {
|
||||
setNames((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
[address]: nameData[0].name,
|
||||
[address]: nameData?.name,
|
||||
};
|
||||
});
|
||||
} else {
|
||||
@ -108,7 +108,7 @@ export const Minting = ({
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// error
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -57,12 +57,13 @@ export const RegisterName = ({setOpenSnack, setInfoSnack, userInfo, show, setTxL
|
||||
try {
|
||||
const res = await fetch(`${getBaseApiReact()}/names/` + name);
|
||||
const data = await res.json()
|
||||
if(data?.message === 'name unknown'){
|
||||
if(data?.message === 'name unknown' || data?.error){
|
||||
setIsNameAvailable(Availability.AVAILABLE)
|
||||
} else {
|
||||
setIsNameAvailable(Availability.NOT_AVAILABLE)
|
||||
}
|
||||
} catch (error) {
|
||||
setIsNameAvailable(Availability.AVAILABLE)
|
||||
console.error(error)
|
||||
} finally {
|
||||
}
|
||||
|
@ -155,6 +155,7 @@ export const Save = ({ isDesktop, disableWidth, myName }) => {
|
||||
data: encryptData,
|
||||
identifier: "ext_saved_settings",
|
||||
service: "DOCUMENT_PRIVATE",
|
||||
uploadType: 'base64',
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response?.error) {
|
||||
|
@ -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 {
|
||||
Avatar,
|
||||
@ -16,6 +16,7 @@ import {
|
||||
Typography,
|
||||
Table,
|
||||
CircularProgress,
|
||||
Autocomplete,
|
||||
} from "@mui/material";
|
||||
import { getAddressInfo, getNameOrAddress } from "../../background";
|
||||
import { getBaseApiReact } from "../../App";
|
||||
@ -26,6 +27,8 @@ import { formatTimestamp } from "../../utils/time";
|
||||
import CloseFullscreenIcon from '@mui/icons-material/CloseFullscreen';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import { executeEvent, subscribeToEvent, unsubscribeFromEvent } from "../../utils/events";
|
||||
import { useNameSearch } from "../../hooks/useNameSearch";
|
||||
import { validateAddress } from "../../utils/validateAddress";
|
||||
|
||||
function formatAddress(str) {
|
||||
if (str.length <= 12) return str;
|
||||
@ -38,6 +41,13 @@ function formatAddress(str) {
|
||||
|
||||
export const UserLookup = ({ isOpenDrawerLookup, setIsOpenDrawerLookup }) => {
|
||||
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 [addressInfo, setAddressInfo] = useState(null);
|
||||
const [isLoadingUser, setIsLoadingUser] = useState(false);
|
||||
@ -58,7 +68,10 @@ export const UserLookup = ({ isOpenDrawerLookup, setIsOpenDrawerLookup }) => {
|
||||
if (!addressInfoRes?.publicKey) {
|
||||
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(
|
||||
`${getBaseApiReact()}/addresses/balance/${owner}`
|
||||
);
|
||||
@ -106,6 +119,7 @@ export const UserLookup = ({ isOpenDrawerLookup, setIsOpenDrawerLookup }) => {
|
||||
setIsOpenDrawerLookup(false)
|
||||
setNameOrAddress('')
|
||||
setErrorMessage('')
|
||||
setInputValue('');
|
||||
setPayments([])
|
||||
setIsLoadingUser(false)
|
||||
setIsLoadingPayments(false)
|
||||
@ -134,27 +148,66 @@ export const UserLookup = ({ isOpenDrawerLookup, setIsOpenDrawerLookup }) => {
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<Autocomplete
|
||||
value={nameOrAddress}
|
||||
onChange={(event: any, newValue: string | null) => {
|
||||
if (!newValue) {
|
||||
setNameOrAddress('');
|
||||
return;
|
||||
}
|
||||
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
|
||||
value={nameOrAddress}
|
||||
onChange={(e) => setNameOrAddress(e.target.value)}
|
||||
size="small"
|
||||
placeholder="Address or Name"
|
||||
autoComplete="off"
|
||||
{...params}
|
||||
label="Address or Name"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && nameOrAddress) {
|
||||
lookupFunc();
|
||||
if (e.key === 'Enter' && inputValue) {
|
||||
lookupFunc(inputValue);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<ButtonBase onClick={()=> {
|
||||
lookupFunc();
|
||||
}} >
|
||||
<SearchIcon sx={{
|
||||
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-root': {
|
||||
'& fieldset': {
|
||||
borderColor: 'white',
|
||||
},
|
||||
'&:hover fieldset': {
|
||||
borderColor: 'white',
|
||||
},
|
||||
'&.Mui-focused fieldset': {
|
||||
borderColor: 'white',
|
||||
},
|
||||
'& input': {
|
||||
color: 'white',
|
||||
marginRight: '20px'
|
||||
}} />
|
||||
</ButtonBase>
|
||||
},
|
||||
},
|
||||
'& .MuiInputLabel-root': {
|
||||
color: 'white',
|
||||
},
|
||||
'& .MuiInputLabel-root.Mui-focused': {
|
||||
color: 'white',
|
||||
},
|
||||
'& .MuiAutocomplete-endAdornment svg': {
|
||||
color: 'white',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<ButtonBase sx={{
|
||||
marginLeft: 'auto',
|
||||
|
||||
|
@ -167,12 +167,9 @@ useEffect(()=> {
|
||||
onClick={async () => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
if(isAlreadyBlocked === true){
|
||||
await removeBlockFromList(address, name)
|
||||
} else if(isAlreadyBlocked === false) {
|
||||
await addToBlockList(address, name)
|
||||
}
|
||||
executeEvent('updateChatMessagesWithBlocks', true)
|
||||
executeEvent("blockUserFromOutside", {
|
||||
user: address
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
|
@ -173,3 +173,6 @@ const STATIC_BCRYPT_SALT = `$${BCRYPT_VERSION}$${BCRYPT_ROUNDS}$IxVE941tXVUD4cW0
|
||||
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 const MAX_SIZE_PUBLIC_NODE = 500 * 1024 * 1024; // 500mb
|
||||
export const MAX_SIZE_PUBLISH = 2000 * 1024 * 1024; // 2GB
|
55
src/hooks/useNameSearch.tsx
Normal file
55
src/hooks/useNameSearch.tsx
Normal 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,
|
||||
};
|
||||
};
|
11
src/hooks/useSortedMyNames.tsx
Normal file
11
src/hooks/useSortedMyNames.tsx
Normal 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]);
|
||||
}
|
@ -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) => {
|
||||
const requestId = generateRequestId(); // Unique ID for each request
|
||||
callbackMap.set(requestId, { resolve, reject }); // Store both resolve and reject callbacks
|
||||
|
@ -1,55 +1,136 @@
|
||||
// @ts-nocheck
|
||||
|
||||
import { Buffer } from "buffer"
|
||||
import Base58 from "../../deps/Base58"
|
||||
import nacl from "../../deps/nacl-fast"
|
||||
import utils from "../../utils/utils"
|
||||
import { createEndpoint, getBaseApi } from "../../background";
|
||||
import { getData } from "../../utils/chromeStorage";
|
||||
import { Buffer } from 'buffer';
|
||||
import Base58 from '../../deps/Base58';
|
||||
import nacl from '../../deps/nacl-fast';
|
||||
import utils from '../../utils/utils';
|
||||
import { createEndpoint, getBaseApi } from '../../background';
|
||||
import { getData } from '../../utils/chromeStorage';
|
||||
import { executeEvent } from '../../utils/events';
|
||||
|
||||
export async function reusableGet(endpoint) {
|
||||
const validApi = await getBaseApi();
|
||||
|
||||
const response = await fetch(validApi + endpoint);
|
||||
const data = await response.json();
|
||||
return data
|
||||
return data;
|
||||
}
|
||||
|
||||
async function reusablePost(endpoint, _body) {
|
||||
// const validApi = await findUsableApi();
|
||||
const url = await createEndpoint(endpoint)
|
||||
const url = await createEndpoint(endpoint);
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: _body
|
||||
body: _body,
|
||||
});
|
||||
let data
|
||||
try {
|
||||
data = await response.clone().json()
|
||||
} catch (e) {
|
||||
data = await response.text()
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(errorText);
|
||||
}
|
||||
let data;
|
||||
try {
|
||||
data = await response.clone().json();
|
||||
} catch (e) {
|
||||
data = await response.text();
|
||||
}
|
||||
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));
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
async function getKeyPair() {
|
||||
const res = await getData<any>("keyPair").catch(() => null);
|
||||
const res = await getData<any>('keyPair').catch(() => null);
|
||||
if (res) {
|
||||
return res
|
||||
return res;
|
||||
} else {
|
||||
throw new Error("Wallet not authenticated");
|
||||
throw new Error('Wallet not authenticated');
|
||||
}
|
||||
}
|
||||
|
||||
export const publishData = async ({
|
||||
registeredName,
|
||||
file,
|
||||
data,
|
||||
service,
|
||||
identifier,
|
||||
uploadType,
|
||||
isBase64,
|
||||
filename,
|
||||
withFee,
|
||||
title,
|
||||
@ -60,206 +141,334 @@ export const publishData = async ({
|
||||
tag3,
|
||||
tag4,
|
||||
tag5,
|
||||
feeAmount
|
||||
feeAmount,
|
||||
appInfo
|
||||
}: any) => {
|
||||
|
||||
const validateName = async (receiverName: string) => {
|
||||
return await reusableGet(`/names/${receiverName}`)
|
||||
}
|
||||
return await reusableGet(`/names/${receiverName}`);
|
||||
};
|
||||
|
||||
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 timestamp = Date.now()
|
||||
const timestamp = Date.now();
|
||||
|
||||
let fee = await reusableGet(`/transactions/unitfee?txType=ARBITRARY×tamp=${timestamp}`)
|
||||
let fee = await reusableGet(
|
||||
`/transactions/unitfee?txType=ARBITRARY×tamp=${timestamp}`
|
||||
);
|
||||
|
||||
return {
|
||||
timestamp,
|
||||
fee: Number(fee),
|
||||
feeToShow: (Number(fee) / 1e8).toFixed(8)
|
||||
}
|
||||
}
|
||||
feeToShow: (Number(fee) / 1e8).toFixed(8),
|
||||
};
|
||||
};
|
||||
|
||||
const signArbitraryWithFee = (arbitraryBytesBase58, arbitraryBytesForSigningBase58, keyPair) => {
|
||||
const signArbitraryWithFee = (
|
||||
arbitraryBytesBase58,
|
||||
arbitraryBytesForSigningBase58,
|
||||
keyPair
|
||||
) => {
|
||||
if (!arbitraryBytesBase58) {
|
||||
throw new Error('ArbitraryBytesBase58 not defined')
|
||||
throw new Error('ArbitraryBytesBase58 not defined');
|
||||
}
|
||||
|
||||
if (!keyPair) {
|
||||
throw new Error('keyPair not defined')
|
||||
throw new Error('keyPair 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 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) => {
|
||||
|
||||
return await reusablePost('/transactions/process?apiVersion=2', Base58.encode(bytes))
|
||||
}
|
||||
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
|
||||
)
|
||||
);
|
||||
|
||||
if (convertedBytesBase58.error) {
|
||||
throw new Error('Error when signing')
|
||||
throw new Error('Error when signing');
|
||||
}
|
||||
|
||||
|
||||
const resKeyPair = await getKeyPair()
|
||||
const parsedData = resKeyPair
|
||||
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
|
||||
publicKey: uint8PublicKey,
|
||||
};
|
||||
|
||||
let signedArbitraryBytes = signArbitraryWithFee(transactionBytesBase58, convertedBytesBase58, keyPair)
|
||||
const response = await processTransactionVersion2(signedArbitraryBytes)
|
||||
let signedArbitraryBytes = signArbitraryWithFee(
|
||||
transactionBytesBase58,
|
||||
convertedBytesBase58,
|
||||
keyPair
|
||||
);
|
||||
const response = await processTransactionVersion2(signedArbitraryBytes);
|
||||
|
||||
let myResponse = { error: '' }
|
||||
let myResponse = { error: '' };
|
||||
|
||||
if (response === false) {
|
||||
throw new Error('Error when signing')
|
||||
throw new Error('Error when signing');
|
||||
} else {
|
||||
myResponse = response
|
||||
myResponse = response;
|
||||
}
|
||||
|
||||
return myResponse
|
||||
if (appInfo?.tabId) {
|
||||
executeEvent('receiveChunks', {
|
||||
tabId: appInfo.tabId,
|
||||
publishLocation: {
|
||||
name: registeredName,
|
||||
identifier,
|
||||
service,
|
||||
},
|
||||
processed: true,
|
||||
});
|
||||
}
|
||||
return myResponse;
|
||||
};
|
||||
|
||||
const validate = async () => {
|
||||
let validNameRes = await validateName(registeredName)
|
||||
let validNameRes = await validateName(registeredName);
|
||||
|
||||
if (validNameRes.error) {
|
||||
throw new Error('Name not found')
|
||||
throw new Error('Name not found');
|
||||
}
|
||||
|
||||
let fee = null
|
||||
let fee = null;
|
||||
|
||||
if (withFee && feeAmount) {
|
||||
fee = feeAmount
|
||||
fee = feeAmount;
|
||||
} else if (withFee) {
|
||||
const res = await getArbitraryFee()
|
||||
const res = await getArbitraryFee();
|
||||
if (res.fee) {
|
||||
fee = res.fee
|
||||
fee = res.fee;
|
||||
} else {
|
||||
throw new Error('unable to get fee')
|
||||
throw new Error('unable to get fee');
|
||||
}
|
||||
}
|
||||
|
||||
let transactionBytes = await uploadData(registeredName, file, fee)
|
||||
let transactionBytes = await uploadData(registeredName, data, fee);
|
||||
if (!transactionBytes || transactionBytes.error) {
|
||||
throw new Error(transactionBytes?.message || 'Error when uploading')
|
||||
throw new Error(transactionBytes?.message || 'Error when uploading');
|
||||
} else if (transactionBytes.includes('Error 500 Internal Server Error')) {
|
||||
throw new Error('Error when uploading')
|
||||
throw new Error('Error when uploading');
|
||||
}
|
||||
|
||||
let signAndProcessRes
|
||||
let signAndProcessRes;
|
||||
|
||||
if (withFee) {
|
||||
signAndProcessRes = await signAndProcessWithFee(transactionBytes)
|
||||
signAndProcessRes = await signAndProcessWithFee(transactionBytes);
|
||||
}
|
||||
|
||||
if (signAndProcessRes?.error) {
|
||||
throw new Error('Error when signing')
|
||||
throw new Error('Error when signing');
|
||||
}
|
||||
|
||||
return signAndProcessRes
|
||||
return signAndProcessRes;
|
||||
};
|
||||
|
||||
const uploadData = async (registeredName: string, data: any, fee: number) => {
|
||||
let postBody = '';
|
||||
let urlSuffix = '';
|
||||
|
||||
if (data != null) {
|
||||
if (uploadType === 'base64') {
|
||||
urlSuffix = '/base64';
|
||||
}
|
||||
|
||||
const uploadData = async (registeredName: string, file:any, fee: number) => {
|
||||
|
||||
let postBody = ''
|
||||
let urlSuffix = ''
|
||||
|
||||
if (file != null) {
|
||||
// If we're sending zipped data, make sure to use the /zip version of the POST /arbitrary/* API
|
||||
if (uploadType === 'zip') {
|
||||
urlSuffix = '/zip'
|
||||
if (uploadType === 'base64') {
|
||||
postBody = data;
|
||||
}
|
||||
} else {
|
||||
throw new Error('No data provided');
|
||||
}
|
||||
|
||||
// If we're sending file data, use the /base64 version of the POST /arbitrary/* API
|
||||
else if (uploadType === 'file') {
|
||||
urlSuffix = '/base64'
|
||||
}
|
||||
|
||||
// Base64 encode the file to work around compatibility issues between javascript and java byte arrays
|
||||
if (isBase64) {
|
||||
postBody = file
|
||||
}
|
||||
|
||||
if (!isBase64) {
|
||||
let fileBuffer = new Uint8Array(await file.arrayBuffer())
|
||||
postBody = Buffer.from(fileBuffer).toString("base64")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
let uploadDataUrl = `/arbitrary/${service}/${registeredName}${urlSuffix}`
|
||||
let uploadDataUrl = `/arbitrary/${service}/${registeredName}`;
|
||||
let paramQueries = '';
|
||||
if (identifier?.trim().length > 0) {
|
||||
uploadDataUrl = `/arbitrary/${service}/${registeredName}/${identifier}${urlSuffix}`
|
||||
uploadDataUrl = `/arbitrary/${service}/${registeredName}/${identifier}`;
|
||||
}
|
||||
|
||||
uploadDataUrl = uploadDataUrl + `?fee=${fee}`
|
||||
|
||||
paramQueries = paramQueries + `?fee=${fee}`;
|
||||
|
||||
if (filename != null && filename != 'undefined') {
|
||||
uploadDataUrl = uploadDataUrl + '&filename=' + encodeURIComponent(filename)
|
||||
paramQueries = paramQueries + '&filename=' + encodeURIComponent(filename);
|
||||
}
|
||||
|
||||
if (title != null && title != 'undefined') {
|
||||
uploadDataUrl = uploadDataUrl + '&title=' + encodeURIComponent(title)
|
||||
paramQueries = paramQueries + '&title=' + encodeURIComponent(title);
|
||||
}
|
||||
|
||||
if (description != null && description != 'undefined') {
|
||||
uploadDataUrl = uploadDataUrl + '&description=' + encodeURIComponent(description)
|
||||
paramQueries =
|
||||
paramQueries + '&description=' + encodeURIComponent(description);
|
||||
}
|
||||
|
||||
if (category != null && category != 'undefined') {
|
||||
uploadDataUrl = uploadDataUrl + '&category=' + encodeURIComponent(category)
|
||||
paramQueries = paramQueries + '&category=' + encodeURIComponent(category);
|
||||
}
|
||||
|
||||
if (tag1 != null && tag1 != 'undefined') {
|
||||
uploadDataUrl = uploadDataUrl + '&tags=' + encodeURIComponent(tag1)
|
||||
paramQueries = paramQueries + '&tags=' + encodeURIComponent(tag1);
|
||||
}
|
||||
|
||||
if (tag2 != null && tag2 != 'undefined') {
|
||||
uploadDataUrl = uploadDataUrl + '&tags=' + encodeURIComponent(tag2)
|
||||
paramQueries = paramQueries + '&tags=' + encodeURIComponent(tag2);
|
||||
}
|
||||
|
||||
if (tag3 != null && tag3 != 'undefined') {
|
||||
uploadDataUrl = uploadDataUrl + '&tags=' + encodeURIComponent(tag3)
|
||||
paramQueries = paramQueries + '&tags=' + encodeURIComponent(tag3);
|
||||
}
|
||||
|
||||
if (tag4 != null && tag4 != 'undefined') {
|
||||
uploadDataUrl = uploadDataUrl + '&tags=' + encodeURIComponent(tag4)
|
||||
paramQueries = paramQueries + '&tags=' + encodeURIComponent(tag4);
|
||||
}
|
||||
|
||||
if (tag5 != null && tag5 != 'undefined') {
|
||||
uploadDataUrl = uploadDataUrl + '&tags=' + encodeURIComponent(tag5)
|
||||
paramQueries = paramQueries + '&tags=' + encodeURIComponent(tag5);
|
||||
}
|
||||
if (uploadType === 'zip') {
|
||||
paramQueries = paramQueries + '&isZip=' + true;
|
||||
}
|
||||
|
||||
return await reusablePost(uploadDataUrl, postBody)
|
||||
if (uploadType === 'base64') {
|
||||
if (urlSuffix) {
|
||||
uploadDataUrl = uploadDataUrl + urlSuffix;
|
||||
}
|
||||
uploadDataUrl = uploadDataUrl + paramQueries;
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
const file = data;
|
||||
const urlCheck = `/arbitrary/check/tmp?totalSize=${file.size}`;
|
||||
|
||||
const checkEndpoint = await createEndpoint(urlCheck);
|
||||
const checkRes = await fetch(checkEndpoint);
|
||||
if (!checkRes.ok) {
|
||||
throw new Error('Not enough space on your hard drive');
|
||||
}
|
||||
|
||||
const chunkUrl = uploadDataUrl + `/chunk`;
|
||||
const chunkSize = 5 * 1024 * 1024; // 5MB
|
||||
|
||||
const totalChunks = Math.ceil(file.size / chunkSize);
|
||||
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()
|
||||
return await validate();
|
||||
} catch (error: any) {
|
||||
throw new Error(error?.message)
|
||||
}
|
||||
throw new Error(error?.message);
|
||||
}
|
||||
};
|
@ -1,9 +1,10 @@
|
||||
import { gateways, getApiKeyFromStorage } from "./background";
|
||||
import { gateways, getApiKeyFromStorage, getNameInfoForOthers } from "./background";
|
||||
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 { executeEvent } from "./utils/events";
|
||||
|
||||
import { ScreenOrientation } from '@capacitor/screen-orientation';
|
||||
|
||||
|
||||
function getLocalStorage(key) {
|
||||
@ -200,7 +201,7 @@ export const isRunningGateway = async ()=> {
|
||||
|
||||
case "PUBLISH_QDN_RESOURCE": {
|
||||
try {
|
||||
const res = await publishQDNResource(request.payload, event.source, isFromExtension);
|
||||
const res = await publishQDNResource(request.payload, event.source, isFromExtension, appInfo);
|
||||
event.source.postMessage({
|
||||
requestId: request.requestId,
|
||||
action: request.action,
|
||||
@ -220,7 +221,7 @@ export const isRunningGateway = async ()=> {
|
||||
|
||||
case "PUBLISH_MULTIPLE_QDN_RESOURCES": {
|
||||
try {
|
||||
const res = await publishMultipleQDNResources(request.payload, event.source, isFromExtension);
|
||||
const res = await publishMultipleQDNResources(request.payload, event.source, isFromExtension, appInfo);
|
||||
event.source.postMessage({
|
||||
requestId: request.requestId,
|
||||
action: request.action,
|
||||
@ -462,7 +463,7 @@ export const isRunningGateway = async ()=> {
|
||||
|
||||
case "UPDATE_FOREIGN_FEE": {
|
||||
try {
|
||||
const res = await updateForeignFee(request.payload);
|
||||
const res = await updateForeignFee(request.payload, isFromExtension);
|
||||
event.source.postMessage({
|
||||
requestId: request.requestId,
|
||||
action: request.action,
|
||||
@ -502,7 +503,7 @@ export const isRunningGateway = async ()=> {
|
||||
|
||||
case "SET_CURRENT_FOREIGN_SERVER": {
|
||||
try {
|
||||
const res = await setCurrentForeignServer(request.payload);
|
||||
const res = await setCurrentForeignServer(request.payload, isFromExtension);
|
||||
event.source.postMessage({
|
||||
requestId: request.requestId,
|
||||
action: request.action,
|
||||
@ -522,7 +523,7 @@ export const isRunningGateway = async ()=> {
|
||||
|
||||
case "ADD_FOREIGN_SERVER": {
|
||||
try {
|
||||
const res = await addForeignServer(request.payload);
|
||||
const res = await addForeignServer(request.payload, isFromExtension);
|
||||
event.source.postMessage({
|
||||
requestId: request.requestId,
|
||||
action: request.action,
|
||||
@ -542,7 +543,7 @@ export const isRunningGateway = async ()=> {
|
||||
|
||||
case "REMOVE_FOREIGN_SERVER": {
|
||||
try {
|
||||
const res = await removeForeignServer(request.payload);
|
||||
const res = await removeForeignServer(request.payload, isFromExtension);
|
||||
event.source.postMessage({
|
||||
requestId: request.requestId,
|
||||
action: request.action,
|
||||
@ -620,6 +621,32 @@ export const isRunningGateway = async ()=> {
|
||||
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": {
|
||||
try {
|
||||
const res = await cancelSellOrder(request.payload, isFromExtension);
|
||||
@ -1206,6 +1233,206 @@ export const isRunningGateway = async ()=> {
|
||||
}
|
||||
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:
|
||||
break;
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
45
src/transactions/BuyNameTransacion.ts
Normal file
45
src/transactions/BuyNameTransacion.ts
Normal 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
|
||||
}
|
||||
}
|
33
src/transactions/CancelSellNameTransacion.ts
Normal file
33
src/transactions/CancelSellNameTransacion.ts
Normal 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
|
||||
}
|
||||
}
|
40
src/transactions/SellNameTransacion.ts
Normal file
40
src/transactions/SellNameTransacion.ts
Normal 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
|
||||
}
|
||||
}
|
35
src/transactions/TransferAssetTransaction.ts
Normal file
35
src/transactions/TransferAssetTransaction.ts
Normal 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
|
||||
}
|
||||
}
|
62
src/transactions/UpdateGroupTransaction.ts
Normal file
62
src/transactions/UpdateGroupTransaction.ts
Normal 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
|
||||
}
|
||||
}
|
@ -20,17 +20,27 @@ import DeployAtTransaction from './DeployAtTransaction.js'
|
||||
import RewardShareTransaction from './RewardShareTransaction.js'
|
||||
import RemoveRewardShareTransaction from './RemoveRewardShareTransaction.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 = {
|
||||
3: RegisterNameTransaction,
|
||||
4: UpdateNameTransaction,
|
||||
2: PaymentTransaction,
|
||||
5: SellNameTransacion,
|
||||
6: CancelSellNameTransacion,
|
||||
7: BuyNameTransacion,
|
||||
8: CreatePollTransaction,
|
||||
9: VoteOnPollTransaction,
|
||||
12: TransferAssetTransaction,
|
||||
16: DeployAtTransaction,
|
||||
18: ChatTransaction,
|
||||
181: GroupChatTransaction,
|
||||
22: CreateGroupTransaction,
|
||||
23: UpdateGroupTransaction,
|
||||
24: AddGroupAdminTransaction,
|
||||
25: RemoveGroupAdminTransaction,
|
||||
26: GroupBanTransaction,
|
||||
|
26
src/utils/chat.ts
Normal file
26
src/utils/chat.ts
Normal 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×tamp=${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());
|
||||
}
|
@ -14,3 +14,18 @@ export function decodeIfEncoded(input) {
|
||||
// Return input as-is if not URI-encoded
|
||||
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;
|
||||
}
|
||||
};
|
@ -43,14 +43,15 @@ export function formatTimestamp(timestamp: number): string {
|
||||
// Both have timestamp, sort by timestamp descending
|
||||
return b.timestamp - a.timestamp;
|
||||
} else if (a.timestamp) {
|
||||
// Only `a` has timestamp, it comes first
|
||||
return -1;
|
||||
} else if (b.timestamp) {
|
||||
// Only `b` has timestamp, it comes first
|
||||
return 1;
|
||||
} else {
|
||||
// Neither has timestamp, sort alphabetically by groupName
|
||||
return a.groupName.localeCompare(b.groupName);
|
||||
// Neither has timestamp, sort alphabetically by groupName (with fallback)
|
||||
const nameA = a.groupName || '';
|
||||
const nameB = b.groupName || '';
|
||||
return nameA.localeCompare(nameB);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user