emoji autocomplete to chat, messages, tasks, and profile
add :emoji shortcode autocomplete dropdown (reusing existing autocomplete mixin w/new helper)
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="searchResults.length > 0"
|
||||
class="autocomplete-selection"
|
||||
:style="autocompleteStyle"
|
||||
>
|
||||
<div
|
||||
v-for="result in searchResults"
|
||||
:key="result.shortcode"
|
||||
class="autocomplete-results d-flex align-items-center"
|
||||
:class="{'hover-background': result.hover}"
|
||||
@click="select(result)"
|
||||
@mouseenter="setHover(result)"
|
||||
@mouseleave="resetSelection()"
|
||||
>
|
||||
<span class="emoji-char">{{ result.emoji }}</span>
|
||||
<span
|
||||
class="shortcode ml-2"
|
||||
:class="{'hover-foreground': result.hover}"
|
||||
>:{{ result.shortcode }}:</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
.autocomplete-results {
|
||||
padding: .5em;
|
||||
}
|
||||
|
||||
.autocomplete-selection {
|
||||
box-shadow: 1px 1px 1px #efefef;
|
||||
}
|
||||
|
||||
.hover-background {
|
||||
background-color: rgba(213, 200, 255, 0.32);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.hover-foreground {
|
||||
color: $purple-300 !important;
|
||||
}
|
||||
|
||||
.emoji-char {
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.shortcode {
|
||||
color: $gray-200;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import emojiDefs from 'markdown-it-emoji/lib/data/full.json';
|
||||
|
||||
export default {
|
||||
props: ['text', 'caretPosition', 'coords', 'textbox'],
|
||||
data () {
|
||||
return {
|
||||
colonRegex: /:([a-zA-Z0-9_+]*)$/,
|
||||
currentSearch: '',
|
||||
searchActive: false,
|
||||
searchResults: [],
|
||||
selected: null,
|
||||
emojiList: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
autocompleteStyle () {
|
||||
function heightToUse (textBox, topCoords) {
|
||||
const textBoxHeight = textBox.clientHeight;
|
||||
return topCoords < textBoxHeight ? topCoords + 30 : textBoxHeight + 10;
|
||||
}
|
||||
return {
|
||||
top: `${heightToUse(this.textbox, this.coords.TOP)}px`,
|
||||
left: `${this.coords.LEFT + 30}px`,
|
||||
marginLeft: '-28px',
|
||||
marginTop: '28px',
|
||||
position: 'absolute',
|
||||
minWidth: '100px',
|
||||
zIndex: 100,
|
||||
backgroundColor: 'white',
|
||||
};
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
text (newText, prevText) {
|
||||
if (!this.textbox) return;
|
||||
const delCharsBool = prevText.length > newText.length;
|
||||
const caretPosition = this.textbox.selectionEnd;
|
||||
const lastFocusChar = delCharsBool ? prevText[caretPosition] : newText[caretPosition - 1];
|
||||
if (
|
||||
newText.length === 0
|
||||
|| (lastFocusChar === ':' && delCharsBool)
|
||||
) {
|
||||
this.cancel();
|
||||
} else {
|
||||
if (lastFocusChar === ':') this.searchActive = true;
|
||||
if (this.searchActive) {
|
||||
this.searchResults = this.solveSearchResults(newText.substring(0, caretPosition));
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
created () {
|
||||
const list = [];
|
||||
const keys = Object.keys(emojiDefs);
|
||||
keys.sort();
|
||||
for (const key of keys) {
|
||||
list.push({ shortcode: key, emoji: emojiDefs[key], hover: false });
|
||||
}
|
||||
this.emojiList = list;
|
||||
},
|
||||
methods: {
|
||||
solveSearchResults (textFocus) {
|
||||
const regexRes = this.colonRegex.exec(textFocus);
|
||||
if (!regexRes) {
|
||||
this.cancel();
|
||||
return [];
|
||||
}
|
||||
this.currentSearch = regexRes[1];
|
||||
|
||||
if (this.currentSearch.length === 0) return [];
|
||||
|
||||
const lowerSearch = this.currentSearch.toLowerCase();
|
||||
return this.emojiList
|
||||
.filter(entry => entry.shortcode.startsWith(lowerSearch))
|
||||
.slice(0, 6)
|
||||
.map(entry => ({ ...entry, hover: false }));
|
||||
},
|
||||
select (result) {
|
||||
const { text } = this;
|
||||
const targetName = `${result.shortcode}: `;
|
||||
const oldCaret = this.caretPosition;
|
||||
const escapedSearch = this.currentSearch.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
let newText = text.substring(0, this.caretPosition)
|
||||
.replace(new RegExp(`${escapedSearch}$`), targetName);
|
||||
const newCaret = newText.length;
|
||||
newText += text.substring(oldCaret, text.length);
|
||||
this.$emit('select', newText, newCaret);
|
||||
|
||||
this.cancel();
|
||||
},
|
||||
setHover (result) {
|
||||
this.resetSelection();
|
||||
result.hover = true;
|
||||
},
|
||||
clearHover () {
|
||||
for (const selection of this.searchResults) {
|
||||
selection.hover = false;
|
||||
}
|
||||
},
|
||||
resetSelection () {
|
||||
this.clearHover();
|
||||
this.selected = null;
|
||||
},
|
||||
selectNext () {
|
||||
if (this.searchResults.length > 0) {
|
||||
this.clearHover();
|
||||
this.selected = this.selected === null
|
||||
? 0
|
||||
: (this.selected + 1) % this.searchResults.length;
|
||||
this.searchResults[this.selected].hover = true;
|
||||
}
|
||||
},
|
||||
selectPrevious () {
|
||||
if (this.searchResults.length > 0) {
|
||||
this.clearHover();
|
||||
this.selected = this.selected === null
|
||||
? this.searchResults.length - 1
|
||||
: (this.selected - 1 + this.searchResults.length) % this.searchResults.length;
|
||||
this.searchResults[this.selected].hover = true;
|
||||
}
|
||||
},
|
||||
makeSelection () {
|
||||
if (this.searchResults.length > 0 && this.selected !== null) {
|
||||
const result = this.searchResults[this.selected];
|
||||
this.select(result);
|
||||
}
|
||||
},
|
||||
cancel () {
|
||||
this.searchActive = false;
|
||||
this.searchResults = [];
|
||||
this.resetSelection();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -41,6 +41,14 @@
|
||||
:chat="group.chat"
|
||||
@select="selectedAutocomplete"
|
||||
/>
|
||||
<emoji-auto-complete
|
||||
ref="emojiAutocomplete"
|
||||
:text="newMessage"
|
||||
:textbox="textbox"
|
||||
:coords="mixinData.autoComplete.coords"
|
||||
:caret-position="mixinData.autoComplete.caretPosition"
|
||||
@select="selectedAutocomplete"
|
||||
/>
|
||||
</div>
|
||||
<community-guidelines />
|
||||
<div class="row chat-actions">
|
||||
@@ -90,6 +98,7 @@ import { MAX_MESSAGE_LENGTH } from '@/../../common/script/constants';
|
||||
import externalLinks from '../../mixins/externalLinks';
|
||||
|
||||
import autocomplete from '../chat/autoComplete';
|
||||
import emojiAutoComplete from '../chat/emojiAutoComplete';
|
||||
import communityGuidelines from './communityGuidelines';
|
||||
import chatMessages from '../chat/chatMessages';
|
||||
import { mapState } from '@/libs/store';
|
||||
@@ -102,6 +111,7 @@ export default {
|
||||
},
|
||||
components: {
|
||||
autocomplete,
|
||||
emojiAutoComplete,
|
||||
communityGuidelines,
|
||||
chatMessages,
|
||||
},
|
||||
|
||||
@@ -70,6 +70,13 @@
|
||||
spellcheck="true"
|
||||
:disabled="challengeAccessRequired"
|
||||
:placeholder="$t('addATitle')"
|
||||
@focus="setActiveField('title')"
|
||||
@keydown="autoCompleteMixinUpdateCarretPosition"
|
||||
@keydown.tab="autoCompleteMixinHandleTab($event)"
|
||||
@keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
|
||||
@keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
|
||||
@keypress.enter="titleEnterHandler($event)"
|
||||
@keydown.esc="autoCompleteMixinHandleEscape($event)"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
@@ -92,11 +99,27 @@
|
||||
</small>
|
||||
</div>
|
||||
<textarea
|
||||
ref="notesTextarea"
|
||||
v-model="task.notes"
|
||||
class="form-control input-notes"
|
||||
:class="cssClass('input')"
|
||||
:placeholder="$t('addNotes')"
|
||||
@focus="setActiveField('notes')"
|
||||
@keydown="autoCompleteMixinUpdateCarretPosition"
|
||||
@keydown.tab="autoCompleteMixinHandleTab($event)"
|
||||
@keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
|
||||
@keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
|
||||
@keypress.enter="autoCompleteMixinSelectAutocomplete($event)"
|
||||
@keydown.esc="autoCompleteMixinHandleEscape($event)"
|
||||
></textarea>
|
||||
<emoji-auto-complete
|
||||
ref="emojiAutocomplete"
|
||||
:text="activeFieldText"
|
||||
:textbox="textbox"
|
||||
:coords="mixinData.autoComplete.coords"
|
||||
:caret-position="mixinData.autoComplete.caretPosition"
|
||||
@select="selectedAutocomplete"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -712,6 +735,7 @@
|
||||
}
|
||||
|
||||
.task-modal-header {
|
||||
position: relative;
|
||||
color: $white;
|
||||
width: 100%;
|
||||
border-top-left-radius: 8px;
|
||||
@@ -1160,6 +1184,8 @@ import lockableLabel from '@/components/tasks/modal-controls/lockableLabel';
|
||||
import selectList from '@/components/ui/selectList';
|
||||
|
||||
import syncTask from '../../mixins/syncTask';
|
||||
import emojiAutoComplete from '@/components/chat/emojiAutoComplete';
|
||||
import { autoCompleteHelperMixin } from '@/mixins/autoCompleteHelper';
|
||||
|
||||
import positiveIcon from '@/assets/svg/positive.svg?raw';
|
||||
import negativeIcon from '@/assets/svg/negative.svg?raw';
|
||||
@@ -1182,15 +1208,18 @@ export default {
|
||||
toggleCheckbox,
|
||||
lockableLabel,
|
||||
selectList,
|
||||
emojiAutoComplete,
|
||||
},
|
||||
directives: {
|
||||
markdown: markdownDirective,
|
||||
},
|
||||
mixins: [syncTask],
|
||||
mixins: [syncTask, autoCompleteHelperMixin],
|
||||
// purpose is either create or edit, task is the task created or edited
|
||||
props: ['task', 'purpose', 'challengeId', 'groupId'],
|
||||
data () {
|
||||
return {
|
||||
textbox: null,
|
||||
activeField: 'title',
|
||||
showAssignedSelect: false,
|
||||
newChecklistItem: null,
|
||||
icons: Object.freeze({
|
||||
@@ -1314,6 +1343,10 @@ export default {
|
||||
selectedTags () {
|
||||
return this.getTagsFor(this.task);
|
||||
},
|
||||
activeFieldText () {
|
||||
if (!this.task) return '';
|
||||
return this.activeField === 'title' ? (this.task.text || '') : (this.task.notes || '');
|
||||
},
|
||||
showStatAssignment () {
|
||||
return this.task.type !== 'reward'
|
||||
&& !this.groupId
|
||||
@@ -1489,6 +1522,35 @@ export default {
|
||||
},
|
||||
focusInput () {
|
||||
this.$refs.inputToFocus.focus();
|
||||
this.setActiveField('title');
|
||||
},
|
||||
setActiveField (field) {
|
||||
this.activeField = field;
|
||||
if (field === 'title') {
|
||||
this.textbox = this.$refs.inputToFocus;
|
||||
} else {
|
||||
this.textbox = this.$refs.notesTextarea;
|
||||
}
|
||||
},
|
||||
titleEnterHandler (e) {
|
||||
const ac = this._getActiveAutocomplete();
|
||||
if (ac && ac.selected !== null) {
|
||||
e.preventDefault();
|
||||
ac.makeSelection();
|
||||
} else if (ac) {
|
||||
ac.cancel();
|
||||
}
|
||||
},
|
||||
selectedAutocomplete (newText, newCaret) {
|
||||
if (this.activeField === 'title') {
|
||||
this.task.text = newText;
|
||||
} else {
|
||||
this.task.notes = newText;
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this.textbox.setSelectionRange(newCaret, newCaret);
|
||||
this.textbox.focus();
|
||||
});
|
||||
},
|
||||
async addTag (name) {
|
||||
const tagResult = await this.createTag({ name });
|
||||
|
||||
@@ -398,14 +398,29 @@
|
||||
:placeholder="$t('imageUrl')"
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-group" style="position: relative;">
|
||||
<label>{{ $t('about') }}</label>
|
||||
<textarea
|
||||
ref="blurbTextarea"
|
||||
v-model="editingProfile.blurb"
|
||||
class="form-control"
|
||||
rows="5"
|
||||
:placeholder="$t('displayBlurbPlaceholder')"
|
||||
@keydown="autoCompleteMixinUpdateCarretPosition"
|
||||
@keydown.tab="autoCompleteMixinHandleTab($event)"
|
||||
@keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
|
||||
@keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
|
||||
@keypress.enter="autoCompleteMixinSelectAutocomplete($event)"
|
||||
@keydown.esc="autoCompleteMixinHandleEscape($event)"
|
||||
></textarea>
|
||||
<emoji-auto-complete
|
||||
ref="emojiAutocomplete"
|
||||
:text="editingProfile.blurb"
|
||||
:textbox="textbox"
|
||||
:coords="mixinData.autoComplete.coords"
|
||||
:caret-position="mixinData.autoComplete.caretPosition"
|
||||
@select="selectedAutocomplete"
|
||||
/>
|
||||
<!-- include ../../shared/formatting-help-->
|
||||
</div>
|
||||
</div>
|
||||
@@ -1001,6 +1016,8 @@ import mute from '@/assets/svg/mute.svg?raw';
|
||||
import shadowMute from '@/assets/svg/shadow-mute.svg?raw';
|
||||
import externalLinks from '../../mixins/externalLinks';
|
||||
import { userCustomStateMixin } from '../../mixins/userState';
|
||||
import emojiAutoComplete from '@/components/chat/emojiAutoComplete';
|
||||
import { autoCompleteHelperMixin } from '@/mixins/autoCompleteHelper';
|
||||
// @TODO: EMAILS.COMMUNITY_MANAGER_EMAIL
|
||||
const COMMUNITY_MANAGER_EMAIL = 'admin@habitica.com';
|
||||
|
||||
@@ -1012,8 +1029,9 @@ export default {
|
||||
MemberDetails,
|
||||
profileStats,
|
||||
toggleSwitch,
|
||||
emojiAutoComplete,
|
||||
},
|
||||
mixins: [externalLinks, userCustomStateMixin('userLoggedIn')],
|
||||
mixins: [externalLinks, userCustomStateMixin('userLoggedIn'), autoCompleteHelperMixin],
|
||||
props: ['userId', 'startingPage'],
|
||||
data () {
|
||||
return {
|
||||
@@ -1033,6 +1051,7 @@ export default {
|
||||
mute,
|
||||
shadowMute,
|
||||
}),
|
||||
textbox: null,
|
||||
userIdToMessage: '',
|
||||
editing: false,
|
||||
editingProfile: {
|
||||
@@ -1121,6 +1140,13 @@ export default {
|
||||
userLoggedIn () {
|
||||
this.loadUser();
|
||||
},
|
||||
editing (val) {
|
||||
if (val) {
|
||||
this.$nextTick(() => {
|
||||
this.textbox = this.$refs.blurbTextarea;
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.loadUser();
|
||||
@@ -1331,6 +1357,13 @@ export default {
|
||||
this.$emit('toggled', this.isOpened);
|
||||
},
|
||||
|
||||
selectedAutocomplete (newText, newCaret) {
|
||||
this.editingProfile.blurb = newText;
|
||||
this.$nextTick(() => {
|
||||
this.textbox.setSelectionRange(newCaret, newCaret);
|
||||
this.textbox.focus();
|
||||
});
|
||||
},
|
||||
reportPlayer () {
|
||||
this.$root.$emit('habitica::report-profile', {
|
||||
memberId: this.user._id,
|
||||
|
||||
@@ -15,46 +15,60 @@ export const autoCompleteHelperMixin = {
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
_getActiveAutocomplete () {
|
||||
if (this.$refs.autocomplete && this.$refs.autocomplete.searchActive) {
|
||||
return this.$refs.autocomplete;
|
||||
}
|
||||
if (this.$refs.emojiAutocomplete && this.$refs.emojiAutocomplete.searchActive) {
|
||||
return this.$refs.emojiAutocomplete;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
autoCompleteMixinHandleTab (e) {
|
||||
if (this.$refs.autocomplete.searchActive) {
|
||||
const ac = this._getActiveAutocomplete();
|
||||
if (ac) {
|
||||
e.preventDefault();
|
||||
if (e.shiftKey) {
|
||||
this.$refs.autocomplete.selectPrevious();
|
||||
ac.selectPrevious();
|
||||
} else {
|
||||
this.$refs.autocomplete.selectNext();
|
||||
ac.selectNext();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
autoCompleteMixinHandleEscape (e) {
|
||||
if (this.$refs.autocomplete.searchActive) {
|
||||
const ac = this._getActiveAutocomplete();
|
||||
if (ac) {
|
||||
e.preventDefault();
|
||||
this.$refs.autocomplete.cancel();
|
||||
ac.cancel();
|
||||
}
|
||||
},
|
||||
|
||||
autoCompleteMixinSelectNextAutocomplete (e) {
|
||||
if (this.$refs.autocomplete.searchActive) {
|
||||
const ac = this._getActiveAutocomplete();
|
||||
if (ac) {
|
||||
e.preventDefault();
|
||||
this.$refs.autocomplete.selectNext();
|
||||
ac.selectNext();
|
||||
}
|
||||
},
|
||||
|
||||
autoCompleteMixinSelectPreviousAutocomplete (e) {
|
||||
if (this.$refs.autocomplete.searchActive) {
|
||||
const ac = this._getActiveAutocomplete();
|
||||
if (ac) {
|
||||
e.preventDefault();
|
||||
this.$refs.autocomplete.selectPrevious();
|
||||
ac.selectPrevious();
|
||||
}
|
||||
},
|
||||
|
||||
autoCompleteMixinSelectAutocomplete (e) {
|
||||
if (this.$refs.autocomplete.searchActive) {
|
||||
if (this.$refs.autocomplete.selected !== null) {
|
||||
const ac = this._getActiveAutocomplete();
|
||||
if (ac) {
|
||||
if (ac.selected !== null) {
|
||||
e.preventDefault();
|
||||
this.$refs.autocomplete.makeSelection();
|
||||
ac.makeSelection();
|
||||
} else {
|
||||
// no autocomplete selected, newline instead
|
||||
this.$refs.autocomplete.cancel();
|
||||
ac.cancel();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -153,9 +153,23 @@
|
||||
:placeholder="$t('needsTextPlaceholder')"
|
||||
:maxlength="MAX_MESSAGE_LENGTH"
|
||||
:class="{'has-content': newMessage.trim() !== '', 'disabled': newMessageDisabled}"
|
||||
@keydown="autoCompleteMixinUpdateCarretPosition"
|
||||
@keyup.ctrl.enter="sendPrivateMessage()"
|
||||
@keydown.tab="autoCompleteMixinHandleTab($event)"
|
||||
@keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
|
||||
@keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
|
||||
@keypress.enter="autoCompleteMixinSelectAutocomplete($event)"
|
||||
@keydown.esc="autoCompleteMixinHandleEscape($event)"
|
||||
>
|
||||
</textarea>
|
||||
<emoji-auto-complete
|
||||
ref="emojiAutocomplete"
|
||||
:text="newMessage"
|
||||
:textbox="textbox"
|
||||
:coords="mixinData.autoComplete.coords"
|
||||
:caret-position="mixinData.autoComplete.caretPosition"
|
||||
@select="selectedAutocomplete"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="sub-new-message-row d-flex"
|
||||
@@ -540,6 +554,7 @@ h3 {
|
||||
}
|
||||
|
||||
.new-message-row {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-left: 1.5rem;
|
||||
padding-top: 1.5rem;
|
||||
@@ -676,6 +691,8 @@ import PmNewMessageStarted from './pm-new-message-started.vue';
|
||||
import StartNewConversationInputHeader from './start-new-conversation-input-header.vue';
|
||||
import positiveIcon from '@/assets/svg/positive.svg?raw';
|
||||
import NotificationMixins from '@/mixins/notifications';
|
||||
import emojiAutoComplete from '@/components/chat/emojiAutoComplete';
|
||||
import { autoCompleteHelperMixin } from '@/mixins/autoCompleteHelper';
|
||||
|
||||
// extract to a shared path
|
||||
const CONVERSATIONS_PER_PAGE = 10;
|
||||
@@ -700,13 +717,14 @@ export default defineComponent({
|
||||
toggleSwitch,
|
||||
userLink,
|
||||
faceAvatar,
|
||||
emojiAutoComplete,
|
||||
},
|
||||
filters: {
|
||||
timeAgo (value) {
|
||||
return moment(new Date(value)).fromNow();
|
||||
},
|
||||
},
|
||||
mixins: [styleHelper, NotificationMixins],
|
||||
mixins: [styleHelper, NotificationMixins, autoCompleteHelperMixin],
|
||||
beforeRouteEnter (to, from, next) {
|
||||
next(vm => {
|
||||
const data = vm.$store.state.privateMessageOptions;
|
||||
@@ -751,6 +769,7 @@ export default defineComponent({
|
||||
/** @type {Record<string, PrivateMessages.PrivateMessageEntry[]>} */
|
||||
messagesByConversation: {}, // cache {uuid: []}
|
||||
|
||||
textbox: null,
|
||||
newMessage: '',
|
||||
messages: [],
|
||||
messagesLoading: false,
|
||||
@@ -964,6 +983,7 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
async mounted () {
|
||||
this.textbox = this.$refs.textarea;
|
||||
this.$store.dispatch('common:setTitle', {
|
||||
section: this.$t('messages'),
|
||||
});
|
||||
@@ -1224,6 +1244,13 @@ export default defineComponent({
|
||||
triggerStartNewConversationState () {
|
||||
this.showStartNewConversationInput = true;
|
||||
},
|
||||
selectedAutocomplete (newText, newCaret) {
|
||||
this.newMessage = newText;
|
||||
this.$nextTick(() => {
|
||||
this.textbox.setSelectionRange(newCaret, newCaret);
|
||||
this.textbox.focus();
|
||||
});
|
||||
},
|
||||
async startConversationByUsername (targetUserName) {
|
||||
// check if the target user exists in current conversations, select that conversation
|
||||
/** @type {PrivateMessages.ConversationSummaryMessageEntry} */
|
||||
|
||||
Reference in New Issue
Block a user