useBestFriends();

This commit is contained in:
2025-07-24 19:50:54 +03:00
parent 980b27bf84
commit c7d3cd85be
24 changed files with 925 additions and 249 deletions

View File

@@ -1,119 +0,0 @@
import { useCurrentAccount } from "@/hooks/roblox/useCurrentAccount";
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<
React.HTMLAttributes<HTMLDivElement>,
HTMLDivElement
>
) {
const friends = useFriendsHome();
const acct = useCurrentAccount();
const presence = useFriendsPresence(
(!!friends ? friends : []).map((f) => f.id)
);
if (!friends) {
return <></>;
}
return (
<div {...props}>
{/* <button onClick={()=>console.log(acct,presence,friends)}>debug</button> */}
<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 no-scrollbar">
<div
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={{
scrollSnapType: "x mandatory",
WebkitOverflowScrolling: "touch",
scrollbarWidth: "none"
}}
>
<div className="w-8" />
{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 (
<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>
</div>
</div>
);
}

View File

@@ -1,15 +0,0 @@
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>
)
}

View File

@@ -1,28 +0,0 @@
"use client";
import { useRobuxBalance } from "@/hooks/roblox/useRobuxBalance";
import { RobuxIcon } from "./RobloxIcons";
import React from "react";
export const QuickTopUI = React.memo(function () {
const robux = useRobuxBalance();
return (
<div className="z-50 absolute top-4 right-4 p-4 flex gap-4 items-center">
<div className="rounded-full bg-crust/50 flex items-center p-2">
<div className="px-2 font-sans text-blue text-xl flex items-center">
<RobuxIcon className="w-6 h-6" />
<p className="pl-1">{robux || "???"}</p>
</div>
</div>
</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>
);
});

View File

