add websocket
This commit is contained in:
@ -4,7 +4,7 @@ interface HeadAvatarProps {
|
||||
skinUrl?: string;
|
||||
size?: number;
|
||||
style?: React.CSSProperties;
|
||||
version?: number; // ✅ добавили
|
||||
version?: number;
|
||||
}
|
||||
|
||||
const DEFAULT_SKIN =
|
||||
@ -14,15 +14,17 @@ export const HeadAvatar: React.FC<HeadAvatarProps> = ({
|
||||
skinUrl,
|
||||
size = 24,
|
||||
style,
|
||||
version = 0, // ✅ дефолт
|
||||
version = 0,
|
||||
...canvasProps
|
||||
}) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const requestIdRef = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
const baseUrl = skinUrl?.trim() ? skinUrl : DEFAULT_SKIN;
|
||||
requestIdRef.current += 1;
|
||||
const requestId = requestIdRef.current;
|
||||
|
||||
// ✅ cache-bust: чтобы браузер НЕ отдавал старую картинку
|
||||
const baseUrl = skinUrl?.trim() ? skinUrl : DEFAULT_SKIN;
|
||||
const finalSkinUrl = `${baseUrl}${baseUrl.includes('?') ? '&' : '?'}v=${version}`;
|
||||
|
||||
const canvas = canvasRef.current;
|
||||
@ -33,6 +35,9 @@ export const HeadAvatar: React.FC<HeadAvatarProps> = ({
|
||||
img.src = finalSkinUrl;
|
||||
|
||||
img.onload = () => {
|
||||
// ✅ игнорим старые onload
|
||||
if (requestIdRef.current !== requestId) return;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
@ -47,9 +52,16 @@ export const HeadAvatar: React.FC<HeadAvatarProps> = ({
|
||||
};
|
||||
|
||||
img.onerror = (e) => {
|
||||
if (requestIdRef.current !== requestId) return;
|
||||
console.error('Не удалось загрузить скин для HeadAvatar:', e);
|
||||
};
|
||||
}, [skinUrl, size, version]); // ✅ version добавили
|
||||
|
||||
return () => {
|
||||
// ✅ гарантированно “убиваем” обработчики старого запроса
|
||||
img.onload = null;
|
||||
img.onerror = null;
|
||||
};
|
||||
}, [skinUrl, size, version]);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
|
||||
@ -49,6 +49,7 @@ import GradientTextField from '../components/GradientTextField';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import type { SxProps, Theme } from '@mui/material/styles';
|
||||
import { getWsBaseUrl } from '../realtime/wsBase';
|
||||
|
||||
interface TabPanelProps {
|
||||
children?: React.ReactNode;
|
||||
@ -80,6 +81,18 @@ const GLASS_BG =
|
||||
const GLASS_BORDER = '1px solid rgba(255,255,255,0.08)';
|
||||
const GLASS_SHADOW = '0 1.2vw 3.2vw rgba(0,0,0,0.55)';
|
||||
|
||||
function upsertIntoList<T extends { id: string }>(list: T[], item: T) {
|
||||
const idx = list.findIndex((x) => x.id === item.id);
|
||||
if (idx === -1) return [item, ...list];
|
||||
const copy = list.slice();
|
||||
copy[idx] = item;
|
||||
return copy;
|
||||
}
|
||||
|
||||
function removeFromList<T extends { id: string }>(list: T[], id: string) {
|
||||
return list.filter((x) => x.id !== id);
|
||||
}
|
||||
|
||||
export default function Marketplace() {
|
||||
const [marketLoading, setMarketLoading] = useState<boolean>(false);
|
||||
|
||||
@ -154,6 +167,91 @@ export default function Marketplace() {
|
||||
}
|
||||
};
|
||||
|
||||
type MarketListedPayload = { serverIp: string; item: MarketplaceItemResponse };
|
||||
type MarketSoldPayload = { serverIp: string; itemId: string };
|
||||
type MarketPricePayload = { serverIp: string; item: MarketplaceItemResponse };
|
||||
type MarketCancelledPayload = { serverIp: string; itemId: string };
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedServer) return;
|
||||
|
||||
const serverIp = selectedServer.ip;
|
||||
|
||||
const base = 'wss://minecraft.api.popa-popa.ru'
|
||||
const wsUrl = `${base}/ws/marketplace?server_ip=${encodeURIComponent(serverIp)}`;
|
||||
|
||||
const ws = new WebSocket(wsUrl);
|
||||
|
||||
const ping = window.setInterval(() => {
|
||||
if (ws.readyState === WebSocket.OPEN) ws.send('ping');
|
||||
}, 25000);
|
||||
|
||||
ws.onmessage = (ev) => {
|
||||
let msg: any;
|
||||
try {
|
||||
msg = JSON.parse(ev.data);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.server_ip !== serverIp && msg.serverIp !== serverIp) return;
|
||||
|
||||
switch (msg.event) {
|
||||
case 'market:item_listed': {
|
||||
const item: MarketplaceItemResponse = msg.item;
|
||||
setMarketItems((prev) => {
|
||||
if (!prev) return prev;
|
||||
if (prev.page !== 1) return prev;
|
||||
return { ...prev, items: upsertIntoList(prev.items, item) };
|
||||
});
|
||||
|
||||
if (item?.seller_name === username) {
|
||||
setMyItems((prev) => {
|
||||
if (!prev) return prev;
|
||||
if (prev.page !== 1) return prev;
|
||||
return { ...prev, items: upsertIntoList(prev.items, item) };
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'market:item_sold': {
|
||||
const itemId: string = msg.item_id ?? msg.itemId;
|
||||
setMarketItems((prev) => (prev ? { ...prev, items: removeFromList(prev.items, itemId) } : prev));
|
||||
setMyItems((prev) => (prev ? { ...prev, items: removeFromList(prev.items, itemId) } : prev));
|
||||
break;
|
||||
}
|
||||
|
||||
case 'market:item_cancelled': {
|
||||
const itemId: string = msg.item_id ?? msg.itemId;
|
||||
setMarketItems((prev) => (prev ? { ...prev, items: removeFromList(prev.items, itemId) } : prev));
|
||||
setMyItems((prev) => (prev ? { ...prev, items: removeFromList(prev.items, itemId) } : prev));
|
||||
break;
|
||||
}
|
||||
|
||||
case 'market:item_price_updated': {
|
||||
const item: MarketplaceItemResponse = msg.item;
|
||||
setMarketItems((prev) => (prev ? { ...prev, items: upsertIntoList(prev.items, item) } : prev));
|
||||
setMyItems((prev) => (prev ? { ...prev, items: upsertIntoList(prev.items, item) } : prev));
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (e) => {
|
||||
console.error('Marketplace WS error:', e);
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
window.clearInterval(ping);
|
||||
};
|
||||
|
||||
return () => {
|
||||
window.clearInterval(ping);
|
||||
ws.close();
|
||||
};
|
||||
}, [selectedServer?.ip, username]);
|
||||
|
||||
useEffect(() => {
|
||||
if (tabValue !== 2) return;
|
||||
if (!selectedServer) return;
|
||||
@ -285,7 +383,7 @@ export default function Marketplace() {
|
||||
const loadMarketItems = async (serverIp: string, pageNumber: number) => {
|
||||
try {
|
||||
setMarketLoading(true);
|
||||
const marketData = await fetchMarketplace(serverIp, pageNumber, 10);
|
||||
const marketData = await fetchMarketplace(serverIp, pageNumber, 12);
|
||||
setMarketItems(marketData);
|
||||
setPage(marketData.page);
|
||||
setTotalPages(marketData.pages);
|
||||
@ -988,6 +1086,7 @@ export default function Marketplace() {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
ml: '25%',
|
||||
flexDirection: 'column',
|
||||
background: 'rgba(20,20,20,0.78)',
|
||||
borderRadius: '2.0vw',
|
||||
|
||||
14
src/renderer/realtime/wsBase.ts
Normal file
14
src/renderer/realtime/wsBase.ts
Normal file
@ -0,0 +1,14 @@
|
||||
export function getWsBaseUrl(): string {
|
||||
// 1) если ты пробрасываешь конфиг в window
|
||||
const w = window as any;
|
||||
if (w.__ENV__?.WS_BASE) return String(w.__ENV__.WS_BASE);
|
||||
|
||||
// 2) если открыто с https/http — строим ws/wss автоматически
|
||||
if (typeof window !== 'undefined' && window.location?.origin) {
|
||||
const origin = window.location.origin; // http(s)://host
|
||||
return origin.replace(/^http/, 'ws');
|
||||
}
|
||||
|
||||
// 3) дефолт
|
||||
return 'wss://minecraft.api.popa-popa.ru';
|
||||
}
|
||||
Reference in New Issue
Block a user