diff --git a/bun.lock b/bun.lock
index 80bf52e..5a8e2b4 100644
--- a/bun.lock
+++ b/bun.lock
@@ -33,6 +33,7 @@
"@radix-ui/react-toggle": "^1.1.9",
"@radix-ui/react-toggle-group": "^1.1.10",
"@radix-ui/react-tooltip": "^1.2.7",
+ "@tailwindcss/line-clamp": "^0.4.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.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=="],
+ "@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-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="],
diff --git a/components/FriendsOnlineSection.tsx b/components/FriendsOnlineSection.tsx
index ded4408..957fe59 100644
--- a/components/FriendsOnlineSection.tsx
+++ b/components/FriendsOnlineSection.tsx
@@ -3,6 +3,8 @@ import { useFriendsHome } from "@/hooks/roblox/useFriendsHome";
import LazyLoadedImage from "./lazyLoadedImage";
import React from "react";
import { VerifiedIcon } from "./RobloxIcons";
+import { useFriendsPresence } from "@/hooks/roblox/usePresence";
+import { StupidHoverThing } from "./MiscStuff";
export function FriendsHomeSect(
props: React.DetailedHTMLProps<
@@ -12,6 +14,9 @@ export function FriendsHomeSect(
) {
const friends = useFriendsHome();
const acct = useCurrentAccount();
+ const presence = useFriendsPresence(
+ (!!friends ? friends : []).map((f) => f.id)
+ );
if (!friends) {
return <>>;
@@ -19,35 +24,93 @@ export function FriendsHomeSect(
return (
+ {/*
*/}
Friends
-
+
- {friends.map((a) => (
-
-
-
- {a.displayName || a.name}
- {a.hasVerifiedBadge ? null : (
-
- )}
-
-
- ))}
+ {friends.map((a) => {
+ const userStatus = presence.find(
+ (b) => b.userId === a.id
+ );
+ const userPresence = userStatus?.userPresenceType || 0;
+ 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";
+ const textColor =
+ userPresence === 1
+ ? "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 (
+
+
+
+
+ {a.displayName || a.name}
+ {!a.hasVerifiedBadge ? (
+
+ ) : null}
+
+ }
+ >
+
+ {a.displayName || a.name}
+
+
+ {!a.hasVerifiedBadge ? (
+
+ ) : null}
+
+
+ );
+ })}
diff --git a/components/MiscStuff.tsx b/components/MiscStuff.tsx
new file mode 100644
index 0000000..232b370
--- /dev/null
+++ b/components/MiscStuff.tsx
@@ -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 (
+
+
+ {children}
+
+
+ {text}
+
+
+ )
+}
diff --git a/components/QuickTopUI.tsx b/components/QuickTopUI.tsx
index 2a59707..101543a 100644
--- a/components/QuickTopUI.tsx
+++ b/components/QuickTopUI.tsx
@@ -2,18 +2,27 @@
import { useRobuxBalance } from "@/hooks/roblox/useRobuxBalance";
import { RobuxIcon } from "./RobloxIcons";
+import React from "react";
-export function QuickTopUI() {
+export const QuickTopUI = React.memo(function () {
const robux = useRobuxBalance();
return (
-
-
-
-
+
+
+
-
{robux}
-
-
-
+
{robux || "???"}
+
+
+
);
-}
+});
+
+export const QuickTopUILogoPart = React.memo(function () {
+ return (
+
+

+
{"ocbwoy3-chan-blox"}
+
+ );
+});
diff --git a/components/RobloxTooltipStuff.tsx b/components/RobloxTooltipStuff.tsx
index 01a0d62..63aef37 100644
--- a/components/RobloxTooltipStuff.tsx
+++ b/components/RobloxTooltipStuff.tsx
@@ -28,7 +28,7 @@ export function RobloxVerifiedSmall(
- Verified user
+ Verified
);
diff --git a/components/loggedInHeader.tsx b/components/loggedInHeader.tsx
index 118edd7..feac433 100644
--- a/components/loggedInHeader.tsx
+++ b/components/loggedInHeader.tsx
@@ -1,12 +1,13 @@
"use client";
-import React from "react";
+import React, { useEffect } from "react";
import LazyLoadedImage from "./lazyLoadedImage";
import { Alert, AlertDescription, AlertTitle } from "./ui/alert";
import { OctagonXIcon } from "lucide-react";
import { RobloxPremiumSmall, RobloxVerifiedSmall } from "./RobloxTooltipStuff";
import { useCurrentAccount } from "@/hooks/roblox/useCurrentAccount";
import { Skeleton } from "./ui/skeleton";
+import { useFriendsPresence } from "@/hooks/roblox/usePresence";
export function HomeLoggedInHeader() {
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 (
<>
+ {/* */}
{!profile ? (
@@ -35,7 +52,7 @@ export function HomeLoggedInHeader() {
)}
@@ -53,7 +70,10 @@ export function HomeLoggedInHeader() {
{profile ? (
- <>@{profile.name}>
+ <>
+ @{profile.name}
+ {(!!userActivity && userPresence === 2) ? <> - {userActivity.lastLocation}> : <>> }
+ >
) : (
)}
diff --git a/hooks/roblox/useCurrentAccount.ts b/hooks/roblox/useCurrentAccount.ts
index bc150bc..b57ac39 100644
--- a/hooks/roblox/useCurrentAccount.ts
+++ b/hooks/roblox/useCurrentAccount.ts
@@ -9,7 +9,7 @@ import { loadThumbnails } from "@/lib/thumbnailLoader";
import { useEffect, useState } from "react";
let isFetching = false;
-let cachedData: any = null;
+let cachedData: UserProfileDetails | null | false = null;
export function useCurrentAccount() {
const [profileDetails, setProfileDetails] = useState<
@@ -18,7 +18,7 @@ export function useCurrentAccount() {
useEffect(() => {
let cancelled = false;
- if (profileDetails !== null) return;
+ if (profileDetails !== null && profileDetails !== undefined) return;
if (isFetching) {
const IN = setInterval(() => {
if (cachedData !== null) {
diff --git a/hooks/roblox/usePresence.ts b/hooks/roblox/usePresence.ts
new file mode 100644
index 0000000..f9d73bc
--- /dev/null
+++ b/hooks/roblox/usePresence.ts
@@ -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 | 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([]);
+
+ 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;
+}
diff --git a/hooks/roblox/useRobuxBalance.ts b/hooks/roblox/useRobuxBalance.ts
index ad8b921..1388d25 100644
--- a/hooks/roblox/useRobuxBalance.ts
+++ b/hooks/roblox/useRobuxBalance.ts
@@ -4,46 +4,40 @@ import { useEffect, useState } from "react";
import { useCurrentAccount } from "./useCurrentAccount";
import { proxyFetch } from "@/lib/utils";
-let isFetching = false;
-let cachedData: number | false | null = null;
-
export function useRobuxBalance() {
const acct = useCurrentAccount();
- const [robux, setRobux] = useState(cachedData);
+ const [robux, setRobux] = useState(null);
useEffect(() => {
- let cancelled = false;
if (!acct) return;
- async function fetchBalance() {
- if (isFetching) return;
- if (!acct) return;
- isFetching = true;
+ let cancelled = false;
+
+ const fetchBalance = async () => {
+ if (!acct || cancelled) return;
try {
const res = await proxyFetch(
`https://economy.roblox.com/v1/users/${acct.id}/currency`
);
const data = await res.json();
if (!cancelled) setRobux(data.robux);
- cachedData = data.robux;
} catch {
if (!cancelled) setRobux(false);
- cachedData = false;
- } finally {
- isFetching = false;
}
- }
+ };
fetchBalance();
+ const interval = setInterval(fetchBalance, 10000);
- function handleTransaction() {
+ const handleTransaction = () => {
fetchBalance();
- }
+ };
window.addEventListener("transactionCompletedEvent", handleTransaction);
return () => {
cancelled = true;
+ clearInterval(interval);
window.removeEventListener(
"transactionCompletedEvent",
handleTransaction
diff --git a/lib/thumbnailLoader.ts b/lib/thumbnailLoader.ts
index fd23a7f..766d19c 100644
--- a/lib/thumbnailLoader.ts
+++ b/lib/thumbnailLoader.ts
@@ -26,7 +26,7 @@ export type ThumbnailRequest = {
export async function getThumbnails(
b: ThumbnailRequest[]
): Promise {
- const batchSize = 50;
+ const batchSize = 100;
const results: AssetThumbnail[] = [];
for (let i = 0; i < b.length; i += batchSize) {
const batch = b.slice(i, i + batchSize);
diff --git a/package.json b/package.json
index 961975d..f347f7f 100644
--- a/package.json
+++ b/package.json
@@ -38,6 +38,7 @@
"@radix-ui/react-toggle": "^1.1.9",
"@radix-ui/react-toggle-group": "^1.1.10",
"@radix-ui/react-tooltip": "^1.2.7",
+ "@tailwindcss/line-clamp": "^0.4.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
diff --git a/tailwind.config.ts b/tailwind.config.ts
index 432598c..856b96e 100644
--- a/tailwind.config.ts
+++ b/tailwind.config.ts
@@ -115,6 +115,7 @@ export default {
},
plugins: [
require("tailwindcss-animate"),
+ require("@tailwindcss/line-clamp"),
require("@catppuccin/tailwindcss")({
prefix: false,
defaultFlavour: "mocha"