diff --git a/src/features/stats/api/achievements/condition.js b/src/features/stats/api/achievements/condition.js index e273ba59..c45ef8da 100644 --- a/src/features/stats/api/achievements/condition.js +++ b/src/features/stats/api/achievements/condition.js @@ -1,9 +1,11 @@ import { achievements } from './index'; import statsModule from '../stats'; +import { getAchievements, saveAchievement } from 'utils/indexedDB'; export async function checkAchievements() { - // First get the events data which contains all user actions + const storedAchievements = await getAchievements(); const events = await statsModule.getStats(); + const currentTabId = statsModule.getCurrentTabId(); // Calculate aggregated stats const aggregatedStats = { @@ -13,83 +15,51 @@ export async function checkAchievements() { level: statsModule.level || 1, }; - console.log('Checking achievements with aggregated stats:', aggregatedStats); + const newlyAchieved = []; + const updatedAchievements = achievements.map((achievement) => { + const stored = storedAchievements.find((a) => a.id === achievement.id); + if (stored && stored.achieved) { + return stored; + } - achievements.forEach((achievement) => { + let achieved = false; switch (achievement.condition.type) { case 'tabsOpened': - console.log( - `Checking achievement: ${achievement.id}, Condition: ${achievement.condition.type}, Required: ${achievement.condition.amount}, Current: ${aggregatedStats['new-tab']}`, - ); - if (aggregatedStats['new-tab'] >= achievement.condition.amount) { - achievement.achieved = true; - } + achieved = aggregatedStats['new-tab'] >= achievement.condition.amount; break; - case 'addonInstall': - console.log( - `Checking achievement: ${achievement.id}, Condition: ${achievement.condition.type}, Required: ${achievement.condition.amount}, Current: ${aggregatedStats['marketplace-install']}`, - ); - if (aggregatedStats['marketplace-install'] >= achievement.condition.amount) { - achievement.achieved = true; - } + achieved = aggregatedStats['marketplace-install'] >= achievement.condition.amount; break; - case 'reachLevel': - console.log( - `Checking achievement: ${achievement.id}, Condition: ${achievement.condition.type}, Required: ${achievement.condition.amount}, Current: ${aggregatedStats.level}`, - ); - if (aggregatedStats.level >= achievement.condition.amount) { - achievement.achieved = true; - } + achieved = aggregatedStats.level >= achievement.condition.amount; break; - case 'earnXp': - console.log( - `Checking achievement: ${achievement.id}, Condition: ${achievement.condition.type}, Required: ${achievement.condition.amount}, Current: ${aggregatedStats.totalXp}`, - ); - if (aggregatedStats.totalXp >= achievement.condition.amount) { - achievement.achieved = true; - } - break; - - default: - console.warn(`Unknown achievement condition type: ${achievement.condition.type}`); + achieved = aggregatedStats.totalXp >= achievement.condition.amount; break; } + + if (achieved && !stored?.achieved) { + const updatedAchievement = { + ...achievement, + achieved: true, + timestamp: new Date().toISOString(), + tabId: currentTabId, + }; + newlyAchieved.push(updatedAchievement); + saveAchievement(updatedAchievement); + return updatedAchievement; + } + + return achievement; }); - return achievements; + return { updatedAchievements, newlyAchieved }; } export async function newAchievements() { - // Get previously achieved achievements - const oldAchievements = JSON.parse(localStorage.getItem('achievements')) || []; - - // Check for new achievements - const checkedAchievements = await checkAchievements(); - const newAchievements = []; - - checkedAchievements.forEach((achievement) => { - const isNewAchievement = - achievement.achieved && !oldAchievements.some((a) => a.id === achievement.id); - - if (isNewAchievement) { - const newAchievement = { - ...achievement, - timestamp: new Date().toISOString(), - }; - newAchievements.push(newAchievement); - } - }); - - // Update stored achievements - if (newAchievements.length > 0) { - const updatedAchievements = [...oldAchievements, ...newAchievements]; - localStorage.setItem('achievements', JSON.stringify(updatedAchievements)); - } - - return newAchievements; + const { updatedAchievements, newlyAchieved } = await checkAchievements(); + // Only return newly achieved achievements for toast notifications + return newlyAchieved; } // Helper function to get achievement progress diff --git a/src/features/stats/api/stats.js b/src/features/stats/api/stats.js index c7d551d0..77a641f1 100644 --- a/src/features/stats/api/stats.js +++ b/src/features/stats/api/stats.js @@ -11,6 +11,8 @@ import variables from 'config/variables'; export default class Stats { static #currentTabId = null; + static #toastTimeout = null; + static #processedAchievements = new Set(); static generateTabId() { return `tab_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`; @@ -24,19 +26,29 @@ export default class Stats { } static async achievementTrigger(stats) { - const newAchievement = await newAchievements(stats); - newAchievement.forEach((achievement) => { - if (achievement) { - const { name } = getLocalisedAchievementData(achievement.id); - toast.info( - `🏆 ${variables.getMessage('settings:sections.stats.achievement_unlocked', { name: name })}`, - { - icon: false, - closeButton: false, - }, - ); - } - }); + // Clear any pending achievement checks + if (this.#toastTimeout) { + clearTimeout(this.#toastTimeout); + } + + this.#toastTimeout = setTimeout(async () => { + const newlyAchieved = await newAchievements(stats); + + newlyAchieved.forEach((achievement) => { + // Only show toast if we haven't processed this achievement in this session + if (!this.#processedAchievements.has(achievement.id)) { + const { name } = getLocalisedAchievementData(achievement.id); + toast.info( + `🏆 ${variables.getMessage('settings:sections.stats.achievement_unlocked', { name })}`, + { + icon: false, + closeButton: false, + }, + ); + this.#processedAchievements.add(achievement.id); + } + }); + }, 1000); // 1 second debounce } static calculateNextLevelXp(level) { diff --git a/src/features/stats/options/StatsOptions.jsx b/src/features/stats/options/StatsOptions.jsx index 827d08dc..4bec507d 100644 --- a/src/features/stats/options/StatsOptions.jsx +++ b/src/features/stats/options/StatsOptions.jsx @@ -17,6 +17,7 @@ import { achievements as initialAchievements, checkAchievements, } from 'features/stats/api/achievements'; +import { clearAchievements } from 'utils/indexedDB'; const Stats = () => { const [stats, setStats] = useState(() => JSON.parse(localStorage.getItem('statsData')) || {}); @@ -26,8 +27,8 @@ const Stats = () => { const [clearmodal, setClearmodal] = useState(false); const updateAchievements = useCallback(async () => { - const achieved = await checkAchievements(); - setAchievements(achieved); + const { updatedAchievements } = await checkAchievements(); // Destructure the result + setAchievements(updatedAchievements); // Use the updatedAchievements array }, []); useEffect(() => { @@ -38,7 +39,7 @@ const Stats = () => { return achievements.filter((achievement) => achievement.achieved).length; }, [achievements]); - const resetStats = () => { + const resetStats = async () => { const emptyStats = { level: 1, totalXp: 0, @@ -48,12 +49,12 @@ const Stats = () => { }; localStorage.setItem('statsData', JSON.stringify(emptyStats)); localStorage.setItem('eventLog', JSON.stringify([])); - localStorage.setItem('achievements', JSON.stringify(initialAchievements)); + await clearAchievements(); // Clear achievements from IndexedDB setStats(emptyStats); setClearmodal(false); toast(variables.getMessage('toasts.stats_reset')); - updateAchievements(); // Call updateAchievements to refresh achievements after reset - Stats.clearSessionStats(); // Clear session stats when resetting + updateAchievements(); + Stats.clearSessionStats(); }; const downloadStats = () => { diff --git a/src/features/stats/options/sections/Achievements.jsx b/src/features/stats/options/sections/Achievements.jsx index 89f3ccb4..861454ef 100644 --- a/src/features/stats/options/sections/Achievements.jsx +++ b/src/features/stats/options/sections/Achievements.jsx @@ -1,8 +1,9 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { FaTrophy } from 'react-icons/fa'; import { MdAccessTime, MdLock } from 'react-icons/md'; import { PreferencesWrapper } from 'components/Layout/Settings'; import { getLocalisedAchievementData } from 'features/stats/api/achievements'; +import { achievements as initialAchievements } from 'features/stats/api/achievements'; import { motion } from 'framer-motion'; import { FaChevronDown, FaChevronUp } from 'react-icons/fa'; @@ -14,20 +15,43 @@ const AchievementElement = ({ achievementKey, id, achieved, timestamp }) => { const { name, description } = getLocalisedAchievementData(id); return ( -