Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
*.class
target/
.DS_Store
*.tmp
19 changes: 18 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>dansplugins</groupId>
<artifactId>Mailboxes</artifactId>
<version>1.2.0</version>
<version>1.3.0-SNAPSHOT</version>
<packaging>jar</packaging>

<name>Mailbox</name>
Expand All @@ -27,6 +27,11 @@
<target>${java.version}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
Expand Down Expand Up @@ -70,5 +75,17 @@
<version>1.13-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.12.4</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public class HelpCommand {
public boolean execute(CommandSender sender) {
sender.sendMessage(ChatColor.AQUA + "=== Mailboxes Commands ===");
sender.sendMessage(ChatColor.AQUA + "/m help - View a list of helpful commands.");
sender.sendMessage(ChatColor.AQUA + "/m list - List your messages.");
sender.sendMessage(ChatColor.AQUA + "/m list [type] [page] - List your messages with pagination.");
sender.sendMessage(ChatColor.AQUA + "/m open - Open a message.");
sender.sendMessage(ChatColor.AQUA + "/m send (player) \"message\" [-attach] - Send a message to another player.");
sender.sendMessage(ChatColor.AQUA + " Add -attach flag to attach the item in your hand.");
Expand Down
37 changes: 29 additions & 8 deletions src/main/java/dansplugins/mailboxes/commands/ListCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import java.util.stream.Collectors;

public class ListCommand {
private static final int DEFAULT_PAGE_SIZE = 10;
private final Logger logger;
private final PersistentData persistentData;

Expand All @@ -36,25 +37,45 @@ public boolean execute(CommandSender sender, String[] args) {
return false;
}

int page = 1;
int pageSize = DEFAULT_PAGE_SIZE;
String listType = "active";

if (args.length > 0) {
String list = args[0];
if (list.equalsIgnoreCase("active")) {
mailbox.sendListOfActiveMessagesToPlayer(player);
listType = args[0].toLowerCase();

// Parse page number if provided
if (args.length > 1) {
try {
page = Integer.parseInt(args[1]);
if (page < 1) {
player.sendMessage(ChatColor.RED + "Page number must be 1 or greater.");
return false;
}
} catch (NumberFormatException e) {
player.sendMessage(ChatColor.RED + "Invalid page number: " + args[1]);
return false;
}
}

if (listType.equals("active")) {
mailbox.sendListOfActiveMessagesToPlayer(player, page, pageSize);
}
else if (list.equalsIgnoreCase("archived")) {
mailbox.sendListOfArchivedMessagesToPlayer(player);
else if (listType.equals("archived")) {
mailbox.sendListOfArchivedMessagesToPlayer(player, page, pageSize);
}
else if (list.equalsIgnoreCase("unread")) {
mailbox.sendListOfUnreadMessagesToPlayer(player);
else if (listType.equals("unread")) {
mailbox.sendListOfUnreadMessagesToPlayer(player, page, pageSize);
}
else {
player.sendMessage(ChatColor.RED + "Sub-commands: active, archived, unread");
player.sendMessage(ChatColor.AQUA + "Usage: /m list [type] [page]");
return false;
}
return true;
}

mailbox.sendListOfActiveMessagesToPlayer(player);
mailbox.sendListOfActiveMessagesToPlayer(player, page, pageSize);
return true;
}

Expand Down
119 changes: 79 additions & 40 deletions src/main/java/dansplugins/mailboxes/objects/Mailbox.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import dansplugins.mailboxes.utils.Logger;
import net.md_5.bungee.api.chat.ClickEvent;
import net.md_5.bungee.api.chat.ComponentBuilder;
import net.md_5.bungee.api.chat.HoverEvent;
import net.md_5.bungee.api.chat.TextComponent;
import org.bukkit.ChatColor;
import org.bukkit.entity.Player;

Expand All @@ -13,6 +17,7 @@
import java.util.UUID;

public class Mailbox implements Savable {
private static final int DEFAULT_PAGE_SIZE = 10;
private final Logger logger;

private int ID;
Expand Down Expand Up @@ -97,23 +102,11 @@ public void removeActiveMessage(int ID) {
}

public void sendListOfActiveMessagesToPlayer(Player player) {
if (activeMessages.size() == 0) {
player.sendMessage(ChatColor.AQUA + "You don't have any active messages at this time.");
return;
}
player.sendMessage(ChatColor.AQUA + "=== Active Messages ===");
player.sendMessage(ChatColor.AQUA + "D: date, S: sender, 📎: has attachments");
for (Message message : activeMessages) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
String toSend = "* ID: " + message.getID() + " - D: " + dateFormat.format(message.getDate()) + " - S: " + message.getSender();
if (message.hasAttachments()) {
toSend += " 📎";
}
if (message.isUnread()) {
toSend = ChatColor.BOLD + toSend;
}
player.sendMessage(ChatColor.AQUA + toSend);
}
sendListOfActiveMessagesToPlayer(player, 1, DEFAULT_PAGE_SIZE);
}

public void sendListOfActiveMessagesToPlayer(Player player, int page, int pageSize) {
sendPaginatedMessageList(player, activeMessages, page, pageSize, "Active Messages", "active");
}

public ArrayList<Message> getArchivedMessages() {
Expand Down Expand Up @@ -145,23 +138,11 @@ public void removeArchivedMessage(int ID) {
}

public void sendListOfArchivedMessagesToPlayer(Player player) {
if (archivedMessages.size() == 0) {
player.sendMessage(ChatColor.AQUA + "You don't have any archived messages at this time.");
return;
}
player.sendMessage(ChatColor.AQUA + "=== Archived Messages ===");
player.sendMessage(ChatColor.AQUA + "D: date, S: sender, 📎: has attachments");
for (Message message : archivedMessages) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
String toSend = "* ID: " + message.getID() + " - D: " + dateFormat.format(message.getDate()) + " - S: " + message.getSender();
if (message.hasAttachments()) {
toSend += " 📎";
}
if (message.isUnread()) {
toSend = ChatColor.BOLD + toSend;
}
player.sendMessage(ChatColor.AQUA + toSend);
}
sendListOfArchivedMessagesToPlayer(player, 1, DEFAULT_PAGE_SIZE);
}

public void sendListOfArchivedMessagesToPlayer(Player player, int page, int pageSize) {
sendPaginatedMessageList(player, archivedMessages, page, pageSize, "Archived Messages", "archived");
}

public void archiveMessage(Message message) {
Expand Down Expand Up @@ -201,22 +182,80 @@ public ArrayList<Message> getUnreadMessages() {
}

public void sendListOfUnreadMessagesToPlayer(Player player) {
sendListOfUnreadMessagesToPlayer(player, 1, DEFAULT_PAGE_SIZE);
}

public void sendListOfUnreadMessagesToPlayer(Player player, int page, int pageSize) {
ArrayList<Message> unreadMessages = getUnreadMessages();
if (unreadMessages.size() == 0) {
player.sendMessage(ChatColor.AQUA + "You don't have any unread messages at this time.");
sendPaginatedMessageList(player, unreadMessages, page, pageSize, "Unread Messages", "unread");
}

private void sendPaginatedMessageList(Player player, ArrayList<Message> messages, int page, int pageSize, String listTitle, String listType) {
if (messages.size() == 0) {
if (page > 1) {
player.sendMessage(ChatColor.RED + "Invalid page number. You don't have any " + listType + " messages.");
} else {
player.sendMessage(ChatColor.AQUA + "You don't have any " + listType + " messages at this time.");
}
return;
}

int totalPages = (int) Math.ceil((double) messages.size() / pageSize);
if (page < 1 || page > totalPages) {
player.sendMessage(ChatColor.RED + "Invalid page number. Valid pages: 1-" + totalPages);
return;
}
player.sendMessage(ChatColor.AQUA + "=== Unread Messages ===");

player.sendMessage(ChatColor.AQUA + "=== " + listTitle + " (Page " + page + "/" + totalPages + ") ===");
player.sendMessage(ChatColor.AQUA + "D: date, S: sender, 📎: has attachments");
for (Message message : getUnreadMessages()) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");

int startIndex = (page - 1) * pageSize;
int endIndex = Math.min(startIndex + pageSize, messages.size());

SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
for (int i = startIndex; i < endIndex; i++) {
Message message = messages.get(i);
String toSend = "* ID: " + message.getID() + " - D: " + dateFormat.format(message.getDate()) + " - S: " + message.getSender();
if (message.hasAttachments()) {
toSend += " 📎";
}
toSend = ChatColor.BOLD + toSend;
if (message.isUnread()) {
toSend = ChatColor.BOLD + toSend;
}
player.sendMessage(ChatColor.AQUA + toSend);
}

// Show navigation info
if (totalPages > 1) {
ComponentBuilder navigationBuilder = new ComponentBuilder("Page " + page + " of " + totalPages)
.color(net.md_5.bungee.api.ChatColor.GRAY);

if (page > 1) {
navigationBuilder.append(" | ", ComponentBuilder.FormatRetention.NONE)
.color(net.md_5.bungee.api.ChatColor.GRAY);
TextComponent previousLink = new TextComponent("[Previous Page]");
previousLink.setColor(net.md_5.bungee.api.ChatColor.AQUA);
previousLink.setBold(true);
previousLink.setClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/m list " + listType + " " + (page - 1)));
previousLink.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT,
new ComponentBuilder("Click to go to page " + (page - 1)).create()));
navigationBuilder.append(previousLink, ComponentBuilder.FormatRetention.NONE);
}

if (page < totalPages) {
navigationBuilder.append(" | ", ComponentBuilder.FormatRetention.NONE)
.color(net.md_5.bungee.api.ChatColor.GRAY);
TextComponent nextLink = new TextComponent("[Next Page]");
nextLink.setColor(net.md_5.bungee.api.ChatColor.AQUA);
nextLink.setBold(true);
nextLink.setClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/m list " + listType + " " + (page + 1)));
nextLink.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT,
new ComponentBuilder("Click to go to page " + (page + 1)).create()));
navigationBuilder.append(nextLink, ComponentBuilder.FormatRetention.NONE);
}

player.spigot().sendMessage(navigationBuilder.create());
}
}

public Map<String, String> save() {
Expand Down
69 changes: 69 additions & 0 deletions src/test/java/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Unit Tests for Pagination Feature

This directory contains unit tests for the pagination system implemented for the `/m list` command.

## Test Coverage

### ListCommandTest
Tests the command parsing and execution logic for the ListCommand class:

- **testExecuteWithNonPlayerSender**: Verifies that non-player senders cannot use the command
- **testExecuteWithNoMailbox**: Verifies proper error handling when player has no mailbox
- **testExecuteWithNoArgsDefaultsToActivePage1**: Verifies default behavior (active messages, page 1)
- **testExecuteWithActiveListType**: Verifies "active" list type navigation
- **testExecuteWithArchivedListType**: Verifies "archived" list type navigation
- **testExecuteWithUnreadListType**: Verifies "unread" list type navigation
- **testExecuteWithPageNumber**: Verifies page number parsing and navigation
- **testExecuteWithInvalidPageNumber**: Verifies error handling for non-numeric page numbers
- **testExecuteWithNegativePageNumber**: Verifies error handling for page numbers < 1
- **testExecuteWithInvalidListType**: Verifies error handling for invalid list types
- **testGetTabCompletionsForListType**: Verifies tab completion filtering
- **testGetTabCompletionsReturnsAllTypesOnEmpty**: Verifies tab completion with empty input
- **testGetTabCompletionsForOtherArguments**: Verifies tab completion for other arguments

### MailboxPaginationTest
Tests the pagination logic and message display for the Mailbox class:

- **testEmptyActiveMessagesShowsAppropriateMessage**: Verifies message for empty mailbox
- **testEmptyActiveMessagesWithHighPageShowsError**: Verifies error for high page on empty mailbox
- **testSinglePageOfMessages**: Verifies display when all messages fit on one page
- **testMultiplePagesShowsNavigation**: Verifies navigation links appear for multiple pages
- **testPageBoundaryCalculation**: Verifies correct page boundaries for middle pages
- **testLastPageWithPartialMessages**: Verifies correct display for last page with fewer messages
- **testInvalidPageNumberTooHigh**: Verifies error handling for page numbers exceeding total pages
- **testInvalidPageNumberNegative**: Verifies error handling for negative page numbers
- **testArchivedMessagesPagination**: Verifies pagination works for archived messages
- **testUnreadMessagesPagination**: Verifies pagination works for unread messages (cross-list)
- **testPageSizeRespected**: Verifies custom page size is correctly applied
- **testDefaultPageSizeUsedByOverloadedMethod**: Verifies default page size (10) is used by convenience methods

## Running the Tests

To run all tests:
```bash
mvn test
```

To run a specific test class:
```bash
mvn test -Dtest=ListCommandTest
mvn test -Dtest=MailboxPaginationTest
```

To run a specific test method:
```bash
mvn test -Dtest=ListCommandTest#testExecuteWithPageNumber
```

## Test Dependencies

The tests use:
- **JUnit 4.13.2**: Testing framework
- **Mockito 3.12.4**: Mocking framework for Bukkit/Spigot objects

## Notes

- Tests use Mockito to mock Bukkit/Spigot API objects (Player, CommandSender, etc.)
- Tests verify both happy path and error handling scenarios
- Tests ensure backward compatibility with existing command usage
- Tests validate the clickable navigation components are being sent to players
Loading