Skip to content

Commit

Permalink
Merge pull request #7 from JarrettSpiker/dev
Browse files Browse the repository at this point in the history
Release 2.0.0: Customizable links
  • Loading branch information
JarrettSpiker authored Jan 2, 2022
2 parents d6a0e8c + 98f64f2 commit d56369b
Show file tree
Hide file tree
Showing 14 changed files with 474 additions and 33 deletions.
4 changes: 3 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
{
"cSpell.words": [
"foundryredirect"
"flexrow",
"foundryredirect",
"MODULEPATH"
]
}
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,23 @@ After install, Foundry Redirect will generate a URL for your world, and display

Native Foundry VTT links will still be shown if you scroll, just in case you need them!

## Customizing Your Link

By default, Foundry Redirect will generate a link which looks like `https://foundryredirect.com/(a random ID)`. For example `https://foundryredirect.com/abcd1234-98gh-11zz-a976-611b200d4b84` This link will remain the same as your IP address changes, but is still unwieldy and difficult to memorize.

Instead of using the default random ID, you may customize it to something more unique to your world! From the Invitation Links window, click the "Customize" link to open the customization window. Then you can edit the link, "Test Address" to see if it is available, and Submit when you are happy with it

![customizeUI1](./images/FoundryRedirectCustomize1.png)

![customizeUI12](./images/FoundryRedirectCustomize2.png)

**Warning:** After customizing your link, your previous link will no longer work.

# Changelog

2.0.0
- Allow redirect address to be customized

1.1.0
- Update module description w/ security considerations and download numbers.
- Add support for Foundry 9
Expand All @@ -32,4 +46,4 @@ MIT License

This module works by maintaining a database of Foundry IP addresses and the links which redirect to them, on AWS. Only the developer has access to the database.

