thelounge/client/components/Chat.vue
2022-02-23 16:40:54 -08:00

360 lines
7.9 KiB
Vue

<template>
<div id="chat-container" class="window" :data-current-channel="channel.name" lang="">
<div
id="chat"
:class="{
'hide-motd': !$store.state.settings.motd,
'colored-nicks': $store.state.settings.coloredNicks,
'time-seconds': $store.state.settings.showSeconds,
'time-12h': $store.state.settings.use12hClock,
}"
>
<div
:id="'chan-' + channel.id"
class="chat-view"
:data-type="channel.type"
:aria-label="channel.name"
role="tabpanel"
>
<div class="header">
<SidebarToggle />
<span class="title" :aria-label="'Currently open ' + channel.type">{{
channel.name
}}</span>
<div v-if="channel.editTopic === true" class="topic-container">
<input
ref="topicInput"
:value="channel.topic"
class="topic-input"
placeholder="Set channel topic"
enterkeyhint="done"
@keyup.enter="saveTopic"
@keyup.esc="channel.editTopic = false"
/>
<span aria-label="Save topic" class="save-topic" @click="saveTopic">
<span type="button" aria-label="Save topic"></span>
</span>
</div>
<span
v-else
:title="channel.topic"
:class="{topic: true, empty: !channel.topic}"
@dblclick="editTopic"
><ParsedMessage
v-if="channel.topic"
:network="network"
:text="channel.topic"
/></span>
<MessageSearchForm
v-if="
$store.state.settings.searchEnabled &&
['channel', 'query'].includes(channel.type)
"
:network="network"
:channel="channel"
/>
<button
class="mentions"
aria-label="Open your mentions"
@click="openMentions"
/>
<button
class="menu"
aria-label="Open the context menu"
@click="openContextMenu"
/>
<span
v-if="channel.type === 'channel'"
class="rt-tooltip tooltipped tooltipped-w"
aria-label="Toggle user list"
>
<button
class="rt"
aria-label="Toggle user list"
@click="$store.commit('toggleUserlist')"
/>
</span>
</div>
<div v-if="channel.type === 'special'" class="chat-content">
<div class="chat">
<div class="messages">
<div class="msg">
<component
:is="specialComponent"
:network="network"
:channel="channel"
/>
</div>
</div>
</div>
</div>
<div v-else class="chat-content">
<div
:class="[
'scroll-down tooltipped tooltipped-w tooltipped-no-touch',
{'scroll-down-shown': !channel.scrolledToBottom},
]"
aria-label="Jump to recent messages"
@click="$refs.messageList.jumpToBottom()"
>
<div class="scroll-down-arrow" />
</div>
<ChatUserList v-if="channel.type === 'channel'" :channel="channel" />
<MessageList
ref="messageList"
:network="network"
:channel="channel"
:focused="focused"
/>
</div>
</div>
</div>
<div
v-if="$store.state.currentUserVisibleError"
id="user-visible-error"
@click="hideUserVisibleError"
>
{{ $store.state.currentUserVisibleError }}
</div>
<ChatInput :network="network" :channel="channel" />
</div>
</template>
<style scoped>
@import "../css/special-channels.css";
@import "../css/colored-nicks.css";
#chat.hide-motd .msg[data-command="motd"] {
display: none !important;
}
#user-visible-error {
font-size: 14px;
line-height: 1.5;
font-weight: 600;
padding: 10px;
word-spacing: 3px;
text-transform: uppercase;
background: #e74c3c;
color: #fff;
text-align: center;
cursor: pointer;
}
.scroll-down {
position: absolute;
bottom: 16px;
right: 16px;
z-index: 2;
pointer-events: none;
opacity: 0;
transform: translateY(16px);
transition: transform 0.2s, opacity 0.2s;
cursor: pointer;
}
.scroll-down-shown {
opacity: 1;
transform: none;
pointer-events: auto;
}
.scroll-down-arrow {
width: 36px;
height: 36px;
line-height: 34px;
border-radius: 50%;
background: var(--window-bg-color);
color: var(--button-color);
border: 2px solid var(--button-color);
text-align: center;
transition: background 0.2s, color 0.2s;
box-shadow: 0 6px 10px 0 rgb(0 0 0 / 15%);
}
.scroll-down:hover .scroll-down-arrow {
background: var(--button-color);
color: var(--button-text-color-hover);
}
.scroll-down-arrow::after {
content: "\f107"; /* https://fontawesome.com/icons/angle-down?style=solid */
}
#chat .chat-view[data-type="special"] table th {
word-break: normal;
}
.topic-container {
position: relative;
flex-grow: 1;
padding-left: 10px;
}
.header .topic.empty {
min-width: 0;
}
.header .topic-input {
color: inherit;
background: transparent;
border: 1px solid #cdd3da;
border-radius: 2px;
padding-right: 37px;
padding-left: 10px;
width: 100%;
height: 35px;
overflow: hidden;
font-size: 14px;
line-height: normal;
outline: none;
}
.topic-container .save-topic {
position: absolute;
top: 6px;
right: 0;
}
.topic-container .save-topic span {
font-size: 16px;
color: #607992;
width: 35px;
height: 35px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
appearance: none;
}
.topic-container .save-topic span:hover {
opacity: 0.6;
}
/deep/ table.channel-list .channel,
/deep/ table.channel-list .topic,
/deep/ table.ban-list .hostmask,
/deep/ table.ban-list .banned_by,
/deep/ table.ban-list .banned_at,
/deep/ table.ignore-list .hostmask,
/deep/ table.ignore-list .when {
text-align: left;
}
</style>
<script>
import socket from "../js/socket";
import eventbus from "../js/eventbus";
import ParsedMessage from "./ParsedMessage.vue";
import MessageList from "./MessageList.vue";
import ChatInput from "./ChatInput.vue";
import ChatUserList from "./ChatUserList.vue";
import SidebarToggle from "./SidebarToggle.vue";
import MessageSearchForm from "./MessageSearchForm.vue";
import ListBans from "./Special/ListBans.vue";
import ListInvites from "./Special/ListInvites.vue";
import ListChannels from "./Special/ListChannels.vue";
import ListIgnored from "./Special/ListIgnored.vue";
export default {
name: "Chat",
components: {
ParsedMessage,
MessageList,
ChatInput,
ChatUserList,
SidebarToggle,
MessageSearchForm,
},
props: {
network: Object,
channel: Object,
focused: String,
},
computed: {
specialComponent() {
switch (this.channel.special) {
case "list_bans":
return ListBans;
case "list_invites":
return ListInvites;
case "list_channels":
return ListChannels;
case "list_ignored":
return ListIgnored;
}
return undefined;
},
},
watch: {
channel() {
this.channelChanged();
},
"channel.editTopic"(newValue) {
if (newValue) {
this.$nextTick(() => {
this.$refs.topicInput.focus();
});
}
},
},
mounted() {
this.channelChanged();
if (this.channel.editTopic) {
this.$nextTick(() => {
this.$refs.topicInput.focus();
});
}
},
methods: {
channelChanged() {
// Triggered when active channel is set or changed
this.channel.highlight = 0;
this.channel.unread = 0;
socket.emit("open", this.channel.id);
if (this.channel.usersOutdated) {
this.channel.usersOutdated = false;
socket.emit("names", {
target: this.channel.id,
});
}
},
hideUserVisibleError() {
this.$store.commit("currentUserVisibleError", null);
},
editTopic() {
if (this.channel.type === "channel") {
this.channel.editTopic = true;
}
},
saveTopic() {
this.channel.editTopic = false;
const newTopic = this.$refs.topicInput.value;
if (this.channel.topic !== newTopic) {
const target = this.channel.id;
const text = `/raw TOPIC ${this.channel.name} :${newTopic}`;
socket.emit("input", {target, text});
}
},
openContextMenu(event) {
eventbus.emit("contextmenu:channel", {
event: event,
channel: this.channel,
network: this.network,
});
},
openMentions(event) {
eventbus.emit("mentions:toggle", {
event: event,
});
},
},
};
</script>