Skip to content
Snippets Groups Projects
Unverified Commit 2f90da3f authored by Rafael Ferreira's avatar Rafael Ferreira Committed by GitHub
Browse files

[NEW] Email Inboxes for Omnichannel (#20101)

parent 64d77c99
No related branches found
No related tags found
No related merge requests found
Showing
with 446 additions and 159 deletions
......@@ -38,5 +38,6 @@ import './v1/oauthapps';
import './v1/custom-sounds';
import './v1/custom-user-status';
import './v1/instances';
import './v1/email-inbox';
export { API, APIClass, defaultRateLimiterOptions } from './api';
import { EmailInbox } from '../../../models/server/raw';
import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
import { Users } from '../../../models';
export async function findEmailInboxes({ userId, query = {}, pagination: { offset, count, sort } }) {
if (!await hasPermissionAsync(userId, 'manage-email-inbox')) {
throw new Error('error-not-allowed');
}
const cursor = EmailInbox.find(query, {
sort: sort || { name: 1 },
skip: offset,
limit: count,
});
const total = await cursor.count();
const emailInboxes = await cursor.toArray();
return {
emailInboxes,
count: emailInboxes.length,
offset,
total,
};
}
export async function findOneEmailInbox({ userId, _id }) {
if (!await hasPermissionAsync(userId, 'manage-email-inbox')) {
throw new Error('error-not-allowed');
}
return EmailInbox.findOneById(_id);
}
export async function insertOneOrUpdateEmailInbox(userId, emailInboxParams) {
const { _id, active, name, email, description, senderInfo, department, smtp, imap } = emailInboxParams;
if (!_id) {
emailInboxParams._createdAt = new Date();
emailInboxParams._updatedAt = new Date();
emailInboxParams._createdBy = Users.findOne(userId, { fields: { username: 1 } });
return EmailInbox.insertOne(emailInboxParams);
}
const emailInbox = await findOneEmailInbox({ userId, id: _id });
if (!emailInbox) {
throw new Error('error-invalid-email-inbox');
}
const updateEmailInbox = {
$set: {
active,
name,
email,
description,
senderInfo,
smtp,
imap,
_updatedAt: new Date(),
},
};
if (department === 'All') {
updateEmailInbox.$unset = {
department: 1,
};
} else {
updateEmailInbox.$set.department = department;
}
return EmailInbox.updateOne({ _id }, updateEmailInbox);
}
export async function findOneEmailInboxByEmail({ userId, email }) {
if (!await hasPermissionAsync(userId, 'manage-email-inbox')) {
throw new Error('error-not-allowed');
}
return EmailInbox.findOne({ email });
}
import { check, Match } from 'meteor/check';
import { API } from '../api';
import { findEmailInboxes, findOneEmailInbox, insertOneOrUpdateEmailInbox } from '../lib/emailInbox';
import { hasPermission } from '../../../authorization/server/functions/hasPermission';
import { EmailInbox } from '../../../models';
import Users from '../../../models/server/models/Users';
import { sendTestEmailToInbox } from '../../../../server/features/EmailInbox/EmailInbox_Outgoing';
API.v1.addRoute('email-inbox.list', { authRequired: true }, {
get() {
const { offset, count } = this.getPaginationItems();
const { sort, query } = this.parseJsonQuery();
const emailInboxes = Promise.await(findEmailInboxes({ userId: this.userId, query, pagination: { offset, count, sort } }));
return API.v1.success(emailInboxes);
},
});
API.v1.addRoute('email-inbox', { authRequired: true }, {
post() {
if (!hasPermission(this.userId, 'manage-email-inbox')) {
throw new Error('error-not-allowed');
}
check(this.bodyParams, {
_id: Match.Maybe(String),
name: String,
email: String,
active: Boolean,
description: Match.Maybe(String),
senderInfo: Match.Maybe(String),
department: Match.Maybe(String),
smtp: Match.ObjectIncluding({
password: String,
port: Number,
secure: Boolean,
server: String,
username: String,
}),
imap: Match.ObjectIncluding({
password: String,
port: Number,
secure: Boolean,
server: String,
username: String,
}),
});
const emailInboxParams = this.bodyParams;
const { _id } = emailInboxParams;
Promise.await(insertOneOrUpdateEmailInbox(this.userId, emailInboxParams));
return API.v1.success({ _id });
},
});
API.v1.addRoute('email-inbox/:_id', { authRequired: true }, {
get() {
check(this.urlParams, {
_id: String,
});
const { _id } = this.urlParams;
if (!_id) { throw new Error('error-invalid-param'); }
const emailInboxes = Promise.await(findOneEmailInbox({ userId: this.userId, _id }));
return API.v1.success(emailInboxes);
},
delete() {
if (!hasPermission(this.userId, 'manage-email-inbox')) {
throw new Error('error-not-allowed');
}
check(this.urlParams, {
_id: String,
});
const { _id } = this.urlParams;
if (!_id) { throw new Error('error-invalid-param'); }
const emailInboxes = EmailInbox.findOneById(_id);
if (!emailInboxes) {
return API.v1.notFound();
}
EmailInbox.removeById(_id);
return API.v1.success({ _id });
},
});
API.v1.addRoute('email-inbox.search', { authRequired: true }, {
get() {
if (!hasPermission(this.userId, 'manage-email-inbox')) {
throw new Error('error-not-allowed');
}
check(this.queryParams, {
email: String,
});
const { email } = this.queryParams;
const emailInbox = Promise.await(EmailInbox.findOne({ email }));
return API.v1.success({ emailInbox });
},
});
API.v1.addRoute('email-inbox.send-test/:_id', { authRequired: true }, {
post() {
if (!hasPermission(this.userId, 'manage-email-inbox')) {
throw new Error('error-not-allowed');
}
check(this.urlParams, {
_id: String,
});
const { _id } = this.urlParams;
if (!_id) { throw new Error('error-invalid-param'); }
const emailInbox = Promise.await(findOneEmailInbox({ userId: this.userId, _id }));
if (!emailInbox) {
return API.v1.notFound();
}
const user = Users.findOneById(this.userId);
Promise.await(sendTestEmailToInbox(emailInbox, user));
return API.v1.success({ _id });
},
});
......@@ -52,6 +52,7 @@ Meteor.startup(function() {
{ _id: 'leave-c', roles: ['admin', 'user', 'bot', 'anonymous', 'app'] },
{ _id: 'leave-p', roles: ['admin', 'user', 'bot', 'anonymous', 'app'] },
{ _id: 'manage-assets', roles: ['admin'] },
{ _id: 'manage-email-inbox', roles: ['admin'] },
{ _id: 'manage-emoji', roles: ['admin'] },
{ _id: 'manage-user-status', roles: ['admin'] },
{ _id: 'manage-outgoing-integrations', roles: ['admin'] },
......
import { Meteor } from 'meteor/meteor';
import IMAP from 'imap';
import POP3Lib from 'poplib';
import { simpleParser } from 'mailparser';
import { settings } from '../../../settings';
import { IMAPInterceptor } from '../../../../server/email/IMAPInterceptor';
import { processDirectEmail } from '.';
export class IMAPIntercepter {
constructor() {
this.imap = new IMAP({
export class IMAPIntercepter extends IMAPInterceptor {
constructor(imapConfig, options = {}) {
imapConfig = {
user: settings.get('Direct_Reply_Username'),
password: settings.get('Direct_Reply_Password'),
host: settings.get('Direct_Reply_Host'),
port: settings.get('Direct_Reply_Port'),
debug: settings.get('Direct_Reply_Debug') ? console.log : false,
tls: !settings.get('Direct_Reply_IgnoreTLS'),
connTimeout: 30000,
keepalive: true,
});
this.delete = settings.get('Direct_Reply_Delete');
// On successfully connected.
this.imap.on('ready', Meteor.bindEnvironment(() => {
if (this.imap.state !== 'disconnected') {
this.openInbox(Meteor.bindEnvironment((err) => {
if (err) {
throw err;
}
// fetch new emails & wait [IDLE]
this.getEmails();
// If new message arrived, fetch them
this.imap.on('mail', Meteor.bindEnvironment(() => {
this.getEmails();
}));
}));
} else {
console.log('IMAP didnot connected.');
this.imap.end();
}
}));
this.imap.on('error', (err) => {
console.log('Error occurred ...');
throw err;
});
}
openInbox(cb) {
this.imap.openBox('INBOX', false, cb);
}
start() {
this.imap.connect();
}
isActive() {
if (this.imap && this.imap.state && this.imap.state === 'disconnected') {
return false;
}
return true;
}
stop(callback = new Function()) {
this.imap.end();
this.imap.once('end', callback);
}
restart() {
this.stop(() => {
console.log('Restarting IMAP ....');
this.start();
});
}
// Fetch all UNSEEN messages and pass them for further processing
getEmails() {
this.imap.search(['UNSEEN'], Meteor.bindEnvironment((err, newEmails) => {
if (err) {
console.log(err);
throw err;
}
// newEmails => array containing serials of unseen messages
if (newEmails.length > 0) {
const f = this.imap.fetch(newEmails, {
// fetch headers & first body part.
bodies: ['HEADER.FIELDS (FROM TO DATE MESSAGE-ID)', '1'],
struct: true,
markSeen: true,
});
f.on('message', Meteor.bindEnvironment((msg, seqno) => {
const email = {};
msg.on('body', (stream, info) => {
let headerBuffer = '';
let bodyBuffer = '';
stream.on('data', (chunk) => {
if (info.which === '1') {
bodyBuffer += chunk.toString('utf8');
} else {
headerBuffer += chunk.toString('utf8');
}
});
...imapConfig,
};
stream.once('end', () => {
if (info.which === '1') {
email.body = bodyBuffer;
} else {
// parse headers
email.headers = IMAP.parseHeader(headerBuffer);
options.deleteAfterRead = settings.get('Direct_Reply_Delete');
email.headers.to = email.headers.to[0];
email.headers.date = email.headers.date[0];
email.headers.from = email.headers.from[0];
}
});
});
super(imapConfig, options);
// On fetched each message, pass it further
msg.once('end', Meteor.bindEnvironment(() => {
// delete message from inbox
if (this.delete) {
this.imap.seq.addFlags(seqno, 'Deleted', (err) => {
if (err) { console.log(`Mark deleted error: ${ err }`); }
});
}
processDirectEmail(email);
}));
}));
f.once('error', (err) => {
console.log(`Fetch error: ${ err }`);
});
}
}));
this.on('email', Meteor.bindEnvironment((email) => processDirectEmail(email)));
}
}
......
......@@ -39,6 +39,8 @@
<ul>
{{#with room}}
{{#if servedBy}}<li><strong>{{_ "Agent"}}</strong>: {{servedBy.username}}</li>{{/if}}
{{#if email}}<li><strong>{{_ "Email_Inbox"}}</strong>: {{email.inbox}}</li>{{/if}}
{{#if email}}<li><strong>{{_ "Email_subject"}}</strong>: {{email.subject}}</li>{{/if}}
{{#if facebook}}<li><i class="icon-facebook"></i>{{_ "Facebook_Page"}}: {{facebook.page.name}}</li>{{/if}}
{{#if sms}}<li><i class="i con-mobile"></i>{{_ "SMS_Enabled"}}</li>{{/if}}
{{#if topic}}<li><strong>{{_ "Topic"}}</strong>: {{{markdown topic}}}</li>{{/if}}
......
......@@ -202,7 +202,8 @@ Template.visitorInfo.helpers({
},
canSendTranscript() {
return hasPermission('send-omnichannel-chat-transcript');
const room = Template.instance().room.get();
return !room.email && hasPermission('send-omnichannel-chat-transcript');
},
roomClosedDateTime() {
......
......@@ -55,7 +55,7 @@ export abstract class AbstractBusinessHourType {
businessHourData.active = Boolean(businessHourData.active);
businessHourData = this.convertWorkHours(businessHourData);
if (businessHourData._id) {
await this.BusinessHourRepository.updateOne(businessHourData._id, businessHourData);
await this.BusinessHourRepository.updateOne({ _id: businessHourData._id }, { $set: businessHourData });
return businessHourData._id;
}
const { insertedId } = await this.BusinessHourRepository.insertOne(businessHourData);
......
......@@ -6,6 +6,21 @@ import { checkServiceStatus, createLivechatRoom, createLivechatInquiry } from '.
import { callbacks } from '../../../callbacks/server';
import { RoutingManager } from './RoutingManager';
const queueInquiry = async (room, inquiry, defaultAgent) => {
if (!defaultAgent) {
defaultAgent = RoutingManager.getMethod().delegateAgent(defaultAgent, inquiry);
}
inquiry = await callbacks.run('livechat.beforeRouteChat', inquiry, defaultAgent);
if (inquiry.status === 'ready') {
return RoutingManager.delegateInquiry(inquiry, defaultAgent);
}
if (inquiry.status === 'queued') {
Meteor.defer(() => callbacks.run('livechat.chatQueued', room));
}
};
export const QueueManager = {
async requestRoom({ guest, message, roomInfo, agent, extraData }) {
check(message, Match.ObjectIncluding({
......@@ -26,23 +41,38 @@ export const QueueManager = {
const name = (roomInfo && roomInfo.fname) || guest.name || guest.username;
const room = LivechatRooms.findOneById(createLivechatRoom(rid, name, guest, roomInfo, extraData));
let inquiry = LivechatInquiry.findOneById(createLivechatInquiry({ rid, name, guest, message, extraData }));
const inquiry = LivechatInquiry.findOneById(createLivechatInquiry({ rid, name, guest, message, extraData }));
LivechatRooms.updateRoomCount();
if (!agent) {
agent = RoutingManager.getMethod().delegateAgent(agent, inquiry);
}
await queueInquiry(room, inquiry, agent);
inquiry = await callbacks.run('livechat.beforeRouteChat', inquiry, agent);
if (inquiry.status === 'ready') {
return RoutingManager.delegateInquiry(inquiry, agent);
return room;
},
async unarchiveRoom(archivedRoom = {}) {
const { _id: rid, open, closedAt, fname: name, servedBy, v, departmentId: department, lastMessage: message } = archivedRoom;
if (!rid || !closedAt || !!open) {
return archivedRoom;
}
if (inquiry.status === 'queued') {
Meteor.defer(() => callbacks.run('livechat.chatQueued', room));
const oldInquiry = LivechatInquiry.findOneByRoomId(rid);
if (oldInquiry) {
LivechatInquiry.removeByRoomId(rid);
}
const guest = {
...v,
...department && { department },
};
const defaultAgent = servedBy && { agentId: servedBy._id, username: servedBy.username };
LivechatRooms.unarchiveOneById(rid);
const room = LivechatRooms.findOneById(rid);
const inquiry = LivechatInquiry.findOneById(createLivechatInquiry({ rid, name, guest, message }));
await queueInquiry(room, inquiry, defaultAgent);
return room;
},
};
......@@ -109,7 +109,7 @@ export const sendNoWrap = ({ to, from, replyTo, subject, html, text, headers })
}
if (!text) {
text = stripHtml(html);
text = stripHtml(html).result;
}
if (settings.get('email_plain_text_only')) {
......@@ -127,7 +127,7 @@ export const send = ({ to, from, replyTo, subject, html, text, data, headers })
subject: replace(subject, data),
text: text
? replace(text, data)
: stripHtml(replace(html, data)),
: stripHtml(replace(html, data)).result,
html: wrap(html, data),
headers,
});
......
......@@ -39,6 +39,7 @@ import ReadReceipts from './models/ReadReceipts';
import LivechatExternalMessage from './models/LivechatExternalMessages';
import OmnichannelQueue from './models/OmnichannelQueue';
import Analytics from './models/Analytics';
import EmailInbox from './models/EmailInbox';
export { AppsLogsModel } from './models/apps-logs-model';
export { AppsPersistenceModel } from './models/apps-persistence-model';
......@@ -90,4 +91,5 @@ export {
LivechatInquiry,
Analytics,
OmnichannelQueue,
EmailInbox,
};
import { Base } from './_Base';
export class EmailInbox extends Base {
constructor() {
super('email_inbox');
this.tryEnsureIndex({ email: 1 }, { unique: true });
}
findOneById(_id, options) {
return this.findOne(_id, options);
}
create(data) {
return this.insert(data);
}
updateById(_id, data) {
return this.update({ _id }, data);
}
removeById(_id) {
return this.remove(_id);
}
}
export default new EmailInbox();
......@@ -19,6 +19,7 @@ export class LivechatRooms extends Base {
this.tryEnsureIndex({ closedAt: 1 }, { sparse: true });
this.tryEnsureIndex({ servedBy: 1 }, { sparse: true });
this.tryEnsureIndex({ 'v.token': 1 }, { sparse: true });
this.tryEnsureIndex({ 'v.token': 1, 'email.thread': 1 }, { sparse: true });
this.tryEnsureIndex({ 'v._id': 1 }, { sparse: true });
}
......@@ -168,6 +169,28 @@ export class LivechatRooms extends Base {
return this.findOne(query, options);
}
findOneByVisitorTokenAndEmailThread(visitorToken, emailThread, options) {
const query = {
t: 'l',
'v.token': visitorToken,
'email.thread': emailThread,
};
return this.findOne(query, options);
}
findOneOpenByVisitorTokenAndEmailThread(visitorToken, emailThread, options) {
const query = {
t: 'l',
open: true,
'v.token': visitorToken,
'email.thread': emailThread,
};
return this.findOne(query, options);
}
findOneLastServedAndClosedByVisitorToken(visitorToken, options = {}) {
const query = {
t: 'l',
......@@ -706,6 +729,26 @@ export class LivechatRooms extends Base {
return this.update(query, update);
}
unarchiveOneById(roomId) {
const query = {
_id: roomId,
t: 'l',
};
const update = {
$set: {
open: true,
},
$unset: {
servedBy: 1,
closedAt: 1,
closedBy: 1,
closer: 1,
},
};
return this.update(query, update);
}
}
export default new LivechatRooms(Rooms.model, true);
import { Collection, FindOneOptions, Cursor, WriteOpResult, DeleteWriteOpResultObject, FilterQuery, UpdateQuery, UpdateOneOptions } from 'mongodb';
import {
Collection,
CollectionInsertOneOptions,
Cursor,
DeleteWriteOpResultObject,
FilterQuery,
FindOneOptions,
InsertOneWriteOpResult,
ObjectID,
ObjectId,
OptionalId,
UpdateManyOptions,
UpdateOneOptions,
UpdateQuery,
UpdateWriteOpResult,
WithId,
WriteOpResult,
} from 'mongodb';
// [extracted from @types/mongo] TypeScript Omit (Exclude to be specific) does not work for objects with an "any" indexed type, and breaks discriminated unions
type EnhancedOmit<T, K> = string | number extends keyof T
? T // T has indexed type e.g. { _id: string; [k: string]: any; } or it is "any"
: T extends any
? Pick<T, Exclude<keyof T, K>> // discriminated unions
: never;
// [extracted from @types/mongo]
type ExtractIdType<TSchema> = TSchema extends { _id: infer U } // user has defined a type for _id
? {} extends U
? Exclude<U, {}>
: unknown extends U
? ObjectId
: U
: ObjectId;
type ModelOptionalId<T> = EnhancedOmit<T, '_id'> & { _id?: ExtractIdType<T> };
interface ITrash {
__collection__: string;
......@@ -70,6 +105,24 @@ export class BaseRaw<T> implements IBaseRaw<T> {
return this.col.update(filter, update, options);
}
updateOne(filter: FilterQuery<T>, update: UpdateQuery<T> | Partial<T>, options?: UpdateOneOptions & { multi?: boolean }): Promise<UpdateWriteOpResult> {
return this.col.updateOne(filter, update, options);
}
updateMany(filter: FilterQuery<T>, update: UpdateQuery<T> | Partial<T>, options?: UpdateManyOptions): Promise<UpdateWriteOpResult> {
return this.col.updateMany(filter, update, options);
}
insertOne(doc: ModelOptionalId<T>, options?: CollectionInsertOneOptions): Promise<InsertOneWriteOpResult<WithId<T>>> {
if (!doc._id || typeof doc._id !== 'string') {
const oid = new ObjectID();
doc = { _id: oid.toHexString(), ...doc };
}
// TODO reavaluate following type casting
return this.col.insertOne(doc as unknown as OptionalId<T>, options);
}
removeById(_id: string): Promise<DeleteWriteOpResultObject> {
const query: object = { _id };
return this.col.deleteOne(query);
......
import { BaseRaw } from './BaseRaw';
import { IEmailInbox } from '../../../../definition/IEmailInbox';
export class EmailInboxRaw extends BaseRaw<IEmailInbox> {
//
}
......@@ -57,20 +57,6 @@ export class LivechatBusinessHoursRaw extends BaseRaw<ILivechatBusinessHour> {
});
}
async updateOne(_id: string, data: Omit<ILivechatBusinessHour, '_id'>): Promise<any> {
const query = {
_id,
};
const update = {
$set: {
...data,
},
};
return this.col.updateOne(query, update);
}
// TODO: Remove this function after remove the deprecated method livechat:saveOfficeHours
async updateDayOfGlobalBusinessHour(day: Omit<IBusinessHourWorkHour, 'code'>): Promise<any> {
return this.col.updateOne({
......
......@@ -63,6 +63,8 @@ import { IntegrationHistoryRaw } from './IntegrationHistory';
import IntegrationHistoryModel from '../models/IntegrationHistory';
import OmnichannelQueueModel from '../models/OmnichannelQueue';
import { OmnichannelQueueRaw } from './OmnichannelQueue';
import EmailInboxModel from '../models/EmailInbox';
import { EmailInboxRaw } from './EmailInbox';
import { api } from '../../../../server/sdk/api';
import { initWatchers } from '../../../../server/modules/watchers/watchers.module';
......@@ -100,6 +102,7 @@ export const InstanceStatus = new InstanceStatusRaw(InstanceStatusModel.model.ra
export const IntegrationHistory = new IntegrationHistoryRaw(IntegrationHistoryModel.model.rawCollection(), trashCollection);
export const Sessions = new SessionsRaw(SessionsModel.model.rawCollection(), trashCollection);
export const OmnichannelQueue = new OmnichannelQueueRaw(OmnichannelQueueModel.model.rawCollection(), trashCollection);
export const EmailInbox = new EmailInboxRaw(EmailInboxModel.model.rawCollection(), trashCollection);
const map = {
[Messages.col.collectionName]: MessagesModel,
......@@ -116,6 +119,7 @@ const map = {
[InstanceStatus.col.collectionName]: InstanceStatusModel,
[IntegrationHistory.col.collectionName]: IntegrationHistoryModel,
[Integrations.col.collectionName]: IntegrationsModel,
[EmailInbox.col.collectionName]: EmailInboxModel,
};
if (!process.env.DISABLE_DB_WATCH) {
......@@ -134,6 +138,7 @@ if (!process.env.DISABLE_DB_WATCH) {
InstanceStatus,
IntegrationHistory,
Integrations,
EmailInbox,
};
initWatchers(models, api.broadcastLocal.bind(api), (model, fn) => {
......
......@@ -106,16 +106,25 @@ Template.messageBox.onCreated(function() {
});
Template.messageBox.onRendered(function() {
const $input = $(this.find('.js-input-message'));
this.source = $input[0];
$input.on('dataChange', () => {
const messages = $input.data('reply') || [];
this.replyMessageData.set(messages);
});
let inputSetup = false;
this.autorun(() => {
const { rid, subscription } = Template.currentData();
const room = Session.get(`roomData${ rid }`);
if (!inputSetup) {
const $input = $(this.find('.js-input-message'));
this.source = $input[0];
if (this.source) {
inputSetup = true;
}
$input.on('dataChange', () => {
const messages = $input.data('reply') || [];
console.log('dataChange', messages);
this.replyMessageData.set(messages);
});
}
if (!room) {
return this.state.set({
room: false,
......
......@@ -9,12 +9,15 @@ import {
Layout,
MessageAction,
} from '../../../../../ui-utils/client';
import {
addMessageToList,
} from '../../../../../ui-utils/client/lib/MessageAction';
import { call } from '../../../../../ui-utils/client/lib/callMethod';
import { promises } from '../../../../../promises/client';
import { isURL } from '../../../../../utils/lib/isURL';
import { openUserCard } from '../../../lib/UserCard';
import { messageArgs } from '../../../../../ui-utils/client/lib/messageArgs';
import { ChatMessage, Rooms } from '../../../../../models';
import { ChatMessage, Rooms, Messages } from '../../../../../models';
import { t } from '../../../../../utils/client';
import { chatMessages } from '../room';
import { EmojiEvents } from '../../../../../reactions/client/init';
......@@ -220,6 +223,26 @@ export const getCommonRoomEvents = () => ({
input.value = msg;
input.focus();
},
async 'click .js-actionButton-respondWithQuotedMessage'(event, instance) {
const { rid } = instance.data;
const { id: msgId } = event.currentTarget;
const { $input } = chatMessages[rid];
if (!msgId) {
return;
}
const message = Messages.findOne({ _id: msgId });
let messages = $input.data('reply') || [];
messages = addMessageToList(messages, message);
$input
.focus()
.data('mention-user', false)
.data('reply', messages)
.trigger('dataChange');
},
async 'click .js-actionButton-sendMessage'(event, instance) {
const { rid } = instance.data;
const msg = event.currentTarget.value;
......
export declare const slashCommand: {
add(command: string, callback: Function, options: object /* , result, providesPreview = false, previewer, previewCallback*/): void;
};
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment