314 lines
6.8 KiB
Vue
314 lines
6.8 KiB
Vue
<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 v-if="network && channel" 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 v-if="network && channel" 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
|
|
class="messages"
|
|
role="log"
|
|
aria-live="polite"
|
|
aria-relevant="additions"
|
|
>
|
|
<div
|
|
v-for="(message, id) in messages"
|
|
:key="message.id"
|
|
class="result"
|
|
@click="jump(message, id)"
|
|
>
|
|
<DateMarker
|
|
v-if="shouldDisplayDateMarker(message, id)"
|
|
:key="message.id + '-date'"
|
|
:message="message"
|
|
/>
|
|
<Message
|
|
:key="message.id"
|
|
:channel="channel"
|
|
:network="network"
|
|
:message="message"
|
|
:data-id="message.id"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style>
|
|
.channel-name {
|
|
font-weight: 700;
|
|
}
|
|
</style>
|
|
|
|
<script lang="ts">
|
|
import socket from "../../js/socket";
|
|
import eventbus from "../../js/eventbus";
|
|
|
|
import SidebarToggle from "../SidebarToggle.vue";
|
|
import Message from "../Message.vue";
|
|
import MessageSearchForm from "../MessageSearchForm.vue";
|
|
import DateMarker from "../DateMarker.vue";
|
|
import {watch, computed, defineComponent, nextTick, ref, onMounted, onUnmounted} from "vue";
|
|
import type {ClientMessage} from "../../js/types";
|
|
|
|
import {useStore} from "../../js/store";
|
|
import {useRoute, useRouter} from "vue-router";
|
|
import {switchToChannel} from "../../js/router";
|
|
|
|
export default defineComponent({
|
|
name: "SearchResults",
|
|
components: {
|
|
SidebarToggle,
|
|
Message,
|
|
DateMarker,
|
|
MessageSearchForm,
|
|
},
|
|
setup() {
|
|
const store = useStore();
|
|
const route = useRoute();
|
|
const router = useRouter();
|
|
|
|
const chat = ref<HTMLDivElement>();
|
|
|
|
const loadMoreButton = ref<HTMLButtonElement>();
|
|
|
|
const offset = ref(0);
|
|
const moreResultsAvailable = ref(false);
|
|
const oldScrollTop = ref(0);
|
|
const oldChatHeight = ref(0);
|
|
|
|
const messages = computed(() => {
|
|
const results = store.state.messageSearchResults?.results;
|
|
|
|
if (!results) {
|
|
return [];
|
|
}
|
|
|
|
return results;
|
|
});
|
|
|
|
const chan = computed(() => {
|
|
const chanId = parseInt(String(route.params.id || ""), 10);
|
|
return store.getters.findChannel(chanId);
|
|
});
|
|
|
|
const network = computed(() => {
|
|
if (!chan.value) {
|
|
return null;
|
|
}
|
|
|
|
return chan.value.network;
|
|
});
|
|
|
|
const channel = computed(() => {
|
|
if (!chan.value) {
|
|
return null;
|
|
}
|
|
|
|
return chan.value.channel;
|
|
});
|
|
|
|
const setActiveChannel = () => {
|
|
if (!chan.value) {
|
|
return;
|
|
}
|
|
|
|
store.commit("activeChannel", chan.value);
|
|
};
|
|
|
|
const closeSearch = () => {
|
|
if (!channel.value) {
|
|
return;
|
|
}
|
|
|
|
switchToChannel(channel.value);
|
|
};
|
|
|
|
const shouldDisplayDateMarker = (message: ClientMessage, id: number) => {
|
|
const previousMessage = messages.value[id - 1];
|
|
|
|
if (!previousMessage) {
|
|
return true;
|
|
}
|
|
|
|
return new Date(previousMessage.time).getDay() !== new Date(message.time).getDay();
|
|
};
|
|
|
|
const clearSearchState = () => {
|
|
offset.value = 0;
|
|
store.commit("messageSearchInProgress", false);
|
|
store.commit("messageSearchResults", null);
|
|
};
|
|
|
|
const doSearch = () => {
|
|
clearSearchState(); // this is a new search, so we need to clear anything before that
|
|
socket.emit("search", {
|
|
networkUuid: network.value?.uuid,
|
|
channelName: channel.value?.name,
|
|
searchTerm: String(route.query.q || ""),
|
|
offset: offset.value,
|
|
});
|
|
};
|
|
|
|
const onShowMoreClick = () => {
|
|
if (!chat.value) {
|
|
return;
|
|
}
|
|
|
|
offset.value += 100;
|
|
store.commit("messageSearchInProgress", true);
|
|
|
|
oldScrollTop.value = chat.value.scrollTop;
|
|
oldChatHeight.value = chat.value.scrollHeight;
|
|
|
|
socket.emit("search", {
|
|
networkUuid: network.value?.uuid,
|
|
channelName: channel.value?.name,
|
|
searchTerm: String(route.query.q || ""),
|
|
offset: offset.value,
|
|
});
|
|
};
|
|
|
|
const jumpToBottom = async () => {
|
|
await nextTick();
|
|
|
|
const el = chat.value;
|
|
|
|
if (!el) {
|
|
return;
|
|
}
|
|
|
|
el.scrollTop = el.scrollHeight;
|
|
};
|
|
|
|
const jump = (message: ClientMessage, id: number) => {
|
|
// 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)
|
|
};
|
|
|
|
watch(
|
|
() => route.params.id,
|
|
() => {
|
|
doSearch();
|
|
setActiveChannel();
|
|
}
|
|
);
|
|
|
|
watch(
|
|
() => route.query,
|
|
() => {
|
|
doSearch();
|
|
setActiveChannel();
|
|
}
|
|
);
|
|
|
|
watch(messages, async () => {
|
|
moreResultsAvailable.value = !!(
|
|
messages.value.length && !(messages.value.length % 100)
|
|
);
|
|
|
|
if (!offset.value) {
|
|
await jumpToBottom();
|
|
} else {
|
|
await nextTick();
|
|
|
|
const el = chat.value;
|
|
|
|
if (!el) {
|
|
return;
|
|
}
|
|
|
|
const currentChatHeight = el.scrollHeight;
|
|
el.scrollTop = oldScrollTop.value + currentChatHeight - oldChatHeight.value;
|
|
}
|
|
});
|
|
|
|
onMounted(() => {
|
|
setActiveChannel();
|
|
doSearch();
|
|
|
|
eventbus.on("escapekey", closeSearch);
|
|
eventbus.on("re-search", doSearch);
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
eventbus.off("escapekey", closeSearch);
|
|
eventbus.off("re-search", doSearch);
|
|
clearSearchState();
|
|
});
|
|
|
|
return {
|
|
chat,
|
|
loadMoreButton,
|
|
messages,
|
|
moreResultsAvailable,
|
|
network,
|
|
channel,
|
|
route,
|
|
offset,
|
|
store,
|
|
setActiveChannel,
|
|
closeSearch,
|
|
shouldDisplayDateMarker,
|
|
doSearch,
|
|
onShowMoreClick,
|
|
jumpToBottom,
|
|
jump,
|
|
};
|
|
},
|
|
});
|
|
</script>
|