mirror of
https://github.com/thelounge/thelounge
synced 2024-11-21 19:43:07 +00:00
Merge pull request #4489 from thelounge/maxleiter/tabbedSettings
Refactor settings to their own tabs and routes
This commit is contained in:
commit
5f7acbf994
10 changed files with 850 additions and 639 deletions
171
client/components/Settings/Account.vue
Normal file
171
client/components/Settings/Account.vue
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
!$store.state.serverConfiguration.public &&
|
||||||
|
!$store.state.serverConfiguration.ldapEnabled
|
||||||
|
"
|
||||||
|
id="change-password"
|
||||||
|
role="group"
|
||||||
|
aria-labelledby="label-change-password"
|
||||||
|
>
|
||||||
|
<h2 id="label-change-password">Change password</h2>
|
||||||
|
<div class="password-container">
|
||||||
|
<label for="current-password" class="sr-only"> Enter current password </label>
|
||||||
|
<RevealPassword v-slot:default="slotProps">
|
||||||
|
<input
|
||||||
|
id="current-password"
|
||||||
|
autocomplete="current-password"
|
||||||
|
:type="slotProps.isVisible ? 'text' : 'password'"
|
||||||
|
name="old_password"
|
||||||
|
class="input"
|
||||||
|
placeholder="Enter current password"
|
||||||
|
/>
|
||||||
|
</RevealPassword>
|
||||||
|
</div>
|
||||||
|
<div class="password-container">
|
||||||
|
<label for="new-password" class="sr-only"> Enter desired new password </label>
|
||||||
|
<RevealPassword v-slot:default="slotProps">
|
||||||
|
<input
|
||||||
|
id="new-password"
|
||||||
|
:type="slotProps.isVisible ? 'text' : 'password'"
|
||||||
|
name="new_password"
|
||||||
|
autocomplete="new-password"
|
||||||
|
class="input"
|
||||||
|
placeholder="Enter desired new password"
|
||||||
|
/>
|
||||||
|
</RevealPassword>
|
||||||
|
</div>
|
||||||
|
<div class="password-container">
|
||||||
|
<label for="new-password-verify" class="sr-only"> Repeat new password </label>
|
||||||
|
<RevealPassword v-slot:default="slotProps">
|
||||||
|
<input
|
||||||
|
id="new-password-verify"
|
||||||
|
:type="slotProps.isVisible ? 'text' : 'password'"
|
||||||
|
name="verify_password"
|
||||||
|
autocomplete="new-password"
|
||||||
|
class="input"
|
||||||
|
placeholder="Repeat new password"
|
||||||
|
/>
|
||||||
|
</RevealPassword>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="passwordChangeStatus && passwordChangeStatus.success"
|
||||||
|
class="feedback success"
|
||||||
|
>
|
||||||
|
Successfully updated your password
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="passwordChangeStatus && passwordChangeStatus.error"
|
||||||
|
class="feedback error"
|
||||||
|
>
|
||||||
|
{{ passwordErrors[passwordChangeStatus.error] }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button type="submit" class="btn" @click.prevent="changePassword">
|
||||||
|
Change password
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!$store.state.serverConfiguration.public" class="session-list" role="group">
|
||||||
|
<h2>Sessions</h2>
|
||||||
|
|
||||||
|
<h3>Current session</h3>
|
||||||
|
<Session v-if="currentSession" :session="currentSession" />
|
||||||
|
|
||||||
|
<template v-if="activeSessions.length > 0">
|
||||||
|
<h3>Active sessions</h3>
|
||||||
|
<Session
|
||||||
|
v-for="session in activeSessions"
|
||||||
|
:key="session.token"
|
||||||
|
:session="session"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<h3>Other sessions</h3>
|
||||||
|
<p v-if="$store.state.sessions.length === 0">Loading…</p>
|
||||||
|
<p v-else-if="otherSessions.length === 0">
|
||||||
|
<em>You are not currently logged in to any other device.</em>
|
||||||
|
</p>
|
||||||
|
<Session
|
||||||
|
v-for="session in otherSessions"
|
||||||
|
v-else
|
||||||
|
:key="session.token"
|
||||||
|
:session="session"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import socket from "../../js/socket";
|
||||||
|
import RevealPassword from "../RevealPassword.vue";
|
||||||
|
import Session from "../Session.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "UserSettings",
|
||||||
|
components: {
|
||||||
|
RevealPassword,
|
||||||
|
Session,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
passwordChangeStatus: null,
|
||||||
|
passwordErrors: {
|
||||||
|
missing_fields: "Please enter a new password",
|
||||||
|
password_mismatch: "Both new password fields must match",
|
||||||
|
password_incorrect:
|
||||||
|
"The current password field does not match your account password",
|
||||||
|
update_failed: "Failed to update your password",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
currentSession() {
|
||||||
|
return this.$store.state.sessions.find((item) => item.current);
|
||||||
|
},
|
||||||
|
activeSessions() {
|
||||||
|
return this.$store.state.sessions.filter((item) => !item.current && item.active > 0);
|
||||||
|
},
|
||||||
|
otherSessions() {
|
||||||
|
return this.$store.state.sessions.filter((item) => !item.current && !item.active);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
socket.emit("sessions:get");
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
changePassword() {
|
||||||
|
const allFields = new FormData(this.$refs.settingsForm);
|
||||||
|
const data = {
|
||||||
|
old_password: allFields.get("old_password"),
|
||||||
|
new_password: allFields.get("new_password"),
|
||||||
|
verify_password: allFields.get("verify_password"),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!data.old_password || !data.new_password || !data.verify_password) {
|
||||||
|
this.passwordChangeStatus = {
|
||||||
|
success: false,
|
||||||
|
error: "missing_fields",
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.new_password !== data.verify_password) {
|
||||||
|
this.passwordChangeStatus = {
|
||||||
|
success: false,
|
||||||
|
error: "password_mismatch",
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.once("change-password", (response) => {
|
||||||
|
this.passwordChangeStatus = response;
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.emit("change-password", data);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
169
client/components/Settings/Appearance.vue
Normal file
169
client/components/Settings/Appearance.vue
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h2>Messages</h2>
|
||||||
|
<div>
|
||||||
|
<label class="opt">
|
||||||
|
<input :checked="$store.state.settings.motd" type="checkbox" name="motd" />
|
||||||
|
Show <abbr title="Message Of The Day">MOTD</abbr>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="opt">
|
||||||
|
<input
|
||||||
|
:checked="$store.state.settings.showSeconds"
|
||||||
|
type="checkbox"
|
||||||
|
name="showSeconds"
|
||||||
|
/>
|
||||||
|
Include seconds in timestamp
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="opt">
|
||||||
|
<input
|
||||||
|
:checked="$store.state.settings.use12hClock"
|
||||||
|
type="checkbox"
|
||||||
|
name="use12hClock"
|
||||||
|
/>
|
||||||
|
Use 12-hour timestamps
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<template v-if="$store.state.serverConfiguration.prefetch">
|
||||||
|
<h2>Link previews</h2>
|
||||||
|
<div>
|
||||||
|
<label class="opt">
|
||||||
|
<input :checked="$store.state.settings.media" type="checkbox" name="media" />
|
||||||
|
Auto-expand media
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="opt">
|
||||||
|
<input :checked="$store.state.settings.links" type="checkbox" name="links" />
|
||||||
|
Auto-expand websites
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<h2 id="label-status-messages">
|
||||||
|
Status messages
|
||||||
|
<span
|
||||||
|
class="tooltipped tooltipped-n tooltipped-no-delay"
|
||||||
|
aria-label="Joins, parts, quits, kicks, nick changes, and mode changes"
|
||||||
|
>
|
||||||
|
<button class="extra-help" />
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
<div role="group" aria-labelledby="label-status-messages">
|
||||||
|
<label class="opt">
|
||||||
|
<input
|
||||||
|
:checked="$store.state.settings.statusMessages === 'shown'"
|
||||||
|
type="radio"
|
||||||
|
name="statusMessages"
|
||||||
|
value="shown"
|
||||||
|
/>
|
||||||
|
Show all status messages individually
|
||||||
|
</label>
|
||||||
|
<label class="opt">
|
||||||
|
<input
|
||||||
|
:checked="$store.state.settings.statusMessages === 'condensed'"
|
||||||
|
type="radio"
|
||||||
|
name="statusMessages"
|
||||||
|
value="condensed"
|
||||||
|
/>
|
||||||
|
Condense status messages together
|
||||||
|
</label>
|
||||||
|
<label class="opt">
|
||||||
|
<input
|
||||||
|
:checked="$store.state.settings.statusMessages === 'hidden'"
|
||||||
|
type="radio"
|
||||||
|
name="statusMessages"
|
||||||
|
value="hidden"
|
||||||
|
/>
|
||||||
|
Hide all status messages
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<h2>Visual Aids</h2>
|
||||||
|
<div>
|
||||||
|
<label class="opt">
|
||||||
|
<input
|
||||||
|
:checked="$store.state.settings.coloredNicks"
|
||||||
|
type="checkbox"
|
||||||
|
name="coloredNicks"
|
||||||
|
/>
|
||||||
|
Enable colored nicknames
|
||||||
|
</label>
|
||||||
|
<label class="opt">
|
||||||
|
<input
|
||||||
|
:checked="$store.state.settings.autocomplete"
|
||||||
|
type="checkbox"
|
||||||
|
name="autocomplete"
|
||||||
|
/>
|
||||||
|
Enable autocomplete
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="opt">
|
||||||
|
<label for="nickPostfix" class="opt">
|
||||||
|
Nick autocomplete postfix
|
||||||
|
<span
|
||||||
|
class="tooltipped tooltipped-n tooltipped-no-delay"
|
||||||
|
aria-label="Nick autocomplete postfix (for example a comma)"
|
||||||
|
>
|
||||||
|
<button class="extra-help" />
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="nickPostfix"
|
||||||
|
:value="$store.state.settings.nickPostfix"
|
||||||
|
type="text"
|
||||||
|
name="nickPostfix"
|
||||||
|
class="input"
|
||||||
|
placeholder="Nick autocomplete postfix (e.g. ', ')"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Theme</h2>
|
||||||
|
<div>
|
||||||
|
<label for="theme-select" class="sr-only">Theme</label>
|
||||||
|
<select
|
||||||
|
id="theme-select"
|
||||||
|
:value="$store.state.settings.theme"
|
||||||
|
name="theme"
|
||||||
|
class="input"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="theme in $store.state.serverConfiguration.themes"
|
||||||
|
:key="theme.name"
|
||||||
|
:value="theme.name"
|
||||||
|
>
|
||||||
|
{{ theme.displayName }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2>Custom Stylesheet</h2>
|
||||||
|
<label for="user-specified-css-input" class="sr-only">
|
||||||
|
Custom stylesheet. You can override any style with CSS here.
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="user-specified-css-input"
|
||||||
|
:value="$store.state.settings.userStyles"
|
||||||
|
class="input"
|
||||||
|
name="userStyles"
|
||||||
|
placeholder="/* You can override any style with CSS here */"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
textarea#user-specified-css-input {
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "AppearanceSettings",
|
||||||
|
};
|
||||||
|
</script>
|
143
client/components/Settings/General.vue
Normal file
143
client/components/Settings/General.vue
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="canRegisterProtocol || hasInstallPromptEvent">
|
||||||
|
<h2>Native app</h2>
|
||||||
|
<button
|
||||||
|
v-if="hasInstallPromptEvent"
|
||||||
|
type="button"
|
||||||
|
class="btn"
|
||||||
|
@click.prevent="nativeInstallPrompt"
|
||||||
|
>
|
||||||
|
Add The Lounge to Home screen
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="canRegisterProtocol"
|
||||||
|
type="button"
|
||||||
|
class="btn"
|
||||||
|
@click.prevent="registerProtocol"
|
||||||
|
>
|
||||||
|
Open irc:// URLs with The Lounge
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="$store.state.serverConfiguration.fileUpload">
|
||||||
|
<h2>File uploads</h2>
|
||||||
|
<div>
|
||||||
|
<label class="opt">
|
||||||
|
<input
|
||||||
|
:checked="$store.state.settings.uploadCanvas"
|
||||||
|
type="checkbox"
|
||||||
|
name="uploadCanvas"
|
||||||
|
/>
|
||||||
|
Attempt to remove metadata from images before uploading
|
||||||
|
<span
|
||||||
|
class="tooltipped tooltipped-n tooltipped-no-delay"
|
||||||
|
aria-label="This option renders the image into a canvas element to remove metadata from the image.
|
||||||
|
This may break orientation if your browser does not support that."
|
||||||
|
>
|
||||||
|
<button class="extra-help" />
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="!$store.state.serverConfiguration.public">
|
||||||
|
<h2>Settings synchronisation</h2>
|
||||||
|
<label class="opt">
|
||||||
|
<input
|
||||||
|
:checked="$store.state.settings.syncSettings"
|
||||||
|
type="checkbox"
|
||||||
|
name="syncSettings"
|
||||||
|
/>
|
||||||
|
Synchronize settings with other clients
|
||||||
|
</label>
|
||||||
|
<template v-if="!$store.state.settings.syncSettings">
|
||||||
|
<div v-if="$store.state.serverHasSettings" class="settings-sync-panel">
|
||||||
|
<p>
|
||||||
|
<strong>Warning:</strong> Checking this box will override the settings of
|
||||||
|
this client with those stored on the server.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Use the button below to enable synchronization, and override any settings
|
||||||
|
already synced to the server.
|
||||||
|
</p>
|
||||||
|
<button type="button" class="btn btn-small" @click="onForceSyncClick">
|
||||||
|
Sync settings and enable
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-else class="settings-sync-panel">
|
||||||
|
<p>
|
||||||
|
<strong>Warning:</strong> No settings have been synced before. Enabling this
|
||||||
|
will sync all settings of this client as the base for other clients.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div v-if="!$store.state.serverConfiguration.public">
|
||||||
|
<h2>Automatic away message</h2>
|
||||||
|
|
||||||
|
<label class="opt">
|
||||||
|
<label for="awayMessage" class="sr-only">Automatic away message</label>
|
||||||
|
<input
|
||||||
|
id="awayMessage"
|
||||||
|
:value="$store.state.settings.awayMessage"
|
||||||
|
type="text"
|
||||||
|
name="awayMessage"
|
||||||
|
class="input"
|
||||||
|
placeholder="Away message if The Lounge is not open"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style></style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let installPromptEvent = null;
|
||||||
|
|
||||||
|
window.addEventListener("beforeinstallprompt", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
installPromptEvent = e;
|
||||||
|
});
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "GeneralSettings",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
canRegisterProtocol: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
hasInstallPromptEvent() {
|
||||||
|
// TODO: This doesn't hide the button after clicking
|
||||||
|
return installPromptEvent !== null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
// Enable protocol handler registration if supported,
|
||||||
|
// and the network configuration is not locked
|
||||||
|
this.canRegisterProtocol =
|
||||||
|
window.navigator.registerProtocolHandler &&
|
||||||
|
!this.$store.state.serverConfiguration.lockNetwork;
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
nativeInstallPrompt() {
|
||||||
|
installPromptEvent.prompt();
|
||||||
|
installPromptEvent = null;
|
||||||
|
},
|
||||||
|
onForceSyncClick() {
|
||||||
|
this.$store.dispatch("settings/syncAll", true);
|
||||||
|
this.$store.dispatch("settings/update", {
|
||||||
|
name: "syncSettings",
|
||||||
|
value: true,
|
||||||
|
sync: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
registerProtocol() {
|
||||||
|
const uri = document.location.origin + document.location.pathname + "?uri=%s";
|
||||||
|
|
||||||
|
window.navigator.registerProtocolHandler("irc", uri, "The Lounge");
|
||||||
|
window.navigator.registerProtocolHandler("ircs", uri, "The Lounge");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
102
client/components/Settings/Navigation.vue
Normal file
102
client/components/Settings/Navigation.vue
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
<template>
|
||||||
|
<!-- 220px is the width of the sidebar, and we add 100px to allow for the text -->
|
||||||
|
<aside class="settings-menu">
|
||||||
|
<h2>Settings</h2>
|
||||||
|
<ul role="navigation" aria-label="Settings tabs">
|
||||||
|
<SettingTabItem name="General" class-name="general" to="" />
|
||||||
|
<SettingTabItem name="Appearance" class-name="appearance" to="appearance" />
|
||||||
|
<SettingTabItem name="Notifications" class-name="notifications" to="notifications" />
|
||||||
|
<SettingTabItem name="Account" class-name="account" to="account" />
|
||||||
|
</ul>
|
||||||
|
</aside>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.settings-menu {
|
||||||
|
position: fixed;
|
||||||
|
/* top: Header + (padding bottom of h2 - border) */
|
||||||
|
top: calc(45px + 5px);
|
||||||
|
/* Mid page minus width of container and 30 pixels for padding */
|
||||||
|
margin-left: calc(50% - 480px - 30px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The calculation is mobile + 2/3 of container width. Fairly arbitrary. */
|
||||||
|
@media screen and (max-width: calc(768px + 320px)) {
|
||||||
|
.settings-menu {
|
||||||
|
position: static;
|
||||||
|
width: min(480px, 100%);
|
||||||
|
align-self: center;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-menu ul {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-menu li {
|
||||||
|
font-size: 18px;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-menu button {
|
||||||
|
color: var(--body-color-muted);
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: inline-block;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-menu li:not(:last-of-type) button {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-menu button::before {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
display: inline-block;
|
||||||
|
content: "";
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-menu .appearance::before {
|
||||||
|
content: "\f108"; /* http://fontawesome.io/icon/desktop/ */
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-menu .account::before {
|
||||||
|
content: "\f007"; /* http://fontawesome.io/icon/user/ */
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-menu .messages::before {
|
||||||
|
content: "\f0e0"; /* http://fontawesome.io/icon/envelope/ */
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-menu .notifications::before {
|
||||||
|
content: "\f0f3"; /* http://fontawesome.io/icon/bell/ */
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-menu .general::before {
|
||||||
|
content: "\f013"; /* http://fontawesome.io/icon/cog/ */
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-menu button:hover,
|
||||||
|
.settings-menu button.active {
|
||||||
|
color: var(--body-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-menu button.active {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import SettingTabItem from "./SettingTabItem.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "SettingsTabs",
|
||||||
|
components: {
|
||||||
|
SettingTabItem,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
187
client/components/Settings/Notifications.vue
Normal file
187
client/components/Settings/Notifications.vue
Normal file
|
@ -0,0 +1,187 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<template v-if="!$store.state.serverConfiguration.public">
|
||||||
|
<h2>Push Notifications</h2>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
id="pushNotifications"
|
||||||
|
type="button"
|
||||||
|
class="btn"
|
||||||
|
:disabled="
|
||||||
|
$store.state.pushNotificationState !== 'supported' &&
|
||||||
|
$store.state.pushNotificationState !== 'subscribed'
|
||||||
|
"
|
||||||
|
@click="onPushButtonClick"
|
||||||
|
>
|
||||||
|
<template v-if="$store.state.pushNotificationState === 'subscribed'">
|
||||||
|
Unsubscribe from push notifications
|
||||||
|
</template>
|
||||||
|
<template v-else-if="$store.state.pushNotificationState === 'loading'">
|
||||||
|
Loading…
|
||||||
|
</template>
|
||||||
|
<template v-else> Subscribe to push notifications </template>
|
||||||
|
</button>
|
||||||
|
<div v-if="$store.state.pushNotificationState === 'nohttps'" class="error">
|
||||||
|
<strong>Warning</strong>: Push notifications are only supported over HTTPS
|
||||||
|
connections.
|
||||||
|
</div>
|
||||||
|
<div v-if="$store.state.pushNotificationState === 'unsupported'" class="error">
|
||||||
|
<strong>Warning</strong>:
|
||||||
|
<span>Push notifications are not supported by your browser.</span>
|
||||||
|
|
||||||
|
<div v-if="isIOS" class="apple-push-unsupported">
|
||||||
|
Safari does
|
||||||
|
<a
|
||||||
|
href="https://bugs.webkit.org/show_bug.cgi?id=182566"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
>not support the web push notification specification</a
|
||||||
|
>, and because all browsers on iOS use Safari under the hood, The Lounge is
|
||||||
|
unable to provide push notifications on iOS devices.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<h2>Browser Notifications</h2>
|
||||||
|
<div>
|
||||||
|
<label class="opt">
|
||||||
|
<input
|
||||||
|
id="desktopNotifications"
|
||||||
|
:checked="$store.state.settings.desktopNotifications"
|
||||||
|
:disabled="$store.state.desktopNotificationState === 'nohttps'"
|
||||||
|
type="checkbox"
|
||||||
|
name="desktopNotifications"
|
||||||
|
/>
|
||||||
|
Enable browser notifications<br />
|
||||||
|
<div v-if="$store.state.desktopNotificationState === 'unsupported'" class="error">
|
||||||
|
<strong>Warning</strong>: Notifications are not supported by your browser.
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="$store.state.desktopNotificationState === 'nohttps'"
|
||||||
|
id="warnBlockedDesktopNotifications"
|
||||||
|
class="error"
|
||||||
|
>
|
||||||
|
<strong>Warning</strong>: Notifications are only supported over HTTPS
|
||||||
|
connections.
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="$store.state.desktopNotificationState === 'blocked'"
|
||||||
|
id="warnBlockedDesktopNotifications"
|
||||||
|
class="error"
|
||||||
|
>
|
||||||
|
<strong>Warning</strong>: Notifications are blocked by your browser.
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="opt">
|
||||||
|
<input
|
||||||
|
:checked="$store.state.settings.notification"
|
||||||
|
type="checkbox"
|
||||||
|
name="notification"
|
||||||
|
/>
|
||||||
|
Enable notification sound
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="opt">
|
||||||
|
<button id="play" @click.prevent="playNotification">Play sound</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="opt">
|
||||||
|
<input
|
||||||
|
:checked="$store.state.settings.notifyAllMessages"
|
||||||
|
type="checkbox"
|
||||||
|
name="notifyAllMessages"
|
||||||
|
/>
|
||||||
|
Enable notification for all messages
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!$store.state.serverConfiguration.public">
|
||||||
|
<label class="opt">
|
||||||
|
<label for="highlights" class="opt">
|
||||||
|
Custom highlights
|
||||||
|
<span
|
||||||
|
class="tooltipped tooltipped-n tooltipped-no-delay"
|
||||||
|
aria-label="If a message contains any of these comma-separated
|
||||||
|
expressions, it will trigger a highlight."
|
||||||
|
>
|
||||||
|
<button class="extra-help" />
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="highlights"
|
||||||
|
:value="$store.state.settings.highlights"
|
||||||
|
type="text"
|
||||||
|
name="highlights"
|
||||||
|
class="input"
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder="Comma-separated, e.g.: word, some more words, anotherword"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!$store.state.serverConfiguration.public">
|
||||||
|
<label class="opt">
|
||||||
|
<label for="highlightExceptions" class="opt">
|
||||||
|
Highlight exceptions
|
||||||
|
<span
|
||||||
|
class="tooltipped tooltipped-n tooltipped-no-delay"
|
||||||
|
aria-label="If a message contains any of these comma-separated
|
||||||
|
expressions, it will not trigger a highlight even if it contains
|
||||||
|
your nickname or expressions defined in custom highlights."
|
||||||
|
>
|
||||||
|
<button class="extra-help" />
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="highlightExceptions"
|
||||||
|
:value="$store.state.settings.highlightExceptions"
|
||||||
|
type="text"
|
||||||
|
name="highlightExceptions"
|
||||||
|
class="input"
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder="Comma-separated, e.g.: word, some more words, anotherword"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import webpush from "../../js/webpush";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "NotificationSettings",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
// https://stackoverflow.com/questions/9038625/detect-if-device-is-ios
|
||||||
|
isIOS: () =>
|
||||||
|
[
|
||||||
|
"iPad Simulator",
|
||||||
|
"iPhone Simulator",
|
||||||
|
"iPod Simulator",
|
||||||
|
"iPad",
|
||||||
|
"iPhone",
|
||||||
|
"iPod",
|
||||||
|
].includes(navigator.platform) ||
|
||||||
|
// iPad on iOS 13 detection
|
||||||
|
(navigator.userAgent.includes("Mac") && "ontouchend" in document),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
playNotification() {
|
||||||
|
const pop = new Audio();
|
||||||
|
pop.src = "audio/pop.wav";
|
||||||
|
pop.play();
|
||||||
|
},
|
||||||
|
onPushButtonClick() {
|
||||||
|
webpush.togglePushSubscription();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
38
client/components/Settings/SettingTabItem.vue
Normal file
38
client/components/Settings/SettingTabItem.vue
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
<template>
|
||||||
|
<li :aria-label="name">
|
||||||
|
<router-link
|
||||||
|
v-slot:default="{navigate, isExactActive}"
|
||||||
|
:to="'/settings/' + to"
|
||||||
|
:class="['icon', className]"
|
||||||
|
:aria-label="name"
|
||||||
|
role="tab"
|
||||||
|
aria-controls="settings"
|
||||||
|
:aria-selected="$route.name === name"
|
||||||
|
custom
|
||||||
|
>
|
||||||
|
<button :class="{active: isExactActive}" @click="navigate" @keypress.enter="navigate">
|
||||||
|
{{ name }}
|
||||||
|
</button>
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "SettingTabListItem",
|
||||||
|
props: {
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
className: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -52,7 +52,7 @@
|
||||||
aria-label="Settings"
|
aria-label="Settings"
|
||||||
role="tab"
|
role="tab"
|
||||||
aria-controls="settings"
|
aria-controls="settings"
|
||||||
:aria-selected="$route.name === 'Settings'"
|
:aria-selected="$route.name === 'General'"
|
||||||
/></span>
|
/></span>
|
||||||
<span
|
<span
|
||||||
class="tooltipped tooltipped-n tooltipped-no-touch"
|
class="tooltipped tooltipped-n tooltipped-no-touch"
|
||||||
|
|
|
@ -3,597 +3,25 @@
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<SidebarToggle />
|
<SidebarToggle />
|
||||||
</div>
|
</div>
|
||||||
<form
|
<Navigation />
|
||||||
ref="settingsForm"
|
|
||||||
class="container"
|
|
||||||
autocomplete="off"
|
|
||||||
@change="onChange"
|
|
||||||
@submit.prevent
|
|
||||||
>
|
|
||||||
<h1 class="title">Settings</h1>
|
|
||||||
|
|
||||||
<div>
|
<div class="container">
|
||||||
<label class="opt">
|
<form ref="settingsForm" autocomplete="off" @change="onChange" @submit.prevent>
|
||||||
<input
|
<router-view></router-view>
|
||||||
:checked="$store.state.settings.advanced"
|
</form>
|
||||||
type="checkbox"
|
</div>
|
||||||
name="advanced"
|
|
||||||
/>
|
|
||||||
Advanced settings
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="canRegisterProtocol || hasInstallPromptEvent">
|
|
||||||
<h2>Native app</h2>
|
|
||||||
<button
|
|
||||||
v-if="hasInstallPromptEvent"
|
|
||||||
type="button"
|
|
||||||
class="btn"
|
|
||||||
@click.prevent="nativeInstallPrompt"
|
|
||||||
>
|
|
||||||
Add The Lounge to Home screen
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="canRegisterProtocol"
|
|
||||||
type="button"
|
|
||||||
class="btn"
|
|
||||||
@click.prevent="registerProtocol"
|
|
||||||
>
|
|
||||||
Open irc:// URLs with The Lounge
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="!$store.state.serverConfiguration.public && $store.state.settings.advanced">
|
|
||||||
<h2>Settings synchronisation</h2>
|
|
||||||
<label class="opt">
|
|
||||||
<input
|
|
||||||
:checked="$store.state.settings.syncSettings"
|
|
||||||
type="checkbox"
|
|
||||||
name="syncSettings"
|
|
||||||
/>
|
|
||||||
Synchronize settings with other clients
|
|
||||||
</label>
|
|
||||||
<template v-if="!$store.state.settings.syncSettings">
|
|
||||||
<div v-if="$store.state.serverHasSettings" class="settings-sync-panel">
|
|
||||||
<p>
|
|
||||||
<strong>Warning:</strong> Checking this box will override the settings
|
|
||||||
of this client with those stored on the server.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Use the button below to enable synchronization, and override any
|
|
||||||
settings already synced to the server.
|
|
||||||
</p>
|
|
||||||
<button type="button" class="btn btn-small" @click="onForceSyncClick">
|
|
||||||
Sync settings and enable
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div v-else class="settings-sync-panel">
|
|
||||||
<p>
|
|
||||||
<strong>Warning:</strong> No settings have been synced before. Enabling
|
|
||||||
this will sync all settings of this client as the base for other
|
|
||||||
clients.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2>Messages</h2>
|
|
||||||
<div>
|
|
||||||
<label class="opt">
|
|
||||||
<input :checked="$store.state.settings.motd" type="checkbox" name="motd" />
|
|
||||||
Show <abbr title="Message Of The Day">MOTD</abbr>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="opt">
|
|
||||||
<input
|
|
||||||
:checked="$store.state.settings.showSeconds"
|
|
||||||
type="checkbox"
|
|
||||||
name="showSeconds"
|
|
||||||
/>
|
|
||||||
Include seconds in timestamp
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="opt">
|
|
||||||
<input
|
|
||||||
:checked="$store.state.settings.use12hClock"
|
|
||||||
type="checkbox"
|
|
||||||
name="use12hClock"
|
|
||||||
/>
|
|
||||||
Use 12-hour timestamps
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div v-if="!$store.state.serverConfiguration.public && $store.state.settings.advanced">
|
|
||||||
<h2>Automatic away message</h2>
|
|
||||||
|
|
||||||
<label class="opt">
|
|
||||||
<label for="awayMessage" class="sr-only">Automatic away message</label>
|
|
||||||
<input
|
|
||||||
id="awayMessage"
|
|
||||||
:value="$store.state.settings.awayMessage"
|
|
||||||
type="text"
|
|
||||||
name="awayMessage"
|
|
||||||
class="input"
|
|
||||||
placeholder="Away message if The Lounge is not open"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<h2 id="label-status-messages">
|
|
||||||
Status messages
|
|
||||||
<span
|
|
||||||
class="tooltipped tooltipped-n tooltipped-no-delay"
|
|
||||||
aria-label="Joins, parts, quits, kicks, nick changes, and mode changes"
|
|
||||||
>
|
|
||||||
<button class="extra-help" />
|
|
||||||
</span>
|
|
||||||
</h2>
|
|
||||||
<div role="group" aria-labelledby="label-status-messages">
|
|
||||||
<label class="opt">
|
|
||||||
<input
|
|
||||||
:checked="$store.state.settings.statusMessages === 'shown'"
|
|
||||||
type="radio"
|
|
||||||
name="statusMessages"
|
|
||||||
value="shown"
|
|
||||||
/>
|
|
||||||
Show all status messages individually
|
|
||||||
</label>
|
|
||||||
<label class="opt">
|
|
||||||
<input
|
|
||||||
:checked="$store.state.settings.statusMessages === 'condensed'"
|
|
||||||
type="radio"
|
|
||||||
name="statusMessages"
|
|
||||||
value="condensed"
|
|
||||||
/>
|
|
||||||
Condense status messages together
|
|
||||||
</label>
|
|
||||||
<label class="opt">
|
|
||||||
<input
|
|
||||||
:checked="$store.state.settings.statusMessages === 'hidden'"
|
|
||||||
type="radio"
|
|
||||||
name="statusMessages"
|
|
||||||
value="hidden"
|
|
||||||
/>
|
|
||||||
Hide all status messages
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<h2>Visual Aids</h2>
|
|
||||||
<div>
|
|
||||||
<label class="opt">
|
|
||||||
<input
|
|
||||||
:checked="$store.state.settings.coloredNicks"
|
|
||||||
type="checkbox"
|
|
||||||
name="coloredNicks"
|
|
||||||
/>
|
|
||||||
Enable colored nicknames
|
|
||||||
</label>
|
|
||||||
<label class="opt">
|
|
||||||
<input
|
|
||||||
:checked="$store.state.settings.autocomplete"
|
|
||||||
type="checkbox"
|
|
||||||
name="autocomplete"
|
|
||||||
/>
|
|
||||||
Enable autocomplete
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div v-if="$store.state.settings.advanced">
|
|
||||||
<label class="opt">
|
|
||||||
<label for="nickPostfix" class="opt">
|
|
||||||
Nick autocomplete postfix
|
|
||||||
<span
|
|
||||||
class="tooltipped tooltipped-n tooltipped-no-delay"
|
|
||||||
aria-label="Nick autocomplete postfix (for example a comma)"
|
|
||||||
>
|
|
||||||
<button class="extra-help" />
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="nickPostfix"
|
|
||||||
:value="$store.state.settings.nickPostfix"
|
|
||||||
type="text"
|
|
||||||
name="nickPostfix"
|
|
||||||
class="input"
|
|
||||||
placeholder="Nick autocomplete postfix (e.g. ', ')"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2>Theme</h2>
|
|
||||||
<div>
|
|
||||||
<label for="theme-select" class="sr-only">Theme</label>
|
|
||||||
<select
|
|
||||||
id="theme-select"
|
|
||||||
:value="$store.state.settings.theme"
|
|
||||||
name="theme"
|
|
||||||
class="input"
|
|
||||||
>
|
|
||||||
<option
|
|
||||||
v-for="theme in $store.state.serverConfiguration.themes"
|
|
||||||
:key="theme.name"
|
|
||||||
:value="theme.name"
|
|
||||||
>
|
|
||||||
{{ theme.displayName }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template v-if="$store.state.serverConfiguration.prefetch">
|
|
||||||
<h2>Link previews</h2>
|
|
||||||
<div>
|
|
||||||
<label class="opt">
|
|
||||||
<input
|
|
||||||
:checked="$store.state.settings.media"
|
|
||||||
type="checkbox"
|
|
||||||
name="media"
|
|
||||||
/>
|
|
||||||
Auto-expand media
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="opt">
|
|
||||||
<input
|
|
||||||
:checked="$store.state.settings.links"
|
|
||||||
type="checkbox"
|
|
||||||
name="links"
|
|
||||||
/>
|
|
||||||
Auto-expand websites
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="$store.state.settings.advanced && $store.state.serverConfiguration.fileUpload"
|
|
||||||
>
|
|
||||||
<h2>File uploads</h2>
|
|
||||||
<div>
|
|
||||||
<label class="opt">
|
|
||||||
<input
|
|
||||||
:checked="$store.state.settings.uploadCanvas"
|
|
||||||
type="checkbox"
|
|
||||||
name="uploadCanvas"
|
|
||||||
/>
|
|
||||||
Attempt to remove metadata from images before uploading
|
|
||||||
<span
|
|
||||||
class="tooltipped tooltipped-n tooltipped-no-delay"
|
|
||||||
aria-label="This option renders the image into a canvas element to remove metadata from the image.
|
|
||||||
This may break orientation if your browser does not support that."
|
|
||||||
>
|
|
||||||
<button class="extra-help" />
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template v-if="!$store.state.serverConfiguration.public">
|
|
||||||
<h2>Push Notifications</h2>
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
id="pushNotifications"
|
|
||||||
type="button"
|
|
||||||
class="btn"
|
|
||||||
:disabled="
|
|
||||||
$store.state.pushNotificationState !== 'supported' &&
|
|
||||||
$store.state.pushNotificationState !== 'subscribed'
|
|
||||||
"
|
|
||||||
@click="onPushButtonClick"
|
|
||||||
>
|
|
||||||
<template v-if="$store.state.pushNotificationState === 'subscribed'">
|
|
||||||
Unsubscribe from push notifications
|
|
||||||
</template>
|
|
||||||
<template v-else-if="$store.state.pushNotificationState === 'loading'">
|
|
||||||
Loading…
|
|
||||||
</template>
|
|
||||||
<template v-else> Subscribe to push notifications </template>
|
|
||||||
</button>
|
|
||||||
<div v-if="$store.state.pushNotificationState === 'nohttps'" class="error">
|
|
||||||
<strong>Warning</strong>: Push notifications are only supported over HTTPS
|
|
||||||
connections.
|
|
||||||
</div>
|
|
||||||
<div v-if="$store.state.pushNotificationState === 'unsupported'" class="error">
|
|
||||||
<strong>Warning</strong>:
|
|
||||||
<span>Push notifications are not supported by your browser.</span>
|
|
||||||
|
|
||||||
<div v-if="isIOS" class="apple-push-unsupported">
|
|
||||||
Safari does
|
|
||||||
<a
|
|
||||||
href="https://bugs.webkit.org/show_bug.cgi?id=182566"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>not support the web push notification specification</a
|
|
||||||
>, and because all browsers on iOS use Safari under the hood, The Lounge
|
|
||||||
is unable to provide push notifications on iOS devices.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<h2>Browser Notifications</h2>
|
|
||||||
<div>
|
|
||||||
<label class="opt">
|
|
||||||
<input
|
|
||||||
id="desktopNotifications"
|
|
||||||
:checked="$store.state.settings.desktopNotifications"
|
|
||||||
:disabled="$store.state.desktopNotificationState === 'nohttps'"
|
|
||||||
type="checkbox"
|
|
||||||
name="desktopNotifications"
|
|
||||||
/>
|
|
||||||
Enable browser notifications<br />
|
|
||||||
<div
|
|
||||||
v-if="$store.state.desktopNotificationState === 'unsupported'"
|
|
||||||
class="error"
|
|
||||||
>
|
|
||||||
<strong>Warning</strong>: Notifications are not supported by your browser.
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="$store.state.desktopNotificationState === 'nohttps'"
|
|
||||||
id="warnBlockedDesktopNotifications"
|
|
||||||
class="error"
|
|
||||||
>
|
|
||||||
<strong>Warning</strong>: Notifications are only supported over HTTPS
|
|
||||||
connections.
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="$store.state.desktopNotificationState === 'blocked'"
|
|
||||||
id="warnBlockedDesktopNotifications"
|
|
||||||
class="error"
|
|
||||||
>
|
|
||||||
<strong>Warning</strong>: Notifications are blocked by your browser.
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="opt">
|
|
||||||
<input
|
|
||||||
:checked="$store.state.settings.notification"
|
|
||||||
type="checkbox"
|
|
||||||
name="notification"
|
|
||||||
/>
|
|
||||||
Enable notification sound
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="opt">
|
|
||||||
<button id="play" @click.prevent="playNotification">Play sound</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="$store.state.settings.advanced">
|
|
||||||
<label class="opt">
|
|
||||||
<input
|
|
||||||
:checked="$store.state.settings.notifyAllMessages"
|
|
||||||
type="checkbox"
|
|
||||||
name="notifyAllMessages"
|
|
||||||
/>
|
|
||||||
Enable notification for all messages
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="!$store.state.serverConfiguration.public && $store.state.settings.advanced">
|
|
||||||
<label class="opt">
|
|
||||||
<label for="highlights" class="opt">
|
|
||||||
Custom highlights
|
|
||||||
<span
|
|
||||||
class="tooltipped tooltipped-n tooltipped-no-delay"
|
|
||||||
aria-label="If a message contains any of these comma-separated
|
|
||||||
expressions, it will trigger a highlight."
|
|
||||||
>
|
|
||||||
<button class="extra-help" />
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="highlights"
|
|
||||||
:value="$store.state.settings.highlights"
|
|
||||||
type="text"
|
|
||||||
name="highlights"
|
|
||||||
class="input"
|
|
||||||
autocomplete="off"
|
|
||||||
placeholder="Comma-separated, e.g.: word, some more words, anotherword"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="!$store.state.serverConfiguration.public && $store.state.settings.advanced">
|
|
||||||
<label class="opt">
|
|
||||||
<label for="highlightExceptions" class="opt">
|
|
||||||
Highlight exceptions
|
|
||||||
<span
|
|
||||||
class="tooltipped tooltipped-n tooltipped-no-delay"
|
|
||||||
aria-label="If a message contains any of these comma-separated
|
|
||||||
expressions, it will not trigger a highlight even if it contains
|
|
||||||
your nickname or expressions defined in custom highlights."
|
|
||||||
>
|
|
||||||
<button class="extra-help" />
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="highlightExceptions"
|
|
||||||
:value="$store.state.settings.highlightExceptions"
|
|
||||||
type="text"
|
|
||||||
name="highlightExceptions"
|
|
||||||
class="input"
|
|
||||||
autocomplete="off"
|
|
||||||
placeholder="Comma-separated, e.g.: word, some more words, anotherword"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="
|
|
||||||
!$store.state.serverConfiguration.public &&
|
|
||||||
!$store.state.serverConfiguration.ldapEnabled
|
|
||||||
"
|
|
||||||
id="change-password"
|
|
||||||
role="group"
|
|
||||||
aria-labelledby="label-change-password"
|
|
||||||
>
|
|
||||||
<h2 id="label-change-password">Change password</h2>
|
|
||||||
<div class="password-container">
|
|
||||||
<label for="current-password" class="sr-only"> Enter current password </label>
|
|
||||||
<RevealPassword v-slot:default="slotProps">
|
|
||||||
<input
|
|
||||||
id="current-password"
|
|
||||||
autocomplete="current-password"
|
|
||||||
:type="slotProps.isVisible ? 'text' : 'password'"
|
|
||||||
name="old_password"
|
|
||||||
class="input"
|
|
||||||
placeholder="Enter current password"
|
|
||||||
/>
|
|
||||||
</RevealPassword>
|
|
||||||
</div>
|
|
||||||
<div class="password-container">
|
|
||||||
<label for="new-password" class="sr-only"> Enter desired new password </label>
|
|
||||||
<RevealPassword v-slot:default="slotProps">
|
|
||||||
<input
|
|
||||||
id="new-password"
|
|
||||||
:type="slotProps.isVisible ? 'text' : 'password'"
|
|
||||||
name="new_password"
|
|
||||||
autocomplete="new-password"
|
|
||||||
class="input"
|
|
||||||
placeholder="Enter desired new password"
|
|
||||||
/>
|
|
||||||
</RevealPassword>
|
|
||||||
</div>
|
|
||||||
<div class="password-container">
|
|
||||||
<label for="new-password-verify" class="sr-only"> Repeat new password </label>
|
|
||||||
<RevealPassword v-slot:default="slotProps">
|
|
||||||
<input
|
|
||||||
id="new-password-verify"
|
|
||||||
:type="slotProps.isVisible ? 'text' : 'password'"
|
|
||||||
name="verify_password"
|
|
||||||
autocomplete="new-password"
|
|
||||||
class="input"
|
|
||||||
placeholder="Repeat new password"
|
|
||||||
/>
|
|
||||||
</RevealPassword>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="passwordChangeStatus && passwordChangeStatus.success"
|
|
||||||
class="feedback success"
|
|
||||||
>
|
|
||||||
Successfully updated your password
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else-if="passwordChangeStatus && passwordChangeStatus.error"
|
|
||||||
class="feedback error"
|
|
||||||
>
|
|
||||||
{{ passwordErrors[passwordChangeStatus.error] }}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button type="submit" class="btn" @click.prevent="changePassword">
|
|
||||||
Change password
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="$store.state.settings.advanced">
|
|
||||||
<h2>Custom Stylesheet</h2>
|
|
||||||
<label for="user-specified-css-input" class="sr-only">
|
|
||||||
Custom stylesheet. You can override any style with CSS here.
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
id="user-specified-css-input"
|
|
||||||
:value="$store.state.settings.userStyles"
|
|
||||||
class="input"
|
|
||||||
name="userStyles"
|
|
||||||
placeholder="/* You can override any style with CSS here */"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="!$store.state.serverConfiguration.public" class="session-list" role="group">
|
|
||||||
<h2>Sessions</h2>
|
|
||||||
|
|
||||||
<h3>Current session</h3>
|
|
||||||
<Session v-if="currentSession" :session="currentSession" />
|
|
||||||
|
|
||||||
<template v-if="activeSessions.length > 0">
|
|
||||||
<h3>Active sessions</h3>
|
|
||||||
<Session
|
|
||||||
v-for="session in activeSessions"
|
|
||||||
:key="session.token"
|
|
||||||
:session="session"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<h3>Other sessions</h3>
|
|
||||||
<p v-if="$store.state.sessions.length === 0">Loading…</p>
|
|
||||||
<p v-else-if="otherSessions.length === 0">
|
|
||||||
<em>You are not currently logged in to any other device.</em>
|
|
||||||
</p>
|
|
||||||
<Session
|
|
||||||
v-for="session in otherSessions"
|
|
||||||
v-else
|
|
||||||
:key="session.token"
|
|
||||||
:session="session"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
|
||||||
textarea#user-specified-css-input {
|
|
||||||
height: 100px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import socket from "../../js/socket";
|
|
||||||
import webpush from "../../js/webpush";
|
|
||||||
import RevealPassword from "../RevealPassword.vue";
|
|
||||||
import Session from "../Session.vue";
|
|
||||||
import SidebarToggle from "../SidebarToggle.vue";
|
import SidebarToggle from "../SidebarToggle.vue";
|
||||||
|
import Navigation from "../Settings/Navigation.vue";
|
||||||
let installPromptEvent = null;
|
|
||||||
|
|
||||||
window.addEventListener("beforeinstallprompt", (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
installPromptEvent = e;
|
|
||||||
});
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Settings",
|
name: "Settings",
|
||||||
components: {
|
components: {
|
||||||
RevealPassword,
|
|
||||||
Session,
|
|
||||||
SidebarToggle,
|
SidebarToggle,
|
||||||
},
|
Navigation,
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
canRegisterProtocol: false,
|
|
||||||
passwordChangeStatus: null,
|
|
||||||
passwordErrors: {
|
|
||||||
missing_fields: "Please enter a new password",
|
|
||||||
password_mismatch: "Both new password fields must match",
|
|
||||||
password_incorrect:
|
|
||||||
"The current password field does not match your account password",
|
|
||||||
update_failed: "Failed to update your password",
|
|
||||||
},
|
|
||||||
isIOS: navigator.platform.match(/(iPhone|iPod|iPad)/i) || false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
hasInstallPromptEvent() {
|
|
||||||
// TODO: This doesn't hide the button after clicking
|
|
||||||
return installPromptEvent !== null;
|
|
||||||
},
|
|
||||||
currentSession() {
|
|
||||||
return this.$store.state.sessions.find((item) => item.current);
|
|
||||||
},
|
|
||||||
activeSessions() {
|
|
||||||
return this.$store.state.sessions.filter((item) => !item.current && item.active > 0);
|
|
||||||
},
|
|
||||||
otherSessions() {
|
|
||||||
return this.$store.state.sessions.filter((item) => !item.current && !item.active);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
socket.emit("sessions:get");
|
|
||||||
|
|
||||||
// Enable protocol handler registration if supported,
|
|
||||||
// and the network configuration is not locked
|
|
||||||
this.canRegisterProtocol =
|
|
||||||
window.navigator.registerProtocolHandler &&
|
|
||||||
!this.$store.state.serverConfiguration.lockNetwork;
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onChange(event) {
|
onChange(event) {
|
||||||
|
@ -615,62 +43,6 @@ export default {
|
||||||
|
|
||||||
this.$store.dispatch("settings/update", {name, value, sync: true});
|
this.$store.dispatch("settings/update", {name, value, sync: true});
|
||||||
},
|
},
|
||||||
changePassword() {
|
|
||||||
const allFields = new FormData(this.$refs.settingsForm);
|
|
||||||
const data = {
|
|
||||||
old_password: allFields.get("old_password"),
|
|
||||||
new_password: allFields.get("new_password"),
|
|
||||||
verify_password: allFields.get("verify_password"),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!data.old_password || !data.new_password || !data.verify_password) {
|
|
||||||
this.passwordChangeStatus = {
|
|
||||||
success: false,
|
|
||||||
error: "missing_fields",
|
|
||||||
};
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.new_password !== data.verify_password) {
|
|
||||||
this.passwordChangeStatus = {
|
|
||||||
success: false,
|
|
||||||
error: "password_mismatch",
|
|
||||||
};
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.once("change-password", (response) => {
|
|
||||||
this.passwordChangeStatus = response;
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.emit("change-password", data);
|
|
||||||
},
|
|
||||||
onForceSyncClick() {
|
|
||||||
this.$store.dispatch("settings/syncAll", true);
|
|
||||||
this.$store.dispatch("settings/update", {
|
|
||||||
name: "syncSettings",
|
|
||||||
value: true,
|
|
||||||
sync: true,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
registerProtocol() {
|
|
||||||
const uri = document.location.origin + document.location.pathname + "?uri=%s";
|
|
||||||
|
|
||||||
window.navigator.registerProtocolHandler("irc", uri, "The Lounge");
|
|
||||||
window.navigator.registerProtocolHandler("ircs", uri, "The Lounge");
|
|
||||||
},
|
|
||||||
nativeInstallPrompt() {
|
|
||||||
installPromptEvent.prompt();
|
|
||||||
installPromptEvent = null;
|
|
||||||
},
|
|
||||||
playNotification() {
|
|
||||||
const pop = new Audio();
|
|
||||||
pop.src = "audio/pop.wav";
|
|
||||||
pop.play();
|
|
||||||
},
|
|
||||||
onPushButtonClick() {
|
|
||||||
webpush.togglePushSubscription();
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -298,6 +298,7 @@ p {
|
||||||
#connect .extra-help,
|
#connect .extra-help,
|
||||||
#settings .extra-help,
|
#settings .extra-help,
|
||||||
#settings #play::before,
|
#settings #play::before,
|
||||||
|
#settings .settings-menu .icon::before,
|
||||||
#form #upload::before,
|
#form #upload::before,
|
||||||
#form #submit::before,
|
#form #submit::before,
|
||||||
#chat .msg[data-type="away"] .from::before,
|
#chat .msg[data-type="away"] .from::before,
|
||||||
|
@ -397,7 +398,9 @@ p {
|
||||||
}
|
}
|
||||||
|
|
||||||
#footer .connect::before { content: "\f067"; /* http://fontawesome.io/icon/plus/ */ }
|
#footer .connect::before { content: "\f067"; /* http://fontawesome.io/icon/plus/ */ }
|
||||||
|
|
||||||
#footer .settings::before { content: "\f013"; /* http://fontawesome.io/icon/cog/ */ }
|
#footer .settings::before { content: "\f013"; /* http://fontawesome.io/icon/cog/ */ }
|
||||||
|
|
||||||
#footer .help::before { content: "\f059"; /* http://fontawesome.io/icon/question/ */ }
|
#footer .help::before { content: "\f059"; /* http://fontawesome.io/icon/question/ */ }
|
||||||
|
|
||||||
#form #upload::before { content: "\f0c6"; /* https://fontawesome.com/icons/paperclip?style=solid */ }
|
#form #upload::before { content: "\f0c6"; /* https://fontawesome.com/icons/paperclip?style=solid */ }
|
||||||
|
@ -722,7 +725,7 @@ background on hover (unless active) */
|
||||||
background-color: rgb(48 62 74 / 50%); /* #303e4a x 50% alpha */
|
background-color: rgb(48 62 74 / 50%); /* #303e4a x 50% alpha */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Darker background and defualt cursor for active channels */
|
/* Darker background and default cursor for active channels */
|
||||||
#footer button.active,
|
#footer button.active,
|
||||||
.channel-list-item.active {
|
.channel-list-item.active {
|
||||||
background-color: #303e4a;
|
background-color: #303e4a;
|
||||||
|
|
|
@ -17,6 +17,11 @@ import SearchResults from "../components/Windows/SearchResults.vue";
|
||||||
import RoutedChat from "../components/RoutedChat.vue";
|
import RoutedChat from "../components/RoutedChat.vue";
|
||||||
import store from "./store";
|
import store from "./store";
|
||||||
|
|
||||||
|
import AppearanceSettings from "../components/Settings/Appearance.vue";
|
||||||
|
import GeneralSettings from "../components/Settings/General.vue";
|
||||||
|
import AccountSettings from "../components/Settings/Account.vue";
|
||||||
|
import NotificationSettings from "../components/Settings/Notifications.vue";
|
||||||
|
|
||||||
const router = new VueRouter({
|
const router = new VueRouter({
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
|
@ -40,9 +45,30 @@ const router = new VueRouter({
|
||||||
props: (route) => ({queryParams: route.query}),
|
props: (route) => ({queryParams: route.query}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Settings",
|
|
||||||
path: "/settings",
|
path: "/settings",
|
||||||
component: Settings,
|
component: Settings,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: "General",
|
||||||
|
path: "",
|
||||||
|
component: GeneralSettings,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Appearance",
|
||||||
|
path: "appearance",
|
||||||
|
component: AppearanceSettings,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Account",
|
||||||
|
path: "account",
|
||||||
|
component: AccountSettings,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Notifications",
|
||||||
|
path: "notifications",
|
||||||
|
component: NotificationSettings,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Help",
|
name: "Help",
|
||||||
|
|
Loading…
Reference in a new issue