From b88ae377b17f42c5b1334a38ea9983aafd31ccd8 Mon Sep 17 00:00:00 2001 From: Sheldan <5037282+Sheldan@users.noreply.github.com> Date: Tue, 26 Nov 2024 23:17:24 +0100 Subject: [PATCH] [AB-xxx] adding feature to focus on a particular user in both leaderboard command and leaderboard UI adding button to view the specified user on loaderboard at the rank command --- .../experience/api/LeaderboardController.java | 82 ++++++++++++- .../command/LeaderBoardCommand.java | 61 +++++++--- .../abstracto/experience/command/Rank.java | 16 ++- .../converter/LeaderBoardModelConverter.java | 20 ++-- .../repository/UserExperienceRepository.java | 27 +++++ .../service/AUserExperienceServiceBean.java | 27 ++++- .../UserExperienceManagementServiceBean.java | 6 + .../LeaderBoardModelConverterTest.java | 87 -------------- .../experience/model/LeaderBoardEntry.java | 21 ++-- .../database/LeaderBoardEntryResult.java | 7 +- .../model/template/LeaderBoardModel.java | 6 + .../experience/model/template/RankModel.java | 1 + .../service/AUserExperienceService.java | 1 + .../UserExperienceManagementService.java | 1 + .../python/endpoints/leaderboard.py | 17 ++- ui/experience-tracking/public/index.html | 2 +- ui/experience-tracking/src/App.tsx | 4 +- .../src/components/Leaderboard.tsx | 109 ++++++++++++++++-- ui/experience-tracking/tsconfig.json | 2 +- 19 files changed, 345 insertions(+), 152 deletions(-) delete mode 100644 abstracto-application/abstracto-modules/experience-tracking/experience-tracking-impl/src/test/java/dev/sheldan/abstracto/experience/converter/LeaderBoardModelConverterTest.java diff --git a/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-impl/src/main/java/dev/sheldan/abstracto/experience/api/LeaderboardController.java b/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-impl/src/main/java/dev/sheldan/abstracto/experience/api/LeaderboardController.java index 6f1f45ab2..48109e272 100644 --- a/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-impl/src/main/java/dev/sheldan/abstracto/experience/api/LeaderboardController.java +++ b/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-impl/src/main/java/dev/sheldan/abstracto/experience/api/LeaderboardController.java @@ -1,15 +1,26 @@ package dev.sheldan.abstracto.experience.api; +import dev.sheldan.abstracto.core.models.ServerUser; import dev.sheldan.abstracto.core.models.database.AServer; +import dev.sheldan.abstracto.core.models.database.AUserInAServer; import dev.sheldan.abstracto.core.models.frontend.RoleDisplay; import dev.sheldan.abstracto.core.models.frontend.UserDisplay; import dev.sheldan.abstracto.core.service.GuildService; import dev.sheldan.abstracto.core.service.management.ServerManagementService; +import dev.sheldan.abstracto.core.service.management.UserInServerManagementService; import dev.sheldan.abstracto.experience.model.api.UserExperienceDisplay; +import dev.sheldan.abstracto.experience.model.database.AExperienceLevel; import dev.sheldan.abstracto.experience.model.database.AExperienceRole; import dev.sheldan.abstracto.experience.model.database.AUserExperience; +import dev.sheldan.abstracto.experience.model.database.LeaderBoardEntryResult; import dev.sheldan.abstracto.experience.service.ExperienceLevelService; +import dev.sheldan.abstracto.experience.service.management.ExperienceLevelManagementService; +import dev.sheldan.abstracto.experience.service.management.ExperienceRoleManagementService; import dev.sheldan.abstracto.experience.service.management.UserExperienceManagementService; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Role; @@ -23,6 +34,7 @@ import org.springframework.data.web.SortDefault; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @@ -41,9 +53,18 @@ public class LeaderboardController { @Autowired private ExperienceLevelService experienceLevelService; + @Autowired + private UserInServerManagementService userInServerManagementService; + + @Autowired + private ExperienceRoleManagementService experienceRoleManagementService; + + @Autowired + private ExperienceLevelManagementService experienceLevelManagementService; + @GetMapping(value = "/leaderboards/{serverId}", produces = "application/json") public Page getLeaderboard(@PathVariable("serverId") Long serverId, - @PageableDefault(value = 25, page = 0) + @PageableDefault(value = 50, page = 0) @SortDefault(sort = "experience", direction = Sort.Direction.DESC) Pageable pageable) { AServer server = serverManagementService.loadServer(serverId); @@ -53,6 +74,23 @@ public class LeaderboardController { .map(userExperience -> convertFromUser(guild, userExperience, pageable, allElements)); } + @GetMapping(value = "/leaderboards/{serverId}/{userId}", produces = "application/json") + public List getLeaderboardForUser(@PathVariable("serverId") Long serverId, @PathVariable("userId") Long userId, + @RequestParam("windowSize") Integer windowSize) { + AUserInAServer aUserInAServer = userInServerManagementService.loadOrCreateUser(ServerUser.fromId(serverId, userId)); + Map experienceRolesForServer = experienceRoleManagementService.getExperienceRolesForServer(aUserInAServer.getServerReference()) + .stream() + .collect(Collectors.toMap(AExperienceRole::getId, Function.identity())); + + Map levels = + experienceLevelManagementService.getLevelConfig().stream().collect(Collectors.toMap(AExperienceLevel::getLevel, Function.identity())); + Guild guild = guildService.getGuildById(serverId); + List allElements = userExperienceManagementService.getWindowedLeaderboardEntriesForUser(aUserInAServer, windowSize); + return allElements.stream() + .map(leaderboardEntry -> convertFromLeaderboardEntry(guild, leaderboardEntry, experienceRolesForServer, levels)) + .toList(); + } + private UserExperienceDisplay convertFromUser(Guild guild, AUserExperience aUserExperience, Pageable pageable, Page page) { Long userId = aUserExperience.getUser().getUserReference().getId(); Member member = guild.getMember(UserSnowflake.fromId(userId)); @@ -92,4 +130,46 @@ public class LeaderboardController { .build(); } + private UserExperienceDisplay convertFromLeaderboardEntry(Guild guild, LeaderBoardEntryResult leaderBoardEntryResult, + Map experienceRolesForServer, Map levels) { + Long userId = leaderBoardEntryResult.getUserId(); + Member member = guild.getMember(UserSnowflake.fromId(userId)); + UserDisplay userDisplay = null; + RoleDisplay roleDisplay = null; + Long experienceNeededToNextLevel = experienceLevelService.calculateExperienceToNextLevel(leaderBoardEntryResult.getLevel(), + leaderBoardEntryResult.getExperience()); + AExperienceLevel currentExperienceLevel = levels.get(leaderBoardEntryResult.getLevel()); + Long nextLevelExperience = experienceLevelService.calculateNextLevel(leaderBoardEntryResult.getLevel()).getExperienceNeeded(); + if(experienceRolesForServer.containsKey(leaderBoardEntryResult.getRoleId())) { + AExperienceRole experienceRole = experienceRolesForServer.get(leaderBoardEntryResult.getRoleId()); + Role role = guild.getRoleById(experienceRole.getRole().getId()); + if(role != null) { + roleDisplay = RoleDisplay.fromRole(role); + } else { + roleDisplay = RoleDisplay.fromARole(experienceRole.getRole()); + } + } + if(member != null) { + userDisplay = UserDisplay.fromMember(member); + } + Long currentExpNeeded = currentExperienceLevel.getExperienceNeeded(); + Long experienceWithinLevel = leaderBoardEntryResult.getExperience() - currentExpNeeded; + Long experienceNeededForCurrentLevel = nextLevelExperience - currentExpNeeded; + return UserExperienceDisplay + .builder() + .id(String.valueOf(userId)) + .messages(leaderBoardEntryResult.getMessageCount()) + .level(leaderBoardEntryResult.getLevel()) + .rank(leaderBoardEntryResult.getRank()) + .experience(leaderBoardEntryResult.getExperience()) + .experienceToNextLevel(experienceNeededToNextLevel) + .currentLevelExperienceNeeded(experienceNeededForCurrentLevel) + .experienceOnCurrentLevel(experienceWithinLevel) + .percentage(((float) experienceWithinLevel / experienceNeededForCurrentLevel) * 100) + .nextLevelExperienceNeeded(nextLevelExperience) + .role(roleDisplay) + .member(userDisplay) + .build(); + } + } diff --git a/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-impl/src/main/java/dev/sheldan/abstracto/experience/command/LeaderBoardCommand.java b/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-impl/src/main/java/dev/sheldan/abstracto/experience/command/LeaderBoardCommand.java index 1b02a8cbf..218fac13b 100644 --- a/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-impl/src/main/java/dev/sheldan/abstracto/experience/command/LeaderBoardCommand.java +++ b/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-impl/src/main/java/dev/sheldan/abstracto/experience/command/LeaderBoardCommand.java @@ -1,19 +1,24 @@ package dev.sheldan.abstracto.experience.command; import dev.sheldan.abstracto.core.command.condition.AbstractConditionableCommand; -import dev.sheldan.abstracto.core.command.config.*; +import dev.sheldan.abstracto.core.command.config.CommandConfiguration; +import dev.sheldan.abstracto.core.command.config.HelpInfo; +import dev.sheldan.abstracto.core.command.config.Parameter; +import dev.sheldan.abstracto.core.command.config.ParameterValidator; import dev.sheldan.abstracto.core.command.config.validator.MinIntegerValueValidator; import dev.sheldan.abstracto.core.command.execution.CommandContext; import dev.sheldan.abstracto.core.command.execution.CommandResult; -import dev.sheldan.abstracto.core.interaction.slash.SlashCommandConfig; -import dev.sheldan.abstracto.core.interaction.slash.parameter.SlashCommandParameterService; import dev.sheldan.abstracto.core.config.FeatureDefinition; import dev.sheldan.abstracto.core.interaction.InteractionService; +import dev.sheldan.abstracto.core.interaction.slash.SlashCommandConfig; +import dev.sheldan.abstracto.core.interaction.slash.parameter.SlashCommandParameterService; import dev.sheldan.abstracto.core.models.database.AServer; import dev.sheldan.abstracto.core.models.database.AUserInAServer; import dev.sheldan.abstracto.core.service.ChannelService; import dev.sheldan.abstracto.core.service.management.ServerManagementService; import dev.sheldan.abstracto.core.service.management.UserInServerManagementService; +import dev.sheldan.abstracto.core.templating.model.MessageToSend; +import dev.sheldan.abstracto.core.templating.service.TemplateService; import dev.sheldan.abstracto.core.utils.FutureUtils; import dev.sheldan.abstracto.experience.config.ExperienceFeatureDefinition; import dev.sheldan.abstracto.experience.config.ExperienceSlashCommandNames; @@ -23,8 +28,10 @@ import dev.sheldan.abstracto.experience.model.LeaderBoardEntry; import dev.sheldan.abstracto.experience.model.template.LeaderBoardEntryModel; import dev.sheldan.abstracto.experience.model.template.LeaderBoardModel; import dev.sheldan.abstracto.experience.service.AUserExperienceService; -import dev.sheldan.abstracto.core.templating.model.MessageToSend; -import dev.sheldan.abstracto.core.templating.service.TemplateService; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; import lombok.extern.slf4j.Slf4j; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; @@ -33,11 +40,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.CompletableFuture; - /** * Shows the experience gain information of the top 10 users in the server, or if a page number is provided as a parameter, only the members which are on this page. */ @@ -48,6 +50,7 @@ public class LeaderBoardCommand extends AbstractConditionableCommand { public static final String LEADER_BOARD_POST_EMBED_TEMPLATE = "leaderboard_post"; private static final String LEDERBOARD_COMMAND_NAME = "leaderboard"; private static final String PAGE_PARAMETER = "page"; + private static final String FOCUS_PARAMETER = "focus"; @Autowired private AUserExperienceService userExperienceService; @@ -80,21 +83,26 @@ public class LeaderBoardCommand extends AbstractConditionableCommand { List parameters = commandContext.getParameters().getParameters(); // parameter is optional, in case its not present, we default to the 0th page Integer page = !parameters.isEmpty() ? (Integer) parameters.get(0) : 1; - return getMessageToSend(commandContext.getAuthor(), page) + return getMessageToSend(commandContext.getAuthor(), page, false) .thenCompose(messageToSend -> FutureUtils.toSingleFutureGeneric(channelService.sendMessageToSendToChannel(messageToSend, commandContext.getChannel()))) .thenApply(aVoid -> CommandResult.fromIgnored()); } - private CompletableFuture getMessageToSend(Member actorUser, Integer page) { + private CompletableFuture getMessageToSend(Member actorUser, Integer page, boolean focusMe) { AServer server = serverManagementService.loadServer(actorUser.getGuild()); - LeaderBoard leaderBoard = userExperienceService.findLeaderBoardData(server, page); + LeaderBoard leaderBoard; + AUserInAServer aUserInAServer = userInServerManagementService.loadOrCreateUser(actorUser); + if (focusMe) { + leaderBoard = userExperienceService.findLeaderBoardDataForUserFocus(aUserInAServer); + } else { + leaderBoard = userExperienceService.findLeaderBoardData(server, page); + } List futures = new ArrayList<>(); - CompletableFuture> completableFutures = converter.fromLeaderBoard(leaderBoard); + CompletableFuture> completableFutures = converter.fromLeaderBoard(leaderBoard, actorUser.getGuild().getIdLong()); futures.add(completableFutures); log.info("Rendering leaderboard for page {} in server {} for user {}.", page, actorUser.getId(), actorUser.getGuild().getId()); - AUserInAServer aUserInAServer = userInServerManagementService.loadOrCreateUser(actorUser); LeaderBoardEntry userRank = userExperienceService.getRankOfUserInServer(aUserInAServer); - CompletableFuture> userRankFuture = converter.fromLeaderBoardEntry(Arrays.asList(userRank)); + CompletableFuture> userRankFuture = converter.fromLeaderBoardEntry(Arrays.asList(userRank), actorUser.getGuild().getIdLong()); futures.add(userRankFuture); String leaderboardUrl; if(!StringUtils.isBlank(leaderboardExternalURL)) { @@ -108,6 +116,7 @@ public class LeaderBoardCommand extends AbstractConditionableCommand { .builder() .userExperiences(finalModels) .leaderboardUrl(leaderboardUrl) + .showPlacement(!focusMe) .userExecuting(userRankFuture.join().get(0)) .build(); return CompletableFuture.completedFuture(templateService.renderEmbedTemplate(LEADER_BOARD_POST_EMBED_TEMPLATE, leaderBoardModel, actorUser.getGuild().getIdLong())); @@ -118,12 +127,18 @@ public class LeaderBoardCommand extends AbstractConditionableCommand { @Override public CompletableFuture executeSlash(SlashCommandInteractionEvent event) { Integer page; + boolean focusMe; + if (slashCommandParameterService.hasCommandOption(FOCUS_PARAMETER, event)) { + focusMe = slashCommandParameterService.getCommandOption(FOCUS_PARAMETER, event, Boolean.class); + } else { + focusMe = false; + } if(slashCommandParameterService.hasCommandOption(PAGE_PARAMETER, event)) { page = slashCommandParameterService.getCommandOption(PAGE_PARAMETER, event, Integer.class); } else { page = 1; } - return getMessageToSend(event.getMember(), page) + return getMessageToSend(event.getMember(), page, focusMe) .thenCompose(messageToSend -> interactionService.replyMessageToSend(messageToSend, event)) .thenApply(aVoid -> CommandResult.fromIgnored()); } @@ -139,7 +154,17 @@ public class LeaderBoardCommand extends AbstractConditionableCommand { .templated(true) .type(Integer.class) .build(); - List parameters = Arrays.asList(pageParameter); + + Parameter focusMe = Parameter + .builder() + .name(FOCUS_PARAMETER) + .validators(leaderBoardPageValidators) + .optional(true) + .slashCommandOnly(true) + .templated(true) + .type(Boolean.class) + .build(); + List parameters = Arrays.asList(pageParameter, focusMe); HelpInfo helpInfo = HelpInfo .builder() .templated(true) diff --git a/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-impl/src/main/java/dev/sheldan/abstracto/experience/command/Rank.java b/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-impl/src/main/java/dev/sheldan/abstracto/experience/command/Rank.java index d3d696f13..535fb1ab1 100644 --- a/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-impl/src/main/java/dev/sheldan/abstracto/experience/command/Rank.java +++ b/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-impl/src/main/java/dev/sheldan/abstracto/experience/command/Rank.java @@ -30,7 +30,9 @@ import dev.sheldan.abstracto.core.templating.service.TemplateService; import lombok.extern.slf4j.Slf4j; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @@ -78,6 +80,9 @@ public class Rank extends AbstractConditionableCommand { @Autowired private InteractionService interactionService; + @Value("${abstracto.experience.leaderboard.externalUrl}") + private String leaderboardExternalURL; + @Override public CompletableFuture executeAsync(CommandContext commandContext) { List parameters = commandContext.getParameters().getParameters(); @@ -87,7 +92,7 @@ public class Rank extends AbstractConditionableCommand { } AUserInAServer aUserInAServer = userInServerManagementService.loadOrCreateUser(targetMember); LeaderBoardEntry userRank = userExperienceService.getRankOfUserInServer(aUserInAServer); - CompletableFuture> future = converter.fromLeaderBoardEntry(Arrays.asList(userRank)); + CompletableFuture> future = converter.fromLeaderBoardEntry(Arrays.asList(userRank), commandContext.getGuild().getIdLong()); RankModel rankModel = RankModel .builder() .member(targetMember) @@ -115,6 +120,13 @@ public class Rank extends AbstractConditionableCommand { rankModel.setExperienceToNextLevel(experienceNeededToNextLevel); rankModel.setInLevelExperience(experienceWithinLevel); rankModel.setNextLevelExperience(nextLevelExperience); + String leaderboardUrl; + if(!StringUtils.isBlank(leaderboardExternalURL)) { + leaderboardUrl = String.format("%s/experience/leaderboards/%s/%s", leaderboardExternalURL, toRender.getGuild().getIdLong(), toRender.getId()); + } else { + leaderboardUrl = null; + } + rankModel.setLeaderboardUrl(leaderboardUrl); return templateService.renderEmbedTemplate(RANK_POST_EMBED_TEMPLATE, rankModel, toRender.getGuild().getIdLong()); } @@ -128,7 +140,7 @@ public class Rank extends AbstractConditionableCommand { } AUserInAServer aUserInAServer = userInServerManagementService.loadOrCreateUser(targetMember); LeaderBoardEntry userRank = userExperienceService.getRankOfUserInServer(aUserInAServer); - CompletableFuture> future = converter.fromLeaderBoardEntry(Arrays.asList(userRank)); + CompletableFuture> future = converter.fromLeaderBoardEntry(Arrays.asList(userRank), event.getGuild().getIdLong()); RankModel rankModel = RankModel .builder() .member(targetMember) diff --git a/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-impl/src/main/java/dev/sheldan/abstracto/experience/converter/LeaderBoardModelConverter.java b/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-impl/src/main/java/dev/sheldan/abstracto/experience/converter/LeaderBoardModelConverter.java index 077c1dae8..354e7ca0a 100644 --- a/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-impl/src/main/java/dev/sheldan/abstracto/experience/converter/LeaderBoardModelConverter.java +++ b/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-impl/src/main/java/dev/sheldan/abstracto/experience/converter/LeaderBoardModelConverter.java @@ -3,7 +3,6 @@ package dev.sheldan.abstracto.experience.converter; import dev.sheldan.abstracto.core.service.MemberService; import dev.sheldan.abstracto.experience.model.LeaderBoard; import dev.sheldan.abstracto.experience.model.LeaderBoardEntry; -import dev.sheldan.abstracto.experience.model.database.AUserExperience; import dev.sheldan.abstracto.experience.model.template.LeaderBoardEntryModel; import dev.sheldan.abstracto.experience.service.management.UserExperienceManagementService; import lombok.extern.slf4j.Slf4j; @@ -42,26 +41,23 @@ public class LeaderBoardModelConverter { * @return The list of {@link LeaderBoardEntryModel leaderboarEntryModels} which contain the fully fledged information provided to the * leader board template */ - public CompletableFuture> fromLeaderBoard(LeaderBoard leaderBoard) { + public CompletableFuture> fromLeaderBoard(LeaderBoard leaderBoard, Long serverId) { log.debug("Converting {} entries to a list of leaderboard entries.", leaderBoard.getEntries().size()); - return fromLeaderBoardEntry(leaderBoard.getEntries()); + return fromLeaderBoardEntry(leaderBoard.getEntries(), serverId); } - public CompletableFuture> fromLeaderBoardEntry(List leaderBoardEntries) { + public CompletableFuture> fromLeaderBoardEntry(List leaderBoardEntries, Long serverId) { List userIds = new ArrayList<>(); - Long serverId = leaderBoardEntries.get(0).getExperience().getServer().getId(); Map models = leaderBoardEntries .stream() .map(leaderBoardEntry -> { - AUserExperience experience = leaderBoardEntry.getExperience(); - Long userId = experience.getUser().getUserReference().getId(); - userIds.add(userId); + userIds.add(leaderBoardEntry.getUserId()); return LeaderBoardEntryModel .builder() - .userId(userId) - .experience(experience.getExperience()) - .messageCount(experience.getMessageCount()) - .level(experience.getLevelOrDefault()) + .userId(leaderBoardEntry.getUserId()) + .experience(leaderBoardEntry.getExperience()) + .messageCount(leaderBoardEntry.getMessageCount()) + .level(leaderBoardEntry.getLevel()) .rank(leaderBoardEntry.getRank()) .build(); }) diff --git a/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-impl/src/main/java/dev/sheldan/abstracto/experience/repository/UserExperienceRepository.java b/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-impl/src/main/java/dev/sheldan/abstracto/experience/repository/UserExperienceRepository.java index 57d6ed13b..14532fada 100644 --- a/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-impl/src/main/java/dev/sheldan/abstracto/experience/repository/UserExperienceRepository.java +++ b/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-impl/src/main/java/dev/sheldan/abstracto/experience/repository/UserExperienceRepository.java @@ -55,6 +55,33 @@ public interface UserExperienceRepository extends JpaRepository t.rank " + + " ORDER BY r.rank " + + " LIMIT :after) " + + "ORDER BY rank", nativeQuery = true) + List getRankOfUserWithWindow(@Param("userInServerId") Long id, @Param("serverId") Long serverId, @Param("before") Long beforeInclusive, @Param("after") Long after); + @Modifying(clearAutomatically = true) @Query("update AUserExperience u set u.currentExperienceRole = null where u.currentExperienceRole.id = :roleId") void removeExperienceRoleFromUsers(@Param("roleId") Long experienceRoleId); diff --git a/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-impl/src/main/java/dev/sheldan/abstracto/experience/service/AUserExperienceServiceBean.java b/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-impl/src/main/java/dev/sheldan/abstracto/experience/service/AUserExperienceServiceBean.java index d7497e152..5d83be779 100644 --- a/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-impl/src/main/java/dev/sheldan/abstracto/experience/service/AUserExperienceServiceBean.java +++ b/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-impl/src/main/java/dev/sheldan/abstracto/experience/service/AUserExperienceServiceBean.java @@ -491,11 +491,32 @@ public class AUserExperienceServiceBean implements AUserExperienceService { int pageOffset = page * pageSize; for (int i = 0; i < experiences.size(); i++) { AUserExperience userExperience = experiences.get(i); - entries.add(LeaderBoardEntry.builder().experience(userExperience).rank(pageOffset + i + 1).build()); + LeaderBoardEntry entry = LeaderBoardEntry.fromAUserExperience(userExperience); + entry.setRank(pageOffset + i + 1); + entries.add(entry); } return LeaderBoard.builder().entries(entries).build(); } + @Override + public LeaderBoard findLeaderBoardDataForUserFocus(AUserInAServer aUserInAServer) { + List allEntries = + userExperienceManagementService.getWindowedLeaderboardEntriesForUser(aUserInAServer, 10) + .stream().map(leaderBoardEntryResult -> LeaderBoardEntry + .builder() + .experience(leaderBoardEntryResult.getExperience()) + .level(leaderBoardEntryResult.getLevel()) + .userId(leaderBoardEntryResult.getUserId()) + .messageCount(leaderBoardEntryResult.getMessageCount()) + .rank(leaderBoardEntryResult.getRank()) + .build()) + .collect(Collectors.toList()); + return LeaderBoard + .builder() + .entries(allEntries) + .build(); + } + @Override public LeaderBoardEntry getRankOfUserInServer(AUserInAServer userInAServer) { log.debug("Retrieving rank for {}", userInAServer.getUserReference().getId()); @@ -509,7 +530,9 @@ public class AUserExperienceServiceBean implements AUserExperienceService { if(rankOfUserInServer != null) { rank = rankOfUserInServer.getRank(); } - return LeaderBoardEntry.builder().experience(aUserExperience).rank(rank).build(); + LeaderBoardEntry entry = LeaderBoardEntry.fromAUserExperience(aUserExperience); + entry.setRank(rank); + return entry; } } diff --git a/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-impl/src/main/java/dev/sheldan/abstracto/experience/service/management/UserExperienceManagementServiceBean.java b/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-impl/src/main/java/dev/sheldan/abstracto/experience/service/management/UserExperienceManagementServiceBean.java index 2f6f81cef..375af3d2e 100644 --- a/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-impl/src/main/java/dev/sheldan/abstracto/experience/service/management/UserExperienceManagementServiceBean.java +++ b/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-impl/src/main/java/dev/sheldan/abstracto/experience/service/management/UserExperienceManagementServiceBean.java @@ -82,6 +82,12 @@ public class UserExperienceManagementServiceBean implements UserExperienceManage return repository.findTop10ByUser_ServerReferenceOrderByExperienceDesc(aServer, PageRequest.of(page, size)); } + @Override + public List getWindowedLeaderboardEntriesForUser(AUserInAServer aUserInAServer, Integer windowSize) { + return repository.getRankOfUserWithWindow(aUserInAServer.getUserInServerId(), aUserInAServer.getServerReference().getId(), windowSize.longValue() / 2 + 1, + windowSize.longValue() / 2); + } + @Override public LeaderBoardEntryResult getRankOfUserInServer(AUserExperience userExperience) { return repository.getRankOfUserInServer(userExperience.getId(), userExperience.getServer().getId()); diff --git a/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-impl/src/test/java/dev/sheldan/abstracto/experience/converter/LeaderBoardModelConverterTest.java b/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-impl/src/test/java/dev/sheldan/abstracto/experience/converter/LeaderBoardModelConverterTest.java deleted file mode 100644 index 88590df03..000000000 --- a/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-impl/src/test/java/dev/sheldan/abstracto/experience/converter/LeaderBoardModelConverterTest.java +++ /dev/null @@ -1,87 +0,0 @@ -package dev.sheldan.abstracto.experience.converter; - -import dev.sheldan.abstracto.core.models.database.AServer; -import dev.sheldan.abstracto.core.models.database.AUser; -import dev.sheldan.abstracto.core.models.database.AUserInAServer; -import dev.sheldan.abstracto.core.service.MemberService; -import dev.sheldan.abstracto.experience.model.LeaderBoard; -import dev.sheldan.abstracto.experience.model.LeaderBoardEntry; -import dev.sheldan.abstracto.experience.model.database.AUserExperience; -import dev.sheldan.abstracto.experience.model.template.LeaderBoardEntryModel; -import net.dv8tion.jda.api.entities.Member; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; - -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.CompletableFuture; - -import static org.mockito.Mockito.when; - -@RunWith(MockitoJUnitRunner.class) -public class LeaderBoardModelConverterTest { - - @InjectMocks - public LeaderBoardModelConverter testUnit; - - @Mock - private MemberService memberService; - - @Mock - private LeaderBoardModelConverter self; - - private static final Long SERVER_ID = 4L; - private static final Long USER_ID = 5L; - private static final Long USER_ID_2 = 6L; - private static final Long USER_IN_SERVER_ID = 7L; - private static final Long USER_IN_SERVER_ID_2 = 8L; - private static final Long EXPERIENCE = 9L; - private static final Long MESSAGES = 10L; - private static final Integer LEVEL = 54; - - @Test - public void testFromLeaderBoard() { - Integer firstRank = 1; - - LeaderBoardEntry entry = getEntry(firstRank, USER_ID, USER_IN_SERVER_ID); - Integer secondRank = 2; - LeaderBoardEntry entry2 = getEntry(secondRank, USER_ID_2, USER_IN_SERVER_ID_2); - List entries = Arrays.asList(entry, entry2); - LeaderBoard leaderBoard = Mockito.mock(LeaderBoard.class); - when(leaderBoard.getEntries()).thenReturn(entries); - Member member = Mockito.mock(Member.class); - when(member.getIdLong()).thenReturn(USER_ID); - when(memberService.getMembersInServerAsync(SERVER_ID, Arrays.asList(USER_ID, USER_ID_2))).thenReturn(CompletableFuture.completedFuture(Arrays.asList(member))); - CompletableFuture> leaderBoardEntryModels = testUnit.fromLeaderBoard(leaderBoard); - LeaderBoardEntryModel firstEntry = leaderBoardEntryModels.join().get(0); - Assert.assertEquals(USER_ID, firstEntry.getUserId()); - LeaderBoardEntryModel secondEntry = leaderBoardEntryModels.join().get(1); - Assert.assertEquals(USER_ID_2, secondEntry.getUserId()); - Assert.assertEquals(entries.size(), leaderBoardEntryModels.join().size()); - } - - private LeaderBoardEntry getEntry(Integer rank, Long userId, Long userInServerId) { - AUserExperience experience = Mockito.mock(AUserExperience.class); - when(experience.getMessageCount()).thenReturn(MESSAGES); - when(experience.getExperience()).thenReturn(EXPERIENCE); - AUserInAServer userInAServer = Mockito.mock(AUserInAServer.class); - when(experience.getUser()).thenReturn(userInAServer); - AUser user = Mockito.mock(AUser.class); - when(userInAServer.getUserReference()).thenReturn(user); - when(user.getId()).thenReturn(userId); - AServer server = Mockito.mock(AServer.class); - when(experience.getServer()).thenReturn(server); - when(server.getId()).thenReturn(SERVER_ID); - LeaderBoardEntry entry = Mockito.mock(LeaderBoardEntry.class); - when(entry.getRank()).thenReturn(rank); - when(entry.getExperience()).thenReturn(experience); - return entry; - } - - -} diff --git a/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-int/src/main/java/dev/sheldan/abstracto/experience/model/LeaderBoardEntry.java b/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-int/src/main/java/dev/sheldan/abstracto/experience/model/LeaderBoardEntry.java index 77be4a3d1..eca951757 100644 --- a/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-int/src/main/java/dev/sheldan/abstracto/experience/model/LeaderBoardEntry.java +++ b/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-int/src/main/java/dev/sheldan/abstracto/experience/model/LeaderBoardEntry.java @@ -12,12 +12,19 @@ import lombok.Setter; @Setter @Builder public class LeaderBoardEntry { - /** - * Object representing the current experience status of a user in a guild. - */ - private AUserExperience experience; - /** - * The rank this user has in the respective guild. - */ + private Long userId; + private Integer level; + private Long experience; + private Long messageCount; private Integer rank; + + public static LeaderBoardEntry fromAUserExperience(AUserExperience aUserExperience) { + return LeaderBoardEntry + .builder() + .experience(aUserExperience.getExperience()) + .userId(aUserExperience.getUser().getUserReference().getId()) + .messageCount(aUserExperience.getMessageCount()) + .level(aUserExperience.getLevelOrDefault()) + .build(); + } } diff --git a/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-int/src/main/java/dev/sheldan/abstracto/experience/model/database/LeaderBoardEntryResult.java b/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-int/src/main/java/dev/sheldan/abstracto/experience/model/database/LeaderBoardEntryResult.java index 113205811..03605018f 100644 --- a/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-int/src/main/java/dev/sheldan/abstracto/experience/model/database/LeaderBoardEntryResult.java +++ b/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-int/src/main/java/dev/sheldan/abstracto/experience/model/database/LeaderBoardEntryResult.java @@ -7,11 +7,8 @@ public interface LeaderBoardEntryResult { Long getId(); - /** - * The {@link dev.sheldan.abstracto.core.models.database.AUserInAServer} id of the user - * @return The ID of the user in a server - */ - Long getUserInServerId(); + Long getUserId(); + Long getRoleId(); /** * The experience of the {@link dev.sheldan.abstracto.core.models.database.AUserInAServer} diff --git a/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-int/src/main/java/dev/sheldan/abstracto/experience/model/template/LeaderBoardModel.java b/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-int/src/main/java/dev/sheldan/abstracto/experience/model/template/LeaderBoardModel.java index 157fbefb6..15967f830 100644 --- a/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-int/src/main/java/dev/sheldan/abstracto/experience/model/template/LeaderBoardModel.java +++ b/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-int/src/main/java/dev/sheldan/abstracto/experience/model/template/LeaderBoardModel.java @@ -1,6 +1,7 @@ package dev.sheldan.abstracto.experience.model.template; import dev.sheldan.abstracto.core.models.context.SlimUserInitiatedServerContext; +import lombok.Builder; import lombok.Getter; import lombok.Setter; import lombok.experimental.SuperBuilder; @@ -25,4 +26,9 @@ public class LeaderBoardModel extends SlimUserInitiatedServerContext { */ private LeaderBoardEntryModel userExecuting; private String leaderboardUrl; + /** + * Whether to show the users own placement + */ + @Builder.Default + private boolean showPlacement = true; } diff --git a/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-int/src/main/java/dev/sheldan/abstracto/experience/model/template/RankModel.java b/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-int/src/main/java/dev/sheldan/abstracto/experience/model/template/RankModel.java index 6a22a6e22..3aba1ebfc 100644 --- a/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-int/src/main/java/dev/sheldan/abstracto/experience/model/template/RankModel.java +++ b/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-int/src/main/java/dev/sheldan/abstracto/experience/model/template/RankModel.java @@ -47,4 +47,5 @@ public class RankModel { * The member to show the rank for */ private Member member; + private String leaderboardUrl; } diff --git a/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-int/src/main/java/dev/sheldan/abstracto/experience/service/AUserExperienceService.java b/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-int/src/main/java/dev/sheldan/abstracto/experience/service/AUserExperienceService.java index 6ef2a269a..2a4b7d542 100644 --- a/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-int/src/main/java/dev/sheldan/abstracto/experience/service/AUserExperienceService.java +++ b/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-int/src/main/java/dev/sheldan/abstracto/experience/service/AUserExperienceService.java @@ -54,6 +54,7 @@ public interface AUserExperienceService { * from the desired page */ LeaderBoard findLeaderBoardData(AServer server, Integer page); + LeaderBoard findLeaderBoardDataForUserFocus(AUserInAServer aUserInAServer); /** * Retrieves the {@link LeaderBoardEntry} from a specific {@link AUserInAServer} containing information about the diff --git a/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-int/src/main/java/dev/sheldan/abstracto/experience/service/management/UserExperienceManagementService.java b/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-int/src/main/java/dev/sheldan/abstracto/experience/service/management/UserExperienceManagementService.java index bba9cefba..ac3edb0c0 100644 --- a/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-int/src/main/java/dev/sheldan/abstracto/experience/service/management/UserExperienceManagementService.java +++ b/abstracto-application/abstracto-modules/experience-tracking/experience-tracking-int/src/main/java/dev/sheldan/abstracto/experience/service/management/UserExperienceManagementService.java @@ -62,6 +62,7 @@ public interface UserExperienceManagementService { * @return A list desc ordered by {@link AUserExperience} experience only containing the elements between {@code start} and @{code end} */ List findLeaderBoardUsersPaginated(AServer server, Integer page, Integer size); + List getWindowedLeaderboardEntriesForUser(AUserInAServer aUserInAServer, Integer windowSize); /** * Returns the {@link LeaderBoardEntryResult} of the given {@link AUserExperience}. diff --git a/python/components/experience-tracking/python/endpoints/leaderboard.py b/python/components/experience-tracking/python/endpoints/leaderboard.py index 47bd71fb0..b049adbfe 100644 --- a/python/components/experience-tracking/python/endpoints/leaderboard.py +++ b/python/components/experience-tracking/python/endpoints/leaderboard.py @@ -13,9 +13,16 @@ leaderboard_url = f'http://{backend_host}:{backend_port}/experience/v1/leaderboa @app.route('/experience/v1/leaderboards/') def get_leaderboard(serverId): page = int(request.args.get('page', 0, type=int)) - size = int(request.args.get('size', 25, type=int)) + size = int(request.args.get('size', 50, type=int)) leaderboard = requests.get(f'{leaderboard_url}/{serverId}?page={page}&size={size}') - logging.info(f'returning leaderboard for server') + logging.info(f'returning leaderboard for server {serverId}') + return leaderboard.text, leaderboard.status_code + +@app.route('/experience/v1/leaderboards//') +def get_leaderboard_for_user(serverId, userId): + windowSize = int(request.args.get('windowSize', 50, type=int)) + leaderboard = requests.get(f'{leaderboard_url}/{serverId}/{userId}?windowSize={windowSize}') + logging.info(f'returning leaderboard for server {serverId} for user {userId}') return leaderboard.text, leaderboard.status_code @app.route('/experience/v1/leaderboards//config') @@ -27,4 +34,8 @@ def get_experience_config(serverId): @app.route('/experience/leaderboards/') def render_index(serverId): - return render_template('experience/leaderboards/index.html', serverId=serverId) \ No newline at end of file + return render_template('experience/leaderboards/index.html', serverId=serverId) + +@app.route('/experience/leaderboards//') +def render_index_for_user(serverId, userId): + return render_template('experience/leaderboards/index.html', serverId=serverId, userId=userId) \ No newline at end of file diff --git a/ui/experience-tracking/public/index.html b/ui/experience-tracking/public/index.html index d15927549..a2eb11738 100644 --- a/ui/experience-tracking/public/index.html +++ b/ui/experience-tracking/public/index.html @@ -8,7 +8,7 @@ name="description" content="Leaderboard for experience" /> - + Experience leaderboard diff --git a/ui/experience-tracking/src/App.tsx b/ui/experience-tracking/src/App.tsx index 914df43b5..7abf95126 100644 --- a/ui/experience-tracking/src/App.tsx +++ b/ui/experience-tracking/src/App.tsx @@ -5,10 +5,12 @@ import {Leaderboard} from "./components/Leaderboard"; function App() { // @ts-ignore const serverId: bigint = window.serverId + // @ts-ignore + const userId: bigint = window.userId return ( <>
- +
) diff --git a/ui/experience-tracking/src/components/Leaderboard.tsx b/ui/experience-tracking/src/components/Leaderboard.tsx index 45b27c792..ca80f40e9 100644 --- a/ui/experience-tracking/src/components/Leaderboard.tsx +++ b/ui/experience-tracking/src/components/Leaderboard.tsx @@ -4,26 +4,87 @@ import {ExperienceMember, GuildInfo} from "../data/leaderboard"; import {ExperienceConfigDisplay} from "./ExperienceConfigDisplay"; import {ErrorDisplay} from "./ErrorDisplay"; -export function Leaderboard({serverId}: { serverId: bigint }) { +export function Leaderboard({serverId, userId}: { serverId: bigint, userId: bigint }) { - const pageSize = 25; + const pageSize = 50; + const windowSize = 10; const [members, setMembers] = useState([]) const [memberCount, setMemberCount] = useState(0) - const [pageCount, setPageCount] = useState(0) - const [hasMore, setHasMore] = useState(true) + const [pageCountEnd, setPageCountEnd] = useState(0) + const [pageCountStart, setPageCountStart] = useState(0) + const [pageOffsetEnd, setPageOffsetEnd] = useState(0) + const [pageOffsetStart, setPageOffsetStart] = useState(0) + const [hasMoreAfterwards, setHasMoreAfterwards] = useState(true) + const [hasMoreBefore, setHasMoreBefore] = useState(true) + const [userSpecific, setUserSpecific] = useState(false) const [hasError, setError] = useState(false) const [guildInfo, setGuildInfo] = useState({} as GuildInfo) - async function loadLeaderboard(page: number, size: number) { + async function loadLeaderboardForGuild(page: number, size: number, takeStart: number, skipStart: number, addStart: boolean) { try { const leaderboardResponse = await fetch(`/experience/v1/leaderboards/${serverId}?page=${page}&size=${size}`) const leaderboardJson = await leaderboardResponse.json() - const loadedMembers: Array = leaderboardJson.content; + let loadedMembers: ExperienceMember[] = leaderboardJson.content; + if(takeStart !== 0) { + loadedMembers = loadedMembers.slice(0, takeStart) + } + if(skipStart !== 0) { + loadedMembers = loadedMembers.slice(skipStart, loadedMembers.length) + } setMemberCount(memberCount + loadedMembers.length) - setHasMore(!leaderboardJson.last) - setPageCount(page) + if(hasMoreBefore) { + setHasMoreBefore(!leaderboardJson.first) + } + if(hasMoreAfterwards) { + setHasMoreAfterwards(!leaderboardJson.last) + } + if(addStart) { + members.unshift(... loadedMembers) + setMembers(members) + } else { + setMembers(members.concat(loadedMembers)) + } + } catch (error) { + console.log(error) + setError(true) + } + } + + + async function loadLeaderboardForUser(userId: bigint, windowSize: number) { + try { + const leaderboardResponse = await fetch(`/experience/v1/leaderboards/${serverId}/${userId}?windowSize=${windowSize}`) + const loadedMembers: Array = await leaderboardResponse.json(); + setMemberCount(memberCount + loadedMembers.length) + if(windowSize === loadedMembers.length) { // simple case, we got back the full package + setHasMoreBefore(true) + setHasMoreAfterwards(true) + } else { + const indexOfUser = loadedMembers.findIndex(value => value.id === userId.toString()) + if(indexOfUser < (windowSize / 2)) { // the user is in the upper half + setHasMoreBefore(false) + } else { + setHasMoreBefore(true) + } + if((windowSize - indexOfUser) < (windowSize / 2)) { // not the full window was reached + setHasMoreAfterwards(false) + } else { + setHasMoreAfterwards(true) + } + } setMembers(members.concat(loadedMembers)) + const lastRank = loadedMembers[loadedMembers.length -1].rank; + let lastPage = Math.floor(lastRank / pageSize) + const pageOffsetEnd = lastRank % pageSize + setPageOffsetEnd(pageOffsetEnd) // this is how far we got in the last page, take everything starting here + setPageCountEnd(lastPage) // this is the page the last entry is on, the next page we need to load + + const firstRank = loadedMembers[0].rank; + const firstPage = Math.floor(firstRank / pageSize) + const pageOffsetStart = firstRank % pageSize - firstPage * pageSize - 1 + setPageOffsetStart(pageOffsetStart) // this is how many we want to use, starting from the top + setPageCountStart(firstPage) // this the page we want to load } catch (error) { console.log(error) setError(true) @@ -42,16 +103,36 @@ export function Leaderboard({serverId}: { serverId: bigint }) { useEffect(()=> { if(memberCount === 0) { - loadLeaderboard(0, pageSize) + if(userId === 0n) { + loadLeaderboardForGuild(0, pageSize, pageSize, 0, false) + } else { + setUserSpecific(true) + loadLeaderboardForUser(userId, windowSize) + } } loadGuildInfo() // eslint-disable-next-line react-hooks/exhaustive-deps },[]) - function loadMore() { - loadLeaderboard(pageCount + 1, pageSize) + async function loadMore() { + await loadLeaderboardForGuild(pageCountEnd + 1, pageSize, pageSize, 0, false) + setPageCountEnd(pageCountEnd + 1) + } + + async function loadBefore() { + await loadLeaderboardForGuild(pageCountStart, pageSize, pageOffsetStart != 0 ? pageOffsetStart : 0, 0, true) + setPageOffsetStart(0) + setPageCountStart(pageCountStart - 1) + } + + async function loadAfter() { + await loadLeaderboardForGuild(pageCountEnd, pageSize, pageOffsetEnd != 0 ? 0 : pageSize, pageOffsetEnd, false) + setPageOffsetEnd(0) + setPageCountEnd(pageCountEnd + 1) } let loadMoreButton = ; + let loadBeforeButton = ; + let loadAfterButton = ; return ( <> {!hasError ? @@ -76,6 +157,7 @@ export function Leaderboard({serverId}: { serverId: bigint }) {
+ {hasMoreBefore && userSpecific ? loadBeforeButton : ''} @@ -101,7 +183,10 @@ export function Leaderboard({serverId}: { serverId: bigint }) { {members.map((member, index) => )}
- {hasMore ? loadMoreButton : ''} +
+ {hasMoreAfterwards && !userSpecific ? loadMoreButton : ''} + {hasMoreAfterwards && userSpecific ? loadAfterButton : ''} +
diff --git a/ui/experience-tracking/tsconfig.json b/ui/experience-tracking/tsconfig.json index a273b0cfc..d51be2111 100644 --- a/ui/experience-tracking/tsconfig.json +++ b/ui/experience-tracking/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es5", + "target": "es2021", "lib": [ "dom", "dom.iterable",