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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ The roadmap below contains some of the features that I'm **planning to add** in
- [ ] **Pre-defined authors**: Rather than having to enter the name of the changelog's author every time (possibly including formatting), there could be a list of pre-defined authors in the plugin config that a user could choose from.
- [ ] **Customizable changelog dialog layout**: Currently, the changelog dialog always shows all previous changelogs, with unread ones marked red. In the future, it'd be nice to have an option for this, e.g. so players on join could also only see the most recent changelog.
- [ ] **Paged changelogs**: At the moment, all existing changelogs are just added to the dialog without any additional checks. While this is fine with just a few of them, it could quickly become an issue with a long history of changelogs. So adding a page system with a configurable amount of changelogs per page would be great!
- [ ] **Player first-join storage**: Right now, changelogs are just displayed to players upon joining whenever they haven't read them (except for the **first time** a player joins a server). This can be very confusing to new-ish players, since they weren't there for all the previous changes to begin with! A possible (while not 100% perfect) solution to this would be storing when the player initially joined the server / was first seen by the plugin, to then only display relevant changelogs to them.
- [x] **Player first-join storage**: Right now, changelogs are just displayed to players upon joining whenever they haven't read them (except for the **first time** a player joins a server). This can be very confusing to new-ish players, since they weren't there for all the previous changes to begin with! A possible (while not 100% perfect) solution to this would be storing when the player initially joined the server / was first seen by the plugin, to then only display relevant changelogs to them.
- [ ] **Other UI types**: The changes in this lower section are planned for a not-so-close future, the stuff above is more relevant for now! Anyway, even though other UI types (like chat, books, etc.) may not be as intuitive, some server owners might still prefer them. While there are plugins out there that can already do this, it would be nice for those to be available along with the additional features this plugin provides.
- [ ] **A poll/voting system?**: A bit of a far-fetched idea, but since changelogs include a lot of, well, *change*, they can sometimes be controversial along players. I feel like a voting system might fit this plugin kind of well, so players can immediately share their opinion on a specific change right when they see it!

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.time.Instant;
import java.util.List;
import java.util.UUID;

