website/web/templates/photo/upload.html
Noah Petherbridge 295183559d Admin labels on photos surrounding explicit flags
* Add 'admin labels' to photos so an admin can classify a photo as:
  * Not Explicit: e.g. it was flagged by the community but does not
    actually need to be explicit. This option will hide the prompt to
    report the explicit photo again.
  * Force Explicit: if a user is fighting an explicit flag and keeps
    removing it from their photo, the photo can be force marked
    explicit.
* Admin labels appear on the Permalink page and in the edit photo
  settings when viewed as a photo moderator admin.
2024-10-02 16:22:19 -07:00

742 lines
38 KiB
HTML

{{define "title"}}Upload a Photo{{end}}
{{define "content"}}
<div class="container">
<section class="hero is-link is-bold">
<div class="hero-body">
<div class="container">
<h1 class="title">
{{if .EditPhoto}}
Edit Photo
{{else if eq .Intent "profile_pic"}}
Upload a Profile Picture
{{else}}
Upload a Photo
{{end}}
</h1>
</div>
</div>
</section>
{{ $Root := . }}
{{ $User := .CurrentUser }}
<!-- Drag/Drop Modal -->
<div class="modal" id="drop-modal">
<div class="modal-background"></div>
<div class="modal-content">
<div class="box content has-text-centered">
<h1><i class="fa fa-upload mr-2"></i> Drop image to select it for upload</h1>
</div>
</div>
</div>
{{if .EditPhoto}}
<form action="/photo/edit" method="POST">
<input type="hidden" name="id" value="{{.EditPhoto.ID}}">
{{else}}
<form action="/photo/upload" method="POST" enctype="multipart/form-data">
{{end}}
{{InputCSRF}}
<input type="hidden" id="intent" name="intent" value="{{.Intent}}">
<div class="block p-4">
<!-- Upload disclaimers, but not if editing a photo -->
{{if not .EditPhoto}}
<div class="content block">
<p>
You can use this page to upload a new photo to your profile. Please remember
the rules below:
</p>
<ul>
<li>
🤳 <strong>Self pictures only:</strong> you may only upload pictures which depict
<em>you</em> in them. If the picture also contains other people, be sure you
have their consent to post it here!
</li>
<li>
🔞 <strong>Mark whether your picture is explicit:</strong> not all nudists want to
see sexual content or close-up shots of genitalia. If your picture is not a
"normal nude" please check the Explicit box to help the rest of us out!
</li>
<li>
🧑 <strong>Your main profile picture must show your face:</strong> it doesn't have
to be a nude pic but your face needs to be in it. Additional photos uploaded to
your page do not need to require your face in them.
</li>
</ul>
</div>
{{end}}
<!-- Quota notification -->
{{if not .EditPhoto}}
<div class="notification {{if ge .PhotoCount .PhotoQuota}}is-warning{{else}}is-info{{end}} block">
<p class="block">
You have currently uploaded <strong>{{.PhotoCount}}</strong> of your allowed {{.PhotoQuota}} photos.
{{if ge .PhotoCount .PhotoQuota}}
To upload a new photo, please <a href="/u/{{.CurrentUser.Username}}/photos">delete</a>
an existing photo first to make room.
{{end}}
</p>
{{if and (eq .Intent "profile_pic") (ge .PhotoCount .PhotoQuota)}}
<p class="block">
You have reached your maximum number of photos. <strong>To use one of your existing photos as your profile picture,</strong>
please visit your <a href="/u/{{.CurrentUser.Username}}/photos">Gallery page</a>, click on the "Edit" button for a photo,
then the button to set it as your profile picture will appear on the Edit page. Remember to click "Save" when done!
</p>
{{else}}
<p class="block">
You may upload <strong>{{SubtractInt .PhotoQuota .PhotoCount}}</strong> more photo{{Pluralize (SubtractInt .PhotoQuota .PhotoCount)}}.
{{if not .CurrentUser.Certified}}
After your account has been <a href="/photo/certification">certified</a>, you will be able to upload
additional pictures.
{{end}}
</p>
{{end}}
</div>
{{end}}
<!-- No profile picture and they aren't uploading one now? -->
{{if .NoProfilePicture}}
<div class="notification is-warning block">
<p class="block">
<strong>Notice:</strong> you currently do not have a Default Profile Picture set up on your
account. You can <a href="/photo/upload?intent=profile_pic">click here to upload a new one</a>
or click the Edit button on <a href="/u/{{.CurrentUser.Username}}/photos">one of your existing photos</a>
to crop a square profile picture from one of them.
</p>
</div>
{{end}}
{{if or .EditPhoto (lt .PhotoCount .PhotoQuota)}}
<div class="columns">
<div class="column">
<div class="card">
<header class="card-header has-background-link">
<p class="card-header-title has-text-light">
<i class="fa fa-camera pr-2"></i>
{{if .EditPhoto}}
Your Photo
{{else}}
Select a Photo
{{end}}
</p>
</header>
<!-- Upload field, not when editing -->
{{if not .EditPhoto}}
<div class="card-content">
<p class="block">
Browse or drag a photo onto this page:
</p>
<div class="field block">
<div class="file has-name is-fullwidth">
<label class="file-label">
<input class="file-input" type="file"
name="file"
id="file"
accept=".jpg,.jpeg,.jpe,.png{{if ne .Intent "profile_pic"}},.gif{{end}}"
required>
<span class="file-cta">
<span class="file-icon">
<i class="fas fa-upload"></i>
</span>
<span class="file-label">
Choose a file…
</span>
</span>
<span class="file-name" id="fileName">
Select a file
</span>
</label>
</div>
</div>
<div class="box" id="imagePreview" style="display: none">
<h3 class="subtitle">
{{if .NeedsCrop}}Crop image:{{else}}Selected image:{{end}}
</h3>
{{if .NeedsCrop}}
<p class="block">
Select a square crop of this image for your profile picture. The full
image will go among the rest of your photos, and the square version
will be used as your profile pic and avatar.
</p>
{{end}}
<!-- Container of img tags for the selected photo preview. -->
<div id="previewBox" class="block"></div>
{{if .NeedsCrop}}
<div class="block has-text-centered">
<button type="button" class="button block is-info" onclick="resetCrop()">Reset</button>
</div>
{{end}}
</div>
<!-- Holder of image crop coordinates in x,y,w,h format. -->
<input type="hidden" name="crop" id="cropCoords">
</div>
{{else}}<!-- when .EditPhoto -->
<div class="card-content">
<figure id="editphoto-preview" class="image block">
{{if HasSuffix .EditPhoto.Filename ".mp4"}}
<video autoplay loop controls controlsList="nodownload" playsinline>
<source src="{{PhotoURL .EditPhoto.Filename}}" type="video/mp4">
</video>
{{else}}
<img src="{{PhotoURL .EditPhoto.Filename}}" id="editphoto-img">
{{end}}
</figure>
<!-- Re-crop as profile picture: not when it's a GIF -->
{{if not (HasSuffix .EditPhoto.Filename ".mp4")}}
<div class="block has-text-centered" id="editphoto-begin-crop">
<button type="button" class="button" onclick="setProfilePhoto()">
<span class="icon"><i class="fa fa-crop-simple"></i></span>
<span>Set this as my profile photo (crop image)</span>
</button>
</div>
{{end}}
<div class="block has-text-centered" id="editphoto-cropping" style="display: none">
<button type="button" class="button block is-info" onclick="resetCrop()">Reset</button>
</div>
<!-- Holder of image crop coordinates in x,y,w,h format. -->
<input type="hidden" name="crop" id="cropCoords">
</div>
{{end}}
</div><!-- /card -->
</div><!-- /column -->
<div class="column">
<div class="card">
<header class="card-header has-background-link">
<p class="card-header-title has-text-light">
<i class="fa fa-pencil pr-2"></i>
Photo Settings
</p>
</header>
<div class="card-content">
<div class="field mb-5">
<label class="label" for="caption">Caption</label>
<input type="text" class="input"
name="caption"
id="caption"
placeholder="A short sentence about this picture"
value="{{.EditPhoto.Caption}}">
</div>
<div class="field mb-5">
<div class="columns is-mobile mb-0">
<div class="column">
<label class="label" for="alt_text">Alt Text</label>
</div>
<div class="column is-narrow">
<small id="alt_text_length">0/5000</small>
</div>
</div>
<textarea class="textarea"
name="alt_text"
id="alt_text"
cols="40"
rows="4"
placeholder="Write a description of this photo for the visually impaired"
>{{.EditPhoto.AltText}}</textarea>
<p class="help">
Write a description of this photo to help people with disabilities (such as the
vision impaired) to understand the content and meaning of this photo.
Max 5,000 characters.
<a href="/faq#alt-text" target="_blank">Learn more <i class="fa fa-external-link"></i></a>
</p>
</div>
<div class="field mb-5">
<label class="label">
<i class="fa fa-thumbtack mr-1 has-text-success"></i>
Pinned Photo
</label>
<label class="checkbox">
<input type="checkbox"
name="pinned"
value="true"
{{if .EditPhoto.Pinned}}checked{{end}}>
Keep this photo at the top of my gallery
</label>
<p class="help">
You may "pin" your favorite photos to keep them always at the front of
your photo gallery.
</p>
</div>
<div class="field mb-5">
<label class="label">Photo Visibility</label>
<div>
<label class="radio">
<input type="radio"
name="visibility"
value="public"
{{if or (not .EditPhoto) (eq .EditPhoto.Visibility "public")}}checked{{end}}>
<strong class="has-text-link ml-1">
<span>Public <small>(members only)</small></span>
<span class="icon"><i class="fa fa-eye"></i></span>
</strong>
</label>
<p class="help">
This photo will appear on your profile page and can be seen by any
logged-in user account. It may also appear on the site-wide Photo
Gallery if that option is enabled, below.
</p>
</div>
<div>
<label class="radio">
<input type="radio"
name="visibility"
value="friends"
{{if eq .EditPhoto.Visibility "friends"}}checked{{end}}>
<strong class="has-text-warning ml-1">
<span>Friends only</span>
<span class="icon"><i class="fa fa-user-group"></i></span>
</strong>
</label>
<p class="help">
Only users you have accepted as a friend can see this photo on your
profile page and on the site-wide Photo Gallery if that option is
enabled, below.
</p>
</div>
<div>
<label class="radio">
<input type="radio"
name="visibility"
value="private"
{{if eq .EditPhoto.Visibility "private"}}checked{{end}}>
<strong class="has-text-private ml-1">
<span>Private</span>
<span class="icon"><i class="fa fa-lock"></i></span>
</strong>
</label>
<p class="help">
This photo is visible only to you and to users for whom you have
granted access
(<a href="/photo/private" target="_blank" class="has-text-private">manage grants <i class="fa fa-external-link"></i></a>).
</p>
</div>
<div class="has-text-warning is-size-7 mt-4">
<i class="fa fa-info-circle mr-1"></i>
<strong class="has-text-warning">Reminder:</strong> There are risks inherent with sharing
pictures on the Internet, and {{PrettyTitle}} can't guarantee that another member of the site
won't download and possibly redistribute your photos. You may mark your picture as "Friends only"
or "Private" to limit who on the website will see it, but anybody who <em>can</em> see it could potentially
save it to their computer. <a href="/faq#downloading" target="_blank">Learn more <i class="fa fa-external-link"></i></a>
</div>
</div>
<div class="field mb-5">
<label class="label">
<span>Site Photo Gallery</span>
<span class="icon"><i class="fa fa-image"></i></span>
</label>
<label class="checkbox">
<input type="checkbox"
name="gallery"
value="true"
{{if and (or .EditPhoto.Gallery (not .EditPhoto)) (not .SiteGalleryThrottled)}}checked{{end}}
{{if .SiteGalleryThrottled}}disabled{{end}}>
{{if .SiteGalleryThrottled}}<del>{{end}}
Show this photo in the site-wide Photo Gallery
{{if .SiteGalleryThrottled}}</del>{{end}}
</label>
{{if .SiteGalleryThrottled}}
<p class="help has-text-warning">
<i class="fa fa-exclamation-triangle"></i>
You have shared too many photos with the Site Gallery recently!<br><br>
We currently limit members to featuring <strong>{{.SiteGalleryThrottleLimit}} photos</strong>
on the Site Gallery per day, so that one member doesn't dominate page after
page of the gallery by uploading <em>all</em> of their pictures at once.
<a href="/faq#site-gallery-throttle" target="_blank">Learn more <i class="fa fa-external-link"></i></a>
<br><br>
You may still upload all the photos you like to <em>your</em> gallery, but new ones can not be featured
on the Site Gallery until you have waited 24 hours. You MAY "edit" your recently posted photos
and un-check the Site Gallery box if you <em>really</em> want this one to be featured now.
</p>
{{else}}
<p class="help">
Leave this box checked and your photo can appear in the site's Photo Gallery
page. Mainly your <strong class="has-text-link">Public</strong> photos will appear
on the Gallery, and your approved friends may see your
<strong class="has-text-warning">Friends-only</strong> photos there as well.
<strong class="has-text-private">Private</strong> photos may appear in
the gallery to users whom you have granted access. If this is undesirable,
un-check the Gallery box to skip the Site Gallery.
</p>
{{end}}
</div>
<div class="field mb-5">
<label class="label has-text-danger">
<span>Explicit Content</span>
<span class="icon"><i class="fa fa-fire"></i></span>
</label>
<!-- Flagged Explicit photo: show a warning if this photo was recently flagged by the community. -->
{{$IsFlagged := and .EditPhoto .EditPhoto.Flagged .EditPhoto.Explicit}}
{{if $IsFlagged}}
<div class="notification is-danger is-light py-3 px-3 has-text-smaller content">
<p>
<strong>
<i class="fa fa-exclamation-triangle"></i>
Notice:
</strong>
This photo was classified by the community as containing 'Explicit' content.
</p>
<p>
Please review <a href="/tos#explicit-photos">what {{.Title}} considers an 'Explicit' photo</a>
and if this photo fits the description, <strong>please</strong> leave this photo with the
'Explicit' box checked, below.
</p>
<p>
If you disagree that this photo should have been marked as 'Explicit,' you <strong>MAY</strong>
uncheck the box below and remove the Explicit status. <strong>Note:</strong> the website admin will
be notified to take a look as well if you do this, to verify that your photo has the correct 'Explicit'
setting.
</p>
</div>
{{end}}
{{if eq .Intent "profile_pic"}}
<span class="has-text-danger">
Your default profile picture should
<strong class="has-text-danger">NOT</strong>
contain explicit content.
</span>
<p class="help">
Your default profile picture is about your face. You can have nudity
in it, too, but not a close-up shot of your genitals or sporting an
erection or engaging in sexual conduct. You can upload pictures like
that to your page, just not as your default profile picture!
</p>
{{else}}
<label class="checkbox"
{{if $IsFlagged}}title="You MAY remove this check if you disagree that this photo should be marked Explicit"{{end}}
>
<input type="checkbox"
name="explicit"
value="true"
{{if .EditPhoto.Explicit}}checked{{end}}
{{if .EditPhoto.HasAdminLabelForceExplicit}}disabled{{end}}>
{{if $IsFlagged}}<del class="cursor-not-allowed">{{end}}
This photo contains explicit content
{{if $IsFlagged}}</del>{{end}}
</label>
<p class="help">
Mark this box if this photo contains any explicit content, including an
erect penis, close-up of genitalia, or any depiction of sexual activity.
Use your best judgment. "Normal nudes" such as full body nudes in a
non-sexual context do not need to check this box.
</p>
{{end}}
</div>
{{if not .EditPhoto}}
<div class="field">
<label class="label">Confirm Upload</label>
<label class="checkbox">
<input type="checkbox"
name="confirm1"
value="true"
required>
I assert that this is a photo <strong>of myself</strong> and that I have
permission to upload this picture.
</label>
{{if eq .Intent "profile_pic"}}
<label class="checkbox">
<input type="checkbox"
name="confirm2"
value="true"
required>
I assert that this picture shows my face and is not explicit
</label>
{{else}}
<input type="hidden" name="confirm2" value="true">
{{end}}
</div>
{{end}}
<div class="field">
<button type="submit" class="button is-primary">
{{if .EditPhoto}}
Save Changes
{{else}}
Upload Photo
{{end}}
</button>
</div>
<!-- Admin Labels -->
{{if and .EditPhoto (.RequestUser.HasAdminScope "social.moderator.photo")}}
<hr>
<div class="field">
<label class="label has-text-danger">
<span>Admin Labels</span>
<span class="icon"><i class="fa fa-peace"></i></span>
</label>
<p class="help">
The options below can apply moderation rules to this picture, especially regarding
its 'explicit' status. For example: if a community member flagged this picture as
explicit, but it does NOT need to be marked as such, select that label below: and
the website will no longer allow this photo to be flagged as explicit again.
</p>
</div>
{{range .AvailableAdminLabels}}
<div class="field">
<label class="checkbox">
<input type="checkbox"
name="admin_label"
value="{{.Value}}"
{{if ($Root.EditPhoto.HasAdminLabel .Value)}}checked{{end}}>
{{.Label}}
</label>
<p class="help">
{{.Help}}
</p>
</div>
{{end}}
<p class="help">
<strong>Reminder:</strong> click on 'Save Changes' to apply these labels!
</p>
{{end}}
</div>
</div>
</div>
</div><!-- /columns -->
{{end}}<!-- if under quota -->
</div>
</form>
<!-- image cropper -->
<link rel="stylesheet" href="/static/js/croppr/croppr.min.css">
<script src="/static/js/croppr/croppr.js"></script>
<script type="text/javascript">
var croppr = null;
const usingCroppr = {{if .NeedsCrop}}true{{else}}false{{end}};
function resetCrop() {
if (croppr !== null) {
croppr.reset();
}
}
{{if not .EditPhoto}}
window.addEventListener("DOMContentLoaded", (event) => {
let $file = document.querySelector("#file"),
$fileName = document.querySelector("#fileName"),
$hiddenPreview = document.querySelector("#imagePreview"),
$previewBox = document.querySelector("#previewBox"),
$cropField = document.querySelector("#cropCoords"),
$dropArea = document.querySelector("#drop-modal"),
$body = document.querySelector("body");
// Common handler for file selection, either via input
// field or drag/drop onto the page.
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.");
return;
}
$fileName.innerHTML = file.name;
// Read the image to show the preview on-page.
const reader = new FileReader();
reader.addEventListener("load", () => {
const uploadedImg = reader.result;
$hiddenPreview.style.display = "block";
// Create a new <img> tag the first time.
if (croppr !== null) {
croppr.setImage(uploadedImg);
croppr.reset();
return;
}
// If not using croppr, flush the old img preview out.
if (!usingCroppr) {
$previewBox.innerHTML = "";
}
let img = document.createElement("img");
img.src = uploadedImg;
img.style.display = "block";
img.style.maxWidth = "100%";
img.style.height = "auto";
// Add it to the wrapper div.
$previewBox.appendChild(img);
if (usingCroppr) {
croppr = new Croppr(img, {
aspectRatio: 1,
minSize: [ 32, 32, 'px' ],
returnMode: 'real',
onCropStart: (data) => {
// console.log(data);
},
onCropMove: (data) => {
// console.log(data);
},
onCropEnd: (data) => {
// console.log(data);
$cropField.value = [
data.x, data.y, data.width, data.height
].join(",");
},
onInitialize: (inst) => {
// Populate the default crop value into the form field.
let data = inst.getValue();
$cropField.value = [
data.x, data.y, data.width, data.height
].join(",");
}
});
}
});
reader.readAsDataURL(file);
};
// Set up drag/drop file upload events.
$body.addEventListener("dragenter", function(e) {
e.preventDefault();
e.stopPropagation();
$dropArea.classList.add("is-active");
});
$body.addEventListener("dragover", function(e) {
e.preventDefault();
e.stopPropagation();
$dropArea.classList.add("is-active");
});
$body.addEventListener("dragleave", function(e) {
e.preventDefault();
e.stopPropagation();
$dropArea.classList.remove("is-active");
});
$body.addEventListener("drop", function(e) {
e.preventDefault();
e.stopPropagation();
$dropArea.classList.remove("is-active");
// Grab the file.
let dt = e.dataTransfer;
let file = dt.files[0];
// Set the file on the input field too.
$file.files = dt.files;
onFile(file);
});
// Clear the answer in case of page reload.
$cropField.value = "";
$file.addEventListener("change", function() {
let file = this.files[0];
onFile(file);
});
});
{{end}}
// Alt Text helper.
document.addEventListener("DOMContentLoaded", () => {
let $altText = document.querySelector("#alt_text"),
$length = document.querySelector("#alt_text_length"),
maxLength = 5000;
$altText.maxLength = maxLength;
$altText.addEventListener("keydown", (e) => {
if ($altText.value.length >= maxLength) {
return false;
}
});
let update = () => {
$length.innerHTML = `${$altText.value.length}/${maxLength}`;
};
$altText.addEventListener("keyup", update);
$altText.addEventListener("change", update);
update();
});
// EditPhoto only: a button to crop their photo to set as a profile pic.
function setProfilePhoto() {
let $begin = document.querySelector("#editphoto-begin-crop"),
$cropRow = document.querySelector("#editphoto-cropping"),
$preview = document.querySelector("#editphoto-preview")
$cropField = document.querySelector("#cropCoords"),
$intent = document.querySelector("#intent");
img = document.querySelector("#editphoto-img");
// Toggle the button display, from begin crop to the crop reset button.
$begin.style.display = 'none';
$cropRow.style.display = 'block';
// Set intent to profile-pic so when the form posts the crop coords will
// create a new profile pic for this user.
$intent.value = "profile-pic";
croppr = new Croppr(img, {
aspectRatio: 1,
minSize: [ 32, 32, 'px' ],
returnMode: 'real',
onCropStart: (data) => {
// console.log(data);
},
onCropMove: (data) => {
// console.log(data);
},
onCropEnd: (data) => {
// console.log(data);
$cropField.value = [
data.x, data.y, data.width, data.height
].join(",");
},
onInitialize: (inst) => {
// Populate the default crop value into the form field.
let data = inst.getValue();
$cropField.value = [
data.x, data.y, data.width, data.height
].join(",");
}
});
}
</script>
</div>
{{end}}