@@ -644,6 +693,9 @@
{{ if .discordEnabled }}
{{ end }}
+ {{ if .referralsEnabled }}
+
+ {{ end }}
diff --git a/html/form.html b/html/form.html
index 6bd2638..787cdb0 100644
--- a/html/form.html
+++ b/html/form.html
@@ -43,7 +43,7 @@
-
+
{{ if .passwordReset }}
{{ .strings.passwordReset }}
@@ -53,11 +53,14 @@
{{ if .passwordReset }}
- {{ .strings.enterYourPassword }}
+ {{ .strings.enterYourPassword }}
{{ else }}
- {{ .helpMessage }}
+ {{ .helpMessage }}
{{ end }}
+ {{ if .fromUser }}
+ {{ .strings.invitedBy }} {{ .fromUser }}
+ {{ end }}
diff --git a/html/user.html b/html/user.html
index e073cf2..5753fc1 100644
--- a/html/user.html
+++ b/html/user.html
@@ -24,6 +24,7 @@
window.matrixRequired = {{ .matrixRequired }};
window.matrixUserID = "{{ .matrixUser }}";
window.validationStrings = JSON.parse({{ .validationStrings }});
+ window.referralsEnabled = {{ .referralsEnabled }};
{{ template "header.html" . }}
{{ .strings.myAccount }}
@@ -150,6 +151,20 @@
+ {{ if .referralsEnabled }}
+
+
+
{{ .strings.referrals }}
+
+
+
+
+
+
+
+
+
+ {{ end }}
diff --git a/lang/admin/da-dk.json b/lang/admin/da-dk.json
index da0e967..b147dc0 100644
--- a/lang/admin/da-dk.json
+++ b/lang/admin/da-dk.json
@@ -79,7 +79,6 @@
"inviteUsersCreated": "Oprettet brugere",
"inviteNoProfile": "Ingen Profil",
"inviteDateCreated": "Oprettet",
- "inviteRemainingUses": "Resterende anvendelser",
"inviteNoInvites": "Ingen",
"inviteExpiresInTime": "Udløber om {n}",
"notifyEvent": "Meddel den:",
diff --git a/lang/admin/de-de.json b/lang/admin/de-de.json
index bbcf259..79fb2aa 100644
--- a/lang/admin/de-de.json
+++ b/lang/admin/de-de.json
@@ -53,7 +53,6 @@
"inviteUsersCreated": "Erstellte Benutzer",
"inviteNoProfile": "Kein Profil",
"inviteDateCreated": "Erstellt",
- "inviteRemainingUses": "Verbleibende Verwendungen",
"inviteNoInvites": "Keine",
"inviteExpiresInTime": "Läuft in {n} ab",
"notifyEvent": "Benachrichtigen bei:",
diff --git a/lang/admin/el-gr.json b/lang/admin/el-gr.json
index e84f19b..1a2e369 100644
--- a/lang/admin/el-gr.json
+++ b/lang/admin/el-gr.json
@@ -56,7 +56,6 @@
"inviteUsersCreated": "Δημιουργηθέντες χρήστες",
"inviteNoProfile": "Κανένα Προφίλ",
"inviteDateCreated": "Δημιουργηθέντα",
- "inviteRemainingUses": "Εναπομείναντες χρήσεις",
"inviteNoInvites": "Καμία",
"inviteExpiresInTime": "Λήγει σε {n}",
"notifyEvent": "Ενημέρωση όταν:",
diff --git a/lang/admin/en-gb.json b/lang/admin/en-gb.json
index d716caa..cee85b9 100644
--- a/lang/admin/en-gb.json
+++ b/lang/admin/en-gb.json
@@ -124,7 +124,6 @@
"addProfileStoreHomescreenLayout": "Store homescreen layout",
"inviteNoUsersCreated": "None yet!",
"inviteUsersCreated": "Created users",
- "inviteRemainingUses": "Remaining uses",
"inviteNoInvites": "None",
"inviteExpiresInTime": "Expires in {n}",
"notifyEvent": "Notify on:",
diff --git a/lang/admin/en-us.json b/lang/admin/en-us.json
index db9773f..2e3169c 100644
--- a/lang/admin/en-us.json
+++ b/lang/admin/en-us.json
@@ -4,6 +4,7 @@
},
"strings": {
"invites": "Invites",
+ "invite": "Invite",
"accounts": "Accounts",
"settings": "Settings",
"inviteMonths": "Months",
@@ -63,6 +64,10 @@
"markdownSupported": "Markdown is supported.",
"modifySettings": "Modify Settings",
"modifySettingsDescription": "Apply settings from an existing profile, or source them directly from a user.",
+ "enableReferrals": "Enable Referrals",
+ "disableReferrals": "Disable Referrals",
+ "enableReferralsDescription": "Give users a personal referral link similiar to an invite, to send to friends/family. Can be sourced from a referral template in a profile, or from an existing invite.",
+ "enableReferralsProfileDescription": "Give users created with this profile a personal referral link similiar to an invite, to send to friends/family. Create an invite with the desired settings, then select it here. Each referral will then be based on this invite. You can delete the invite once complete.",
"applyHomescreenLayout": "Apply homescreen layout",
"sendDeleteNotificationEmail": "Send notification message",
"sendDeleteNotifiationExample": "Your account has been deleted.",
@@ -90,7 +95,6 @@
"inviteUsersCreated": "Created users",
"inviteNoProfile": "No Profile",
"inviteDateCreated": "Created",
- "inviteRemainingUses": "Remaining uses",
"inviteNoInvites": "None",
"inviteExpiresInTime": "Expires in {n}",
"notifyEvent": "Notify on:",
@@ -132,6 +136,7 @@
"updateAppliedRefresh": "Update applied, please refresh.",
"telegramVerified": "Telegram account verified.",
"accountConnected": "Account connected.",
+ "referralsEnabled": "Referrals enabled.",
"errorSettingsAppliedNoHomescreenLayout": "Settings were applied, but applying homescreen layout may have failed.",
"errorHomescreenAppliedNoSettings": "Homescreen layout was applied, but applying settings may have failed.",
"errorSettingsFailed": "Application failed.",
@@ -152,6 +157,7 @@
"errorSendWelcomeEmail": "Failed to send welcome message (check console/logs)",
"errorApplyUpdate": "Failed to apply update, try manually.",
"errorCheckUpdate": "Failed to check for update.",
+ "errorNoReferralTemplate": "Profile doesn't contain referral template, add one in settings.",
"updateAvailable": "A new update is available, check settings.",
"noUpdatesAvailable": "No new updates available."
},
@@ -160,6 +166,10 @@
"singular": "Modify Settings for {n} user",
"plural": "Modify Settings for {n} users"
},
+ "enableReferralsFor": {
+ "singular": "Enable Referrals for {n} user",
+ "plural": "Enable Referrals for {n} users"
+ },
"deleteNUsers": {
"singular": "Delete {n} user",
"plural": "Delete {n} users"
@@ -213,4 +223,4 @@
"plural": "Extended expiry for {n} users."
}
}
-}
+}
\ No newline at end of file
diff --git a/lang/admin/es-es.json b/lang/admin/es-es.json
index 1506e76..536022d 100644
--- a/lang/admin/es-es.json
+++ b/lang/admin/es-es.json
@@ -75,7 +75,6 @@
"inviteUsersCreated": "Usuarios creados",
"inviteNoProfile": "Sin perfil",
"inviteDateCreated": "Creado",
- "inviteRemainingUses": "Usos restantes",
"inviteNoInvites": "Ninguno",
"inviteExpiresInTime": "Caduca en {n}",
"notifyEvent": "Notificar en:",
diff --git a/lang/admin/fr-fr.json b/lang/admin/fr-fr.json
index cb8eb5a..01a1b54 100644
--- a/lang/admin/fr-fr.json
+++ b/lang/admin/fr-fr.json
@@ -55,7 +55,6 @@
"inviteUsersCreated": "Utilisateurs créés",
"inviteNoProfile": "Aucun profil",
"inviteDateCreated": "Créer",
- "inviteRemainingUses": "Utilisations restantes",
"inviteNoInvites": "Aucune",
"inviteExpiresInTime": "Expires dans {n}",
"notifyEvent": "Notifier sur :",
diff --git a/lang/admin/hu-hu.json b/lang/admin/hu-hu.json
index 4161ef0..dca6069 100644
--- a/lang/admin/hu-hu.json
+++ b/lang/admin/hu-hu.json
@@ -87,7 +87,6 @@
"inviteUsersCreated": "",
"inviteNoProfile": "",
"inviteDateCreated": "",
- "inviteRemainingUses": "",
"inviteNoInvites": "",
"inviteExpiresInTime": "",
"notifyEvent": "",
diff --git a/lang/admin/id-id.json b/lang/admin/id-id.json
index 5957396..365ed65 100644
--- a/lang/admin/id-id.json
+++ b/lang/admin/id-id.json
@@ -56,7 +56,6 @@
"inviteUsersCreated": "Pengguna yang telah dibuat",
"inviteNoProfile": "Tidak ada profil",
"inviteDateCreated": "Dibuat",
- "inviteRemainingUses": "Penggunaan yang tersisa",
"inviteNoInvites": "Tidak ada",
"inviteExpiresInTime": "Kadaluarsa dalam {n}",
"notifyEvent": "Beritahu pada:",
diff --git a/lang/admin/nl-nl.json b/lang/admin/nl-nl.json
index 46b9452..a8e9172 100644
--- a/lang/admin/nl-nl.json
+++ b/lang/admin/nl-nl.json
@@ -53,7 +53,6 @@
"inviteUsersCreated": "Aangemaakte gebruikers",
"inviteNoProfile": "Geen profiel",
"inviteDateCreated": "Aangemaakt",
- "inviteRemainingUses": "Resterend aantal keer te gebruiken",
"inviteNoInvites": "Geen",
"inviteExpiresInTime": "Verloopt over {n}",
"notifyEvent": "Meldingen:",
diff --git a/lang/admin/pl-pl.json b/lang/admin/pl-pl.json
index 6b1bdc2..fddce21 100644
--- a/lang/admin/pl-pl.json
+++ b/lang/admin/pl-pl.json
@@ -87,7 +87,6 @@
"inviteUsersCreated": "",
"inviteNoProfile": "",
"inviteDateCreated": "Utworzone",
- "inviteRemainingUses": "",
"inviteNoInvites": "",
"inviteExpiresInTime": "",
"notifyEvent": "",
diff --git a/lang/admin/pt-br.json b/lang/admin/pt-br.json
index 0310937..37b63eb 100644
--- a/lang/admin/pt-br.json
+++ b/lang/admin/pt-br.json
@@ -54,7 +54,6 @@
"inviteUsersCreated": "Usuários criado",
"inviteNoProfile": "Sem Perfil",
"inviteDateCreated": "Criado",
- "inviteRemainingUses": "Uso restantes",
"inviteNoInvites": "Nenhum",
"inviteExpiresInTime": "Expira em {n}",
"notifyEvent": "Notificar em:",
diff --git a/lang/admin/sv-se.json b/lang/admin/sv-se.json
index 1f7e1f1..823167c 100644
--- a/lang/admin/sv-se.json
+++ b/lang/admin/sv-se.json
@@ -65,7 +65,6 @@
"inviteUsersCreated": "Skapade användare",
"inviteNoProfile": "Ingen profil",
"inviteDateCreated": "Skapad",
- "inviteRemainingUses": "Återstående användningar",
"inviteNoInvites": "Ingen",
"inviteExpiresInTime": "Går ut om {n}",
"notifyEvent": "Meddela den:",
diff --git a/lang/admin/vi-vn.json b/lang/admin/vi-vn.json
index 7dd7e56..3ad6ba2 100644
--- a/lang/admin/vi-vn.json
+++ b/lang/admin/vi-vn.json
@@ -86,7 +86,6 @@
"inviteUsersCreated": "Người dùng đã tạo",
"inviteNoProfile": "Không có Tài khoản mẫu",
"inviteDateCreated": "Tạo",
- "inviteRemainingUses": "Số lần sử dụng còn lại",
"inviteNoInvites": "Không có",
"inviteExpiresInTime": "Hết hạn trong {n}",
"notifyEvent": "Thông báo khi:",
diff --git a/lang/admin/zh-hans.json b/lang/admin/zh-hans.json
index 6190c8f..0309721 100644
--- a/lang/admin/zh-hans.json
+++ b/lang/admin/zh-hans.json
@@ -80,7 +80,6 @@
"inviteUsersCreated": "已创建的用户",
"inviteNoProfile": "没有个人资料",
"inviteDateCreated": "已创建",
- "inviteRemainingUses": "剩余使用次数",
"inviteNoInvites": "无",
"inviteExpiresInTime": "在 {n} 到期",
"notifyEvent": "通知:",
diff --git a/lang/admin/zh-hant.json b/lang/admin/zh-hant.json
index 5546e09..2cbf43b 100644
--- a/lang/admin/zh-hant.json
+++ b/lang/admin/zh-hant.json
@@ -87,7 +87,6 @@
"inviteUsersCreated": "創建的帳戶",
"inviteNoProfile": "無資料",
"inviteDateCreated": "已創建",
- "inviteRemainingUses": "剩餘使用次數",
"inviteNoInvites": "無",
"inviteExpiresInTime": "在 {n} 到期",
"notifyEvent": "通知:",
diff --git a/lang/common/da-dk.json b/lang/common/da-dk.json
index ce3fb1d..3a196a7 100644
--- a/lang/common/da-dk.json
+++ b/lang/common/da-dk.json
@@ -35,7 +35,8 @@
"expiry": "Udløb",
"add": "Tilføj",
"edit": "Rediger",
- "delete": "Slet"
+ "delete": "Slet",
+ "inviteRemainingUses": "Resterende anvendelser"
},
"notifications": {
"errorLoginBlank": "Brugernavnet og/eller adgangskoden blev efterladt tomme.",
diff --git a/lang/common/de-de.json b/lang/common/de-de.json
index 6459eef..024a4de 100644
--- a/lang/common/de-de.json
+++ b/lang/common/de-de.json
@@ -35,7 +35,8 @@
"expiry": "Ablaufdatum",
"add": "Hinzufügen",
"edit": "Bearbeiten",
- "delete": "Löschen"
+ "delete": "Löschen",
+ "inviteRemainingUses": "Verbleibende Verwendungen"
},
"notifications": {
"errorLoginBlank": "Der Benutzername und/oder das Passwort wurden nicht ausgefüllt.",
diff --git a/lang/common/el-gr.json b/lang/common/el-gr.json
index 3c0c5c9..8a06da8 100644
--- a/lang/common/el-gr.json
+++ b/lang/common/el-gr.json
@@ -25,7 +25,8 @@
"disable": "Απενεργοποίηση",
"expiry": "Λήξη",
"edit": "Επεξεργασία",
- "delete": "Διαγραφή"
+ "delete": "Διαγραφή",
+ "inviteRemainingUses": "Εναπομείναντες χρήσεις"
},
"notifications": {
"errorLoginBlank": "Το όνομα χρήστη και/ή ο κωδικός ήταν κενά.",
diff --git a/lang/common/en-gb.json b/lang/common/en-gb.json
index ff6147b..6283091 100644
--- a/lang/common/en-gb.json
+++ b/lang/common/en-gb.json
@@ -35,7 +35,8 @@
"expiry": "Expiry",
"add": "Add",
"edit": "Edit",
- "delete": "Delete"
+ "delete": "Delete",
+ "inviteRemainingUses": "Remaining uses"
},
"notifications": {
"errorLoginBlank": "The username and/or password was left blank.",
diff --git a/lang/common/en-us.json b/lang/common/en-us.json
index 43539db..57e4046 100644
--- a/lang/common/en-us.json
+++ b/lang/common/en-us.json
@@ -39,7 +39,9 @@
"add": "Add",
"edit": "Edit",
"delete": "Delete",
- "myAccount": "My Account"
+ "myAccount": "My Account",
+ "referrals": "Referrals",
+ "inviteRemainingUses": "Remaining uses"
},
"notifications": {
"errorLoginBlank": "The username and/or password were left blank.",
@@ -62,4 +64,4 @@
"plural": "{n} Days"
}
}
-}
+}
\ No newline at end of file
diff --git a/lang/common/es-es.json b/lang/common/es-es.json
index f0bec53..f28e38d 100644
--- a/lang/common/es-es.json
+++ b/lang/common/es-es.json
@@ -35,7 +35,8 @@
"expiry": "Expiración",
"add": "Agregar",
"edit": "Editar",
- "delete": "Eliminar"
+ "delete": "Eliminar",
+ "inviteRemainingUses": "Usos restantes"
},
"notifications": {
"errorLoginBlank": "El nombre de usuario y/o la contraseña se dejaron en blanco.",
diff --git a/lang/common/fr-fr.json b/lang/common/fr-fr.json
index d7dfd5c..78abba9 100644
--- a/lang/common/fr-fr.json
+++ b/lang/common/fr-fr.json
@@ -35,7 +35,8 @@
"expiry": "Expiration",
"add": "Ajouter",
"edit": "Éditer",
- "delete": "Effacer"
+ "delete": "Effacer",
+ "inviteRemainingUses": "Utilisations restantes"
},
"notifications": {
"errorLoginBlank": "Le nom d'utilisateur et/ou le mot de passe sont vides.",
diff --git a/lang/common/id-id.json b/lang/common/id-id.json
index e8f4e9b..c6b10b5 100644
--- a/lang/common/id-id.json
+++ b/lang/common/id-id.json
@@ -19,7 +19,8 @@
"login": "Masuk",
"logout": "Keluar",
"edit": "Edit",
- "delete": "Hapus"
+ "delete": "Hapus",
+ "inviteRemainingUses": "Penggunaan yang tersisa"
},
"notifications": {
"errorLoginBlank": "Nama pengguna dan / atau sandi kosong.",
diff --git a/lang/common/it-it.json b/lang/common/it-it.json
index 58d5021..9a5db2d 100644
--- a/lang/common/it-it.json
+++ b/lang/common/it-it.json
@@ -28,4 +28,4 @@
},
"notifications": {},
"quantityStrings": {}
-}
+}
\ No newline at end of file
diff --git a/lang/common/nds.json b/lang/common/nds.json
new file mode 100644
index 0000000..a7f6836
--- /dev/null
+++ b/lang/common/nds.json
@@ -0,0 +1,8 @@
+{
+ "meta": {
+ "name": "Nedderdütsch (NDS)"
+ },
+ "strings": {},
+ "notifications": {},
+ "quantityStrings": {}
+}
\ No newline at end of file
diff --git a/lang/common/nl-nl.json b/lang/common/nl-nl.json
index 0591de1..cb5213d 100644
--- a/lang/common/nl-nl.json
+++ b/lang/common/nl-nl.json
@@ -35,7 +35,8 @@
"expiry": "Verloop",
"add": "Voeg toe",
"edit": "Bewerken",
- "delete": "Verwijderen"
+ "delete": "Verwijderen",
+ "inviteRemainingUses": "Resterend aantal keer te gebruiken"
},
"notifications": {
"errorLoginBlank": "De gebruikersnaam en/of wachtwoord is leeg.",
diff --git a/lang/common/pt-br.json b/lang/common/pt-br.json
index b7953fa..006bbb6 100644
--- a/lang/common/pt-br.json
+++ b/lang/common/pt-br.json
@@ -35,7 +35,8 @@
"expiry": "Expira",
"add": "Adicionar",
"edit": "Editar",
- "delete": "Deletar"
+ "delete": "Deletar",
+ "inviteRemainingUses": "Uso restantes"
},
"notifications": {
"errorLoginBlank": "O nome de usuário e/ou senha foram deixados em branco.",
diff --git a/lang/common/sl-si.json b/lang/common/sl-si.json
index 95187bd..9ecaecd 100644
--- a/lang/common/sl-si.json
+++ b/lang/common/sl-si.json
@@ -28,4 +28,4 @@
},
"notifications": {},
"quantityStrings": {}
-}
+}
\ No newline at end of file
diff --git a/lang/common/sv-se.json b/lang/common/sv-se.json
index 0eb8b5f..ff602c8 100644
--- a/lang/common/sv-se.json
+++ b/lang/common/sv-se.json
@@ -22,7 +22,8 @@
"disabled": "Inaktiverad",
"expiry": "Löper ut",
"edit": "Redigera",
- "delete": "Radera"
+ "delete": "Radera",
+ "inviteRemainingUses": "Återstående användningar"
},
"notifications": {
"errorLoginBlank": "Användarnamnet och/eller lösenordet lämnades tomt.",
diff --git a/lang/common/vi-vn.json b/lang/common/vi-vn.json
index 3a8abbd..f4584ed 100644
--- a/lang/common/vi-vn.json
+++ b/lang/common/vi-vn.json
@@ -13,7 +13,8 @@
"expiry": "Hết hạn",
"add": "Thêm",
"edit": "Chỉnh sửa",
- "delete": "Xóa"
+ "delete": "Xóa",
+ "inviteRemainingUses": "Số lần sử dụng còn lại"
},
"notifications": {
"errorConnection": "Không thể kết nối với jfa-go.",
diff --git a/lang/common/zh-hans.json b/lang/common/zh-hans.json
index 2d12e74..faab5b4 100644
--- a/lang/common/zh-hans.json
+++ b/lang/common/zh-hans.json
@@ -35,7 +35,8 @@
"expiry": "到期",
"add": "添加",
"edit": "编辑",
- "delete": "删除"
+ "delete": "删除",
+ "inviteRemainingUses": "剩余使用次数"
},
"notifications": {
"errorLoginBlank": "用户名/密码留空。",
diff --git a/lang/common/zh-hant.json b/lang/common/zh-hant.json
index 8b0b42f..87c4409 100644
--- a/lang/common/zh-hant.json
+++ b/lang/common/zh-hant.json
@@ -35,7 +35,8 @@
"expiry": "到期",
"add": "添加",
"edit": "編輯",
- "delete": "刪除"
+ "delete": "刪除",
+ "inviteRemainingUses": "剩餘使用次數"
},
"notifications": {
"errorLoginBlank": "帳戶名稱和/或密碼留空。",
diff --git a/lang/email/it-it.json b/lang/email/it-it.json
index fe844bc..89b12b8 100644
--- a/lang/email/it-it.json
+++ b/lang/email/it-it.json
@@ -49,4 +49,4 @@
"clickBelow": "",
"confirmEmail": ""
}
-}
+}
\ No newline at end of file
diff --git a/lang/form/en-us.json b/lang/form/en-us.json
index 5fd7b4c..14f7acf 100644
--- a/lang/form/en-us.json
+++ b/lang/form/en-us.json
@@ -34,7 +34,10 @@
"resetPasswordThroughLink": "To reset your password, enter your username, email address or a linked contact method username, and submit. A link will be sent to reset your password.",
"resetSent": "Reset Sent.",
"resetSentDescription": "If an account with the given username/contact method exists, a password reset link has been sent via all contact methods available. The code will expire in 30 minutes.",
- "changePassword": "Change Password"
+ "changePassword": "Change Password",
+ "referralsDescription": "Invite friends & family to Jellyfin with this link. Come back here for a new one if it expires.",
+ "copyReferral": "Copy Link",
+ "invitedBy": "Invited By"
},
"notifications": {
"errorUserExists": "User already exists.",
diff --git a/lang/form/it-it.json b/lang/form/it-it.json
index aace6a0..1a7c9ab 100644
--- a/lang/form/it-it.json
+++ b/lang/form/it-it.json
@@ -48,4 +48,4 @@
"plural": "Deve avere almeno {n} caratteri speciali"
}
}
-}
+}
\ No newline at end of file
diff --git a/lang/form/sl-si.json b/lang/form/sl-si.json
index fff664a..46dadbb 100644
--- a/lang/form/sl-si.json
+++ b/lang/form/sl-si.json
@@ -57,4 +57,4 @@
"plural": "Potrebnih je vsaj {n} posebnih znakov"
}
}
-}
+}
\ No newline at end of file
diff --git a/lang/pwreset/sl-si.json b/lang/pwreset/sl-si.json
index f867af0..2168146 100644
--- a/lang/pwreset/sl-si.json
+++ b/lang/pwreset/sl-si.json
@@ -13,4 +13,4 @@
"changeYourPassword": "Spremenite svoje geslo po prijavi.",
"enterYourPassword": "Vnesite svoje novo geslo spodaj."
}
-}
+}
\ No newline at end of file
diff --git a/lang/setup/en-us.json b/lang/setup/en-us.json
index eac01bf..1159d34 100644
--- a/lang/setup/en-us.json
+++ b/lang/setup/en-us.json
@@ -157,4 +157,4 @@
"emailMessage": "Email Message",
"emailMessageNotice": "Displays at the bottom of emails."
}
-}
+}
\ No newline at end of file
diff --git a/lang/setup/nds.json b/lang/setup/nds.json
index d45cc97..3cd0f7b 100644
--- a/lang/setup/nds.json
+++ b/lang/setup/nds.json
@@ -149,4 +149,4 @@
"emailMessage": "",
"emailMessageNotice": ""
}
-}
+}
\ No newline at end of file
diff --git a/lang/setup/sl-si.json b/lang/setup/sl-si.json
index 9d7a172..0b770ea 100644
--- a/lang/setup/sl-si.json
+++ b/lang/setup/sl-si.json
@@ -146,4 +146,4 @@
"emailMessage": "",
"emailMessageNotice": ""
}
-}
+}
\ No newline at end of file
diff --git a/lang/telegram/it-it.json b/lang/telegram/it-it.json
index 3273db0..c03f59c 100644
--- a/lang/telegram/it-it.json
+++ b/lang/telegram/it-it.json
@@ -13,4 +13,4 @@
"languageSet": "",
"discordDMs": ""
}
-}
+}
\ No newline at end of file
diff --git a/lang/telegram/nds.json b/lang/telegram/nds.json
index 2b8ad0a..26d8053 100644
--- a/lang/telegram/nds.json
+++ b/lang/telegram/nds.json
@@ -13,4 +13,4 @@
"languageSet": "",
"discordDMs": ""
}
-}
+}
\ No newline at end of file
diff --git a/lang/telegram/sl-si.json b/lang/telegram/sl-si.json
index 96a211d..616961d 100644
--- a/lang/telegram/sl-si.json
+++ b/lang/telegram/sl-si.json
@@ -13,4 +13,4 @@
"languageSet": "Jezik nastavljen na {language}.",
"discordDMs": "Prosimo preverite svoja zasebna sporočila za odziv."
}
-}
+}
\ No newline at end of file
diff --git a/models.go b/models.go
index 7120d06..d9ac8aa 100644
--- a/models.go
+++ b/models.go
@@ -71,10 +71,11 @@ type inviteProfileDTO struct {
}
type profileDTO struct {
- Admin bool `json:"admin" example:"false"` // Whether profile has admin rights or not
- LibraryAccess string `json:"libraries" example:"all"` // Number of libraries profile has access to
- FromUser string `json:"fromUser" example:"jeff"` // The user the profile is based on
- Ombi bool `json:"ombi"` // Whether or not Ombi settings are stored in this profile.
+ Admin bool `json:"admin" example:"false"` // Whether profile has admin rights or not
+ LibraryAccess string `json:"libraries" example:"all"` // Number of libraries profile has access to
+ FromUser string `json:"fromUser" example:"jeff"` // The user the profile is based on
+ Ombi bool `json:"ombi"` // Whether or not Ombi settings are stored in this profile.
+ ReferralsEnabled bool `json:"referrals_enabled" example:"true"` // Whether or not the profile has referrals enabled, and has a template invite stored.
}
type getProfilesDTO struct {
@@ -150,6 +151,7 @@ type respUser struct {
NotifyThroughMatrix bool `json:"notify_matrix"`
Label string `json:"label"` // Label of user, shown next to their name.
AccountsAdmin bool `json:"accounts_admin"` // Whether or not the user is a jfa-go admin.
+ ReferralsEnabled bool `json:"referrals_enabled"`
}
type getUsersDTO struct {
@@ -388,6 +390,7 @@ type MyDetailsDTO struct {
Discord *MyDetailsContactMethodsDTO `json:"discord,omitempty"`
Telegram *MyDetailsContactMethodsDTO `json:"telegram,omitempty"`
Matrix *MyDetailsContactMethodsDTO `json:"matrix,omitempty"`
+ HasReferrals bool `json:"has_referrals,omitempty"`
}
type MyDetailsContactMethodsDTO struct {
@@ -414,3 +417,14 @@ type ChangeMyPasswordDTO struct {
Old string `json:"old"`
New string `json:"new"`
}
+
+type GetMyReferralRespDTO struct {
+ Code string `json:"code"`
+ RemainingUses int `json:"remaining_uses"`
+ NoLimit bool `json:"no_limit"`
+ Expiry int64 `json:"expiry"` // Come back after this time to get a new referral
+}
+
+type EnableDisableReferralDTO struct {
+ Users []string `json:"users"`
+}
diff --git a/router.go b/router.go
index 56754df..e2426cc 100644
--- a/router.go
+++ b/router.go
@@ -226,6 +226,12 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
api.DELETE(p+"/profiles/ombi/:profile", app.DeleteOmbiProfile)
}
api.POST(p+"/matrix/login", app.MatrixLogin)
+ if app.config.Section("user_page").Key("referrals").MustBool(false) {
+ api.POST(p+"/users/referral/:mode/:source", app.EnableReferralForUsers)
+ api.DELETE(p+"/users/referral", app.DisableReferralForUsers)
+ api.POST(p+"/profiles/referral/:profile/:invite", app.EnableReferralForProfile)
+ api.DELETE(p+"/profiles/referral/:profile", app.DisableReferralForProfile)
+ }
if userPageEnabled {
user.GET(p+"/details", app.MyDetails)
@@ -242,6 +248,9 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
user.DELETE(p+"/telegram", app.UnlinkMyTelegram)
user.DELETE(p+"/matrix", app.UnlinkMyMatrix)
user.POST(p+"/password", app.ChangeMyPassword)
+ if app.config.Section("user_page").Key("referrals").MustBool(false) {
+ user.GET(p+"/referral", app.GetMyReferral)
+ }
}
}
}
diff --git a/scripts/langmover/common.json b/scripts/langmover/common.json
index 3d078d3..a4421d5 100644
--- a/scripts/langmover/common.json
+++ b/scripts/langmover/common.json
@@ -38,7 +38,10 @@
"expiry": "common",
"add": "common",
"edit": "common",
- "delete": "admin"
+ "delete": "common",
+ "myAccount": "common",
+ "referrals": "common",
+ "inviteRemainingUses": "admin"
},
"notifications": {
"errorLoginBlank": "common",
diff --git a/storage.go b/storage.go
index c7aa0bb..5d8a527 100644
--- a/storage.go
+++ b/storage.go
@@ -429,15 +429,16 @@ type DiscordUser struct {
Discriminator string
Lang string
Contact bool
- JellyfinID string `json:"-" badgerhold:"key"` // Used internally in discord.go
+ JellyfinID string `json:"-" badgerhold:"key"`
}
type EmailAddress struct {
- Addr string `badgerhold:"index"`
- Label string // User Label.
- Contact bool
- Admin bool // Whether or not user is jfa-go admin.
- JellyfinID string `badgerhold:"key"`
+ Addr string `badgerhold:"index"`
+ Label string // User Label.
+ Contact bool
+ Admin bool // Whether or not user is jfa-go admin.
+ JellyfinID string `badgerhold:"key"`
+ ReferralTemplateKey string
}
type customEmails struct {
@@ -470,16 +471,17 @@ type userPageContent struct {
// timePattern: %Y-%m-%dT%H:%M:%S.%f
type Profile struct {
- Name string `badgerhold:"key"`
- Admin bool `json:"admin,omitempty" badgerhold:"index"`
- LibraryAccess string `json:"libraries,omitempty"`
- FromUser string `json:"fromUser,omitempty"`
- Homescreen bool `json:"homescreen"`
- Policy mediabrowser.Policy `json:"policy,omitempty"`
- Configuration mediabrowser.Configuration `json:"configuration,omitempty"`
- Displayprefs map[string]interface{} `json:"displayprefs,omitempty"`
- Default bool `json:"default,omitempty"`
- Ombi map[string]interface{} `json:"ombi,omitempty"`
+ Name string `badgerhold:"key"`
+ Admin bool `json:"admin,omitempty" badgerhold:"index"`
+ LibraryAccess string `json:"libraries,omitempty"`
+ FromUser string `json:"fromUser,omitempty"`
+ Homescreen bool `json:"homescreen"`
+ Policy mediabrowser.Policy `json:"policy,omitempty"`
+ Configuration mediabrowser.Configuration `json:"configuration,omitempty"`
+ Displayprefs map[string]interface{} `json:"displayprefs,omitempty"`
+ Default bool `json:"default,omitempty"`
+ Ombi map[string]interface{} `json:"ombi,omitempty"`
+ ReferralTemplateKey string
}
type Invite struct {
@@ -495,11 +497,14 @@ type Invite struct {
UserMinutes int `json:"user-minutes,omitempty"`
SendTo string `json:"email"`
// Used to be stored as formatted time, now as Unix.
- UsedBy [][]string `json:"used-by"`
- Notify map[string]map[string]bool `json:"notify"`
- Profile string `json:"profile"`
- Label string `json:"label,omitempty"`
- Captchas map[string]*captcha.Data // Map of Captcha IDs to answers
+ UsedBy [][]string `json:"used-by"`
+ Notify map[string]map[string]bool `json:"notify"`
+ Profile string `json:"profile"`
+ Label string `json:"label,omitempty"`
+ Captchas map[string]*captcha.Data // Map of Captcha IDs to answers
+ IsReferral bool `json:"is_referral" badgerhold:"index"`
+ ReferrerJellyfinID string `json:"referrer_id"`
+ ReferrerTemplateForProfile string
}
type Lang struct {
diff --git a/ts/admin.ts b/ts/admin.ts
index df65535..f29fe2b 100644
--- a/ts/admin.ts
+++ b/ts/admin.ts
@@ -78,6 +78,11 @@ window.availableProfiles = window.availableProfiles || [];
if (window.linkResetEnabled) {
window.modals.sendPWR = new Modal(document.getElementById("modal-send-pwr"));
}
+
+ if (window.referralsEnabled) {
+ window.modals.enableReferralsUser = new Modal(document.getElementById("modal-enable-referrals-user"));
+ window.modals.enableReferralsProfile = new Modal(document.getElementById("modal-enable-referrals-profile"));
+ }
})();
var inviteCreator = new createInvite();
diff --git a/ts/modules/accounts.ts b/ts/modules/accounts.ts
index 3a56bce..99ba6b9 100644
--- a/ts/modules/accounts.ts
+++ b/ts/modules/accounts.ts
@@ -23,6 +23,7 @@ interface User {
notify_matrix: boolean;
label: string;
accounts_admin: boolean;
+ referrals_enabled: boolean;
}
interface getPinResponse {
@@ -69,6 +70,8 @@ class user implements User {
private _labelEditButton: HTMLElement;
private _accounts_admin: HTMLInputElement
private _selected: boolean;
+ private _referralsEnabled: boolean;
+ private _referralsEnabledCheck: HTMLElement;
lastNotifyMethod = (): string => {
// Telegram, Matrix, Discord
@@ -162,6 +165,17 @@ class user implements User {
}
}
+ get referrals_enabled(): boolean { return this._referralsEnabled; }
+ set referrals_enabled(v: boolean) {
+ this._referralsEnabled = v;
+ if (!window.referralsEnabled) return;
+ if (!v) {
+ this._referralsEnabledCheck.textContent = ``;
+ } else {
+ this._referralsEnabledCheck.innerHTML = `
`;
+ }
+ }
+
private _constructDropdown = (): HTMLDivElement => {
const el = document.createElement("div") as HTMLDivElement;
const telegram = this._telegramUsername != "";
@@ -506,6 +520,11 @@ class user implements User {
|
`;
}
+ if (window.referralsEnabled) {
+ innerHTML += `
+
|
+ `;
+ }
innerHTML += `
|
|
@@ -544,6 +563,10 @@ class user implements User {
});
};
}
+
+ if (window.referralsEnabled) {
+ this._referralsEnabledCheck = this._row.querySelector(".accounts-referrals");
+ }
this._notifyDropdown = this._constructDropdown();
@@ -716,6 +739,7 @@ class user implements User {
this.discord_id = user.discord_id;
this.label = user.label;
this.accounts_admin = user.accounts_admin;
+ this.referrals_enabled = user.referrals_enabled;
}
asElement = (): HTMLTableRowElement => { return this._row; }
@@ -748,9 +772,14 @@ export class accountsList {
private _modifySettings = document.getElementById("accounts-modify-user") as HTMLSpanElement;
private _modifySettingsProfile = document.getElementById("radio-use-profile") as HTMLInputElement;
private _modifySettingsUser = document.getElementById("radio-use-user") as HTMLInputElement;
+ private _enableReferrals = document.getElementById("accounts-enable-referrals") as HTMLSpanElement;
+ private _enableReferralsProfile = document.getElementById("radio-referrals-use-profile") as HTMLInputElement;
+ private _enableReferralsInvite = document.getElementById("radio-referrals-use-invite") as HTMLInputElement;
private _sendPWR = document.getElementById("accounts-send-pwr") as HTMLSpanElement;
private _profileSelect = document.getElementById("modify-user-profiles") as HTMLSelectElement;
private _userSelect = document.getElementById("modify-user-users") as HTMLSelectElement;
+ private _referralsProfileSelect = document.getElementById("enable-referrals-user-profiles") as HTMLSelectElement;
+ private _referralsInviteSelect = document.getElementById("enable-referrals-user-invites") as HTMLSelectElement;
private _search = document.getElementById("accounts-search") as HTMLInputElement;
private _selectAll = document.getElementById("accounts-select-all") as HTMLInputElement;
@@ -906,6 +935,14 @@ export class accountsList {
bool: true,
string: false,
date: true
+ },
+ "referrals-enabled": {
+ name: window.lang.strings("referrals"),
+ getter: "referrals_enabled",
+ bool: true,
+ string: false,
+ date: false,
+ dependsOnTableHeader: "accounts-header-referrals"
}
}
@@ -919,7 +956,6 @@ export class accountsList {
// const words = query.split(" ");
let words: string[] = [];
- // FIXME: SPLIT BY SPACE, UNLESS IN QUOTES
let quoteSymbol = ``;
let queryStart = -1;
@@ -985,7 +1021,6 @@ export class accountsList {
boolState = false;
}
if (isBool) {
- // FIXME: Generate filter card for each filter class
const filterCard = document.createElement("span");
filterCard.ariaLabel = window.lang.strings("clickToRemoveFilter");
filterCard.classList.add("button", "~" + (boolState ? "positive" : "critical"), "@high", "center", "mx-2", "h-full");
@@ -1058,7 +1093,7 @@ export class accountsList {
let attempt: { year?: number, month?: number, day?: number, hour?: number, minute?: number } = dateParser.attempt(split[1]);
// Month in Date objects is 0-based, so make our parsed date that way too
- if ("month" in attempt) attempt["month"] -= 1;
+ if ("month" in attempt) attempt.month -= 1;
let date: Date = (Date as any).fromString(split[1]) as Date;
console.log("Read", attempt, "and", date);
@@ -1124,7 +1159,7 @@ export class accountsList {
}
}
}
- return result
+ return result;
};
@@ -1154,6 +1189,9 @@ export class accountsList {
this._selectAll.indeterminate = false;
this._selectAll.checked = false;
this._modifySettings.classList.add("unfocused");
+ if (window.referralsEnabled) {
+ this._enableReferrals.classList.add("unfocused");
+ }
this._deleteUser.classList.add("unfocused");
if (window.emailEnabled || window.telegramEnabled) {
this._announceButton.parentElement.classList.add("unfocused");
@@ -1176,6 +1214,9 @@ export class accountsList {
this._selectAll.indeterminate = true;
}
this._modifySettings.classList.remove("unfocused");
+ if (window.referralsEnabled) {
+ this._enableReferrals.classList.remove("unfocused");
+ }
this._deleteUser.classList.remove("unfocused");
this._deleteUser.textContent = window.lang.quantity("deleteUser", list.length);
if (window.emailEnabled || window.telegramEnabled) {
@@ -1184,6 +1225,7 @@ export class accountsList {
let anyNonExpiries = list.length == 0 ? true : false;
let allNonExpiries = true;
let noContactCount = 0;
+ let referralState = Number(this._users[list[0]].referrals_enabled); // -1 = hide, 0 = show "enable", 1 = show "disable"
// Only show enable/disable button if all selected have the same state.
this._shouldEnable = this._users[list[0]].disabled
let showDisableEnable = true;
@@ -1203,6 +1245,9 @@ export class accountsList {
if (!this._users[id].lastNotifyMethod()) {
noContactCount++;
}
+ if (window.referralsEnabled && referralState != -1 && Number(this._users[id].referrals_enabled) != referralState) {
+ referralState = -1;
+ }
}
this._settingExpiry = false;
if (!anyNonExpiries && !allNonExpiries) {
@@ -1236,6 +1281,22 @@ export class accountsList {
this._disableEnable.parentElement.classList.remove("unfocused");
this._disableEnable.textContent = message;
}
+ if (window.referralsEnabled) {
+ if (referralState == -1) {
+ this._enableReferrals.classList.add("unfocused");
+ } else {
+ this._enableReferrals.classList.remove("unfocused");
+ }
+ if (referralState == 0) {
+ this._enableReferrals.classList.add("~urge");
+ this._enableReferrals.classList.remove("~warning");
+ this._enableReferrals.textContent = window.lang.strings("enableReferrals");
+ } else if (referralState == 1) {
+ this._enableReferrals.classList.add("~warning");
+ this._enableReferrals.classList.remove("~urge");
+ this._enableReferrals.textContent = window.lang.strings("disableReferrals");
+ }
+ }
}
}
@@ -1662,6 +1723,90 @@ export class accountsList {
};
window.modals.modifyUser.show();
}
+
+ enableReferrals = () => {
+ const modalHeader = document.getElementById("header-enable-referrals-user");
+ modalHeader.textContent = window.lang.quantity("enableReferralsFor", this._collectUsers().length)
+ let list = this._collectUsers();
+
+ // Check if we're disabling or enabling
+ if (this._users[list[0]].referrals_enabled) {
+ _delete("/users/referral", {"users": list}, (req: XMLHttpRequest) => {
+ if (req.readyState != 4 || req.status != 200) return;
+ window.notifications.customSuccess("disabledReferralsSuccess", window.lang.quantity("appliedSettings", list.length));
+ this.reload();
+ });
+ return;
+ }
+
+ (() => {
+ _get("/invites", null, (req: XMLHttpRequest) => {
+ if (req.readyState != 4 || req.status != 200) return;
+
+ // 1. Invites
+
+ let innerHTML = "";
+ let invites = req.response["invites"] as Array
;
+ window.availableProfiles = req.response["profiles"];
+ if (invites) {
+ for (let inv of invites) {
+ let name = inv.code;
+ if (inv.label) {
+ name = `${inv.label} (${inv.code})`;
+ }
+ innerHTML += ``;
+ }
+ this._enableReferralsInvite.checked = true;
+ } else {
+ this._enableReferralsInvite.checked = false;
+ innerHTML += ``;
+ }
+ this._enableReferralsProfile.checked = !(this._enableReferralsInvite.checked);
+ this._referralsInviteSelect.innerHTML = innerHTML;
+
+ // 2. Profiles
+
+ innerHTML = "";
+ for (const profile of window.availableProfiles) {
+ innerHTML += ``;
+ }
+ this._referralsProfileSelect.innerHTML = innerHTML;
+ });
+ })();
+
+ const form = document.getElementById("form-enable-referrals-user") as HTMLFormElement;
+ const button = form.querySelector("span.submit") as HTMLSpanElement;
+ form.onsubmit = (event: Event) => {
+ event.preventDefault();
+ toggleLoader(button);
+ let send = {
+ "users": list
+ };
+ // console.log("profile:", this._enableReferralsProfile.checked, this._enableReferralsInvite.checked);
+ if (this._enableReferralsProfile.checked && !this._enableReferralsInvite.checked) {
+ send["from"] = "profile";
+ send["profile"] = this._referralsProfileSelect.value;
+ } else if (this._enableReferralsInvite.checked && !this._enableReferralsProfile.checked) {
+ send["from"] = "invite";
+ send["id"] = this._referralsInviteSelect.value;
+ }
+ _post("/users/referral/" + send["from"] + "/" + (send["id"] ? send["id"] : send["profile"]), send, (req: XMLHttpRequest) => {
+ if (req.readyState == 4) {
+ toggleLoader(button);
+ if (req.status == 400) {
+ window.notifications.customError("noReferralTemplateError", window.lang.notif("errorNoReferralTemplate"));
+ } else if (req.status == 200 || req.status == 204) {
+ window.notifications.customSuccess("enableReferralsSuccess", window.lang.quantity("appliedSettings", list.length));
+ }
+ this.reload();
+ window.modals.enableReferralsUser.close();
+ }
+ });
+ };
+ this._enableReferralsProfile.checked = true;
+ this._enableReferralsInvite.checked = false;
+ window.modals.enableReferralsUser.show();
+ }
extendExpiry = (enableUser?: boolean) => {
const list = this._collectUsers();
@@ -1794,6 +1939,43 @@ export class accountsList {
this._modifySettingsProfile.onchange = checkSource;
this._modifySettingsUser.onchange = checkSource;
+ if (window.referralsEnabled) {
+ const profileSpan = this._enableReferralsProfile.nextElementSibling as HTMLSpanElement;
+ const inviteSpan = this._enableReferralsInvite.nextElementSibling as HTMLSpanElement;
+ const checkReferralSource = () => {
+ console.log("States:", this._enableReferralsProfile.checked, this._enableReferralsInvite.checked);
+ if (this._enableReferralsProfile.checked) {
+ this._referralsInviteSelect.parentElement.classList.add("unfocused");
+ this._referralsProfileSelect.parentElement.classList.remove("unfocused")
+ profileSpan.classList.add("@high");
+ profileSpan.classList.remove("@low");
+ inviteSpan.classList.remove("@high");
+ inviteSpan.classList.add("@low");
+ } else {
+ this._referralsInviteSelect.parentElement.classList.remove("unfocused");
+ this._referralsProfileSelect.parentElement.classList.add("unfocused");
+ inviteSpan.classList.add("@high");
+ inviteSpan.classList.remove("@low");
+ profileSpan.classList.remove("@high");
+ profileSpan.classList.add("@low");
+ }
+ };
+ profileSpan.onclick = () => {
+ this._enableReferralsProfile.checked = true;
+ this._enableReferralsInvite.checked = false;
+ checkReferralSource();
+ };
+ inviteSpan.onclick = () => {;
+ this._enableReferralsInvite.checked = true;
+ this._enableReferralsProfile.checked = false;
+ checkReferralSource();
+ };
+ this._enableReferrals.onclick = () => {
+ this.enableReferrals();
+ profileSpan.onclick(null);
+ };
+ }
+
this._deleteUser.onclick = this.deleteUsers;
this._deleteUser.classList.add("unfocused");
diff --git a/ts/modules/profiles.ts b/ts/modules/profiles.ts
index 39ad52c..8bf8c32 100644
--- a/ts/modules/profiles.ts
+++ b/ts/modules/profiles.ts
@@ -5,6 +5,7 @@ interface Profile {
libraries: string;
fromUser: string;
ombi: boolean;
+ referrals_enabled: boolean;
}
class profile implements Profile {
@@ -16,6 +17,8 @@ class profile implements Profile {
private _fromUser: HTMLTableDataCellElement;
private _defaultRadio: HTMLInputElement;
private _ombi: boolean;
+ private _referralsButton: HTMLSpanElement;
+ private _referralsEnabled: boolean;
get name(): string { return this._name.textContent; }
set name(v: string) { this._name.textContent = v; }
@@ -51,7 +54,22 @@ class profile implements Profile {
get fromUser(): string { return this._fromUser.textContent; }
set fromUser(v: string) { this._fromUser.textContent = v; }
-
+
+ get referrals_enabled(): boolean { return this._referralsEnabled; }
+ set referrals_enabled(v: boolean) {
+ if (!window.referralsEnabled) return;
+ this._referralsEnabled = v;
+ if (v) {
+ this._referralsButton.textContent = window.lang.strings("delete");
+ this._referralsButton.classList.add("~critical");
+ this._referralsButton.classList.remove("~neutral");
+ } else {
+ this._referralsButton.textContent = window.lang.strings("add");
+ this._referralsButton.classList.add("~neutral");
+ this._referralsButton.classList.remove("~critical");
+ }
+ }
+
get default(): boolean { return this._defaultRadio.checked; }
set default(v: boolean) { this._defaultRadio.checked = v; }
@@ -64,6 +82,9 @@ class profile implements Profile {
if (window.ombiEnabled) innerHTML += `
|
`;
+ if (window.referralsEnabled) innerHTML += `
+ |
+ `;
innerHTML += `
|
|
@@ -75,6 +96,8 @@ class profile implements Profile {
this._libraries = this._row.querySelector("td.profile-libraries") as HTMLTableDataCellElement;
if (window.ombiEnabled)
this._ombiButton = this._row.querySelector("span.profile-ombi") as HTMLSpanElement;
+ if (window.referralsEnabled)
+ this._referralsButton = this._row.querySelector("span.profile-referrals") as HTMLSpanElement;
this._fromUser = this._row.querySelector("td.profile-from") as HTMLTableDataCellElement;
this._defaultRadio = this._row.querySelector("input[type=radio]") as HTMLInputElement;
this._defaultRadio.onclick = () => document.dispatchEvent(new CustomEvent("profiles-default", { detail: this.name }));
@@ -89,9 +112,11 @@ class profile implements Profile {
this.fromUser = p.fromUser;
this.libraries = p.libraries;
this.ombi = p.ombi;
+ this.referrals_enabled = p.referrals_enabled;
}
setOmbiFunc = (ombiFunc: (ombi: boolean) => void) => { this._ombiButton.onclick = () => ombiFunc(this._ombi); }
+ setReferralFunc = (referralFunc: (enabled: boolean) => void) => { this._referralsButton.onclick = () => referralFunc(this._referralsEnabled); }
remove = () => { document.dispatchEvent(new CustomEvent("profiles-delete", { detail: this._name })); this._row.remove(); }
@@ -173,6 +198,14 @@ export class ProfileEditor {
this._ombiProfiles.load(name);
}
});
+ if (window.referralsEnabled)
+ this._profiles[name].setReferralFunc((enabled: boolean) => {
+ if (enabled) {
+ this.disableReferrals(name);
+ } else {
+ this.enableReferrals(name);
+ }
+ });
this._table.appendChild(this._profiles[name].asElement());
}
}
@@ -185,6 +218,62 @@ export class ProfileEditor {
}
})
+ disableReferrals = (name: string) => _delete("/profiles/referral/" + name, null, (req: XMLHttpRequest) => {
+ if (req.readyState != 4) return;
+ this.load();
+ });
+
+ enableReferrals = (name: string) => {
+ const referralsInviteSelect = document.getElementById("enable-referrals-profile-invites") as HTMLSelectElement;
+ _get("/invites", null, (req: XMLHttpRequest) => {
+ if (req.readyState != 4 || req.status != 200) return;
+
+ let innerHTML = "";
+ let invites = req.response["invites"] as Array;
+ window.availableProfiles = req.response["profiles"];
+ if (invites) {
+ for (let inv of invites) {
+ let name = inv.code;
+ if (inv.label) {
+ name = `${inv.label} (${inv.code})`;
+ }
+ innerHTML += ``;
+ }
+ } else {
+ innerHTML += ``;
+ }
+
+ referralsInviteSelect.innerHTML = innerHTML;
+ });
+
+ const form = document.getElementById("form-enable-referrals-profile") as HTMLFormElement;
+ const button = form.querySelector("span.submit") as HTMLSpanElement;
+ form.onsubmit = (event: Event) => {
+ event.preventDefault();
+ toggleLoader(button);
+
+ let send = {
+ "profile": name,
+ "invite": referralsInviteSelect.value
+ };
+
+ _post("/profiles/referral/" + send["profile"] + "/" + send["invite"], send, (req: XMLHttpRequest) => {
+ if (req.readyState == 4) {
+ toggleLoader(button);
+ if (req.status == 400) {
+ window.notifications.customError("unknownError", window.lang.notif("errorUnknown"));
+ } else if (req.status == 200 || req.status == 204) {
+ window.notifications.customSuccess("enableReferralsSuccess", window.lang.notif("referralsEnabled"));
+ }
+ window.modals.enableReferralsProfile.close();
+ this.load();
+ }
+ });
+ };
+ window.modals.profiles.close();
+ window.modals.enableReferralsProfile.show();
+ };
+
constructor() {
(document.getElementById('setting-profiles') as HTMLSpanElement).onclick = this.load;
document.addEventListener("profiles-default", (event: CustomEvent) => {
diff --git a/ts/typings/d.ts b/ts/typings/d.ts
index cae4f54..9befa20 100644
--- a/ts/typings/d.ts
+++ b/ts/typings/d.ts
@@ -40,6 +40,7 @@ declare interface Window {
jellyfinLogin: boolean;
jfAdminOnly: boolean;
jfAllowAll: boolean;
+ referralsEnabled: boolean;
}
declare interface Update {
@@ -113,6 +114,8 @@ declare interface Modals {
pwr?: Modal;
logs: Modal;
email?: Modal;
+ enableReferralsUser?: Modal;
+ enableReferralsProfile?: Modal;
}
interface Invite {
diff --git a/ts/user.ts b/ts/user.ts
index e37043b..05afdb9 100644
--- a/ts/user.ts
+++ b/ts/user.ts
@@ -1,7 +1,7 @@
import { ThemeManager } from "./modules/theme.js";
import { lang, LangFile, loadLangSelector } from "./modules/lang.js";
import { Modal } from "./modules/modal.js";
-import { _get, _post, _delete, notificationBox, whichAnimationEvent, toDateString, toggleLoader, addLoader, removeLoader } from "./modules/common.js";
+import { _get, _post, _delete, notificationBox, whichAnimationEvent, toDateString, toggleLoader, addLoader, removeLoader, toClipboard } from "./modules/common.js";
import { Login } from "./modules/login.js";
import { Discord, Telegram, Matrix, ServiceConfiguration, MatrixConfiguration } from "./modules/account-linking.js";
import { Validator, ValidatorConf, ValidatorRespDTO } from "./modules/validator.js";
@@ -18,6 +18,7 @@ interface userWindow extends Window {
matrixUserID: string;
discordSendPINMessage: string;
pwrEnabled: string;
+ referralsEnabled: boolean;
}
declare var window: userWindow;
@@ -107,6 +108,14 @@ interface MyDetails {
discord?: MyDetailsContactMethod;
telegram?: MyDetailsContactMethod;
matrix?: MyDetailsContactMethod;
+ has_referrals: boolean;
+}
+
+interface MyReferral {
+ code: string;
+ remaining_uses: number;
+ no_limit: boolean;
+ expiry: number;
}
interface ContactDTO {
@@ -237,6 +246,107 @@ class ContactMethods {
};
}
+class ReferralCard {
+ private _card: HTMLElement;
+ private _code: string;
+ private _url: string;
+ private _expiry: Date;
+ private _expiryUnix: number;
+ private _remainingUses: number;
+ private _noLimit: boolean;
+
+ private _button: HTMLButtonElement;
+ private _infoArea: HTMLDivElement;
+ private _remainingUsesEl: HTMLSpanElement;
+ private _expiryEl: HTMLSpanElement;
+
+ get code(): string { return this._code; }
+ set code(c: string) {
+ this._code = c;
+ let url = window.location.href;
+ for (let split of ["#", "?", "account", "my"]) {
+ url = url.split(split)[0];
+ }
+ if (url.slice(-1) != "/") { url += "/"; }
+ url = url + "invite/" + this._code;
+ this._url = url;
+ }
+
+ get remaining_uses(): number { return this._remainingUses; }
+ set remaining_uses(v: number) {
+ this._remainingUses = v;
+ if (v > 0 && !(this._noLimit))
+ this._remainingUsesEl.textContent = `${v}`;
+ }
+
+ get no_limit(): boolean { return this._noLimit; }
+ set no_limit(v: boolean) {
+ this._noLimit = v;
+ if (v)
+ this._remainingUsesEl.textContent = `∞`;
+ else
+ this._remainingUsesEl.textContent = `${this._remainingUses}`;
+ }
+
+ get expiry(): Date { return this._expiry; };
+ set expiry(expiryUnix: number) {
+ this._expiryUnix = expiryUnix;
+ this._expiry = new Date(expiryUnix * 1000);
+ this._expiryEl.textContent = toDateString(this._expiry);
+ }
+
+ constructor(card: HTMLElement) {
+ this._card = card;
+ this._button = this._card.querySelector(".user-referrals-button") as HTMLButtonElement;
+ this._infoArea = this._card.querySelector(".user-referrals-info") as HTMLDivElement;
+
+ this._infoArea.innerHTML = `
+
+
+ ${window.lang.strings("inviteRemainingUses")}
+
+
+
+
+
${window.lang.strings("expiry")}
+
+
+ `;
+
+ this._remainingUsesEl = this._infoArea.querySelector(".referral-remaining-uses") as HTMLSpanElement;
+ this._expiryEl = this._infoArea.querySelector(".referral-expiry") as HTMLSpanElement;
+
+ document.addEventListener("timefmt-change", () => {
+ this.expiry = this._expiryUnix;
+ });
+
+ this._button.addEventListener("click", () => {
+ toClipboard(this._url);
+ const content = this._button.innerHTML;
+ this._button.innerHTML = `
+ ${window.lang.strings("copied")}
+ `;
+ this._button.classList.add("~positive");
+ this._button.classList.remove("~info");
+ setTimeout(() => {
+ this._button.classList.add("~info");
+ this._button.classList.remove("~positive");
+ this._button.innerHTML = content;
+ }, 2000);
+ });
+ }
+
+ hide = () => this._card.classList.add("unfocused");
+
+ update = (referral: MyReferral) => {
+ this.code = referral.code;
+ this.remaining_uses = referral.remaining_uses;
+ this.no_limit = referral.no_limit;
+ this.expiry = referral.expiry;
+ this._card.classList.remove("unfocused");
+ };
+}
+
class ExpiryCard {
private _card: HTMLElement;
private _expiry: Date;
@@ -318,6 +428,9 @@ class ExpiryCard {
var expiryCard = new ExpiryCard(statusCard);
+var referralCard: ReferralCard;
+if (window.referralsEnabled) referralCard = new ReferralCard(document.getElementById("card-referrals"));
+
var contactMethodList = new ContactMethods(contactCard);
const addEditEmail = (add: boolean): void => {
@@ -363,7 +476,8 @@ const discordConf: ServiceConfiguration = {
}
};
-let discord = new Discord(discordConf);
+let discord: Discord;
+if (window.discordEnabled) discord = new Discord(discordConf);
const telegramConf: ServiceConfiguration = {
modal: window.modals.telegram as Modal,
@@ -378,7 +492,8 @@ const telegramConf: ServiceConfiguration = {
}
};
-let telegram = new Telegram(telegramConf);
+let telegram: Telegram;
+if (window.telegramEnabled) telegram = new Telegram(telegramConf);
const matrixConf: MatrixConfiguration = {
modal: window.modals.matrix as Modal,
@@ -393,7 +508,8 @@ const matrixConf: MatrixConfiguration = {
}
};
-let matrix = new Matrix(matrixConf);
+let matrix: Matrix;
+if (window.matrixEnabled) matrix = new Matrix(matrixConf);
const oldPasswordField = document.getElementById("user-old-password") as HTMLInputElement;
@@ -468,14 +584,15 @@ document.addEventListener("details-reload", () => {
// Note the weird format of the functions for discord/telegram:
// "this" was being redefined within the onclick() method, so
// they had to be wrapped in an anonymous function.
- const contactMethods: { name: string, icon: string, f: (add: boolean) => void, required: boolean }[] = [
- {name: "email", icon: `
`, f: addEditEmail, required: true},
- {name: "discord", icon: `
`, f: (add: boolean) => { discord.onclick(); }, required: window.discordRequired},
- {name: "telegram", icon: `
`, f: (add: boolean) => { telegram.onclick() }, required: window.telegramRequired},
- {name: "matrix", icon: `
[m]`, f: (add: boolean) => { matrix.show(); }, required: window.matrixRequired}
+ const contactMethods: { name: string, icon: string, f: (add: boolean) => void, required: boolean, enabled: boolean }[] = [
+ {name: "email", icon: `
`, f: addEditEmail, required: true, enabled: true},
+ {name: "discord", icon: `
`, f: (add: boolean) => { discord.onclick(); }, required: window.discordRequired, enabled: window.discordEnabled},
+ {name: "telegram", icon: `
`, f: (add: boolean) => { telegram.onclick() }, required: window.telegramRequired, enabled: window.telegramEnabled},
+ {name: "matrix", icon: `
[m]`, f: (add: boolean) => { matrix.show(); }, required: window.matrixRequired, enabled: window.matrixEnabled}
];
for (let method of contactMethods) {
+ if (!(method.enabled)) continue;
if (method.name in details) {
contactMethodList.append(method.name, details[method.name], method.icon, method.f, method.required);
}
@@ -509,6 +626,18 @@ document.addEventListener("details-reload", () => {
} else if (!statusCard.classList.contains("unfocused")) {
setBestRowSpan(passwordCard, true);
}
+
+ if (window.referralsEnabled) {
+ if (details.has_referrals) {
+ _get("/my/referral", null, (req: XMLHttpRequest) => {
+ if (req.readyState != 4 || req.status != 200) return;
+ const referral: MyReferral = req.response as MyReferral;
+ referralCard.update(referral);
+ });
+ } else {
+ referralCard.hide();
+ }
+ }
}
});
});
diff --git a/views.go b/views.go
index f2e2dbc..704a35d 100644
--- a/views.go
+++ b/views.go
@@ -173,6 +173,7 @@ func (app *appContext) AdminPage(gc *gin.Context) {
"jfAdminOnly": jfAdminOnly,
"jfAllowAll": jfAllowAll,
"userPageEnabled": app.config.Section("user_page").Key("enabled").MustBool(false),
+ "referralsEnabled": app.config.Section("user_page").Key("enabled").MustBool(false) && app.config.Section("user_page").Key("referrals").MustBool(false),
})
}
@@ -203,6 +204,7 @@ func (app *appContext) MyUserPage(gc *gin.Context) {
"langName": lang,
"jfLink": app.config.Section("ui").Key("redirect_url").String(),
"requirements": app.validator.getCriteria(),
+ "referralsEnabled": app.config.Section("user_page").Key("enabled").MustBool(false) && app.config.Section("user_page").Key("referrals").MustBool(false),
}
if telegramEnabled {
data["telegramUsername"] = app.telegram.username
@@ -617,6 +619,14 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
}
userPageAddress += "/my/account"
+ fromUser := ""
+ if inv.ReferrerJellyfinID != "" {
+ sender, status, err := app.jf.UserByID(inv.ReferrerJellyfinID, false)
+ if status == 200 && err == nil {
+ fromUser = sender.Name
+ }
+ }
+
data := gin.H{
"urlBase": app.getURLBase(gc),
"cssClass": app.cssClass,
@@ -652,6 +662,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
"reCAPTCHASiteKey": app.config.Section("captcha").Key("recaptcha_site_key").MustString(""),
"userPageEnabled": app.config.Section("user_page").Key("enabled").MustBool(false),
"userPageAddress": userPageAddress,
+ "fromUser": fromUser,
}
if telegram {
data["telegramPIN"] = app.telegram.NewAuthToken()