1 Commits
dev ... v0.2.0

Author SHA1 Message Date
f8372b8de8 feat:
All checks were successful
/ build ushare (push) Successful in 45s
/ clean (push) Successful in 0s
1. local msg/file share by webrtc
fix:
  1. meta clean goroutine walk error
  2. clean interval to args(--clean)
2025-06-23 23:08:29 +08:00
14 changed files with 208 additions and 125 deletions

View File

@ -16,7 +16,6 @@ export const useFileUpload = () => {
setProgress(0); setProgress(0);
try { try {
console.log(`[D] api.Upload: upload file = ${file.name}, size = ${file.size}`, file);
const url = `/api/ushare/${file.name}`; const url = `/api/ushare/${file.name}`;
// 1. 初始化上传 // 1. 初始化上传
@ -26,7 +25,7 @@ export const useFileUpload = () => {
}); });
if (!res1.ok) { if (!res1.ok) {
console.log(`[D] upload: put file not ok, status = ${res1.status}, res = ${await res1.text()}`) console.log(`[W] upload: put file not ok, status = ${res1.status}, res = ${await res1.text()}`)
if (res1.status === 401) { if (res1.status === 401) {
window.location.href = "/login?next=/share" window.location.href = "/login?next=/share"
return "" return ""

View File

@ -17,7 +17,8 @@ const useClass = createUseStyles({
background: "rgba(0, 0, 0, 0.5)", background: "rgba(0, 0, 0, 0.5)",
backdropFilter: "blur(2px)" backdropFilter: "blur(2px)"
}, },
background: "rgba(255, 255, 255, 0.4)", background: "rgba(212,212,212,0.85)",
backdropFilter: "blur(8px)",
}, },
dialog_content: { dialog_content: {
padding: "1.5rem", padding: "1.5rem",

View File

@ -50,7 +50,6 @@ export const Drawer: React.FC<DrawerProps> = ({
useEffect(() => { useEffect(() => {
const handleEscape = (e: KeyboardEvent) => { const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') { if (e.key === 'Escape') {
console.log('[D] escape close:', close)
close() close()
} }
}; };

View File

@ -1,6 +1,87 @@
import React, {useState} from 'react'; import React, {useState} from 'react';
import {Dialog} from "../../../component/dialog/dialog.tsx"; import {Dialog} from "../../../component/dialog/dialog.tsx";
import {ReceivedMessage} from "./types.ts"; import {ReceivedMessage} from "./types.ts";
import {createUseStyles} from "react-jss";
const useStyles = createUseStyles({
root: {
marginBottom: '1rem',
},
sender: {
margin: '0 0 0.5rem 0',
fontSize: '0.9rem',
color: '#666',
},
msgBox: {
background: 'rgba(255,255,255,0.96)',
color: '#222',
padding: '1rem',
borderRadius: '4px',
border: '1px solid #ddd',
maxHeight: '400px',
minHeight: '80px',
overflowY: 'auto',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
},
downloadLink: {
color: '#007aff',
textDecoration: 'underline',
},
btnRow: {
display: 'flex',
flexDirection: 'row',
gap: '0.5rem',
justifyContent: 'flex-end',
alignItems: 'center',
marginTop: '8px',
},
copySuccess: {
color: '#28a745',
fontSize: '0.9rem',
marginRight: '0.5rem',
animation: 'fadeIn 0.3s ease-in',
},
copyBtn: {
padding: '8px 16px',
background: '#007aff',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '0.9rem',
transition: 'all 0.2s',
'&:hover': {
background: '#0056b3',
},
},
copyBtnSuccess: {
background: '#28a745',
},
closeBtn: {
padding: '8px 16px',
background: '#6c757d',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '0.9rem',
transition: 'all 0.2s',
},
progressBar: {
width: '100%',
height: 8,
background: '#eee',
borderRadius: 4,
margin: '12px 0',
overflow: 'hidden',
},
progressInner: {
height: '100%',
background: '#4dabf7',
transition: 'width 0.3s',
},
});
interface MessageDialogProps { interface MessageDialogProps {
open: boolean; open: boolean;
@ -10,16 +91,14 @@ interface MessageDialogProps {
export const MessageDialog: React.FC<MessageDialogProps> = ({open, message, onClose}) => { export const MessageDialog: React.FC<MessageDialogProps> = ({open, message, onClose}) => {
const [copySuccess, setCopySuccess] = useState(false); const [copySuccess, setCopySuccess] = useState(false);
const classes = useStyles();
const handleCopyMessage = () => { const handleCopyMessage = () => {
if (message) { if (message) {
navigator.clipboard.writeText(message.text || '').then(() => { navigator.clipboard.writeText(message.text || '').then(() => {
console.log('消息已复制到剪贴板');
setCopySuccess(true); setCopySuccess(true);
// 2秒后隐藏成功提示
setTimeout(() => setCopySuccess(false), 2000); setTimeout(() => setCopySuccess(false), 2000);
}).catch(err => { }).catch(() => {
console.error('复制失败:', err);
alert('复制失败,请手动复制'); alert('复制失败,请手动复制');
}); });
} }
@ -32,82 +111,49 @@ export const MessageDialog: React.FC<MessageDialogProps> = ({open, message, onCl
onClose={onClose} onClose={onClose}
footer={false} footer={false}
> >
<div style={{ marginBottom: '1rem' }}> <div className={classes.root}>
<p style={{ margin: '0 0 0.5rem 0', fontSize: '0.9rem', color: '#666' }}> <p className={classes.sender}>
: {message?.sender} : {message?.sender}
</p> </p>
<div style={{ <div className={classes.msgBox}>
background: '#f5f5f5',
padding: '1rem',
borderRadius: '4px',
border: '1px solid #ddd',
maxHeight: '200px',
overflowY: 'auto',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word'
}}>
{message?.isFile ? ( {message?.isFile ? (
<> <>
<div>📎 : {message.fileName} ({message.fileSize ? (message.fileSize/1024).toFixed(1) : ''} KB)</div> <div>📎 : {message.fileName} ({message.fileSize ? (message.fileSize/1024).toFixed(1) : ''} KB)</div>
<a href={message.fileBlobUrl} download={message.fileName} style={{color:'#007aff',textDecoration:'underline'}}> {message.receiving ? (
<>
</a> <div style={{fontSize: '0.95em', color: '#888'}}>... {Math.round((message.progress||0)*100)}%</div>
<div className={classes.progressBar}>
<div className={classes.progressInner} style={{width: `${Math.round((message.progress||0)*100)}%`}} />
</div>
</>
) : (
<a href={message.fileBlobUrl} download={message.fileName} className={classes.downloadLink}>
</a>
)}
</> </>
) : ( ) : (
message?.text message?.text
)} )}
</div> </div>
</div> </div>
<div style={{ display: 'flex', flexDirection: 'row', gap: '0.5rem', justifyContent: 'flex-end', alignItems: 'center', marginTop: '8px' }}> <div className={classes.btnRow}>
{copySuccess && ( {copySuccess && (
<span style={{ <span className={classes.copySuccess}>
color: '#28a745',
fontSize: '0.9rem',
marginRight: '0.5rem',
animation: 'fadeIn 0.3s ease-in'
}}>
</span> </span>
)} )}
{!message?.isFile && ( {!message?.isFile && (
<button <button
onClick={handleCopyMessage} onClick={handleCopyMessage}
style={{ className={copySuccess ? `${classes.copyBtn} ${classes.copyBtnSuccess}` : classes.copyBtn}
padding: '8px 16px',
background: copySuccess ? '#28a745' : '#007aff',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '0.9rem',
transition: 'all 0.2s'
}}
onMouseEnter={(e) => {
if (!copySuccess) {
e.currentTarget.style.backgroundColor = '#0056b3';
}
}}
onMouseLeave={(e) => {
if (!copySuccess) {
e.currentTarget.style.backgroundColor = '#007aff';
}
}}
> >
{copySuccess ? '已复制' : '复制消息'} {copySuccess ? '已复制' : '复制消息'}
</button> </button>
)} )}
<button <button
onClick={onClose} onClick={onClose}
style={{ className={classes.closeBtn}
padding: '8px 16px',
background: '#6c757d',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '0.9rem',
transition: 'all 0.2s'
}}
> >
</button> </button>

View File

@ -1,5 +1,4 @@
import {Resp} from "../../../interface/response.ts"; import { WSMessage, ReceivedMessage} from "./types.ts";
import {Client, WSMessage, ReceivedMessage} from "./types.ts";
import {useLocalStore} from "../../../store/local.ts"; import {useLocalStore} from "../../../store/local.ts";
// 文件接收缓存 // 文件接收缓存
@ -9,7 +8,7 @@ export const handleFileChunk = (chunk: any, onFileReceived: (msg: import("./type
if (chunk.type === 'file') { if (chunk.type === 'file') {
// 文件元信息,初始化缓存 // 文件元信息,初始化缓存
fileReceiveCache[chunk.name + '_' + chunk.timestamp] = { fileReceiveCache[chunk.name + '_' + chunk.timestamp] = {
chunks: [], chunks: new Array(chunk.totalChunks),
total: chunk.totalChunks, total: chunk.totalChunks,
received: 0, received: 0,
name: chunk.name, name: chunk.name,
@ -17,6 +16,16 @@ export const handleFileChunk = (chunk: any, onFileReceived: (msg: import("./type
sender: chunk.sender, sender: chunk.sender,
timestamp: chunk.timestamp timestamp: chunk.timestamp
}; };
// 首次弹出进度
onFileReceived({
sender: chunk.sender,
timestamp: chunk.timestamp,
fileName: chunk.name,
fileSize: chunk.size,
isFile: true,
progress: 0,
receiving: true
});
} else if (chunk.type === 'file-chunk') { } else if (chunk.type === 'file-chunk') {
const key = chunk.name + '_' + chunk.timestamp; const key = chunk.name + '_' + chunk.timestamp;
const cache = fileReceiveCache[key]; const cache = fileReceiveCache[key];
@ -25,21 +34,49 @@ export const handleFileChunk = (chunk: any, onFileReceived: (msg: import("./type
const uint8 = new Uint8Array(chunk.data); const uint8 = new Uint8Array(chunk.data);
cache.chunks[chunk.chunkIndex] = uint8.buffer; cache.chunks[chunk.chunkIndex] = uint8.buffer;
cache.received++; cache.received++;
// 全部收到 // 实时回调进度
if (cache.received === cache.total) { if (cache.received < cache.total) {
const blob = new Blob(cache.chunks);
const url = URL.createObjectURL(blob);
onFileReceived({ onFileReceived({
sender: cache.sender, sender: cache.sender,
timestamp: cache.timestamp, timestamp: cache.timestamp,
fileName: cache.name, fileName: cache.name,
fileSize: cache.size, fileSize: cache.size,
fileBlobUrl: url, isFile: true,
isFile: true progress: cache.received / cache.total,
receiving: true
}); });
delete fileReceiveCache[key];
} }
} }
} else if (chunk.type === 'file-end') {
const key = chunk.name + '_' + chunk.timestamp;
const cache = fileReceiveCache[key];
if (cache && cache.received === cache.total) {
const blob = new Blob(cache.chunks);
const url = URL.createObjectURL(blob);
onFileReceived({
sender: cache.sender,
timestamp: cache.timestamp,
fileName: cache.name,
fileSize: cache.size,
fileBlobUrl: url,
isFile: true,
progress: 1,
receiving: false
});
delete fileReceiveCache[key];
} else if (cache) {
// 分块未齐全,提示异常
onFileReceived({
sender: cache.sender,
timestamp: cache.timestamp,
fileName: cache.name,
fileSize: cache.size,
isFile: true,
progress: cache.received / cache.total,
receiving: true,
text: '文件分块未齐全,接收失败'
});
}
} }
}; };
@ -60,21 +97,18 @@ export class RTCHandler {
// 更新回调函数的方法 // 更新回调函数的方法
updateCallbacks = (newCallbacks: RTCHandlerCallbacks) => { updateCallbacks = (newCallbacks: RTCHandlerCallbacks) => {
console.log('[D] Updating RTC handler callbacks');
this.callbacks = newCallbacks; this.callbacks = newCallbacks;
}; };
setupDataChannel = async (ch: RTCDataChannel, type: 'sender' | 'receiver') => { setupDataChannel = async (ch: RTCDataChannel, type: 'sender' | 'receiver') => {
console.log(`[D] Setting up data channel for type: ${type}`);
ch.onopen = () => { ch.onopen = () => {
console.log(`[D] 通道已打开!类型: ${type}`); console.log(`[D] 通道已打开!类型: ${type}`);
console.log('[D] Calling onChannelOpen callback with type:', type);
this.callbacks.onChannelOpen(type); this.callbacks.onChannelOpen(type);
useLocalStore.getState().setChannel(ch); useLocalStore.getState().setChannel(ch);
}; };
ch.onmessage = (e) => { ch.onmessage = (e) => {
console.log('[D] Received message:', e.data); // console.log('[D] Received message:', e.data);
try { try {
const data = JSON.parse(e.data); const data = JSON.parse(e.data);
if (data.type === 'message') { if (data.type === 'message') {
@ -84,10 +118,9 @@ export class RTCHandler {
timestamp: Date.now(), timestamp: Date.now(),
sender: data.sender || '未知用户' sender: data.sender || '未知用户'
}; };
console.log('[D] Calling onMessageReceived callback with message:', message);
this.callbacks.onMessageReceived(message); this.callbacks.onMessageReceived(message);
} else if (data.type === 'file') { } else if (data.type === 'file' || data.type === 'file-chunk' || data.type === 'file-end') {
// 处理文件消息 // 处理文件相关消息
handleFileChunk(data, this.callbacks.onMessageReceived); handleFileChunk(data, this.callbacks.onMessageReceived);
} }
} catch (error) { } catch (error) {
@ -97,27 +130,23 @@ export class RTCHandler {
timestamp: Date.now(), timestamp: Date.now(),
sender: '未知用户' sender: '未知用户'
}; };
console.log('[D] Calling onMessageReceived callback with plain text message:', message);
this.callbacks.onMessageReceived(message); this.callbacks.onMessageReceived(message);
} }
}; };
ch.onclose = () => { ch.onclose = () => {
console.log('[D] 通道关闭'); console.log('[D] 通道关闭');
console.log('[D] Calling onChannelClose callback');
this.callbacks.onChannelClose(); this.callbacks.onChannelClose();
useLocalStore.getState().setChannel(); useLocalStore.getState().setChannel();
}; };
}; };
handleBubbleClick = async (bubbleId: string, currentUserId: string) => { handleBubbleClick = async (bubbleId: string, currentUserId: string) => {
console.log(`[D] click id = ${bubbleId}`);
const current_rtc = this.rtcRef.current; const current_rtc = this.rtcRef.current;
if (!current_rtc) return; if (!current_rtc) return;
current_rtc.onnegotiationneeded = async () => { current_rtc.onnegotiationneeded = async () => {
const offer = await current_rtc.createOffer(); const offer = await current_rtc.createOffer();
console.log('[D] offer created', offer);
await current_rtc.setLocalDescription(offer); await current_rtc.setLocalDescription(offer);
const data = { const data = {
id: bubbleId, id: bubbleId,
@ -132,7 +161,6 @@ export class RTCHandler {
}; };
current_rtc.onicecandidate = async (e) => { current_rtc.onicecandidate = async (e) => {
console.log('[D] on candidate ', e);
await fetch('/api/ulocal/candidate', { await fetch('/api/ulocal/candidate', {
method: 'POST', method: 'POST',
headers: {"Content-Type": "application/json"}, headers: {"Content-Type": "application/json"},
@ -148,7 +176,7 @@ export class RTCHandler {
let current_id: string; let current_id: string;
let current_rtc: RTCPeerConnection | null; let current_rtc: RTCPeerConnection | null;
const msg = JSON.parse(e.data) as WSMessage; const msg = JSON.parse(e.data) as WSMessage;
console.log('[D] ws event msg =', msg); // console.log('[D] ws event msg =', msg);
switch (msg.type) { switch (msg.type) {
case "enter": case "enter":
@ -213,7 +241,6 @@ export class RTCHandler {
return; return;
} }
if (!candidate_data.candidate) { if (!candidate_data.candidate) {
console.log('[W] candidate data null');
return; return;
} }
await current_rtc.addIceCandidate(candidate_data.candidate); await current_rtc.addIceCandidate(candidate_data.candidate);
@ -223,10 +250,8 @@ export class RTCHandler {
sendMessage = (msg: string, files: File[], senderName: string) => { sendMessage = (msg: string, files: File[], senderName: string) => {
const ch = useLocalStore.getState().channel; const ch = useLocalStore.getState().channel;
console.log('[D] ready to send:', msg, files, ch);
const CHUNK_SIZE = 64 * 1024; // 64KB const CHUNK_SIZE = 64 * 1024; // 64KB
const BUFFERED_AMOUNT_THRESHOLD = 1 * 1024 * 1024; // 1MB const BUFFERED_AMOUNT_THRESHOLD = 1 * 1024 * 1024; // 1MB
if (ch && ch.readyState === 'open') { if (ch && ch.readyState === 'open') {
if (msg.trim()) { if (msg.trim()) {
// 发送文本消息 // 发送文本消息
@ -254,23 +279,28 @@ export class RTCHandler {
}; };
ch.send(JSON.stringify(fileData)); ch.send(JSON.stringify(fileData));
ch.bufferedAmountLowThreshold = BUFFERED_AMOUNT_THRESHOLD;
let offset = 0; let offset = 0;
let chunkIndex = 0; let chunkIndex = 0;
const reader = new FileReader(); const reader = new FileReader();
function sendNextChunk() { const sendNextChunk = () => {
if (offset >= file.size) return; if (offset >= file.size) {
if (ch && ch.bufferedAmount && ch.bufferedAmount > BUFFERED_AMOUNT_THRESHOLD) { // 分块全部发送完毕,发送 file-end
// 等待缓冲区变低 ch.send(JSON.stringify({
type: 'file-end',
name: file.name,
timestamp,
}));
return;
}
if (ch.bufferedAmount > BUFFERED_AMOUNT_THRESHOLD) {
ch.addEventListener('bufferedamountlow', sendNextChunk, { once: true }); ch.addEventListener('bufferedamountlow', sendNextChunk, { once: true });
return; return;
} }
const slice = file.slice(offset, offset + CHUNK_SIZE); const slice = file.slice(offset, offset + CHUNK_SIZE);
reader.onload = (e) => { reader.onload = (e) => {
if (e.target?.result) { if (e.target?.result) {
ch?.send(JSON.stringify({ ch.send(JSON.stringify({
type: 'file-chunk', type: 'file-chunk',
name: file.name, name: file.name,
timestamp, timestamp,
@ -284,7 +314,7 @@ export class RTCHandler {
} }
}; };
reader.readAsArrayBuffer(slice); reader.readAsArrayBuffer(slice);
} };
sendNextChunk(); sendNextChunk();
}); });
} }

View File

@ -6,11 +6,12 @@ interface SendDialogProps {
open: boolean; open: boolean;
onSend: (msg: string, files: File[]) => void; onSend: (msg: string, files: File[]) => void;
onClose: () => void; onClose: () => void;
name: string;
} }
export const SendDialog: React.FC<SendDialogProps> = ({open, onSend, onClose}) => { export const SendDialog: React.FC<SendDialogProps> = ({open, onSend, onClose, name}) => {
return ( return (
<Dialog open={open} title="发送消息" onClose={onClose}> <Dialog open={open} title={`发送消息给${name}`} onClose={onClose}>
<Sender onSend={onSend} /> <Sender onSend={onSend} />
</Dialog> </Dialog>
); );

View File

@ -150,17 +150,10 @@ export const Sender: React.FC<SenderProps> = ({onSend}) => {
</div> </div>
<div className={classes.buttons}> <div className={classes.buttons}>
<input type="file" ref={fileInputRef} hidden onChange={handleFileSelect}/> <input type="file" ref={fileInputRef} hidden onChange={handleFileSelect}/>
<button <button className={classes.action_btn} onClick={() => {
className={classes.action_btn}
style={{ opacity: 0.5, cursor: 'not-allowed' }}
onClick={() => { message.warning('暂不支持文件发送') }}
>
📁
</button>
{/* <button className={classes.action_btn} onClick={() => {
fileInputRef.current && fileInputRef.current.click() fileInputRef.current && fileInputRef.current.click()
}}>📁 }}>📁
</button> */} </button>
<button className={classes.action_btn} onClick={handleSubmit}> </button> <button className={classes.action_btn} onClick={handleSubmit}> </button>
</div> </div>
</div> </div>

View File

@ -33,4 +33,6 @@ export interface ReceivedMessage {
fileSize?: number; fileSize?: number;
fileBlobUrl?: string; fileBlobUrl?: string;
isFile?: boolean; isFile?: boolean;
progress?: number; // 0-1
receiving?: boolean; // 是否正在接收
} }

View File

@ -55,14 +55,15 @@ const useClass = createUseStyles({
export const LocalSharing: React.FC = () => { export const LocalSharing: React.FC = () => {
const classes = useClass(); const classes = useClass();
const {id, name, set, setChannel} = useLocalStore(); const {id, name, set, } = useLocalStore();
const [rtc, setRTC] = useState<RTCPeerConnection>(); const [_rtc, setRTC] = useState<RTCPeerConnection>();
const rtcRef = useRef<RTCPeerConnection | null>(null); const rtcRef = useRef<RTCPeerConnection | null>(null);
const [clients, setClients] = useState<Client[]>([]); const [clients, setClients] = useState<Client[]>([]);
const {connect, close} = useWebsocket({}); const {connect, close} = useWebsocket({});
const [open, setOpen] = useState<{ send: boolean; receive: boolean }>({send: false, receive: false}); const [open, setOpen] = useState<{ send: boolean; receive: boolean }>({send: false, receive: false});
const [receivedMessage, setReceivedMessage] = useState<ReceivedMessage | null>(null); const [receivedMessage, setReceivedMessage] = useState<ReceivedMessage | null>(null);
const [showMessageDialog, setShowMessageDialog] = useState(false); const [showMessageDialog, setShowMessageDialog] = useState(false);
const [receivingFile, setReceivingFile] = useState<ReceivedMessage | null>(null);
// RTC处理器的回调函数 - 使用useCallback确保稳定性 // RTC处理器的回调函数 - 使用useCallback确保稳定性
const onChannelOpen = useCallback((type: 'sender' | 'receiver') => { const onChannelOpen = useCallback((type: 'sender' | 'receiver') => {
@ -71,9 +72,16 @@ export const LocalSharing: React.FC = () => {
}, []); }, []);
const onMessageReceived = useCallback((message: ReceivedMessage) => { const onMessageReceived = useCallback((message: ReceivedMessage) => {
console.log('[D] Message received:', message); if (message.isFile && message.receiving) {
setReceivedMessage(message); setReceivingFile(message);
setShowMessageDialog(true); } else if (message.isFile && !message.receiving) {
setReceivingFile(null);
setReceivedMessage(message);
setShowMessageDialog(true);
} else {
setReceivedMessage(message);
setShowMessageDialog(true);
}
}, []); }, []);
const onChannelClose = useCallback(() => { const onChannelClose = useCallback(() => {
@ -93,7 +101,6 @@ export const LocalSharing: React.FC = () => {
// 更新RTC处理器的回调函数 // 更新RTC处理器的回调函数
useEffect(() => { useEffect(() => {
if (rtcHandlerRef.current) { if (rtcHandlerRef.current) {
console.log('[D] Updating RTC handler callbacks');
rtcHandlerRef.current.updateCallbacks(rtcCallbacks); rtcHandlerRef.current.updateCallbacks(rtcCallbacks);
} }
}, [rtcCallbacks]); }, [rtcCallbacks]);
@ -108,7 +115,6 @@ export const LocalSharing: React.FC = () => {
const handleWSEvent = async (e: MessageEvent) => { const handleWSEvent = async (e: MessageEvent) => {
const msgData = JSON.parse(e.data); const msgData = JSON.parse(e.data);
console.log('[D] ws event msg =', msgData);
if (msgData.type === "enter" || msgData.type === "leave") { if (msgData.type === "enter" || msgData.type === "leave") {
await updateClients(); await updateClients();
@ -129,7 +135,6 @@ export const LocalSharing: React.FC = () => {
const handleBubbleClick = async (bubble: any) => { const handleBubbleClick = async (bubble: any) => {
setOpen({send: true, receive: false}); setOpen({send: true, receive: false});
console.log('[D] Bubble clicked:', bubble.id, 'Current RTC handler:', rtcHandlerRef.current);
if (rtcHandlerRef.current) { if (rtcHandlerRef.current) {
try { try {
await rtcHandlerRef.current.handleBubbleClick(bubble.id, id); await rtcHandlerRef.current.handleBubbleClick(bubble.id, id);
@ -165,7 +170,6 @@ export const LocalSharing: React.FC = () => {
const response = await fetch('/api/ulocal/register', {method: 'POST'}); const response = await fetch('/api/ulocal/register', {method: 'POST'});
const data = ((await response.json()) as Resp<{ id: string; name: string }>).data; const data = ((await response.json()) as Resp<{ id: string; name: string }>).data;
set(data.id, data.name); set(data.id, data.name);
console.log(`[D] register id = ${data.id}`);
connect(`/api/ulocal/ws?id=${data.id}`, {fn: handleWSEvent}); connect(`/api/ulocal/ws?id=${data.id}`, {fn: handleWSEvent});
await updateClients(); await updateClients();
@ -174,7 +178,6 @@ export const LocalSharing: React.FC = () => {
setRTC(_rtc); setRTC(_rtc);
// 在RTC连接创建后立即创建处理器实例 // 在RTC连接创建后立即创建处理器实例
console.log('[D] Creating RTC handler after connection setup');
rtcHandlerRef.current = new RTCHandler(rtcRef, rtcCallbacks); rtcHandlerRef.current = new RTCHandler(rtcRef, rtcCallbacks);
return () => { return () => {
@ -189,6 +192,14 @@ export const LocalSharing: React.FC = () => {
const bubbles = generateBubbles(clients, id); const bubbles = generateBubbles(clients, id);
useEffect(() => {
if (receivingFile && receivingFile.isFile && !receivingFile.receiving) {
setReceivingFile(null);
setReceivedMessage(receivingFile);
setShowMessageDialog(true);
}
}, [receivingFile]);
return ( return (
<div className={classes.container}> <div className={classes.container}>
<CloudBackground/> <CloudBackground/>
@ -209,8 +220,17 @@ export const LocalSharing: React.FC = () => {
open={open.send} open={open.send}
onSend={handleSend} onSend={handleSend}
onClose={() => setOpen({send: false, receive: false})} onClose={() => setOpen({send: false, receive: false})}
name={name}
/> />
{/* 文件接收进度弹窗 */}
{receivingFile && (
<MessageDialog
open={true}
message={receivingFile}
onClose={() => setReceivingFile(null)}
/>
)}
<MessageDialog <MessageDialog
open={showMessageDialog} open={showMessageDialog}

View File

@ -136,7 +136,6 @@ const PanelLeftUpload: React.FC<{ set_code: (code:string) => void }> = ({set_cod
} }
function onFileChange(e: React.ChangeEvent<HTMLInputElement>) { function onFileChange(e: React.ChangeEvent<HTMLInputElement>) {
console.log('[D] onFileChange: e =', e)
setFile(e.currentTarget.files ? e.currentTarget.files[0] : null) setFile(e.currentTarget.files ? e.currentTarget.files[0] : null)
} }

View File

@ -42,7 +42,6 @@ export const PanelRight = () => {
async function onFetchFile() { async function onFetchFile() {
const url = `/ushare/${code}` const url = `/ushare/${code}`
console.log('[D] onFetchFile: url =', url)
const link = document.createElement('a'); const link = document.createElement('a');
link.href = url; link.href = url;
document.body.appendChild(link); document.body.appendChild(link);

View File

@ -1,13 +1,12 @@
import {useState} from "react"; import {useState} from "react";
import {createUseStyles} from "react-jss"; import {createUseStyles} from "react-jss";
import {Dialog} from "../../component/dialog/dialog.tsx";
const useClass = createUseStyles({ const useClass = createUseStyles({
container: {} container: {}
}) })
export const TestPage = () => { export const TestPage = () => {
const classes = useClass() const classes = useClass()
const [open, setOpen] = useState<boolean>(false) const [_open, setOpen] = useState<boolean>(false)
const handleOpen = () => { const handleOpen = () => {
setOpen(true) setOpen(true)
@ -15,8 +14,5 @@ export const TestPage = () => {
return <div className={classes.container}> return <div className={classes.container}>
<button onClick={handleOpen}>open</button> <button onClick={handleOpen}>open</button>
<Dialog open={open} title="hello world" >
<input />
</Dialog>
</div> </div>
} }

View File

@ -8,7 +8,7 @@ export interface LocalStore {
setChannel: (chan?: RTCDataChannel) => void; setChannel: (chan?: RTCDataChannel) => void;
} }
export const useLocalStore = create<LocalStore>()((_set, get) => ({ export const useLocalStore = create<LocalStore>()((_set, _get) => ({
id: '', id: '',
name: '', name: '',
set: (id: string, name: string) => { set: (id: string, name: string) => {

View File

@ -167,8 +167,6 @@
const text = inputBox.value; const text = inputBox.value;
const files = fileInput.files; const files = fileInput.files;
console.log('文本内容:', text);
console.log('发送文件:', Array.from(files));
// 清空内容 // 清空内容
inputBox.value = ''; inputBox.value = '';