diff --git a/server/api/api.go b/server/api/api.go index f96ec89..a4b1ac2 100644 --- a/server/api/api.go +++ b/server/api/api.go @@ -47,6 +47,8 @@ func NewApi(rdb *redis.Client, secret string, live777Url string, live777Token st r.Patch("/login/offline", handle.UpdateUserList) r.Post("/login/invite", handle.Invite) r.Patch("/login/invitee", handle.GetInvitation) + r.Post("/login/remove", handle.RemoveStream) + r.Patch("/login/makeremove", handle.MakeRemove) r.Post("/room/", handle.CreateRoom) r.Get("/room/{roomId}", handle.ShowRoom) //r.Patch("/room/{roomId}", handle.UpdateRoom) diff --git a/server/api/v1/login.go b/server/api/v1/login.go index 9e91fdc..6f36cac 100644 --- a/server/api/v1/login.go +++ b/server/api/v1/login.go @@ -42,6 +42,14 @@ type UpdateUserStatusRequest struct { Status string `json:"status"` } +type RemoveStreamRequest struct { + StreamId string `json:"streamId"` +} + +type MakeRemoveRequest struct { + StreamId string `json:"streamId"` +} + func (h *Handler) Login(w http.ResponseWriter, r *http.Request) { var loginReq LoginRequest @@ -234,3 +242,48 @@ func (h *Handler) Signup(w http.ResponseWriter, r *http.Request) { log.Printf("New user registered successfully: %s\n", signupReq.UserId) render.JSON(w, r, LoginResponse{Success: true, Message: "Registration successful! You can now login."}) } + +func (h *Handler) RemoveStream(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + var req RemoveStreamRequest + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request", http.StatusBadRequest) + return + } + + if err := h.rdb.SAdd(ctx, model.StreamRemovalKey, req.StreamId).Err(); err != nil { + http.Error(w, "Failed to add stream to removal set", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) +} + +func (h *Handler) MakeRemove(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + var req MakeRemoveRequest + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request", http.StatusBadRequest) + return + } + + isMember, err := h.rdb.SIsMember(ctx, model.StreamRemovalKey, req.StreamId).Result() + if err != nil { + http.Error(w, "Failed to check stream removal set", http.StatusInternalServerError) + return + } + + value := 0 + if isMember { + value = 1 + if err := h.rdb.SRem(ctx, model.StreamRemovalKey, req.StreamId).Err(); err != nil { + http.Error(w, "Failed to remove stream from set", http.StatusInternalServerError) + return + } + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]int{"value": value}) +} diff --git a/server/model/redis.go b/server/model/redis.go index 6a5480e..0bcd34b 100644 --- a/server/model/redis.go +++ b/server/model/redis.go @@ -11,6 +11,7 @@ const ( UserStorageKey = "user_storage" UserOnlineStatusKey = "user_online_status" InvitationKey = "invitation" + StreamRemovalKey = "stream_removal" ) func ClearRedis(rdb *redis.Client) { diff --git a/webapp/components/join.tsx b/webapp/components/join.tsx index f84d539..2e64871 100644 --- a/webapp/components/join.tsx +++ b/webapp/components/join.tsx @@ -3,6 +3,7 @@ import { useAtom } from 'jotai' import { locationAtom, meetingIdAtom, + adminAtom } from '../store/atom' import { getStorage, setStorage, delStorage, setStorageStream, setStorageMeeting } from '../lib/storage' import { newRoom, newUser, setApiToken, setRoomId } from '../lib/api' @@ -22,6 +23,7 @@ export const getLoginStatus = async () => { export default function Join() { const [loc, setLoc] = useAtom(locationAtom) const [__, setAtomMeetingId] = useAtom(meetingIdAtom) + const [_, setAdmin] = useAtom(adminAtom) const [tmpId, setTmpId] = useState('') const newMeeting = async () => { @@ -29,6 +31,7 @@ export default function Join() { const meetingId = (await newRoom()).roomId enterMeeting(meetingId) setRoomId(meetingId) + setAdmin(true) } const joinMeeting = async () => { @@ -39,6 +42,7 @@ export default function Join() { //}) enterMeeting(meetingId) setRoomId(meetingId) + setAdmin(false) } const enterMeeting = (meetingId: string) => { diff --git a/webapp/components/layout.tsx b/webapp/components/layout.tsx index 7304008..da239df 100644 --- a/webapp/components/layout.tsx +++ b/webapp/components/layout.tsx @@ -13,7 +13,7 @@ import { import copy from 'copy-to-clipboard' import SvgDone from './svg/done' import SvgEnd from './svg/end' -import { getRoom, delStream, Stream } from '../lib/api' +import { getRoom, delStream, Stream, makeRemove } from '../lib/api' import { getStorageStream } from '../lib/storage' export default function Layout(props: { meetingId: string }) { @@ -38,6 +38,11 @@ export default function Layout(props: { meetingId: string }) { return map }, {} as { [_: string]: Stream }) setRemoteUserStatus(r) + + const result = await makeRemove(localStreamId) + if (result.value === 1) { + await callEnd() + } } const callEnd = async () => { diff --git a/webapp/components/player/detail.tsx b/webapp/components/player/detail.tsx index 79b72fb..941e7fa 100644 --- a/webapp/components/player/detail.tsx +++ b/webapp/components/player/detail.tsx @@ -1,7 +1,24 @@ import { UserStatus } from '../../store/atom' +import { removeStream } from '../../lib/api' +import { useState } from 'react' +import { useAtom } from 'jotai' +import { adminAtom } from '../../store/atom' export default function Detail(props: { streamId: string, connStatus: string, userStatus: UserStatus, restart: () => void }) { const { streamId, connStatus, userStatus, restart } = props + const [isButtonDisabled, setIsButtonDisabled] = useState(false) + const [isAdmin] = useAtom(adminAtom) + + const handleDelete = () => { + if (!isButtonDisabled) { + removeStream(streamId) + setIsButtonDisabled(true) + setTimeout(() => { + setIsButtonDisabled(false) + }, 5000) + } + } + return (
{userStatus.name} @@ -21,6 +38,13 @@ export default function Detail(props: { streamId: string, connStatus: string, us

{userStatus.state}

+ {isAdmin && ( + + )}
) diff --git a/webapp/lib/api.ts b/webapp/lib/api.ts index 1af86ca..91767c5 100644 --- a/webapp/lib/api.ts +++ b/webapp/lib/api.ts @@ -210,6 +210,28 @@ async function signup(userId: string, password: string): Promise<{ success: bool return response.json() } +async function removeStream(streamId: string): Promise { + await fetch('/login/remove', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ streamId }), + }) +} + +async function makeRemove(localStreamId: string): Promise<{ value: number }> { + return (await fetch('/login/makeremove', { + method: 'PATCH', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ streamId: localStreamId }), + })).json() +} + export { setRoomId, setApiToken, @@ -223,6 +245,8 @@ export { newStream, setStream, delStream, + removeStream, + makeRemove, login, getUserOnlineStatus, diff --git a/webapp/store/atom.ts b/webapp/store/atom.ts index 75a0e3d..7e3d4d2 100644 --- a/webapp/store/atom.ts +++ b/webapp/store/atom.ts @@ -53,20 +53,21 @@ userPasswordAtom.debugLabel = 'userPasswordAtom' const isLoggedInAtom = atom(false) isLoggedInAtom.debugLabel = 'isLoggedInAtom' +const adminAtom = atom(false) +adminAtom.debugLabel = 'adminAtom' + export { locationAtom, - presentationStreamAtom, - meetingIdAtom, meetingJoinedAtom, + presentationStreamAtom, enabledPresentationAtom, deviceSpeakerAtom, speakerStatusAtom, - settingsEnabledScreenAtom, - userIdAtom, userPasswordAtom, + adminAtom, isLoggedInAtom, }