Alert/Confirm Modals + Auto Revoke Certification Photo
* If a Certified member deletes the final picture from their gallery page, their Certification Photo will be automatically rejected and they are instructed to begin the process again from the beginning. * Add nice Alert and Confirm modals around the website in place of the standard browser feature. Note: the inline confirm on submit buttons are still using the standard feature for now, as intercepting submit buttons named "intent" causes problems in getting the final form to submit.
This commit is contained in:
parent
24230915b6
commit
1c013aa8d8
|
@ -171,7 +171,7 @@ func batchDeletePhotos(
|
|||
} {
|
||||
if len(filename) > 0 {
|
||||
if err := pphoto.Delete(filename); err != nil {
|
||||
log.Error("Delete Photo: couldn't remove file from disk: %s: %s", filename, err)
|
||||
session.FlashError(w, r, "Delete Photo: couldn't remove file from disk: %s: %s", filename, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -191,6 +191,15 @@ func batchDeletePhotos(
|
|||
}
|
||||
}
|
||||
|
||||
// Maybe revoke their Certified status if they have cleared out their gallery.
|
||||
for _, owner := range owners {
|
||||
if revoked := models.MaybeRevokeCertificationForEmptyGallery(owner); revoked {
|
||||
if owner.ID == currentUser.ID {
|
||||
session.FlashError(w, r, "Notice: because you have deleted your entire photo gallery, your Certification status has been automatically revoked.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
session.Flash(w, r, "%d photo(s) deleted!", len(photos))
|
||||
}
|
||||
|
||||
|
|
|
@ -2,8 +2,10 @@ package models
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"code.nonshy.com/nonshy/website/pkg/log"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
|
@ -101,6 +103,64 @@ func CountCertificationPhotosNeedingApproval() int64 {
|
|||
return count
|
||||
}
|
||||
|
||||
// MaybeRevokeCertificationForEmptyGallery will delete a user's certification photo if they delete every picture from their gallery.
|
||||
//
|
||||
// Returns true if their certification was revoked.
|
||||
func MaybeRevokeCertificationForEmptyGallery(user *User) bool {
|
||||
cert, err := GetCertificationPhoto(user.ID)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Ignore if their cert photo status is not applicable to be revoked.
|
||||
if cert.Status == CertificationPhotoNeeded || cert.Status == CertificationPhotoRejected {
|
||||
return false
|
||||
}
|
||||
|
||||
if count := CountPhotos(user.ID); count == 0 {
|
||||
// Revoke their cert status.
|
||||
cert.Status = CertificationPhotoRejected
|
||||
cert.SecondaryVerified = false
|
||||
cert.AdminComment = "Your certification photo has been automatically rejected because you have deleted every photo on your gallery. " +
|
||||
"To restore your certified status, please upload photos to your gallery and submit a new Certification Photo for approval."
|
||||
|
||||
if err := cert.Save(); err != nil {
|
||||
log.Error("MaybeRevokeCertificationForEmptyGallery(%s): %s", user.Username, err)
|
||||
}
|
||||
|
||||
// Update the user's Certified flag. Note: we freshly query the user here in case they had JUST deleted
|
||||
// their default profile picture - so that we don't (re)set their old ProfilePhotoID by accident!
|
||||
if user, err := GetUser(user.ID); err == nil {
|
||||
user.Certified = false
|
||||
if err := user.Save(); err != nil {
|
||||
log.Error("MaybeRevokeCertificationForEmptyGallery(%s): saving user certified flag: %s", user.Username, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Notify the site admin for visibility.
|
||||
fb := &Feedback{
|
||||
Intent: "report",
|
||||
Subject: "A certified user has deleted all their pictures",
|
||||
UserID: user.ID,
|
||||
TableName: "users",
|
||||
TableID: user.ID,
|
||||
Message: fmt.Sprintf(
|
||||
"The username **@%s** has deleted every picture in their gallery, and so their Certification Photo status has been revoked.",
|
||||
user.Username,
|
||||
),
|
||||
}
|
||||
|
||||
// Save the feedback.
|
||||
if err := CreateFeedback(fb); err != nil {
|
||||
log.Error("Couldn't save feedback from user auto-revoking their cert photo: %s", err)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Save photo.
|
||||
func (p *CertificationPhoto) Save() error {
|
||||
result := DB.Save(p)
|
||||
|
|
|
@ -162,6 +162,7 @@ func (t *Template) Reload() error {
|
|||
// Base template layout.
|
||||
var baseTemplates = []string{
|
||||
config.TemplatePath + "/base.html",
|
||||
config.TemplatePath + "/partials/alert_modal.html",
|
||||
config.TemplatePath + "/partials/user_avatar.html",
|
||||
config.TemplatePath + "/partials/like_modal.html",
|
||||
config.TemplatePath + "/partials/right_click.html",
|
||||
|
|
108
web/static/js/alert-modal.js
Normal file
108
web/static/js/alert-modal.js
Normal file
|
@ -0,0 +1,108 @@
|
|||
/**
|
||||
* Alert and Confirm modals.
|
||||
*
|
||||
* Usage:
|
||||
*
|
||||
* modalAlert({message: "Hello world!"}).then(callback);
|
||||
* modalConfirm({message: "Are you sure?"}).then(callback);
|
||||
*
|
||||
* Available options for modalAlert:
|
||||
* - message
|
||||
* - title: Alert
|
||||
*
|
||||
* Available options for modalConfirm:
|
||||
* - message
|
||||
* - title: Confirm
|
||||
* - buttons: ["Ok", "Cancel"]
|
||||
* - event (pass `event` for easy inline onclick handlers)
|
||||
* - element (pass `this` for easy inline onclick handlers)
|
||||
*
|
||||
* Example onclick for modalConfirm:
|
||||
*
|
||||
* <button onclick="modalConfirm({message: 'Are you sure?', event, element}">Delete</button>
|
||||
*
|
||||
* The `element` is used to find the nearest <form> and submit it on OK.
|
||||
* The `event` is used to cancel the submit button's default.
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const $modal = document.querySelector("#nonshy-alert-modal"),
|
||||
$ok = $modal.querySelector("button.nonshy-alert-ok-button"),
|
||||
$cancel = $modal.querySelector("button.nonshy-alert-cancel-button"),
|
||||
$title = $modal.querySelector("#nonshy-alert-modal-title"),
|
||||
$body = $modal.querySelector("#nonshy-alert-modal-body"),
|
||||
cls = 'is-active';
|
||||
|
||||
// Current caller's promise.
|
||||
var currentPromise = null;
|
||||
|
||||
const hideModal = () => {
|
||||
currentPromise = null;
|
||||
$modal.classList.remove(cls);
|
||||
};
|
||||
|
||||
const showModal = ({
|
||||
message,
|
||||
title="Alert",
|
||||
isConfirm=false,
|
||||
buttons=["Ok", "Cancel"],
|
||||
}) => {
|
||||
$ok.innerHTML = buttons[0];
|
||||
$cancel.innerHTML = buttons[1];
|
||||
$cancel.style.display = isConfirm ? "" : "none";
|
||||
|
||||
// Strip HTML from message but allow line breaks.
|
||||
message = message.replace(/</g, "<");
|
||||
message = message.replace(/>/g, ">");
|
||||
message = message.replace(/\n/g, "<br>");
|
||||
|
||||
$title.innerHTML = title;
|
||||
$body.innerHTML = message;
|
||||
|
||||
// Show the modal.
|
||||
$modal.classList.add(cls);
|
||||
|
||||
// Return as a promise.
|
||||
return new Promise((resolve, reject) => {
|
||||
currentPromise = resolve;
|
||||
});
|
||||
};
|
||||
|
||||
// Click events for the modal buttons.
|
||||
$ok.addEventListener('click', (e) => {
|
||||
if (currentPromise !== null) {
|
||||
currentPromise();
|
||||
}
|
||||
hideModal();
|
||||
});
|
||||
$cancel.addEventListener('click', (e) => {
|
||||
hideModal();
|
||||
});
|
||||
|
||||
// Key bindings to dismiss the modal.
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if ($modal.classList.contains(cls)) {
|
||||
if (e.key == 'Enter') {
|
||||
$ok.click();
|
||||
} else if (e.key == 'Escape') {
|
||||
$cancel.click();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Exported global functions to invoke the modal.
|
||||
window.modalAlert = async ({ message, title="Alert" }) => {
|
||||
return showModal({
|
||||
message,
|
||||
title,
|
||||
isConfirm: false,
|
||||
});
|
||||
};
|
||||
window.modalConfirm = async ({ message, title="Confirm", buttons=["Ok", "Cancel"] }) => {
|
||||
return showModal({
|
||||
message,
|
||||
title,
|
||||
isConfirm: true,
|
||||
buttons,
|
||||
});
|
||||
};
|
||||
});
|
|
@ -43,7 +43,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data.StatusCode !== 200) {
|
||||
window.alert(data.data.error);
|
||||
modalAlert({message: data.data.error});
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -54,7 +54,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
$label.innerHTML = `Like (${likes})`;
|
||||
}
|
||||
}).catch(resp => {
|
||||
window.alert(resp);
|
||||
console.error("Like:", resp);
|
||||
}).finally(() => {
|
||||
busy = false;
|
||||
})
|
||||
|
|
|
@ -85,15 +85,16 @@
|
|||
$dob = document.querySelector("#dob");
|
||||
|
||||
$manualEntry.addEventListener("click", function(e) {
|
||||
$manualEntry.blur();
|
||||
e.preventDefault();
|
||||
|
||||
let answer = window.prompt("Enter your birthdate in 'YYYY-MM-DD' format").trim().replace(/\//g, '-');
|
||||
if (answer.match(/^(\d{2})-(\d{2})-(\d{4})/)) {
|
||||
let group = answer.match(/^(\d{2})-(\d{2})-(\d{4})/);
|
||||
answer = `${group[3]}-${group[1]}-${group[2]}`;
|
||||
window.alert(`NOTE: Your input was interpreted to be in MM/DD/YYYY order and has been read as: ${answer}`);
|
||||
modalAlert({message: `NOTE: Your input was interpreted to be in MM/DD/YYYY order and has been read as: ${answer}`});
|
||||
} else if (!answer.match(/^\d{4}-\d{2}-\d{2}/)) {
|
||||
window.alert(`Please enter the date in YYYY-MM-DD format.`);
|
||||
modalAlert({message: `Please enter the date in YYYY-MM-DD format.`});
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -777,16 +777,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
$deleteButton.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (!dontAskAgain) {
|
||||
if (!window.confirm(
|
||||
"Do you want to DELETE this notification?\n\nNote: If you click Ok, you will not be asked "+
|
||||
"the next time you want to delete another notification until your next page reload."
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
dontAskAgain = true;
|
||||
}
|
||||
|
||||
let callback = () => {
|
||||
busy = true;
|
||||
return fetch("/v1/notifications/delete", {
|
||||
method: "POST",
|
||||
|
@ -807,10 +799,23 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
// Hide the notification row immediately.
|
||||
node.style.display = 'none';
|
||||
}).catch(resp => {
|
||||
window.alert(resp);
|
||||
console.error(resp);
|
||||
}).finally(() => {
|
||||
busy = false;
|
||||
});
|
||||
};
|
||||
|
||||
if (!dontAskAgain) {
|
||||
modalConfirm({
|
||||
message: "Do you want to DELETE this notification?\n\nNote: If you click Ok, you will not be asked "+
|
||||
"the next time you want to delete another notification until your next page reload.",
|
||||
}).then(() => {
|
||||
dontAskAgain = true;
|
||||
callback();
|
||||
});
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
|
||||
// If the notification doesn't have a "NEW!" badge, no action needed.
|
||||
|
@ -853,7 +858,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
.then((data) => {
|
||||
console.log(data);
|
||||
}).catch(resp => {
|
||||
window.alert(resp);
|
||||
console.error(resp);
|
||||
}).finally(() => {
|
||||
busy = false;
|
||||
if (href !== undefined && href !== "#") {
|
||||
|
@ -875,11 +880,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
|
||||
const link = node.dataset.link,
|
||||
prompt = node.dataset.confirm.replace(/\\n/g, "\n");
|
||||
if (!window.confirm(prompt)) return;
|
||||
|
||||
modalConfirm({message: prompt}).then(() => {
|
||||
window.location = link;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
|
|
|
@ -253,7 +253,7 @@ window.addEventListener("DOMContentLoaded", (event) => {
|
|||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data.StatusCode !== 200) {
|
||||
window.alert(data.data.error);
|
||||
modalAlert({message: data.data.error});
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -266,7 +266,7 @@ window.addEventListener("DOMContentLoaded", (event) => {
|
|||
$unError.style.display = "block";
|
||||
}
|
||||
}).catch(resp => {
|
||||
window.alert(resp);
|
||||
console.error(resp);
|
||||
}).finally(() => {
|
||||
$unCheck.style.display = "none";
|
||||
})
|
||||
|
@ -283,15 +283,16 @@ window.addEventListener("DOMContentLoaded", (event) => {
|
|||
|
||||
if ($manualEntry != undefined) {
|
||||
$manualEntry.addEventListener("click", function(e) {
|
||||
$manualEntry.blur();
|
||||
e.preventDefault();
|
||||
|
||||
let answer = window.prompt("Enter your birthdate in 'YYYY-MM-DD' format").trim().replace(/\//g, '-');
|
||||
if (answer.match(/^(\d{2})-(\d{2})-(\d{4})/)) {
|
||||
let group = answer.match(/^(\d{2})-(\d{2})-(\d{4})/);
|
||||
answer = `${group[3]}-${group[1]}-${group[2]}`;
|
||||
window.alert(`NOTE: Your input was interpreted to be in MM/DD/YYYY order and has been read as: ${answer}`);
|
||||
modalAlert({message: `NOTE: Your input was interpreted to be in MM/DD/YYYY order and has been read as: ${answer}`});
|
||||
} else if (!answer.match(/^\d{4}-\d{2}-\d{2}/)) {
|
||||
window.alert(`Please enter the date in YYYY-MM-DD format.`);
|
||||
modalAlert({message: `Please enter the date in YYYY-MM-DD format.`});
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -126,7 +126,7 @@
|
|||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data.StatusCode !== 200) {
|
||||
window.alert(data.data.error);
|
||||
modalAlert({message: data.data.error});
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -139,7 +139,7 @@
|
|||
$unError.style.display = "block";
|
||||
}
|
||||
}).catch(resp => {
|
||||
window.alert(resp);
|
||||
console.error(resp);
|
||||
}).finally(() => {
|
||||
$unCheck.style.display = "none";
|
||||
})
|
||||
|
|
|
@ -404,6 +404,9 @@
|
|||
{{- end}}
|
||||
{{template "scripts" .}}
|
||||
|
||||
<!-- Alert modal -->
|
||||
{{template "alert-modal"}}
|
||||
|
||||
<!-- Likes modal -->
|
||||
{{template "like-modal"}}
|
||||
|
||||
|
|
|
@ -296,7 +296,7 @@
|
|||
<button type="submit"
|
||||
name="intent"
|
||||
value="submit"
|
||||
class="button is-success">
|
||||
class="button ml-4 is-success">
|
||||
Post Message
|
||||
</button>
|
||||
</div>
|
||||
|
@ -430,10 +430,11 @@
|
|||
|
||||
removeOption(idx) {
|
||||
if (this.answers.length <= 2) {
|
||||
if (!window.confirm("A poll needs at least two options. Remove the poll?")) {
|
||||
return;
|
||||
}
|
||||
modalConfirm({
|
||||
message: "A poll needs at least two options. Remove the poll?",
|
||||
}).then(() => {
|
||||
this.removePoll();
|
||||
})
|
||||
return;
|
||||
}
|
||||
this.answers.splice(idx, 1);
|
||||
|
|
30
web/templates/partials/alert_modal.html
Normal file
30
web/templates/partials/alert_modal.html
Normal file
|
@ -0,0 +1,30 @@
|
|||
<!-- "Alert" & "Confirm" modals to replace native browser features. -->
|
||||
|
||||
{{define "alert-modal"}}
|
||||
<div class="modal nonshy-important-modal" id="nonshy-alert-modal">
|
||||
<div class="modal-background"></div>
|
||||
<div class="modal-content">
|
||||
<div class="card">
|
||||
<div class="card-header has-background-warning">
|
||||
<p class="card-header-title has-text-dark-dark">
|
||||
<span id="nonshy-alert-modal-title">Alert</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-content content">
|
||||
<div id="nonshy-alert-modal-body">Alert</div>
|
||||
</div>
|
||||
<div class="card-footer has-text-centered">
|
||||
<div class="card-footer-item">
|
||||
<button type="button" class="button is-success nonshy-alert-ok-button">
|
||||
Ok
|
||||
</button>
|
||||
<button type="button" class="button ml-4 nonshy-alert-cancel-button">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript" src="/static/js/alert-modal.js?build={{.BuildHash}}"></script>
|
||||
{{end}}
|
|
@ -187,7 +187,7 @@
|
|||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data.StatusCode !== 200) {
|
||||
window.alert(data.data.error);
|
||||
modalAlert({message: data.data.error});
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -196,7 +196,7 @@
|
|||
this.total = data.data.pager.Total;
|
||||
this.result = likes;
|
||||
}).catch(resp => {
|
||||
window.alert(resp);
|
||||
console.error(resp);
|
||||
}).finally(() => {
|
||||
this.busy = false;
|
||||
});
|
||||
|
|
|
@ -166,7 +166,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data.StatusCode !== 200) {
|
||||
window.alert(data.data.error);
|
||||
modalAlert({message: data.data.error});
|
||||
return;
|
||||
}
|
||||
showHide(false);
|
||||
|
@ -182,10 +182,13 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||
}
|
||||
|
||||
setTimeout(() => {
|
||||
window.alert("This photo has been flagged to be marked as Explicit. Thanks for your help!");
|
||||
modalAlert({
|
||||
message: "This photo has been flagged to be marked as Explicit. Thanks for your help!",
|
||||
title: "Marked Explicit!",
|
||||
});
|
||||
}, 200);
|
||||
}).catch(resp => {
|
||||
window.alert(resp);
|
||||
modalAlert({message: resp});
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -913,7 +913,7 @@
|
|||
$modalImg.title = altText;
|
||||
$altText.style.display = altText ? "block" : "none";
|
||||
$altText.onclick = (e) => {
|
||||
window.alert(altText);
|
||||
modalAlert({message: altText, title: "Alt Text"});
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return false;
|
||||
|
@ -936,7 +936,7 @@
|
|||
console.error("When marking photo %d as viewed: status code %d: %s", photoID, data.StatusCode, data.data.error);
|
||||
return;
|
||||
}
|
||||
}).catch(window.alert);
|
||||
}).catch(console.error);
|
||||
}
|
||||
|
||||
document.querySelectorAll(".js-modal-trigger").forEach(node => {
|
||||
|
|
|
@ -568,7 +568,7 @@
|
|||
let onFile = (file) => {
|
||||
// Too large? (8 MB GIFs) - NOTE: also see config.go so this matches.
|
||||
if (file.size >= 1024*1024*8) {
|
||||
window.alert("That file is too large! Choose something less than 8 MB.");
|
||||
modalAlert({message: "That file is too large! Choose something less than 8 MB."});
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user