However, if you are unconfortable the IP of your Foundry instance existing on a database outside of your control, the developer recommends against the use of this module.
However, if you are uncomfortable the IP of your Foundry instance existing on a database outside of your control, the developer recommends against the use of this module.
Binary file added images/FoundryRedirectCustomize1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/FoundryRedirectCustomize2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified images/FoundryRedirectScreenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "foundry-redirect-module",
"version": "1.1.0",
"version": "2.0.0",
"description": "Foundry Redirect: Better invitation links",
"main": "index.js",
"scripts": {
Expand Down
4 changes: 2 additions & 2 deletions src/module.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
"title": "Foundry Redirect: Better invitation links",
"description": "A module to create static invitation links which look like a regular URL, and don't need to be resent every time your IP address changes",
"author": "JarrettSpiker",
"version": "1.1.0",
"version": "2.0.0",
"minimumCoreVersion": "0.7.9",
"compatibleCoreVersion" : "9",
"compatibleCoreVersion" : "9.238",
"scripts": [
"scripts/module.js"
],
Expand Down
277 changes: 277 additions & 0 deletions src/scripts/customization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
import { debugLog, displayErrorMessageToUser, displayInfoMessageToUser } from "./logging";
import { checkCustomAddress, CustomAddressStatus, customizeRedirectAddress, DEFAULT_SERVER_BASE_URL, getRedirectAddress } from "./server";

export async function displayCustomizationDialogue(callback : ()=>void) {
let content = await renderTemplate("modules/foundry-redirect/templates/customizationDialogue.html", {})
let dialogue = new Dialog({
title: "Foundry Redirect Customization",
buttons: {},
render: (html) => {onRender(html, dialogue)},
content : content,
default : "cancel",
close: () => callback(),
},
{
width: 575
});
dialogue.render(true);
}

async function submitChanges(target:GlobalEventHandlers, event:MouseEvent, dialogue:Dialog){
event.preventDefault();
let formElement = findCustomizationFormFromClickTarget(target);
if(!formElement) {
return;
}
let inputElement = findInputElement(formElement);
if(!inputElement) {
return;
}
let newAddress = inputElement.value;
let response = await customizeRedirectAddress(newAddress);
if(response.success){
displayInfoMessageToUser("Successfully updated invitation links");
} else {
displayErrorMessageToUser(response.message);
}
dialogue.close();
}

function cancel(dialogue:Dialog, ev: MouseEvent) {
ev.preventDefault();
dialogue.close();
}

async function testAddress(this: GlobalEventHandlers, ev: MouseEvent){
ev.preventDefault();
let formElement = findCustomizationFormFromClickTarget(this);
if(!formElement) {
return;
}
let statusMessage = findStatusMessage(formElement);
if(!statusMessage) {
return;
}
let inputElement = findInputElement(formElement);
if(!inputElement){
return;
}
setLoadingState(formElement);
let customAddress = inputElement.value;
let available = await checkCustomAddress(customAddress);
setAddressAvailableStatus(formElement, available)
}

async function onRender(html: HTMLElement | JQuery<HTMLElement>, dialogue:Dialog){

// find current redirect address and load data into dialogue
let currentAddress = await getRedirectAddress();
if(!currentAddress){
displayErrorMessageToUser("Failed to find redirect address when loading customization dialogue")
return;
}
if(!currentAddress.externalAddress.startsWith(DEFAULT_SERVER_BASE_URL)){
displayInfoMessageToUser("Server address has unexpected prefix " + currentAddress)
return;
}

let publicId = currentAddress.externalAddress.substring(DEFAULT_SERVER_BASE_URL.length + 1);

let redirectElement = findInputElement(html);
let testButton = findTestAddressButton(html);

let submitButton = findSubmitButton(html);
let cancelButton = findCancelButton(html);

if(!redirectElement || !testButton || !cancelButton || !submitButton){
displayErrorMessageToUser("Error rendering Foundry Redirect Customization")
return;
}
redirectElement.value = publicId;

// add listeners to the various buttons
testButton.onclick = testAddress;
submitButton.onclick = (ev) => submitChanges(submitButton!, ev, dialogue);
cancelButton.onclick = (ev) => cancel(dialogue, ev);

// allow address to be edited, and test to be clicked
hideStatusMessageSection(html, true);
testButton.disabled = false;
redirectElement.disabled = false;
}

function hideStatusMessageSection(html: HTMLElement | JQuery<HTMLElement>, hide :boolean) {
let statusMessageIcon = findStatusMessageIcon(html);
if(statusMessageIcon){
statusMessageIcon.style.visibility = hide ? "hidden" : "visible";
}
let statusMessage = findStatusMessage(html);
if (statusMessage){
statusMessage.style.visibility = hide ? "hidden" : "visible";
}
}

function findStatusMessage(html: HTMLElement | JQuery<HTMLElement>) : HTMLElement | undefined {
let statusMessage = findElementById(html, "redirect-status-message");
if(!statusMessage || !(statusMessage instanceof HTMLElement)){
debugLog("Redirect status message element could not be found")
debugLog(statusMessage)
return;
}
return statusMessage;
}

function findStatusMessageIcon(html: HTMLElement | JQuery<HTMLElement>) : HTMLElement | undefined {
let statusIcon = findElementById(html, "redirect-status-icon");
if(!statusIcon || !(statusIcon instanceof HTMLElement)){
debugLog("Redirect status message element could not be found")
debugLog(statusIcon)
return;
}
return statusIcon;
}

function findTestAddressButton(html: HTMLElement | JQuery<HTMLElement>) : HTMLButtonElement | undefined {
let testButton = findElementById(html, "test-redirect-address");
if(!testButton || !(testButton instanceof HTMLButtonElement)){
debugLog("Test Redirect element could not be found")
debugLog(testButton)
return;
}
return testButton
}

function findInputElement(html: HTMLElement | JQuery<HTMLElement>) : HTMLInputElement | undefined {
let redirectElement = findElementById(html, "redirect-value")
if(!redirectElement || !(redirectElement instanceof HTMLInputElement)){
debugLog("Redirect value element could not be found")
debugLog(redirectElement)
return;
}
return redirectElement;
}

function findSubmitButton(html: HTMLElement | JQuery<HTMLElement>) : HTMLButtonElement | undefined {
let button = findElementById(html, "submit-redirect");
if(!button || !(button instanceof HTMLButtonElement)){
debugLog("Submit redirect button could not be found")
debugLog(button)
return;
}
return button
}

function findCancelButton(html: HTMLElement | JQuery<HTMLElement>) : HTMLButtonElement | undefined {
let button = findElementById(html, "cancel-redirect");
if(!button || !(button instanceof HTMLButtonElement)){
debugLog("Cancel redirect button could not be found")
debugLog(button)
return;
}
return button
}

function setLoadingState(form:HTMLElement){
let statusMessage = findStatusMessage(form);
if(!statusMessage) {
return;
}
let statusIcon = findStatusMessageIcon(form);
if(!statusIcon) {
return;
}
let testAddressButton = findTestAddressButton(form);
if(!testAddressButton){
return;
}
statusMessage.textContent= "Loading..."
let classesToRemove : string[] = []
statusIcon.classList.forEach(c =>{
if(c.startsWith("fa-")){
classesToRemove.push(c)
}
})
classesToRemove.forEach(c=>{
statusIcon?.classList.remove(c)
})
statusIcon.classList.add("fa-spin")
statusIcon.classList.add("fa-spinner")
statusIcon.style.color = "";

testAddressButton.disabled = true;
hideStatusMessageSection(form, false)
}

function setAddressAvailableStatus(form:HTMLElement, available:CustomAddressStatus) {
let statusMessage = findStatusMessage(form);
if(!statusMessage) {
return;
}
let statusIcon = findStatusMessageIcon(form);
if(!statusIcon) {
return;
}
let testAddressButton = findTestAddressButton(form);
if(!testAddressButton){
return;
}
let submitButton = findSubmitButton(form);
if(!submitButton){
return;
}

let message = available.message;
if(message.startsWith('"')){
message = message.substring(1)
}
if(message.endsWith('"')){
message = message.substring(0, message.length-1)
}
statusMessage.textContent = message;
let classesToRemove : string[] = []
statusIcon.classList.forEach(c =>{
if(c.startsWith("fa-")){
classesToRemove.push(c)
}
})
classesToRemove.forEach(c=>{
statusIcon?.classList.remove(c)
})
statusIcon.classList.add(available.isAvailable ? "fa-check" : "fa-times-circle")
statusIcon.style.color = available.isAvailable ? "#006400" : "#8B0000";
testAddressButton.disabled = false;
hideStatusMessageSection(form, false)
submitButton.disabled = !available.isAvailable;
}

function findElementById(html: HTMLElement | JQuery<HTMLElement>, id:string) {
if(html instanceof HTMLElement) {
return html.querySelector(`#${id}`)
} else {
let q = html.find(`#${id}`)
if(q.length < 1) {
return null
}
return q.get()[0]
}
}

// recurses from the target of an onclick event, to the top level
// form defined for this dialog based on its ID
function findCustomizationFormFromClickTarget(target:GlobalEventHandlers) : HTMLElement | undefined {

if(!(target instanceof HTMLElement)){
debugLog("Target of click was not an HTMLElement")
debugLog(target)
return;
}
let formElement : HTMLElement | null = target;
while(formElement && formElement.id !== "redirect-customization-form") {
formElement = formElement.parentElement;
}
if(!formElement){
debugLog("Could not locate customization form from onclick action handler")
return;
}
return formElement
}
8 changes: 8 additions & 0 deletions src/scripts/foundryUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ export function getUser() : StoredDocument<User> | null {
return g.user;
}

export function isGm() : boolean {
let user = getUser();
if(!user){
return false;
}
return user.isGM;
}

export function getOrCreateFoundryId() : string {
let user = getUser();
let foundryId = user?.getFlag("core", FOUNDRY_ID_FLAG);
Expand Down
Loading

0 comments on commit d56369b

Please sign in to comment.