feat: Add global message search (#1385)

* feat: Search messages by content

* Fix search UI

* Add specs

* chore: Filter search results

* Update highlight logic

* Rename query to searchTerm

Co-authored-by: Sojan <sojan@pepalo.com>
This commit is contained in:
Pranav Raj S
2020-11-08 01:46:45 +05:30
committed by GitHub
parent 22683cae66
commit 7718cf7d2c
15 changed files with 399 additions and 30 deletions

View File

@@ -1,6 +1,18 @@
<template>
<section class="app-content columns">
<chat-list :conversation-inbox="inboxId" :label="label"></chat-list>
<chat-list :conversation-inbox="inboxId" :label="label">
<button class="search--button" @click="onSearch">
<i class="ion-ios-search-strong search--icon" />
<div class="text-truncate">
{{ $t('CONVERSATION.SEARCH_MESSAGES') }}
</div>
</button>
<search
v-if="showSearchModal"
:show="showSearchModal"
:on-close="closeSearch"
/>
</chat-list>
<conversation-box
:inbox-id="inboxId"
:is-contact-panel-open="isContactPanelOpen"
@@ -16,18 +28,19 @@
</template>
<script>
/* eslint no-console: 0 */
import { mapGetters } from 'vuex';
import ChatList from '../../../components/ChatList';
import ContactPanel from './ContactPanel';
import ConversationBox from '../../../components/widgets/conversation/ConversationBox';
import Search from './search/Search.vue';
export default {
components: {
ChatList,
ContactPanel,
ConversationBox,
Search,
},
props: {
inboxId: {
@@ -47,6 +60,7 @@ export default {
data() {
return {
panelToggleState: true,
showSearchModal: false,
};
},
computed: {
@@ -116,6 +130,32 @@ export default {
onToggleContactPanel() {
this.isContactPanelOpen = !this.isContactPanelOpen;
},
onSearch() {
this.showSearchModal = true;
},
closeSearch() {
this.showSearchModal = false;
},
},
};
</script>
<style scoped>
.search--button {
align-items: center;
border: 0;
color: var(--s-400);
cursor: pointer;
display: flex;
font-size: var(--font-size-small);
font-weight: 400;
padding: var(--space-normal);
text-align: left;
line-height: var(--font-size-large);
}
.search--icon {
color: var(--s-600);
font-size: var(--font-size-large);
padding-right: var(--space-small);
}
</style>

View File

@@ -0,0 +1,173 @@
<template>
<woot-modal
class="message-search--modal"
:show.sync="show"
:on-close="onClose"
>
<woot-modal-header :header-title="$t('CONVERSATION.SEARCH.TITLE')" />
<div class="search--container">
<input
v-model="searchTerm"
v-focus
:placeholder="$t('CONVERSATION.SEARCH.PLACEHOLDER')"
type="text"
/>
<div v-if="uiFlags.isFetching" class="search--activity-message">
<woot-spinner size="" />
{{ $t('CONVERSATION.SEARCH.LOADING_MESSAGE') }}
</div>
<div
v-if="searchTerm && conversations.length && !uiFlags.isFetching"
class="search-results--container"
>
<div v-for="conversation in conversations" :key="conversation.id">
<button
v-for="message in conversation.messages"
:key="message.id"
class="search--messages"
@click="() => onClick(conversation)"
>
<div class="search--messages__metadata">
<span>#{{ conversation.id }}</span>
<span>{{ dynamicTime(message.created_at) }}</span>
</div>
<div v-html="prepareContent(message.content)" />
</button>
</div>
</div>
<div
v-else-if="
searchTerm &&
!conversations.length &&
!uiFlags.isFetching &&
hasSearched
"
class="search--activity-message"
>
{{ $t('CONVERSATION.SEARCH.NO_MATCHING_RESULTS') }}
</div>
</div>
</woot-modal>
</template>
<script>
import { mapGetters } from 'vuex';
import { frontendURL, conversationUrl } from '../../../../helper/URLHelper';
import timeMixin from '../../../../mixins/time';
export default {
directives: {
focus: {
inserted(el) {
el.focus();
},
},
},
mixins: [timeMixin],
props: {
show: {
type: Boolean,
default: false,
},
onClose: {
type: Function,
default: () => {},
},
},
data() {
return {
searchTerm: '',
hasSearched: false,
};
},
computed: {
...mapGetters({
conversations: 'conversationSearch/getConversations',
uiFlags: 'conversationSearch/getUIFlags',
accountId: 'getCurrentAccountId',
}),
},
watch: {
searchTerm(newValue) {
if (this.typingTimer) {
clearTimeout(this.typingTimer);
}
this.typingTimer = setTimeout(() => {
this.hasSearched = true;
this.$store.dispatch('conversationSearch/get', { q: newValue });
}, 1000);
},
},
mounted() {
this.$store.dispatch('conversationSearch/get', { q: '' });
},
methods: {
prepareContent(content = '') {
return content.replace(
new RegExp(`(${this.searchTerm})`, 'ig'),
'<span class="searchkey--highlight">$1</span>'
);
},
onClick(conversation) {
const path = conversationUrl({
accountId: this.accountId,
id: conversation.id,
});
window.location = frontendURL(path);
},
},
};
</script>
<style lang="scss">
.search--container {
font-size: var(--font-size-default);
padding: var(--space-normal) var(--space-large);
}
.search-results--container {
max-height: 300px;
overflow: scroll;
}
.searchkey--highlight {
background: var(--w-500);
color: var(--white);
}
.search--activity-message {
color: var(--s-800);
text-align: center;
}
.search--messages {
border-bottom: 1px solid var(--b-100);
color: var(--color-body);
cursor: pointer;
font-size: var(--font-size-small);
line-height: 1.5;
padding: var(--space-normal);
text-align: left;
width: 100%;
&:hover {
background: var(--w-50);
}
}
.message-search--modal .modal-container {
width: 800px;
min-height: 460px;
}
.search--messages__metadata {
display: flex;
justify-content: space-between;
margin-bottom: var(--space-small);
color: var(--s-500);
}
</style>