-
-
-
-
-
{{ .strings.markdownSupported }}
-
{{ .quantityStrings.addUser.Singular }}
-
{{ .strings.announce }}
+
+
{{ .strings.announce }}
+
+
+
{{ .strings.templates }}
+
+
+
+
{{ .strings.modifySettings }}
{{ .strings.extendExpiry }}
{{ .strings.disable }}
diff --git a/lang/admin/en-us.json b/lang/admin/en-us.json
index 1c53ded..16bc3cd 100644
--- a/lang/admin/en-us.json
+++ b/lang/admin/en-us.json
@@ -48,6 +48,7 @@
"unknown": "Unknown",
"label": "Label",
"announce": "Announce",
+ "templates": "Templates",
"subject": "Subject",
"message": "Message",
"variables": "Variables",
@@ -100,7 +101,10 @@
"searchDiscordUser": "Start typing the Discord username to find the user.",
"findDiscordUser": "Find Discord user",
"linkMatrixDescription": "Enter the username and password of the user to use as a bot. Once submitted, the app will restart.",
- "matrixHomeServer": "Home server address"
+ "matrixHomeServer": "Home server address",
+ "saveAsTemplate": "Save as template",
+ "deleteTemplate": "Delete template",
+ "templateEnterName": "Enter a name to save this template."
},
"notifications": {
"changedEmailAddress": "Changed email address of {n}.",
@@ -109,6 +113,7 @@
"saveSettings": "Settings were saved",
"saveEmail": "Email saved.",
"sentAnnouncement": "Announcement sent.",
+ "savedAnnouncement": "Announcement saved.",
"setOmbiDefaults": "Stored ombi defaults.",
"updateApplied": "Update applied, please restart.",
"updateAppliedRefresh": "Update applied, please refresh.",
diff --git a/main.go b/main.go
index 5162d43..736a660 100644
--- a/main.go
+++ b/main.go
@@ -359,6 +359,10 @@ func start(asDaemon, firstCall bool) {
if err := app.storage.loadMatrixUsers(); err != nil {
app.err.Printf("Failed to load Matrix users: %v", err)
}
+ app.storage.announcements_path = app.config.Section("files").Key("announcements").String()
+ if err := app.storage.loadAnnouncements(); err != nil {
+ app.err.Printf("Failed to load announcement templates: %v", err)
+ }
app.storage.profiles_path = app.config.Section("files").Key("user_profiles").String()
app.storage.loadProfiles()
diff --git a/models.go b/models.go
index 2cca9b6..0329da7 100644
--- a/models.go
+++ b/models.go
@@ -172,6 +172,16 @@ type announcementDTO struct {
Message string `json:"message"` // Email content (markdown supported)
}
+type announcementTemplate struct {
+ Name string `json:"name"` // Name of template
+ Subject string `json:"subject"` // Email subject
+ Message string `json:"message"` // Email content (markdown supported)
+}
+
+type getAnnouncementsDTO struct {
+ Announcements []string `json:"announcements"` // list of announcement names.
+}
+
type errorListDTO map[string]map[string]string
type configDTO map[string]interface{}
diff --git a/router.go b/router.go
index 720cc58..9276746 100644
--- a/router.go
+++ b/router.go
@@ -163,6 +163,12 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
// api.POST(p + "/setDefaults", app.SetDefaults)
api.POST(p+"/users/settings", app.ApplySettings)
api.POST(p+"/users/announce", app.Announce)
+
+ api.GET(p+"/users/announce", app.GetAnnounceTemplates)
+ api.POST(p+"/users/announce/template", app.SaveAnnounceTemplate)
+ api.GET(p+"/users/announce/:name", app.GetAnnounceTemplate)
+ api.DELETE(p+"/users/announce/:name", app.DeleteAnnounceTemplate)
+
api.GET(p+"/config/update", app.CheckUpdate)
api.POST(p+"/config/update", app.ApplyUpdate)
api.GET(p+"/config/emails", app.GetCustomEmails)
diff --git a/storage.go b/storage.go
index f7d8e72..9c3b777 100644
--- a/storage.go
+++ b/storage.go
@@ -15,22 +15,23 @@ import (
)
type Storage struct {
- timePattern string
- invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path, profiles_path, customEmails_path, users_path, telegram_path, discord_path, matrix_path string
- users map[string]time.Time
- invites Invites
- profiles map[string]Profile
- defaultProfile string
- displayprefs, ombi_template map[string]interface{}
- emails map[string]EmailAddress
- telegram map[string]TelegramUser // Map of Jellyfin User IDs to telegram users.
- discord map[string]DiscordUser // Map of Jellyfin user IDs to discord users.
- matrix map[string]MatrixUser // Map of Jellyfin user IDs to Matrix users.
- customEmails customEmails
- policy mediabrowser.Policy
- configuration mediabrowser.Configuration
- lang Lang
- invitesLock, usersLock sync.Mutex
+ timePattern string
+ invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path, profiles_path, customEmails_path, users_path, telegram_path, discord_path, matrix_path, announcements_path string
+ users map[string]time.Time
+ invites Invites
+ profiles map[string]Profile
+ defaultProfile string
+ displayprefs, ombi_template map[string]interface{}
+ emails map[string]EmailAddress
+ telegram map[string]TelegramUser // Map of Jellyfin User IDs to telegram users.
+ discord map[string]DiscordUser // Map of Jellyfin user IDs to discord users.
+ matrix map[string]MatrixUser // Map of Jellyfin user IDs to Matrix users.
+ customEmails customEmails
+ policy mediabrowser.Policy
+ configuration mediabrowser.Configuration
+ lang Lang
+ announcements map[string]announcementTemplate
+ invitesLock, usersLock sync.Mutex
}
type TelegramUser struct {
@@ -839,6 +840,14 @@ func (st *Storage) storeOmbiTemplate() error {
return storeJSON(st.ombi_path, st.ombi_template)
}
+func (st *Storage) loadAnnouncements() error {
+ return loadJSON(st.announcements_path, &st.announcements)
+}
+
+func (st *Storage) storeAnnouncements() error {
+ return storeJSON(st.announcements_path, st.announcements)
+}
+
func (st *Storage) loadProfiles() error {
err := loadJSON(st.profiles_path, &st.profiles)
for name, profile := range st.profiles {
diff --git a/ts/modules/accounts.ts b/ts/modules/accounts.ts
index 17d8e06..dec6c15 100644
--- a/ts/modules/accounts.ts
+++ b/ts/modules/accounts.ts
@@ -26,7 +26,13 @@ interface getPinResponse {
token: string;
username: string;
}
-
+
+interface announcementTemplate {
+ name: string;
+ subject: string;
+ message: string;
+}
+
var addDiscord: (passData: string) => void;
class user implements User {
@@ -547,6 +553,8 @@ export class accountsList {
private _addUserButton = document.getElementById("accounts-add-user") as HTMLSpanElement;
private _announceButton = document.getElementById("accounts-announce") as HTMLSpanElement;
+ private _announceSaveButton = document.getElementById("save-announce") as HTMLSpanElement;
+ private _announceNameLabel = document.getElementById("announce-name") as HTMLLabelElement;
private _announcePreview: HTMLElement;
private _previewLoaded = false;
private _announceTextarea = document.getElementById("textarea-announce") as HTMLTextAreaElement;
@@ -799,16 +807,60 @@ export class accountsList {
this._announcePreview.innerHTML = content;
}
}
- announce = () => {
+ saveAnnouncement = (event: Event) => {
+ event.preventDefault();
+ const form = document.getElementById("form-announce") as HTMLFormElement;
+ const button = form.querySelector("span.submit") as HTMLSpanElement;
+ if (this._announceNameLabel.classList.contains("unfocused")) {
+ this._announceNameLabel.classList.remove("unfocused");
+ form.onsubmit = this.saveAnnouncement;
+ button.textContent = window.lang.get("strings", "saveAsTemplate");
+ this._announceSaveButton.classList.add("unfocused");
+ const details = document.getElementById("announce-details");
+ details.classList.add("unfocused");
+ return;
+ }
+ const name = (this._announceNameLabel.querySelector("input") as HTMLInputElement).value;
+ if (!name) { return; }
+ const subject = document.getElementById("announce-subject") as HTMLInputElement;
+ let send: announcementTemplate = {
+ name: name,
+ subject: subject.value,
+ message: this._announceTextarea.value
+ }
+ _post("/users/announce/template", send, (req: XMLHttpRequest) => {
+ if (req.readyState == 4) {
+ this.reload();
+ toggleLoader(button);
+ window.modals.announce.close();
+ if (req.status != 200 && req.status != 204) {
+ window.notifications.customError("announcementError", window.lang.notif("errorFailureCheckLogs"));
+ } else {
+ window.notifications.customSuccess("announcementSuccess", window.lang.notif("savedAnnouncement"));
+ }
+ }
+ });
+ }
+ announce = (event?: Event, template?: announcementTemplate) => {
const modalHeader = document.getElementById("header-announce");
modalHeader.textContent = window.lang.quantity("announceTo", this._collectUsers().length);
const form = document.getElementById("form-announce") as HTMLFormElement;
let list = this._collectUsers();
const button = form.querySelector("span.submit") as HTMLSpanElement;
+ removeLoader(button);
+ button.textContent = window.lang.get("strings", "send");
+ const details = document.getElementById("announce-details");
+ details.classList.remove("unfocused");
+ this._announceSaveButton.classList.remove("unfocused");
const subject = document.getElementById("announce-subject") as HTMLInputElement;
-
- subject.value = "";
- this._announceTextarea.value = "";
+ this._announceNameLabel.classList.add("unfocused");
+ if (template) {
+ subject.value = template.subject;
+ this._announceTextarea.value = template.message;
+ } else {
+ subject.value = "";
+ this._announceTextarea.value = "";
+ }
form.onsubmit = (event: Event) => {
event.preventDefault();
toggleLoader(button);
@@ -853,7 +905,53 @@ export class accountsList {
}
});
}
-
+ loadTemplates = () => _get("/users/announce", null, (req: XMLHttpRequest) => {
+ if (req.readyState == 4) {
+ if (req.status != 200) {
+ this._announceButton.nextElementSibling.children[0].classList.add("unfocused");
+ return;
+ }
+ this._announceButton.nextElementSibling.children[0].classList.remove("unfocused");
+ const list = req.response["announcements"] as string[];
+ if (list.length == 0) {
+ this._announceButton.nextElementSibling.children[0].classList.add("unfocused");
+ return;
+ }
+ const dList = document.getElementById("accounts-announce-templates") as HTMLDivElement;
+ dList.textContent = '';
+ for (let name of list) {
+ const el = document.createElement("div") as HTMLDivElement;
+ el.classList.add("flex-expand", "ellipsis", "mt-half");
+ el.innerHTML = `
+
${name}×
+ `;
+ (el.querySelector("span.accounts-announce-template-button") as HTMLSpanElement).onclick = () => {
+ _get("/users/announce/" + name, null, (req: XMLHttpRequest) => {
+ if (req.readyState == 4) {
+ let template: announcementTemplate;
+ if (req.status != 200) {
+ window.notifications.customError("getTemplateError", window.lang.notif("errorFailureCheckLogs"));
+ } else {
+ template = req.response;
+ }
+ this.announce(null, template);
+ }
+ });
+ };
+ (el.querySelector("span.accounts-announce-template-delete") as HTMLSpanElement).onclick = () => {
+ _delete("/users/announce/" + name, null, (req: XMLHttpRequest) => {
+ if (req.readyState == 4) {
+ if (req.status != 200) {
+ window.notifications.customError("deleteTemplateError", window.lang.notif("errorFailureCheckLogs"));
+ }
+ this.reload();
+ }
+ });
+ };
+ dList.appendChild(el);
+ }
+ }
+ });
enableDisableUsers = () => {
// We can share the delete modal for this
const modalHeader = document.getElementById("header-delete-user");
@@ -1154,26 +1252,31 @@ export class accountsList {
}
});
});
+
+ this._announceSaveButton.onclick = this.saveAnnouncement;
}
- reload = () => _get("/users", null, (req: XMLHttpRequest) => {
- if (req.readyState == 4 && req.status == 200) {
- // same method as inviteList.reload()
- let accountsOnDOM: { [id: string]: boolean } = {};
- for (let id in this._users) { accountsOnDOM[id] = true; }
- for (let u of (req.response["users"] as User[])) {
- if (u.id in this._users) {
- this._users[u.id].update(u);
- delete accountsOnDOM[u.id];
- } else {
- this.add(u);
+ reload = () => {
+ _get("/users", null, (req: XMLHttpRequest) => {
+ if (req.readyState == 4 && req.status == 200) {
+ // same method as inviteList.reload()
+ let accountsOnDOM: { [id: string]: boolean } = {};
+ for (let id in this._users) { accountsOnDOM[id] = true; }
+ for (let u of (req.response["users"] as User[])) {
+ if (u.id in this._users) {
+ this._users[u.id].update(u);
+ delete accountsOnDOM[u.id];
+ } else {
+ this.add(u);
+ }
}
+ for (let id in accountsOnDOM) {
+ this._users[id].remove();
+ delete this._users[id];
+ }
+ this._checkCheckCount();
}
- for (let id in accountsOnDOM) {
- this._users[id].remove();
- delete this._users[id];
- }
- this._checkCheckCount();
- }
- })
+ });
+ this.loadTemplates();
+ }
}