diff --git a/src/common/draggable-window/DraggableWindow.tsx b/src/common/draggable-window/DraggableWindow.tsx index 9bb7e34..be5e552 100644 --- a/src/common/draggable-window/DraggableWindow.tsx +++ b/src/common/draggable-window/DraggableWindow.tsx @@ -80,12 +80,10 @@ export const DraggableWindow: FC = props => { const windowHeight = elementRef.current.offsetHeight; const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; - const maxOutX = windowWidth * DRAG_OUTSIDE_PERCENT; const maxOutY = windowHeight * DRAG_OUTSIDE_PERCENT; - const clampedX = Math.max(-maxOutX, Math.min(newX, viewportWidth - windowWidth + maxOutX)); - const clampedY = Math.max(-maxOutY, Math.min(newY, viewportHeight - windowHeight + maxOutY)); + const clampedY = Math.max(BOUNDS_THRESHOLD_TOP, Math.min(newY, viewportHeight - windowHeight + maxOutY)); return { x: clampedX, y: clampedY }; }, []); diff --git a/src/components/toolbar/YouTubePlayerView.tsx b/src/components/toolbar/YouTubePlayerView.tsx index b06bc49..9b1507e 100644 --- a/src/components/toolbar/YouTubePlayerView.tsx +++ b/src/components/toolbar/YouTubePlayerView.tsx @@ -1,17 +1,8 @@ import { ControlYoutubeDisplayPlaybackMessageComposer, YouTubeRoomBroadcastEvent, YouTubeRoomPlayComposer, YouTubeRoomWatchersEvent, YouTubeRoomWatchingComposer } from "@nitrots/nitro-renderer"; import { FC, useEffect, useRef, useState } from "react"; import YouTube from "react-youtube"; -import { - GetRoomSession, - GetSessionDataManager, - SendMessageComposer, - YoutubeVideoPlaybackStateEnum, -} from "../../api"; -import { - NitroCardContentView, - NitroCardHeaderView, - NitroCardView, -} from "../../common"; +import { GetRoomSession, GetSessionDataManager, LocalizeText, SendMessageComposer, YoutubeVideoPlaybackStateEnum } from "../../api"; +import { NitroCardContentView, NitroCardHeaderView, NitroCardView, LayoutAvatarImageView } from "../../common"; import { useFurnitureYoutubeWidget, useMessageEvent } from "../../hooks"; const CONTROL_COMMAND_PREVIOUS_VIDEO = 0; @@ -33,14 +24,7 @@ const extractVideoId = (input: string): string => { export const YouTubePlayerView: FC<{}> = () => { const [isOpen, setIsOpen] = useState(false); - const [tab, setTab] = useState< - | "player" - | "playlist" - | "spectators" - | "settings" - | "history" - | "share" - >("player"); + const [tab, setTab] = useState< | "player" | "playlist" | "spectators" | "settings" | "history" | "share" >("player"); const [inputValue, setInputValue] = useState(""); const [isRoomMode, setIsRoomMode] = useState(false); const [volume, setVolume] = useState(100); @@ -53,65 +37,54 @@ export const YouTubePlayerView: FC<{}> = () => { const [showVolumeSlider, setShowVolumeSlider] = useState(true); const playerRef = useRef(null); - const { - objectId: youtubeObjectId, - videoId: roomVideoId, - currentVideoState, - hasControl, - } = useFurnitureYoutubeWidget(); + const { objectId: youtubeObjectId, videoId: roomVideoId, currentVideoState, hasControl } = useFurnitureYoutubeWidget(); - const [spectators, setSpectators] = useState< - { id: number; name: string; look: string }[] - >([]); - // Room broadcast state: set when someone broadcasts a video to the room + const [spectators, setSpectators] = useState< { id: number; name: string; look: string }[] >([]); const [broadcastVideo, setBroadcastVideo] = useState(""); const [broadcastSender, setBroadcastSender] = useState(""); const [broadcastPlaylist, setBroadcastPlaylist] = useState([]); const [watcherIds, setWatcherIds] = useState>(new Set()); - - // Listen for room-wide YouTube broadcast from the server useMessageEvent(YouTubeRoomBroadcastEvent, event => { const parser = event.getParser(); setBroadcastVideo(parser.videoId); setBroadcastSender(parser.senderName); setBroadcastPlaylist(parser.playlist); - // Auto-open the player and load the broadcast video if (parser.videoId) { setInputValue(parser.videoId); setIsOpen(true); setTab("player"); + } else { + setInputValue(""); + setBroadcastVideo(""); + setBroadcastSender(""); + setBroadcastPlaylist([]); } }); - // Listen for updated watcher list from the server - useMessageEvent(YouTubeRoomWatchersEvent, event => { - setWatcherIds(new Set(event.getParser().watcherIds)); - loadRoomUsers(); // refresh spectator list so we can mark watchers - }); + useMessageEvent(YouTubeRoomWatchersEvent, event => { setWatcherIds(new Set(event.getParser().watcherIds)); loadRoomUsers(); }); - // Notify server when we open/close the YouTube player + const sentWatchingRef = useRef(false); + const hasVideo = !!(inputValue && extractVideoId(inputValue)); useEffect(() => { - if (isOpen) { + if (isOpen && hasVideo && !sentWatchingRef.current) { try { SendMessageComposer(new YouTubeRoomWatchingComposer(true)); } catch(e) {} - } - return () => { + sentWatchingRef.current = true; + } else if ((!isOpen || !hasVideo) && sentWatchingRef.current) { try { SendMessageComposer(new YouTubeRoomWatchingComposer(false)); } catch(e) {} - }; - }, [isOpen]); + sentWatchingRef.current = false; + } + }, [isOpen, hasVideo]); - // Enumerate room users via the session's userDataManager. Uses the - // same brute-force index scan that the old FurnitureYoutubeDisplayView - // used (and which worked). The fancier GetRoomEngine().getRoomObjects() - // approach doesn't reliably return objects when called from the toolbar - // context (outside the room widget tree). const loadRoomUsers = () => { try { const roomSession = GetRoomSession(); if (!roomSession) { setSpectators([]); return; } const users: { id: number; name: string; look: string }[] = []; + const seen = new Set(); for (let i = 0; i < 500; i++) { const userData = roomSession.userDataManager.getUserDataByIndex(i); - if (userData && userData.name && userData.type === 1) { + if (userData && userData.name && userData.type === 1 && !seen.has(userData.userId)) { + seen.add(userData.userId); users.push({ id: userData.userId, name: userData.name, look: userData.figure }); } } @@ -121,8 +94,6 @@ export const YouTubePlayerView: FC<{}> = () => { } }; - // Load room users when the player opens so the spectators count - // is visible on the tab button immediately. useEffect(() => { if (isOpen) loadRoomUsers(); }, [isOpen]); @@ -139,10 +110,6 @@ export const YouTubePlayerView: FC<{}> = () => { }, [youtubeObjectId, roomVideoId]); useEffect(() => { - // Hold the same handler reference for both add and remove. Using a - // fresh arrow function in the cleanup is a no-op because - // removeEventListener requires reference equality; every mount - // would otherwise leak a permanent listener on window. const handler = () => setIsOpen((p) => !p); window.addEventListener("youtube:toggle", handler); return () => window.removeEventListener("youtube:toggle", handler); @@ -154,7 +121,6 @@ export const YouTubePlayerView: FC<{}> = () => { try { const parsed = JSON.parse(savedHistory); if (Array.isArray(parsed)) { - // Accept both legacy {id,title,...} objects and plain string[] setHistory(parsed.map((entry: any) => typeof entry === "string" ? entry : entry?.id).filter(Boolean)); } } catch (e) {} @@ -240,10 +206,10 @@ export const YouTubePlayerView: FC<{}> = () => { if (!isOpen) return null; const videoId = extractVideoId(inputValue); - const isPlaying = - currentVideoState === YoutubeVideoPlaybackStateEnum.PLAYING; + const isPlaying = currentVideoState === YoutubeVideoPlaybackStateEnum.PLAYING; const isPaused = currentVideoState === YoutubeVideoPlaybackStateEnum.PAUSED; - const isMyRoom = GetSessionDataManager().isModerator || hasControl; + const roomSession = GetRoomSession(); + const isMyRoom = GetSessionDataManager().isModerator || (roomSession && roomSession.isRoomOwner); const QuickVolumeButton = ({ value, @@ -265,10 +231,10 @@ export const YouTubePlayerView: FC<{}> = () => { return ( setIsOpen(false)} /> @@ -297,12 +263,12 @@ export const YouTubePlayerView: FC<{}> = () => { > 📤 - {spectators.length > 0 && ( + {watcherIds.size > 0 && ( )} + )} )} @@ -402,7 +383,7 @@ export const YouTubePlayerView: FC<{}> = () => { onChange={(e) => setInputValue(e.target.value)} disabled={!!broadcastVideo && !isMyRoom} className={`flex-1 p-2 rounded text-white text-sm ${(!!broadcastVideo && !isMyRoom) ? "bg-gray-800" : "bg-gray-700"}`} - placeholder="YouTube URL of video ID" + placeholder="YouTube URL / video ID" /> {isMyRoom && videoId && ( )} @@ -428,7 +409,7 @@ export const YouTubePlayerView: FC<{}> = () => { type="text" value={inputValue} onChange={(e) => setInputValue(e.target.value)} - placeholder="Video URL toevoegen..." + placeholder="Add video URL..." className="flex-1 p-2 bg-gray-700 text-white rounded text-sm" onKeyDown={(e) => e.key === "Enter" && addToPlaylist() @@ -446,18 +427,18 @@ export const YouTubePlayerView: FC<{}> = () => { onClick={() => setInputValue("")} className="flex-1 px-3 py-2 bg-gray-700 rounded text-white text-sm" > - 🔄 Nieuwe video + 🔄 New video {playlist.length === 0 ? (
- Playlist is leeg + Playlist is empty
) : (
@@ -498,18 +479,18 @@ export const YouTubePlayerView: FC<{}> = () => {
- 🕐 Bekeken video's ({history.length}) + 🕐 Watch history ({history.length})
{history.length === 0 ? (
- Nog geen video's bekeken + No videos watched yet
) : (
@@ -536,7 +517,7 @@ export const YouTubePlayerView: FC<{}> = () => {
- 📤 Video delen + 📤 Share video
{videoId ? (
@@ -561,13 +542,13 @@ export const YouTubePlayerView: FC<{}> = () => {
) : (
- Selecteer eerst een video om te delen + Select a video first to share
)}
- 📋 Snel delen + 📋 Quick share
)} - {tab === "spectators" && ( + {tab === "spectators" && (() => { + const watchers: { id: number; name: string; look: string }[] = []; + const rs = GetRoomSession(); + if (rs) { + for (const uid of watcherIds) { + const ud = rs.userDataManager.getUserData(uid); + if (ud && ud.name) { + watchers.push({ id: ud.userId, name: ud.name, look: ud.figure }); + } + } + } + return (
- đŸ‘ī¸ Gebruikers in kamer ({spectators.length}) + đŸ“ē {watchers.length} watching
- {spectators.length === 0 ? ( + {watchers.length === 0 ? (
- Geen gebruikers in deze kamer + No one is watching
) : (
- {spectators.map((user) => ( + {watchers.map((user) => (
- {user.name} { - ( - e.target as HTMLImageElement - ).src = - "data:image/svg+xml,"; - }} - /> +
+ +
{user.name} - {watcherIds.has(user.id) && ( - đŸ“ē - )} + đŸ“ē
))}
)}
- )} + ); + })()} {tab === "settings" && (
@@ -701,7 +684,7 @@ export const YouTubePlayerView: FC<{}> = () => { } className="w-4 h-4" /> - 🔇 Dempen + 🔇 Mute
diff --git a/src/css/chat/Chats.css b/src/css/chat/Chats.css index c6cdde2..84ca763 100644 --- a/src/css/chat/Chats.css +++ b/src/css/chat/Chats.css @@ -9,7 +9,6 @@ &.type-0 { - // normal .message { font-weight: 400; } @@ -17,7 +16,6 @@ &.type-1 { - // whisper .message { font-weight: 400; font-style: italic; @@ -27,7 +25,6 @@ &.type-2 { - // shout .message { font-weight: 700; } @@ -1097,4 +1094,4 @@ &.bubble-53 { background-image: url('@/assets/images/chat/chatbubbles/bubble_53.png'); } -} \ No newline at end of file +} diff --git a/src/css/widgets/FurnitureWidgets.css b/src/css/widgets/FurnitureWidgets.css index 71eae75..e5f9c4c 100644 --- a/src/css/widgets/FurnitureWidgets.css +++ b/src/css/widgets/FurnitureWidgets.css @@ -177,7 +177,6 @@ &.stickie-yellow { background-image: url('@/assets/images/room-widgets/stickie-widget/stickie-yellow.png'); - //background-position: -191px -184px; } &.stickie-green { diff --git a/src/hooks/notification/useNotification.ts b/src/hooks/notification/useNotification.ts index 911fd63..4422e27 100644 --- a/src/hooks/notification/useNotification.ts +++ b/src/hooks/notification/useNotification.ts @@ -226,6 +226,7 @@ const useNotificationState = () => { const parser = event.getParser(); + // Skip if AchievementNotificationMessageEvent already showed a notification for this badge if(recentBadgeNotifications.has(parser.badgeCode)) return; recentBadgeNotifications.add(parser.badgeCode); @@ -233,6 +234,9 @@ const useNotificationState = () => const badgeName = LocalizeBadgeName(parser.badgeCode); const badgeImage = GetSessionDataManager().getBadgeUrl(parser.badgeCode); + // senderName is non-empty only when a staff member awarded the badge + // via the `:badge` command. Empty for achievements, catalog buys, + // wired rewards, poll rewards, etc. const senderName = parser.senderName || ''; showSingleBubble(badgeName, NotificationBubbleType.BADGE_RECEIVED, badgeImage, parser.badgeCode, senderName);