1
0
mirror of https://github.com/h3poteto/whalebird-desktop synced 2025-02-09 08:18:44 +01:00

refs #985 Search hashtag from cache and API

This commit is contained in:
AkiraFukushima 2019-08-07 00:06:09 +09:00
parent e5778500fe
commit b337526e5a
2 changed files with 175 additions and 113 deletions

View File

@ -1,55 +1,52 @@
<template> <template>
<div class="status"> <div class="status">
<textarea <textarea
v-model="status" v-model="status"
ref="status" ref="status"
v-shortkey="openSuggest ? {up: ['arrowup'], down: ['arrowdown'], enter: ['enter'], esc: ['esc']} : {linux: ['ctrl', 'enter'], mac: ['meta', 'enter'], left: ['arrowleft'], right: ['arrowright']}" v-shortkey="
@shortkey="handleKey" openSuggest
@paste="onPaste" ? { up: ['arrowup'], down: ['arrowdown'], enter: ['enter'], esc: ['esc'] }
v-on:input="startSuggest" : { linux: ['ctrl', 'enter'], mac: ['meta', 'enter'], left: ['arrowleft'], right: ['arrowright'] }
:placeholder="$t('modals.new_toot.status')" "
role="textbox" @shortkey="handleKey"
contenteditable="true" @paste="onPaste"
aria-multiline="true" v-on:input="startSuggest"
autofocus> :placeholder="$t('modals.new_toot.status')"
</textarea> role="textbox"
<el-popover contenteditable="true"
placement="bottom-start" aria-multiline="true"
width="300" autofocus
trigger="manual" >
v-model="openSuggest"> </textarea>
<ul class="suggest-list"> <el-popover placement="bottom-start" width="300" trigger="manual" :value="openSuggest">
<li <ul class="suggest-list">
v-for="(item, index) in filteredSuggestion" <li
:key="index" v-for="(item, index) in filteredSuggestion"
@click="insertItem(item)" :key="index"
@shortkey="insertItem(item)" @click="insertItem(item)"
@mouseover="highlightedIndex = index" @shortkey="insertItem(item)"
:class="{'highlighted': highlightedIndex === index}"> @mouseover="highlightedIndex = index"
<span v-if="item.image"> :class="{ highlighted: highlightedIndex === index }"
<img :src="item.image" class="icon" /> >
</span> <span v-if="item.image">
<span v-if="item.code"> <img :src="item.image" class="icon" />
{{ item.code }} </span>
</span> <span v-if="item.code">
{{ item.name }} {{ item.code }}
</li> </span>
</ul> {{ item.name }}
</el-popover> </li>
<div v-click-outside="hideEmojiPicker"> </ul>
<el-button type="text" class="emoji-selector" @click="toggleEmojiPicker"> </el-popover>
<icon name="regular/smile" scale="1.2"></icon> <div v-click-outside="hideEmojiPicker">
</el-button> <el-button type="text" class="emoji-selector" @click="toggleEmojiPicker">
<div v-if="openEmojiPicker" class="emoji-picker"> <icon name="regular/smile" scale="1.2"></icon>
<picker </el-button>
set="emojione" <div v-if="openEmojiPicker" class="emoji-picker">
:autoFocus="true" <picker set="emojione" :autoFocus="true" :custom="pickerEmojis" @select="selectEmoji" />
:custom="pickerEmojis" </div>
@select="selectEmoji"
/>
</div> </div>
</div> </div>
</div>
</template> </template>
<script> <script>
@ -80,13 +77,9 @@ export default {
default: false default: false
} }
}, },
data () { data() {
return { return {
openSuggest: false,
highlightedIndex: 0, highlightedIndex: 0,
startIndex: null,
matchWord: null,
filteredSuggestion: [],
openEmojiPicker: false openEmojiPicker: false
} }
}, },
@ -96,21 +89,23 @@ export default {
}), }),
...mapState('TimelineSpace/Modals/NewToot/Status', { ...mapState('TimelineSpace/Modals/NewToot/Status', {
filteredAccounts: state => state.filteredAccounts, filteredAccounts: state => state.filteredAccounts,
filteredHashtags: state => state.filteredHashtags filteredHashtags: state => state.filteredHashtags,
openSuggest: state => state.openSuggest,
startIndex: state => state.startIndex,
matchWord: state => state.matchWord,
filteredSuggestion: state => state.filteredSuggestion
}), }),
...mapGetters('TimelineSpace/Modals/NewToot/Status', [ ...mapGetters('TimelineSpace/Modals/NewToot/Status', ['pickerEmojis']),
'pickerEmojis'
]),
status: { status: {
get: function () { get: function() {
return this.value return this.value
}, },
set: function (value) { set: function(value) {
this.$emit('input', value) this.$emit('input', value)
} }
} }
}, },
mounted () { mounted() {
// When change account, the new toot modal is recreated. // When change account, the new toot modal is recreated.
// So can not catch open event in watch. // So can not catch open event in watch.
this.$refs.status.focus() this.$refs.status.focus()
@ -119,9 +114,9 @@ export default {
} }
}, },
watch: { watch: {
opened: function (newState, oldState) { opened: function(newState, oldState) {
if (!oldState && newState) { if (!oldState && newState) {
this.$nextTick(function () { this.$nextTick(function() {
this.$refs.status.focus() this.$refs.status.focus()
if (this.fixCursorPos) { if (this.fixCursorPos) {
this.$refs.status.setSelectionRange(0, 0) this.$refs.status.setSelectionRange(0, 0)
@ -134,7 +129,7 @@ export default {
} }
}, },
methods: { methods: {
async startSuggest (e) { async startSuggest(e) {
const currentValue = e.target.value const currentValue = e.target.value
// Start suggest after user stop writing // Start suggest after user stop writing
setTimeout(async () => { setTimeout(async () => {
@ -143,7 +138,7 @@ export default {
} }
}, 700) }, 700)
}, },
async suggest (e) { async suggest(e) {
// e.target.sectionStart: Cursor position // e.target.sectionStart: Cursor position
// e.target.value: current value of the textarea // e.target.value: current value of the textarea
const [start, word] = suggestText(e.target.value, e.target.selectionStart) const [start, word] = suggestText(e.target.value, e.target.selectionStart)
@ -165,36 +160,32 @@ export default {
return false return false
} }
}, },
async suggestAccount (start, word) { async suggestAccount(start, word) {
try { try {
await this.$store.dispatch('TimelineSpace/Modals/NewToot/Status/searchAccount', word) await this.$store.dispatch('TimelineSpace/Modals/NewToot/Status/searchAccount', word)
this.openSuggest = true this.$store.commit('TimelineSpace/Modals/NewToot/Status/changeOpenSuggest', true)
this.startIndex = start this.$store.commit('TimelineSpace/Modals/NewToot/Status/changeStartIndex', start)
this.matchWord = word this.$store.commit('TimelineSpace/Modals/NewToot/Status/changeMatchWord', word)
this.filteredSuggestion = this.filteredAccounts this.$store.commit('TimelineSpace/Modasl/NewToot/Status/filteredSuggestionFromAccounts')
return true return true
} catch (err) { } catch (err) {
console.log(err) console.log(err)
return false return false
} }
}, },
async suggestHashtag (start, word) { async suggestHashtag(start, word) {
try { try {
await this.$store.dispatch('TimelineSpace/Modals/NewToot/Status/searchHashtag', word) await this.$store.dispatch('TimelineSpace/Modals/NewToot/Status/suggestHashtag', { word: word, start: start })
this.openSuggest = true
this.startIndex = start
this.matchWord = word
this.filteredSuggestion = this.filteredHashtags
return true return true
} catch (err) { } catch (err) {
console.log(err) console.log(err)
return false return false
} }
}, },
suggestEmoji (start, word) { suggestEmoji(start, word) {
// Find native emojis // Find native emojis
const filteredEmojiName = emojilib.ordered.filter(emoji => `:${emoji}`.includes(word)) const filteredEmojiName = emojilib.ordered.filter(emoji => `:${emoji}`.includes(word))
const filteredNativeEmoji = filteredEmojiName.map((name) => { const filteredNativeEmoji = filteredEmojiName.map(name => {
return { return {
name: `:${name}:`, name: `:${name}:`,
code: emojilib.lib[name].char code: emojilib.lib[name].char
@ -204,29 +195,32 @@ export default {
const filteredCustomEmoji = this.customEmojis.filter(emoji => emoji.name.includes(word)) const filteredCustomEmoji = this.customEmojis.filter(emoji => emoji.name.includes(word))
const filtered = filteredNativeEmoji.concat(filteredCustomEmoji) const filtered = filteredNativeEmoji.concat(filteredCustomEmoji)
if (filtered.length > 0) { if (filtered.length > 0) {
this.openSuggest = true this.$store.commit('TimelineSpace/Modals/NewToot/Status/changeOpenSuggest', true)
this.startIndex = start this.$store.commit('TimelineSpace/Modals/NewToot/Status/changeStartIndex', start)
this.matchWord = word this.$store.commit('TimelineSpace/Modals/NewToot/Status/changeMatchWord', word)
this.filteredSuggestion = filtered.filter((e, i, array) => { this.$store.commit(
return (array.findIndex(ar => e.name === ar.name) === i) 'TimelineSpace/Modals/NewToot/Status/changeFilteredSuggestion',
}) filtered.filter((e, i, array) => {
return array.findIndex(ar => e.name === ar.name) === i
})
)
} else { } else {
this.closeSuggest() this.closeSuggest()
} }
return true return true
}, },
closeSuggest () { closeSuggest() {
if (this.openSuggest) { if (this.openSuggest) {
this.openSuggest = false this.$store.commit('TimelineSpace/Modals/NewToot/Status/changeOpenSuggest', false)
this.startIndex = null this.$store.commit('TimelineSpace/Modals/NewToot/Status/changeStartIndex', null)
this.matchWord = null this.$store.commit('TimelineSpace/Modals/NewToot/Status/changeMatchWord', null)
this.highlightedIndex = 0 this.highlightedIndex = 0
this.filteredSuggestion = [] this.$store.commit('TimelineSpace/Modals/NewToot/Status/clearFilteredSuggestion')
this.$store.commit('TimelineSpace/Modals/NewToot/Status/clearFilteredAccounts') this.$store.commit('TimelineSpace/Modals/NewToot/Status/clearFilteredAccounts')
this.$store.commit('TimelineSpace/Modals/NewToot/Status/clearFilteredHashtags') this.$store.commit('TimelineSpace/Modals/NewToot/Status/clearFilteredHashtags')
} }
}, },
suggestHighlight (index) { suggestHighlight(index) {
if (index < 0) { if (index < 0) {
this.highlightedIndex = 0 this.highlightedIndex = 0
} else if (index >= this.filteredSuggestion.length) { } else if (index >= this.filteredSuggestion.length) {
@ -235,7 +229,7 @@ export default {
this.highlightedIndex = index this.highlightedIndex = index
} }
}, },
insertItem (item) { insertItem(item) {
if (item.code) { if (item.code) {
const str = `${this.status.slice(0, this.startIndex - 1)}${item.code} ${this.status.slice(this.startIndex + this.matchWord.length)}` const str = `${this.status.slice(0, this.startIndex - 1)}${item.code} ${this.status.slice(this.startIndex + this.matchWord.length)}`
this.status = str this.status = str
@ -245,14 +239,14 @@ export default {
} }
this.closeSuggest() this.closeSuggest()
}, },
selectCurrentItem () { selectCurrentItem() {
const item = this.filteredSuggestion[this.highlightedIndex] const item = this.filteredSuggestion[this.highlightedIndex]
this.insertItem(item) this.insertItem(item)
}, },
onPaste (e) { onPaste(e) {
this.$emit('paste', e) this.$emit('paste', e)
}, },
handleKey (event) { handleKey(event) {
const current = event.target.selectionStart const current = event.target.selectionStart
switch (event.srcKey) { switch (event.srcKey) {
case 'up': case 'up':
@ -281,13 +275,13 @@ export default {
return true return true
} }
}, },
toggleEmojiPicker () { toggleEmojiPicker() {
this.openEmojiPicker = !this.openEmojiPicker this.openEmojiPicker = !this.openEmojiPicker
}, },
hideEmojiPicker () { hideEmojiPicker() {
this.openEmojiPicker = false this.openEmojiPicker = false
}, },
selectEmoji (emoji) { selectEmoji(emoji) {
const current = this.$refs.status.selectionStart const current = this.$refs.status.selectionStart
if (emoji.native) { if (emoji.native) {
this.status = `${this.status.slice(0, current)}${emoji.native} ${this.status.slice(current)}` this.status = `${this.status.slice(0, current)}${emoji.native} ${this.status.slice(current)}`

View File

@ -2,31 +2,47 @@ import { ipcRenderer } from 'electron'
import Mastodon, { Account, Tag, Response, Results } from 'megalodon' import Mastodon, { Account, Tag, Response, Results } from 'megalodon'
import { Module, MutationTree, ActionTree, GetterTree } from 'vuex' import { Module, MutationTree, ActionTree, GetterTree } from 'vuex'
import { RootState } from '@/store/index' import { RootState } from '@/store/index'
import { LocalTag } from '~/src/types/localTag'
interface Suggest { type Suggest = {
name: string name: string
image: string | null image: string | null
} }
interface SuggestAccount extends Suggest {} type SuggestAccount = Suggest
interface SuggestHashtag extends Suggest {} type SuggestHashtag = Suggest
export type StatusState = { export type StatusState = {
filteredAccounts: Array<SuggestAccount> filteredAccounts: Array<SuggestAccount>
filteredHashtags: Array<SuggestHashtag> filteredHashtags: Array<SuggestHashtag>
openSuggest: boolean
startIndex: number | null
matchWord: string | null
filteredSuggestion: Array<Suggest>
} }
const state = (): StatusState => ({ const state = (): StatusState => ({
filteredAccounts: [], filteredAccounts: [],
filteredHashtags: [] filteredHashtags: [],
openSuggest: false,
startIndex: null,
matchWord: null,
filteredSuggestion: []
}) })
export const MUTATION_TYPES = { export const MUTATION_TYPES = {
UPDATE_FILTERED_ACCOUNTS: 'updateFilteredAccounts', UPDATE_FILTERED_ACCOUNTS: 'updateFilteredAccounts',
CLEAR_FILTERED_ACCOUNTS: 'clearFilteredAccounts', CLEAR_FILTERED_ACCOUNTS: 'clearFilteredAccounts',
UPDATE_FILTERED_HASHTAGS: 'updateFilteredHashtags', APPEND_FILTERED_HASHTAGS: 'appendFilteredHashtags',
CLEAR_FILTERED_HASHTAGS: 'clearFilteredHashtags' CLEAR_FILTERED_HASHTAGS: 'clearFilteredHashtags',
CHANGE_OPEN_SUGGEST: 'changeOpenSuggest',
CHANGE_START_INDEX: 'changeStartIndex',
CHANGE_MATCH_WORD: 'changeMatchWord',
FILTERED_SUGGESTION_FROM_HASHTAGS: 'filteredSuggestionFromHashtags',
FILTERED_SUGGESTION_FROM_ACCOUNTS: 'filteredSuggestionFromAccounts',
CLEAR_FILTERED_SUGGESTION: 'clearFilteredSuggestion',
CHANGE_FILTERED_SUGGESTION: 'changeFilteredSuggestion'
} }
const mutations: MutationTree<StatusState> = { const mutations: MutationTree<StatusState> = {
@ -39,17 +55,44 @@ const mutations: MutationTree<StatusState> = {
[MUTATION_TYPES.CLEAR_FILTERED_ACCOUNTS]: state => { [MUTATION_TYPES.CLEAR_FILTERED_ACCOUNTS]: state => {
state.filteredAccounts = [] state.filteredAccounts = []
}, },
[MUTATION_TYPES.UPDATE_FILTERED_HASHTAGS]: (state, tags: Array<Tag>) => { [MUTATION_TYPES.APPEND_FILTERED_HASHTAGS]: (state, tags: Array<Tag>) => {
state.filteredHashtags = tags.map(t => ({ const appended = tags.map(t => ({
name: `#${t}`, name: `#${t}`,
image: null image: null
})) }))
state.filteredHashtags = appended.filter((elem, index, self) => self.indexOf(elem) === index)
}, },
[MUTATION_TYPES.CLEAR_FILTERED_HASHTAGS]: state => { [MUTATION_TYPES.CLEAR_FILTERED_HASHTAGS]: state => {
state.filteredHashtags = [] state.filteredHashtags = []
},
[MUTATION_TYPES.CHANGE_OPEN_SUGGEST]: (state, value: boolean) => {
state.openSuggest = value
},
[MUTATION_TYPES.CHANGE_START_INDEX]: (state, index: number | null) => {
state.startIndex = index
},
[MUTATION_TYPES.CHANGE_MATCH_WORD]: (state, word: string | null) => {
state.matchWord = word
},
[MUTATION_TYPES.FILTERED_SUGGESTION_FROM_HASHTAGS]: state => {
state.filteredSuggestion = state.filteredHashtags
},
[MUTATION_TYPES.FILTERED_SUGGESTION_FROM_ACCOUNTS]: state => {
state.filteredSuggestion = state.filteredAccounts
},
[MUTATION_TYPES.CLEAR_FILTERED_SUGGESTION]: state => {
state.filteredSuggestion = []
},
[MUTATION_TYPES.CHANGE_FILTERED_SUGGESTION]: (state, suggestion: Array<Suggest>) => {
state.filteredSuggestion = suggestion
} }
} }
type WordStart = {
word: string
start: number
}
const actions: ActionTree<StatusState, RootState> = { const actions: ActionTree<StatusState, RootState> = {
searchAccount: async ({ commit, rootState }, word: string) => { searchAccount: async ({ commit, rootState }, word: string) => {
const client = new Mastodon(rootState.TimelineSpace.account.accessToken!, rootState.TimelineSpace.account.baseURL + '/api/v1') const client = new Mastodon(rootState.TimelineSpace.account.accessToken!, rootState.TimelineSpace.account.baseURL + '/api/v1')
@ -58,14 +101,39 @@ const actions: ActionTree<StatusState, RootState> = {
if (res.data.accounts.length === 0) throw new Error('Empty') if (res.data.accounts.length === 0) throw new Error('Empty')
return res.data.accounts return res.data.accounts
}, },
searchHashtag: async ({ commit, rootState }, word: string) => { suggestHashtag: async ({ commit, rootState }, wordStart: WordStart) => {
const client = new Mastodon(rootState.TimelineSpace.account.accessToken!, rootState.TimelineSpace.account.baseURL + '/api/v2') commit(MUTATION_TYPES.CLEAR_FILTERED_HASHTAGS)
const res: Response<Results> = await client.get<Results>('/search', { q: word }) commit(MUTATION_TYPES.FILTERED_SUGGESTION_FROM_HASHTAGS)
commit(MUTATION_TYPES.UPDATE_FILTERED_HASHTAGS, res.data.hashtags) const { word, start } = wordStart
console.log(res.data) const searchCache = () => {
ipcRenderer.send('insert-cache-hashtags', res.data.hashtags) return new Promise(resolve => {
if (res.data.hashtags.length === 0) throw new Error('Empty') const target = word.replace('#', '')
return res.data.hashtags ipcRenderer.once('response-get-cache-hashtags', (_, tags: Array<LocalTag>) => {
const mached = tags.filter(tag => tag.tagName.includes(target)).map(tag => tag.tagName)
commit(MUTATION_TYPES.APPEND_FILTERED_HASHTAGS, mached)
if (mached.length === 0) throw new Error('Empty')
commit(MUTATION_TYPES.CHANGE_OPEN_SUGGEST, true)
commit(MUTATION_TYPES.CHANGE_START_INDEX, start)
commit(MUTATION_TYPES.CHANGE_MATCH_WORD, word)
commit(MUTATION_TYPES.FILTERED_SUGGESTION_FROM_HASHTAGS)
resolve(mached)
})
ipcRenderer.send('get-cache-hashtags')
})
}
const searchAPI = async () => {
const client = new Mastodon(rootState.TimelineSpace.account.accessToken!, rootState.TimelineSpace.account.baseURL + '/api/v1')
const res: Response<Results> = await client.get<Results>('/search', { q: word })
commit(MUTATION_TYPES.APPEND_FILTERED_HASHTAGS, res.data.hashtags)
ipcRenderer.send('insert-cache-hashtags', res.data.hashtags)
if (res.data.hashtags.length === 0) throw new Error('Empty')
commit(MUTATION_TYPES.CHANGE_OPEN_SUGGEST, true)
commit(MUTATION_TYPES.CHANGE_START_INDEX, start)
commit(MUTATION_TYPES.CHANGE_MATCH_WORD, word)
commit(MUTATION_TYPES.FILTERED_SUGGESTION_FROM_HASHTAGS)
return res.data.hashtags
}
await Promise.all([searchCache(), searchAPI()])
} }
} }