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}`,
icon: "fa fa-comment-slash",
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 ` + `chat messages or any DMs they send you going forward. Also, ${username} will ` +
`not be able to see whether your webcam is active until you unmute them.` `not be able to see whether your webcam is active until you unmute them.`,
)) { }).then(() => {
return;
}
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}`,
icon: "fa fa-comment",
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, ` + `will be able to see their chat messages or DMs going forward, but most importantly, ` +
`they may be able to watch your webcam now if you are broadcasting!`, `they may be able to watch your webcam now if you are broadcasting!`,
)) { }).then(() => {
return;
}
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; let channel = this.channel;
this.setChannel(this.config.channels[0].ID); this.setChannel(this.config.channels[0].ID);
delete (this.channels[channel]); delete (this.channels[channel]);
delete (this.directMessageHistory[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."
}).then(() => {
this.client.send({ 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."
}).then(() => {
this.onTakeback({ this.onTakeback({
msgID: msg.msgID, msgID: msg.msgID,
}); });
})
}, },
/* message reaction emojis */ /* message reaction emojis */
@ -2586,24 +2629,25 @@ 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?`
}).then(() => {
this.sendUnboot(username); this.sendUnboot(username);
delete (this.WebRTC.booted[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",
icon: "fa fa-user-xmark",
message: `Kick ${username} off your camera? This will also prevent them ` +
`from seeing that your camera is active for the remainder of your ` + `from seeing that your camera is active for the remainder of your ` +
`chat session.`)) { `chat session.`
return; }).then(() => {
}
this.sendBoot(username); this.sendBoot(username);
this.WebRTC.booted[username] = true; this.WebRTC.booted[username] = true;
@ -2621,6 +2665,7 @@ export default {
`to them it appears as though you had turned yours off.<br><br>This will be ` + `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.` `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,10 +3558,7 @@ 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);
@ -3557,6 +3600,7 @@ export default {
}).finally(() => { }).finally(() => {
this.clearDirectMessages.busy = false; 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()">