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:
parent
e728644a77
commit
8906e89a51
21
index.html
21
index.html
|
@ -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>
|
||||||
|
|
926
src/App.vue
926
src/App.vue
File diff suppressed because it is too large
Load Diff
71
src/components/ExplicitOpenModal.vue
Normal file
71
src/components/ExplicitOpenModal.vue
Normal 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>
|
65
src/components/LoginModal.vue
Normal file
65
src/components/LoginModal.vue
Normal 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>
|
113
src/components/ReportModal.vue
Normal file
113
src/components/ReportModal.vue
Normal 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
79
src/lib/LocalStorage.js
Normal 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
18
src/lib/browsers.js
Normal 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 };
|
Loading…
Reference in New Issue
Block a user