feat(gui): Display developer responses to feedback (#302)

This commit is contained in:
Mohan 2025-04-28 13:12:43 +02:00 committed by GitHub
parent f1e5cdfbfe
commit 53a994e6dc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 1216 additions and 45 deletions

View file

@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
- GUI: Feedback submitted can be responded to by the core developers. The responses will be displayed under the "Feedback" tab.
## [1.0.0-rc.17] - 2025-04-18
- GUI: The user will now be asked to approve the swap offer again before the Bitcoin lock transaction is published. Makers should take care to only assume a swap has been accepted by the taker if the Bitcoin lock transaction is detected (`Advancing state state=bitcoin lock transaction in mempool ...`). Swaps that have been safely aborted will not be displayed in the GUI anymore.

View file

@ -38,3 +38,25 @@ You can also check whether the current bindings are up to date:
```bash
yarn run check-bindings
```
## Debugging
Because the GUI is running in an embedded browser, we can't use the usual Browser extensions to debug the GUI. Instead we use standalone React DevTools / Redux DevTools.
### React DevTools
Run this command to start the React DevTools server. The frontend will connect to this server automatically:
```bash
npx react-devtools
```
### Redux DevTools
Run this command to start the Redux DevTools server. The frontend will connect to this server automatically. You can then debug the global Redux state. Observe how it changes over time, go back in time, see dispatch history, etc.
You may have to go to `Settings -> 'use local custom server' -> connect` inside the devtools window for the state to be reflected correctly.
```bash
npx redux-devtools --hostname=localhost --port=8098 --open
```

View file

@ -47,6 +47,7 @@
},
"devDependencies": {
"@eslint/js": "^9.9.0",
"@redux-devtools/remote": "^0.9.5",
"@tauri-apps/cli": "^2.0.0",
"@testing-library/react": "^16.0.1",
"@testing-library/user-event": "^14.5.2",

View file

@ -26,3 +26,49 @@ export interface Alert {
body: string;
severity: "info" | "warning" | "error";
}
// Define the correct 9-element tuple type for PrimitiveDateTime
export type PrimitiveDateTimeString = [
number, // Year
number, // Day of Year
number, // Hour
number, // Minute
number, // Second
number, // Nanosecond
number, // Offset Hour
number, // Offset Minute
number // Offset Second
];
export interface Feedback {
id: string;
created_at: PrimitiveDateTimeString;
}
export interface Attachment {
id: number;
message_id: number;
key: string;
content: string;
created_at: PrimitiveDateTimeString;
}
export interface Message {
id: number;
feedback_id: string;
is_from_staff: boolean;
content: string;
created_at: PrimitiveDateTimeString;
attachments?: Attachment[];
}
export interface MessageWithAttachments {
message: Message;
attachments: Attachment[];
}
// Define type for Attachment data in request body
export interface AttachmentInput {
key: string;
content: string;
}

View file

@ -5,13 +5,14 @@
// - and to submit feedback
// - fetch currency rates from CoinGecko
import { Alert, ExtendedMakerStatus } from "models/apiModel";
import { Alert, Attachment, AttachmentInput, ExtendedMakerStatus, Feedback, Message, MessageWithAttachments, PrimitiveDateTimeString } from "models/apiModel";
import { store } from "./store/storeRenderer";
import { setBtcPrice, setXmrBtcRate, setXmrPrice } from "store/features/ratesSlice";
import { FiatCurrency } from "store/features/settingsSlice";
import { setAlerts } from "store/features/alertsSlice";
import { registryConnectionFailed, setRegistryMakers } from "store/features/makersSlice";
import logger from "utils/logger";
import { setConversation } from "store/features/conversationsSlice";
const PUBLIC_REGISTRY_API_BASE_URL = "https://api.unstoppableswap.net";
@ -28,11 +29,14 @@ async function fetchAlertsViaHttp(): Promise<Alert[]> {
}
export async function submitFeedbackViaHttp(
body: string,
attachedData: string,
content: string,
attachments?: AttachmentInput[]
): Promise<string> {
type Response = {
feedbackId: string;
type Response = string;
const requestPayload = {
body: content,
attachments: attachments || [], // Ensure attachments is always an array
};
const response = await fetch(`${PUBLIC_REGISTRY_API_BASE_URL}/api/submit-feedback`, {
@ -40,16 +44,56 @@ export async function submitFeedbackViaHttp(
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ body, attachedData }),
body: JSON.stringify(requestPayload), // Send the corrected structure
});
if (!response.ok) {
throw new Error(`Status: ${response.status}`);
const errorBody = await response.text();
throw new Error(`Failed to submit feedback. Status: ${response.status}. Body: ${errorBody}`);
}
const responseBody = (await response.json()) as Response;
return responseBody;
}
return responseBody.feedbackId;
export async function fetchFeedbackMessagesViaHttp(feedbackId: string): Promise<Message[]> {
const response = await fetch(`${PUBLIC_REGISTRY_API_BASE_URL}/api/feedback/${feedbackId}/messages`);
if (!response.ok) {
const errorBody = await response.text();
throw new Error(`Failed to fetch messages for feedback ${feedbackId}. Status: ${response.status}. Body: ${errorBody}`);
}
// Assuming the response is directly the Message[] array including attachments
return (await response.json()) as Message[];
}
export async function appendFeedbackMessageViaHttp(
feedbackId: string,
content: string,
attachments?: AttachmentInput[]
): Promise<number> {
type Response = number;
const body = {
feedback_id: feedbackId,
content,
attachments: attachments || [], // Ensure attachments is always an array
};
const response = await fetch(`${PUBLIC_REGISTRY_API_BASE_URL}/api/append-feedback-message`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(body), // Send new structure
});
if (!response.ok) {
const errorBody = await response.text();
throw new Error(`Failed to append message for feedback ${feedbackId}. Status: ${response.status}. Body: ${errorBody}`);
}
const responseBody = (await response.json()) as Response;
return responseBody;
}
async function fetchCurrencyPrice(currency: string, fiatCurrency: FiatCurrency): Promise<number> {
@ -74,7 +118,6 @@ async function fetchXmrBtcRate(): Promise<number> {
return lastTradePrice;
}
function fetchBtcPrice(fiatCurrency: FiatCurrency): Promise<number> {
return fetchCurrencyPrice("bitcoin", fiatCurrency);
}
@ -108,7 +151,6 @@ export async function updateRates(): Promise<void> {
}
}
/**
* Update public registry
*/
@ -127,4 +169,25 @@ export async function updatePublicRegistry(): Promise<void> {
} catch (error) {
logger.error(error, "Error fetching alerts");
}
}
/**
* Fetch all conversations
* Goes through all feedback ids and fetches all the messages for each feedback id
*/
export async function fetchAllConversations(): Promise<void> {
const feedbackIds = store.getState().conversations.knownFeedbackIds;
console.log("Fetching all conversations", feedbackIds);
for (const feedbackId of feedbackIds) {
try {
console.log("Fetching messages for feedback id", feedbackId);
const messages = await fetchFeedbackMessagesViaHttp(feedbackId);
console.log("Fetched messages for feedback id", feedbackId, messages);
store.dispatch(setConversation({ feedbackId, messages }));
} catch (error) {
logger.error(error, "Error fetching messages for feedback id", feedbackId);
}
}
}

View file

