parent
67e41e4fb0
commit
07fb5e867c
62
routes/_actions/autosuggest.js
Normal file
62
routes/_actions/autosuggest.js
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
import { store } from '../_store/store'
|
||||||
|
|
||||||
|
export async function insertUsername (realm, username, startIndex, endIndex) {
|
||||||
|
let { currentInstance } = store.get()
|
||||||
|
let oldText = store.getComposeData(realm, 'text')
|
||||||
|
let pre = oldText.substring(0, startIndex)
|
||||||
|
let post = oldText.substring(endIndex)
|
||||||
|
let newText = `${pre}@${username} ${post}`
|
||||||
|
store.setComposeData(realm, {text: newText})
|
||||||
|
store.setForAutosuggest(currentInstance, realm, {autosuggestSearchResults: []})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function clickSelectedAutosuggestionUsername (realm) {
|
||||||
|
let {
|
||||||
|
composeSelectionStart,
|
||||||
|
autosuggestSearchText,
|
||||||
|
autosuggestSelected,
|
||||||
|
autosuggestSearchResults
|
||||||
|
} = store.get()
|
||||||
|
let account = autosuggestSearchResults[autosuggestSelected]
|
||||||
|
let startIndex = composeSelectionStart - autosuggestSearchText.length
|
||||||
|
let endIndex = composeSelectionStart
|
||||||
|
await insertUsername(realm, account.acct, startIndex, endIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function insertEmojiAtPosition (realm, emoji, startIndex, endIndex) {
|
||||||
|
let { currentInstance } = store.get()
|
||||||
|
let oldText = store.getComposeData(realm, 'text') || ''
|
||||||
|
let pre = oldText.substring(0, startIndex)
|
||||||
|
let post = oldText.substring(endIndex)
|
||||||
|
let newText = `${pre}:${emoji.shortcode}: ${post}`
|
||||||
|
store.setComposeData(realm, {text: newText})
|
||||||
|
store.setForAutosuggest(currentInstance, realm, {autosuggestSearchResults: []})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function clickSelectedAutosuggestionEmoji (realm) {
|
||||||
|
let {
|
||||||
|
composeSelectionStart,
|
||||||
|
autosuggestSearchText,
|
||||||
|
autosuggestSelected,
|
||||||
|
autosuggestSearchResults
|
||||||
|
} = store.get()
|
||||||
|
let emoji = autosuggestSearchResults[autosuggestSelected]
|
||||||
|
let startIndex = composeSelectionStart - autosuggestSearchText.length
|
||||||
|
let endIndex = composeSelectionStart
|
||||||
|
await insertEmojiAtPosition(realm, emoji, startIndex, endIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function selectAutosuggestItem (item) {
|
||||||
|
let {
|
||||||
|
currentComposeRealm,
|
||||||
|
composeSelectionStart,
|
||||||
|
autosuggestSearchText
|
||||||
|
} = store.get()
|
||||||
|
let startIndex = composeSelectionStart - autosuggestSearchText.length
|
||||||
|
let endIndex = composeSelectionStart
|
||||||
|
if (item.acct) {
|
||||||
|
/* no await */ insertUsername(currentComposeRealm, item.acct, startIndex, endIndex)
|
||||||
|
} else {
|
||||||
|
/* no await */ insertEmojiAtPosition(currentComposeRealm, item, startIndex, endIndex)
|
||||||
|
}
|
||||||
|
}
|
|
@ -50,28 +50,6 @@ export async function postStatus (realm, text, inReplyToId, mediaIds,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function insertUsername (realm, username, startIndex, endIndex) {
|
|
||||||
let oldText = store.getComposeData(realm, 'text')
|
|
||||||
let pre = oldText.substring(0, startIndex)
|
|
||||||
let post = oldText.substring(endIndex)
|
|
||||||
let newText = `${pre}@${username} ${post}`
|
|
||||||
store.setComposeData(realm, {text: newText})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function clickSelectedAutosuggestionUsername (realm) {
|
|
||||||
let {
|
|
||||||
composeSelectionStart,
|
|
||||||
composeAutosuggestionSearchText,
|
|
||||||
composeAutosuggestionSelected,
|
|
||||||
composeAutosuggestionSearchResults
|
|
||||||
} = store.get()
|
|
||||||
composeAutosuggestionSelected = composeAutosuggestionSelected || 0
|
|
||||||
let account = composeAutosuggestionSearchResults[composeAutosuggestionSelected]
|
|
||||||
let startIndex = composeSelectionStart - composeAutosuggestionSearchText.length
|
|
||||||
let endIndex = composeSelectionStart
|
|
||||||
await insertUsername(realm, account.acct, startIndex, endIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setReplySpoiler (realm, spoiler) {
|
export function setReplySpoiler (realm, spoiler) {
|
||||||
let contentWarning = store.getComposeData(realm, 'contentWarning')
|
let contentWarning = store.getComposeData(realm, 'contentWarning')
|
||||||
let contentWarningShown = store.getComposeData(realm, 'contentWarningShown')
|
let contentWarningShown = store.getComposeData(realm, 'contentWarningShown')
|
||||||
|
|
|
@ -28,25 +28,3 @@ export function insertEmoji (realm, emoji) {
|
||||||
let newText = `${pre}:${emoji.shortcode}: ${post}`
|
let newText = `${pre}:${emoji.shortcode}: ${post}`
|
||||||
store.setComposeData(realm, {text: newText})
|
store.setComposeData(realm, {text: newText})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function insertEmojiAtPosition (realm, emoji, startIndex, endIndex) {
|
|
||||||
let oldText = store.getComposeData(realm, 'text') || ''
|
|
||||||
let pre = oldText.substring(0, startIndex)
|
|
||||||
let post = oldText.substring(endIndex)
|
|
||||||
let newText = `${pre}:${emoji.shortcode}: ${post}`
|
|
||||||
store.setComposeData(realm, {text: newText})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function clickSelectedAutosuggestionEmoji (realm) {
|
|
||||||
let {
|
|
||||||
composeSelectionStart,
|
|
||||||
composeAutosuggestionSearchText,
|
|
||||||
composeAutosuggestionSelected,
|
|
||||||
composeAutosuggestionSearchResults
|
|
||||||
} = store.get()
|
|
||||||
composeAutosuggestionSelected = composeAutosuggestionSelected || 0
|
|
||||||
let emoji = composeAutosuggestionSearchResults[composeAutosuggestionSelected]
|
|
||||||
let startIndex = composeSelectionStart - composeAutosuggestionSearchText.length
|
|
||||||
let endIndex = composeSelectionStart
|
|
||||||
await insertEmojiAtPosition(realm, emoji, startIndex, endIndex)
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
<div class="compose-autosuggest {shown ? 'shown' : ''} {realm === 'dialog' ? 'is-dialog' : ''}"
|
<div class="compose-autosuggest {shown ? 'shown' : ''} {realm === 'dialog' ? 'is-dialog' : ''}"
|
||||||
aria-hidden="true" >
|
aria-hidden="true" >
|
||||||
<ComposeAutosuggestionList
|
<ComposeAutosuggestionList
|
||||||
items={searchResults}
|
items={autosuggestSearchResults}
|
||||||
on:click="onClick(event)"
|
on:click="onClick(event)"
|
||||||
{type}
|
type={autosuggestType}
|
||||||
{selected}
|
selected={autosuggestSelected}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<style>
|
<style>
|
||||||
|
@ -39,134 +39,65 @@
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
import { store } from '../../_store/store'
|
import { store } from '../../_store/store'
|
||||||
import { insertUsername } from '../../_actions/compose'
|
|
||||||
import { insertEmojiAtPosition } from '../../_actions/emoji'
|
|
||||||
import { scheduleIdleTask } from '../../_utils/scheduleIdleTask'
|
|
||||||
import { once } from '../../_utils/once'
|
|
||||||
import ComposeAutosuggestionList from './ComposeAutosuggestionList.html'
|
import ComposeAutosuggestionList from './ComposeAutosuggestionList.html'
|
||||||
import {
|
import get from 'lodash-es/get'
|
||||||
searchAccountsByUsername as searchAccountsByUsernameInDatabase
|
import { selectAutosuggestItem } from '../../_actions/autosuggest'
|
||||||
} from '../../_database/accountsAndRelationships'
|
|
||||||
import { observe } from 'svelte-extras'
|
import { observe } from 'svelte-extras'
|
||||||
|
import { once } from '../../_utils/once'
|
||||||
const SEARCH_RESULTS_LIMIT = 4
|
|
||||||
const DATABASE_SEARCH_RESULTS_LIMIT = 30
|
|
||||||
const MIN_PREFIX_LENGTH = 1
|
|
||||||
const ACCOUNT_SEARCH_REGEX = new RegExp(`(?:\\s|^)(@\\S{${MIN_PREFIX_LENGTH},})$`)
|
|
||||||
const EMOJI_SEARCH_REGEX = new RegExp(`(?:\\s|^)(:[^:]{${MIN_PREFIX_LENGTH},})$`)
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
oncreate () {
|
oncreate () {
|
||||||
// perf improves for input responsiveness
|
this._promiseChain = Promise.resolve()
|
||||||
this.observe('composeSelectionStart', () => {
|
this.observe('shouldBeShown', (shouldBeShown) => {
|
||||||
scheduleIdleTask(() => {
|
|
||||||
let { composeSelectionStart } = this.get()
|
|
||||||
this.set({composeSelectionStartDeferred: composeSelectionStart})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
this.observe('composeFocused', (composeFocused) => {
|
|
||||||
let updateFocusedState = () => {
|
|
||||||
scheduleIdleTask(() => {
|
|
||||||
let { composeFocused } = this.get()
|
|
||||||
this.set({composeFocusedDeferred: composeFocused})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: hack so that when the user clicks the button, and the textarea blurs,
|
// TODO: hack so that when the user clicks the button, and the textarea blurs,
|
||||||
// we don't immediately hide the dropdown which would cause the click to get lost
|
// we don't immediately hide the dropdown which would cause the click to get lost
|
||||||
if (composeFocused) {
|
this._promiseChain = this._promiseChain.then(() => {
|
||||||
updateFocusedState()
|
if (!shouldBeShown) {
|
||||||
} else {
|
return Promise.race([
|
||||||
Promise.race([
|
new Promise(resolve => setTimeout(resolve, 200)),
|
||||||
new Promise(resolve => setTimeout(resolve, 200)),
|
new Promise(resolve => this.once('autosuggestItemSelected', resolve))
|
||||||
new Promise(resolve => this.once('autosuggestItemSelected', resolve))
|
])
|
||||||
]).then(updateFocusedState)
|
}
|
||||||
}
|
}).then(() => {
|
||||||
})
|
this.set({shown: shouldBeShown})
|
||||||
this.observe('searchText', async searchText => {
|
|
||||||
let { thisComposeFocused } = this.get()
|
|
||||||
if (!thisComposeFocused || !searchText) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let type = searchText.startsWith('@') ? 'account' : 'emoji'
|
|
||||||
let results = (type === 'account')
|
|
||||||
? await this.searchAccounts(searchText)
|
|
||||||
: await this.searchEmoji(searchText)
|
|
||||||
this.store.set({
|
|
||||||
composeAutosuggestionSelected: 0,
|
|
||||||
composeAutosuggestionSearchText: searchText,
|
|
||||||
composeAutosuggestionSearchResults: results,
|
|
||||||
composeAutosuggestionType: type
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
this.observe('shown', shown => {
|
|
||||||
let { thisComposeFocused } = this.get()
|
|
||||||
if (!thisComposeFocused) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.store.set({composeAutosuggestionShown: shown})
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
observe,
|
observe,
|
||||||
once,
|
once,
|
||||||
onClick (item) {
|
onClick (item) {
|
||||||
this.fire('autosuggestItemSelected')
|
this.fire('autosuggestItemSelected')
|
||||||
let { realm } = this.get()
|
selectAutosuggestItem(item)
|
||||||
let { composeSelectionStart, composeAutosuggestionSearchText } = this.store.get()
|
|
||||||
let startIndex = composeSelectionStart - composeAutosuggestionSearchText.length
|
|
||||||
let endIndex = composeSelectionStart
|
|
||||||
if (item.acct) {
|
|
||||||
/* no await */ insertUsername(realm, item.acct, startIndex, endIndex)
|
|
||||||
} else {
|
|
||||||
/* no await */ insertEmojiAtPosition(realm, item, startIndex, endIndex)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async searchAccounts (searchText) {
|
|
||||||
searchText = searchText.substring(1)
|
|
||||||
let { currentInstance } = this.store.get()
|
|
||||||
let results = await searchAccountsByUsernameInDatabase(
|
|
||||||
currentInstance, searchText, DATABASE_SEARCH_RESULTS_LIMIT)
|
|
||||||
return results.slice(0, SEARCH_RESULTS_LIMIT)
|
|
||||||
},
|
|
||||||
searchEmoji (searchText) {
|
|
||||||
searchText = searchText.toLowerCase().substring(1)
|
|
||||||
let { currentCustomEmoji } = this.store.get()
|
|
||||||
let results = currentCustomEmoji.filter(emoji => emoji.shortcode.toLowerCase().startsWith(searchText))
|
|
||||||
.sort((a, b) => a.shortcode.toLowerCase() < b.shortcode.toLowerCase() ? -1 : 1)
|
|
||||||
.slice(0, SEARCH_RESULTS_LIMIT)
|
|
||||||
return results
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
composeSelectionStart: ({ $composeSelectionStart }) => $composeSelectionStart,
|
/* eslint-disable camelcase */
|
||||||
composeFocused: ({ $composeFocused }) => $composeFocused,
|
composeSelectionStart: ({ $autosuggestData_composeSelectionStart, $currentInstance, realm }) => (
|
||||||
thisComposeFocused: ({ composeFocusedDeferred, realm }) => composeFocusedDeferred === realm,
|
get($autosuggestData_composeSelectionStart, [$currentInstance, realm], 0)
|
||||||
searchResults: ({ $composeAutosuggestionSearchResults }) => $composeAutosuggestionSearchResults || [],
|
),
|
||||||
type: ({ $composeAutosuggestionType }) => $composeAutosuggestionType || 'account',
|
composeFocused: ({ $autosuggestData_composeFocused, $currentInstance, realm }) => (
|
||||||
selected: ({ $composeAutosuggestionSelected }) => $composeAutosuggestionSelected || 0,
|
get($autosuggestData_composeFocused, [$currentInstance, realm], false)
|
||||||
searchText: ({ text, composeSelectionStartDeferred, thisComposeFocused }) => {
|
),
|
||||||
if (!thisComposeFocused) {
|
autosuggestSearchResults: ({ $autosuggestData_autosuggestSearchResults, $currentInstance, realm }) => (
|
||||||
return
|
get($autosuggestData_autosuggestSearchResults, [$currentInstance, realm], [])
|
||||||
}
|
),
|
||||||
let selectionStart = composeSelectionStartDeferred
|
autosuggestType: ({ $autosuggestData_autosuggestType, $currentInstance, realm }) => (
|
||||||
if (!text || selectionStart < MIN_PREFIX_LENGTH) {
|
get($autosuggestData_autosuggestType, [$currentInstance, realm])
|
||||||
return
|
),
|
||||||
}
|
autosuggestSelected: ({ $autosuggestData_autosuggestSelected, $currentInstance, realm }) => (
|
||||||
|
get($autosuggestData_autosuggestSelected, [$currentInstance, realm], 0)
|
||||||
let textUpToCursor = text.substring(0, selectionStart)
|
),
|
||||||
let match = textUpToCursor.match(ACCOUNT_SEARCH_REGEX) || textUpToCursor.match(EMOJI_SEARCH_REGEX)
|
autosuggestSearchText: ({ $autosuggestData_autosuggestSelected, $currentInstance, realm }) => (
|
||||||
return match && match[1]
|
get($autosuggestData_autosuggestSelected, [$currentInstance, realm])
|
||||||
},
|
),
|
||||||
shown: ({ thisComposeFocused, searchText, searchResults }) => {
|
/* eslint-enable camelcase */
|
||||||
return !!(thisComposeFocused &&
|
shouldBeShown: ({ realm, $autosuggestShown, composeFocused }) => (
|
||||||
searchText &&
|
!!($autosuggestShown && composeFocused)
|
||||||
searchResults.length)
|
)
|
||||||
}
|
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
composeFocusedDeferred: void 0,
|
shown: false
|
||||||
composeSelectionStartDeferred: 0
|
|
||||||
}),
|
}),
|
||||||
store: () => store,
|
store: () => store,
|
||||||
components: {
|
components: {
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<li class="compose-autosuggest-list-item">
|
<li class="compose-autosuggest-list-item">
|
||||||
<button class="compose-autosuggest-list-button {i === selected ? 'selected' : ''}"
|
<button class="compose-autosuggest-list-button {i === selected ? 'selected' : ''}"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
on:click="fire('click', item)">
|
on:click="onClick(event, item)">
|
||||||
<div class="compose-autosuggest-list-grid">
|
<div class="compose-autosuggest-list-grid">
|
||||||
{#if type === 'account'}
|
{#if type === 'account'}
|
||||||
<Avatar
|
<Avatar
|
||||||
|
@ -102,6 +102,13 @@
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
store: () => store,
|
store: () => store,
|
||||||
|
methods: {
|
||||||
|
onClick (event, item) {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
this.fire('click', item)
|
||||||
|
}
|
||||||
|
},
|
||||||
components: {
|
components: {
|
||||||
Avatar
|
Avatar
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,8 +33,10 @@
|
||||||
import debounce from 'lodash-es/debounce'
|
import debounce from 'lodash-es/debounce'
|
||||||
import { mark, stop } from '../../_utils/marks'
|
import { mark, stop } from '../../_utils/marks'
|
||||||
import { selectionChange } from '../../_utils/events'
|
import { selectionChange } from '../../_utils/events'
|
||||||
import { clickSelectedAutosuggestionUsername } from '../../_actions/compose'
|
import {
|
||||||
import { clickSelectedAutosuggestionEmoji } from '../../_actions/emoji'
|
clickSelectedAutosuggestionUsername,
|
||||||
|
clickSelectedAutosuggestionEmoji
|
||||||
|
} from '../../_actions/autosuggest'
|
||||||
import { observe } from 'svelte-extras'
|
import { observe } from 'svelte-extras'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -95,14 +97,21 @@
|
||||||
stop('autosize.destroy()')
|
stop('autosize.destroy()')
|
||||||
},
|
},
|
||||||
onBlur () {
|
onBlur () {
|
||||||
this.store.set({composeFocused: null})
|
scheduleIdleTask(() => {
|
||||||
|
this.store.setForCurrentAutosuggest({composeFocused: false})
|
||||||
|
})
|
||||||
},
|
},
|
||||||
onFocus () {
|
onFocus () {
|
||||||
let { realm } = this.get()
|
scheduleIdleTask(() => {
|
||||||
this.store.set({composeFocused: realm})
|
let {realm} = this.get()
|
||||||
|
this.store.set({currentComposeRealm: realm})
|
||||||
|
this.store.setForCurrentAutosuggest({composeFocused: true})
|
||||||
|
})
|
||||||
},
|
},
|
||||||
onSelectionChange (selectionStart) {
|
onSelectionChange (selectionStart) {
|
||||||
this.store.set({composeSelectionStart: selectionStart})
|
scheduleIdleTask(() => {
|
||||||
|
this.store.setForCurrentAutosuggest({composeSelectionStart: selectionStart})
|
||||||
|
})
|
||||||
},
|
},
|
||||||
onKeydown (e) {
|
onKeydown (e) {
|
||||||
let { keyCode } = e
|
let { keyCode } = e
|
||||||
|
@ -132,14 +141,14 @@
|
||||||
},
|
},
|
||||||
clickSelectedAutosuggestion (event) {
|
clickSelectedAutosuggestion (event) {
|
||||||
let {
|
let {
|
||||||
composeAutosuggestionShown,
|
autosuggestShown,
|
||||||
composeAutosuggestionType
|
autosuggestType
|
||||||
} = this.store.get()
|
} = this.store.get()
|
||||||
if (!composeAutosuggestionShown) {
|
if (!autosuggestShown) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
let { realm } = this.get()
|
let { realm } = this.get()
|
||||||
if (composeAutosuggestionType === 'account') {
|
if (autosuggestType === 'account') {
|
||||||
/* no await */ clickSelectedAutosuggestionUsername(realm)
|
/* no await */ clickSelectedAutosuggestionUsername(realm)
|
||||||
} else { // emoji
|
} else { // emoji
|
||||||
/* no await */ clickSelectedAutosuggestionEmoji(realm)
|
/* no await */ clickSelectedAutosuggestionEmoji(realm)
|
||||||
|
@ -150,33 +159,31 @@
|
||||||
},
|
},
|
||||||
incrementAutosuggestSelected (increment, event) {
|
incrementAutosuggestSelected (increment, event) {
|
||||||
let {
|
let {
|
||||||
composeAutosuggestionShown,
|
autosuggestShown,
|
||||||
composeAutosuggestionSelected,
|
autosuggestSelected,
|
||||||
composeAutosuggestionSearchResults
|
autosuggestSearchResults
|
||||||
} = this.store.get()
|
} = this.store.get()
|
||||||
if (!composeAutosuggestionShown) {
|
if (!autosuggestShown) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let selected = composeAutosuggestionSelected || 0
|
autosuggestSelected += increment
|
||||||
let searchResults = composeAutosuggestionSearchResults || []
|
if (autosuggestSelected >= 0) {
|
||||||
selected += increment
|
autosuggestSelected = autosuggestSelected % autosuggestSearchResults.length
|
||||||
if (selected >= 0) {
|
|
||||||
selected = selected % searchResults.length
|
|
||||||
} else {
|
} else {
|
||||||
selected = searchResults.length + selected
|
autosuggestSelected = autosuggestSearchResults.length + autosuggestSelected
|
||||||
}
|
}
|
||||||
this.store.set({composeAutosuggestionSelected: selected})
|
this.store.setForCurrentAutosuggest({autosuggestSelected})
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
},
|
},
|
||||||
clearAutosuggestions (event) {
|
clearAutosuggestions (event) {
|
||||||
let { composeAutosuggestionShown } = this.store.get()
|
let { autosuggestShown } = this.store.get()
|
||||||
if (!composeAutosuggestionShown) {
|
if (!autosuggestShown) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.store.set({
|
this.store.setForCurrentAutosuggest({
|
||||||
composeAutosuggestionSearchResults: [],
|
autosuggestSearchResults: [],
|
||||||
composeAutosuggestionSelected: 0
|
autosuggestSelected: 0
|
||||||
})
|
})
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
|
|
56
routes/_store/computations/autosuggestComputations.js
Normal file
56
routes/_store/computations/autosuggestComputations.js
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
const MIN_PREFIX_LENGTH = 1
|
||||||
|
const ACCOUNT_SEARCH_REGEX = new RegExp(`(?:\\s|^)(@\\S{${MIN_PREFIX_LENGTH},})$`)
|
||||||
|
const EMOJI_SEARCH_REGEX = new RegExp(`(?:\\s|^)(:[^:]{${MIN_PREFIX_LENGTH},})$`)
|
||||||
|
|
||||||
|
function computeForAutosuggest (store, key, defaultValue) {
|
||||||
|
store.compute(key,
|
||||||
|
['currentInstance', 'currentComposeRealm', `autosuggestData_${key}`],
|
||||||
|
(currentInstance, currentComposeRealm, root) => {
|
||||||
|
let instanceData = root && root[currentInstance]
|
||||||
|
return (currentComposeRealm && instanceData && currentComposeRealm in instanceData) ? instanceData[currentComposeRealm] : defaultValue
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function autosuggestComputations (store) {
|
||||||
|
computeForAutosuggest(store, 'composeFocused', false)
|
||||||
|
computeForAutosuggest(store, 'composeSelectionStart', 0)
|
||||||
|
computeForAutosuggest(store, 'autosuggestSelected', 0)
|
||||||
|
computeForAutosuggest(store, 'autosuggestSearchResults', [])
|
||||||
|
computeForAutosuggest(store, 'autosuggestType', null)
|
||||||
|
|
||||||
|
store.compute(
|
||||||
|
'currentComposeText',
|
||||||
|
['currentComposeData', 'currentComposeRealm'],
|
||||||
|
(currentComposeData, currentComposeRealm) => (
|
||||||
|
currentComposeData[currentComposeRealm] && currentComposeData[currentComposeRealm].text) || ''
|
||||||
|
)
|
||||||
|
|
||||||
|
store.compute(
|
||||||
|
'autosuggestSearchText',
|
||||||
|
['currentComposeText', 'composeSelectionStart'],
|
||||||
|
(currentComposeText, composeSelectionStart) => {
|
||||||
|
let selectionStart = composeSelectionStart
|
||||||
|
if (!currentComposeText || selectionStart < MIN_PREFIX_LENGTH) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
let textUpToCursor = currentComposeText.substring(0, selectionStart)
|
||||||
|
let match = textUpToCursor.match(ACCOUNT_SEARCH_REGEX) || textUpToCursor.match(EMOJI_SEARCH_REGEX)
|
||||||
|
return (match && match[1]) || ''
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
store.compute(
|
||||||
|
'autosuggestNumSearchResults',
|
||||||
|
['autosuggestSearchResults'],
|
||||||
|
(autosuggestSearchResults) => autosuggestSearchResults.length
|
||||||
|
)
|
||||||
|
|
||||||
|
store.compute(
|
||||||
|
'autosuggestShown',
|
||||||
|
['composeFocused', 'autosuggestSearchText', 'autosuggestNumSearchResults'],
|
||||||
|
(composeFocused, autosuggestSearchText, autosuggestNumSearchResults) => (
|
||||||
|
!!(composeFocused && autosuggestSearchText && autosuggestNumSearchResults)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,9 +1,11 @@
|
||||||
import { instanceComputations } from './instanceComputations'
|
import { instanceComputations } from './instanceComputations'
|
||||||
import { timelineComputations } from './timelineComputations'
|
import { timelineComputations } from './timelineComputations'
|
||||||
import { navComputations } from './navComputations'
|
import { navComputations } from './navComputations'
|
||||||
|
import { autosuggestComputations } from './autosuggestComputations'
|
||||||
|
|
||||||
export function computations (store) {
|
export function computations (store) {
|
||||||
instanceComputations(store)
|
instanceComputations(store)
|
||||||
timelineComputations(store)
|
timelineComputations(store)
|
||||||
navComputations(store)
|
navComputations(store)
|
||||||
|
autosuggestComputations(store)
|
||||||
}
|
}
|
||||||
|
|
19
routes/_store/mixins/autosuggestMixins.js
Normal file
19
routes/_store/mixins/autosuggestMixins.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
export function autosuggestMixins (Store) {
|
||||||
|
Store.prototype.setForAutosuggest = function (instanceName, realm, obj) {
|
||||||
|
let valuesToSet = {}
|
||||||
|
for (let key of Object.keys(obj)) {
|
||||||
|
let rootKey = `autosuggestData_${key}`
|
||||||
|
let root = this.get()[rootKey] || {}
|
||||||
|
let instanceData = root[instanceName] = root[instanceName] || {}
|
||||||
|
instanceData[realm] = obj[key]
|
||||||
|
valuesToSet[rootKey] = root
|
||||||
|
}
|
||||||
|
|
||||||
|
this.set(valuesToSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
Store.prototype.setForCurrentAutosuggest = function (obj) {
|
||||||
|
let { currentInstance, currentComposeRealm } = this.get()
|
||||||
|
this.setForAutosuggest(currentInstance, currentComposeRealm, obj)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,11 @@
|
||||||
import { timelineMixins } from './timelineMixins'
|
import { timelineMixins } from './timelineMixins'
|
||||||
import { instanceMixins } from './instanceMixins'
|
import { instanceMixins } from './instanceMixins'
|
||||||
import { statusMixins } from './statusMixins'
|
import { statusMixins } from './statusMixins'
|
||||||
|
import { autosuggestMixins } from './autosuggestMixins'
|
||||||
|
|
||||||
export function mixins (Store) {
|
export function mixins (Store) {
|
||||||
instanceMixins(Store)
|
instanceMixins(Store)
|
||||||
timelineMixins(Store)
|
timelineMixins(Store)
|
||||||
statusMixins(Store)
|
statusMixins(Store)
|
||||||
|
autosuggestMixins(Store)
|
||||||
}
|
}
|
||||||
|
|
42
routes/_store/observers/autosuggestObservers.js
Normal file
42
routes/_store/observers/autosuggestObservers.js
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import {
|
||||||
|
searchAccountsByUsername as searchAccountsByUsernameInDatabase
|
||||||
|
} from '../../_database/accountsAndRelationships'
|
||||||
|
|
||||||
|
const SEARCH_RESULTS_LIMIT = 4
|
||||||
|
const DATABASE_SEARCH_RESULTS_LIMIT = 30
|
||||||
|
|
||||||
|
async function searchAccounts (store, searchText) {
|
||||||
|
searchText = searchText.substring(1)
|
||||||
|
let { currentInstance } = store.get()
|
||||||
|
let results = await searchAccountsByUsernameInDatabase(
|
||||||
|
currentInstance, searchText, DATABASE_SEARCH_RESULTS_LIMIT)
|
||||||
|
return results.slice(0, SEARCH_RESULTS_LIMIT)
|
||||||
|
}
|
||||||
|
|
||||||
|
function searchEmoji (store, searchText) {
|
||||||
|
searchText = searchText.toLowerCase().substring(1)
|
||||||
|
let { currentCustomEmoji } = store.get()
|
||||||
|
let results = currentCustomEmoji.filter(emoji => emoji.shortcode.toLowerCase().startsWith(searchText))
|
||||||
|
.sort((a, b) => a.shortcode.toLowerCase() < b.shortcode.toLowerCase() ? -1 : 1)
|
||||||
|
.slice(0, SEARCH_RESULTS_LIMIT)
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
export function autosuggestObservers (store) {
|
||||||
|
store.observe('autosuggestSearchText', async autosuggestSearchText => {
|
||||||
|
let { composeFocused } = store.get()
|
||||||
|
if (!composeFocused || !autosuggestSearchText) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let type = autosuggestSearchText.startsWith('@') ? 'account' : 'emoji'
|
||||||
|
let results = (type === 'account')
|
||||||
|
? await searchAccounts(store, autosuggestSearchText)
|
||||||
|
: await searchEmoji(store, autosuggestSearchText)
|
||||||
|
store.setForCurrentAutosuggest({
|
||||||
|
autosuggestSelected: 0,
|
||||||
|
autosuggestSearchText: autosuggestSearchText,
|
||||||
|
autosuggestSearchResults: results,
|
||||||
|
autosuggestType: type
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ import { timelineObservers } from './timelineObservers'
|
||||||
import { notificationObservers } from './notificationObservers'
|
import { notificationObservers } from './notificationObservers'
|
||||||
import { onlineObservers } from './onlineObservers'
|
import { onlineObservers } from './onlineObservers'
|
||||||
import { navObservers } from './navObservers'
|
import { navObservers } from './navObservers'
|
||||||
|
import { autosuggestObservers } from './autosuggestObservers'
|
||||||
|
|
||||||
export function observers (store) {
|
export function observers (store) {
|
||||||
instanceObservers(store)
|
instanceObservers(store)
|
||||||
|
@ -10,4 +11,5 @@ export function observers (store) {
|
||||||
notificationObservers(store)
|
notificationObservers(store)
|
||||||
onlineObservers(store)
|
onlineObservers(store)
|
||||||
navObservers(store)
|
navObservers(store)
|
||||||
|
autosuggestObservers(store)
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,7 +76,9 @@ module.exports = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new LodashModuleReplacementPlugin()
|
new LodashModuleReplacementPlugin({
|
||||||
|
paths: true
|
||||||
|
})
|
||||||
].concat(isDev ? [
|
].concat(isDev ? [
|
||||||
new webpack.HotModuleReplacementPlugin({
|
new webpack.HotModuleReplacementPlugin({
|
||||||
requestTimeout: 120000
|
requestTimeout: 120000
|
||||||
|
|
Loading…
Reference in a new issue