Skip to content

Commit

Permalink
Merge pull request #35 from lucasmetzen/road-to-release
Browse files Browse the repository at this point in the history
Pre-release 3.0.0-theta: Mostly improve styling details
  • Loading branch information
lucasmetzen authored Dec 26, 2024
2 parents 239f3a2 + 3496ab2 commit 59da805
Show file tree
Hide file tree
Showing 17 changed files with 291 additions and 177 deletions.
4 changes: 4 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Set the default behavior, in case people don't have core.autocrlf set.
# This is needed to allow for compatibility of line separators between Windows and MacOS.
# https://github.com/github/docs/blob/main/content/get-started/getting-started-with-git/configuring-git-to-handle-line-endings.md#example
* text eol=crlf
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
.idea/
notes/
tmp/
._*
*.code-workspace

# Foundry VTT's file used to lock module, preventing automatic updates:
*.lock
86 changes: 86 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# ![logo](docs/logo.svg?raw=true) Lucas's Awesome Messenger Extension, or short: LAME Messenger

![Supported Foundry VTT version](https://img.shields.io/endpoint?url=https://foundryshields.com/version?url=https://github.com/lucasmetzen/foundryvtt-messenger/releases/download/latest/module.json "Supported Foundry VTT version")
[![GitHub issues](https://img.shields.io/github/issues/lucasmetzen/foundryvtt-messenger/bug.svg)](https://github.com/lucasmetzen/foundryvtt-messenger/issues/)

LAME Messenger for Foundry VTT provides a simple messenger interface to easily whisper messages.

[add representative screenshot with absolute URL: https://github.com/lucasmetzen/foundryvtt-messenger/blob/main/images/logo.svg?raw=true]

- Do you keep missing whispers other players send you?
- Would you like to see a chat history of sent and received private messages without having to scroll through all chat cards in the sidebar?
- Ever wanted to send a single message to multiple players at the same time?
- Are you tired of having to type `/whisper` each time you want to send a direct message to a player?
- Can you never remember a player's username when you want to text them privately?

If you answered at least one of those questions with "Yes", then LAME Messenger is the right module for you!

---

## Features

- Dedicated window for sending and receiving whispers with chat history
- Send a message to multiple recipients at the same time
![message sent to two users](docs/README-message-sent-to-two-users.webp?raw=true)
- Shows a user's avatar image in addition to their name if set in world's user configuration
![avatar in user configuration](docs/README-user-avatar.webp?raw=true)
- No need to type `/whisper` command and recipient's username
- Visual and auditory notification for incoming whisper (optional)
- Messenger window opens upon receiving a whisper (optional)


## Installation

This module can be installed automatically from the Foundry Virtual Tabletop module browser, or by using the following module manifest url:
https://github.com/lucasmetzen/foundryvtt-messenger/releases/download/latest/module.json


## Configurable options

- Show notification message for incoming whisper
- Notification message is displayed until dismissed
- Show currently disconnected users in user selection
![disconnected users shown](docs/README-disconnected-users-shown.webp?raw=true)
- Show button in chat sidebar (next to the roll type selector) to open LAME messenger window (shown by default)
![button in chat sidebar](docs/README-button-in-chat-sidebar.webp?raw=true)
- Show button in scene controls toolbar (left side of screen) to open LAME messenger window
![button in scene controls toolbar](docs/README-button-in-scene-controls-toolbar.webp?raw=true)


## Notes

- LAME Messenger's chat history only shows messages you have sent and received until you either reload the browser window, or log out of Foundry VTT. The module currently does not store messages separately from Foundry VTT (as seen in the sidebar's chat tab), and doesn't compile a history from the whispers existing in the world's database. A persistent solution is planned for future releases.
- The module is not a replacement for Foundry VTT's built-in whisper messaging. It only provides a handy graphical interface.
- Messages not sent privately as a whisper (AKA "public" or dice rolls) are not handled by LAME Messenger. Public chat might be included in a future release.


## Troubleshooting

_"I don't hear the notification sound when I receive a whisper."_
The sound is played in the `interface` context, so please make sure to set the volume for `Interface` to audible levels.

_"The messenger's history is empty after I reloaded the browser windows, or after logging out and in again."_
Solution: LAME Messenger currently does not store messages separately from Foundry VTT (as seen in the sidebar's chat tab), and doesn't compile a history from the whispers existing in the world's database. A persistent solution is planned for future releases.

_"I can't send a message I've typed in the messenger."_
You might not have selected at least one user to send to.

_"No users are shown to select from."_
You can only send messages to users that are currently connected. Therefore, offline users are by default not listed (and if you set the option to show them, they aren't selectable unless they connect).

_"I can't see messages other users sent to users other than myself."_
That's by design, as LAME Messenger handles whispers the same way Foundry VTT does.


## Planned features

- Tabbed chat for each user
- Configurable notification sound file
- Configurable keyboard shortcuts (opening window, line break in message, sending)
- Option to show the character's portrait image instead of the user's avatar if the user has an associated actor


## Credits

- Thanks to BerlinAtmospheres (https://youtu.be/syG19v7pJC4?t=26) for the Pst-pst sound.
- Thanks to Darksmaug, LittleKing205, Aphasmayra, and dawnofdope for additional beta/user-testing.
Binary file added docs/README-button-in-chat-sidebar.webp
Binary file not shown.
Binary file added docs/README-button-in-scene-controls-toolbar.webp
Binary file not shown.
Binary file added docs/README-disconnected-users-shown.webp
Binary file not shown.
Binary file added docs/README-message-sent-to-two-users.webp
Binary file not shown.
Binary file added docs/README-user-avatar.webp
Binary file not shown.
1 change: 1 addition & 0 deletions docs/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion languages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
},
"IncomingWhisperFrom": "Whisper from",
"NoUsersToShow": "No users to show.",
"NoUsersToShowExplanation": "Either you are the only user in the world, all other users are banned, or they are inactive and the setting to show inactive ones is disabled."
"NoUsersToShowExplanation": "Either you are the only user in the world, all other users are banned, or they are inactive and the setting to show inactive ones is disabled.",
"UserIsOffline": "Can't send to user as they are offline."
}
}
4 changes: 4 additions & 0 deletions scripts/helpers/handlebars-helpers.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,8 @@ export function registerHandlebarsHelpers() {
Handlebars.registerHelper("beautifiedHistory", function () {
return window.LAME.beautifyHistory();
});

Handlebars.registerHelper("userColor", function (userId) {
return `var(--user-color-${userId})`;
});
}
4 changes: 4 additions & 0 deletions scripts/helpers/log.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@ const CONSOLE_MESSAGE_PRESET = [`%c${CONSOLE_LOG_PREFIX}%c |`, 'background: #800
export function log(...args) {
console.log(...CONSOLE_MESSAGE_PRESET, ...args);
}

export function warn(...args) {
console.warn(...CONSOLE_MESSAGE_PRESET, ...args);
}
75 changes: 27 additions & 48 deletions scripts/module.mjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
const {ApplicationV2, HandlebarsApplicationMixin} = foundry.applications.api;

import { localize, MODULE_ID, MODULE_ICON_CLASSES, TEMPLATE_PARTS_PATH } from "./config.mjs";
import { log } from "./helpers/log.mjs";
import { getSetting, registerSettings } from "./settings.mjs";
import { registerHandlebarsHelpers } from "./helpers/handlebars-helpers.mjs";
import {localize, MODULE_ID, MODULE_ICON_CLASSES, TEMPLATE_PARTS_PATH} from "./config.mjs";
import {log, warn} from "./helpers/log.mjs";
import {getSetting, registerSettings} from "./settings.mjs";
import {registerHandlebarsHelpers} from "./helpers/handlebars-helpers.mjs";

class LAME extends HandlebarsApplicationMixin(ApplicationV2) {

Expand Down Expand Up @@ -62,9 +62,10 @@ class LAME extends HandlebarsApplicationMixin(ApplicationV2) {
};
}

_onRender(_context, _options) { }
_onRender(_context, _options) {
}

_onFirstRender(context, options) {
_onFirstRender(_context, _options) {
/* Create div and move some of the partial elements into it. This is needed to maintain the ability to re-render
* specific partials on demand. Which would not be possible if a PART simply has multiple `templates` besides
* the main entry point template, as the "child" templates would not have targetable identifiers.
Expand All @@ -87,7 +88,7 @@ class LAME extends HandlebarsApplicationMixin(ApplicationV2) {
html.find('.message').on("keypress", event => this._onKeyPressEvent(event, html));
}

static async onSubmit(event, form, formData) {}
static async onSubmit(_event, _form, _formData) { }

constructor(app) {
super(app);
Expand All @@ -97,9 +98,6 @@ class LAME extends HandlebarsApplicationMixin(ApplicationV2) {
static async init() {
registerSettings();
registerHandlebarsHelpers();
}

static setup() {
window.LAME = new LAME();
}

Expand All @@ -117,44 +115,38 @@ class LAME extends HandlebarsApplicationMixin(ApplicationV2) {
beautified.push(
msg[0] + // (date &) time
` ${toOrFrom} ` + // in or out
this.getUserNameFromId(msg[2]) + ': ' + // User name
msg[2] + ': ' + // User name
msg[3] // message
);
}
return beautified;
}

async renderHistoryPartial() {
log("renderHistoryPartial > this", this);
await this.renderPart('history');
}

async render(...args) {
log("this.rendered", this.rendered)
if (!this.rendered) {
log("window not shown. forcing rendering")
return await super.render(true, ...args);
}

//await super.render(false, ...args);
await super.render(false);
/* async _render(force=false, options={}) {
await super._render(force, options);*/
}

/* This is needed as I can't figure out how to stop the window from re-rendering when it's already shown and
* one of the buttons is clicked to open the window. So I simply avoid additional logic and use `force: false`
* in render() if the window is already shown.
*/
async renderPart(partId){
async renderPart(partId) {
if (!this.rendered) {
log("Trying to render partial while window is not shown. This should not happen.");
warn(`Trying to render partial "${partId}" while window is not shown. This should not happen.`);
return false;
}

await super.render(false, { parts: [partId] }); // Note: This calls SUPER directly.
}

async renderHistoryPartial() {
await this.renderPart('history');
}

computeUsersData() {
const showInactiveUsers = getSetting('showInactiveUsers');

Expand All @@ -169,7 +161,6 @@ class LAME extends HandlebarsApplicationMixin(ApplicationV2) {
name: user.name,
id: user.id,
avatar: user.avatar,
color: user.color,
active: user.active // user currently connected
};
usersData.push(data);
Expand All @@ -182,17 +173,6 @@ class LAME extends HandlebarsApplicationMixin(ApplicationV2) {
await window.LAME.renderPart('users');
}

getUserNameFromId(id) {
// TODO: this is called way too often
/*log('getUserNameFromId > id', id)
log('getUserNameFromId > users', window.LAME.users)*/
return window.LAME.users.find(user => user.id === id).name;
}

getUserIdFromName(name) {
return window.LAME.users.find(user => user.name === name).id;
}

sendWhisperTo(userNames, msg) {
for (let username of userNames) {
// chatData needs to be defined for each message as the .whisper assignment is not overwritten on subsequent loops,
Expand All @@ -216,7 +196,7 @@ class LAME extends HandlebarsApplicationMixin(ApplicationV2) {
// Get selected users:
const checkedUserElements = html.find('input[id^="user-"]:checked');
let selectedUserNames = [];
checkedUserElements.each(function() {
checkedUserElements.each(function () {
selectedUserNames.push(this.id.replace('user-', ''));
});
if (selectedUserNames.length === 0) {
Expand Down Expand Up @@ -283,22 +263,20 @@ class LAME extends HandlebarsApplicationMixin(ApplicationV2) {

addIncomingMessageToHistory(data) {
const time = this.currentTime();
// TODO: data.user.id should be replaced by data.author.name and the whole process simplified
// TODO: data.user is also now deprecated since v12 in favor of data.author.
this.history.push([time, 'in', data.user.id, data.content]);
this.history.push([time, 'in', data.author.name, data.content]);
}

addOutgoingMessageToHistory(recipients, msg) {
for (const recipient of recipients) {
const time = this.currentTime();
this.history.push([time, 'out', this.getUserIdFromName(recipient), msg]);
this.history.push([time, 'out', recipient, msg]);
}
}

}

// Add button to scene controls toolbar:
Hooks.on('renderSceneControls', (controls, html) => {
Hooks.on('renderSceneControls', (_controls, html) => {
if (!getSetting("buttonInSceneControlToolbar")) return;

const messengerBtn = $(
Expand Down Expand Up @@ -329,25 +307,26 @@ Hooks.on("renderSidebarTab", async (app, html, _data) => {
html.find("#chat-controls select.roll-type-select").after(messengerBtn);
});

Hooks.on("createChatMessage", async (data, options, senderUserId) => {
Hooks.on("createChatMessage", async (data, _options, senderUserId) => {
const isToMe = (data?.whisper ?? []).includes(game.userId),
isFromMe = senderUserId === game.userId;

if (!isToMe || isFromMe) return;

// Ignore private messages to GM that are player's roll results (e.g. Private/Blind GM rolls):
// Ignore private messages to GM that are players' roll results (e.g. Private/Blind GM rolls):
if (data.rolls.length > 0) return;

// log('incoming whisper', data, options, senderUserId)
// Ignore D&D5e system's "character has been awarded ..." messages.
if (data.content.includes('<span class=\"award-entry\">')) return;

await window.LAME.handleIncomingPrivateMessage(data);
});

Hooks.once('init', LAME.init); // this feels VERY early in Foundry's initialisation...
Hooks.once('setup', LAME.setup);
// Hooks.once('setup', LAME.setup);
Hooks.once('ready', LAME.ready);

// Update internal player list when user (dis)connects:
Hooks.on('userConnected', async(_user, _connected) => {
Hooks.on('userConnected', async (_user, _connected) => {
// https://foundryvtt.com/api/functions/hookEvents.userConnected.html
await window.LAME.computeUsersDataAndRenderPartial();
});
Loading

0 comments on commit 59da805

Please sign in to comment.