invites: add /invites/send, store more details in new SentTo field

deprecated SendTo string field for SentTo, which holds successful send
addresses, and failures with a reason that isn't plain text. Will soon
add an interface for sending invites after their creation. For #444
(ha).
This commit is contained in:
Harvey Tindall
2025-12-05 12:03:21 +00:00
parent 3635a13682
commit 5fa528fd2d
19 changed files with 327 additions and 144 deletions

View File

@@ -2,7 +2,7 @@ import { ThemeManager } from "./modules/theme.js";
import { lang, LangFile, loadLangSelector } from "./modules/lang.js";
import { Modal } from "./modules/modal.js";
import { Tabs, Tab } from "./modules/tabs.js";
import { inviteList, createInvite } from "./modules/invites.js";
import { DOMInviteList, createInvite } from "./modules/invites.js";
import { accountsList } from "./modules/accounts.js";
import { settingsList } from "./modules/settings.js";
import { activityList } from "./modules/activity.js";
@@ -105,7 +105,7 @@ var accounts = new accountsList();
var activity = new activityList();
window.invites = new inviteList();
window.invites = new DOMInviteList();
var settings = new settingsList();

View File

@@ -1247,6 +1247,12 @@ export class accountsList extends PaginatedList {
this._search.showHideSearchOptionsHeader();
this.registerURLListener();
// Get rid of nasty CSS
window.modals.announce.onclose = () => {
const preview = document.getElementById("announce-preview") as HTMLDivElement;
preview.textContent = ``;
}
}
reload = (callback?: (resp: paginatedDTO) => void) => {

View File

@@ -123,20 +123,55 @@ class DOMInvite implements Invite {
} else {
chip.classList.add("button");
chip.parentElement.classList.add("h-full");
if (address.includes("Failed")) {
if (address.includes(window.lang.strings("failed"))) {
icon.classList.remove("ri-mail-line");
icon.classList.add("ri-mail-close-line");
chip.classList.remove("~neutral");
chip.classList.add("~critical");
} else {
address = "Sent to " + address;
icon.classList.remove("ri-mail-close-line");
icon.classList.add("ri-mail-line");
chip.classList.remove("~critical");
chip.classList.add("~neutral");
}
}
tooltip.textContent = address;
// innerHTML as the newer sent_to re-uses this with HTML.
tooltip.innerHTML = address;
}
private _sent_to: SentToList;
get sent_to(): SentToList { return this._sent_to; }
set sent_to(v: SentToList) {
this._sent_to = v;
if (!v || !(v.success || v.failed)) return;
let text = "";
if (v.success && v.success.length > 0) {
text += window.lang.strings("sentTo") + ": " + v.success.join(", ") + " <br>"
}
if (v.failed && v.failed.length > 0) {
text += window.lang.strings("failed") + ": " + v.failed.map((el: SendFailure) => {
let err: string;
switch (el.reason) {
case "CheckLogs":
err = window.lang.notif("errorCheckLogs");
break;
case "NoUser":
err = window.lang.notif("errorNoUser");
break;
case "MultiUser":
err = window.lang.notif("errorMultiUser");
break;
case "InvalidAddress":
err = window.lang.notif("errorInvalidAddress");
break;
default:
err = el.reason;
break;
}
return el.address + " (" + err + ")";
}).join(", ");
}
if (text.length != 0) this.send_to = text;
}
private _usedBy: { [name: string]: number };
@@ -455,6 +490,7 @@ class DOMInvite implements Invite {
this.code = invite.code;
this.created = invite.created;
this.send_to = invite.send_to;
this.sent_to = invite.sent_to;
this.expiresIn = invite.expiresIn;
if (window.notificationsEnabled) {
this.notifyCreation = invite.notifyCreation;
@@ -477,7 +513,7 @@ class DOMInvite implements Invite {
remove = () => { this._container.remove(); }
}
export class inviteList implements inviteList {
export class DOMInviteList implements InviteList {
private _list: HTMLDivElement;
private _empty: boolean;
// since invite reload sends profiles, this event it broadcast so the createInvite object can load them.
@@ -493,7 +529,7 @@ export class inviteList implements inviteList {
};
public static readonly _inviteURLEvent = "invite-url";
registerURLListener = () => document.addEventListener(inviteList._inviteURLEvent, (event: CustomEvent) => {
registerURLListener = () => document.addEventListener(DOMInviteList._inviteURLEvent, (event: CustomEvent) => {
this.focusInvite(event.detail);
})
@@ -587,12 +623,14 @@ export class inviteList implements inviteList {
}));
}
export const inviteURLEvent = (id: string) => { return new CustomEvent(inviteList._inviteURLEvent, {"detail": id}) };
export const inviteURLEvent = (id: string) => { return new CustomEvent(DOMInviteList._inviteURLEvent, {"detail": id}) };
function parseInvite(invite: { [f: string]: string | number | { [name: string]: number } | boolean }): Invite {
// FIXME: Please, i beg you, get rid of this horror!
function parseInvite(invite: { [f: string]: string | number | { [name: string]: number } | boolean | SentToList }): Invite {
let parsed: Invite = {};
parsed.code = invite["code"] as string;
parsed.send_to = invite["send_to"] as string || "";
parsed.sent_to = invite["sent_to"] as SentToList || null;
parsed.label = invite["label"] as string || "";
parsed.user_label = invite["user_label"] as string || "";
let time = "";

View File

@@ -1,4 +1,5 @@
import { _get } from "../modules/common.js";
import { Template } from "@hrfee/simpletemplate";
interface Meta {
name: string;
@@ -39,6 +40,15 @@ export class lang implements Lang {
return str;
}
template = (sect: string, key: string, subs: { [key: string]: any }): string => {
if (sect == "quantityStrings" || sect == "meta") { return ""; }
const map = new Map<string, any>();
for (let key of Object.keys(subs)) { map.set(key, subs[key]); }
const [out, err] = Template(this._lang[sect][key], map);
if (err != null) throw err;
return out;
}
quantity = (key: string, number: number): string => {
if (number == 1) {
return this._lang.quantityStrings[key].singular.replace("{n}", ""+number)

View File

@@ -1626,7 +1626,8 @@ class MessageEditor {
} else {
for (let i = this._templ.conditionals.length-1; i >= 0; i--) {
let ci = i % colors.length;
innerHTML += '<span class="button ~' + colors[ci] +' @low mb-4" style="margin-left: 0.25rem; margin-right: 0.25rem;"></span>'
// FIXME: Store full color strings (with ~) so tailwind sees them.
innerHTML += '<span class="button ~' + colors[ci] +' @low"></span>'
}
this._conditionalsLabel.classList.remove("unfocused");
this._conditionals.innerHTML = innerHTML
@@ -1754,6 +1755,20 @@ class MessageEditor {
}
});
};
const descriptions = document.getElementsByClassName("editor-syntax-description") as HTMLCollectionOf<HTMLParagraphElement>;
for (let el of descriptions) {
el.innerHTML = window.lang.template("strings", "syntaxDescription", {
"variable": `<span class="font-mono font-bold">{varname}</span>`,
"ifTruth": `<span class="font-mono font-bold">{if address}Message sent to {address}{end}</span>`,
"ifCompare": `<span class="font-mono font-bold">{if profile == "Friends"}Friend{else if profile != "Admins"}User{end}</span>`
});
};
// Get rid of nasty CSS
window.modals.editor.onclose = () => {
this._preview.textContent = ``;
}
}
}

View File

@@ -48,7 +48,7 @@ declare interface GlobalWindow extends Window {
transitionEvent: string;
animationEvent: string;
tabs: Tabs;
invites: inviteList;
invites: InviteList;
notifications: NotificationBox;
language: string;
lang: Lang;
@@ -61,6 +61,42 @@ declare interface GlobalWindow extends Window {
loginAppearance: string;
}
declare interface InviteList {
empty: boolean;
invites: { [code: string]: Invite }
add: (invite: Invite) => void;
reload: (callback?: () => void) => void;
isInviteURL: () => boolean;
loadInviteURL: () => void;
}
declare interface Invite {
code?: string;
expiresIn?: string;
remainingUses?: string;
send_to?: string; // DEPRECATED: use sent_to instead.
sent_to?: SentToList;
usedBy?: { [name: string]: number };
created?: number;
notifyExpiry?: boolean;
notifyCreation?: boolean;
profile?: string;
label?: string;
user_label?: string;
userExpiry?: boolean;
userExpiryTime?: string;
}
declare interface SendFailure {
address: string;
reason: "CheckLogs" | "NoUser" | "MultiUser" | "InvalidAddress";
}
declare interface SentToList {
success: string[];
failed: SendFailure[];
}
declare interface Update {
version: string;
commit: string;
@@ -83,6 +119,7 @@ declare interface Lang {
strings: (key: string) => string;
notif: (key: string) => string;
var: (sect: string, key: string, ...subs: string[]) => string;
template: (sect: string, key: string, subs: { [key: string]: string }) => string;
quantity: (key: string, number: number) => string;
}
@@ -131,31 +168,6 @@ declare interface Modals {
backups?: Modal;
}
interface Invite {
code?: string;
expiresIn?: string;
remainingUses?: string;
send_to?: string;
usedBy?: { [name: string]: number };
created?: number;
notifyExpiry?: boolean;
notifyCreation?: boolean;
profile?: string;
label?: string;
user_label?: string;
userExpiry?: boolean;
userExpiryTime?: string;
}
interface inviteList {
empty: boolean;
invites: { [code: string]: Invite }
add: (invite: Invite) => void;
reload: (callback?: () => void) => void;
isInviteURL: () => boolean;
loadInviteURL: () => void;
}
interface paginatedDTO {
last_page: boolean;
}