wip: v0.2.6
This commit is contained in:
parent
b8645a68ed
commit
5bc695bde3
@ -3,8 +3,8 @@ import { createRoot } from 'react-dom/client'
|
|||||||
import './index.css'
|
import './index.css'
|
||||||
import {createBrowserRouter, RouterProvider} from "react-router-dom";
|
import {createBrowserRouter, RouterProvider} from "react-router-dom";
|
||||||
import {Login} from "./page/login.tsx";
|
import {Login} from "./page/login.tsx";
|
||||||
import {FileSharing} from "./page/share.tsx";
|
import {FileSharing} from "./page/share/share.tsx";
|
||||||
import {LocalSharing} from "./page/local.tsx";
|
import {LocalSharing} from "./page/local/local.tsx";
|
||||||
|
|
||||||
const container = document.getElementById('root')
|
const container = document.getElementById('root')
|
||||||
const root = createRoot(container!)
|
const root = createRoot(container!)
|
||||||
|
46
frontend/src/page/local/hook/register.tsx
Normal file
46
frontend/src/page/local/hook/register.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import {useState, useCallback,} from 'react';
|
||||||
|
|
||||||
|
export const useRegister = () => {
|
||||||
|
const [id, setId] = useState('');
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<Error | null>(null);
|
||||||
|
|
||||||
|
// 封装为可手动触发的异步函数
|
||||||
|
const register = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/ulocal/register', {
|
||||||
|
method: 'POST'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await response.json()).data;
|
||||||
|
setId(data.id);
|
||||||
|
setName(data.name);
|
||||||
|
|
||||||
|
return {id: data.id, name: data.name};
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err : new Error('Request failed'));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 如果需要自动执行(组件挂载时自动注册)
|
||||||
|
// useEffect(() => { register() }, [register]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
register, // 手动触发函数
|
||||||
|
loading,
|
||||||
|
error
|
||||||
|
};
|
||||||
|
};
|
@ -1,9 +1,9 @@
|
|||||||
import {CloudBackground} from "../component/fluid/cloud.tsx";
|
import {CloudBackground} from "../../component/fluid/cloud.tsx";
|
||||||
import {useEffect, useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
import {createUseStyles} from "react-jss";
|
import {createUseStyles} from "react-jss";
|
||||||
import {useWebsocket} from "../hook/websocket/u-ws.tsx";
|
import {useWebsocket} from "../../hook/websocket/u-ws.tsx";
|
||||||
import {Resp} from "../interface/response.ts";
|
import {Resp} from "../../interface/response.ts";
|
||||||
import {message} from "../hook/message/u-message.tsx";
|
import {useRegister} from "./hook/register.tsx";
|
||||||
|
|
||||||
const useClass = createUseStyles({
|
const useClass = createUseStyles({
|
||||||
'@global': {
|
'@global': {
|
||||||
@ -86,15 +86,6 @@ interface Client {
|
|||||||
candidate: RTCIceCandidateInit;
|
candidate: RTCIceCandidateInit;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Store {
|
|
||||||
client: Client | null
|
|
||||||
clients: Client[]
|
|
||||||
rtc: RTCPeerConnection | null
|
|
||||||
ch: RTCDataChannel | null
|
|
||||||
offer: RTCSessionDescription | null
|
|
||||||
candidate: RTCIceCandidate | null
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupDataChannel(ch: RTCDataChannel) {
|
function setupDataChannel(ch: RTCDataChannel) {
|
||||||
ch.onopen = () => console.log('通道已打开!');
|
ch.onopen = () => console.log('通道已打开!');
|
||||||
ch.onmessage = (e) => handleFileChunk(e.data);
|
ch.onmessage = (e) => handleFileChunk(e.data);
|
||||||
@ -109,7 +100,10 @@ function handleFileChunk(chunk: any) {
|
|||||||
|
|
||||||
export const LocalSharing: React.FC = () => {
|
export const LocalSharing: React.FC = () => {
|
||||||
const classes = useClass();
|
const classes = useClass();
|
||||||
const [rtcStore, setRTCStore] = useState<Store>({} as Store)
|
const {id, name, register } = useRegister()
|
||||||
|
const [rtc, setRTC] = useState<RTCPeerConnection| null>(null);
|
||||||
|
const [client, setClient] = useState<Client| null>(null);
|
||||||
|
const [clients, setClients] = useState<Client[]>([]);
|
||||||
const {connect, close} = useWebsocket({})
|
const {connect, close} = useWebsocket({})
|
||||||
|
|
||||||
// 生成随机颜色
|
// 生成随机颜色
|
||||||
@ -121,8 +115,8 @@ export const LocalSharing: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 防碰撞位置生成
|
// 防碰撞位置生成
|
||||||
const generateBubbles = (cs: Client[]) => {
|
const generateBubbles = () => {
|
||||||
if (!cs) return []
|
if (!clients) return []
|
||||||
|
|
||||||
const BUBBLE_SIZE = 100;
|
const BUBBLE_SIZE = 100;
|
||||||
const centerX = window.innerWidth / 2;
|
const centerX = window.innerWidth / 2;
|
||||||
@ -132,11 +126,11 @@ export const LocalSharing: React.FC = () => {
|
|||||||
let currentRadius = 0;
|
let currentRadius = 0;
|
||||||
let angleStep = (2 * Math.PI) / 6; // 初始6个位置
|
let angleStep = (2 * Math.PI) / 6; // 初始6个位置
|
||||||
|
|
||||||
for (let index = 0; index < cs.length; index++) {
|
for (let index = 0; index < clients.length; index++) {
|
||||||
let attempt = 0;
|
let attempt = 0;
|
||||||
let validPosition = false;
|
let validPosition = false;
|
||||||
|
|
||||||
if (cs[index].id == rtcStore.client?.id) {
|
if (clients[index].id == id) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -164,8 +158,8 @@ export const LocalSharing: React.FC = () => {
|
|||||||
|
|
||||||
if (inBounds && !collision) {
|
if (inBounds && !collision) {
|
||||||
bubbles.push({
|
bubbles.push({
|
||||||
id: cs[index].id,
|
id: clients[index].id,
|
||||||
name: cs[index].name,
|
name: clients[index].name,
|
||||||
x: x,
|
x: x,
|
||||||
y: y,
|
y: y,
|
||||||
color: generateColor(),
|
color: generateColor(),
|
||||||
@ -181,6 +175,7 @@ export const LocalSharing: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('[D] generated bubbles =', bubbles);
|
||||||
return bubbles;
|
return bubbles;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -188,9 +183,8 @@ export const LocalSharing: React.FC = () => {
|
|||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
const res = await fetch(`/api/ulocal/clients`)
|
const res = await fetch(`/api/ulocal/clients`)
|
||||||
const jes = await res.json() as Resp<Client[]>
|
const jes = await res.json() as Resp<Client[]>
|
||||||
setRTCStore(val => {
|
console.log('[D] update clients =', jes)
|
||||||
return {...val, clients: jes.data}
|
setClients(jes.data)
|
||||||
})
|
|
||||||
}, 500)
|
}, 500)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -200,9 +194,7 @@ export const LocalSharing: React.FC = () => {
|
|||||||
switch (msg.type) {
|
switch (msg.type) {
|
||||||
case "register":
|
case "register":
|
||||||
const reg_resp = JSON.parse(e.data) as { body: Client }
|
const reg_resp = JSON.parse(e.data) as { body: Client }
|
||||||
setRTCStore(val => {
|
setClient(reg_resp.body)
|
||||||
return {...val, client: reg_resp.body}
|
|
||||||
})
|
|
||||||
break
|
break
|
||||||
case "enter":
|
case "enter":
|
||||||
await updateClients()
|
await updateClients()
|
||||||
@ -211,126 +203,51 @@ export const LocalSharing: React.FC = () => {
|
|||||||
await updateClients()
|
await updateClients()
|
||||||
break
|
break
|
||||||
case "offer":
|
case "offer":
|
||||||
const res_offer = JSON.parse(e.data) as {
|
|
||||||
offer: RTCSessionDescriptionInit,
|
|
||||||
id: number,
|
|
||||||
candidate: RTCIceCandidateInit
|
|
||||||
}
|
|
||||||
console.log('[D] offer res =', res_offer)
|
|
||||||
await rtcStore.rtc?.setRemoteDescription(res_offer.offer)
|
|
||||||
const answer = await rtcStore.rtc?.createAnswer()
|
|
||||||
await rtcStore.rtc?.setLocalDescription(answer)
|
|
||||||
await fetch('/api/ulocal/answer', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {"Content-Type": "application/json"},
|
|
||||||
body: JSON.stringify({id: rtcStore.client?.id, answer: answer})
|
|
||||||
})
|
|
||||||
break
|
break
|
||||||
case "answer":
|
case "answer":
|
||||||
const res_answer = JSON.parse(e.data) as { answer: RTCSessionDescriptionInit, id: number }
|
|
||||||
await rtcStore.rtc?.setRemoteDescription(res_answer.answer)
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// const fn = async () => {
|
|
||||||
// // const rtc = new RTCPeerConnection({iceServers: [{urls: "stun:stun.qq.com:3478"}]})
|
|
||||||
// const rtc = new RTCPeerConnection({})
|
|
||||||
// rtc.onconnectionstatechange = () => {
|
|
||||||
// console.log('rtc connection state =', rtc.connectionState)
|
|
||||||
// }
|
|
||||||
// const dataChannel = rtc.createDataChannel('fileTransfer', {ordered: true});
|
|
||||||
// setupDataChannel(dataChannel);
|
|
||||||
// const waitCandidate = (): Promise<RTCIceCandidate | null> => {
|
|
||||||
// return new Promise<RTCIceCandidate | null>(resolve => {
|
|
||||||
// rtc.onicecandidate = (e) => {
|
|
||||||
// resolve(e.candidate)
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// const waitNegotiation = (): Promise<void> => {
|
|
||||||
// return new Promise<void>(resolve => {
|
|
||||||
// rtc.onnegotiationneeded = async () => {
|
|
||||||
// const _offer = await rtc.createOffer()
|
|
||||||
// await rtc.setLocalDescription(_offer)
|
|
||||||
// resolve()
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// await waitNegotiation()
|
|
||||||
// const candidate: RTCIceCandidate | null = await waitCandidate();
|
|
||||||
//
|
|
||||||
// const res = await fetch("/api/ulocal/register", {
|
|
||||||
// method: "POST",
|
|
||||||
// headers: {"Content-Type": "application/json"},
|
|
||||||
// body: JSON.stringify({candidate: candidate})
|
|
||||||
// })
|
|
||||||
// const jes = await res.json() as Resp<Client>;
|
|
||||||
//
|
|
||||||
// console.log('[D] register resp =', jes)
|
|
||||||
//
|
|
||||||
// if (!jes.data.id) {
|
|
||||||
// message.error("注册失败")
|
|
||||||
// throw new Error("register failed")
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// setRTCStore(val => {
|
|
||||||
// return {
|
|
||||||
// ...val,
|
|
||||||
// client: jes.data,
|
|
||||||
// candidate: candidate,
|
|
||||||
// offer: rtc.localDescription,
|
|
||||||
// rtc: rtc,
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
//
|
|
||||||
// const api = `${window.location.protocol === 'https' ? 'wss' : 'ws'}://${window.location.host}/api/ulocal/ws?id=${jes.data.id}`
|
|
||||||
// console.log('[D] websocket url =', api)
|
|
||||||
// connect(api, {fn: handleWSEvent})
|
|
||||||
//
|
|
||||||
// await updateClients()
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// fn()
|
|
||||||
//
|
|
||||||
// return () => close();
|
|
||||||
// }, []
|
|
||||||
// );
|
|
||||||
|
|
||||||
// 气泡点击处理
|
// 气泡点击处理
|
||||||
const handleBubbleClick = async (bubble: Bubble) => {
|
const handleBubbleClick = async (bubble: Bubble) => {
|
||||||
const offer = await rtcStore.rtc?.createOffer()
|
const offer = await rtc?.createOffer()
|
||||||
await rtcStore.rtc?.setLocalDescription(offer)
|
await rtc?.setLocalDescription(offer)
|
||||||
// await fetch('/api/ulocal/offer', {
|
const data = {
|
||||||
// method: 'POST',
|
id: bubble.id,
|
||||||
// headers: {"Content-Type": "application/json"},
|
offer: offer,
|
||||||
// body: JSON.stringify({
|
}
|
||||||
// id: bubble.id,
|
await fetch('/api/ulocal/offer', {
|
||||||
// offer: rtcStore.offer,
|
method: 'POST',
|
||||||
// candidate: rtcStore.candidate,
|
headers: {"Content-Type": "application/json"},
|
||||||
// })
|
body: JSON.stringify(data)
|
||||||
// })
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fn = async () => {
|
const fn = async () => {
|
||||||
connect("/api/ulocal/ws", {fn: handleWSEvent})
|
const reg_data = await register();
|
||||||
|
console.log(`[D] register id = ${reg_data?.id}`);
|
||||||
|
connect(`/api/ulocal/ws?id=${reg_data?.id}`, {fn: handleWSEvent})
|
||||||
await updateClients()
|
await updateClients()
|
||||||
const rtc = new RTCPeerConnection({iceServers: [{urls: "stun:stun.qq.com:3478"}]})
|
const rtc = new RTCPeerConnection({iceServers: [{urls: "stun:stun.qq.com:3478"}]})
|
||||||
setRTCStore(val => {return {...val, rtc: rtc}})
|
rtc.onicecandidate = async (e) => {
|
||||||
|
if(e.candidate) {
|
||||||
|
await fetch('/api/ulocal/candidate')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setRTC(rtc)
|
||||||
|
|
||||||
|
return () => {close();}
|
||||||
};
|
};
|
||||||
fn()
|
fn()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return <div className={classes.container}>
|
return <div className={classes.container}>
|
||||||
<CloudBackground/>
|
<CloudBackground/>
|
||||||
<h1 className={classes.title}>{rtcStore.client?.name}</h1>
|
<h1 className={classes.title}>{name}</h1>
|
||||||
{rtcStore.clients && generateBubbles(rtcStore.clients).map(bubble => {
|
{clients && generateBubbles().map(bubble => {
|
||||||
// const client = clients.find(c => c.id === bubble.id);
|
return client ? (
|
||||||
return rtcStore.client ? (
|
|
||||||
<div
|
<div
|
||||||
key={bubble.id}
|
key={bubble.id}
|
||||||
className={classes.bubble}
|
className={classes.bubble}
|
@ -1,9 +1,9 @@
|
|||||||
import {createUseStyles} from "react-jss";
|
import {createUseStyles} from "react-jss";
|
||||||
import {UButton} from "../../component/button/u-button.tsx";
|
import {UButton} from "../../../component/button/u-button.tsx";
|
||||||
import React, {useState} from "react";
|
import React, {useState} from "react";
|
||||||
import {useStore} from "../../store/share.ts";
|
import {useStore} from "../../../store/share.ts";
|
||||||
import {message} from "../../hook/message/u-message.tsx";
|
import {message} from "../../../hook/message/u-message.tsx";
|
||||||
import {useFileUpload} from "../../api/upload.ts";
|
import {useFileUpload} from "../../../api/upload.ts";
|
||||||
|
|
||||||
const useUploadStyle = createUseStyles({
|
const useUploadStyle = createUseStyles({
|
||||||
container: {
|
container: {
|
@ -1,6 +1,6 @@
|
|||||||
import {createUseStyles} from "react-jss";
|
import {createUseStyles} from "react-jss";
|
||||||
import {UButton} from "../../component/button/u-button.tsx";
|
import {UButton} from "../../../component/button/u-button.tsx";
|
||||||
import {useStore} from "../../store/share.ts";
|
import {useStore} from "../../../store/share.ts";
|
||||||
|
|
||||||
const useStyle = createUseStyles({
|
const useStyle = createUseStyles({
|
||||||
container: {
|
container: {
|
@ -25,6 +25,7 @@ func Start(ctx context.Context) <-chan struct{} {
|
|||||||
|
|
||||||
{
|
{
|
||||||
api := app.Group("/api/ulocal")
|
api := app.Group("/api/ulocal")
|
||||||
|
api.Post("/register", handler.LocalRegister())
|
||||||
api.Post("/offer", handler.LocalOffer())
|
api.Post("/offer", handler.LocalOffer())
|
||||||
api.Post("/answer", handler.LocalAnswer())
|
api.Post("/answer", handler.LocalAnswer())
|
||||||
api.Get("/clients", handler.LocalClients())
|
api.Get("/clients", handler.LocalClients())
|
||||||
|
@ -129,11 +129,13 @@ type roomController struct {
|
|||||||
sync.Mutex
|
sync.Mutex
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
//rooms map[string]map[string]*roomClient // map[room_id(remote-IP)][Id]
|
//rooms map[string]map[string]*roomClient // map[room_id(remote-IP)][Id]
|
||||||
|
pre map[string]*roomClient
|
||||||
clients map[string]*roomClient
|
clients map[string]*roomClient
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
RoomController = &roomController{
|
RoomController = &roomController{
|
||||||
|
pre: make(map[string]*roomClient),
|
||||||
clients: make(map[string]*roomClient),
|
clients: make(map[string]*roomClient),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -142,10 +144,9 @@ func (rc *roomController) Start(ctx context.Context) {
|
|||||||
rc.ctx = ctx
|
rc.ctx = ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rc *roomController) Register(conn *websocket.Conn, ip, userAgent string) *roomClient {
|
func (rc *roomController) Register(ip, userAgent string) *roomClient {
|
||||||
nrc := &roomClient{
|
nrc := &roomClient{
|
||||||
controller: rc,
|
controller: rc,
|
||||||
conn: conn,
|
|
||||||
ClientType: ClientTypeDesktop,
|
ClientType: ClientTypeDesktop,
|
||||||
AppType: RoomAppTypeWeb,
|
AppType: RoomAppTypeWeb,
|
||||||
IP: ip,
|
IP: ip,
|
||||||
@ -163,16 +164,32 @@ func (rc *roomController) Register(conn *websocket.Conn, ip, userAgent string) *
|
|||||||
nrc.ClientType = ClientTypeTablet
|
nrc.ClientType = ClientTypeTablet
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug("controller.room: registry client, IP = %s, Id = %s, Name = %s", nrc.IP, nrc.Id, nrc.Name)
|
rc.Lock()
|
||||||
|
defer rc.Unlock()
|
||||||
|
|
||||||
nrc.start(rc.ctx)
|
rc.pre[nrc.Id] = nrc
|
||||||
|
|
||||||
nrc.msgChan <- map[string]any{"type": "register", "time": time.Now().UnixMilli(), "body": nrc}
|
return nrc
|
||||||
rc.Broadcast(map[string]any{"type": "enter", "time": time.Now().UnixMilli(), "body": nrc})
|
}
|
||||||
|
|
||||||
|
func (rc *roomController) Enter(conn *websocket.Conn, id string) *roomClient {
|
||||||
|
log.Debug("controller.room: registry client, id = %s", id)
|
||||||
|
|
||||||
rc.Lock()
|
rc.Lock()
|
||||||
|
defer rc.Unlock()
|
||||||
|
|
||||||
|
nrc, ok := rc.pre[id]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
nrc.conn = conn
|
||||||
|
nrc.start(rc.ctx)
|
||||||
|
|
||||||
|
rc.Broadcast(map[string]any{"type": "enter", "time": time.Now().UnixMilli(), "body": nrc})
|
||||||
|
|
||||||
|
delete(rc.pre, nrc.Id)
|
||||||
rc.clients[nrc.Id] = nrc
|
rc.clients[nrc.Id] = nrc
|
||||||
rc.Unlock()
|
|
||||||
|
|
||||||
return nrc
|
return nrc
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,19 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func LocalRegister() nf.HandlerFunc {
|
||||||
|
return func(c *nf.Ctx) error {
|
||||||
|
var (
|
||||||
|
ip = c.IP(true)
|
||||||
|
ua = c.Get("User-Agent")
|
||||||
|
)
|
||||||
|
|
||||||
|
client := controller.RoomController.Register(ip, ua)
|
||||||
|
|
||||||
|
return resp.Resp200(c, client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func LocalClients() nf.HandlerFunc {
|
func LocalClients() nf.HandlerFunc {
|
||||||
return func(c *nf.Ctx) error {
|
return func(c *nf.Ctx) error {
|
||||||
list := controller.RoomController.List()
|
list := controller.RoomController.List()
|
||||||
@ -27,10 +40,11 @@ func LocalWS() nf.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return func(c *nf.Ctx) error {
|
return func(c *nf.Ctx) error {
|
||||||
var (
|
id := c.Query("id")
|
||||||
ip = c.IP(true)
|
|
||||||
ua = c.Get("User-Agent")
|
if id == "" {
|
||||||
)
|
return c.Status(http.StatusBadRequest).JSON(map[string]string{"error": "id is empty"})
|
||||||
|
}
|
||||||
|
|
||||||
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -38,7 +52,7 @@ func LocalWS() nf.HandlerFunc {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
controller.RoomController.Register(conn, ip, ua)
|
controller.RoomController.Enter(conn, id)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user