Merge pull request #4197 from Nachtalb/richrd/message-search
Message Search: Re-Rebase + Fixes / Adjustments
This commit is contained in:
commit
26a38b12ab
@ -39,6 +39,14 @@
|
|||||||
:network="network"
|
:network="network"
|
||||||
:text="channel.topic"
|
:text="channel.topic"
|
||||||
/></span>
|
/></span>
|
||||||
|
<MessageSearchForm
|
||||||
|
v-if="
|
||||||
|
$store.state.settings.searchEnabled &&
|
||||||
|
['channel', 'query'].includes(channel.type)
|
||||||
|
"
|
||||||
|
:network="network"
|
||||||
|
:channel="channel"
|
||||||
|
/>
|
||||||
<button
|
<button
|
||||||
class="mentions"
|
class="mentions"
|
||||||
aria-label="Open your mentions"
|
aria-label="Open your mentions"
|
||||||
@ -85,7 +93,12 @@
|
|||||||
>
|
>
|
||||||
<div class="scroll-down-arrow" />
|
<div class="scroll-down-arrow" />
|
||||||
</div>
|
</div>
|
||||||
<MessageList ref="messageList" :network="network" :channel="channel" />
|
<MessageList
|
||||||
|
ref="messageList"
|
||||||
|
:network="network"
|
||||||
|
:channel="channel"
|
||||||
|
:focused="focused"
|
||||||
|
/>
|
||||||
<ChatUserList v-if="channel.type === 'channel'" :channel="channel" />
|
<ChatUserList v-if="channel.type === 'channel'" :channel="channel" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -109,6 +122,7 @@ import MessageList from "./MessageList.vue";
|
|||||||
import ChatInput from "./ChatInput.vue";
|
import ChatInput from "./ChatInput.vue";
|
||||||
import ChatUserList from "./ChatUserList.vue";
|
import ChatUserList from "./ChatUserList.vue";
|
||||||
import SidebarToggle from "./SidebarToggle.vue";
|
import SidebarToggle from "./SidebarToggle.vue";
|
||||||
|
import MessageSearchForm from "./MessageSearchForm.vue";
|
||||||
import ListBans from "./Special/ListBans.vue";
|
import ListBans from "./Special/ListBans.vue";
|
||||||
import ListInvites from "./Special/ListInvites.vue";
|
import ListInvites from "./Special/ListInvites.vue";
|
||||||
import ListChannels from "./Special/ListChannels.vue";
|
import ListChannels from "./Special/ListChannels.vue";
|
||||||
@ -122,10 +136,12 @@ export default {
|
|||||||
ChatInput,
|
ChatInput,
|
||||||
ChatUserList,
|
ChatUserList,
|
||||||
SidebarToggle,
|
SidebarToggle,
|
||||||
|
MessageSearchForm,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
network: Object,
|
network: Object,
|
||||||
channel: Object,
|
channel: Object,
|
||||||
|
focused: String,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
specialComponent() {
|
specialComponent() {
|
||||||
|
@ -17,6 +17,7 @@ export default {
|
|||||||
name: "DateMarker",
|
name: "DateMarker",
|
||||||
props: {
|
props: {
|
||||||
message: Object,
|
message: Object,
|
||||||
|
focused: Boolean,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
localeDate() {
|
localeDate() {
|
||||||
|
@ -3,7 +3,11 @@
|
|||||||
:id="'msg-' + message.id"
|
:id="'msg-' + message.id"
|
||||||
:class="[
|
:class="[
|
||||||
'msg',
|
'msg',
|
||||||
{self: message.self, highlight: message.highlight, 'previous-source': isPreviousSource},
|
{
|
||||||
|
self: message.self,
|
||||||
|
highlight: message.highlight || focused,
|
||||||
|
'previous-source': isPreviousSource,
|
||||||
|
},
|
||||||
]"
|
]"
|
||||||
:data-type="message.type"
|
:data-type="message.type"
|
||||||
:data-command="message.command"
|
:data-command="message.command"
|
||||||
@ -25,9 +29,12 @@
|
|||||||
<template v-else-if="message.type === 'action'">
|
<template v-else-if="message.type === 'action'">
|
||||||
<span class="from"><span class="only-copy">* </span></span>
|
<span class="from"><span class="only-copy">* </span></span>
|
||||||
<span class="content" dir="auto">
|
<span class="content" dir="auto">
|
||||||
<Username :user="message.from" dir="auto" /> <ParsedMessage
|
<Username
|
||||||
:message="message"
|
:user="message.from"
|
||||||
/>
|
:network="network"
|
||||||
|
:channel="channel"
|
||||||
|
dir="auto"
|
||||||
|
/> <ParsedMessage :message="message" />
|
||||||
<LinkPreview
|
<LinkPreview
|
||||||
v-for="preview in message.previews"
|
v-for="preview in message.previews"
|
||||||
:key="preview.link"
|
:key="preview.link"
|
||||||
@ -41,7 +48,7 @@
|
|||||||
<span v-if="message.type === 'message'" class="from">
|
<span v-if="message.type === 'message'" class="from">
|
||||||
<template v-if="message.from && message.from.nick">
|
<template v-if="message.from && message.from.nick">
|
||||||
<span class="only-copy"><</span>
|
<span class="only-copy"><</span>
|
||||||
<Username :user="message.from" />
|
<Username :user="message.from" :network="network" :channel="channel" />
|
||||||
<span class="only-copy">> </span>
|
<span class="only-copy">> </span>
|
||||||
</template>
|
</template>
|
||||||
</span>
|
</span>
|
||||||
@ -55,7 +62,7 @@
|
|||||||
<span v-else class="from">
|
<span v-else class="from">
|
||||||
<template v-if="message.from && message.from.nick">
|
<template v-if="message.from && message.from.nick">
|
||||||
<span class="only-copy">-</span>
|
<span class="only-copy">-</span>
|
||||||
<Username :user="message.from" />
|
<Username :user="message.from" :network="network" :channel="channel" />
|
||||||
<span class="only-copy">- </span>
|
<span class="only-copy">- </span>
|
||||||
</template>
|
</template>
|
||||||
</span>
|
</span>
|
||||||
@ -107,6 +114,7 @@ export default {
|
|||||||
network: Object,
|
network: Object,
|
||||||
keepScrollPosition: Function,
|
keepScrollPosition: Function,
|
||||||
isPreviousSource: Boolean,
|
isPreviousSource: Boolean,
|
||||||
|
focused: Boolean,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
timeFormat() {
|
timeFormat() {
|
||||||
|
@ -30,6 +30,7 @@ export default {
|
|||||||
network: Object,
|
network: Object,
|
||||||
messages: Array,
|
messages: Array,
|
||||||
keepScrollPosition: Function,
|
keepScrollPosition: Function,
|
||||||
|
focused: Boolean,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
v-if="shouldDisplayDateMarker(message, id)"
|
v-if="shouldDisplayDateMarker(message, id)"
|
||||||
:key="message.id + '-date'"
|
:key="message.id + '-date'"
|
||||||
:message="message"
|
:message="message"
|
||||||
|
:focused="message.id == focused"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="shouldDisplayUnreadMarker(message.id)"
|
v-if="shouldDisplayUnreadMarker(message.id)"
|
||||||
@ -38,6 +39,7 @@
|
|||||||
:network="network"
|
:network="network"
|
||||||
:keep-scroll-position="keepScrollPosition"
|
:keep-scroll-position="keepScrollPosition"
|
||||||
:messages="message.messages"
|
:messages="message.messages"
|
||||||
|
:focused="message.id == focused"
|
||||||
/>
|
/>
|
||||||
<Message
|
<Message
|
||||||
v-else
|
v-else
|
||||||
@ -47,6 +49,7 @@
|
|||||||
:message="message"
|
:message="message"
|
||||||
:keep-scroll-position="keepScrollPosition"
|
:keep-scroll-position="keepScrollPosition"
|
||||||
:is-previous-source="isPreviousSource(message, id)"
|
:is-previous-source="isPreviousSource(message, id)"
|
||||||
|
:focused="message.id == focused"
|
||||||
@toggle-link-preview="onLinkPreviewToggle"
|
@toggle-link-preview="onLinkPreviewToggle"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@ -75,6 +78,7 @@ export default {
|
|||||||
props: {
|
props: {
|
||||||
network: Object,
|
network: Object,
|
||||||
channel: Object,
|
channel: Object,
|
||||||
|
focused: String,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
condensedMessages() {
|
condensedMessages() {
|
||||||
|
149
client/components/MessageSearchForm.vue
Normal file
149
client/components/MessageSearchForm.vue
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
<template>
|
||||||
|
<form :class="['message-search', {opened: searchOpened}]" @submit.prevent="searchMessages">
|
||||||
|
<div class="input-wrapper">
|
||||||
|
<input
|
||||||
|
ref="searchInputField"
|
||||||
|
v-model="searchInput"
|
||||||
|
type="text"
|
||||||
|
name="search"
|
||||||
|
class="input"
|
||||||
|
placeholder="Search messages…"
|
||||||
|
@blur="closeSearch"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
v-if="!onSearchPage"
|
||||||
|
class="search"
|
||||||
|
type="button"
|
||||||
|
aria-label="Search messages in this channel"
|
||||||
|
@mousedown.prevent="toggleSearch"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
form.message-search {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
form.message-search .input-wrapper {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
form.message-search input {
|
||||||
|
width: 100%;
|
||||||
|
height: auto !important;
|
||||||
|
margin: 7px 0;
|
||||||
|
border: 0;
|
||||||
|
color: inherit;
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
form.message-search input::placeholder {
|
||||||
|
color: rgba(0, 0, 0, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 480px) {
|
||||||
|
form.message-search input {
|
||||||
|
min-width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
form.message-search input:focus {
|
||||||
|
min-width: 220px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
form.message-search .input-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
top: 45px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 1;
|
||||||
|
height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--window-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
form.message-search .input-wrapper input {
|
||||||
|
margin: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
form.message-search.opened .input-wrapper {
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat form.message-search button {
|
||||||
|
display: flex;
|
||||||
|
color: #607992;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "MessageSearchForm",
|
||||||
|
props: {
|
||||||
|
network: Object,
|
||||||
|
channel: Object,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
searchOpened: false,
|
||||||
|
searchInput: "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
onSearchPage() {
|
||||||
|
return this.$route.name === "SearchResults";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
"$route.query.q"() {
|
||||||
|
this.searchInput = this.$route.query.q;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.searchInput = this.$route.query.q;
|
||||||
|
this.searchOpened = this.onSearchPage;
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
closeSearch() {
|
||||||
|
if (!this.onSearchPage) {
|
||||||
|
this.searchOpened = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleSearch() {
|
||||||
|
if (this.searchOpened) {
|
||||||
|
this.$refs.searchInputField.blur();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.searchOpened = true;
|
||||||
|
this.$refs.searchInputField.focus();
|
||||||
|
},
|
||||||
|
searchMessages(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (!this.searchInput) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$router
|
||||||
|
.push({
|
||||||
|
name: "SearchResults",
|
||||||
|
params: {
|
||||||
|
id: this.channel.id,
|
||||||
|
},
|
||||||
|
query: {
|
||||||
|
q: this.searchInput,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (err.name === "NavigationDuplicated") {
|
||||||
|
// Search for the same query again
|
||||||
|
this.$root.$emit("re-search");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
@ -1,5 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<Chat v-if="activeChannel" :network="activeChannel.network" :channel="activeChannel.channel" />
|
<Chat
|
||||||
|
v-if="activeChannel"
|
||||||
|
:network="activeChannel.network"
|
||||||
|
:channel="activeChannel.channel"
|
||||||
|
:focused="this.$route.query.focused"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
@ -20,6 +20,8 @@ export default {
|
|||||||
user: Object,
|
user: Object,
|
||||||
active: Boolean,
|
active: Boolean,
|
||||||
onHover: Function,
|
onHover: Function,
|
||||||
|
channel: Object,
|
||||||
|
network: Object,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
mode() {
|
mode() {
|
||||||
@ -42,6 +44,8 @@ export default {
|
|||||||
eventbus.emit("contextmenu:user", {
|
eventbus.emit("contextmenu:user", {
|
||||||
event: event,
|
event: event,
|
||||||
user: this.user,
|
user: this.user,
|
||||||
|
network: this.network,
|
||||||
|
channel: this.channel,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
246
client/components/Windows/SearchResults.vue
Normal file
246
client/components/Windows/SearchResults.vue
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
<template>
|
||||||
|
<div id="chat-container" class="window">
|
||||||
|
<div
|
||||||
|
id="chat"
|
||||||
|
:class="{
|
||||||
|
'colored-nicks': $store.state.settings.coloredNicks,
|
||||||
|
'time-seconds': $store.state.settings.showSeconds,
|
||||||
|
'time-12h': $store.state.settings.use12hClock,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="chat-view"
|
||||||
|
data-type="search-results"
|
||||||
|
aria-label="Search results"
|
||||||
|
role="tabpanel"
|
||||||
|
>
|
||||||
|
<div class="header">
|
||||||
|
<SidebarToggle />
|
||||||
|
<span class="title"
|
||||||
|
>Searching in <span class="channel-name">{{ channel.name }}</span> for</span
|
||||||
|
>
|
||||||
|
<span class="topic">{{ $route.query.q }}</span>
|
||||||
|
<MessageSearchForm :network="network" :channel="channel" />
|
||||||
|
<button
|
||||||
|
class="close"
|
||||||
|
aria-label="Close search window"
|
||||||
|
title="Close search window"
|
||||||
|
@click="closeSearch"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="chat-content">
|
||||||
|
<div ref="chat" class="chat" tabindex="-1">
|
||||||
|
<div v-show="moreResultsAvailable" class="show-more">
|
||||||
|
<button
|
||||||
|
ref="loadMoreButton"
|
||||||
|
:disabled="
|
||||||
|
$store.state.messageSearchInProgress ||
|
||||||
|
!$store.state.isConnected
|
||||||
|
"
|
||||||
|
class="btn"
|
||||||
|
@click="onShowMoreClick"
|
||||||
|
>
|
||||||
|
<span v-if="$store.state.messageSearchInProgress">Loading…</span>
|
||||||
|
<span v-else>Show older messages</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="$store.state.messageSearchInProgress && !offset"
|
||||||
|
class="search-status"
|
||||||
|
>
|
||||||
|
Searching…
|
||||||
|
</div>
|
||||||
|
<div v-else-if="!messages.length && !offset" class="search-status">
|
||||||
|
No results found.
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="messages"
|
||||||
|
role="log"
|
||||||
|
aria-live="polite"
|
||||||
|
aria-relevant="additions"
|
||||||
|
>
|
||||||
|
<template v-for="(message, id) in messages">
|
||||||
|
<div :key="message.id" class="result" @:click="jump(message, id)">
|
||||||
|
<DateMarker
|
||||||
|
v-if="shouldDisplayDateMarker(message, id)"
|
||||||
|
:key="message.date"
|
||||||
|
:message="message"
|
||||||
|
/>
|
||||||
|
<Message
|
||||||
|
:key="message.id"
|
||||||
|
:channel="channel"
|
||||||
|
:network="network"
|
||||||
|
:message="message"
|
||||||
|
:data-id="message.id"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.channel-name {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import socket from "../../js/socket";
|
||||||
|
|
||||||
|
import SidebarToggle from "../SidebarToggle.vue";
|
||||||
|
import Message from "../Message.vue";
|
||||||
|
import MessageSearchForm from "../MessageSearchForm.vue";
|
||||||
|
import DateMarker from "../DateMarker.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "SearchResults",
|
||||||
|
components: {
|
||||||
|
SidebarToggle,
|
||||||
|
Message,
|
||||||
|
DateMarker,
|
||||||
|
MessageSearchForm,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
offset: 0,
|
||||||
|
moreResultsAvailable: false,
|
||||||
|
oldScrollTop: 0,
|
||||||
|
oldChatHeight: 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
search() {
|
||||||
|
return this.$store.state.messageSearchResults;
|
||||||
|
},
|
||||||
|
messages() {
|
||||||
|
if (!this.search) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.search.results.slice().reverse();
|
||||||
|
},
|
||||||
|
chan() {
|
||||||
|
const chanId = parseInt(this.$route.params.id, 10);
|
||||||
|
return this.$store.getters.findChannel(chanId);
|
||||||
|
},
|
||||||
|
network() {
|
||||||
|
if (!this.chan) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.chan.network;
|
||||||
|
},
|
||||||
|
channel() {
|
||||||
|
if (!this.chan) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.chan.channel;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
"$route.params.id"() {
|
||||||
|
this.doSearch();
|
||||||
|
this.setActiveChannel();
|
||||||
|
},
|
||||||
|
"$route.query.q"() {
|
||||||
|
this.doSearch();
|
||||||
|
this.setActiveChannel();
|
||||||
|
},
|
||||||
|
messages() {
|
||||||
|
this.moreResultsAvailable = this.messages.length && !(this.messages.length % 100);
|
||||||
|
|
||||||
|
if (!this.offset) {
|
||||||
|
this.jumpToBottom();
|
||||||
|
} else {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const currentChatHeight = this.$refs.chat.scrollHeight;
|
||||||
|
this.$refs.chat.scrollTop =
|
||||||
|
this.oldScrollTop + currentChatHeight - this.oldChatHeight;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.setActiveChannel();
|
||||||
|
this.doSearch();
|
||||||
|
this.$root.$on("re-search", this.doSearch); // Enable MessageSearchForm to search for the same query again
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.$root.$off("re-search");
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setActiveChannel() {
|
||||||
|
this.$store.commit("activeChannel", this.chan);
|
||||||
|
},
|
||||||
|
closeSearch() {
|
||||||
|
this.$root.switchToChannel(this.channel);
|
||||||
|
},
|
||||||
|
shouldDisplayDateMarker(message, id) {
|
||||||
|
const previousMessage = this.messages[id - 1];
|
||||||
|
|
||||||
|
if (!previousMessage) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Date(previousMessage.time).getDay() !== new Date(message.time).getDay();
|
||||||
|
},
|
||||||
|
doSearch() {
|
||||||
|
this.offset = 0;
|
||||||
|
this.$store.commit("messageSearchInProgress", true);
|
||||||
|
|
||||||
|
if (!this.offset) {
|
||||||
|
this.$store.commit("messageSearchResults", null); // Only reset if not getting offset
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.emit("search", {
|
||||||
|
networkUuid: this.network.uuid,
|
||||||
|
channelName: this.channel.name,
|
||||||
|
searchTerm: this.$route.query.q,
|
||||||
|
offset: this.offset,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onShowMoreClick() {
|
||||||
|
this.offset += 100;
|
||||||
|
this.$store.commit("messageSearchInProgress", true);
|
||||||
|
|
||||||
|
this.oldScrollTop = this.$refs.chat.scrollTop;
|
||||||
|
this.oldChatHeight = this.$refs.chat.scrollHeight;
|
||||||
|
|
||||||
|
socket.emit("search", {
|
||||||
|
networkUuid: this.network.uuid,
|
||||||
|
channelName: this.channel.name,
|
||||||
|
searchTerm: this.$route.query.q,
|
||||||
|
offset: this.offset + 1,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
jumpToBottom() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const el = this.$refs.chat;
|
||||||
|
el.scrollTop = el.scrollHeight;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
jump(message, id) {
|
||||||
|
// TODO: Implement jumping to messages!
|
||||||
|
// This is difficult because it means client will need to handle a potentially nonlinear message set
|
||||||
|
// (loading IntersectionObserver both before AND after the messages)
|
||||||
|
this.$router.push({
|
||||||
|
name: "MessageList",
|
||||||
|
params: {
|
||||||
|
id: this.chan.id,
|
||||||
|
},
|
||||||
|
query: {
|
||||||
|
focused: id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
@ -284,7 +284,9 @@ p {
|
|||||||
#viewport .lt::before,
|
#viewport .lt::before,
|
||||||
#viewport .rt::before,
|
#viewport .rt::before,
|
||||||
#chat button.mentions::before,
|
#chat button.mentions::before,
|
||||||
|
#chat button.close::before,
|
||||||
#chat button.menu::before,
|
#chat button.menu::before,
|
||||||
|
#chat button.search::before,
|
||||||
.channel-list-item::before,
|
.channel-list-item::before,
|
||||||
#footer .icon,
|
#footer .icon,
|
||||||
#chat .count::before,
|
#chat .count::before,
|
||||||
@ -342,6 +344,8 @@ p {
|
|||||||
#viewport .rt::before { content: "\f0c0"; /* https://fontawesome.com/icons/users?style=solid */ }
|
#viewport .rt::before { content: "\f0c0"; /* https://fontawesome.com/icons/users?style=solid */ }
|
||||||
#chat button.menu::before { content: "\f142"; /* http://fontawesome.io/icon/ellipsis-v/ */ }
|
#chat button.menu::before { content: "\f142"; /* http://fontawesome.io/icon/ellipsis-v/ */ }
|
||||||
#chat button.mentions::before { content: "\f1fa"; /* https://fontawesome.com/icons/at?style=solid */ }
|
#chat button.mentions::before { content: "\f1fa"; /* https://fontawesome.com/icons/at?style=solid */ }
|
||||||
|
#chat button.search::before { content: "\f002"; /* https://fontawesome.com/icons/search?style=solid */ }
|
||||||
|
#chat button.close::before { content: "\f00d"; /* https://fontawesome.com/icons/times?style=solid */ }
|
||||||
|
|
||||||
.context-menu-join::before { content: "\f067"; /* http://fontawesome.io/icon/plus/ */ }
|
.context-menu-join::before { content: "\f067"; /* http://fontawesome.io/icon/plus/ */ }
|
||||||
.context-menu-user::before { content: "\f007"; /* http://fontawesome.io/icon/user/ */ }
|
.context-menu-user::before { content: "\f007"; /* http://fontawesome.io/icon/user/ */ }
|
||||||
@ -575,7 +579,9 @@ p {
|
|||||||
#viewport .lt,
|
#viewport .lt,
|
||||||
#viewport .rt,
|
#viewport .rt,
|
||||||
#chat button.mentions,
|
#chat button.mentions,
|
||||||
#chat button.menu {
|
#chat button.search,
|
||||||
|
#chat button.menu,
|
||||||
|
#chat button.close {
|
||||||
color: #607992;
|
color: #607992;
|
||||||
display: flex;
|
display: flex;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@ -589,7 +595,9 @@ p {
|
|||||||
#viewport .lt::before,
|
#viewport .lt::before,
|
||||||
#viewport .rt::before,
|
#viewport .rt::before,
|
||||||
#chat button.mentions::before,
|
#chat button.mentions::before,
|
||||||
#chat button.menu::before {
|
#chat button.search::before,
|
||||||
|
#chat button.menu::before,
|
||||||
|
#chat button.close::before {
|
||||||
width: 36px;
|
width: 36px;
|
||||||
line-height: 36px; /* Fix alignment in Microsoft Edge */
|
line-height: 36px; /* Fix alignment in Microsoft Edge */
|
||||||
}
|
}
|
||||||
@ -1187,6 +1195,7 @@ textarea.input {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#chat .show-more {
|
#chat .show-more {
|
||||||
|
margin-top: 50px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
padding-top: 15px;
|
padding-top: 15px;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
@ -2848,3 +2857,10 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
|
|||||||
#chat table.channel-list .topic {
|
#chat table.channel-list .topic {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-view[data-type="search-results"] .search-status {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
@ -184,10 +184,12 @@ export function generateChannelContextMenu($root, channel, network) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function generateUserContextMenu($root, channel, network, user) {
|
export function generateUserContextMenu($root, channel, network, user) {
|
||||||
const currentChannelUser = channel.users.find((u) => u.nick === network.nick) || {};
|
const currentChannelUser = channel
|
||||||
|
? channel.users.find((u) => u.nick === network.nick) || {}
|
||||||
|
: {};
|
||||||
|
|
||||||
const whois = () => {
|
const whois = () => {
|
||||||
const chan = $root.$store.getters.findChannelOnCurrentNetwork(user.nick);
|
const chan = network.channels.find((c) => c.name === user.nick);
|
||||||
|
|
||||||
if (chan) {
|
if (chan) {
|
||||||
$root.switchToChannel(chan);
|
$root.switchToChannel(chan);
|
||||||
|
@ -87,6 +87,9 @@ function parse(createElement, text, message = undefined, network = undefined) {
|
|||||||
|
|
||||||
const parts = channelParts.concat(linkParts).concat(emojiParts).concat(nameParts);
|
const parts = channelParts.concat(linkParts).concat(emojiParts).concat(nameParts);
|
||||||
|
|
||||||
|
// The channel the message belongs to might not exist if the user isn't joined to it.
|
||||||
|
const messageChannel = message ? message.channel : null;
|
||||||
|
|
||||||
// Merge the styling information with the channels / URLs / nicks / text objects and
|
// Merge the styling information with the channels / URLs / nicks / text objects and
|
||||||
// generate HTML strings with the resulting fragments
|
// generate HTML strings with the resulting fragments
|
||||||
return merge(parts, styleFragments, cleanText).map((textPart) => {
|
return merge(parts, styleFragments, cleanText).map((textPart) => {
|
||||||
@ -184,6 +187,8 @@ function parse(createElement, text, message = undefined, network = undefined) {
|
|||||||
user: {
|
user: {
|
||||||
nick: textPart.nick,
|
nick: textPart.nick,
|
||||||
},
|
},
|
||||||
|
channel: messageChannel,
|
||||||
|
network,
|
||||||
},
|
},
|
||||||
attrs: {
|
attrs: {
|
||||||
dir: "auto",
|
dir: "auto",
|
||||||
|
@ -13,6 +13,7 @@ import Settings from "../components/Windows/Settings.vue";
|
|||||||
import Help from "../components/Windows/Help.vue";
|
import Help from "../components/Windows/Help.vue";
|
||||||
import Changelog from "../components/Windows/Changelog.vue";
|
import Changelog from "../components/Windows/Changelog.vue";
|
||||||
import NetworkEdit from "../components/Windows/NetworkEdit.vue";
|
import NetworkEdit from "../components/Windows/NetworkEdit.vue";
|
||||||
|
import SearchResults from "../components/Windows/SearchResults.vue";
|
||||||
import RoutedChat from "../components/RoutedChat.vue";
|
import RoutedChat from "../components/RoutedChat.vue";
|
||||||
import store from "./store";
|
import store from "./store";
|
||||||
|
|
||||||
@ -63,6 +64,11 @@ const router = new VueRouter({
|
|||||||
path: "/chan-:id",
|
path: "/chan-:id",
|
||||||
component: RoutedChat,
|
component: RoutedChat,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "SearchResults",
|
||||||
|
path: "/chan-:id/search",
|
||||||
|
component: SearchResults,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -109,6 +109,9 @@ export const config = normalizeConfig({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
searchEnabled: {
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export function createState() {
|
export function createState() {
|
||||||
|
@ -25,3 +25,4 @@ import "./changelog";
|
|||||||
import "./setting";
|
import "./setting";
|
||||||
import "./history_clear";
|
import "./history_clear";
|
||||||
import "./mentions";
|
import "./mentions";
|
||||||
|
import "./search";
|
||||||
|
13
client/js/socket-events/search.js
Normal file
13
client/js/socket-events/search.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import socket from "../socket";
|
||||||
|
import store from "../store";
|
||||||
|
|
||||||
|
socket.on("search:results", (response) => {
|
||||||
|
store.commit("messageSearchInProgress", false);
|
||||||
|
|
||||||
|
if (store.state.messageSearchResults) {
|
||||||
|
store.commit("addMessageSearchResults", response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
store.commit("messageSearchResults", response);
|
||||||
|
});
|
@ -38,6 +38,9 @@ const store = new Vuex.Store({
|
|||||||
versionStatus: "loading",
|
versionStatus: "loading",
|
||||||
versionDataExpired: false,
|
versionDataExpired: false,
|
||||||
serverHasSettings: false,
|
serverHasSettings: false,
|
||||||
|
messageSearchResults: null,
|
||||||
|
messageSearchInProgress: false,
|
||||||
|
searchEnabled: false,
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
appLoaded(state) {
|
appLoaded(state) {
|
||||||
@ -112,12 +115,39 @@ const store = new Vuex.Store({
|
|||||||
serverHasSettings(state, value) {
|
serverHasSettings(state, value) {
|
||||||
state.serverHasSettings = value;
|
state.serverHasSettings = value;
|
||||||
},
|
},
|
||||||
|
messageSearchInProgress(state, value) {
|
||||||
|
state.messageSearchInProgress = value;
|
||||||
|
},
|
||||||
|
messageSearchResults(state, value) {
|
||||||
|
state.messageSearchResults = value;
|
||||||
|
},
|
||||||
|
addMessageSearchResults(state, value) {
|
||||||
|
// Append the search results and add networks and channels to new messages
|
||||||
|
value.results = [...state.messageSearchResults.results, ...value.results];
|
||||||
|
|
||||||
|
state.messageSearchResults = value;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
findChannelOnCurrentNetwork: (state) => (name) => {
|
findChannelOnCurrentNetwork: (state) => (name) => {
|
||||||
name = name.toLowerCase();
|
name = name.toLowerCase();
|
||||||
return state.activeChannel.network.channels.find((c) => c.name.toLowerCase() === name);
|
return state.activeChannel.network.channels.find((c) => c.name.toLowerCase() === name);
|
||||||
},
|
},
|
||||||
|
findChannelOnNetwork: (state) => (networkUuid, channelName) => {
|
||||||
|
for (const network of state.networks) {
|
||||||
|
if (network.uuid !== networkUuid) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const channel of network.channels) {
|
||||||
|
if (channel.name === channelName) {
|
||||||
|
return {network, channel};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
findChannel: (state) => (id) => {
|
findChannel: (state) => (id) => {
|
||||||
for (const network of state.networks) {
|
for (const network of state.networks) {
|
||||||
for (const channel of network.channels) {
|
for (const channel of network.channels) {
|
||||||
|
@ -114,10 +114,20 @@ body {
|
|||||||
#viewport .rt,
|
#viewport .rt,
|
||||||
#chat button.mentions,
|
#chat button.mentions,
|
||||||
#chat button.menu,
|
#chat button.menu,
|
||||||
|
#chat button.close,
|
||||||
#form #submit {
|
#form #submit {
|
||||||
color: #b7c5d1;
|
color: #b7c5d1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Search Form */
|
||||||
|
form.message-search input {
|
||||||
|
background-color: #28333d;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat form.message-search button {
|
||||||
|
color: #b7c5d1;
|
||||||
|
}
|
||||||
|
|
||||||
/* Setup text colors */
|
/* Setup text colors */
|
||||||
#chat .msg[data-type="error"],
|
#chat .msg[data-type="error"],
|
||||||
#chat .msg[data-type="error"] .from {
|
#chat .msg[data-type="error"] .from {
|
||||||
|
@ -63,6 +63,7 @@ function Client(manager, name, config = {}) {
|
|||||||
messageStorage: [],
|
messageStorage: [],
|
||||||
highlightRegex: null,
|
highlightRegex: null,
|
||||||
highlightExceptionRegex: null,
|
highlightExceptionRegex: null,
|
||||||
|
messageProvider: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const client = this;
|
const client = this;
|
||||||
@ -72,7 +73,8 @@ function Client(manager, name, config = {}) {
|
|||||||
|
|
||||||
if (!Helper.config.public && client.config.log) {
|
if (!Helper.config.public && client.config.log) {
|
||||||
if (Helper.config.messageStorage.includes("sqlite")) {
|
if (Helper.config.messageStorage.includes("sqlite")) {
|
||||||
client.messageStorage.push(new MessageStorage(client));
|
client.messageProvider = new MessageStorage(client);
|
||||||
|
client.messageStorage.push(client.messageProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Helper.config.messageStorage.includes("text")) {
|
if (Helper.config.messageStorage.includes("text")) {
|
||||||
@ -111,6 +113,8 @@ function Client(manager, name, config = {}) {
|
|||||||
client.awayMessage = client.config.clientSettings.awayMessage;
|
client.awayMessage = client.config.clientSettings.awayMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
client.config.clientSettings.searchEnabled = client.messageProvider !== undefined;
|
||||||
|
|
||||||
client.compileCustomHighlights();
|
client.compileCustomHighlights();
|
||||||
|
|
||||||
_.forOwn(client.config.sessions, (session) => {
|
_.forOwn(client.config.sessions, (session) => {
|
||||||
@ -539,6 +543,14 @@ Client.prototype.clearHistory = function (data) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Client.prototype.search = function (query) {
|
||||||
|
if (this.messageProvider === undefined) {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.messageProvider.search(query);
|
||||||
|
};
|
||||||
|
|
||||||
Client.prototype.open = function (socketId, target) {
|
Client.prototype.open = function (socketId, target) {
|
||||||
// Due to how socket.io works internally, normal events may arrive later than
|
// Due to how socket.io works internally, normal events may arrive later than
|
||||||
// the disconnect event, and because we can't control this timing precisely,
|
// the disconnect event, and because we can't control this timing precisely,
|
||||||
|
@ -236,17 +236,11 @@ Chan.prototype.writeUserLog = function (client, msg) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Chan.prototype.loadMessages = function (client, network) {
|
Chan.prototype.loadMessages = function (client, network) {
|
||||||
if (!this.isLoggable()) {
|
if (!this.isLoggable() || !client.messageProvider) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const messageStorage = client.messageStorage.find((s) => s.canProvideMessages());
|
client.messageProvider
|
||||||
|
|
||||||
if (!messageStorage) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
messageStorage
|
|
||||||
.getMessages(network, this)
|
.getMessages(network, this)
|
||||||
.then((messages) => {
|
.then((messages) => {
|
||||||
if (messages.length === 0) {
|
if (messages.length === 0) {
|
||||||
|
@ -189,7 +189,7 @@ Network.prototype.createIrcFramework = function (client) {
|
|||||||
|
|
||||||
// Request only new messages from ZNC if we have sqlite logging enabled
|
// Request only new messages from ZNC if we have sqlite logging enabled
|
||||||
// See http://wiki.znc.in/Playback
|
// See http://wiki.znc.in/Playback
|
||||||
if (client.config.log && client.messageStorage.find((s) => s.canProvideMessages())) {
|
if (client.messageProvider) {
|
||||||
this.irc.requestCap("znc.in/playback");
|
this.irc.requestCap("znc.in/playback");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -200,9 +200,70 @@ class MessageStorage {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
search(query) {
|
||||||
|
if (!this.isEnabled) {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let select =
|
||||||
|
'SELECT msg, type, time, network, channel FROM messages WHERE type = "message" AND json_extract(msg, "$.text") LIKE ?';
|
||||||
|
const params = [`%${query.searchTerm}%`];
|
||||||
|
|
||||||
|
if (query.networkUuid) {
|
||||||
|
select += " AND network = ? ";
|
||||||
|
params.push(query.networkUuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.channelName) {
|
||||||
|
select += " AND channel = ? ";
|
||||||
|
params.push(query.channelName.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxResults = 100;
|
||||||
|
|
||||||
|
select += " ORDER BY time DESC LIMIT ? OFFSET ? ";
|
||||||
|
params.push(maxResults);
|
||||||
|
query.offset = parseInt(query.offset, 10) || 0;
|
||||||
|
params.push(query.offset);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.database.all(select, params, (err, rows) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
const response = {
|
||||||
|
searchTerm: query.searchTerm,
|
||||||
|
target: query.channelName,
|
||||||
|
networkUuid: query.networkUuid,
|
||||||
|
offset: query.offset,
|
||||||
|
results: parseSearchRowsToMessages(query.offset, rows),
|
||||||
|
};
|
||||||
|
resolve(response);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
canProvideMessages() {
|
canProvideMessages() {
|
||||||
return this.isEnabled;
|
return this.isEnabled;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = MessageStorage;
|
module.exports = MessageStorage;
|
||||||
|
|
||||||
|
function parseSearchRowsToMessages(id, rows) {
|
||||||
|
const messages = [];
|
||||||
|
|
||||||
|
for (const row of rows) {
|
||||||
|
const msg = JSON.parse(row.msg);
|
||||||
|
msg.time = row.time;
|
||||||
|
msg.type = row.type;
|
||||||
|
msg.networkUuid = row.network;
|
||||||
|
msg.channelName = row.channel;
|
||||||
|
msg.id = id;
|
||||||
|
messages.push(new Msg(msg));
|
||||||
|
id += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
@ -643,6 +643,12 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
|
|||||||
const clientSettings = client.config.clientSettings;
|
const clientSettings = client.config.clientSettings;
|
||||||
socket.emit("setting:all", clientSettings);
|
socket.emit("setting:all", clientSettings);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on("search", (query) => {
|
||||||
|
client.search(query).then((results) => {
|
||||||
|
socket.emit("search:results", results);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.on("sign-out", (tokenToSignOut) => {
|
socket.on("sign-out", (tokenToSignOut) => {
|
||||||
|
Loading…
Reference in New Issue
Block a user