import { Meteor } from 'meteor/meteor'; import { ReactiveVar } from 'meteor/reactive-var'; import { ReactiveDict } from 'meteor/reactive-dict'; import { Session } from 'meteor/session'; import { Template } from 'meteor/templating'; import { Tracker } from 'meteor/tracker'; import { EmojiPicker } from '../../../emoji'; import { Users } from '../../../models'; import { settings } from '../../../settings'; import { fileUpload, KonchatNotification, } from '../../../ui'; import { messageBox, popover, call, keyCodes, isRTL, } from '../../../ui-utils'; import { t, roomTypes, getUserPreference, } from '../../../utils'; import moment from 'moment'; import { setupAutogrow } from './messageBoxAutogrow'; import { formattingButtons, applyFormatting, } from './messageBoxFormatting'; import './messageBoxActions'; import './messageBoxReplyPreview'; import './messageBoxTyping'; import './messageBoxAudioMessage'; import './messageBoxNotSubscribed'; import './messageBox.html'; Template.messageBox.onCreated(function() { this.state = new ReactiveDict(); EmojiPicker.init(); this.popupConfig = new ReactiveVar(null); this.replyMessageData = new ReactiveVar(); this.isMicrophoneDenied = new ReactiveVar(true); this.isSendIconVisible = new ReactiveVar(false); this.set = (value) => { const { input } = this; if (!input) { return; } input.value = value; $(input).trigger('change').trigger('input'); }; this.insertNewLine = () => { const { input, autogrow } = this; if (!input) { return; } if (document.selection) { input.focus(); const sel = document.selection.createRange(); sel.text = '\n'; } else if (input.selectionStart || input.selectionStart === 0) { const newPosition = input.selectionStart + 1; const before = input.value.substring(0, input.selectionStart); const after = input.value.substring(input.selectionEnd, input.value.length); input.value = `${ before }\n${ after }`; input.selectionStart = input.selectionEnd = newPosition; } else { input.value += '\n'; } $(input).trigger('change').trigger('input'); input.blur(); input.focus(); autogrow.update(); }; this.send = (event) => { const { input } = this; if (!input) { return; } const { autogrow, data: { rid, tmid, onSend } } = this; const { value } = input; this.set(''); onSend && onSend.call(this.data, event, { rid, tmid, value }, () => { autogrow.update(); input.focus(); }); }; }); Template.messageBox.onRendered(function() { const $input = $(this.find('.js-input-message')); $input.on('dataChange', () => { const messages = $input.data('reply') || []; this.replyMessageData.set(messages); }); this.autorun(() => { const { rid, subscription } = Template.currentData(); const room = Session.get(`roomData${ rid }`); if (!room) { return this.state.set({ room: false, isBlockedOrBlocker: false, mustJoinWithCode: false, }); } const isBlocked = (room && room.t === 'd' && subscription && subscription.blocked); const isBlocker = (room && room.t === 'd' && subscription && subscription.blocker); const isBlockedOrBlocker = isBlocked || isBlocker; const mustJoinWithCode = !subscription && room.joinCodeRequired; return this.state.set({ room: false, isBlockedOrBlocker, mustJoinWithCode, }); }); this.autorun(() => { const { rid, onInputChanged, onResize } = Template.currentData(); Tracker.afterFlush(() => { const input = this.find('.js-input-message'); if (this.input === input) { return; } this.input = input; onInputChanged && onInputChanged(input); if (input && rid) { this.popupConfig.set({ rid, getInput: () => input, }); } else { this.popupConfig.set(null); } if (this.autogrow) { this.autogrow.destroy(); this.autogrow = null; } if (!input) { return; } const shadow = this.find('.js-input-message-shadow'); this.autogrow = setupAutogrow(input, shadow, onResize); }); }); }); Template.messageBox.onDestroyed(function() { if (!this.autogrow) { return; } this.autogrow.destroy(); }); Template.messageBox.helpers({ isAnonymousOrMustJoinWithCode() { const instance = Template.instance(); const { rid } = Template.currentData(); if (!rid) { return false; } const isAnonymous = !Meteor.userId(); return isAnonymous || instance.state.get('mustJoinWithCode'); }, isWritable() { const { rid, subscription } = Template.currentData(); if (!rid) { return true; } const isBlockedOrBlocker = Template.instance().state.get('isBlockedOrBlocker'); if (isBlockedOrBlocker) { return false; } const isReadOnly = roomTypes.readOnly(rid, Users.findOne({ _id: Meteor.userId() }, { fields: { username: 1 } })); const isArchived = roomTypes.archived(rid) || (subscription && subscription.t === 'd' && subscription.archived); return !isReadOnly && !isArchived; }, popupConfig() { return Template.instance().popupConfig.get(); }, input() { return Template.instance().input; }, replyMessageData() { return Template.instance().replyMessageData.get(); }, isEmojiEnabled() { return getUserPreference(Meteor.userId(), 'useEmojis'); }, maxMessageLength() { return settings.get('Message_AllowConvertLongMessagesToAttachment') ? null : settings.get('Message_MaxAllowedSize'); }, isSendIconVisible() { return Template.instance().isSendIconVisible.get(); }, canSend() { const { rid } = Template.currentData(); if (!rid) { return true; } return roomTypes.verifyCanSendMessage(rid); }, actions() { const actionGroups = messageBox.actions.get(); return Object.values(actionGroups) .reduce((actions, actionGroup) => [...actions, ...actionGroup], []); }, formattingButtons() { return formattingButtons.filter(({ condition }) => !condition || condition()); }, isBlockedOrBlocker() { return Template.instance().state.get('isBlockedOrBlocker'); }, }); const handleFormattingShortcut = (event, instance) => { const isMacOS = navigator.platform.indexOf('Mac') !== -1; const isCmdOrCtrlPressed = (isMacOS && event.metaKey) || (!isMacOS && event.ctrlKey); if (!isCmdOrCtrlPressed) { return false; } const key = event.key.toLowerCase(); const { pattern } = formattingButtons .filter(({ condition }) => !condition || condition()) .find(({ command }) => command === key) || {}; if (!pattern) { return false; } const { input } = instance; applyFormatting(pattern, input); return true; }; const handleSubmit = (event, instance) => { const { which: keyCode } = event; const isSubmitKey = keyCode === keyCodes.CARRIAGE_RETURN || keyCode === keyCodes.NEW_LINE; if (!isSubmitKey) { return false; } const sendOnEnter = getUserPreference(Meteor.userId(), 'sendOnEnter'); const sendOnEnterActive = sendOnEnter == null || sendOnEnter === 'normal' || (sendOnEnter === 'desktop' && Meteor.Device.isDesktop()); const withModifier = event.shiftKey || event.ctrlKey || event.altKey || event.metaKey; const isSending = (sendOnEnterActive && !withModifier) || (!sendOnEnterActive && withModifier); if (isSending) { instance.send(event); return true; } instance.insertNewLine(); return true; }; Template.messageBox.events({ async 'click .js-join'(event) { event.stopPropagation(); event.preventDefault(); const joinCodeInput = Template.instance().find('[name=joinCode]'); const joinCode = joinCodeInput && joinCodeInput.value; await call('joinRoom', this.rid, joinCode); }, 'click .js-emoji-picker'(event, instance) { event.stopPropagation(); event.preventDefault(); if (!getUserPreference(Meteor.userId(), 'useEmojis')) { return; } if (EmojiPicker.isOpened()) { EmojiPicker.close(); return; } EmojiPicker.open(event.currentTarget, (emoji) => { const emojiValue = `:${ emoji }: `; const { input } = instance; const caretPos = input.selectionStart; const textAreaTxt = input.value; input.focus(); if (!document.execCommand || !document.execCommand('insertText', false, emojiValue)) { instance.set(textAreaTxt.substring(0, caretPos) + emojiValue + textAreaTxt.substring(caretPos)); input.focus(); } input.selectionStart = caretPos + emojiValue.length; input.selectionEnd = caretPos + emojiValue.length; }); }, 'focus .js-input-message'() { KonchatNotification.removeRoomNotification(this.rid); }, 'keydown .js-input-message'(event, instance) { const isEventHandled = handleFormattingShortcut(event, instance) || handleSubmit(event, instance); if (isEventHandled) { event.preventDefault(); event.stopPropagation(); return; } const { rid, tmid, onKeyDown } = this; onKeyDown && onKeyDown.call(this, event, { rid, tmid }); }, 'keyup .js-input-message'(event) { const { rid, tmid, onKeyUp } = this; onKeyUp && onKeyUp.call(this, event, { rid, tmid }); }, 'paste .js-input-message'(event, instance) { const { rid, tmid } = this; const { input, autogrow } = instance; setTimeout(() => autogrow && autogrow.update(), 50); if (!event.originalEvent.clipboardData) { return; } const files = [...event.originalEvent.clipboardData.items] .filter((item) => (item.kind === 'file' && item.type.indexOf('image/') !== -1)) .map((item) => ({ file: item.getAsFile(), name: `Clipboard - ${ moment().format(settings.get('Message_TimeAndDateFormat')) }`, })) .filter(({ file }) => file !== null); if (files.length) { event.preventDefault(); fileUpload(files, input, { rid, tmid }); return; } }, 'input .js-input-message'(event, instance) { const { input } = instance; if (!input) { return; } instance.isSendIconVisible.set(!!input.value); if (input.value.length > 0) { input.dir = isRTL(input.value) ? 'rtl' : 'ltr'; } const { rid, tmid, onValueChanged } = this; onValueChanged && onValueChanged.call(this, event, { rid, tmid }); }, 'propertychange .js-input-message'(event, instance) { if (event.originalEvent.propertyName !== 'value') { return; } const { input } = instance; if (!input) { return; } instance.sendIconDisabled.set(!!input.value); if (input.value.length > 0) { input.dir = isRTL(input.value) ? 'rtl' : 'ltr'; } const { rid, tmid, onValueChanged } = this; onValueChanged && onValueChanged.call(this, event, { rid, tmid }); }, async 'click .js-send'(event, instance) { instance.send(event); }, 'click .js-action-menu'(event, instance) { const groups = messageBox.actions.get(); const config = { popoverClass: 'message-box', columns: [ { groups: Object.keys(groups).map((group) => { const items = []; groups[group].forEach((item) => { items.push({ icon: item.icon, name: t(item.label), type: 'messagebox-action', id: item.id, }); }); return { title: t(group), items, }; }), }, ], offsetVertical: 10, direction: 'top-inverted', currentTarget: event.currentTarget.firstElementChild.firstElementChild, data: { rid: this.rid, tmid: this.tmid, messageBox: instance.firstNode, }, activeElement: event.currentTarget, }; popover.open(config); }, 'click .js-message-actions .js-message-action'(event, instance) { const { id } = event.currentTarget.dataset; const actions = messageBox.actions.getById(id); actions .filter(({ action }) => !!action) .forEach(({ action }) => { action.call(null, { rid: this.rid, tmid: this.tmid, messageBox: instance.firstNode, event, }); }); }, 'click .js-format'(event, instance) { event.preventDefault(); event.stopPropagation(); const { id } = event.currentTarget.dataset; const { pattern } = formattingButtons .filter(({ condition }) => !condition || condition()) .find(({ label }) => label === id) || {}; if (!pattern) { return; } applyFormatting(pattern, instance.input); }, });