Compare commits
3 commits
Author | SHA1 | Date | |
---|---|---|---|
8ee09b4e3e | |||
d2b14dbe94 | |||
30c0324799 |
6 changed files with 121 additions and 50 deletions
|
@ -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
|
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 !
|
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.
|
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
|
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.
|
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,
|
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.
|
but won't show up as a pop-up when they next log in.
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"id": "the-mills-messages",
|
"id": "the-mills-messages",
|
||||||
"title": "The Mill's Messages",
|
"title": "The Mill's Messages",
|
||||||
"description": "A little message-sending modules for GMs",
|
"description": "A little message-sending modules for GMs",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"compatibility": {
|
"compatibility": {
|
||||||
"minimum": "12",
|
"minimum": "12",
|
||||||
"verified": "12",
|
"verified": "12",
|
||||||
|
@ -49,5 +49,5 @@
|
||||||
"readme": "README.md",
|
"readme": "README.md",
|
||||||
"url": "https://git.tfk-astrodome.net/trotFunky/TheMillsMessages/src/branch/release",
|
"url": "https://git.tfk-astrodome.net/trotFunky/TheMillsMessages/src/branch/release",
|
||||||
"manifest": "https://git.tfk-astrodome.net/trotFunky/TheMillsMessages/raw/branch/release/module.json",
|
"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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 { module_settings, register_settings } from "./settings.mjs";
|
||||||
import { prepare_send_message_html, validate_form } from "./send_dialog_form.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} */
|
/** @type {SocketlibSocket} */
|
||||||
let socket;
|
let socket;
|
||||||
const SocketMessages = Object.freeze({
|
const SocketMessages = Object.freeze({
|
||||||
DisplayMessage: "display-message",
|
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.
|
* Handle the dialog to send messages to players from NPCs.
|
||||||
* The basic dialog is simple : display a few fields selecting which players
|
* The basic dialog is simple : display a few fields selecting which players
|
||||||
|
|
|
@ -3,6 +3,10 @@
|
||||||
* so they can be used in the main module.
|
* 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
|
* @typedef {Object} MessageFormResponse
|
||||||
* @property {string[]} recipients The list of player IDs the message will be sent to
|
* @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
|
* @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
|
* Given that "required" does not seem to have much impact, check that everything
|
||||||
* has been properly filled.
|
* has been properly filled.
|
||||||
|
@ -88,6 +109,22 @@ export function prepare_send_message_html(existing_values = null, chain = false)
|
||||||
localize: true
|
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({
|
const message_input = foundry.applications.elements.HTMLProseMirrorElement.create({
|
||||||
name: "message",
|
name: "message",
|
||||||
required: true,
|
required: true,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import {module_id} from "./mills_messages.mjs";
|
import { module_id } from "./utils.mjs";
|
||||||
|
|
||||||
export const module_settings = Object.freeze({
|
export const module_settings = Object.freeze({
|
||||||
MessageDialogTitle: "messageDialogTitle",
|
MessageDialogTitle: "messageDialogTitle",
|
||||||
|
|
77
scripts/utils.mjs
Normal file
77
scripts/utils.mjs
Normal 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();
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue