mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-01 05:50:35 +03:00
Made social/meta text and images in preview modal editable (#1850)
refs https://github.com/TryGhost/Team/issues/482 - replace titles and descriptions with text fields when clicked - save on blur - blur+save on <kbd>Enter</kbd> - blur+restore on <kbd>Escape</kbd> - create newline with <kbd>Shift+Enter</kbd> in description fields - if there's no available image or fallback show a "+ Add image" header in the previews when hovering - if there is an image show an upload/change image button when hovering - show a delete image button when hovering once a custom image has been uploaded Co-authored-by: Sanne de Vries <sannedv@protonmail.com>
This commit is contained in:
parent
a16519baa9
commit
ff09a20ac8
@ -42,8 +42,10 @@ export default Component.extend({
|
||||
this.send('confirm');
|
||||
});
|
||||
|
||||
key('escape', 'modal', () => {
|
||||
key('escape', 'modal', (event) => {
|
||||
if (!event.target.dataset.preventEscapeCloseModal) {
|
||||
this.send('closeModal');
|
||||
}
|
||||
});
|
||||
|
||||
key.setScope('modal');
|
||||
|
@ -15,23 +15,95 @@
|
||||
<span class="gh-social-og-desc w-100 mb2" />
|
||||
<span class="gh-social-og-desc w-60" />
|
||||
</div>
|
||||
<div class="gh-social-og-preview">
|
||||
{{#if this.facebookImage}}
|
||||
<div class="gh-social-og-preview-image" style={{background-image-style this.facebookImage}}></div>
|
||||
<div
|
||||
class="gh-social-og-preview"
|
||||
{{on "mouseenter" (action (mut this.facebookHovered) true)}}
|
||||
{{on "mouseleave" (action (mut this.facebookHovered) false)}}
|
||||
>
|
||||
{{#if (and this.facebookHovered (not this.facebookImage))}}
|
||||
{{!-- only shown on hover when there's no image or fallback --}}
|
||||
<button class="w-100 ba bw2 bb-0 br4 br--top" {{on "click" (fn this.triggerFileDialog "facebook")}}>+ Add Image</button>
|
||||
{{/if}}
|
||||
|
||||
<GhUploader
|
||||
@extensions={{this.imageExtensions}}
|
||||
@onComplete={{this.setFacebookImage}}
|
||||
as |uploader|
|
||||
>
|
||||
{{#each uploader.errors as |error|}}
|
||||
<div class="error pa2"><span class="response">{{or error.context error.message}}</span></div>
|
||||
{{/each}}
|
||||
|
||||
{{#if (or this.facebookImage uploader.isUploading)}}
|
||||
<div class="gh-social-og-preview-image relative" style={{background-image-style this.facebookImage}}>
|
||||
<div class="flex h-100 items-center justify-center">
|
||||
{{#if (or this.facebookHovered uploader.isUploading)}}
|
||||
{{#if uploader.isUploading}}
|
||||
{{uploader.progressBar}}
|
||||
{{else}}
|
||||
<button type="button" class="gh-btn" {{on "click" (fn this.triggerFileDialog "facebook")}}><span>{{if @post.ogImage "Change" "Upload"}} image</span></button>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if (and this.facebookHovered @post.ogImage)}}
|
||||
<button type="button" class="gh-btn gh-btn-black gh-btn-icon gh-social-preview-img-delete" title="Remove custom Facebook image" {{on "click" this.clearFacebookImage}}>
|
||||
<span>{{svg-jar "trash"}}</span>
|
||||
<span class="hidden">Remove custom Facebook image</span>
|
||||
</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div style="display:none">
|
||||
<GhFileInput id="facebookFileInput" @multiple={{false}} @action={{uploader.setFiles}} @accept={{this.imageMimeTypes}} />
|
||||
</div>
|
||||
</GhUploader>
|
||||
|
||||
<div class="gh-social-og-preview-content">
|
||||
<div class="gh-social-og-preview-meta">
|
||||
{{this.config.blogDomain}}
|
||||
</div>
|
||||
<div class="gh-social-og-preview-title">{{truncate this.facebookTitle 88}}</div>
|
||||
<div class="gh-social-og-preview-desc">{{truncate this.facebookDescription}}</div>
|
||||
<div class="gh-social-og-preview-title editable">
|
||||
{{#if this.editingFacebookTitle}}
|
||||
<input
|
||||
type="text"
|
||||
class="gh-input"
|
||||
value={{@post.ogTitle}}
|
||||
maxlength="300"
|
||||
title="Facebook title"
|
||||
{{on "blur" this.setFacebookTitle}}
|
||||
{{on-key "Enter" this.blurElement}}
|
||||
{{on-key "Escape" (fn this.cancelEdit "ogTitle")}}
|
||||
{{autofocus}}
|
||||
data-prevent-escape-close-modal="true"
|
||||
/>
|
||||
{{else}}
|
||||
<span class="pointer" {{on "click" this.editFacebookTitle}}>{{truncate this.facebookTitle 88}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{#if this.editingFacebookDescription}}
|
||||
<textarea
|
||||
class="gh-input"
|
||||
maxlength="500"
|
||||
title="Facebook description"
|
||||
{{on "blur" this.setFacebookDescription}}
|
||||
{{on-key "Enter" this.blurElement}}
|
||||
{{on-key "Escape" (fn this.cancelEdit "ogDescription")}}
|
||||
{{autofocus}}
|
||||
data-prevent-escape-close-modal="true"
|
||||
>{{@post.ogDescription}}</textarea>
|
||||
{{else}}
|
||||
<div class="gh-social-og-preview-desc">
|
||||
<span class="pointer" {{on "click" this.editFacebookDescription}}>{{truncate this.facebookDescription}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-social-og-reactions">
|
||||
<span class="gh-social-og-likes">{{svg-jar "facebook-like" class="z-999"}}{{svg-jar "facebook-heart" class="nl1"}}182</span>
|
||||
<span class="gh-social-og-comments">7 comments</span>
|
||||
<span class="gh-social-og-comments ml2">2 shares</span>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -45,13 +117,81 @@
|
||||
<span class="gh-social-og-desc w-100 mb2" />
|
||||
<span class="gh-social-og-desc w-60" />
|
||||
</div>
|
||||
<div class="gh-social-twitter-post-preview">
|
||||
{{#if this.twitterImage}}
|
||||
<div class="gh-social-twitter-preview-image" style={{background-image-style this.twitterImage}}></div>
|
||||
<div class="gh-social-twitter-post-preview"
|
||||
{{on "mouseenter" (action (mut this.twitterHovered) true)}}
|
||||
{{on "mouseleave" (action (mut this.twitterHovered) false)}}
|
||||
>
|
||||
{{#if (and this.twitterHovered (not this.twitterImage))}}
|
||||
{{!-- only shown on hover when there's no image or fallback --}}
|
||||
<button class="w-100 ba bw2 bb-0 br4 br--top" {{on "click" (fn this.triggerFileDialog "twitter")}}>+ Add Image</button>
|
||||
{{/if}}
|
||||
|
||||
<GhUploader
|
||||
@extensions={{this.imageExtensions}}
|
||||
@onComplete={{this.setTwitterImage}}
|
||||
as |uploader|
|
||||
>
|
||||
{{#each uploader.errors as |error|}}
|
||||
<div class="error pa2"><span class="response">{{or error.context error.message}}</span></div>
|
||||
{{/each}}
|
||||
|
||||
{{#if (or this.twitterImage uploader.isUploading)}}
|
||||
<div class="gh-social-twitter-preview-image relative" style={{background-image-style this.twitterImage}}>
|
||||
<div class="flex h-100 items-center justify-center">
|
||||
{{#if (or this.twitterHovered uploader.isUploading)}}
|
||||
{{#if uploader.isUploading}}
|
||||
{{uploader.progressBar}}
|
||||
{{else}}
|
||||
<button type="button" class="gh-btn" {{on "click" (fn this.triggerFileDialog "twitter")}}><span>{{if @post.twitterImage "Change" "Upload"}} image</span></button>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if (and this.twitterHovered @post.twitterImage)}}
|
||||
<button type="button" class="gh-btn gh-btn-black gh-btn-icon gh-social-preview-img-delete" title="Remove custom Twitter image" {{on "click" this.clearTwitterImage}}>
|
||||
<span>{{svg-jar "trash"}}</span>
|
||||
<span class="hidden">Remove custom Twitter image</span>
|
||||
</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div style="display:none">
|
||||
<GhFileInput id="twitterFileInput" @multiple={{false}} @action={{uploader.setFiles}} @accept={{this.imageMimeTypes}} />
|
||||
</div>
|
||||
</GhUploader>
|
||||
|
||||
<div class="gh-social-twitter-preview-content">
|
||||
<div class="gh-social-twitter-preview-title">{{this.twitterTitle}}</div>
|
||||
<div class="gh-social-twitter-preview-desc">{{truncate this.twitterDescription}}</div>
|
||||
{{#if this.editingTwitterTitle}}
|
||||
<input
|
||||
type="text"
|
||||
class="gh-input"
|
||||
value={{@post.twitterTitle}}
|
||||
maxlength="300"
|
||||
title="Twitter title"
|
||||
{{on "blur" this.setTwitterTitle}}
|
||||
{{on-key "Enter" this.blurElement}}
|
||||
{{on-key "Escape" (fn this.cancelEdit "twitterTitle")}}
|
||||
{{autofocus}}
|
||||
data-prevent-escape-close-modal="true"
|
||||
/>
|
||||
{{else}}
|
||||
<div class="gh-social-twitter-preview-title editable pointer" {{on "click" this.editTwitterTitle}}>{{this.twitterTitle}}</div>
|
||||
{{/if}}
|
||||
{{#if this.editingTwitterDescription}}
|
||||
<textarea
|
||||
class="gh-input"
|
||||
maxlength="500"
|
||||
title="Twitter description"
|
||||
{{on "blur" this.setTwitterDescription}}
|
||||
{{on-key "Enter" this.blurElement}}
|
||||
{{on-key "Escape" (fn this.cancelEdit "twitterDescription")}}
|
||||
{{autofocus}}
|
||||
data-prevent-escape-close-modal="true"
|
||||
>{{@post.twitterDescription}}</textarea>
|
||||
{{else}}
|
||||
<div class="gh-social-twitter-preview-desc editable pointer" {{on "click" this.editTwitterDescription}}>{{truncate this.twitterDescription}}</div>
|
||||
{{/if}}
|
||||
<div class="gh-social-twitter-preview-meta">
|
||||
{{svg-jar "twitter-link"}}
|
||||
{{this.config.blogDomain}}
|
||||
@ -74,9 +214,39 @@
|
||||
<div class="gh-seo-preview">
|
||||
<div class="gh-seo-search-bar mb12">{{svg-jar "google-search"}}</div>
|
||||
<div class="gh-seo-preview-link">{{this.config.blogDomain}} > {{#if this.ghostPaths.subdir}}{{this.ghostPaths.subdir}} > {{/if}}{{@post.slug}}</div>
|
||||
<div class="gh-seo-preview-title">{{this.serpTitle}}</div>
|
||||
<div class="gh-seo-preview-desc">
|
||||
{{moment-format (now) "DD MMM YYYY"}} — {{truncate this.serpDescription 170}}
|
||||
<div class="gh-seo-preview-title editable">
|
||||
{{#if this.editingMetaTitle}}
|
||||
<input
|
||||
type="text"
|
||||
class="gh-input"
|
||||
value={{@post.metaTitle}}
|
||||
maxlength="300"
|
||||
title="Meta title"
|
||||
{{on "blur" this.setMetaTitle}}
|
||||
{{on-key "Enter" this.blurElement}}
|
||||
{{on-key "Escape" (fn this.cancelEdit "metaTitle")}}
|
||||
{{autofocus}}
|
||||
data-prevent-escape-close-modal="true"
|
||||
>
|
||||
{{else}}
|
||||
<span class="pointer" {{on "click" this.editMetaTitle}}>{{this.serpTitle}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="gh-seo-preview-desc editable">
|
||||
{{#if this.editingMetaDescription}}
|
||||
<textarea
|
||||
class="gh-input"
|
||||
maxlength="500"
|
||||
title="Meta description"
|
||||
{{on "blur" this.setMetaDescription}}
|
||||
{{on-key "Enter" this.blurElement}}
|
||||
{{on-key "Escape" (fn this.cancelEdit "metaDescription")}}
|
||||
{{autofocus}}
|
||||
data-prevent-escape-close-modal="true"
|
||||
>{{@post.metaDescription}}</textarea>
|
||||
{{else}}
|
||||
<span class="pointer" {{on "click" this.editMetaDescription}}>{{moment-format (now) "DD MMM YYYY"}} — {{truncate this.serpDescription 170}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,17 +1,48 @@
|
||||
import Component from '@glimmer/component';
|
||||
import {
|
||||
IMAGE_EXTENSIONS,
|
||||
IMAGE_MIME_TYPES
|
||||
} from 'ghost-admin/components/gh-image-uploader';
|
||||
import {action} from '@ember/object';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
import {tracked} from '@glimmer/tracking';
|
||||
export default class ModalPostPreviewSocialComponent extends Component {
|
||||
@service config;
|
||||
@service settings;
|
||||
@service ghostPaths;
|
||||
|
||||
@tracked editingFacebookTitle = false;
|
||||
@tracked editingFacebookDescription = false;
|
||||
@tracked editingTwitterTitle = false;
|
||||
@tracked editingTwitterDescription = false;
|
||||
@tracked editingMetaTitle = false;
|
||||
@tracked editingMetaDescription = false;
|
||||
|
||||
imageExtensions = IMAGE_EXTENSIONS;
|
||||
imageMimeTypes = IMAGE_MIME_TYPES;
|
||||
|
||||
get _fallbackDescription() {
|
||||
return this.args.post.customExcerpt ||
|
||||
this.serpDescription ||
|
||||
this.settings.get('description');
|
||||
}
|
||||
|
||||
@action
|
||||
blurElement(event) {
|
||||
if (!event.shiftKey) {
|
||||
event.preventDefault();
|
||||
event.target.blur();
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
triggerFileDialog(name) {
|
||||
const input = document.querySelector(`#${name}FileInput input`);
|
||||
if (input) {
|
||||
input.click();
|
||||
}
|
||||
}
|
||||
|
||||
// SERP
|
||||
|
||||
get serpTitle() {
|
||||
@ -45,6 +76,32 @@ export default class ModalPostPreviewSocialComponent extends Component {
|
||||
return this.args.post.metaDescription || this.args.post.excerpt;
|
||||
}
|
||||
|
||||
@action
|
||||
editMetaTitle() {
|
||||
this.editingMetaTitle = true;
|
||||
}
|
||||
|
||||
@action
|
||||
setMetaTitle(event) {
|
||||
const title = event.target.value;
|
||||
this.args.post.metaTitle = title.trim();
|
||||
this.args.post.save();
|
||||
this.editingMetaTitle = false;
|
||||
}
|
||||
|
||||
@action
|
||||
editMetaDescription() {
|
||||
this.editingMetaDescription = true;
|
||||
}
|
||||
|
||||
@action
|
||||
setMetaDescription(event) {
|
||||
const description = event.target.value;
|
||||
this.args.post.metaDescription = description.trim();
|
||||
this.args.post.save();
|
||||
this.editingMetaDescription = false;
|
||||
}
|
||||
|
||||
// Facebook
|
||||
|
||||
get facebookTitle() {
|
||||
@ -59,6 +116,51 @@ export default class ModalPostPreviewSocialComponent extends Component {
|
||||
return this.args.post.ogImage || this.args.post.featureImage || this.settings.get('ogImage') || this.settings.get('coverImage');
|
||||
}
|
||||
|
||||
@action
|
||||
editFacebookTitle() {
|
||||
this.editingFacebookTitle = true;
|
||||
}
|
||||
|
||||
@action
|
||||
cancelEdit(property, event) {
|
||||
event.preventDefault();
|
||||
event.target.value = this.args.post[property];
|
||||
event.target.blur();
|
||||
}
|
||||
|
||||
@action
|
||||
setFacebookTitle(event) {
|
||||
const title = event.target.value;
|
||||
this.args.post.ogTitle = title.trim();
|
||||
this.args.post.save();
|
||||
this.editingFacebookTitle = false;
|
||||
}
|
||||
|
||||
@action
|
||||
editFacebookDescription() {
|
||||
this.editingFacebookDescription = true;
|
||||
}
|
||||
|
||||
@action
|
||||
setFacebookDescription() {
|
||||
const description = event.target.value;
|
||||
this.args.post.ogDescription = description.trim();
|
||||
this.args.post.save();
|
||||
this.editingFacebookDescription = false;
|
||||
}
|
||||
|
||||
@action
|
||||
setFacebookImage([image]) {
|
||||
this.args.post.ogImage = image.url;
|
||||
this.args.post.save();
|
||||
}
|
||||
|
||||
@action
|
||||
clearFacebookImage() {
|
||||
this.args.post.ogImage = null;
|
||||
this.args.post.save();
|
||||
}
|
||||
|
||||
// Twitter
|
||||
|
||||
get twitterTitle() {
|
||||
@ -72,4 +174,42 @@ export default class ModalPostPreviewSocialComponent extends Component {
|
||||
get twitterImage() {
|
||||
return this.args.post.twitterImage || this.args.post.featureImage || this.settings.get('twitterImage') || this.settings.get('coverImage');
|
||||
}
|
||||
|
||||
@action
|
||||
editTwitterTitle() {
|
||||
this.editingTwitterTitle = true;
|
||||
}
|
||||
|
||||
@action
|
||||
setTwitterTitle(event) {
|
||||
const title = event.target.value;
|
||||
this.args.post.twitterTitle = title.trim();
|
||||
this.args.post.save();
|
||||
this.editingTwitterTitle = false;
|
||||
}
|
||||
|
||||
@action
|
||||
editTwitterDescription() {
|
||||
this.editingTwitterDescription = true;
|
||||
}
|
||||
|
||||
@action
|
||||
setTwitterDescription() {
|
||||
const description = event.target.value;
|
||||
this.args.post.twitterDescription = description.trim();
|
||||
this.args.post.save();
|
||||
this.editingTwitterDescription = false;
|
||||
}
|
||||
|
||||
@action
|
||||
setTwitterImage([image]) {
|
||||
this.args.post.twitterImage = image.url;
|
||||
this.args.post.save();
|
||||
}
|
||||
|
||||
@action
|
||||
clearTwitterImage() {
|
||||
this.args.post.twitterImage = null;
|
||||
this.args.post.save();
|
||||
}
|
||||
}
|
||||
|
@ -304,6 +304,17 @@
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.gh-social-og-preview-title.editable:hover {
|
||||
margin: 2px -1px -1px -1px;
|
||||
border: 1px solid var(--lightgrey-l1);
|
||||
background: var(--white);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.gh-social-og-preview-title .gh-input {
|
||||
margin-top: -2px !important;
|
||||
}
|
||||
|
||||
.gh-social-og-preview-desc {
|
||||
max-height: 20px;
|
||||
overflow: hidden;
|
||||
@ -317,6 +328,15 @@
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.gh-social-og-preview-desc.editable:hover {
|
||||
max-height: 24px;
|
||||
margin: 2px 0 -3px -1px;
|
||||
padding-bottom: 2px;
|
||||
border: 1px solid var(--lightgrey-l1);
|
||||
background: var(--white);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.gh-social-og-reactions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -384,32 +404,50 @@
|
||||
|
||||
.gh-social-twitter-preview-title {
|
||||
width: 487px;
|
||||
max-height: 1.3em;
|
||||
max-height: 20px;
|
||||
overflow: hidden;
|
||||
overflow-wrap: break-word;
|
||||
margin: 0 0 0.15em;
|
||||
margin: 0 0 2px;
|
||||
color: #0f1419;
|
||||
font-size: 15px;
|
||||
line-height: 1.3125;
|
||||
line-height: 20px;
|
||||
font-weight: 400;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.gh-social-twitter-preview-title.editable:hover {
|
||||
max-height: 25px;
|
||||
margin: -2px 0 -3px -2px;
|
||||
padding: 1px 0 4px 1px;
|
||||
border: 1px solid var(--lightgrey);
|
||||
background: var(--white);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.gh-social-twitter-preview-desc {
|
||||
width: 487px;
|
||||
max-height: 40px;
|
||||
overflow: hidden;
|
||||
overflow-wrap: break-word;
|
||||
margin-top: 0.32333em;
|
||||
margin-top: 5px;
|
||||
color: #5b7083;
|
||||
font-size: 15px;
|
||||
line-height: 1.3125;
|
||||
line-height: 20px;
|
||||
font-weight: 400;
|
||||
text-overflow: ellipsis;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.gh-social-twitter-preview-desc.editable:hover {
|
||||
max-height: 42px;
|
||||
margin: 4px 0 -1px -2px;
|
||||
padding: 0 0 1px 1px;
|
||||
border: 1px solid var(--lightgrey);
|
||||
background: var(--white);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.gh-social-twitter-preview-meta {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
@ -458,6 +496,18 @@
|
||||
fill: #5b7083;
|
||||
}
|
||||
|
||||
.gh-social-preview-img-delete {
|
||||
margin-left: 1.2rem;
|
||||
}
|
||||
|
||||
.gh-social-preview-img-delete:hover {
|
||||
background: var(--red) !important;
|
||||
}
|
||||
|
||||
.gh-social-preview-img-delete svg {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.gh-seo-preview-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
@ -534,13 +584,38 @@
|
||||
-webkit-text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.gh-seo-preview-title.editable:hover {
|
||||
margin: 0 0 2px -1px;
|
||||
padding: 3px 0 0;
|
||||
background: var(--white);
|
||||
border: 1px solid var(--lightgrey);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.gh-seo-preview-title .gh-input {
|
||||
margin-top: -3px;
|
||||
}
|
||||
|
||||
.gh-seo-preview-desc {
|
||||
max-height: 45px;
|
||||
overflow: hidden;
|
||||
color: #4d5156;
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.58;
|
||||
line-height: 22px;
|
||||
font-weight: 400;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.gh-seo-preview-desc.editable:hover {
|
||||
max-height: 47px;
|
||||
margin: -1px 0 -2px -1px;
|
||||
padding-bottom: 2px;
|
||||
background: var(--white);
|
||||
border: 1px solid var(--lightgrey);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.gh-seo-preview-desc .gh-input {
|
||||
max-width: 100%;
|
||||
}
|
Loading…
Reference in New Issue
Block a user