diff options
author | Daniel Calviño Sánchez <danxuliu@gmail.com> | 2020-01-08 04:23:05 +0300 |
---|---|---|
committer | Joas Schilling <coding@schilljs.com> | 2020-01-09 22:19:18 +0300 |
commit | c728282147a1c376630902fbe7bf0d3f3d3b5cb6 (patch) | |
tree | 3c6f1be316b50e9104f906c113e42f398380fe81 /src | |
parent | 920deedec88bdd8ceb3fdc3c382ebb0a1630b32e (diff) |
Add basic autocompletion of mentions
The vue-at component is used to provide the autocompletion UI. This
component handles the edition in its wrapped contenteditable, so the
"vue-contenteditable-directive" is no longer needed. As the new message
form hides its overflowing children (and it can not be removed, as it
breaks the layout when typing long (tall) messages) the
"vue-at-reparenter" helper is used.
In this initial version the candidate mentions show the id of the user
or guest that matches the current "@" mention being written and, when
selected, adds that id as plain text to the editable content.
Signed-off-by: Daniel Calviño Sánchez <danxuliu@gmail.com>
Diffstat (limited to 'src')
-rw-r--r-- | src/components/NewMessageForm/AdvancedInput/AdvancedInput.vue | 70 | ||||
-rw-r--r-- | src/components/NewMessageForm/NewMessageForm.vue | 1 | ||||
-rw-r--r-- | src/main.js | 2 | ||||
-rw-r--r-- | src/mainChatTab.js | 2 | ||||
-rw-r--r-- | src/services/mentionsService.js | 42 |
5 files changed, 107 insertions, 10 deletions
diff --git a/src/components/NewMessageForm/AdvancedInput/AdvancedInput.vue b/src/components/NewMessageForm/AdvancedInput/AdvancedInput.vue index d5304e835..4dcf557ee 100644 --- a/src/components/NewMessageForm/AdvancedInput/AdvancedInput.vue +++ b/src/components/NewMessageForm/AdvancedInput/AdvancedInput.vue @@ -20,19 +20,33 @@ --> <template> - <div ref="contentEditable" - v-contenteditable:text.dangerousHTML="activeInput" - :placeHolder="placeholderText" - class="new-message-form__advancedinput" - @keydown.enter="handleKeydown" - @paste="onPaste" /> + <At ref="at" + v-model="text" + :members="autoCompleteMentionCandidates" + @at="handleAtEvent"> + <div ref="contentEditable" + :contenteditable="activeInput" + :placeHolder="placeholderText" + class="new-message-form__advancedinput" + @keydown.enter="handleKeydown" + @paste="onPaste" /> + </At> </template> <script> +import At from 'vue-at' +import VueAtReparenter from '../../../mixins/vueAtReparenter' import { EventBus } from '../../../services/EventBus' +import { searchPossibleMentions } from '../../../services/mentionsService' export default { name: 'AdvancedInput', + components: { + At, + }, + mixins: [ + VueAtReparenter, + ], props: { /** * The placeholder for the input field @@ -54,10 +68,19 @@ export default { type: String, required: true, }, + + /** + * The token of the conversation to get candidate mentions for. + */ + token: { + type: String, + required: true, + }, }, data: function() { return { text: '', + autoCompleteMentionCandidates: [], } }, watch: { @@ -69,6 +92,14 @@ export default { value(value) { this.text = value }, + atwho(atwho) { + if (!atwho) { + // Clear mention candidates when closing the panel. Otherwise + // they would be shown when the panel is opened again until the + // new ones are received. + this.autoCompleteMentionCandidates = [] + } + }, }, mounted() { this.focusInput() @@ -107,12 +138,39 @@ export default { * @param {object} event the event object; */ handleKeydown(event) { + // Prevent submit event when vue-at panel is open, as that should + // just select the mention from the panel. + if (this.atwho) { + return + } + // TODO: add support for CTRL+ENTER new line if (!(event.shiftKey)) { event.preventDefault() this.$emit('submit', event) } }, + + /** + * Sets the autocomplete mention candidates based on the matched text + * after the "@". + * + * @param {String} chunk the matched text to look candidate mentions for. + */ + async handleAtEvent(chunk) { + const response = await searchPossibleMentions(this.token, chunk) + const possibleMentions = response.data.ocs.data + + // Wrap mention ids with spaces in quotes. + possibleMentions.forEach(possibleMention => { + if (possibleMention.id.indexOf(' ') !== -1 + || possibleMention.id.indexOf('guest/') === 0) { + possibleMention.id = '"' + possibleMention.id + '"' + } + }) + + this.autoCompleteMentionCandidates = possibleMentions.map(possibleMention => possibleMention.id) + }, }, } </script> diff --git a/src/components/NewMessageForm/NewMessageForm.vue b/src/components/NewMessageForm/NewMessageForm.vue index e29dc0e92..0a6dd36d4 100644 --- a/src/components/NewMessageForm/NewMessageForm.vue +++ b/src/components/NewMessageForm/NewMessageForm.vue @@ -40,6 +40,7 @@ v-bind="messageToBeReplied" /> <AdvancedInput v-model="text" + :token="token" @submit="handleSubmit" /> </div> <button diff --git a/src/main.js b/src/main.js index 7230aebf5..f3756d7fa 100644 --- a/src/main.js +++ b/src/main.js @@ -38,7 +38,6 @@ import { generateFilePath } from '@nextcloud/router' import { getRequestToken } from '@nextcloud/auth' // Directives -import contenteditableDirective from 'vue-contenteditable-directive' import VueClipboard from 'vue-clipboard2' import { translate, translatePlural } from '@nextcloud/l10n' import VueObserveVisibility from 'vue-observe-visibility' @@ -60,7 +59,6 @@ Vue.prototype.n = translatePlural Vue.prototype.OC = OC Vue.prototype.OCA = OCA -Vue.use(contenteditableDirective) Vue.use(Vuex) Vue.use(VueRouter) Vue.use(VueClipboard) diff --git a/src/mainChatTab.js b/src/mainChatTab.js index 955ebcd6d..4ddc04384 100644 --- a/src/mainChatTab.js +++ b/src/mainChatTab.js @@ -34,7 +34,6 @@ import { generateFilePath } from '@nextcloud/router' import { getRequestToken } from '@nextcloud/auth' // Directives -import contenteditableDirective from 'vue-contenteditable-directive' import { translate, translatePlural } from '@nextcloud/l10n' // CSP config for webpack dynamic chunk loading @@ -53,7 +52,6 @@ Vue.prototype.n = translatePlural Vue.prototype.OC = OC Vue.prototype.OCA = OCA -Vue.use(contenteditableDirective) Vue.use(Vuex) const newTab = () => new Vue({ diff --git a/src/services/mentionsService.js b/src/services/mentionsService.js new file mode 100644 index 000000000..0c1b8db5c --- /dev/null +++ b/src/services/mentionsService.js @@ -0,0 +1,42 @@ +/** + * + * @copyright Copyright (c) 2020, Daniel Calviño Sánchez <danxuliu@gmail.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +import axios from '@nextcloud/axios' +import { generateOcsUrl } from '@nextcloud/router' + +/** + * Fetch possible mentions + * + * @param {string} token The token of the conversation. + * @param {string} searchText The string that will be used in the search query. + */ +const searchPossibleMentions = async function(token, searchText) { + try { + const response = await axios.get(generateOcsUrl('apps/spreed/api/v1/chat', 2) + `${token}/mentions?search=${searchText}`) + return response + } catch (error) { + console.debug('Error while searching possible mentions: ', error) + } +} + +export { + searchPossibleMentions, +} |