Vue modals to replace window.alert/window.confirm

Apparently some iPad browsers were having their local webcam freeze
after a window.confirm prompt was shown. This replaces all uses of
window.confirm/window.alert with an in-app modal.
This commit is contained in:
Noah 2024-09-10 21:24:52 -07:00
parent d4b69311ae
commit 9b8e7dc440
4 changed files with 255 additions and 119 deletions

View File

@ -5,6 +5,7 @@ import 'floating-vue/dist/style.css';
import { Mentionable } from 'vue-mention'; import { Mentionable } from 'vue-mention';
import EmojiPicker from 'vue3-emoji-picker'; import EmojiPicker from 'vue3-emoji-picker';
import AlertModal from './components/AlertModal.vue';
import LoginModal from './components/LoginModal.vue'; import LoginModal from './components/LoginModal.vue';
import ExplicitOpenModal from './components/ExplicitOpenModal.vue'; import ExplicitOpenModal from './components/ExplicitOpenModal.vue';
import ReportModal from './components/ReportModal.vue'; import ReportModal from './components/ReportModal.vue';
@ -50,6 +51,7 @@ export default {
EmojiPicker, EmojiPicker,
// My components // My components
AlertModal,
LoginModal, LoginModal,
ExplicitOpenModal, ExplicitOpenModal,
ReportModal, ReportModal,
@ -320,6 +322,17 @@ export default {
} }
}, },
// Generic Alert/Confirm modal to replace native browser events.
// See also: modalAlert, modalConfirm functions.
alertModal: {
visible: false,
isConfirm: false,
title: "Alert",
icon: "",
message: "",
callback() {},
},
loginModal: { loginModal: {
visible: false, visible: false,
}, },
@ -1320,23 +1333,25 @@ export default {
} }
if (mute) { if (mute) {
if (!window.confirm( this.modalConfirm({
`Do you want to mute ${username}? If muted, you will no longer see their ` + title: `Mute ${username}`,
`chat messages or any DMs they send you going forward. Also, ${username} will ` + icon: "fa fa-comment-slash",
`not be able to see whether your webcam is active until you unmute them.` message: `Do you want to mute ${username}? If muted, you will no longer see their ` +
)) { `chat messages or any DMs they send you going forward. Also, ${username} will ` +
return; `not be able to see whether your webcam is active until you unmute them.`,
} }).then(() => {
this.muted[username] = true; this.muted[username] = true;
});
} else { } else {
if (!window.confirm( this.modalConfirm({
`Do you want to remove your mute on ${username}? If you un-mute them, you ` + title: `Un-mute ${username}`,
`will be able to see their chat messages or DMs going forward, but most importantly, ` + icon: "fa fa-comment",
`they may be able to watch your webcam now if you are broadcasting!`, message: `Do you want to remove your mute on ${username}? If you un-mute them, you ` +
)) { `will be able to see their chat messages or DMs going forward, but most importantly, ` +
return; `they may be able to watch your webcam now if you are broadcasting!`,
} }).then(() => {
delete this.muted[username]; delete this.muted[username];
});
} }
// Hang up videos both ways. // Hang up videos both ways.
@ -1871,6 +1886,31 @@ export default {
* Front-end web app concerns. * Front-end web app concerns.
*/ */
// Generic window.alert replacement modal.
async modalAlert({ message, title="Alert", icon="", isConfirm=false }) {
return new Promise((resolve, reject) => {
this.alertModal.isConfirm = isConfirm;
this.alertModal.title = title;
this.alertModal.icon = icon;
this.alertModal.message = message;
this.alertModal.callback = () => {
resolve();
};
this.alertModal.visible = true;
});
},
async modalConfirm({ message, title="Confirmation", icon=""}) {
return this.modalAlert({
isConfirm: true,
message,
title,
icon,
})
},
modalClose() {
this.alertModal.visible = false;
},
// Settings modal. // Settings modal.
showSettings() { showSettings() {
this.settingsModal.visible = true; this.settingsModal.visible = true;
@ -2002,39 +2042,42 @@ export default {
// Validate we're in a DM currently. // Validate we're in a DM currently.
if (this.channel.indexOf("@") !== 0) return; if (this.channel.indexOf("@") !== 0) return;
if (!window.confirm( this.modalConfirm({
"Do you want to close this chat thread? Your conversation history will " + title: "Close conversation thread",
"be forgotten on your computer, but your chat partner may still have " + icon: "fa fa-trash",
"your chat thread open on their end." message: "Do you want to close this chat thread? This will remove the conversation from your view, but " +
)) { "your chat partner may still have the conversation open on their device.",
return; }).then(() => {
} let channel = this.channel;
this.setChannel(this.config.channels[0].ID);
let channel = this.channel; delete (this.channels[channel]);
this.setChannel(this.config.channels[0].ID); delete (this.directMessageHistory[channel]);
delete (this.channels[channel]); });
delete (this.directMessageHistory[channel]);
}, },
/* Take back messages (for everyone) or remove locally */ /* Take back messages (for everyone) or remove locally */
takeback(msg) { takeback(msg) {
if (!window.confirm( this.modalConfirm({
"Do you want to take this message back? Doing so will remove this message from everybody's view in the chat room." title: "Take back message",
)) return; icon: "fa fa-rotate-left",
message: "Do you want to take this message back? Doing so will remove this message from everybody's view in the chat room."
this.client.send({ }).then(() => {
this.client.send({
action: "takeback", action: "takeback",
msgID: msg.msgID, msgID: msg.msgID,
}); });
});
}, },
removeMessage(msg) { removeMessage(msg) {
if (!window.confirm( this.modalConfirm({
"Do you want to remove this message from your view? This will delete the message only for you, but others in this chat thread may still see it." title: "Hide this message",
)) return; icon: "fa fa-trash",
message: "Do you want to remove this message from your view? This will delete the message only for you, but others in this chat thread may still see it."
this.onTakeback({ }).then(() => {
msgID: msg.msgID, this.onTakeback({
}); msgID: msg.msgID,
});
})
}, },
/* message reaction emojis */ /* message reaction emojis */
@ -2586,41 +2629,43 @@ export default {
bootUser(username) { bootUser(username) {
// Un-boot? // Un-boot?
if (this.isBooted(username)) { if (this.isBooted(username)) {
if (!window.confirm(`Allow ${username} to watch your webcam again?`)) { this.modalConfirm({
return; title: "Unboot user",
} icon: "fa fa-user-xmark",
message: `Allow ${username} to watch your webcam again?`
this.sendUnboot(username); }).then(() => {
delete (this.WebRTC.booted[username]); this.sendUnboot(username);
delete (this.WebRTC.booted[username]);
})
return; return;
} }
// Boot them off our webcam. // Boot them off our webcam.
if (!window.confirm( this.modalConfirm({
`Kick ${username} off your camera? This will also prevent them ` + title: "Boot user",
`from seeing that your camera is active for the remainder of your ` + icon: "fa fa-user-xmark",
`chat session.`)) { message: `Kick ${username} off your camera? This will also prevent them ` +
return; `from seeing that your camera is active for the remainder of your ` +
} `chat session.`
}).then(() => {
this.sendBoot(username);
this.WebRTC.booted[username] = true;
this.sendBoot(username); // Close the WebRTC peer connections.
this.WebRTC.booted[username] = true; if (this.WebRTC.pc[username] != undefined) {
this.closeVideo(username);
}
// Close the WebRTC peer connections. // Remove them from our list.
if (this.WebRTC.pc[username] != undefined) { delete (this.webcam.watching[username]);
this.closeVideo(username);
}
// Remove them from our list. this.ChatClient(
delete (this.webcam.watching[username]); `You have booted ${username} off your camera. They will no longer be able ` +
`to connect to your camera, or even see that your camera is active at all -- ` +
this.ChatClient( `to them it appears as though you had turned yours off.<br><br>This will be ` +
`You have booted ${username} off your camera. They will no longer be able ` + `in place for the remainder of your current chat session.`
`to connect to your camera, or even see that your camera is active at all -- ` + );
`to them it appears as though you had turned yours off.<br><br>This will be ` + });
`in place for the remainder of your current chat session.`
);
}, },
isBooted(username) { isBooted(username) {
return this.WebRTC.booted[username] === true; return this.WebRTC.booted[username] === true;
@ -3499,12 +3544,13 @@ export default {
this.directMessageHistory[channel].busy = false; this.directMessageHistory[channel].busy = false;
}); });
}, },
async clearMessageHistory(prompt = false) { async clearMessageHistory() {
if (!this.jwt.valid || this.clearDirectMessages.busy) return; if (!this.jwt.valid || this.clearDirectMessages.busy) return;
if (prompt) { this.modalConfirm({
if (!window.confirm( title: "Clear all DMs",
"This will delete all of your DMs history stored on the server. People you have " + icon: "fa fa-exclamation-triangle",
message: "This will delete all of your DMs history stored on the server. People you have " +
"chatted with will have their past messages sent to you erased as well.\n\n" + "chatted with will have their past messages sent to you erased as well.\n\n" +
"Note: messages that are currently displayed on your chat partner's screen will " + "Note: messages that are currently displayed on your chat partner's screen will " +
"NOT be removed by this action -- if this is a concern and you want to 'take back' " + "NOT be removed by this action -- if this is a concern and you want to 'take back' " +
@ -3512,50 +3558,48 @@ export default {
"message you sent to them. The 'clear history' button only clears the database, but " + "message you sent to them. The 'clear history' button only clears the database, but " +
"does not send takebacks to pull the message from everybody else's screen.\n\n" + "does not send takebacks to pull the message from everybody else's screen.\n\n" +
"Are you sure you want to clear your stored DMs history on the server?", "Are you sure you want to clear your stored DMs history on the server?",
)) { }).then(async () => {
return;
}
}
if (this.clearDirectMessages.timeout !== null) { if (this.clearDirectMessages.timeout !== null) {
clearTimeout(this.clearDirectMessages.timeout); clearTimeout(this.clearDirectMessages.timeout);
}
this.clearDirectMessages.busy = true;
return fetch("/api/message/clear", {
method: "POST",
mode: "same-origin",
cache: "no-cache",
credentials: "same-origin",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
"JWTToken": this.jwt.token,
}),
})
.then((response) => response.json())
.then((data) => {
if (data.Error) {
console.error("ClearMessageHistory: ", data.Error);
return;
} }
this.clearDirectMessages.ok = true; this.clearDirectMessages.busy = true;
this.clearDirectMessages.messagesErased = data.MessagesErased; return fetch("/api/message/clear", {
this.clearDirectMessages.timeout = setTimeout(() => { method: "POST",
this.clearDirectMessages.ok = false; mode: "same-origin",
}, 15000); cache: "no-cache",
credentials: "same-origin",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
"JWTToken": this.jwt.token,
}),
})
.then((response) => response.json())
.then((data) => {
if (data.Error) {
console.error("ClearMessageHistory: ", data.Error);
return;
}
this.ChatClient( this.clearDirectMessages.ok = true;
"Your direct message history has been cleared from the server's database. "+ this.clearDirectMessages.messagesErased = data.MessagesErased;
"(" + data.MessagesErased + " messages erased)", this.clearDirectMessages.timeout = setTimeout(() => {
); this.clearDirectMessages.ok = false;
}).catch(resp => { }, 15000);
console.error("DirectMessageHistory: ", resp);
this.ChatClient("Error clearing your chat history: " + resp); this.ChatClient(
}).finally(() => { "Your direct message history has been cleared from the server's database. "+
this.clearDirectMessages.busy = false; "(" + data.MessagesErased + " messages erased)",
);
}).catch(resp => {
console.error("DirectMessageHistory: ", resp);
this.ChatClient("Error clearing your chat history: " + resp);
}).finally(() => {
this.clearDirectMessages.busy = false;
});
}); });
}, },
@ -3571,10 +3615,17 @@ export default {
return false; return false;
}, },
reportMessage(message) { reportMessage(message, force=false) {
// User is reporting a message on chat. // User is reporting a message on chat.
if (message.reported) { if (message.reported && !force) {
if (!window.confirm("You have already reported this message. Do you want to report it again?")) return; this.modalConfirm({
title: "Report Message",
icon: "fa fa-info-circle",
message: "You have already reported this message. Do you want to report it again?",
}).then(() => {
this.reportMessage(message, true);
});
return;
} }
// Clone the user object. // Clone the user object.
@ -3622,6 +3673,15 @@ export default {
</script> </script>
<template> <template>
<!-- Alert/Confirm modal: to avoid blocking the page with native calls. -->
<AlertModal :visible="alertModal.visible"
:is-confirm="alertModal.isConfirm"
:title="alertModal.title"
:icon="alertModal.icon"
:message="alertModal.message"
@callback="alertModal.callback"
@close="modalClose()"></AlertModal>
<!-- Sign In modal --> <!-- Sign In modal -->
<LoginModal :visible="loginModal.visible" @sign-in="signIn"></LoginModal> <LoginModal :visible="loginModal.visible" @sign-in="signIn"></LoginModal>
@ -3641,7 +3701,7 @@ export default {
<div class="modal-content"> <div class="modal-content">
<div class="card"> <div class="card">
<header class="card-header has-background-info"> <header class="card-header has-background-info">
<p class="card-header-title has-text-light">Chat Settings</p> <p class="card-header-title">Chat Settings</p>
</header> </header>
<div class="card-content"> <div class="card-content">
@ -4090,7 +4150,7 @@ export default {
<!-- Clear DMs history on server --> <!-- Clear DMs history on server -->
<div class="field" v-if="this.jwt.valid"> <div class="field" v-if="this.jwt.valid">
<a href="#" @click.prevent="clearMessageHistory(true)" class="button is-small has-text-danger"> <a href="#" @click.prevent="clearMessageHistory()" class="button is-small has-text-danger">
<i class="fa fa-trash mr-1"></i> Clear direct message history <i class="fa fa-trash mr-1"></i> Clear direct message history
</a> </a>

View File

@ -0,0 +1,76 @@
<script>
export default {
props: {
visible: Boolean,
isConfirm: Boolean,
title: String,
icon: String,
message: String,
},
data() {
return {
username: '',
};
},
methods: {
callback() {
this.$emit('close');
this.$emit('callback');
},
close() {
this.$emit('close');
}
}
}
</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">
<i v-if="icon" :class="icon" class="mr-2"></i>
{{ title }}
</p>
<button class="delete mr-3 mt-3" aria-label="close" @click.prevent="close"></button>
</header>
<div class="card-content">
<form @submit.prevent="callback()">
<p class="literal mb-4">{{ message }}</p>
<div class="columns is-centered">
<div class="column is-narrow">
<button type="submit"
class="button is-success px-5">
OK
</button>
<button v-if="isConfirm"
type="button"
class="button is-link ml-3 px-5"
@click="close">
Cancel
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.modal {
/* a high priority modal over other modals. note: bulma's default z-index is 40 for modals */
z-index: 42;
}
.literal {
white-space: pre-wrap;
}
</style>

View File

@ -30,7 +30,7 @@ export default {
<div class="modal-content"> <div class="modal-content">
<div class="card"> <div class="card">
<header class="card-header has-background-info"> <header class="card-header has-background-info">
<p class="card-header-title has-text-light">This camera may contain Explicit content</p> <p class="card-header-title">This camera may contain Explicit content</p>
</header> </header>
<div class="card-content"> <div class="card-content">
<p class="block"> <p class="block">

View File

@ -22,7 +22,7 @@ export default {
<div class="modal-content"> <div class="modal-content">
<div class="card"> <div class="card">
<header class="card-header has-background-info"> <header class="card-header has-background-info">
<p class="card-header-title has-text-light">Sign In</p> <p class="card-header-title">Sign In</p>
</header> </header>
<div class="card-content"> <div class="card-content">
<form @submit.prevent="signIn()"> <form @submit.prevent="signIn()">