Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e3375fb84 | ||
|
|
58ce9bfc19 | ||
|
|
63a071f0d3 | ||
|
|
f0d3ae09d9 |
@@ -2,10 +2,6 @@
|
||||
transition: box-shadow 0.35s $swift-ease-out-function,
|
||||
height 2s $swift-ease-out-function;
|
||||
|
||||
&.is-focused {
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.reply-box__top {
|
||||
.icon {
|
||||
color: $medium-gray;
|
||||
@@ -47,8 +43,6 @@
|
||||
}
|
||||
|
||||
&.is-private {
|
||||
@apply bg-yellow-100 dark:bg-yellow-800;
|
||||
|
||||
.reply-box__top {
|
||||
@apply bg-yellow-100 dark:bg-yellow-800;
|
||||
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
<template>
|
||||
<div class="reply-box" :class="replyBoxClass">
|
||||
<div
|
||||
class="border-t border-slate-50 dark:border-slate-700 bg-white dark:bg-slate-900 reply-box"
|
||||
:class="{
|
||||
'bg-yellow-100 dark:bg-yellow-800 is-private': isPrivate,
|
||||
'shadow-sm': isFocused || hasAttachments,
|
||||
}"
|
||||
>
|
||||
<banner
|
||||
v-if="showSelfAssignBanner"
|
||||
action-button-variant="clear"
|
||||
@@ -136,48 +142,58 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// Vuex
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
// Mixins
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
||||
import CannedResponse from './CannedResponse.vue';
|
||||
import ResizableTextArea from 'shared/components/ResizableTextArea.vue';
|
||||
import AttachmentPreview from 'dashboard/components/widgets/AttachmentsPreview.vue';
|
||||
import ReplyTopPanel from 'dashboard/components/widgets/WootWriter/ReplyTopPanel.vue';
|
||||
import ReplyEmailHead from './ReplyEmailHead.vue';
|
||||
import ReplyBottomPanel from 'dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue';
|
||||
import MessageSignatureMissingAlert from './MessageSignatureMissingAlert';
|
||||
import Banner from 'dashboard/components/ui/Banner.vue';
|
||||
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
|
||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
||||
import WootAudioRecorder from 'dashboard/components/widgets/WootWriter/AudioRecorder.vue';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { AUDIO_FORMATS } from 'shared/constants/messages';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import {
|
||||
getMessageVariables,
|
||||
getUndefinedVariablesInMessage,
|
||||
replaceVariablesInMessage,
|
||||
} from '@chatwoot/utils';
|
||||
import WhatsappTemplates from './WhatsappTemplates/Modal.vue';
|
||||
import { buildHotKeys } from 'shared/helpers/KeyboardHelpers';
|
||||
import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper';
|
||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
import { trimContent, debounce } from '@chatwoot/utils';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
|
||||
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import rtlMixin from 'shared/mixins/rtlMixin';
|
||||
import fileUploadMixin from 'dashboard/mixins/fileUploadMixin';
|
||||
import replyDraftMixin from 'dashboard/mixins/replyDraftMixin';
|
||||
import audioRecordingMixin from 'dashboard/mixins/audioRecordingMixin';
|
||||
import emailEditorMixin from 'dashboard/mixins/emailEditorMixin';
|
||||
import attachmentHandlerMixin from 'dashboard/mixins/attachmentHandlerMixin';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
|
||||
// constants
|
||||
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper';
|
||||
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
|
||||
// components
|
||||
import AttachmentPreview from 'dashboard/components/widgets/AttachmentsPreview.vue';
|
||||
import Banner from 'dashboard/components/ui/Banner.vue';
|
||||
import CannedResponse from './CannedResponse.vue';
|
||||
import EmojiInput from 'shared/components/emoji/EmojiInput.vue';
|
||||
import MessageSignatureMissingAlert from './MessageSignatureMissingAlert.vue';
|
||||
import ReplyBottomPanel from 'dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue';
|
||||
import ReplyEmailHead from './ReplyEmailHead.vue';
|
||||
import ReplyTopPanel from 'dashboard/components/widgets/WootWriter/ReplyTopPanel.vue';
|
||||
import ResizableTextArea from 'shared/components/ResizableTextArea.vue';
|
||||
import WhatsappTemplates from './WhatsappTemplates/Modal.vue';
|
||||
import WootAudioRecorder from 'dashboard/components/widgets/WootWriter/AudioRecorder.vue';
|
||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
||||
|
||||
// Helpers
|
||||
import { buildHotKeys } from 'shared/helpers/KeyboardHelpers';
|
||||
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
|
||||
import {
|
||||
appendSignature,
|
||||
removeSignature,
|
||||
replaceSignature,
|
||||
extractTextFromMarkdown,
|
||||
} from 'dashboard/helper/editorHelper';
|
||||
|
||||
const EmojiInput = () => import('shared/components/emoji/EmojiInput');
|
||||
import {
|
||||
getMessageVariables,
|
||||
getUndefinedVariablesInMessage,
|
||||
replaceVariablesInMessage,
|
||||
debounce,
|
||||
} from '@chatwoot/utils';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -202,6 +218,10 @@ export default {
|
||||
messageFormatterMixin,
|
||||
rtlMixin,
|
||||
fileUploadMixin,
|
||||
replyDraftMixin,
|
||||
audioRecordingMixin,
|
||||
emailEditorMixin,
|
||||
attachmentHandlerMixin,
|
||||
],
|
||||
props: {
|
||||
selectedTweet: {
|
||||
@@ -222,17 +242,10 @@ export default {
|
||||
message: '',
|
||||
isFocused: false,
|
||||
showEmojiPicker: false,
|
||||
attachedFiles: [],
|
||||
isRecordingAudio: false,
|
||||
recordingAudioState: '',
|
||||
recordingAudioDurationText: '',
|
||||
isUploading: false,
|
||||
replyType: REPLY_EDITOR_MODES.REPLY,
|
||||
mentionSearchKey: '',
|
||||
hasSlashCommand: false,
|
||||
bccEmails: '',
|
||||
ccEmails: '',
|
||||
toEmails: '',
|
||||
doAutoSaveDraft: () => {},
|
||||
showWhatsAppTemplatesModal: false,
|
||||
updateEditorSelectionWith: '',
|
||||
@@ -389,15 +402,6 @@ export default {
|
||||
: '(↵)';
|
||||
return `${sendMessageText} ${keyLabel}`;
|
||||
},
|
||||
replyBoxClass() {
|
||||
return {
|
||||
'is-private': this.isPrivate,
|
||||
'is-focused': this.isFocused || this.hasAttachments,
|
||||
};
|
||||
},
|
||||
hasAttachments() {
|
||||
return this.attachedFiles.length;
|
||||
},
|
||||
hasRecordedAudio() {
|
||||
return (
|
||||
this.$refs.audioRecorderInput &&
|
||||
@@ -407,12 +411,6 @@ export default {
|
||||
isRichEditorEnabled() {
|
||||
return this.isAWebWidgetInbox || this.isAnEmailChannel;
|
||||
},
|
||||
showAudioRecorder() {
|
||||
return !this.isOnPrivateNote && this.showFileUpload;
|
||||
},
|
||||
showAudioRecorderEditor() {
|
||||
return this.showAudioRecorder && this.isRecordingAudio;
|
||||
},
|
||||
isOnPrivateNote() {
|
||||
return this.replyType === REPLY_EDITOR_MODES.NOTE;
|
||||
},
|
||||
@@ -492,12 +490,6 @@ export default {
|
||||
editorStateId() {
|
||||
return `draft-${this.conversationIdByRoute}-${this.replyType}`;
|
||||
},
|
||||
audioRecordFormat() {
|
||||
if (this.isAWhatsAppChannel || this.isAPIInbox) {
|
||||
return AUDIO_FORMATS.OGG;
|
||||
}
|
||||
return AUDIO_FORMATS.WAV;
|
||||
},
|
||||
messageVariables() {
|
||||
const variables = getMessageVariables({
|
||||
conversation: this.currentChat,
|
||||
@@ -595,31 +587,6 @@ export default {
|
||||
);
|
||||
}
|
||||
},
|
||||
saveDraft(conversationId, replyType) {
|
||||
if (this.message || this.message === '') {
|
||||
const key = `draft-${conversationId}-${replyType}`;
|
||||
const draftToSave = trimContent(this.message || '');
|
||||
|
||||
this.$store.dispatch('draftMessages/set', {
|
||||
key,
|
||||
message: draftToSave,
|
||||
});
|
||||
}
|
||||
},
|
||||
setToDraft(conversationId, replyType) {
|
||||
this.saveDraft(conversationId, replyType);
|
||||
this.message = '';
|
||||
},
|
||||
getFromDraft() {
|
||||
if (this.conversationIdByRoute) {
|
||||
const key = `draft-${this.conversationIdByRoute}-${this.replyType}`;
|
||||
const messageFromStore =
|
||||
this.$store.getters['draftMessages/get'](key) || '';
|
||||
|
||||
// ensure that the message has signature set based on the ui setting
|
||||
this.message = this.toggleSignatureForDraft(messageFromStore);
|
||||
}
|
||||
},
|
||||
toggleSignatureForDraft(message) {
|
||||
if (this.isPrivate) {
|
||||
return message;
|
||||
@@ -629,12 +596,6 @@ export default {
|
||||
? appendSignature(message, this.signatureToApply)
|
||||
: removeSignature(message, this.signatureToApply);
|
||||
},
|
||||
removeFromDraft() {
|
||||
if (this.conversationIdByRoute) {
|
||||
const key = `draft-${this.conversationIdByRoute}-${this.replyType}`;
|
||||
this.$store.dispatch('draftMessages/delete', { key });
|
||||
}
|
||||
},
|
||||
handleKeyEvents(e) {
|
||||
const keyCode = buildHotKeys(e);
|
||||
if (keyCode === 'escape') {
|
||||
@@ -856,32 +817,9 @@ export default {
|
||||
this.attachedFiles = [];
|
||||
this.isRecordingAudio = false;
|
||||
},
|
||||
clearEmailField() {
|
||||
this.ccEmails = '';
|
||||
this.bccEmails = '';
|
||||
this.toEmails = '';
|
||||
},
|
||||
toggleEmojiPicker() {
|
||||
this.showEmojiPicker = !this.showEmojiPicker;
|
||||
},
|
||||
toggleAudioRecorder() {
|
||||
this.isRecordingAudio = !this.isRecordingAudio;
|
||||
this.isRecorderAudioStopped = !this.isRecordingAudio;
|
||||
if (!this.isRecordingAudio) {
|
||||
this.clearMessage();
|
||||
this.clearEmailField();
|
||||
}
|
||||
},
|
||||
toggleAudioRecorderPlayPause() {
|
||||
if (this.isRecordingAudio) {
|
||||
if (!this.isRecorderAudioStopped) {
|
||||
this.isRecorderAudioStopped = true;
|
||||
this.$refs.audioRecorderInput.stopAudioRecording();
|
||||
} else if (this.isRecorderAudioStopped) {
|
||||
this.$refs.audioRecorderInput.playPause();
|
||||
}
|
||||
}
|
||||
},
|
||||
hideEmojiPicker() {
|
||||
if (this.showEmojiPicker) {
|
||||
this.toggleEmojiPicker();
|
||||
@@ -903,18 +841,6 @@ export default {
|
||||
onFocus() {
|
||||
this.isFocused = true;
|
||||
},
|
||||
onStateProgressRecorderChanged(duration) {
|
||||
this.recordingAudioDurationText = duration;
|
||||
},
|
||||
onStateRecorderChanged(state) {
|
||||
this.recordingAudioState = state;
|
||||
if (state && 'notallowederror'.includes(state)) {
|
||||
this.toggleAudioRecorder();
|
||||
}
|
||||
},
|
||||
onFinishRecorder(file) {
|
||||
return file && this.onFileUpload(file);
|
||||
},
|
||||
toggleTyping(status) {
|
||||
const conversationId = this.currentChat.id;
|
||||
const isPrivate = this.isPrivate;
|
||||
@@ -929,24 +855,6 @@ export default {
|
||||
isPrivate,
|
||||
});
|
||||
},
|
||||
attachFile({ blob, file }) {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file.file);
|
||||
reader.onloadend = () => {
|
||||
this.attachedFiles.push({
|
||||
currentChatId: this.currentChat.id,
|
||||
resource: blob || file,
|
||||
isPrivate: this.isPrivate,
|
||||
thumb: reader.result,
|
||||
blobSignedId: blob ? blob.signed_id : undefined,
|
||||
});
|
||||
};
|
||||
},
|
||||
removeAttachment(itemIndex) {
|
||||
this.attachedFiles = this.attachedFiles.filter(
|
||||
(item, index) => itemIndex !== index
|
||||
);
|
||||
},
|
||||
getMessagePayloadForWhatsapp(message) {
|
||||
const multipleMessagePayload = [];
|
||||
|
||||
@@ -1015,51 +923,6 @@ export default {
|
||||
|
||||
return messagePayload;
|
||||
},
|
||||
setCcEmails(value) {
|
||||
this.bccEmails = value.bccEmails;
|
||||
this.ccEmails = value.ccEmails;
|
||||
},
|
||||
setCCAndToEmailsFromLastChat() {
|
||||
if (!this.lastEmail) return;
|
||||
|
||||
const {
|
||||
content_attributes: { email: emailAttributes = {} },
|
||||
} = this.lastEmail;
|
||||
|
||||
// Retrieve the email of the current conversation's sender
|
||||
const conversationContact = this.currentChat?.meta?.sender?.email || '';
|
||||
let cc = emailAttributes.cc ? [...emailAttributes.cc] : [];
|
||||
let to = [];
|
||||
|
||||
// there might be a situation where the current conversation will include a message from a third person,
|
||||
// and the current conversation contact is in CC.
|
||||
// This is an edge-case, reported here: CW-1511 [ONLY FOR INTERNAL REFERENCE]
|
||||
// So we remove the current conversation contact's email from the CC list if present
|
||||
if (cc.includes(conversationContact)) {
|
||||
cc = cc.filter(email => email !== conversationContact);
|
||||
}
|
||||
|
||||
// If the last incoming message sender is different from the conversation contact, add them to the "to"
|
||||
// and add the conversation contact to the CC
|
||||
if (!emailAttributes.from.includes(conversationContact)) {
|
||||
to.push(...emailAttributes.from);
|
||||
cc.push(conversationContact);
|
||||
}
|
||||
|
||||
// Remove the conversation contact's email from the BCC list if present
|
||||
let bcc = (emailAttributes.bcc || []).filter(
|
||||
email => email !== conversationContact
|
||||
);
|
||||
|
||||
// Ensure only unique email addresses are in the CC list
|
||||
bcc = [...new Set(bcc)];
|
||||
cc = [...new Set(cc)];
|
||||
to = [...new Set(to)];
|
||||
|
||||
this.ccEmails = cc.join(', ');
|
||||
this.bccEmails = bcc.join(', ');
|
||||
this.toEmails = to.join(', ');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1077,13 +940,6 @@ export default {
|
||||
@apply bg-transparent py-0 px-4;
|
||||
}
|
||||
|
||||
.reply-box {
|
||||
@apply border-t border-slate-50 dark:border-slate-700 bg-white dark:bg-slate-900;
|
||||
|
||||
&.is-private {
|
||||
@apply bg-yellow-50 dark:bg-yellow-200;
|
||||
}
|
||||
}
|
||||
.send-button {
|
||||
@apply mb-0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
attachedFiles: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasAttachments() {
|
||||
return this.attachedFiles.length;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
attachFile({ blob, file }) {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file.file);
|
||||
reader.onloadend = () => {
|
||||
this.attachedFiles.push({
|
||||
currentChatId: this.currentChat.id,
|
||||
resource: blob || file,
|
||||
isPrivate: this.isPrivate,
|
||||
thumb: reader.result,
|
||||
blobSignedId: blob ? blob.signed_id : undefined,
|
||||
});
|
||||
};
|
||||
},
|
||||
removeAttachment(itemIndex) {
|
||||
this.attachedFiles = this.attachedFiles.filter(
|
||||
(item, index) => itemIndex !== index
|
||||
);
|
||||
},
|
||||
clearAttachments() {
|
||||
this.attachedFiles = [];
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
import { AUDIO_FORMATS } from 'shared/constants/messages';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
isRecordingAudio: false,
|
||||
isRecorderAudioStopped: false,
|
||||
recordingAudioState: '',
|
||||
recordingAudioDurationText: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
audioRecordFormat() {
|
||||
if (this.isAWhatsAppChannel || this.isAPIInbox) {
|
||||
return AUDIO_FORMATS.OGG;
|
||||
}
|
||||
return AUDIO_FORMATS.WAV;
|
||||
},
|
||||
showAudioRecorder() {
|
||||
return !this.isOnPrivateNote && this.showFileUpload;
|
||||
},
|
||||
showAudioRecorderEditor() {
|
||||
return this.showAudioRecorder && this.isRecordingAudio;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleAudioRecorder() {
|
||||
this.isRecordingAudio = !this.isRecordingAudio;
|
||||
this.isRecorderAudioStopped = !this.isRecordingAudio;
|
||||
if (!this.isRecordingAudio) {
|
||||
this.clearMessage();
|
||||
this.clearEmailField();
|
||||
}
|
||||
},
|
||||
toggleAudioRecorderPlayPause() {
|
||||
if (this.isRecordingAudio) {
|
||||
if (!this.isRecorderAudioStopped) {
|
||||
this.isRecorderAudioStopped = true;
|
||||
this.$refs.audioRecorderInput.stopAudioRecording();
|
||||
} else if (this.isRecorderAudioStopped) {
|
||||
this.$refs.audioRecorderInput.playPause();
|
||||
}
|
||||
}
|
||||
},
|
||||
onStateProgressRecorderChanged(duration) {
|
||||
this.recordingAudioDurationText = duration;
|
||||
},
|
||||
onStateRecorderChanged(state) {
|
||||
this.recordingAudioState = state;
|
||||
if (state && 'notallowederror'.includes(state)) {
|
||||
this.toggleAudioRecorder();
|
||||
}
|
||||
},
|
||||
onFinishRecorder(file) {
|
||||
return file && this.onFileUpload(file);
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
// emailHandlingMixin.js
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
ccEmails: '',
|
||||
bccEmails: '',
|
||||
toEmails: '',
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
setCcEmails(value) {
|
||||
this.bccEmails = value.bccEmails;
|
||||
this.ccEmails = value.ccEmails;
|
||||
},
|
||||
setCCAndToEmailsFromLastChat() {
|
||||
if (!this.lastEmail) return;
|
||||
|
||||
const {
|
||||
content_attributes: { email: emailAttributes = {} },
|
||||
} = this.lastEmail;
|
||||
|
||||
const conversationContact = this.currentChat?.meta?.sender?.email || '';
|
||||
let cc = emailAttributes.cc ? [...emailAttributes.cc] : [];
|
||||
let to = [];
|
||||
|
||||
if (cc.includes(conversationContact)) {
|
||||
cc = cc.filter(email => email !== conversationContact);
|
||||
}
|
||||
|
||||
if (!emailAttributes.from.includes(conversationContact)) {
|
||||
to.push(...emailAttributes.from);
|
||||
cc.push(conversationContact);
|
||||
}
|
||||
|
||||
let bcc = (emailAttributes.bcc || []).filter(
|
||||
email => email !== conversationContact
|
||||
);
|
||||
|
||||
bcc = [...new Set(bcc)];
|
||||
cc = [...new Set(cc)];
|
||||
to = [...new Set(to)];
|
||||
|
||||
this.ccEmails = cc.join(', ');
|
||||
this.bccEmails = bcc.join(', ');
|
||||
this.toEmails = to.join(', ');
|
||||
},
|
||||
clearEmailField() {
|
||||
this.ccEmails = '';
|
||||
this.bccEmails = '';
|
||||
this.toEmails = '';
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import { trimContent } from '@chatwoot/utils';
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
getFromDraft() {
|
||||
if (this.conversationIdByRoute) {
|
||||
const key = `draft-${this.conversationIdByRoute}-${this.replyType}`;
|
||||
const messageFromStore =
|
||||
this.$store.getters['draftMessages/get'](key) || '';
|
||||
|
||||
// ensure that the message has signature set based on the ui setting
|
||||
this.message = this.toggleSignatureForDraft(messageFromStore);
|
||||
}
|
||||
},
|
||||
removeFromDraft() {
|
||||
if (this.conversationIdByRoute) {
|
||||
const key = `draft-${this.conversationIdByRoute}-${this.replyType}`;
|
||||
this.$store.dispatch('draftMessages/delete', { key });
|
||||
}
|
||||
},
|
||||
saveDraft(conversationId, replyType) {
|
||||
if (this.message || this.message === '') {
|
||||
const key = `draft-${conversationId}-${replyType}`;
|
||||
const draftToSave = trimContent(this.message || '');
|
||||
|
||||
this.$store.dispatch('draftMessages/set', {
|
||||
key,
|
||||
message: draftToSave,
|
||||
});
|
||||
}
|
||||
},
|
||||
setToDraft(conversationId, replyType) {
|
||||
this.saveDraft(conversationId, replyType);
|
||||
this.message = '';
|
||||
},
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user