diff --git a/server/api/api.go b/server/api/api.go index 0059ed2..0c33bac 100644 --- a/server/api/api.go +++ b/server/api/api.go @@ -45,6 +45,8 @@ func NewApi(rdb *redis.Client, secret string, live777Url string, live777Token st r.Get("/login/userlist", handle.UserList) r.Patch("/login/offline", handle.UpdateUserList) + r.Post("/login/invite", handle.Invite) + r.Patch("/login/invitee", handle.GetInvitation) r.Post("/room/", handle.CreateRoom) r.Get("/room/{roomId}", handle.ShowRoom) //r.Patch("/room/{roomId}", handle.UpdateRoom) diff --git a/server/api/v1/invite.go b/server/api/v1/invite.go new file mode 100644 index 0000000..5826516 --- /dev/null +++ b/server/api/v1/invite.go @@ -0,0 +1,79 @@ +package v1 + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "woom/server/model" + + "github.com/redis/go-redis/v9" +) + +func (h *Handler) Invite(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + // 解析请求体中的JSON数据 + var inviteData struct { + MeetingId string `json:"meetingId"` + InviterId string `json:"inviterId"` + InviteeId string `json:"inviteeId"` + } + + if err := json.NewDecoder(r.Body).Decode(&inviteData); err != nil { + http.Error(w, "Invalid request payload", http.StatusBadRequest) + return + } + + inviteValue := fmt.Sprintf("%s %s", inviteData.MeetingId, inviteData.InviterId) + + err := h.rdb.HSet(ctx, model.InvitationKey, inviteData.InviteeId, inviteValue).Err() + if err != nil { + log.Printf("Failed to save invitation for user %s: %v", inviteData.InviteeId, err) + http.Error(w, "Failed to store invitation", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + response := map[string]string{ + "success": "true", + "message": "Invitation sent successfully", + } + if err := json.NewEncoder(w).Encode(response); err != nil { + log.Printf("Failed to encode response: %v", err) + http.Error(w, "Failed to send response", http.StatusInternalServerError) + } +} + +func (h *Handler) GetInvitation(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var reqBody struct { + InviteeId string `json:"inviteeId"` + } + + if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + inviteeId := reqBody.InviteeId + value, err := h.rdb.HGet(ctx, model.InvitationKey, inviteeId).Result() + + if err == redis.Nil { + return + } else if err != nil { + return + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(map[string]string{"value": value}); err != nil { + http.Error(w, "Failed to encode response", http.StatusInternalServerError) + return + } + + if err := h.rdb.HDel(ctx, model.InvitationKey, inviteeId).Err(); err != nil { + log.Printf("Failed to delete inviteeId %s from invitation: %v\n", inviteeId, err) + } +} diff --git a/server/model/redis.go b/server/model/redis.go index e79c142..c305438 100644 --- a/server/model/redis.go +++ b/server/model/redis.go @@ -10,6 +10,7 @@ import ( const ( UserStorageKey = "user_storage" UserOnlineStatusKey = "user_online_status" + InvitationKey = "invitation" ) func ClearRedis(rdb *redis.Client) { @@ -43,7 +44,6 @@ func InitUserData(rdb *redis.Client) { log.Printf("Failed to set user %s online status: %v\n", user, err) } } - } func GenerateUsers() map[string]string { diff --git a/webapp/components/Invite.tsx b/webapp/components/Invite.tsx index a651e0a..640f1b6 100644 --- a/webapp/components/Invite.tsx +++ b/webapp/components/Invite.tsx @@ -1,37 +1,37 @@ -import { useState } from 'react' -import { sendInvite } from '../lib/api' - -interface InviteProps { - meetingId: string; - inviterId: string; - inviteeId: string; -} - -export default function Invite({ meetingId, inviterId, inviteeId }: InviteProps) { - const [isInvited, setIsInvited] = useState(false) - - const handleInvite = async () => { - try { - const response = await sendInvite(meetingId, inviterId, inviteeId) - if (response.success) { - setIsInvited(true) - console.log('Invite sent successfully') - } else { - console.error('Failed to send invite') - } - } catch (error) { - console.error('Error sending invite:', error) - } - } - - return ( -
- -
- ) -} +import { useState } from 'react' +import { sendInvite } from '../lib/api' + +interface InviteProps { + meetingId: string; + inviterId: string; + inviteeId: string; +} + +export default function Invite({ meetingId, inviterId, inviteeId }: InviteProps) { + const [isInvited, setIsInvited] = useState(false) + + const handleInvite = async () => { + try { + const response = await sendInvite(meetingId, inviterId, inviteeId) + if (response.success) { + setIsInvited(true) + console.log('Invite sent successfully') + } else { + console.error('Failed to send invite') + } + } catch (error) { + console.error('Error sending invite:', error) + } + } + + return ( +
+ +
+ ) +} diff --git a/webapp/components/userlist.tsx b/webapp/components/userlist.tsx index 588b2d6..f04d47c 100644 --- a/webapp/components/userlist.tsx +++ b/webapp/components/userlist.tsx @@ -1,82 +1,100 @@ -import { useEffect, useState } from 'react' -import { getUserOnlineStatus, updateUserStatus } from '../lib/api' -import { getStorage } from '../lib/storage' - -export default function UserList() { - const [userStatus, setUserStatus] = useState<{ [userId: string]: string }>({}) - const [isOpen, setIsOpen] = useState(false) - - const fetchUserStatus = async () => { - try { - const status = await getUserOnlineStatus() - setUserStatus(status) - } catch (error) { - console.error('Failed to fetch user status:', error) - } - } - - useEffect(() => { - fetchUserStatus() - const interval = setInterval(fetchUserStatus, 5000) - return () => clearInterval(interval) - }, []) - - useEffect(() => { - const cleanup = async () => { - const userId = getStorage()?.userId - updateUserStatus(userId, '0') - } - - window.addEventListener('beforeunload', cleanup) - window.addEventListener('unload', cleanup) - return () => { - window.removeEventListener('beforeunload', cleanup) - window.removeEventListener('unload', cleanup) - } - }, []) - - const sortedUserStatus = Object.keys(userStatus) - .sort((_, b) => (userStatus[b] === '1' ? 1 : -1)) - .map((userId) => ({ - userId, - status: userStatus[userId], - })) - - return ( -
- - - {isOpen && ( -
-

User Online Status

-
    - {sortedUserStatus.map(({ userId, status }) => ( -
  • - {userId} -
    - {status === '1' ? ( - ✔️ - ) : ( - - )} - - {status === '1' ? 'Online' : 'Offline'} - -
    -
  • - ))} -
-
- )} -
- ) -} +import { useEffect, useState } from 'react' +import { useAtom } from 'jotai' +import { getUserOnlineStatus, updateUserStatus } from '../lib/api' +import { getStorage } from '../lib/storage' +import { meetingIdAtom } from '../store/atom' +import Invite from './Invite' + +export default function UserList() { + const [userStatus, setUserStatus] = useState<{ [userId: string]: string }>({}) + const [isOpen, setIsOpen] = useState(false) + + const [meeting] = useAtom(meetingIdAtom) + + const inviterId = getStorage()?.userId + const meetingId = getStorage()?.meeting + + const fetchUserStatus = async () => { + try { + const status = await getUserOnlineStatus() + setUserStatus(status) + } catch (error) { + console.error('Failed to fetch user status:', error) + } + } + + useEffect(() => { + fetchUserStatus() + const interval = setInterval(fetchUserStatus, 5000) + return () => clearInterval(interval) + }, []) + + useEffect(() => { + const cleanup = async () => { + updateUserStatus(inviterId, '0') + } + + window.addEventListener('beforeunload', cleanup) + window.addEventListener('unload', cleanup) + return () => { + window.removeEventListener('beforeunload', cleanup) + window.removeEventListener('unload', cleanup) + } + }, []) + + const sortedUserStatus = Object.keys(userStatus) + .sort((_, b) => (userStatus[b] === '1' ? 1 : -1)) + .map((userId) => ({ + userId, + status: userStatus[userId], + })) + + return ( +
+ + + {isOpen && ( +
+

User Online Status

+
    + {sortedUserStatus.map(({ userId, status }) => ( +
  • + {userId} + +
    + {status === '1' ? ( + ✔️ + ) : ( + + )} + + {status === '1' ? 'Online' : 'Offline'} + +
    + + {status === '1' && meeting ? ( + + ) : ( + Disabled + )} +
  • + ))} +
+
+ )} +
+ ) +} diff --git a/webapp/components/window.tsx b/webapp/components/window.tsx new file mode 100644 index 0000000..5403292 --- /dev/null +++ b/webapp/components/window.tsx @@ -0,0 +1,77 @@ +import { useState, useEffect } from 'react' +import { getInvitation } from '../lib/api' +import { setStorageMeeting } from '../lib/storage' +import { useAtom } from 'jotai' +import { locationAtom, meetingIdAtom } from '../store/atom' + +interface InviteWindowProps { + inviteeId: string +} + +export default function InviteWindow({ inviteeId }: InviteWindowProps) { + const [invitation, setInvitation] = useState(null) + const [isOpen, setIsOpen] = useState(false) + const [_, setLoc] = useAtom(locationAtom) + const [__, setAtomMeetingId] = useAtom(meetingIdAtom) + + type invitation = { value: string } + + const checkInvitation = async () => { + try { + const value = await getInvitation(inviteeId) + console.log('checkInvitation', value) + if (value) { + setInvitation(value) + setIsOpen(true) + } + } catch { /* empty */ } + } + + useEffect(() => { + checkInvitation() + const interval = setInterval(checkInvitation, 5000) + return () => clearInterval(interval) + }, [inviteeId]) + + const handleAccept = () => { + console.log('Accepted the invitation') + const invitationValue = invitation?.value + const roomId = invitationValue.split(' ')[0] + setStorageMeeting(roomId) + setAtomMeetingId(roomId) + setLoc(prev => ({ ...prev, pathname: `/${roomId}` })) + setIsOpen(false) + } + + const handleReject = () => { + console.log('Rejected the invitation') + setIsOpen(false) + } + + return ( + <> + {isOpen && invitation && ( +
+

You have an invitation!

+

+ {invitation.value} +

+
+ + +
+
+ )} + + ) +} diff --git a/webapp/lib/api.ts b/webapp/lib/api.ts index f13c3fb..591c7c4 100644 --- a/webapp/lib/api.ts +++ b/webapp/lib/api.ts @@ -150,9 +150,10 @@ async function updateUserStatus(userId: string, status: string): Promise { } async function sendInvite(meetingId: string, inviterId: string, inviteeId: string): Promise<{ success: boolean; message: string }> { - return (await fetch('/invite', { + return (await fetch('/login/invite', { method: 'POST', headers: { + 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ @@ -163,6 +164,17 @@ async function sendInvite(meetingId: string, inviterId: string, inviteeId: strin })).json() } +async function getInvitation(inviteeId: string): Promise { + return (await fetch('/login/invitee', { + method: 'PATCH', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ inviteeId }), + })).json() +} + export { setRoomId, @@ -182,6 +194,7 @@ export { getUserOnlineStatus, updateUserStatus, sendInvite, + getInvitation, StreamState, } diff --git a/webapp/lib/storage.ts b/webapp/lib/storage.ts index 145c219..fde8711 100644 --- a/webapp/lib/storage.ts +++ b/webapp/lib/storage.ts @@ -5,7 +5,7 @@ const NameKey = 'name' const UserId = 'userId' interface Storage { - meeting?: string, + meeting: string, stream?: string, token?: string, name?: string, diff --git a/webapp/pages/meeting.tsx b/webapp/pages/meeting.tsx index 50bf1e2..d21e035 100644 --- a/webapp/pages/meeting.tsx +++ b/webapp/pages/meeting.tsx @@ -2,15 +2,19 @@ import { useAtom } from 'jotai' import { meetingJoinedAtom } from '../store/atom' import Layout from '../components/layout' import Prepare from '../components/prepare' +import InviteWindow from '../components/window' +import { getStorage } from '../lib/storage' export default function Meeting(props: { meetingId: string }) { const [meetingJoined] = useAtom(meetingJoinedAtom) + const inviteeId = getStorage()?.userId return (
{meetingJoined ? : } +
) } diff --git a/webapp/pages/welcome.tsx b/webapp/pages/welcome.tsx index d057ca3..e52f960 100644 --- a/webapp/pages/welcome.tsx +++ b/webapp/pages/welcome.tsx @@ -1,11 +1,15 @@ import Login from '../components/login' import Join from '../components/join' +import InviteWindow from '../components/window' import { useAtom } from 'jotai' import { isLoggedInAtom } from '../store/atom' +import { getStorage } from '../lib/storage' export default function Welcome() { const [isLoggedIn] = useAtom(isLoggedInAtom) + const inviteeId = getStorage()?.userId + return (
@@ -16,7 +20,12 @@ export default function Welcome() { { !isLoggedIn ? - : + : ( + <> + + + + ) }