diff --git a/src/main/events/index.ts b/src/main/events/index.ts index 80363e4e..ffdfc354 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -50,6 +50,7 @@ import "./user/unblock-user"; import "./user/get-user-friends"; import "./user/get-user-stats"; import "./user/report-user"; +import "./user/get-compared-unlocked-achievements"; import "./profile/get-friend-requests"; import "./profile/get-me"; import "./profile/undo-friendship"; diff --git a/src/main/events/user/get-compared-unlocked-achievements.ts b/src/main/events/user/get-compared-unlocked-achievements.ts new file mode 100644 index 00000000..8c5c8779 --- /dev/null +++ b/src/main/events/user/get-compared-unlocked-achievements.ts @@ -0,0 +1,44 @@ +import type { ComparedAchievements, GameShop } from "@types"; +import { registerEvent } from "../register-event"; +import { userPreferencesRepository } from "@main/repository"; +import { HydraApi } from "@main/services"; + +const getComparedUnlockedAchievements = async ( + _event: Electron.IpcMainInvokeEvent, + objectId: string, + shop: GameShop, + userId: string +) => { + const userPreferences = await userPreferencesRepository.findOne({ + where: { id: 1 }, + }); + + return HydraApi.get( + `/users/${userId}/games/achievements/compare`, + { + shop, + objectId, + language: userPreferences?.language || "en", + } + ).then((achievements) => { + const sortedAchievements = achievements.achievements.sort((a, b) => { + if (a.otherUserStat.unlocked && !b.otherUserStat.unlocked) return -1; + if (!a.otherUserStat.unlocked && b.otherUserStat.unlocked) return 1; + if (a.otherUserStat.unlocked && b.otherUserStat.unlocked) { + return b.otherUserStat.unlockTime! - a.otherUserStat.unlockTime!; + } + + return Number(a.hidden) - Number(b.hidden); + }); + + return { + ...achievements, + achievements: sortedAchievements, + } as ComparedAchievements; + }); +}; + +registerEvent( + "getComparedUnlockedAchievements", + getComparedUnlockedAchievements +); diff --git a/src/preload/index.ts b/src/preload/index.ts index fc2e5a91..0eadd409 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -259,6 +259,17 @@ contextBridge.exposeInMainWorld("electron", { getUserStats: (userId: string) => ipcRenderer.invoke("getUserStats", userId), reportUser: (userId: string, reason: string, description: string) => ipcRenderer.invoke("reportUser", userId, reason, description), + getComparedUnlockedAchievements: ( + objectId: string, + shop: GameShop, + userId: string + ) => + ipcRenderer.invoke( + "getComparedUnlockedAchievements", + objectId, + shop, + userId + ), /* Auth */ signOut: () => ipcRenderer.invoke("signOut"), diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 91bed316..e6a47959 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -29,6 +29,7 @@ import type { GameArtifact, LudusaviBackup, UserAchievement, + ComparedAchievements, } from "@types"; import type { AxiosProgressEvent } from "axios"; import type { DiskSpace } from "check-disk-space"; @@ -202,6 +203,11 @@ declare global { reason: string, description: string ) => Promise; + getComparedUnlockedAchievements: ( + objectId: string, + shop: GameShop, + userId: string + ) => Promise; /* Profile */ getMe: () => Promise; diff --git a/src/renderer/src/helpers.ts b/src/renderer/src/helpers.ts index d4563330..a241bf47 100644 --- a/src/renderer/src/helpers.ts +++ b/src/renderer/src/helpers.ts @@ -36,15 +36,13 @@ export const buildGameDetailsPath = ( export const buildGameAchievementPath = ( game: { shop: GameShop; objectId: string; title: string }, - user?: { userId: string; displayName: string; profileImageUrl: string | null } + user?: { userId: string } ) => { const searchParams = new URLSearchParams({ title: game.title, shop: game.shop, objectId: game.objectId, userId: user?.userId || "", - displayName: user?.displayName || "", - profileImageUrl: user?.profileImageUrl || "", }); return `/achievements/?${searchParams.toString()}`; diff --git a/src/renderer/src/pages/achievements/achievements-content.tsx b/src/renderer/src/pages/achievements/achievements-content.tsx index 09b3d311..ca60be70 100644 --- a/src/renderer/src/pages/achievements/achievements-content.tsx +++ b/src/renderer/src/pages/achievements/achievements-content.tsx @@ -1,40 +1,37 @@ import { setHeaderTitle } from "@renderer/features"; import { useAppDispatch, useDate, useUserDetails } from "@renderer/hooks"; import { steamUrlBuilder } from "@shared"; -import { useContext, useEffect, useMemo, useRef, useState } from "react"; +import { useContext, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import * as styles from "./achievements.css"; import { buildGameDetailsPath, formatDownloadProgress, } from "@renderer/helpers"; -import { - CheckCircleIcon, - LockIcon, - PersonIcon, - TrophyIcon, -} from "@primer/octicons-react"; +import { LockIcon, PersonIcon, TrophyIcon } from "@primer/octicons-react"; import { SPACING_UNIT, vars } from "@renderer/theme.css"; import { gameDetailsContext } from "@renderer/context"; -import { UserAchievement } from "@types"; +import { ComparedAchievements, UserAchievement } from "@types"; import { average } from "color.js"; import Color from "color"; import { Link } from "@renderer/components"; +import { ComparedAchievementList } from "./compared-achievement-list"; interface UserInfo { userId: string; displayName: string; - achievements: UserAchievement[]; profileImageUrl: string | null; + totalAchievementCount: number; + unlockedAchievementCount: number; } interface AchievementsContentProps { otherUser: UserInfo | null; + comparedAchievements: ComparedAchievements | null; } interface AchievementListProps { - user: UserInfo; - otherUser: UserInfo | null; + achievements: UserAchievement[]; } interface AchievementSummaryProps { @@ -46,11 +43,6 @@ function AchievementSummary({ user, isComparison }: AchievementSummaryProps) { const { t } = useTranslation("achievement"); const { userDetails, hasActiveSubscription } = useUserDetails(); - const userTotalAchievementCount = user.achievements.length; - const userUnlockedAchievementCount = user.achievements.filter( - (achievement) => achievement.unlocked - ).length; - const getProfileImage = (user: UserInfo) => { return (
@@ -155,19 +147,19 @@ function AchievementSummary({ user, isComparison }: AchievementSummaryProps) { > - {userUnlockedAchievementCount} / {userTotalAchievementCount} + {user.unlockedAchievementCount} / {user.totalAchievementCount}
{formatDownloadProgress( - userUnlockedAchievementCount / userTotalAchievementCount + user.unlockedAchievementCount / user.totalAchievementCount )} @@ -175,132 +167,30 @@ function AchievementSummary({ user, isComparison }: AchievementSummaryProps) { ); } -function AchievementList({ user, otherUser }: AchievementListProps) { - const achievements = user.achievements; - const otherUserAchievements = otherUser?.achievements; - +function AchievementList({ achievements }: AchievementListProps) { const { t } = useTranslation("achievement"); const { formatDateTime } = useDate(); - const { hasActiveSubscription } = useUserDetails(); - - if (!otherUserAchievements || otherUserAchievements.length === 0) { - return ( -
    - {achievements.map((achievement, index) => ( -
  • - {achievement.displayName} -
    -

    {achievement.displayName}

    -

    {achievement.description}

    -
    - {achievement.unlockTime && ( -
    - {t("unlocked_at")} -

    {formatDateTime(achievement.unlockTime)}

    -
    - )} -
  • - ))} -
- ); - } - return (
    - {otherUserAchievements.map((otherUserAchievement, index) => ( -
  • -
    - {otherUserAchievement.displayName} -
    -

    {otherUserAchievement.displayName}

    -

    {otherUserAchievement.description}

    -
    + {achievements.map((achievement, index) => ( +
  • + {achievement.displayName} +
    +

    {achievement.displayName}

    +

    {achievement.description}

    - - {hasActiveSubscription ? ( - achievements[index].unlocked ? ( -
    - - {formatDateTime(achievements[index].unlockTime!)} -
    - ) : ( -
    - -
    - ) - ) : null} - - {otherUserAchievement.unlocked ? ( -
    - - {formatDateTime(otherUserAchievement.unlockTime!)} -
    - ) : ( -
    - + {achievement.unlockTime && ( +
    + {t("unlocked_at")} +

    {formatDateTime(achievement.unlockTime)}

    )}
  • @@ -309,7 +199,10 @@ function AchievementList({ user, otherUser }: AchievementListProps) { ); } -export function AchievementsContent({ otherUser }: AchievementsContentProps) { +export function AchievementsContent({ + otherUser, + comparedAchievements, +}: AchievementsContentProps) { const heroRef = useRef(null); const containerRef = useRef(null); const [isHeaderStuck, setIsHeaderStuck] = useState(false); @@ -317,20 +210,6 @@ export function AchievementsContent({ otherUser }: AchievementsContentProps) { const { gameTitle, objectId, shop, achievements, gameColor, setGameColor } = useContext(gameDetailsContext); - const sortedAchievements = useMemo(() => { - if (!otherUser || otherUser.achievements.length === 0) return achievements!; - - return achievements!.sort((a, b) => { - const indexA = otherUser.achievements.findIndex( - (achievement) => achievement.name === a.name - ); - const indexB = otherUser.achievements.findIndex( - (achievement) => achievement.name === b.name - ); - return indexA - indexB; - }); - }, [achievements, otherUser]); - const dispatch = useAppDispatch(); const { userDetails, hasActiveSubscription } = useUserDetails(); @@ -367,14 +246,17 @@ export function AchievementsContent({ otherUser }: AchievementsContentProps) { } }; - const getProfileImage = (user: UserInfo) => { + const getProfileImage = ( + profileImageUrl: string | null, + displayName: string + ) => { return (
    - {user.profileImageUrl ? ( + {profileImageUrl ? ( {user.displayName} ) : ( @@ -434,7 +316,13 @@ export function AchievementsContent({ otherUser }: AchievementsContentProps) { user={{ ...userDetails, userId: userDetails.id, - achievements: sortedAchievements, + totalAchievementCount: comparedAchievements + ? comparedAchievements.ownerUser.totalAchievementCount + : achievements!.length, + unlockedAchievementCount: comparedAchievements + ? comparedAchievements.ownerUser.unlockedAchievementCount + : achievements!.filter((achievement) => achievement.unlocked) + .length, }} isComparison={otherUser !== null} /> @@ -458,15 +346,17 @@ export function AchievementsContent({ otherUser }: AchievementsContentProps) {
    {hasActiveSubscription && (
    - {getProfileImage({ - ...userDetails, - userId: userDetails.id, - achievements: sortedAchievements, - })} + {getProfileImage( + userDetails.profileImageUrl, + userDetails.displayName + )}
    )}
    - {getProfileImage(otherUser)} + {getProfileImage( + otherUser.profileImageUrl, + otherUser.displayName + )}
    @@ -480,14 +370,11 @@ export function AchievementsContent({ otherUser }: AchievementsContentProps) { backgroundColor: vars.color.background, }} > - + {otherUser ? ( + + ) : ( + + )} diff --git a/src/renderer/src/pages/achievements/achievements.tsx b/src/renderer/src/pages/achievements/achievements.tsx index a53eaba8..d15ba0b9 100644 --- a/src/renderer/src/pages/achievements/achievements.tsx +++ b/src/renderer/src/pages/achievements/achievements.tsx @@ -1,7 +1,7 @@ import { setHeaderTitle } from "@renderer/features"; import { useAppDispatch, useUserDetails } from "@renderer/hooks"; -import type { GameShop, UserAchievement } from "@types"; -import { useEffect, useState } from "react"; +import type { ComparedAchievements, GameShop } from "@types"; +import { useEffect, useMemo, useState } from "react"; import { useSearchParams } from "react-router-dom"; import { vars } from "@renderer/theme.css"; import { @@ -18,14 +18,11 @@ export default function Achievements() { const shop = searchParams.get("shop"); const title = searchParams.get("title"); const userId = searchParams.get("userId"); - const displayName = searchParams.get("displayName"); - const profileImageUrl = searchParams.get("profileImageUrl"); const { userDetails } = useUserDetails(); - const [otherUserAchievements, setOtherUserAchievements] = useState< - UserAchievement[] | null - >(null); + const [comparedAchievements, setComparedAchievements] = + useState(null); const dispatch = useAppDispatch(); @@ -36,31 +33,34 @@ export default function Achievements() { }, [dispatch, title]); useEffect(() => { - setOtherUserAchievements(null); + setComparedAchievements(null); + if (userDetails?.id == userId) { - setOtherUserAchievements([]); return; } if (objectId && shop && userId) { window.electron - .getGameAchievements(objectId, shop as GameShop, userId) - .then((achievements) => { - setOtherUserAchievements(achievements); - }); + .getComparedUnlockedAchievements(objectId, shop as GameShop, userId) + .then(setComparedAchievements); } }, [objectId, shop, userId]); const otherUserId = userDetails?.id === userId ? null : userId; - const otherUser = otherUserId - ? { - userId: otherUserId, - displayName: displayName || "", - achievements: otherUserAchievements || [], - profileImageUrl: profileImageUrl || "", - } - : null; + const otherUser = useMemo(() => { + if (!otherUserId || !comparedAchievements) return null; + + return { + userId: otherUserId, + displayName: comparedAchievements.otherUser.displayName, + profileImageUrl: comparedAchievements.otherUser.profileImageUrl, + totalAchievementCount: + comparedAchievements.otherUser.totalAchievementCount, + unlockedAchievementCount: + comparedAchievements.otherUser.unlockedAchievementCount, + }; + }, [otherUserId, comparedAchievements]); return ( {({ isLoading, achievements }) => { + const showSkeleton = + isLoading || + achievements === null || + (otherUserId && comparedAchievements === null); + return ( - {isLoading || - achievements === null || - (otherUserId && otherUserAchievements === null) ? ( + {showSkeleton ? ( ) : ( - + )} ); diff --git a/src/renderer/src/pages/achievements/compared-achievement-list.tsx b/src/renderer/src/pages/achievements/compared-achievement-list.tsx new file mode 100644 index 00000000..6c0484c3 --- /dev/null +++ b/src/renderer/src/pages/achievements/compared-achievement-list.tsx @@ -0,0 +1,110 @@ +import type { ComparedAchievements } from "@types"; +import * as styles from "./achievements.css"; +import { CheckCircleIcon, LockIcon } from "@primer/octicons-react"; +import { useDate } from "@renderer/hooks"; +import { SPACING_UNIT } from "@renderer/theme.css"; + +export interface ComparedAchievementListProps { + achievements: ComparedAchievements; +} + +export function ComparedAchievementList({ + achievements, +}: ComparedAchievementListProps) { + const { formatDateTime } = useDate(); + + return ( +
      + {achievements.achievements.map((achievement, index) => ( +
    • +
      + {achievement.displayName} +
      +

      {achievement.displayName}

      +

      {achievement.description}

      +
      +
      + + {achievement.onwerUserStat ? ( + achievement.onwerUserStat.unlocked ? ( +
      + + + {formatDateTime(achievement.onwerUserStat.unlockTime!)} + +
      + ) : ( +
      + +
      + ) + ) : null} + + {achievement.otherUserStat.unlocked ? ( +
      + + + {formatDateTime(achievement.otherUserStat.unlockTime!)} + +
      + ) : ( +
      + +
      + )} +
    • + ))} +
    + ); +} diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.tsx b/src/renderer/src/pages/profile/profile-content/profile-content.tsx index de22313e..a989b81d 100644 --- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx +++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx @@ -56,8 +56,6 @@ export function ProfileContent() { const userParams = userProfile ? { userId: userProfile.id, - displayName: userProfile.displayName, - profileImageUrl: userProfile.profileImageUrl, } : undefined; diff --git a/src/types/index.ts b/src/types/index.ts index 6da26914..c3a91053 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -342,6 +342,33 @@ export interface GameArtifact { downloadCount: number; } +export interface ComparedAchievements { + ownerUser: { + totalAchievementCount: number; + unlockedAchievementCount: number; + }; + otherUser: { + displayName: string; + profileImageUrl: string; + totalAchievementCount: number; + unlockedAchievementCount: number; + }; + achievements: { + hidden: boolean; + icon: string; + displayName: string; + description: string; + onwerUserStat?: { + unlocked: boolean; + unlockTime: number; + }; + otherUserStat: { + unlocked: boolean; + unlockTime: number; + }; + }[]; +} + export * from "./steam.types"; export * from "./real-debrid.types"; export * from "./ludusavi.types";