Expand All @@ -25,6 +26,17 @@ public interface ChangelogStorage {

void markAsRead(int uid, @NotNull UUID player);

@Nullable Instant getFirstSeenAt(@NotNull UUID player);

@NotNull Instant recordFirstSeen(@NotNull UUID player, @NotNull Instant seenAt);

default boolean isUnreadFor(@NotNull ChangelogEntry entry, @NotNull UUID player) {
Instant firstSeenAt = this.getFirstSeenAt(player);
return firstSeenAt != null
&& entry.recordedAt().isAfter(firstSeenAt)
&& !entry.playersRead().contains(player);
}

int nextUID();

@NotNull String getDisplayName();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,24 @@

@RequiredArgsConstructor
public class YamlChangelogStorage implements ChangelogStorage {
private static final @NotNull DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ISO_INSTANT;
private final @NotNull Path filePath;
private List<ChangelogEntry> cache;
private Map<UUID, Instant> playerFirstSeen;

@Override
public void init() {
if (!filePath.toFile().exists()) {
this.cache = new ArrayList<>();
this.playerFirstSeen = new HashMap<>();
this.save();
return;
}
try {
Yaml config = new Yaml();
Map<String, Object> data = config.load(new FileInputStream(filePath.toFile()));
this.cache = this.deserializeEntries(data);
this.playerFirstSeen = this.deserializePlayerFirstSeen(data);
} catch (IOException | YAMLException e) {
throw new RuntimeException("Failed to load changelog data from " + filePath, e);
}
Expand Down Expand Up @@ -66,11 +70,14 @@ public boolean removeEntry(int uid) {

private void save() {
Yaml config = new Yaml();
List<Map<String, Object>> data = this.cache.stream()
List<Map<String, Object>> entries = this.cache.stream()
.map(this::serializeEntry)
.toList();
Map<String, Object> data = new LinkedHashMap<>();
data.put("entries", entries);
data.put("playerFirstSeen", this.serializePlayerFirstSeen());
try (FileOutputStream os = new FileOutputStream(filePath.toFile())) {
config.dump(Map.of("entries", data), new BufferedWriter(new OutputStreamWriter(os)));
config.dump(data, new BufferedWriter(new OutputStreamWriter(os)));
} catch (IOException e) {
throw new RuntimeException("Failed to save changelog data to " + filePath, e);
}
Expand Down Expand Up @@ -110,13 +117,34 @@ private void save() {
return new ChangelogEntry(uid, lines, recordedAt, author, playersRead);
}

private @NotNull Map<UUID, Instant> deserializePlayerFirstSeen(@NotNull Map<String, Object> config) {
try {
//noinspection unchecked
Map<String, String> rawFirstSeen = (Map<String, String>) config.get("playerFirstSeen");
if (rawFirstSeen == null) return new HashMap<>();
return rawFirstSeen.entrySet().stream().collect(Collectors.toMap(
e -> UUID.fromString(e.getKey()),
e -> DATE_FORMATTER.parse(e.getValue(), Instant::from)
));
} catch (Exception e) {
throw new RuntimeException("Failed to deserialize player first-seen data", e);
}
}

private @NotNull Map<String, String> serializePlayerFirstSeen() {
return this.playerFirstSeen.entrySet().stream().collect(Collectors.toMap(
entry -> entry.getKey().toString(),
entry -> DATE_FORMATTER.format(entry.getValue())
));
}

private @NotNull Map<String, Object> serializeEntry(@NotNull ChangelogEntry entry) {
Map<String, Object> data = new HashMap<>();
List<String> serializedLines = entry.lines()
.stream().map(GsonComponentSerializer.gson()::serialize)
.toList();
data.put("serializedLines", serializedLines);
String recordedAtRaw = DateTimeFormatter.ISO_INSTANT.format(entry.recordedAt());
String recordedAtRaw = DATE_FORMATTER.format(entry.recordedAt());
data.put("recordedAt", recordedAtRaw);
String serializedAuthor = Optional.ofNullable(entry.author())
.map(GsonComponentSerializer.gson()::serialize)
Expand Down Expand Up @@ -150,6 +178,18 @@ public void markAsRead(int uid, @NotNull UUID player) {
this.save();
}

@Override
public @Nullable Instant getFirstSeenAt(@NotNull UUID player) {
return this.playerFirstSeen.get(player);
}

@Override
public @NotNull Instant recordFirstSeen(@NotNull UUID player, @NotNull Instant seenAt) {
Instant previous = this.playerFirstSeen.putIfAbsent(player, seenAt);
if (previous != null) this.save();
return Objects.requireNonNullElse(previous, seenAt);
}

@Override
public int nextUID() {
return this.cache.size();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public class ChangelogDialog implements IDialog {

private @NotNull Component formatEntry(@NotNull ChangelogEntry entry, @NotNull IPlayer player, @NotNull DialogSessionManager sessionManager, boolean canManage) {
Component linesComponent = createLinesComponent(player, null, entry.lines());
Component entryComponent = translatableManual(player, "dialog.changelog.entry" + (!entry.hasRead(player) ? "_unread" : ""),
Component entryComponent = translatableManual(player, "dialog.changelog.entry" + (this.storage.isUnreadFor(entry, player.getUniqueId()) ? "_unread" : ""),
text(dateFormatter.format(entry.recordedAt())), linesComponent,
Objects.requireNonNullElseGet(entry.author(), () -> translatableManual(player, "dialog.changelog.unspecified_author"))
);
Expand Down Expand Up @@ -123,7 +123,7 @@ public void onActionTriggered(@NotNull String action, @Nullable NBTCompound data
case "confirm_read" -> {
List<Integer> uids = this.storage.listEntries()
.stream()
.filter(e -> !e.hasRead(source))
.filter(e -> this.storage.isUnreadFor(e, source.getUniqueId()))
.map(ChangelogEntry::uid)
.toList();
uids.forEach(uid -> storage.markAsRead(uid, source.getUniqueId()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import net.kyori.adventure.key.Key;
import org.jetbrains.annotations.NotNull;

import java.time.Instant;
import java.util.function.Supplier;

import static com.codingcat.changelogs.base.ServerChangelogs.info;
Expand Down Expand Up @@ -45,9 +46,10 @@ public void unregisterEvents(@NotNull IEventManager manager) {
}

public boolean checkShowDialog(@NotNull IPlayer player, boolean freeze) {
if (player.isFirstJoin()) return false;
boolean unreadChangelogs = this.storageSupplier.get().listEntries().stream()
.anyMatch(e -> !e.hasRead(player));
ChangelogStorage storage = this.storageSupplier.get();
Instant firstSeenAt = storage.recordFirstSeen(player.getUniqueId(), Instant.now());
boolean unreadChangelogs = storage.listEntries().stream()
.anyMatch(e -> !e.hasRead(player) && e.recordedAt().isAfter(firstSeenAt));
if (!unreadChangelogs) return false;
ChangelogDialog dialog = this.dialogHolder.getFromType(ChangelogDialog.class);
if (freeze) this.sessionManager.freeze(player);
Expand Down