diff --git a/html/admin.html b/html/admin.html index 7c08b15..8f0fd1f 100644 --- a/html/admin.html +++ b/html/admin.html @@ -712,20 +712,6 @@
- -
- {{ if .discordEnabled }} - - - - - {{ else }} - - {{ end }} - -
diff --git a/lang/common/en-us.json b/lang/common/en-us.json index 3a13d57..254f412 100644 --- a/lang/common/en-us.json +++ b/lang/common/en-us.json @@ -9,6 +9,7 @@ "name": "Name", "submit": "Submit", "send": "Send", + "sent": "Sent", "success": "Success", "continue": "Continue", "error": "Error", diff --git a/ts/modules/common.ts b/ts/modules/common.ts index 65ce311..ede5be1 100644 --- a/ts/modules/common.ts +++ b/ts/modules/common.ts @@ -251,14 +251,13 @@ export function toggleLoader(el: HTMLElement, small: boolean = true) { } export function addLoader(el: HTMLElement, small: boolean = true, relative: boolean = false) { - if (!el.classList.contains("loader")) { - el.classList.add("loader"); - if (relative) el.classList.add("rel"); - if (small) { el.classList.add("loader-sm"); } - const dot = document.createElement("span") as HTMLSpanElement; - dot.classList.add("dot") - el.appendChild(dot); - } + if (el.classList.contains("loader")) return; + el.classList.add("loader"); + if (relative) el.classList.add("rel"); + if (small) { el.classList.add("loader-sm"); } + const dot = document.createElement("span") as HTMLSpanElement; + dot.classList.add("dot") + el.appendChild(dot); } export function removeLoader(el: HTMLElement, small: boolean = true) { @@ -347,3 +346,35 @@ export function throttle (callback: () => void, limitMilliseconds: number): () = } } +export function SetupCopyButton(button: HTMLButtonElement, text: string, baseClass?: string, notif?: string) { + if (!notif) notif = window.lang.strings("copied"); + if (!baseClass) baseClass = "~info"; + // script will probably turn this into multiple + const baseClasses = baseClass.split(" "); + button.type = "button"; + button.classList.add("button", ...baseClasses, "@low", "p-1"); + button.title = window.lang.strings("copy"); + const icon = document.createElement("i"); + icon.classList.add("icon", "ri-file-copy-line"); + button.appendChild(icon) + button.onclick = () => { + toClipboard(text); + icon.classList.remove("ri-file-copy-line"); + icon.classList.add("ri-check-line"); + button.classList.remove(...baseClasses); + button.classList.add("~positive"); + setTimeout(() => { + icon.classList.remove("ri-check-line"); + icon.classList.add("ri-file-copy-line"); + button.classList.remove("~positive"); + button.classList.add(...baseClasses); + }, 800); + window.notifications.customPositive("copied", "", notif); + }; +} + +export function CopyButton(text: string, baseClass?: string, notif?: string): HTMLButtonElement { + const button = document.createElement("button"); + SetupCopyButton(button, text, baseClass, notif); + return button; +} diff --git a/ts/modules/discord.ts b/ts/modules/discord.ts index 6dd785f..84e4812 100644 --- a/ts/modules/discord.ts +++ b/ts/modules/discord.ts @@ -10,7 +10,9 @@ export interface DiscordUser { var listeners: { [buttonText: string]: (event: CustomEvent) => void } = {}; -export function newDiscordSearch(title: string, description: string, buttonText: string, buttonFunction: (user: DiscordUser, passData: string) => void): (passData: string) => void { +export type DiscordSearch = (passData: string) => void; + +export function newDiscordSearch(title: string, description: string, buttonText: string, buttonFunction: (user: DiscordUser, passData: string) => void): DiscordSearch { if (!window.discordEnabled) { return () => {}; } diff --git a/ts/modules/invites.ts b/ts/modules/invites.ts index c898cd8..b44b6a2 100644 --- a/ts/modules/invites.ts +++ b/ts/modules/invites.ts @@ -1,5 +1,5 @@ -import { _get, _post, _delete, toClipboard, toggleLoader, toDateString } from "../modules/common.js"; -import { DiscordUser, newDiscordSearch } from "../modules/discord.js"; +import { _get, _post, _delete, toClipboard, toggleLoader, toDateString, SetupCopyButton, addLoader, removeLoader } from "../modules/common.js"; +import { DiscordSearch, DiscordUser, newDiscordSearch } from "../modules/discord.js"; import { reloadProfileNames } from "../modules/profiles.js"; declare var window: GlobalWindow; @@ -31,7 +31,7 @@ class DOMInvite implements Invite { delete = () => _delete("/invites", { "code": this.code }, (req: XMLHttpRequest) => { if (req.readyState == 4 && (req.status == 200 || req.status == 204)) { this.remove(); - const inviteDeletedEvent = new CustomEvent("inviteDeletedEvent", { "detail": this.code }); + const inviteDeletedEvent = new CustomEvent("inviteDeletedEvent", { detail: this.code }); document.dispatchEvent(inviteDeletedEvent); } }) @@ -91,8 +91,10 @@ class DOMInvite implements Invite { const expiry = this._middle.querySelector("span.user-expiry") as HTMLSpanElement; if (!d) { expiry.textContent = ""; + expiry.parentElement.classList.add("unfocused"); } else { expiry.textContent = window.lang.strings("userExpiry"); + expiry.parentElement.classList.remove("unfocused"); } this._userExpiry = d; this._middle.querySelector("strong.user-expiry-time").textContent = d; @@ -138,7 +140,7 @@ class DOMInvite implements Invite { // innerHTML as the newer sent_to re-uses this with HTML. tooltip.innerHTML = address; } - + private _sendToDialog: SendToDialog; private _sent_to: SentToList; get sent_to(): SentToList { return this._sent_to; } set sent_to(v: SentToList) { @@ -358,33 +360,20 @@ class DOMInvite implements Invite { this._codeArea = document.createElement('div') as HTMLDivElement; this._header.appendChild(this._codeArea); - this._codeArea.classList.add("flex", "flex-row", "flex-wrap", "justify-between", "w-full", "items-baseline", "gap-2", "truncate"); + this._codeArea.classList.add("flex", "flex-row", "flex-wrap", "justify-between", "w-full", "items-center", "gap-2", "truncate"); this._codeArea.innerHTML = ` -
+
- +
`; - const copyButton = this._codeArea.querySelector("span.button") as HTMLSpanElement; - copyButton.onclick = () => { - toClipboard(this._codeLink); - const icon = copyButton.children[0]; - icon.classList.remove("ri-file-copy-line"); - icon.classList.add("ri-check-line"); - copyButton.classList.remove("~info"); - copyButton.classList.add("~positive"); - setTimeout(() => { - icon.classList.remove("ri-check-line"); - icon.classList.add("ri-file-copy-line"); - copyButton.classList.remove("~positive"); - copyButton.classList.add("~info"); - }, 800); - }; + const copyButton = this._codeArea.getElementsByClassName("invite-copy-button")[0] as HTMLButtonElement; + SetupCopyButton(copyButton, this._codeLink); this._infoArea = document.createElement('div') as HTMLDivElement; this._header.appendChild(this._infoArea); - this._infoArea.classList.add("inv-infoarea", "flex", "flex-row", "items-baseline", "gap-2"); + this._infoArea.classList.add("inv-infoarea", "flex", "flex-row", "items-center", "gap-2"); this._infoArea.innerHTML = `
@@ -425,26 +414,30 @@ class DOMInvite implements Invite { detailsInner.appendChild(this._left); const leftLeft = document.createElement("div") as HTMLDivElement; this._left.appendChild(leftLeft); - leftLeft.classList.add("inv-profilearea", "min-w-full", "sm:min-w-fit"); + leftLeft.classList.add("inv-profilearea", "min-w-full", "sm:min-w-fit", "flex", "flex-col", "gap-4"); let innerHTML = ` -

${window.lang.strings("profile")}

-
- -
+ `; if (window.notificationsEnabled) { innerHTML += ` -

${window.lang.strings("notifyEvent")}

- - +
+

${window.lang.strings("notifyEvent")}

+ + +
`; } leftLeft.innerHTML = innerHTML; @@ -460,18 +453,19 @@ class DOMInvite implements Invite { this._middle = document.createElement('div') as HTMLDivElement; this._left.appendChild(this._middle); - this._middle.classList.add("flex", "flex-col", "justify-between"); + this._middle.classList.add("flex", "flex-col", "grow", "gap-4"); this._middle.innerHTML = ` -

${window.lang.strings("inviteDateCreated")}

-

${window.lang.strings("inviteRemainingUses")}

-

-

+

${window.lang.strings("inviteDateCreated")}

+

${window.lang.strings("inviteRemainingUses")}

+

+

+
`; this._right = document.createElement('div') as HTMLDivElement; detailsInner.appendChild(this._right); this._right.classList.add("card", "~neutral", "@low", "inv-created-users", "min-w-full", "sm:min-w-fit", "whitespace-nowrap"); - this._right.innerHTML = `${window.lang.strings("inviteUsersCreated")}`; + this._right.innerHTML = `${window.lang.strings("inviteUsersCreated")}`; this._userTable = document.createElement('div') as HTMLDivElement; this._userTable.classList.add("text-sm", "mt-1", ); this._right.appendChild(this._userTable); @@ -506,6 +500,10 @@ class DOMInvite implements Invite { this.user_label = invite.user_label; } this.userExpiryTime = invite.userExpiryTime || ""; + this._sendToDialog = new SendToDialog(this._middle.getElementsByClassName("invite-send-to-dialog")[0] as HTMLElement, invite, () => { + const needsUpdatingEvent = new CustomEvent("inviteNeedsUpdating", { detail: this.code }); + document.dispatchEvent(needsUpdatingEvent); + }); } asElement = (): HTMLDivElement => { return this._container; } @@ -549,6 +547,9 @@ export class DOMInviteList implements InviteList { this._list = document.getElementById('invites') as HTMLDivElement; this.empty = true; this.invites = {}; + // FIXME: Do this better, take advantage of getting the code in e.detail + document.addEventListener("inviteNeedsUpdating", () => { this.reload(); }, false); + document.addEventListener("newInviteEvent", () => { this.reload(); }, false); document.addEventListener("inviteDeletedEvent", (event: CustomEvent) => { const code = event.detail; @@ -667,9 +668,7 @@ function parseInvite(invite: { [f: string]: string | number | { [name: string]: } export class createInvite { - private _sendToEnabled = document.getElementById("create-send-to-enabled") as HTMLInputElement; - private _sendTo = document.getElementById("create-send-to") as HTMLInputElement; - private _discordSearch: HTMLSpanElement; + private _sendTo: SendToDialog; private _userExpiryToggle = document.getElementById("create-user-expiry-enabled") as HTMLInputElement; private _uses = document.getElementById('create-uses') as HTMLInputElement; private _infUses = document.getElementById("create-inf-uses") as HTMLInputElement; @@ -723,31 +722,6 @@ export class createInvite { get user_label(): string { return this._userLabel.value; } set user_label(label: string) { this._userLabel.value = label; } - get sendToEnabled(): boolean { - return this._sendToEnabled.checked; - } - set sendToEnabled(state: boolean) { - this._sendToEnabled.checked = state; - this._sendTo.disabled = !state; - if (state) { - this._sendToEnabled.parentElement.classList.remove("~neutral"); - this._sendToEnabled.parentElement.classList.add("~urge"); - if (window.discordEnabled) { - this._discordSearch.classList.remove("~neutral"); - this._discordSearch.classList.add("~urge"); - this._discordSearch.onclick = () => this._sendToDiscord(""); - } - } else { - this._sendToEnabled.parentElement.classList.remove("~urge"); - this._sendToEnabled.parentElement.classList.add("~neutral"); - if (window.discordEnabled) { - this._discordSearch.classList.remove("~urge"); - this._discordSearch.classList.add("~neutral"); - this._discordSearch.onclick = null; - } - } - } - get infiniteUses(): boolean { return this._infUses.checked; } @@ -849,8 +823,13 @@ export class createInvite { this._userMinutes.value = ""+n; } - get sendTo(): string { return this._sendTo.value; } - set sendTo(address: string) { this._sendTo.value = address; } + get sendTo(): string { + if (!(this._sendTo)) return ""; + if (this._sendTo.addresses.length > 1) console.error("FIXME: SendToDialog has collected more than one address, make them usable or fix it!"); + if (this._sendTo.addresses.length > 0) return this._sendTo.addresses[0]; + else return ""; + } + set sendTo(address: string) { if (!(this._sendTo)) return; this._sendTo.addresses = [address]; } get profile(): string { const val = this._profile.value; @@ -898,7 +877,7 @@ export class createInvite { "multiple-uses": (this.uses > 1 || this.infiniteUses), "no-limit": this.infiniteUses, "remaining-uses": this.uses, - "send-to": this.sendToEnabled ? this.sendTo : "", + "send-to": this.sendTo, "profile": this.profile, "label": this.label, "user_label": this.user_label @@ -921,7 +900,6 @@ export class createInvite { this.minutes = 30; this._infUses.onchange = () => { this.infiniteUses = this.infiniteUses; }; this.infiniteUses = false; - this._sendToEnabled.onchange = () => { this.sendToEnabled = this.sendToEnabled; }; this.userExpiry = false; this._userExpiryToggle.onchange = () => { this.userExpiry = this._userExpiryToggle.checked; } this._userMonths.disabled = true; @@ -964,22 +942,99 @@ export class createInvite { this._minutes.onchange = this._checkDurationValidity; document.addEventListener("profileLoadEvent", () => { this.loadProfiles(); }, false); - if (!window.emailEnabled && !window.discordEnabled) { - document.getElementById("create-send-to-container").classList.add("unfocused"); + const sendToContainer = document.getElementById("create-send-to-container"); + if (window.emailEnabled || window.discordEnabled) { + this._sendTo = new SendToDialog(sendToContainer); + } else { + sendToContainer.classList.add("unfocused"); } + } +} +class SendToDialog { + private _container: HTMLElement; + private _input: HTMLInputElement; + private _submit?: HTMLButtonElement; + // FIXME: Make an interface for multiple addresses + // private _addresses: string[] = []; + get addresses(): string[] { + if (this._input.value) return [this._input.value]; + return []; + } + set addresses(v: string[]) { + if (v.length > 0) this._input.value = v[0]; + // this._addresses = v; + }; + + private _search?: HTMLButtonElement; + private _discordSearch?: DiscordSearch; + + + constructor(container: HTMLElement, invite?: Invite, onSuccess?: () => void) { + this._container = container; + this._container.classList.add("flex", "flex-col", "gap-2"); + this._container.innerHTML = ` + +
+ + + +
+ `; + this._input = this._container.getElementsByClassName("send-to-dialog-input")[0] as HTMLInputElement; if (window.discordEnabled) { - this._discordSearch = document.getElementById("create-send-to-search") as HTMLSpanElement; - this._sendToDiscord = newDiscordSearch( + this._input.type = "text"; + this._input.placeholder = "example@example.com | user#1234"; + this._search = this._container.getElementsByClassName("send-to-dialog-search")[0] as HTMLButtonElement; + this._search.classList.remove("unfocused"); + this._discordSearch = newDiscordSearch( window.lang.strings("findDiscordUser"), window.lang.strings("searchDiscordUser"), window.lang.strings("select"), (user: DiscordUser) => { - this.sendTo = user.name; + this.addresses = [user.name]; + // this.addresses.push(user.name); + window.modals.discord.close(); } ); + // FIXME: Check why we're passing an empty string rather than the input value + this._search.onclick = () => this._discordSearch(""); + } + + if (invite) { + if (this._search) { + this._search.classList.add("~neutral"); + this._search.classList.remove("~urge"); + } + this._submit = this._container.getElementsByClassName("send-to-dialog-submit")[0] as HTMLButtonElement; + this._submit.classList.remove("unfocused"); + this._submit.onclick = () => { + const icon = this._submit.children[0] as HTMLElement; + addLoader(icon, true); + if (this.addresses.length == 0) return; + _post("/invites/send", {"invite": invite.code, "send-to": this.addresses[0]}, (req: XMLHttpRequest) => { + if (req.readyState != 4) return; + removeLoader(icon, true) + if (req.status != 200 && req.status != 204) { + window.notifications.customError("errorSendInvite", window.lang.notif("errorFailureCheckLogs")); + return; + } + window.notifications.customSuccess("sendInvite", window.lang.strings("sent")); + if (onSuccess) onSuccess(); + this.addresses = []; + }); + }; + this._input.addEventListener("keypress", (e: KeyboardEvent) => { + if (e.key === "Enter") { + e.preventDefault(); + this._submit.click(); + } + }) } - this.sendToEnabled = false; } } diff --git a/ts/modules/settings.ts b/ts/modules/settings.ts index 126d27a..1382beb 100644 --- a/ts/modules/settings.ts +++ b/ts/modules/settings.ts @@ -1,4 +1,4 @@ -import { _get, _post, _delete, _download, _upload, toggleLoader, addLoader, removeLoader, insertText, toClipboard, toDateString } from "../modules/common.js"; +import { _get, _post, _delete, _download, _upload, toggleLoader, addLoader, removeLoader, insertText, toClipboard, toDateString, SetupCopyButton } from "../modules/common.js"; import { Marked } from "@ts-stack/markdown"; import { stripMarkdown } from "../modules/stripmd.js"; import { PDT } from "src/data/timezoneNames"; @@ -1117,22 +1117,20 @@ export class settingsList { } for (let b of backups) { const tr = document.createElement("tr") as HTMLTableRowElement; + tr.classList.add("align-middle"); tr.innerHTML = ` - ${b.name} + ${b.name} ${toDateString(new Date(b.date*1000))} ${b.commit || "?"} - +
${b.size} - - + +
`; - tr.querySelector(".backup-copy").addEventListener("click", () => { - toClipboard(b.path); - window.notifications.customPositive("pathCopied", "", window.lang.notif("pathCopied")); - }); + SetupCopyButton(tr.querySelector(".backup-copy"), b.path, null, window.lang.notif("pathCopied")); tr.querySelector(".backup-download").addEventListener("click", () => _download("/backups/" + b.name, b.name)); tr.querySelector(".backup-restore").addEventListener("click", () => { _post("/backups/restore/"+b.name, null, () => {});