@ -3,7 +3,7 @@ import { TauriSwapProgressEventWrapper, TauriContextStatusEvent, TauriLogEvent,
import { contextStatusEventReceived, receivedCliLog, rpcSetBalance, timelockChangeEventReceived, rpcSetBackgroundRefundState, approvalEventReceived } from "store/features/rpcSlice";
import { swapProgressEventReceived } from "store/features/swapSlice";
import logger from "utils/logger";
import { updatePublicRegistry, updateRates } from "./api";
import { fetchAllConversations, updatePublicRegistry, updateRates } from "./api";
import { checkContextAvailability, getSwapInfo, initializeContext, updateAllNodeStatuses } from "./rpc";
import { store } from "./store/storeRenderer";
@ -16,6 +16,9 @@ const STATUS_UPDATE_INTERVAL = 2 * 60 * 1_000;
// Update the exchange rate every 5 minutes
const UPDATE_RATE_INTERVAL = 5 * 60 * 1_000;
// Fetch all conversations every 10 minutes
const FETCH_CONVERSATIONS_INTERVAL = 10 * 60 * 1_000;
function setIntervalImmediate(callback: () => void, interval: number): void {
callback();
setInterval(callback, interval);
@ -26,6 +29,7 @@ export async function setupBackgroundTasks(): Promise<void> {
setIntervalImmediate(updatePublicRegistry, PROVIDER_UPDATE_INTERVAL);
setIntervalImmediate(updateAllNodeStatuses, STATUS_UPDATE_INTERVAL);
setIntervalImmediate(updateRates, UPDATE_RATE_INTERVAL);
setIntervalImmediate(fetchAllConversations, FETCH_CONVERSATIONS_INTERVAL);
// // Setup Tauri event listeners

View file

@ -3,7 +3,7 @@ import { ThemeProvider } from "@material-ui/core/styles";
import "@tauri-apps/plugin-shell";
import { Route, MemoryRouter as Router, Routes } from "react-router-dom";
import Navigation, { drawerWidth } from "./navigation/Navigation";
import HelpPage from "./pages/help/HelpPage";
import SettingsPage from "./pages/help/SettingsPage";
import HistoryPage from "./pages/history/HistoryPage";
import SwapPage from "./pages/swap/SwapPage";
import WalletPage from "./pages/wallet/WalletPage";
@ -14,6 +14,7 @@ import { themes } from "./theme";
import { useEffect } from "react";
import { setupBackgroundTasks } from "renderer/background";
import "@fontsource/roboto";
import FeedbackPage from "./pages/feedback/FeedbackPage";
const useStyles = makeStyles((theme) => ({
innerContent: {
@ -54,7 +55,8 @@ function InnerContent() {
<Route path="/swap" element={<SwapPage />} />
<Route path="/history" element={<HistoryPage />} />
<Route path="/wallet" element={<WalletPage />} />
<Route path="/help" element={<HelpPage />} />
<Route path="/settings" element={<SettingsPage />} />
<Route path="/feedback" element={<FeedbackPage />} />
<Route path="/" element={<SwapPage />} />
</Routes>
</Box>

View file

@ -23,7 +23,7 @@ import TruncatedText from "renderer/components/other/TruncatedText";
import { store } from "renderer/store/storeRenderer";
import { useActiveSwapInfo, useAppSelector } from "store/hooks";
import { logsToRawString, parseDateString } from "utils/parseUtils";
import { submitFeedbackViaHttp } from "../../../api";
import { submitFeedbackViaHttp, AttachmentInput } from "../../../api";
import LoadingButton from "../../other/LoadingButton";
import { PiconeroAmount } from "../../other/Units";
import { getLogsOfSwap, redactLogs } from "renderer/rpc";
@ -31,26 +31,58 @@ import logger from "utils/logger";
import { Label, Visibility } from "@material-ui/icons";
import CliLogsBox from "renderer/components/other/RenderedCliLog";
import { CliLog, parseCliLogString } from "models/cliModel";
import { addFeedbackId } from "store/features/conversationsSlice";
async function submitFeedback(body: string, swapId: string | null, swapLogs: string | null, daemonLogs: string | null) {
let attachedBody = "";
const attachments: AttachmentInput[] = [];
if (swapId !== null) {
const swapInfo = store.getState().rpc.state.swapInfos[swapId];
if (swapInfo === undefined) {
throw new Error(`Swap with id ${swapId} not found`);
if (swapInfo) {
// Add swap info as an attachment
attachments.push({
key: `swap_info_${swapId}.json`,
content: JSON.stringify(swapInfo, null, 2), // Pretty print JSON
});
// Retrieve and add logs for the specific swap
try {
const logs = await getLogsOfSwap(swapId, false);
attachments.push({
key: `swap_logs_${swapId}.txt`,
content: logs.logs.map((l) => JSON.stringify(l)).join("\n"),
});
} catch (logError) {
logger.error(logError, "Failed to get logs for swap", { swapId });
// Optionally add an attachment indicating log retrieval failure
attachments.push({ key: `swap_logs_${swapId}.error`, content: "Failed to retrieve swap logs." });
}
} else {
logger.warn("Selected swap info not found in state", { swapId });
attachments.push({ key: `swap_info_${swapId}.error`, content: "Swap info not found." });
}
attachedBody = `${JSON.stringify(swapInfo, null, 4)}\n\nLogs: ${swapLogs ?? ""}`;
// Add swap logs as an attachment
if (swapLogs) {
attachments.push({
key: `swap_logs_${swapId}.txt`,
content: swapLogs,
});
}
}
// Handle daemon logs
if (daemonLogs !== null) {
attachedBody += `\n\nDaemon Logs: ${daemonLogs ?? ""}`;
attachments.push({
key: "daemon_logs.txt",
content: daemonLogs,
});
}
console.log(`Sending feedback with attachement: \`\n${attachedBody}\``)
await submitFeedbackViaHttp(body, attachedBody);
// Call the updated API function
const feedbackId = await submitFeedbackViaHttp(body, attachments);
// Dispatch only the ID
store.dispatch(addFeedbackId(feedbackId));
}
/*
@ -152,7 +184,12 @@ export default function FeedbackDialog({
try {
setPending(true);
await submitFeedback(bodyText, selectedSwap, logsToRawString(swapLogs), logsToRawString(daemonLogs));
await submitFeedback(
bodyText,
selectedSwap,
logsToRawString(swapLogs ?? []),
logsToRawString(daemonLogs ?? [])
);
enqueueSnackbar("Feedback submitted successfully!", {
variant: "success",
});

View file

@ -1,12 +1,16 @@
import { Box, List } from "@material-ui/core";
import { Box, List, Badge } from "@material-ui/core";
import AccountBalanceWalletIcon from "@material-ui/icons/AccountBalanceWallet";
import HelpOutlineIcon from "@material-ui/icons/HelpOutline";
import HistoryOutlinedIcon from "@material-ui/icons/HistoryOutlined";
import SwapHorizOutlinedIcon from "@material-ui/icons/SwapHorizOutlined";
import FeedbackOutlinedIcon from '@material-ui/icons/FeedbackOutlined';
import RouteListItemIconButton from "./RouteListItemIconButton";
import UnfinishedSwapsBadge from "./UnfinishedSwapsCountBadge";
import { useTotalUnreadMessagesCount } from "store/hooks";
import SettingsIcon from '@material-ui/icons/Settings';
export default function NavigationHeader() {
const totalUnreadCount = useTotalUnreadMessagesCount();
return (
<Box>
<List>
@ -21,8 +25,18 @@ export default function NavigationHeader() {
<RouteListItemIconButton name="Wallet" route="/wallet">
<AccountBalanceWalletIcon />
</RouteListItemIconButton>
<RouteListItemIconButton name="Help & Settings" route="/help">
<HelpOutlineIcon />
<RouteListItemIconButton name="Feedback" route="/feedback">
<Badge
badgeContent={totalUnreadCount}
color="primary"
overlap="rectangular"
invisible={totalUnreadCount === 0}
>
<FeedbackOutlinedIcon />
</Badge>
</RouteListItemIconButton>
<RouteListItemIconButton name="Settings" route="/settings">
<SettingsIcon />
</RouteListItemIconButton>
</List>
</Box>

View file

@ -0,0 +1,25 @@
import { Box, makeStyles } from "@material-ui/core";
import FeedbackInfoBox from "../help/FeedbackInfoBox";
import ConversationsBox from "../help/ConversationsBox";
import ContactInfoBox from "../help/ContactInfoBox";
const useStyles = makeStyles((theme) => ({
outer: {
display: "flex",
gap: theme.spacing(2),
flexDirection: "column",
paddingBottom: theme.spacing(2),
},
}));
export default function FeedbackPage() {
const classes = useStyles();
return (
<Box className={classes.outer}>
<FeedbackInfoBox />
<ConversationsBox />
<ContactInfoBox />
</Box>
);
}

View file

@ -0,0 +1,346 @@
import { useState, useEffect, useMemo } from "react";
import {
Box,
Typography,
makeStyles,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
IconButton,
TableContainer,
Table,
TableHead,
TableRow,
TableCell,
TableBody,
Paper,
Badge,
TextField,
CircularProgress,
InputAdornment,
Tooltip,
List,
ListItem,
ListItemIcon,
Link,
} from "@material-ui/core";
import ChatIcon from '@material-ui/icons/Chat';
import SendIcon from '@material-ui/icons/Send';
import InfoBox from "renderer/components/modal/swap/InfoBox";
import TruncatedText from "renderer/components/other/TruncatedText";
import clsx from 'clsx';
import { useAppSelector, useAppDispatch, useUnreadMessagesCount } from "store/hooks";
import { markMessagesAsSeen } from "store/features/conversationsSlice";
import { appendFeedbackMessageViaHttp, fetchAllConversations } from "renderer/api";
import { useSnackbar } from "notistack";
import logger from "utils/logger";
import AttachmentIcon from '@material-ui/icons/Attachment';
import { Message, PrimitiveDateTimeString } from "models/apiModel";
import { formatDateTime } from "utils/conversionUtils";
// Styles
const useStyles = makeStyles((theme) => ({
content: { display: "flex", flexDirection: "column", alignItems: "flex-start", gap: theme.spacing(2) },
tableContainer: { maxHeight: 300 },
dialogContent: { display: 'flex', flexDirection: 'column' },
messagesContainer: { flexGrow: 1, overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: theme.spacing(1), maxHeight: 400, padding: theme.spacing(1) },
messageRow: { display: 'flex', marginTop: theme.spacing(1) },
staffRow: { justifyContent: 'flex-start' },
userRow: { justifyContent: 'flex-end' },
messageBubble: { padding: theme.spacing(1.5), borderRadius: theme.shape.borderRadius * 2, maxWidth: '75%', wordBreak: 'break-word', boxShadow: theme.shadows[1] },
staffBubble: { border: `1px solid ${theme.palette.divider}`, color: theme.palette.text.primary, borderRadius: theme.spacing(2) },
userBubble: { backgroundColor: theme.palette.primary.main, color: theme.palette.primary.contrastText, borderRadius: theme.spacing(2) },
timestamp: { marginTop: theme.spacing(0.5), fontSize: '0.75rem', opacity: 0.7, textAlign: 'right' },
inputArea: { flexShrink: 0, marginTop: theme.spacing(2) },
attachmentList: {
marginTop: theme.spacing(1),
marginBottom: theme.spacing(1),
paddingLeft: theme.spacing(2),
},
attachmentItem: {
paddingTop: theme.spacing(0.5),
paddingBottom: theme.spacing(0.5),
}
}));
// Hook: sorted feedback IDs by latest activity, then unread
function useSortedFeedbackIds() {
const ids = useAppSelector((s) => s.conversations.knownFeedbackIds || []);
const conv = useAppSelector((s) => s.conversations.conversations);
const seen = useAppSelector((s) => new Set(s.conversations.seenMessages));
return useMemo(() => {
const arr = ids.map((id) => {
const msgs = conv[id] || [];
const unread = msgs.filter((m) => m.is_from_staff && !seen.has(m.id.toString())).length;
const latest = msgs.reduce((d, m) => {
try {
const formattedDate = formatDateTime(m.created_at);
if (formattedDate.startsWith("Invalid")) return d;
const t = new Date(formattedDate).getTime();
return isNaN(t) ? d : Math.max(d, t);
} catch(e) { return d; }
}, 0);
return { id, unread, latest };
});
arr.sort((a, b) => b.latest - a.latest || (b.unread > 0 ? 1 : 0) - (a.unread > 0 ? 1 : 0));
return arr.map((x) => x.id);
}, [ids, conv, seen]);
}
// Main component
export default function ConversationsBox() {
const classes = useStyles();
const sortedIds = useSortedFeedbackIds();
const [openId, setOpenId] = useState<string | null>(null);
useEffect(() => {
// Fetch conversations via API function (handles its own dispatch)
fetchAllConversations();
}, []);
return (
<InfoBox
title="Developer Responses"
icon={null}
loading={false}
mainContent={
<Box className={classes.content}>
<Typography variant="subtitle2">
View your past feedback submissions and any replies from the development team.
</Typography>
{sortedIds.length === 0 ? (
<Typography variant="body2">No feedback submitted yet.</Typography>
) : (
<TableContainer component={Paper} className={classes.tableContainer}>
<Table stickyHeader size="small">
<TableHead>
<TableRow>
<TableCell style={{ width: '25%' }}>Last Message</TableCell>
<TableCell style={{ width: '60%' }}>Preview</TableCell>
<TableCell align="right" style={{ width: '15%' }} />
</TableRow>
</TableHead>
<TableBody>
{sortedIds.map((id) => (
<ConversationRow key={id} feedbackId={id} onOpen={setOpenId} />
))}
</TableBody>
</Table>
</TableContainer>
)}
</Box>
}
additionalContent={
openId && (
<ConversationModal
open={!!openId}
onClose={() => setOpenId(null)}
feedbackId={openId}
/>
)
}
/>
);
}
// Single row
function ConversationRow({ feedbackId, onOpen }: { feedbackId: string, onOpen: (id: string) => void }) {
const msgs = useAppSelector((s) => s.conversations.conversations[feedbackId] || []);
const unread = useUnreadMessagesCount(feedbackId);
const sorted = useMemo(
() =>
[...msgs].sort((a, b) => {
try {
const formattedDateA = formatDateTime(a.created_at);
const formattedDateB = formatDateTime(b.created_at);
if (formattedDateA.startsWith("Invalid")) return 1;
if (formattedDateB.startsWith("Invalid")) return -1;
const dateA = new Date(formattedDateA).getTime();
const dateB = new Date(formattedDateB).getTime();
if (isNaN(dateA)) return 1;
if (isNaN(dateB)) return -1;
return dateB - dateA;
} catch (e) { return 0; }
}),
[msgs]
);
const lastMsg = sorted[0];
const time = lastMsg ? formatDateTime(lastMsg.created_at) : '-';
const content = lastMsg ? lastMsg.content : 'No messages yet';
const preview = (() => {
return content;
})();
const hasStaff = useMemo(() => msgs.some((m) => m.is_from_staff), [msgs]);
return (
<TableRow>
<TableCell style={{ width: '25%' }}>{time}</TableCell>
<TableCell style={{ width: '60%' }}>
"<TruncatedText limit={30}>{preview}</TruncatedText>"
</TableCell>
<TableCell align="right" style={{ width: '15%' }}>
<Badge badgeContent={unread} color="primary" overlap="rectangular">
<Tooltip title={hasStaff ? 'Open Conversation' : 'No developer has responded'} arrow>
<span>
<IconButton size="small" onClick={() => onOpen(feedbackId)} disabled={!hasStaff}>
<ChatIcon />
</IconButton>
</span>
</Tooltip>
</Badge>
</TableCell>
</TableRow>
);
}
// Modal
function ConversationModal({ open, onClose, feedbackId }: { open: boolean, onClose: () => void, feedbackId: string }) {
const classes = useStyles();
const dispatch = useAppDispatch();
const { enqueueSnackbar } = useSnackbar();
const msgs = useAppSelector((s): Message[] => s.conversations.conversations[feedbackId] || []);
const seen = useAppSelector((s) => new Set(s.conversations.seenMessages));
const [text, setText] = useState('');
const [loading, setLoading] = useState(false);
useEffect(() => {
if (open) {
const unseen = msgs.filter((m) => !seen.has(m.id.toString()));
if (unseen.length) dispatch(markMessagesAsSeen(unseen));
}
}, [open, msgs, seen, dispatch]);
const sorted = useMemo(
() =>
[...msgs].sort((a, b) => {
try {
const formattedDateA = formatDateTime(a.created_at);
const formattedDateB = formatDateTime(b.created_at);
if (formattedDateA.startsWith("Invalid")) return 1;
if (formattedDateB.startsWith("Invalid")) return -1;
const dateA = new Date(formattedDateA).getTime();
const dateB = new Date(formattedDateB).getTime();
if (isNaN(dateA)) return 1;
if (isNaN(dateB)) return -1;
return dateB - dateA;
} catch(e) { return 0; }
}),
[msgs]
);
const sendMessage = async () => {
if (!text.trim()) return;
setLoading(true);
try {
await appendFeedbackMessageViaHttp(feedbackId, text);
setText('');
enqueueSnackbar('Message sent successfully!', { variant: 'success' });
fetchAllConversations();
} catch (e) {
logger.error(e, 'Send failed');
enqueueSnackbar('Failed to send message. Please try again.', { variant: 'error' });
} finally {
setLoading(false);
}
};
return (
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth scroll="paper">
<DialogTitle>Conversation <TruncatedText limit={8} truncateMiddle>{feedbackId}</TruncatedText></DialogTitle>
<DialogContent dividers className={classes.dialogContent}>
{sorted.length === 0 ? (
<Typography variant="body2">No messages in this conversation.</Typography>
) : (
<Box className={classes.messagesContainer}>
{sorted.map((m: Message) => {
const raw = m.content;
return (
<Box
key={m.id}
className={clsx(
classes.messageRow,
m.is_from_staff ? classes.staffRow : classes.userRow
)}
>
<Box className={clsx(
classes.messageBubble,
m.is_from_staff ? classes.staffBubble : classes.userBubble
)}
>
<Typography variant="body1" style={{ whiteSpace: 'pre-wrap' }}>
{raw}
</Typography>
{m.attachments && m.attachments.length > 0 && (
<List dense disablePadding className={classes.attachmentList}>
{m.attachments.map((att) => (
<ListItem key={att.id} className={classes.attachmentItem}>
<ListItemIcon style={{ minWidth: 'auto', marginRight: '8px' }}>
<AttachmentIcon fontSize="small" />
</ListItemIcon>
<Link
href="#"
onClick={(e) => {
e.preventDefault();
alert(`Attachment Key: ${att.key}\n\nContent:\n${att.content}`);
}}
variant="body2"
color="inherit"
underline="none"
>
{att.key}
</Link>
</ListItem>
))}
</List>
)}
<Typography variant="caption" className={classes.timestamp}>
{m.is_from_staff ? 'Developer' : 'You'} · {formatDateTime(m.created_at)}
</Typography>
</Box>
</Box>
);
})}
</Box>
)}
<Box className={classes.inputArea}>
<TextField
variant="outlined"
fullWidth
placeholder="Type your message..."
value={text}
onChange={(e) => setText(e.target.value)}
disabled={loading}
multiline
rowsMax={4}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
}}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
color="primary"
onClick={sendMessage}
disabled={!text.trim() || loading}
>
{loading ? <CircularProgress size={24} /> : <SendIcon />}
</IconButton>
</InputAdornment>
),
}}
/>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={onClose} variant="outlined">
Close
</Button>
</DialogActions>
</Dialog>
);
}

View file

@ -1,12 +1,12 @@
import { Box, makeStyles } from "@material-ui/core";
import ContactInfoBox from "./ContactInfoBox";
import DonateInfoBox from "./DonateInfoBox";
import FeedbackInfoBox from "./FeedbackInfoBox";
import DaemonControlBox from "./DaemonControlBox";
import SettingsBox from "./SettingsBox";
import ExportDataBox from "./ExportDataBox";
import { useLocation } from "react-router-dom";
import { useEffect } from "react";
const useStyles = makeStyles((theme) => ({
outer: {
display: "flex",
@ -16,7 +16,7 @@ const useStyles = makeStyles((theme) => ({
},
}));
export default function HelpPage() {
export default function SettingsPage() {
const classes = useStyles();
const location = useLocation();
@ -29,11 +29,9 @@ export default function HelpPage() {
return (
<Box className={classes.outer}>
<FeedbackInfoBox />
<SettingsBox />
<ExportDataBox />
<DaemonControlBox />
<ContactInfoBox />
<DonateInfoBox />
</Box>
);

View file

@ -1,9 +1,8 @@
import { combineReducers, configureStore } from "@reduxjs/toolkit";
import { combineReducers, configureStore, StoreEnhancer } from "@reduxjs/toolkit";
import { persistReducer, persistStore } from "redux-persist";
import sessionStorage from "redux-persist/lib/storage/session";
import { reducers } from "store/combinedReducer";
import { createMainListeners } from "store/middleware/storeListener";
import { getNetworkName } from "store/config";
import { LazyStore } from "@tauri-apps/plugin-store";
// Goal: Maintain application state across page reloads while allowing a clean slate on application restart
@ -14,20 +13,42 @@ import { LazyStore } from "@tauri-apps/plugin-store";
const rootPersistConfig = {
key: "gui-global-state-store",
storage: sessionStorage,
blacklist: ["settings"],
blacklist: ["settings", "conversations"],
};
// Use Tauri's store plugin for persistent settings
const tauriStore = new LazyStore("settings.bin");
// Helper to adapt Tauri storage to redux-persist (expects stringified JSON)
const createTauriStorage = () => ({
getItem: async (key: string): Promise<string | null> => {
const value = await tauriStore.get<unknown>(key); // Use generic get
return value == null ? null : JSON.stringify(value);
},
setItem: async (key: string, value: string): Promise<void> => {
try {
await tauriStore.set(key, JSON.parse(value));
await tauriStore.save();
} catch (err) {
console.error(`Error parsing or setting item "${key}" in Tauri store:`, err);
}
},
removeItem: async (key: string): Promise<void> => {
await tauriStore.delete(key);
await tauriStore.save();
},
});
// Configure how settings are stored and retrieved using Tauri's storage
const settingsPersistConfig = {
key: "settings",
storage: {
getItem: async (key: string) => tauriStore.get(key),
setItem: async (key: string, value: unknown) => tauriStore.set(key, value),
removeItem: async (key: string) => tauriStore.delete(key),
},
storage: createTauriStorage(),
};
// Persist conversations across application restarts
const conversationsPersistConfig = {
key: "conversations",
storage: createTauriStorage(),
};
// Create a persisted version of the settings reducer
@ -36,23 +57,55 @@ const persistedSettingsReducer = persistReducer(
reducers.settings,
);
// Create a persisted version of the conversations reducer
const persistedConversationsReducer = persistReducer(
conversationsPersistConfig,
reducers.conversations,
);
// Combine all reducers, using the persisted settings reducer
const rootReducer = combineReducers({
...reducers,
settings: persistedSettingsReducer,
conversations: persistedConversationsReducer,
});
// Enable persistence for the entire application state
const persistedReducer = persistReducer(rootPersistConfig, rootReducer);
// Set up the Redux store with persistence and custom middleware
// Add DevTools Enhancer logic
let remoteDevToolsEnhancer: StoreEnhancer | undefined;
if (import.meta.env.DEV) {
console.log('Development mode detected, attempting to enable Redux DevTools Remote...');
try {
const { devToolsEnhancer } = await import('@redux-devtools/remote');
remoteDevToolsEnhancer = devToolsEnhancer({
name: 'UnstoppableSwap_RemoteInstance',
realtime: true,
hostname: 'localhost',
port: 8098,
});
console.log('Redux DevTools Remote enhancer is ready.');
} catch (e) {
console.warn('Could not enable Redux DevTools Remote.', e);
remoteDevToolsEnhancer = undefined;
}
}
// Set up the Redux store with persistence, middleware, and remote DevTools
export const store = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
// Disable serializable to silence warnings about non-serializable actions
serializableCheck: false,
}).prepend(createMainListeners().middleware),
}).prepend(createMainListeners().middleware),
enhancers: (getDefaultEnhancers) => {
const defaultEnhancers = getDefaultEnhancers();
return remoteDevToolsEnhancer
? defaultEnhancers.concat(remoteDevToolsEnhancer)
: defaultEnhancers;
},
});
// Create a persistor to manage the persisted store

View file

@ -6,6 +6,7 @@ import swapReducer from "./features/swapSlice";
import torSlice from "./features/torSlice";
import settingsSlice from "./features/settingsSlice";
import nodesSlice from "./features/nodesSlice";
import conversationsSlice from "./features/conversationsSlice";
export const reducers = {
swap: swapReducer,
@ -16,4 +17,5 @@ export const reducers = {
rates: ratesSlice,
settings: settingsSlice,
nodes: nodesSlice,
conversations: conversationsSlice,
};

View file

@ -0,0 +1,54 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { Message } from "../../models/apiModel";
export interface ConversationsSlice {
// List of feedback IDs we know of
knownFeedbackIds: string[];
// Maps feedback IDs to conversations using the updated Message type
conversations: {
[key: string]: Message[]; // Use the imported Message type
};
// Stores IDs for Messages that have been seen by the user
seenMessages: string[];
}
const initialState: ConversationsSlice = {
knownFeedbackIds: [],
conversations: {},
seenMessages: [],
};
const conversationsSlice = createSlice({
name: "conversations",
initialState,
reducers: {
addFeedbackId(slice, action: PayloadAction<string>) {
// Only add if not already present
if (!slice.knownFeedbackIds.includes(action.payload)) {
slice.knownFeedbackIds.push(action.payload);
}
},
// Removes a feedback id from the list of known ones
// Also removes the conversation from the store
removeFeedback(slice, action: PayloadAction<string>) {
slice.knownFeedbackIds = slice.knownFeedbackIds.filter(
(id) => id !== action.payload,
);
delete slice.conversations[action.payload];
},
// Sets the conversations for a given feedback id (Payload uses the correct Message type)
setConversation(slice, action: PayloadAction<{feedbackId: string, messages: Message[]}>) {
slice.conversations[action.payload.feedbackId] = action.payload.messages;
},
// Sets the seen messages for a given feedback id (Payload uses the correct Message type)
markMessagesAsSeen(slice, action: PayloadAction<Message[]>) {
const newSeenIds = action.payload
.map((msg) => msg.id.toString())
.filter(id => !slice.seenMessages.includes(id)); // Avoid duplicates
slice.seenMessages.push(...newSeenIds);
},
},
});
export const { addFeedbackId, removeFeedback, setConversation, markMessagesAsSeen } = conversationsSlice.actions;
export default conversationsSlice.reducer;

View file

@ -146,4 +146,47 @@ export function usePendingApprovals(): PendingApprovalRequest[] {
export function usePendingLockBitcoinApproval(): PendingLockBitcoinApprovalRequest[] {
const approvals = usePendingApprovals();
return approvals.filter((c) => c.content.details.type === "LockBitcoin");
}
/**
* Calculates the number of unread messages from staff for a specific feedback conversation.
* @param feedbackId The ID of the feedback conversation.
* @returns The number of unread staff messages.
*/
export function useUnreadMessagesCount(feedbackId: string): number {
const { conversationsMap, seenMessagesSet } = useAppSelector((state) => ({
conversationsMap: state.conversations.conversations,
// Convert seenMessages array to a Set for efficient lookup
seenMessagesSet: new Set(state.conversations.seenMessages),
}));
const messages = conversationsMap[feedbackId] || [];
const unreadStaffMessages = messages.filter(
(msg) => msg.is_from_staff && !seenMessagesSet.has(msg.id.toString()),
);
return unreadStaffMessages.length;
}
/**
* Calculates the total number of unread messages from staff across all feedback conversations.
* @returns The total number of unread staff messages.
*/
export function useTotalUnreadMessagesCount(): number {
const { conversationsMap, seenMessagesSet } = useAppSelector((state) => ({
conversationsMap: state.conversations.conversations,
seenMessagesSet: new Set(state.conversations.seenMessages),
}));
let totalUnreadCount = 0;
for (const feedbackId in conversationsMap) {
const messages = conversationsMap[feedbackId] || [];
const unreadStaffMessages = messages.filter(
(msg) => msg.is_from_staff && !seenMessagesSet.has(msg.id.toString()),
);
totalUnreadCount += unreadStaffMessages.length;
}
return totalUnreadCount;
}

View file

@ -3,9 +3,10 @@ import { getAllSwapInfos, checkBitcoinBalance, updateAllNodeStatuses, fetchSelle
import logger from "utils/logger";
import { contextStatusEventReceived } from "store/features/rpcSlice";
import { addNode, setFetchFiatPrices, setFiatCurrency } from "store/features/settingsSlice";
import { updateRates } from "renderer/api";
import { fetchFeedbackMessagesViaHttp, updateRates } from "renderer/api";
import { store } from "renderer/store/storeRenderer";
import { swapProgressEventReceived } from "store/features/swapSlice";
import { addFeedbackId, setConversation } from "store/features/conversationsSlice";
export function createMainListeners() {
const listener = createListenerMiddleware();
@ -78,5 +79,15 @@ export function createMainListeners() {
},
});
// Listener for when a feedback id is added
listener.startListening({
actionCreator: addFeedbackId,
effect: async (action) => {
// Whenever a new feedback id is added, fetch the messages and store them in the Redux store
const messages = await fetchFeedbackMessagesViaHttp(action.payload);
store.dispatch(setConversation({ feedbackId: action.payload, messages }));
},
});
return listener;
}

View file

@ -74,4 +74,61 @@ export function bytesToMb(bytes: number): number {
/// Get the markup of a maker's exchange rate compared to the market rate in percent
export function getMarkup(makerPrice: number, marketPrice: number): number {
return (makerPrice - marketPrice) / marketPrice * 100;
}
// Updated function to parse 9-element tuple and format it
export function formatDateTime(dateTime: [number, number, number, number, number, number, number, number, number] | null | undefined): string {
if (!dateTime || !Array.isArray(dateTime) || dateTime.length !== 9) {
// Basic validation for null, undefined, or incorrect structure
return "Invalid Date Input";
}
try {
const [year, dayOfYear, hour, minute, second, nanoseconds, offsetH, offsetM, offsetS] = dateTime;
// More robust validation (example)
if (year < 1970 || dayOfYear < 1 || dayOfYear > 366 || hour < 0 || hour > 23 || minute < 0 || minute > 59 || second < 0 || second > 59 || nanoseconds < 0 || nanoseconds > 999999999) {
return "Invalid Date Components";
}
// Calculate total offset in seconds (handle potential non-zero offsets)
const totalOffsetSeconds = (offsetH * 3600) + (offsetM * 60) + offsetS;
// Calculate milliseconds from nanoseconds
const milliseconds = Math.floor(nanoseconds / 1_000_000);
// Create Date object for the start of the year *in UTC*
const date = new Date(Date.UTC(year, 0, 1)); // Month is 0-indexed (January)
// Add (dayOfYear - 1) days to get the correct date *in UTC*
date.setUTCDate(date.getUTCDate() + dayOfYear - 1);
// Set the time components *in UTC*
date.setUTCHours(hour);
date.setUTCMinutes(minute);
date.setUTCSeconds(second);
date.setUTCMilliseconds(milliseconds);
// Adjust for the timezone offset to get the correct UTC time
// Subtract the offset because Date.UTC assumes UTC, but the components might be for a different offset
date.setTime(date.getTime() - totalOffsetSeconds * 1000);
// Final validation
if (isNaN(date.getTime())) {
return "Invalid Calculated Date";
}
// Format to a readable string (e.g., "YYYY-MM-DD HH:MM:SS UTC")
const yyyy = date.getUTCFullYear();
const mm = String(date.getUTCMonth() + 1).padStart(2, '0');
const dd = String(date.getUTCDate()).padStart(2, '0');
const HH = String(date.getUTCHours()).padStart(2, '0');
const MM = String(date.getUTCMinutes()).padStart(2, '0');
const SS = String(date.getUTCSeconds()).padStart(2, '0');
return `${yyyy}-${mm}-${dd} ${HH}:${MM}:${SS} UTC`;
} catch (e) {
return "Invalid Date Format";
}
}

View file

@ -140,6 +140,13 @@
dependencies:
regenerator-runtime "^0.14.0"
"@babel/runtime@^7.23.2", "@babel/runtime@^7.26.9":
version "7.27.0"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.0.tgz#fbee7cf97c709518ecc1f590984481d5460d4762"
integrity sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==
dependencies:
regenerator-runtime "^0.14.0"
"@babel/template@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.9.tgz#ecb62d81a8a6f5dc5fe8abfc3901fc52ddf15016"
@ -516,6 +523,57 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
"@redux-devtools/core@^4.1.1":
version "4.1.1"
resolved "https://registry.yarnpkg.com/@redux-devtools/core/-/core-4.1.1.tgz#4e0d6fe7d250f10d927872448f0085b6c48cd933"
integrity sha512-ZyyJwiHX4DFDU0llk45tYSFPoIMekdoKLz0Q7soowpNOtchvTxruQx4Xy//Cohkwsw+DH8W1amdo4C/NYT6ARA==
dependencies:
"@babel/runtime" "^7.26.9"
"@redux-devtools/instrument" "^2.2.0"
"@redux-devtools/instrument@^2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@redux-devtools/instrument/-/instrument-2.2.0.tgz#bc9d015da693aa9fabdb32f4fd07ee4c1328eb95"
integrity sha512-HKaL+ghBQ4ZQkM/kEQIKx8dNwz4E1oeiCDfdQlpPXxEi/BrisyrFFncAXb1y2HIJsLV9zSvQUR2jRtMDWgfi8w==
dependencies:
"@babel/runtime" "^7.23.2"
lodash "^4.17.21"
"@redux-devtools/remote@^0.9.5":
version "0.9.5"
resolved "https://registry.yarnpkg.com/@redux-devtools/remote/-/remote-0.9.5.tgz#e0553026ea2d2f132246991c68dad57ac4d034e1"
integrity sha512-ETOUWgB5n6yopU4xH6wSwwmcVQT6liGBJbrWHkJkXCbCq9j/VqXHQ7spNN398p59vDseFZWOPo8KXNI0Mvo1RQ==
dependencies:
"@babel/runtime" "^7.26.9"
"@redux-devtools/instrument" "^2.2.0"
"@redux-devtools/utils" "^3.1.1"
jsan "^3.1.14"
rn-host-detect "^1.2.0"
socketcluster-client "^19.2.3"
"@redux-devtools/serialize@^0.4.2":
version "0.4.2"
resolved "https://registry.yarnpkg.com/@redux-devtools/serialize/-/serialize-0.4.2.tgz#564c0cf2e5cb119a1884b1994a51f6d2e138b9a5"
integrity sha512-YVqZCChJld5l3Ni2psEZ5loe9x5xpf9J4ckz+7OJdzCNsplC7vzjnkQbFxE6+ULZbywRVp+nSBslTXmaXqAw4A==
dependencies:
"@babel/runtime" "^7.23.2"
jsan "^3.1.14"
"@redux-devtools/utils@^3.1.1":
version "3.1.1"
resolved "https://registry.yarnpkg.com/@redux-devtools/utils/-/utils-3.1.1.tgz#a0c0aecf2c2e0f02518d48450dda90b9fe6eeb11"
integrity sha512-l+m3/8a7lcxULInBADIqE/3Tt2DkTJm5MAGVA/4czMCXW0VE+gdjkoRFqgZhTBoDJW1fi1z8pdL+4G/+R1rDJw==
dependencies:
"@babel/runtime" "^7.26.9"
"@redux-devtools/core" "^4.1.1"
"@redux-devtools/serialize" "^0.4.2"
"@types/get-params" "^0.1.2"
get-params "^0.1.2"
immutable "^4.3.7"
jsan "^3.1.14"
nanoid "^5.1.2"
redux "^5.0.1"
"@reduxjs/toolkit@^2.3.0":
version "2.4.0"
resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-2.4.0.tgz#29fd3a19530fc50d648a9b1e0132da0cd5618f19"
@ -877,6 +935,11 @@
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50"
integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==
"@types/get-params@^0.1.2":
version "0.1.2"
resolved "https://registry.yarnpkg.com/@types/get-params/-/get-params-0.1.2.tgz#815f80eceb0f0e2f0bb00a2527c9d2e6e57e2a52"
integrity sha512-ujqPyr1UDsOTDngJPV+WFbR0iHT5AfZKlNPMX6XOCnQcMhEqR+r64dVC/nwYCitqjR3DcpWofnOEAInUQmI/eA==
"@types/hoist-non-react-statics@^3.3.0":
version "3.3.5"
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz#dab7867ef789d87e2b4b0003c9d65c49cc44a494"
@ -1129,6 +1192,28 @@ acorn@^8.14.0:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.0.tgz#063e2c70cac5fb4f6467f0b11152e04c682795b0"
integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==
ag-auth@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/ag-auth/-/ag-auth-2.1.0.tgz#e6f9ecabbf23352456bd1e51ada4d6cf2382198d"
integrity sha512-M4l+IErFmYPk0HAvolaPyvCMyn3oJ4aPHVMeVqlxJIynkHGhyTFiT+LX+jYY34pEdwM03TLkQUMHxpXBMuNmZg==
dependencies:
jsonwebtoken "^9.0.0"
sc-errors "^3.0.0"
ag-channel@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/ag-channel/-/ag-channel-5.0.0.tgz#c2c00dfbe372ae43e0466ec89e29aca1bbb2fb3e"
integrity sha512-bArHkdqQxynim981t8FLZM5TfA0v7p081OlFdOxs6clB79GSGcGlOQMDa31DT9F5VMjzqNiJmhfGwinvfU/3Zg==
dependencies:
consumable-stream "^2.0.0"
ag-request@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/ag-request/-/ag-request-1.1.0.tgz#62ef63c572510bbce34993a5d47e467d0040a17f"
integrity sha512-d4K7QC1KnIpzcnUNNOeh1ddxmYMLiIdhdc1M8osxiHbZP/uoia4IINhhf2+1CrlnNJEPUoUH0Y58Sx0qeqoIvg==
dependencies:
sc-errors "^3.0.0"
ajv@^6.12.4:
version "6.12.6"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
@ -1233,6 +1318,13 @@ assertion-error@^2.0.1:
resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7"
integrity sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==
async-stream-emitter@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/async-stream-emitter/-/async-stream-emitter-7.0.1.tgz#c01832cddcc8f07d8ed528347803ec1517f8886d"
integrity sha512-1bgA3iZ80rCBX2LocvsyZPy0QB3/xM+CsXBze2HDHLmshOqx2JlAANGq23djaJ48e9fpcKzTzS1QM0hAKKI0UQ==
dependencies:
stream-demux "^10.0.1"
atomic-sleep@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b"
@ -1255,6 +1347,14 @@ base64-js@^1.3.1:
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
bl@^1.2.1:
version "1.2.3"
resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.3.tgz#1e8dd80142eac80d7158c9dccc047fb620e035e7"
integrity sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==
dependencies:
readable-stream "^2.3.5"
safe-buffer "^5.1.1"
brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
@ -1287,6 +1387,19 @@ browserslist@^4.24.0:
node-releases "^2.0.18"
update-browserslist-db "^1.1.1"
buffer-equal-constant-time@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==
buffer@^5.2.1:
version "5.7.1"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
dependencies:
base64-js "^1.3.1"
ieee754 "^1.1.13"
buffer@^6.0.3:
version "6.0.3"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6"
@ -1352,6 +1465,15 @@ check-error@^2.1.1:
resolved "https://registry.yarnpkg.com/check-error/-/check-error-2.1.1.tgz#87eb876ae71ee388fa0471fe423f494be1d96ccc"
integrity sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==
clone-deep@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387"
integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==
dependencies:
is-plain-object "^2.0.4"
kind-of "^6.0.2"
shallow-clone "^3.0.0"
clsx@^1.0.4, clsx@^1.1.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
@ -1379,11 +1501,26 @@ concat-map@0.0.1:
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
consumable-stream@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/consumable-stream/-/consumable-stream-2.0.0.tgz#11d3c7281b747eb9efd31c199b3a8b1711bec654"
integrity sha512-I6WA2JVYXs/68rEvi1ie3rZjP6qusTVFEQkbzR+WC+fY56TpwiGTIDJETsrnlxv5CsnmK69ps6CkYvIbpEEqBA==
consumable-stream@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/consumable-stream/-/consumable-stream-3.0.0.tgz#2bf140e0c5f9b63d6fa116ac6b05e53713d3cb41"
integrity sha512-CnnsJ9OG9ouxAjt3pc63/DaerezRo/WudqU71pc5epaIUi7NHu2T4v+3f0nKbbCY7icS/TfQ1Satr9rwZ7Jwsg==
convert-source-map@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a"
integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==
core-util-is@~1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
cross-spawn@^7.0.3, cross-spawn@^7.0.5:
version "7.0.6"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
@ -1509,6 +1646,13 @@ dom-helpers@^5.0.1:
"@babel/runtime" "^7.8.7"
csstype "^3.0.2"
ecdsa-sig-formatter@1.0.11:
version "1.0.11"
resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf"
integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==
dependencies:
safe-buffer "^5.0.1"
electron-to-chromium@^1.5.41:
version "1.5.68"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.68.tgz#4f46be4d465ef00e2100d5557b66f4af70e3ce6c"
@ -1970,6 +2114,11 @@ get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4:
has-symbols "^1.0.3"
hasown "^2.0.0"
get-params@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/get-params/-/get-params-0.1.2.tgz#bae0dfaba588a0c60d7834c0d8dc2ff60eeef2fe"
integrity sha512-41eOxtlGgHQRbFyA8KTH+w+32Em3cRdfBud7j67ulzmIfmaHX9doq47s0fa4P5o9H64BZX9nrYI6sJvk46Op+Q==
get-stream@^6.0.0:
version "6.0.1"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7"
@ -2111,7 +2260,7 @@ hyphenate-style-name@^1.0.3:
resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz#1797bf50369588b47b72ca6d5e65374607cf4436"
integrity sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==
ieee754@^1.2.1:
ieee754@^1.1.13, ieee754@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
@ -2126,6 +2275,11 @@ immer@^10.0.3:
resolved "https://registry.yarnpkg.com/immer/-/immer-10.1.1.tgz#206f344ea372d8ea176891545ee53ccc062db7bc"
integrity sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==
immutable@^4.3.7:
version "4.3.7"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.7.tgz#c70145fc90d89fb02021e65c84eb0226e4e5a381"
integrity sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==
import-fresh@^3.2.1:
version "3.3.0"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
@ -2139,6 +2293,11 @@ imurmurhash@^0.1.4:
resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==
inherits@~2.0.3:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
internal-ip@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-7.0.0.tgz#5b1c6a9d7e188aa73a1b69717daf50c8d8ed774f"
@ -2285,6 +2444,13 @@ is-number@^7.0.0:
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
is-plain-object@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==
dependencies:
isobject "^3.0.1"
is-regex@^1.1.4:
version "1.2.0"
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.2.0.tgz#41b9d266e7eb7451312c64efc37e8a7d453077cf"
@ -2361,11 +2527,21 @@ isarray@^2.0.5:
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==
isarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==
isexe@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
isobject@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==
iterator.prototype@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.3.tgz#016c2abe0be3bbdb8319852884f60908ac62bf9c"
@ -2401,6 +2577,11 @@ js-yaml@^4.1.0:
dependencies:
argparse "^2.0.1"
jsan@^3.1.14:
version "3.1.14"
resolved "https://registry.yarnpkg.com/jsan/-/jsan-3.1.14.tgz#197fee2d260b85acacb049c1ffa41bd09fb1f213"
integrity sha512-wStfgOJqMv4QKktuH273f5fyi3D3vy2pHOiSDGPvpcS/q+wb/M7AK3vkCcaHbkZxDOlDU/lDJgccygKSG2OhtA==
jsesc@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e"
@ -2426,6 +2607,22 @@ json5@^2.2.3:
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
jsonwebtoken@^9.0.0:
version "9.0.2"
resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#65ff91f4abef1784697d40952bb1998c504caaf3"
integrity sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==
dependencies:
jws "^3.2.2"
lodash.includes "^4.3.0"
lodash.isboolean "^3.0.3"
lodash.isinteger "^4.0.4"
lodash.isnumber "^3.0.3"
lodash.isplainobject "^4.0.6"
lodash.isstring "^4.0.1"
lodash.once "^4.0.0"
ms "^2.1.1"
semver "^7.5.4"
jss-plugin-camel-case@^10.5.1:
version "10.10.0"
resolved "https://registry.yarnpkg.com/jss-plugin-camel-case/-/jss-plugin-camel-case-10.10.0.tgz#27ea159bab67eb4837fa0260204eb7925d4daa1c"
@ -2506,6 +2703,23 @@ jss@10.10.0, jss@^10.5.1:
object.assign "^4.1.4"
object.values "^1.1.6"
jwa@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a"
integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==
dependencies:
buffer-equal-constant-time "1.0.1"
ecdsa-sig-formatter "1.0.11"
safe-buffer "^5.0.1"
jws@^3.2.2:
version "3.2.2"
resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304"
integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==
dependencies:
jwa "^1.4.1"
safe-buffer "^5.0.1"
keyv@^4.5.4:
version "4.5.4"
resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93"
@ -2513,6 +2727,11 @@ keyv@^4.5.4:
dependencies:
json-buffer "3.0.1"
kind-of@^6.0.2:
version "6.0.3"
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
levn@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"
@ -2521,6 +2740,11 @@ levn@^0.4.1:
prelude-ls "^1.2.1"
type-check "~0.4.0"
linked-list@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/linked-list/-/linked-list-2.1.0.tgz#fa7b63a6caf4b17862a1eb90d14ead4ee57649f2"
integrity sha512-0GK/ylO6e5cv1PCOIdTRHxOaCgQ+0jKwHt+cHzkiCAZlx0KM5Id1bBAPad6g2mkvBNp1pNdmG0cohFGfqjkv9A==
locate-path@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286"
@ -2528,11 +2752,46 @@ locate-path@^6.0.0:
dependencies:
p-locate "^5.0.0"
lodash.includes@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f"
integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==
lodash.isboolean@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6"
integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==
lodash.isinteger@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343"
integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==
lodash.isnumber@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc"
integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==
lodash.isplainobject@^4.0.6:
version "4.0.6"
resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==
lodash.isstring@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451"
integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==
lodash.merge@^4.6.2:
version "4.6.2"
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
lodash.once@^4.0.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==
lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
@ -2640,6 +2899,11 @@ nanoid@^3.3.7:
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf"
integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==
nanoid@^5.1.2:
version "5.1.5"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-5.1.5.tgz#f7597f9d9054eb4da9548cdd53ca70f1790e87de"
integrity sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==
native-fetch@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/native-fetch/-/native-fetch-3.0.0.tgz#06ccdd70e79e171c365c75117959cf4fe14a09bb"
@ -2902,6 +3166,11 @@ prelude-ls@^1.2.1:
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
process-nextick-args@~2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
process-warning@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-4.0.0.tgz#581e3a7a1fb456c5f4fd239f76bce75897682d5a"
@ -3020,6 +3289,19 @@ react@^18.2.0:
dependencies:
loose-envify "^1.1.0"
readable-stream@^2.3.5, readable-stream@~2.3.6:
version "2.3.8"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b"
integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==
dependencies:
core-util-is "~1.0.0"
inherits "~2.0.3"
isarray "~1.0.0"
process-nextick-args "~2.0.0"
safe-buffer "~5.1.1"
string_decoder "~1.1.1"
util-deprecate "~1.0.1"
readable-stream@^4.0.0:
version "4.5.2"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.5.2.tgz#9e7fc4c45099baeed934bff6eb97ba6cf2729e09"
@ -3117,6 +3399,11 @@ reusify@^1.0.4:
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
rn-host-detect@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/rn-host-detect/-/rn-host-detect-1.2.0.tgz#8b0396fc05631ec60c1cb8789e5070cdb04d0da0"
integrity sha512-btNg5kzHcjZZ7t7mvvV/4wNJ9e3MPgrWivkRgWURzXL0JJ0pwWlU4zrbmdlz3HHzHOxhBhHB4D+/dbMFfu4/4A==
rollup@^4.20.0:
version "4.28.0"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.28.0.tgz#eb8d28ed43ef60a18f21d0734d230ee79dd0de77"
@ -3161,11 +3448,16 @@ safe-array-concat@^1.1.2:
has-symbols "^1.0.3"
isarray "^2.0.5"
safe-buffer@~5.2.0:
safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@~5.2.0:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
safe-regex-test@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.3.tgz#a5b4c0f06e0ab50ea2c395c14d8371232924c377"
@ -3180,6 +3472,16 @@ safe-stable-stringify@^2.3.1:
resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz#4ca2f8e385f2831c432a719b108a3bf7af42a1dd"
integrity sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==
sc-errors@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/sc-errors/-/sc-errors-3.0.0.tgz#df2e124f011be5fdd633e92d1de5ce6a6b4c1b85"
integrity sha512-rIqv2HTPb9DVreZwK/DV0ytRUqyw2DbDcoB9XTKjEQL7oMEQKsfPA8V8dGGr7p8ZYfmvaRIGZ4Wu5qwvs/hGDA==
sc-formatter@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/sc-formatter/-/sc-formatter-4.0.0.tgz#2dda494a08e9d4cb069cbc9238a9f670adb3e7a6"
integrity sha512-MgUIvuca+90fBrCWY5LdlU9YUWjlkPFwdpvmomcwQEu3t2id/6YHdG2nhB6o7nhRp4ocfmcXQTh00r/tJtynSg==
scheduler@^0.23.2:
version "0.23.2"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3"
@ -3197,6 +3499,11 @@ semver@^6.3.1:
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
semver@^7.5.4:
version "7.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.1.tgz#abd5098d82b18c6c81f6074ff2647fd3e7220c9f"
integrity sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==
semver@^7.6.0, semver@^7.6.2:
version "7.6.3"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143"
@ -3224,6 +3531,13 @@ set-function-name@^2.0.1, set-function-name@^2.0.2:
functions-have-names "^1.2.3"
has-property-descriptors "^1.0.2"
shallow-clone@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3"
integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==
dependencies:
kind-of "^6.0.2"
shebang-command@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
@ -3256,6 +3570,25 @@ signal-exit@^3.0.3:
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
socketcluster-client@^19.2.3:
version "19.2.3"
resolved "https://registry.yarnpkg.com/socketcluster-client/-/socketcluster-client-19.2.3.tgz#89d8f6a215f8b6469ab3d93d1d4d0d9abf30c747"
integrity sha512-kYHBTH+P0UXnHQQxTVK9//rSAgETWSaVe8A4wlDpTQPqzpTWn2bq2ARaiLgXx8WouKaS9XcOLDRQc58e2fFscg==
dependencies:
ag-auth "^2.1.0"
ag-channel "^5.0.0"
ag-request "^1.1.0"
async-stream-emitter "^7.0.1"
buffer "^5.2.1"
clone-deep "^4.0.1"
linked-list "^2.1.0"
sc-errors "^3.0.0"
sc-formatter "^4.0.0"
stream-demux "^10.0.1"
uuid "^8.3.2"
vinyl-buffer "^1.0.1"
ws "^8.18.0"
sonic-boom@^4.0.1:
version "4.2.0"
resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-4.2.0.tgz#e59a525f831210fa4ef1896428338641ac1c124d"
@ -3283,6 +3616,14 @@ std-env@^3.8.0:
resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.8.0.tgz#b56ffc1baf1a29dcc80a3bdf11d7fca7c315e7d5"
integrity sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==
stream-demux@^10.0.1:
version "10.0.1"
resolved "https://registry.yarnpkg.com/stream-demux/-/stream-demux-10.0.1.tgz#204b65fb8973c87cea65119e99622405b3dbcc10"
integrity sha512-QjTYLJWpZxZ6uL5R1JzgOzjvao8zDx78ec+uOjHNeVc/9TuasYLldoVrYARZeT1xI1hFYuiKf13IM8b4wamhHg==
dependencies:
consumable-stream "^3.0.0"
writable-consumable-stream "^4.1.0"
string.prototype.matchall@^4.0.11:
version "4.0.11"
resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz#1092a72c59268d2abaad76582dccc687c0297e0a"
@ -3344,6 +3685,13 @@ string_decoder@^1.3.0:
dependencies:
safe-buffer "~5.2.0"
string_decoder@~1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
dependencies:
safe-buffer "~5.1.0"
strip-final-newline@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
@ -3373,6 +3721,14 @@ thread-stream@^3.0.0:
dependencies:
real-require "^0.2.0"
through2@^2.0.3:
version "2.0.5"
resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"
integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==
dependencies:
readable-stream "~2.3.6"
xtend "~4.0.1"
tiny-warning@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
@ -3533,16 +3889,34 @@ use-sync-external-store@^1.0.0:
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz#c3b6390f3a30eba13200d2302dcdf1e7b57b2ef9"
integrity sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==
util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
uuid@^10.0.0:
version "10.0.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294"
integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==
uuid@^8.3.2:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
varint@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/varint/-/varint-6.0.0.tgz#9881eb0ce8feaea6512439d19ddf84bf551661d0"
integrity sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==
vinyl-buffer@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/vinyl-buffer/-/vinyl-buffer-1.0.1.tgz#96c1a3479b8c5392542c612029013b5b27f88bbf"
integrity sha512-LRBE2/g3C1hSHL2k/FynSZcVTRhEw8sb08oKGt/0hukZXwrh2m8nfy+r5yLhGEk7eFFuclhyIuPct/Bxlxk6rg==
dependencies:
bl "^1.2.1"
through2 "^2.0.3"
virtua@^0.33.2:
version "0.33.7"
resolved "https://registry.yarnpkg.com/virtua/-/virtua-0.33.7.tgz#bd46d7d31f257886e6245347354fb4e80e27441f"
@ -3697,6 +4071,23 @@ wrappy@1:
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
writable-consumable-stream@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/writable-consumable-stream/-/writable-consumable-stream-4.1.0.tgz#e677076f91499159361d7917dce379cad624b344"
integrity sha512-4cjCPd4Ayfbix0qqPCzMbnPPZKRh/cKeNCj05unybP3/sRkRAOxh7rSwbhxs3YB6G4/Z2p/2FRBEIQcTeB4jyw==
dependencies:
consumable-stream "^3.0.0"
ws@^8.18.0:
version "8.18.1"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.1.tgz#ea131d3784e1dfdff91adb0a4a116b127515e3cb"
integrity sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==
xtend@~4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
yallist@^3.0.2:
version "3.1.1"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"