Refactor some modals and features into components

Move some chat modals into external components:
* LoginModal
* ExplicitOpenModal
* ReportModal
* The Photo Modal was hoisted into the main index.html page, because it is not
  a Vue component and relied on global onclick handlers and the DOM.

Spin off some external JS modules:
* isAppleWebkit moved to lib/browsers.js
* Local Storage management centralized and moved to lib/LocalStorage.js
This commit is contained in:
Noah 2023-09-06 23:03:12 -07:00
parent e728644a77
commit 8906e89a51
7 changed files with 840 additions and 785 deletions

View File

@ -12,6 +12,18 @@
<title>{{.Config.Title}}</title> <title>{{.Config.Title}}</title>
</head> </head>
<body> <body>
<!-- Photo Detail Modal -->
<div class="modal" id="photo-modal">
<div class="modal-background" onclick="document.querySelector('#photo-modal').classList.remove('is-active')"></div>
<div class="modal-content photo-modal">
<div class="image is-fullwidth">
<img id="modalImage">
</div>
</div>
<button class="modal-close is-large" aria-label="close" onclick="document.querySelector('#photo-modal').classList.remove('is-active')"></button>
</div>
<div id="app"></div> <div id="app"></div>
<!-- BareRTC constants injected by IndexPage route --> <!-- BareRTC constants injected by IndexPage route -->
@ -27,6 +39,15 @@
const UserJWTValid = {{if .JWTAuthOK}}true{{else}}false{{end}}; const UserJWTValid = {{if .JWTAuthOK}}true{{else}}false{{end}};
const UserJWTClaims = {{.JWTClaims.ToJSON}}; const UserJWTClaims = {{.JWTClaims.ToJSON}};
const CachedBlocklist = {{.CachedBlocklist}}; const CachedBlocklist = {{.CachedBlocklist}};
// Show the photo detail modal.
function setModalImage(url) {
let $modalImg = document.querySelector("#modalImage"),
$modal = document.querySelector("#photo-modal");
$modalImg.src = url;
$modal.classList.add("is-active");
return false;
}
</script> </script>
<script type="module" src="/src/main.js"></script> <script type="module" src="/src/main.js"></script>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,71 @@
<script>
export default {
props: {
visible: Boolean,
user: Object,
},
data() {
return {
dontShowAgain: false,
};
},
methods: {
accept() {
if (this.dontShowAgain) {
this.$emit('dont-show-again');
}
this.$emit('accept');
},
cancel() {
this.$emit('cancel');
},
}
}
</script>
<template>
<!-- NSFW Modal: before user views a NSFW camera the first time -->
<div class="modal" :class="{ 'is-active': visible }">
<div class="modal-background"></div>
<div class="modal-content">
<div class="card">
<header class="card-header has-background-info">
<p class="card-header-title has-text-light">This camera may contain Explicit content</p>
</header>
<div class="card-content">
<p class="block">
This camera has been marked as "Explicit/<abbr title="Not Safe For Work">NSFW</abbr>" and may
contain displays of sexuality. If you do not want to see this, look for cameras with
a <span class="button is-small is-info is-outlined px-1"><i class="fa fa-video"></i></span>
blue icon rather than the <span class="button is-small is-danger is-outlined px-1"><i
class="fa fa-video"></i></span>
red ones.
</p>
<div class="field">
<label class="checkbox">
<input type="checkbox" v-model="dontShowAgain">
Don't show this message again
</label>
</div>
<div class="field">
<div class="control has-text-centered">
<button type="button" class="button is-link mr-4"
@click="accept()">
Open webcam
</button>
<button type="button" class="button"
@click="cancel()">
Cancel
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,65 @@
<script>
export default {
props: {
visible: Boolean,
},
data() {
return {
username: '',
};
},
methods: {
signIn() {
this.$emit('signIn', this.username);
}
}
}
</script>
<template>
<div class="modal" :class="{ 'is-active': visible }">
<div class="modal-background"></div>
<div class="modal-content">
<div class="card">
<header class="card-header has-background-info">
<p class="card-header-title has-text-light">Sign In</p>
</header>
<div class="card-content">
<form @submit.prevent="signIn()">
<div v-if="autoLogin" class="content">
<p>
Welcome to <span v-html="config.branding"></span>! Please just click on the "Enter Chat"
button below to log on. Your username has been pre-filled from the website that
sent you here.
</p>
<p>
This dialog box is added as an experiment to see whether it
helps iOS devices (iPads and iPhones) to log in to the chat more reliably, by
having you interact with the page before it connects to the server. Let us
know in chat if your iPhone or iPad is able to log in this way!
</p>
</div>
<div class="field">
<label class="label">Username</label>
<input class="input" v-model="username" placeholder="Username" autocomplete="off" autofocus
:disabled="autoLogin" required>
</div>
<div class="field">
<div class="control">
<button class="button is-link">Enter Chat</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,113 @@
<script>
export default {
props: {
visible: Boolean,
busy: Boolean,
user: Object,
message: Object,
},
data() {
return {
// Configuration
reportClassifications: [
"It's spam",
"It's abusive (racist, homophobic, etc.)",
"It's malicious (e.g. link to a malware website, phishing)",
"It's illegal (e.g. controlled substances, violence)",
"It's child abuse (CP, CSAM, pedophilia, etc.)",
"Other (please describe)",
],
// Our settings.
classification: "It's spam",
comment: "",
};
},
methods: {
accept() {
this.$emit('accept', {
classification: this.classification,
comment: this.comment,
});
},
cancel() {
this.$emit('cancel');
},
}
}
</script>
<template>
<!-- Report Modal -->
<div class="modal" :class="{ 'is-active': visible }">
<div class="modal-background"></div>
<div class="modal-content">
<div class="card">
<header class="card-header has-background-warning">
<p class="card-header-title has-text-dark">Report a message</p>
</header>
<div class="card-content">
<!-- Message preview we are reporting on
TODO: make it DRY: style copied/referenced from chat history cards -->
<div class="box mb-2 px-4 pt-3 pb-1 position-relative">
<div class="media mb-0">
<div class="media-left">
<figure class="image is-48x48">
<img v-if="user?.avatar"
:src="user?.avatar">
<img v-else src="/static/img/shy.png">
</figure>
</div>
<div class="media-content">
<div>
<strong>
<!-- User nickname/display name -->
{{ user?.nickname }}
</strong>
</div>
<!-- User @username below it which may link to a profile URL if JWT -->
<div>
<small class="has-text-grey">
@{{ message.username }}
</small>
</div>
</div>
</div>
<!-- Message copy -->
<div class="content pl-5 py-3 mb-5 report-modal-message" v-html="message.message">
</div>
</div>
<div class="field mb-1">
<label class="label" for="classification">Report classification:</label>
<div class="select is-fullwidth">
<select id="classification" v-model="classification" :disabled="busy">
<option v-for="i in reportClassifications" :value="i">{{ i }}</option>
</select>
</div>
</div>
<div class="field">
<label class="label" for="reportComment">Comment:</label>
<textarea class="textarea" v-model="comment" :disabled="busy" cols="80"
rows="2" placeholder="Optional: describe the issue"></textarea>
</div>
<div class="field">
<div class="control has-text-centered">
<button type="button" class="button is-link mr-4" :disabled="busy"
@click="accept()">Submit report</button>
<button type="button" class="button" @click="cancel()">Cancel</button>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
</style>