@@ -0,0 +1,198 @@
import { useCurrentAccount } from "@/hooks/roblox/useCurrentAccount";
import { useFriendsHome } from "@/hooks/roblox/useFriends";
import { useFriendsPresence } from "@/hooks/roblox/usePresence";
import React, { useEffect, useState } from "react";
import LazyLoadedImage from "../util/LazyLoadedImage";
import { StupidHoverThing } from "../util/MiscStuff";
import { VerifiedIcon } from "./RobloxIcons";
export function FriendCarousel({
friends: friendsUnsorted,
title,
dontSortByActivity,
...props
}: React.DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>,
HTMLDivElement
> & {
title: string;
dontSortByActivity?: boolean;
friends:
| {
hasVerifiedBadge: boolean;
id: number;
name: string;
displayName: string;
}[]
| null
| false;
}) {
const acct = useCurrentAccount();
const presence = useFriendsPresence(
(!!friendsUnsorted ? friendsUnsorted : []).map((f) => f.id)
);
const [friendsLabel, setFriendsLabel] = useState<string>("");
const [friends, setFriends] = useState<
{
hasVerifiedBadge: boolean;
id: number;
name: string;
displayName: string;
}[]
>([]);
useEffect(() => {
let numStudio = 0;
let numGame = 0;
let numOnline = 0;
for (const friend of friendsUnsorted || []) {
const st = presence.find((c) => c.userId === friend.id);
switch (st?.userPresenceType || 0) {
case 1:
numOnline += 1;
break;
case 2:
numGame += 1;
break;
case 3:
numStudio += 1;
break;
}
}
setFriendsLabel(
[
`${friends.length}`,
numGame === 0 ? null : `${numGame} in-game`,
numStudio === 0 ? null : `${numStudio} studio`
]
.filter((a) => !!a)
.join(" | ")
);
if (!friendsUnsorted) {
setFriends([]);
return;
}
setFriends(
friendsUnsorted.sort((a, b) => {
if (!!dontSortByActivity) return -10;
const userStatusA = presence.find((c) => c.userId === a.id);
const userStatusB = presence.find((c) => c.userId === b.id);
return (
(userStatusB?.userPresenceType || 0) -
(userStatusA?.userPresenceType || 0)
);
})
);
}, [friendsUnsorted, presence, dontSortByActivity]);
if (!friends || friends.length === 0) {
return <></>;
}
return (
<div {...props}>
{/* <button onClick={()=>console.log(acct,presence,friends)}>debug</button> */}
<h1 className="text-2xl pt-4 pl-4 -mb-4">
{title}{" "}
<span className="text-overlay1 text-sm pl-2">{friendsLabel}</span>
</h1>
<div className="rounded-xl flex flex-col gap-2 px-4 no-scrollbar">
<div
className="flex p-8 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={{
scrollSnapType: "x mandatory",
WebkitOverflowScrolling: "touch",
scrollbarWidth: "none"
}}
>
{/* <div className="w-8" /> */}
{friends.map((a) => {
const userStatus = presence.find(
(b) => b.userId === a.id
);
const userPresence = userStatus?.userPresenceType || 0;
const borderColor =
userPresence === 1
? "border-blue/25 bg-blue/25"
: userPresence === 2
? "border-green/25 bg-green/25"
: userPresence === 3
? "border-yellow/25 bg-yellow/25"
: userPresence === 0
? "border-surface2/25 bg-surface2/25"
: "border-red/25 bg-red/25";
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 (
<StupidHoverThing
key={a.id}
delayDuration={0}
text={
<div className="text-center items-center justify-center content-center">
<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}
{ userPresence >= 2 ? <p>{userStatus?.lastLocation}</p> : <></>}
</span>
</div>
}
>
<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`}
>
<span className="line-clamp-1 overflow-hidden text-ellipsis">
{a.displayName || a.name}
</span>
{!a.hasVerifiedBadge ? (
<VerifiedIcon
className={`text-base ${fillColor} w-3 h-3 shrink-0`}
/>
) : null}
</span>
</div>
</StupidHoverThing>
);
})}
{/* <div className="w-8" /> */}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,26 @@
import { useFriendsHome } from "@/hooks/roblox/useFriends";
import React from "react";
import { FriendCarousel } from "./FriendCarousel";
import { useBestFriends } from "@/hooks/roblox/useBestFriends";
export function FriendsHomeSect(
props: React.DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>,
HTMLDivElement
>
) {
const friends = useFriendsHome();
return <FriendCarousel {...props} title="Friends" friends={friends} />;
}
export function BestFriendsHomeSect(
props: React.DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>,
HTMLDivElement
>
) {
const friends = useBestFriends();
return <FriendCarousel {...props} title="Best Friends" dontSortByActivity friends={friends} />;
}

View File

@@ -1,13 +1,13 @@
"use client";
import { ContentMetadata } from "@/lib/omniRecommendation";
import LazyLoadedImage from "./lazyLoadedImage";
import LazyLoadedImage from "../util/LazyLoadedImage";
import {
ContextMenu,
ContextMenuContent,
ContextMenuSeparator,
ContextMenuTrigger
} from "./ui/context-menu";
} from "../ui/context-menu";
import { ContextMenuItem } from "@radix-ui/react-context-menu";
import React from "react";

View File

@@ -2,9 +2,8 @@ import { PremiumIconSmall, VerifiedIcon } from "./RobloxIcons";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger
} from "./ui/tooltip";
} from "../ui/tooltip";
export function RobloxPremiumSmall(props: React.SVGProps<SVGSVGElement>) {
return (

View File

@@ -1,21 +1,26 @@
"use client";
import React, { useEffect } from "react";
import LazyLoadedImage from "./lazyLoadedImage";
import { Alert, AlertDescription, AlertTitle } from "./ui/alert";
import LazyLoadedImage from "../util/LazyLoadedImage";
import { Alert, AlertDescription, AlertTitle } from "../ui/alert";
import { OctagonXIcon } from "lucide-react";
import { RobloxPremiumSmall, RobloxVerifiedSmall } from "./RobloxTooltipStuff";
import {
RobloxPremiumSmall,
RobloxVerifiedSmall
} from "@/components/roblox/RobloxTooltips";
import { useCurrentAccount } from "@/hooks/roblox/useCurrentAccount";
import { Skeleton } from "./ui/skeleton";
import { Skeleton } from "../ui/skeleton";
import { useFriendsPresence } from "@/hooks/roblox/usePresence";
import { useAccountSettings } from "@/hooks/roblox/useAccountSettings";
export function HomeLoggedInHeader() {
const profile = useCurrentAccount();
const accountSettings = useAccountSettings();
if (profile === false) {
return (
<div className="justify-center w-screen px-8 py-6">
<Alert variant="destructive" className="space-x-2">
<Alert variant="destructive" className="bg-base/50 space-x-2">
<OctagonXIcon />
<AlertTitle>Failed to fetch account info</AlertTitle>
<AlertDescription>
@@ -29,24 +34,26 @@ export function HomeLoggedInHeader() {
const presence = useFriendsPresence(profile ? [profile.id] : []);
const userActivity = presence.find((b) => b.userId === profile?.id)
const userActivity = presence.find((b) => b.userId === profile?.id);
const userPresence = userActivity?.userPresenceType;
const borderColor =
userPresence === 1
? "border-blue bg-blue/50"
? "border-blue/25 bg-blue/25"
: userPresence === 2
? "border-green bg-green/50"
? "border-green/25 bg-green/25"
: userPresence === 3
? "border-yellow bg-yellow/50"
? "border-yellow/25 bg-yellow/25"
: userPresence === 0
? "border-surface2 bg-surface2/50"
: "border-red bg-red/50";
? "border-surface2/25 bg-surface2/25"
: "border-red/25 bg-red/25";
const isLoaded = !!profile && !!accountSettings;
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">
{!profile ? (
<div className="flex items-center gap-6 rounded-xl px-8 py-6 w-fit mt-8 ml-0">
{!isLoaded ? (
<Skeleton className="w-28 h-28 rounded-full" />
) : (
<LazyLoadedImage
@@ -57,22 +64,39 @@ export function HomeLoggedInHeader() {
)}
<div className="flex flex-col justify-center">
<span className="text-3xl font-bold text-text flex items-center gap-2">
{profile ? (
<>Hello, {profile.displayName}</>
{isLoaded ? (
<>
{!!accountSettings &&
accountSettings.IsPremium === true
? `Howdy, ${profile.displayName}`
: `${profile.displayName}`}
</>
) : (
<>
<Skeleton className="w-96 h-8 rounded-lg" />
</>
)}
{/* TODO: Fetch the User's Roblox Premium subscription state */}
<RobloxPremiumSmall className="w-6 h-6 fill-transparent" />
<RobloxVerifiedSmall className="w-6 h-6 fill-blue text-base" />
{!!accountSettings &&
accountSettings.IsPremium === true ? (
<RobloxPremiumSmall className="w-6 h-6 fill-transparent" />
) : (
<></>
)}
{isLoaded ? (
<RobloxVerifiedSmall className="w-6 h-6 fill-blue text-base" />
) : (
<></>
)}
</span>
<span className="text-base font-mono text-subtext0 mt-1">
{profile ? (
{isLoaded ? (
<>
@{profile.name}
{(!!userActivity && userPresence === 2) ? <> - {userActivity.lastLocation}</> : <></> }
{!!userActivity && userPresence === 2 ? (
<> - {userActivity.lastLocation}</>
) : (
<></>
)}
</>
) : (
<Skeleton className="w-64 h-6 rounded-lg" />

View File

@@ -0,0 +1,52 @@
"use client";
import { useAvatarOutfits } from "@/hooks/roblox/useAvatarOutfits";
import { Button } from "@/components/ui/button";
import { cn, proxyFetch } from "@/lib/utils";
import LazyLoadedImage from "../util/LazyLoadedImage";
import { StupidHoverThing } from "../util/MiscStuff";
import { loadThumbnails } from "@/lib/thumbnailLoader";
import { useCurrentAccount } from "@/hooks/roblox/useCurrentAccount";
type OutfitSelectorProps = {
setVisible: (visible: boolean) => void;
updateOutfit: (outfit: { id: number }, acc: {id: number}) => Promise<void>;
};
export function OutfitSelector({ setVisible, updateOutfit }: OutfitSelectorProps) {
const outfits = useAvatarOutfits();
const acc = useCurrentAccount();
if (!outfits || !acc) return null;
return (
<div className="z-30 isolate absolute inset-0 flex items-center justify-center bg-crust/50">
<button
className="z-10 absolute w-screen h-screen cursor-default"
onClick={() => {
setVisible(false);
}}
/>
<div className="z-20 grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4 p-8 bg-crust/90 rounded-xl">
{(outfits || []).map((outfit: { id: number; name: string }) => (
<button
key={outfit.id}
className="hover:bg-base/50 rounded-lg"
onClick={async () => {
updateOutfit(outfit,acc);
setVisible(false);
}}
>
<StupidHoverThing delayDuration={0} text={outfit.name}>
<LazyLoadedImage
imgId={`Outfit_${outfit.id}`}
alt={outfit.name}
className="w-32 h-32 rounded-md"
/>
</StupidHoverThing>
</button>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,99 @@
"use client";
import { useRobuxBalance } from "@/hooks/roblox/useRobuxBalance";
import { RobuxIcon } from "../roblox/RobloxIcons";
import React, { useState } from "react";
import { Separator } from "../ui/separator";
import { Bell, SettingsIcon, ShirtIcon } from "lucide-react";
import { StupidHoverThing } from "../util/MiscStuff";
import { toast } from "sonner";
import { OutfitSelector } from "./OutfitQuickChooser";
import { proxyFetch } from "@/lib/utils";
import { loadThumbnails } from "@/lib/thumbnailLoader";
/**
requires csrf token cuz u cant use noblox.js on the web
either go to https://roblox.com/my/avataar or the app to change ur fit
*/
async function updateOutfit(outfit: { id: number }, acc: {id: number}) {
try {
const J = (await (
await proxyFetch(
`https://avatar.roblox.com/v3/outfits/${outfit.id}/details`
)
).json()) as {
id: number;
name: string;
assets: any[];
};
await proxyFetch(
`https://avatar.roblox.com/v1/avatar/set-wearing-assets`,
{
method: "POST",
body: JSON.stringify({
assetIds: J.assets.map(a=>a.id).filter(a=>!!a)
})
}
);
loadThumbnails([
{
type: "AvatarHeadShot",
targetId: acc.id,
format: "webp",
size: "720x720"
}
]).catch((a) => {});
} catch {}
}
export const QuickTopUI = React.memo(function () {
const robux = useRobuxBalance();
const [isOutfitSelectorVisible, setIsOutfitSelectorVisible] =
useState<boolean>(false);
return (
<>
{/* {isOutfitSelectorVisible ? (
<OutfitSelector setVisible={setIsOutfitSelectorVisible} updateOutfit={updateOutfit} />
) : (
<></>
)} */}
<div className="z-50 absolute top-4 right-4 p-4 flex gap-2 items-center text-blue/75">
{/* <StupidHoverThing text="Change Outfit">
<button
className="rounded-full bg-crust/50 flex items-center p-2"
onClick={() => {
setIsOutfitSelectorVisible((a) => !a);
}}
>
<ShirtIcon />
</button>
</StupidHoverThing> */}
<StupidHoverThing
text={!robux ? "Loading..." : `You have ${robux} Robux`}
>
<div className="rounded-full bg-crust/50 flex items-center p-2">
<RobuxIcon className="w-6 h-6" />
{robux ? (
<p className="pl-1">{robux || "???"}</p>
) : (
<></>
)}
</div>
</StupidHoverThing>
</div>
</>
);
});
export const QuickTopUILogoPart = React.memo(function () {
return (
<div className="z-[15] relative top-4 left-4 p-4 flex gap-4 items-center text-blue">
<img src="/icon-512.webp" className="-m-1 w-8 h-8" alt="" />
<p className="mt-2">{"not roblox lol"}</p>
</div>
);
});

View File

@@ -1,6 +1,6 @@
import React from "react";
import { useThumbnailURL } from "@/hooks/use-lazy-load";
import { Skeleton } from "./ui/skeleton";
import { Skeleton } from "../ui/skeleton";
import Image from "next/image";
interface LazyLoadedImageProps {

View File

@@ -0,0 +1,16 @@
import { TooltipProps } from "@radix-ui/react-tooltip";
import { VerifiedIcon } from "../roblox/RobloxIcons";
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
export function StupidHoverThing({ children, text, ...props }: React.PropsWithChildren & TooltipProps & { text: string | React.ReactNode }) {
return (
<Tooltip {...props}>
<TooltipTrigger asChild>
{children}
</TooltipTrigger>
<TooltipContent className="bg-surface0 text-text m-2">
<span className="text-sm flex items-center">{text}</span>
</TooltipContent>
</Tooltip>
)
}