Compare commits

..

3 commits
v0.2.0 ... main

Author SHA1 Message Date
8ee09b4e3e v0.3.0 2025-06-11 18:52:55 +01:00
d2b14dbe94 Dialog: Provide hints for the sender name
The dialog form has a simple text input for the sender's name.
This can lead to typos or mistakes if the GM wants to send a message
using the same sender name as a previous message.

Add a new function that goes through all the players' histories and
compiles a list of all unique senders, which we can then use to build
a list of hints that will be used to show suggestions when the GM fills
the sender name in the form.

This is currently quite ineficcient as it re-does all the work for every
single dialog.
A better way would be to cache it or store it explicitly somewhere when
we send a message, but this would require a way for the GM to edit it.
2025-06-11 18:48:44 +01:00
30c0324799 Introduce utils.mjs for misc constants and functions
Move some generic constants and functions, or supporting functions
to a new file.
This makes the main script smaller and makes it more focused on the
core functionality of the module.
2025-06-11 17:56:54 +01:00
6 changed files with 121 additions and 50 deletions

View file

@ -1,4 +1,4 @@
# The Mill's Messages - v0.2.0
# The Mill's Messages - v0.3.0
This little [FoundryVTT](https://foundryvtt.com/) module gives the GMs a way to send messages to PCs with a bit more flair than
the chat, and more spontaneity than journals !
@ -31,6 +31,8 @@ each page containing the messages from one specific recipient.
Both the journals and pages will be created automatically.
Players only have observer permissions on their own journal, which will take the name of their
character, or their own if they don't control one yet.
If the history is enabled, the message form will go look for previous senders' names and provide
a list for autocompletion.
The GM can send a message to an offline player, which will add it to its history if it is enabled,
but won't show up as a pop-up when they next log in.

View file

@ -2,7 +2,7 @@
"id": "the-mills-messages",
"title": "The Mill's Messages",
"description": "A little message-sending modules for GMs",
"version": "0.2.0",
"version": "0.3.0",
"compatibility": {
"minimum": "12",
"verified": "12",
@ -49,5 +49,5 @@
"readme": "README.md",
"url": "https://git.tfk-astrodome.net/trotFunky/TheMillsMessages/src/branch/release",
"manifest": "https://git.tfk-astrodome.net/trotFunky/TheMillsMessages/raw/branch/release/module.json",
"download": "https://git.tfk-astrodome.net/trotFunky/TheMillsMessages/archive/v0.2.0.zip"
"download": "https://git.tfk-astrodome.net/trotFunky/TheMillsMessages/archive/v0.3.0.zip"
}

View file

@ -1,58 +1,13 @@
export const module_id = "the-mills-messages";
import { module_id, history_flag, str_format, create_history_journal } from "./utils.mjs"
import { module_settings, register_settings } from "./settings.mjs";
import { prepare_send_message_html, validate_form } from "./send_dialog_form.mjs";
// User flag to point to the ID of the journal to store message history to
const history_flag = "history-journal";
/** @type {SocketlibSocket} */
let socket;
const SocketMessages = Object.freeze({
DisplayMessage: "display-message",
})
/**
* This is a copy of Foundry's internal formatting used by
* localize.format(), so that we can be consistent.
* @param {String} str The string whose "{named_args}" will be formatted
* @param {Object} data An object with named_args members, whose data is replaced
* @returns {String} The formatted string
*/
function str_format(str, data) {
const fmt = /{[^}]+}/g;
str = str.replace(fmt, k => {
return data[k.slice(1, -1)];
});
return str;
}
/**
* Create the journal entry for the history of the specified user,
* update the user's history flag and return the entry for further
* manipulation if needed.
* @param {User} user The user object that we are creating the history for
* @return {Promise<JournalEntry>} The JournalEntry that was created.
*/
async function create_history_journal(user) {
// Use the settings' title for the journal, or revert to the default one if it is empty.
let format_name = {name: user.character ? user.character.name : user.name};
let formated_journal_title = str_format(game.settings.get(module_id, module_settings.HistoryJournalTitle), format_name)
let title = formated_journal_title.length > 0 ?
formated_journal_title :
game.i18n.format("MM.UI.HistoryJournalTitle", format_name)
return JournalEntry.create({
name: title,
ownership: {
default: CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE,
[user.id]: CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER // Make the history private and unmodifiable
},
}).then(journal => {
return user.setFlag(module_id, history_flag, journal.id).then(() => journal);
});
}
/**
* Handle the dialog to send messages to players from NPCs.
* The basic dialog is simple : display a few fields selecting which players

View file

@ -3,6 +3,10 @@
* so they can be used in the main module.
*/
import { get_existing_senders } from "./utils.mjs";
const sender_hints_id = "sender_hints"
/**
* @typedef {Object} MessageFormResponse
* @property {string[]} recipients The list of player IDs the message will be sent to
@ -16,6 +20,23 @@
* @property {MessageFormResponse} form_response The response form the dialog form
*/
/**
* Generate the HTML for a <datalist> whose options are the provided senders.
* @param {String} hints_id The ID to use for the <datalist> providing the hints
* @param {String[]} senders The list of existing senders, or an empty string if invalid.
*/
function generate_sender_hints(hints_id, senders) {
// Both arguments need to be usable to generate something useful.
if (!hints_id || !senders || senders.length === 0) {
return "";
}
let sender_hints_html = `<datalist id="${hints_id}">`
for (let sender of senders) {
sender_hints_html += `<option value="${sender}">`;
}
return sender_hints_html + "</datalist>";
}
/**
* Given that "required" does not seem to have much impact, check that everything
* has been properly filled.
@ -88,6 +109,22 @@ export function prepare_send_message_html(existing_values = null, chain = false)
localize: true
})
/*
* If we can find previous senders in player's histories, create a datalist
* with them so that the names can appear as autocompletion when filling the form.
*/
/*
* TODO: This is very silly : it re-computes everything for every form that is opened.
* Find a way to cache it or store the senders list it explicitly rather than deduce it
* from the history.
*/
let existing_senders = get_existing_senders();
if (existing_senders.length > 0) {
sender_input.setAttribute("list", sender_hints_id)
// This keeps the datalist withing the input group as proper HTML.
sender_group.insertAdjacentHTML("beforeend", generate_sender_hints(sender_hints_id, existing_senders))
}
const message_input = foundry.applications.elements.HTMLProseMirrorElement.create({
name: "message",
required: true,

View file

@ -1,4 +1,4 @@
import {module_id} from "./mills_messages.mjs";
import { module_id } from "./utils.mjs";
export const module_settings = Object.freeze({
MessageDialogTitle: "messageDialogTitle",

77
scripts/utils.mjs Normal file
View file

@ -0,0 +1,77 @@
/*
* This file contains some module-wide constants and utility functions
* that do not really fit elsewhere.
*/
import { module_settings } from "./settings.mjs";
export const module_id = "the-mills-messages";
// User flag to point to the ID of the journal to store message history to
export const history_flag = "history-journal";
/**
* This is a copy of Foundry's internal formatting used by
* localize.format(), so that we can be consistent.
* @param {String} str The string whose "{named_args}" will be formatted
* @param {Object} data An object with named_args members, whose data is replaced
* @returns {String} The formatted string
*/
export function str_format(str, data) {
const fmt = /{[^}]+}/g;
str = str.replace(fmt, k => {
return data[k.slice(1, -1)];
});
return str;
}
/**
* Create the journal entry for the history of the specified user,
* update the user's history flag and return the entry for further
* manipulation if needed.
* @param {User} user The user object that we are creating the history for
* @return {Promise<JournalEntry>} The JournalEntry that was created.
*/
export async function create_history_journal(user) {
// Use the settings' title for the journal, or revert to the default one if it is empty.
let format_name = {name: user.character ? user.character.name : user.name};
let formated_journal_title = str_format(game.settings.get(module_id, module_settings.HistoryJournalTitle), format_name)
let title = formated_journal_title.length > 0 ?
formated_journal_title :
game.i18n.format("MM.UI.HistoryJournalTitle", format_name)
return JournalEntry.create({
name: title,
ownership: {
default: CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE,
[user.id]: CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER // Make the history private and unmodifiable
},
}).then(journal => {
return user.setFlag(module_id, history_flag, journal.id).then(() => journal);
});
}
/**
* Go through all the user's histories to build a list of existing senders,
* so that they can be displayed or re-used.
* The final list is sorted and does not contain duplicates, as one sender
* can send messages to multiple players at once.
* @returns {String[]} A sorted array of the sender names, if any
*/
export function get_existing_senders() {
/** @type {String[]} */
let senders = []
for (let user of game.users.players) {
/** @type {JournalEntry} */
let history = game.journal.get(user.getFlag(module_id, history_flag))
if (!history) {
continue;
}
for (let page of history.pages) {
if (!senders.includes(page.name)) {
senders.push(page.name);
}
}
}
return senders.sort();
}