smart smart method by google gemini
This commit is contained in:
@@ -2,7 +2,6 @@ import type { Metadata } from "next";
|
|||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
import { QuickTopUI } from "@/components/QuickTopUI";
|
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-geist-sans",
|
||||||
@@ -15,7 +14,7 @@ const geistMono = Geist_Mono({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "OCbwoy3-Chan-blox",
|
title: "ocbwoy3-chan-blox",
|
||||||
description: "roblox meets next.js i think"
|
description: "roblox meets next.js i think"
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -27,10 +26,9 @@ export default function RootLayout({
|
|||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body
|
<body
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
className={`${geistSans.variable} ${geistMono.variable} antialiased overflow-x-hidden`}
|
||||||
>
|
>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<QuickTopUI />
|
|
||||||
{children}
|
{children}
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { FriendsHomeSect } from "@/components/FriendsOnlineSection";
|
import { FriendsHomeSect } from "@/components/FriendsOnlineSection";
|
||||||
import { GameCard } from "@/components/gameCard";
|
import { GameCard } from "@/components/gameCard";
|
||||||
import { HomeLoggedInHeader } from "@/components/loggedInHeader";
|
import { HomeLoggedInHeader } from "@/components/loggedInHeader";
|
||||||
|
import { QuickTopUI, QuickTopUILogoPart } from "@/components/QuickTopUI";
|
||||||
import { VerifiedIcon } from "@/components/RobloxIcons";
|
import { VerifiedIcon } from "@/components/RobloxIcons";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
@@ -35,7 +36,9 @@ export default function Home() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-scroll no-scrollbar w-screen max-h-screen h-screen">
|
<div className="overflow-scroll no-scrollbar w-screen max-h-screen h-screen antialiased overflow-x-hidden">
|
||||||
|
<QuickTopUI />
|
||||||
|
<QuickTopUILogoPart />
|
||||||
<HomeLoggedInHeader />
|
<HomeLoggedInHeader />
|
||||||
<FriendsHomeSect className="pt-8" />
|
<FriendsHomeSect className="pt-8" />
|
||||||
<div className="justify-center w-screen px-8 pt-6">
|
<div className="justify-center w-screen px-8 pt-6">
|
||||||
|
|||||||
3
bun.lock
3
bun.lock
@@ -33,6 +33,7 @@
|
|||||||
"@radix-ui/react-toggle": "^1.1.9",
|
"@radix-ui/react-toggle": "^1.1.9",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.10",
|
"@radix-ui/react-toggle-group": "^1.1.10",
|
||||||
"@radix-ui/react-tooltip": "^1.2.7",
|
"@radix-ui/react-tooltip": "^1.2.7",
|
||||||
|
"@tailwindcss/line-clamp": "^0.4.4",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
@@ -275,6 +276,8 @@
|
|||||||
|
|
||||||
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
|
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
|
||||||
|
|
||||||
|
"@tailwindcss/line-clamp": ["@tailwindcss/line-clamp@0.4.4", "", { "peerDependencies": { "tailwindcss": ">=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1" } }, "sha512-5U6SY5z8N42VtrCrKlsTAA35gy2VSyYtHWCsg1H87NU1SXnEfekTVlrga9fzUDrrHcGi2Lb5KenUWb4lRQT5/g=="],
|
||||||
|
|
||||||
"@types/d3-array": ["@types/d3-array@3.2.1", "", {}, "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg=="],
|
"@types/d3-array": ["@types/d3-array@3.2.1", "", {}, "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg=="],
|
||||||
|
|
||||||
"@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="],
|
"@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="],
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { useFriendsHome } from "@/hooks/roblox/useFriendsHome";
|
|||||||
import LazyLoadedImage from "./lazyLoadedImage";
|
import LazyLoadedImage from "./lazyLoadedImage";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { VerifiedIcon } from "./RobloxIcons";
|
import { VerifiedIcon } from "./RobloxIcons";
|
||||||
|
import { useFriendsPresence } from "@/hooks/roblox/usePresence";
|
||||||
|
import { StupidHoverThing } from "./MiscStuff";
|
||||||
|
|
||||||
export function FriendsHomeSect(
|
export function FriendsHomeSect(
|
||||||
props: React.DetailedHTMLProps<
|
props: React.DetailedHTMLProps<
|
||||||
@@ -12,6 +14,9 @@ export function FriendsHomeSect(
|
|||||||
) {
|
) {
|
||||||
const friends = useFriendsHome();
|
const friends = useFriendsHome();
|
||||||
const acct = useCurrentAccount();
|
const acct = useCurrentAccount();
|
||||||
|
const presence = useFriendsPresence(
|
||||||
|
(!!friends ? friends : []).map((f) => f.id)
|
||||||
|
);
|
||||||
|
|
||||||
if (!friends) {
|
if (!friends) {
|
||||||
return <></>;
|
return <></>;
|
||||||
@@ -19,35 +24,93 @@ export function FriendsHomeSect(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div {...props}>
|
<div {...props}>
|
||||||
|
{/* <button onClick={()=>console.log(acct,presence,friends)}>debug</button> */}
|
||||||
<h1 className="text-2xl pb-2 pl-4">Friends</h1>
|
<h1 className="text-2xl pb-2 pl-4">Friends</h1>
|
||||||
<div className="bg-base p-4 rounded-xl flex flex-col gap-2 px-4">
|
<div className="bg-base p-4 rounded-xl flex flex-col gap-2 px-4 no-scrollbar">
|
||||||
<div
|
<div
|
||||||
className="flex items-center gap-4 overflow-x-auto pb-2 -mx-4 w-screen scrollbar-thin scrollbar-thumb-surface2 scrollbar-track-surface0"
|
className="flex items-center gap-4 overflow-x-auto overflow-y-visible no-scrollbar pb-2 -mx-4 w-screen scrollbar-thin scrollbar-thumb-surface2 scrollbar-track-surface0"
|
||||||
style={{
|
style={{
|
||||||
scrollSnapType: "x mandatory",
|
scrollSnapType: "x mandatory",
|
||||||
WebkitOverflowScrolling: "touch"
|
WebkitOverflowScrolling: "touch",
|
||||||
|
scrollbarWidth: "none"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="w-8" />
|
<div className="w-8" />
|
||||||
{friends.map((a) => (
|
{friends.map((a) => {
|
||||||
<div
|
const userStatus = presence.find(
|
||||||
key={a.id}
|
(b) => b.userId === a.id
|
||||||
className="flex flex-col items-center min-w-[6.5rem]"
|
);
|
||||||
// style={{ scrollSnapAlign: "start" }}
|
const userPresence = userStatus?.userPresenceType || 0;
|
||||||
>
|
const borderColor =
|
||||||
<LazyLoadedImage
|
userPresence === 1
|
||||||
imgId={`AvatarHeadShot_${a.id}`}
|
? "border-blue bg-blue/50"
|
||||||
alt={a.name}
|
: userPresence === 2
|
||||||
className="w-24 h-24 rounded-full border-2 border-surface2 object-cover shadow"
|
? "border-green bg-green/50"
|
||||||
/>
|
: userPresence === 3
|
||||||
<span className="truncate text-xs text-text mt-1 text-center flex items-center justify-center gap-1">
|
? "border-yellow bg-yellow/50"
|
||||||
{a.displayName || a.name}
|
: userPresence === 0
|
||||||
{a.hasVerifiedBadge ? null : (
|
? "border-surface2 bg-surface2/50"
|
||||||
<VerifiedIcon className="text-base fill-blue w-3 h-3" />
|
: "border-red bg-red/50";
|
||||||
)}
|
const textColor =
|
||||||
</span>
|
userPresence === 1
|
||||||
</div>
|
? "text-blue"
|
||||||
))}
|
: userPresence === 2
|
||||||
|
? "text-green"
|
||||||
|
: userPresence === 3
|
||||||
|
? "text-yellow"
|
||||||
|
: userPresence === 0
|
||||||
|
? "text-surface2"
|
||||||
|
: "text-red";
|
||||||
|
const fillColor =
|
||||||
|
userPresence === 1
|
||||||
|
? "fill-blue"
|
||||||
|
: userPresence === 2
|
||||||
|
? "fill-green"
|
||||||
|
: userPresence === 3
|
||||||
|
? "fill-yellow"
|
||||||
|
: userPresence === 0
|
||||||
|
? "fill-surface2"
|
||||||
|
: "fill-red";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={a.id}
|
||||||
|
className="flex flex-col min-w-[6.5rem]"
|
||||||
|
>
|
||||||
|
<LazyLoadedImage
|
||||||
|
imgId={`AvatarHeadShot_${a.id}`}
|
||||||
|
alt={a.name}
|
||||||
|
className={`w-24 h-24 rounded-full border-2 ${borderColor} object-cover shadow-xl`}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className={`text-xs ${textColor} mt-1 text-center flex items-center justify-center gap-1 max-w-[6.5rem] overflow-hidden line-clamp-2`}
|
||||||
|
>
|
||||||
|
<StupidHoverThing
|
||||||
|
text={
|
||||||
|
<span className="space-x-1 flex items-center">
|
||||||
|
<p>{a.displayName || a.name}</p>
|
||||||
|
{!a.hasVerifiedBadge ? (
|
||||||
|
<VerifiedIcon
|
||||||
|
useDefault
|
||||||
|
className={`w-4 h-4 shrink-0`}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span className="line-clamp-1 overflow-hidden text-ellipsis">
|
||||||
|
{a.displayName || a.name}
|
||||||
|
</span>
|
||||||
|
</StupidHoverThing>
|
||||||
|
{!a.hasVerifiedBadge ? (
|
||||||
|
<VerifiedIcon
|
||||||
|
className={`text-base ${fillColor} w-3 h-3 shrink-0`}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
<div className="w-8" />
|
<div className="w-8" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
15
components/MiscStuff.tsx
Normal file
15
components/MiscStuff.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { VerifiedIcon } from "./RobloxIcons";
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
|
||||||
|
|
||||||
|
export function StupidHoverThing({ children, text }: React.PropsWithChildren & { text: string | React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
{children}
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="bg-surface0 text-text m-2">
|
||||||
|
<p className="text-sm flex items-center">{text}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -2,18 +2,27 @@
|
|||||||
|
|
||||||
import { useRobuxBalance } from "@/hooks/roblox/useRobuxBalance";
|
import { useRobuxBalance } from "@/hooks/roblox/useRobuxBalance";
|
||||||
import { RobuxIcon } from "./RobloxIcons";
|
import { RobuxIcon } from "./RobloxIcons";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
export function QuickTopUI() {
|
export const QuickTopUI = React.memo(function () {
|
||||||
const robux = useRobuxBalance();
|
const robux = useRobuxBalance();
|
||||||
return (
|
return (
|
||||||
<span className="z-50 absolute top-4 right-4 p-4 flex gap-4 items-center">
|
<div className="z-50 absolute top-4 right-4 p-4 flex gap-4 items-center">
|
||||||
<img src="/icon-128.webp" className="-m-1 w-8 h-8" alt="" />
|
<div className="rounded-full bg-crust/50 flex items-center p-2">
|
||||||
<span className="rounded-full bg-crust/50 flex items-center p-2">
|
<div className="px-2 font-sans text-blue text-xl flex items-center">
|
||||||
<span className="px-2 font-sans text-blue text-xl flex items-center">
|
|
||||||
<RobuxIcon className="w-6 h-6" />
|
<RobuxIcon className="w-6 h-6" />
|
||||||
<p className="pl-1">{robux}</p>
|
<p className="pl-1">{robux || "???"}</p>
|
||||||
</span>
|
</div>
|
||||||
</span>
|
</div>
|
||||||
</span>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
export const QuickTopUILogoPart = React.memo(function () {
|
||||||
|
return (
|
||||||
|
<div className="z-50 relative top-4 left-4 p-4 flex gap-4 items-center text-blue">
|
||||||
|
<img src="/icon-128.webp" className="-m-1 w-8 h-8" alt="" />
|
||||||
|
<p className="mt-2">{"ocbwoy3-chan-blox"}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export function RobloxVerifiedSmall(
|
|||||||
<VerifiedIcon {...props} />
|
<VerifiedIcon {...props} />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent className="bg-surface0 text-text m-2">
|
<TooltipContent className="bg-surface0 text-text m-2">
|
||||||
<p className="text-sm">Verified user</p>
|
<p className="text-sm">Verified</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
import React, { useEffect } from "react";
|
||||||
import LazyLoadedImage from "./lazyLoadedImage";
|
import LazyLoadedImage from "./lazyLoadedImage";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "./ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "./ui/alert";
|
||||||
import { OctagonXIcon } from "lucide-react";
|
import { OctagonXIcon } from "lucide-react";
|
||||||
import { RobloxPremiumSmall, RobloxVerifiedSmall } from "./RobloxTooltipStuff";
|
import { RobloxPremiumSmall, RobloxVerifiedSmall } from "./RobloxTooltipStuff";
|
||||||
import { useCurrentAccount } from "@/hooks/roblox/useCurrentAccount";
|
import { useCurrentAccount } from "@/hooks/roblox/useCurrentAccount";
|
||||||
import { Skeleton } from "./ui/skeleton";
|
import { Skeleton } from "./ui/skeleton";
|
||||||
|
import { useFriendsPresence } from "@/hooks/roblox/usePresence";
|
||||||
|
|
||||||
export function HomeLoggedInHeader() {
|
export function HomeLoggedInHeader() {
|
||||||
const profile = useCurrentAccount();
|
const profile = useCurrentAccount();
|
||||||
@@ -26,8 +27,24 @@ export function HomeLoggedInHeader() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const presence = useFriendsPresence(profile ? [profile.id] : []);
|
||||||
|
|
||||||
|
const userActivity = presence.find((b) => b.userId === profile?.id)
|
||||||
|
const userPresence = userActivity?.userPresenceType;
|
||||||
|
const borderColor =
|
||||||
|
userPresence === 1
|
||||||
|
? "border-blue bg-blue/50"
|
||||||
|
: userPresence === 2
|
||||||
|
? "border-green bg-green/50"
|
||||||
|
: userPresence === 3
|
||||||
|
? "border-yellow bg-yellow/50"
|
||||||
|
: userPresence === 0
|
||||||
|
? "border-surface2 bg-surface2/50"
|
||||||
|
: "border-red bg-red/50";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{/* <button onClick={()=>console.log(userPresence)}>debug this</button> */}
|
||||||
<div className="flex items-center gap-6 bg-base rounded-xl px-8 py-6 w-fit mt-8 ml-0">
|
<div className="flex items-center gap-6 bg-base rounded-xl px-8 py-6 w-fit mt-8 ml-0">
|
||||||
{!profile ? (
|
{!profile ? (
|
||||||
<Skeleton className="w-28 h-28 rounded-full" />
|
<Skeleton className="w-28 h-28 rounded-full" />
|
||||||
@@ -35,7 +52,7 @@ export function HomeLoggedInHeader() {
|
|||||||
<LazyLoadedImage
|
<LazyLoadedImage
|
||||||
imgId={`AvatarHeadShot_${profile.id}`}
|
imgId={`AvatarHeadShot_${profile.id}`}
|
||||||
alt=""
|
alt=""
|
||||||
className="w-28 h-28 rounded-full shadow-crust"
|
className={`w-28 h-28 rounded-full shadow-crust border-2 ${borderColor}`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-col justify-center">
|
<div className="flex flex-col justify-center">
|
||||||
@@ -53,7 +70,10 @@ export function HomeLoggedInHeader() {
|
|||||||
</span>
|
</span>
|
||||||
<span className="text-base font-mono text-subtext0 mt-1">
|
<span className="text-base font-mono text-subtext0 mt-1">
|
||||||
{profile ? (
|
{profile ? (
|
||||||
<>@{profile.name}</>
|
<>
|
||||||
|
@{profile.name}
|
||||||
|
{(!!userActivity && userPresence === 2) ? <> - {userActivity.lastLocation}</> : <></> }
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Skeleton className="w-64 h-6 rounded-lg" />
|
<Skeleton className="w-64 h-6 rounded-lg" />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { loadThumbnails } from "@/lib/thumbnailLoader";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
let isFetching = false;
|
let isFetching = false;
|
||||||
let cachedData: any = null;
|
let cachedData: UserProfileDetails | null | false = null;
|
||||||
|
|
||||||
export function useCurrentAccount() {
|
export function useCurrentAccount() {
|
||||||
const [profileDetails, setProfileDetails] = useState<
|
const [profileDetails, setProfileDetails] = useState<
|
||||||
@@ -18,7 +18,7 @@ export function useCurrentAccount() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
if (profileDetails !== null) return;
|
if (profileDetails !== null && profileDetails !== undefined) return;
|
||||||
if (isFetching) {
|
if (isFetching) {
|
||||||
const IN = setInterval(() => {
|
const IN = setInterval(() => {
|
||||||
if (cachedData !== null) {
|
if (cachedData !== null) {
|
||||||
|
|||||||
127
hooks/roblox/usePresence.ts
Normal file
127
hooks/roblox/usePresence.ts
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
// smartass method by google gemini
|
||||||
|
|
||||||
|
import { useEffect, useState, useMemo } from "react";
|
||||||
|
import { useCurrentAccount } from "./useCurrentAccount";
|
||||||
|
import { proxyFetch } from "@/lib/utils";
|
||||||
|
|
||||||
|
type PresenceData = {
|
||||||
|
userPresenceType: number;
|
||||||
|
lastLocation: string;
|
||||||
|
placeId: number;
|
||||||
|
rootPlaceId: number;
|
||||||
|
gameId: string;
|
||||||
|
universeId: number;
|
||||||
|
userId: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Internal Shared State ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Map to track subscribers.
|
||||||
|
* Key: The component's update callback function.
|
||||||
|
* Value: The array of user IDs that component is interested in.
|
||||||
|
* This allows multiple components to subscribe with their own lists of IDs.
|
||||||
|
*/
|
||||||
|
let subscribers = new Map<(data: PresenceData[]) => void, number[]>();
|
||||||
|
|
||||||
|
let interval: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
|
let latestData: PresenceData[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches presence for all unique user IDs requested by all subscribed components.
|
||||||
|
* @param acctId - The ID of the currently logged-in user.
|
||||||
|
*/
|
||||||
|
async function fetchPresence(acctId: number) {
|
||||||
|
const allIdArrays = [...subscribers.values()];
|
||||||
|
const uniqueUserIds = [...new Set(allIdArrays.flat())];
|
||||||
|
|
||||||
|
if (!acctId || uniqueUserIds.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await proxyFetch(
|
||||||
|
"https://presence.roblox.com/v1/presence/users",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
userIds: [...new Set([acctId, ...uniqueUserIds])]
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`API request failed with status ${res.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = await res.json();
|
||||||
|
latestData = json.userPresences || [];
|
||||||
|
|
||||||
|
subscribers.forEach((_requestedIds, callback) => callback(latestData));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch presence:", error);
|
||||||
|
latestData = [];
|
||||||
|
subscribers.forEach((_requestedIds, callback) => callback([]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A React hook to get the real-time presence of a list of Roblox users.
|
||||||
|
* This hook can be used by multiple components simultaneously without conflict.
|
||||||
|
*
|
||||||
|
* @param userIds - An array of user IDs to track.
|
||||||
|
* @returns An array of PresenceData objects for the requested user IDs.
|
||||||
|
*/
|
||||||
|
export function useFriendsPresence(userIds: number[]) {
|
||||||
|
const acct = useCurrentAccount();
|
||||||
|
const [data, setData] = useState<PresenceData[]>([]);
|
||||||
|
|
||||||
|
const userIdsKey = useMemo(
|
||||||
|
() => JSON.stringify([...userIds].sort()),
|
||||||
|
[userIds]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!acct || !userIds || userIds.length === 0) {
|
||||||
|
setData([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateCallback = (globalData: PresenceData[]) => {
|
||||||
|
const filteredData = globalData.filter((presence) =>
|
||||||
|
userIds.includes(presence.userId)
|
||||||
|
);
|
||||||
|
setData(filteredData);
|
||||||
|
};
|
||||||
|
|
||||||
|
updateCallback(latestData);
|
||||||
|
|
||||||
|
subscribers.set(updateCallback, userIds);
|
||||||
|
|
||||||
|
if (!interval) {
|
||||||
|
fetchPresence(acct.id);
|
||||||
|
interval = setInterval(() => fetchPresence(acct.id), 5000);
|
||||||
|
} else {
|
||||||
|
fetchPresence(acct.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The cleanup function runs when the component unmounts.
|
||||||
|
return () => {
|
||||||
|
subscribers.delete(updateCallback);
|
||||||
|
|
||||||
|
if (subscribers.size === 0 && interval) {
|
||||||
|
clearInterval(interval);
|
||||||
|
interval = null;
|
||||||
|
latestData = [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [acct, userIdsKey]);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
@@ -4,46 +4,40 @@ import { useEffect, useState } from "react";
|
|||||||
import { useCurrentAccount } from "./useCurrentAccount";
|
import { useCurrentAccount } from "./useCurrentAccount";
|
||||||
import { proxyFetch } from "@/lib/utils";
|
import { proxyFetch } from "@/lib/utils";
|
||||||
|
|
||||||
let isFetching = false;
|
|
||||||
let cachedData: number | false | null = null;
|
|
||||||
|
|
||||||
export function useRobuxBalance() {
|
export function useRobuxBalance() {
|
||||||
const acct = useCurrentAccount();
|
const acct = useCurrentAccount();
|
||||||
const [robux, setRobux] = useState<number | false | null>(cachedData);
|
const [robux, setRobux] = useState<number | false | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
|
||||||
if (!acct) return;
|
if (!acct) return;
|
||||||
|
|
||||||
async function fetchBalance() {
|
let cancelled = false;
|
||||||
if (isFetching) return;
|
|
||||||
if (!acct) return;
|
const fetchBalance = async () => {
|
||||||
isFetching = true;
|
if (!acct || cancelled) return;
|
||||||
try {
|
try {
|
||||||
const res = await proxyFetch(
|
const res = await proxyFetch(
|
||||||
`https://economy.roblox.com/v1/users/${acct.id}/currency`
|
`https://economy.roblox.com/v1/users/${acct.id}/currency`
|
||||||
);
|
);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (!cancelled) setRobux(data.robux);
|
if (!cancelled) setRobux(data.robux);
|
||||||
cachedData = data.robux;
|
|
||||||
} catch {
|
} catch {
|
||||||
if (!cancelled) setRobux(false);
|
if (!cancelled) setRobux(false);
|
||||||
cachedData = false;
|
|
||||||
} finally {
|
|
||||||
isFetching = false;
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
fetchBalance();
|
fetchBalance();
|
||||||
|
const interval = setInterval(fetchBalance, 10000);
|
||||||
|
|
||||||
function handleTransaction() {
|
const handleTransaction = () => {
|
||||||
fetchBalance();
|
fetchBalance();
|
||||||
}
|
};
|
||||||
|
|
||||||
window.addEventListener("transactionCompletedEvent", handleTransaction);
|
window.addEventListener("transactionCompletedEvent", handleTransaction);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
|
clearInterval(interval);
|
||||||
window.removeEventListener(
|
window.removeEventListener(
|
||||||
"transactionCompletedEvent",
|
"transactionCompletedEvent",
|
||||||
handleTransaction
|
handleTransaction
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export type ThumbnailRequest = {
|
|||||||
export async function getThumbnails(
|
export async function getThumbnails(
|
||||||
b: ThumbnailRequest[]
|
b: ThumbnailRequest[]
|
||||||
): Promise<AssetThumbnail[]> {
|
): Promise<AssetThumbnail[]> {
|
||||||
const batchSize = 50;
|
const batchSize = 100;
|
||||||
const results: AssetThumbnail[] = [];
|
const results: AssetThumbnail[] = [];
|
||||||
for (let i = 0; i < b.length; i += batchSize) {
|
for (let i = 0; i < b.length; i += batchSize) {
|
||||||
const batch = b.slice(i, i + batchSize);
|
const batch = b.slice(i, i + batchSize);
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
"@radix-ui/react-toggle": "^1.1.9",
|
"@radix-ui/react-toggle": "^1.1.9",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.10",
|
"@radix-ui/react-toggle-group": "^1.1.10",
|
||||||
"@radix-ui/react-tooltip": "^1.2.7",
|
"@radix-ui/react-tooltip": "^1.2.7",
|
||||||
|
"@tailwindcss/line-clamp": "^0.4.4",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ export default {
|
|||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
require("tailwindcss-animate"),
|
require("tailwindcss-animate"),
|
||||||
|
require("@tailwindcss/line-clamp"),
|
||||||
require("@catppuccin/tailwindcss")({
|
require("@catppuccin/tailwindcss")({
|
||||||
prefix: false,
|
prefix: false,
|
||||||
defaultFlavour: "mocha"
|
defaultFlavour: "mocha"
|
||||||
|
|||||||
Reference in New Issue
Block a user