79
src/lib/LocalStorage.js Normal file
View File

@ -0,0 +1,79 @@
// All the distinct localStorage keys used.
const keys = {
'fontSizeClass': String, // Text magnification
'videoScale': String, // Video magnification (CSS classnames)
'imageDisplaySetting': String, // Show/hide/expand image preference
'scrollback': Number, // Scrollback buffer (int)
'preferredDeviceNames': Object, // Webcam/mic device names (object, keys video,audio)
// Webcam settings (booleans)
'videoMutual': Boolean,
'videoMutualOpen': Boolean,
'videoAutoMute': Boolean,
'videoVipOnly': Boolean,
// Booleans
'joinMessages': Boolean,
'exitMessages': Boolean,
'watchNotif': Boolean,
'muteSounds': Boolean,
'closeDMs': Boolean, // close unsolicited DMs
// Don't Show Again on NSFW modals.
'skip-nsfw-modal': Boolean,
}
// UserSettings centralizes browser settings for the chat room.
class UserSettings {
constructor() {
// Recall stored settings. Only set the keys that were
// found in localStorage on page load.
for (let key of Object.keys(keys)) {
if (localStorage[key] != undefined) {
switch (keys[key]) {
case String:
this[key] = localStorage[key];
case Number:
this[key] = parseInt(localStorage[key]);
case Boolean:
this[key] = localStorage[key] === "true";
case Object:
this[key] = JSON.parse(localStorage[key]);
}
}
}
console.log("LocalStorage: Loaded settings", this);
}
// Return all of the current settings where the user had actually
// left a preference on them (was in localStorage).
getSettings() {
let result = {};
for (let key of Object.keys(keys)) {
if (this[key] != undefined) {
result[key] = this[key];
}
}
return result;
}
// Get a value from localStorage, if set.
get(key) {
return this[key];
}
// Generic setter.
set(key, value) {
if (keys[key] == undefined) {
throw `${key}: not a supported localStorage setting`;
}
localStorage[key] = JSON.stringify(value);
this[key] = value;
}
}
// LocalStorage is a global singleton to access and update user settings.
const LocalStorage = new UserSettings();
export default LocalStorage;

18
src/lib/browsers.js Normal file
View File

@ -0,0 +1,18 @@
// Try and detect whether the user is on an Apple Safari browser, which has
// special nuances in their WebRTC video sharing support. This is intended to
// detect: iPads, iPhones, and Safari on macOS.
function isAppleWebkit() {
// By User-Agent.
if (/iPad|iPhone|iPod/.test(navigator.userAgent)) {
return true;
}
// By (deprecated) navigator.platform.
if (navigator.platform === 'iPad' || navigator.platform === 'iPhone' || navigator.platform === 'iPod') {
return true;
}
return false;
}
export { isAppleWebkit };