Archived
2
0
This repository has been archived on 2024-06-24. You can view files and clone it, but cannot push or open issues or pull requests.
neko-custom/client/src/components/chat.vue

417 lines
9.9 KiB
Vue
Raw Normal View History

2020-01-23 06:16:40 +13:00
<template>
<div class="chat">
<ul class="chat-history" ref="history" @click="onClick">
<template v-for="(message, index) in history">
<li :key="index" class="message" v-if="message.type === 'text'">
<div class="author">
<img :src="`https://api.adorable.io/avatars/40/${member(message.id).username}.png`" />
</div>
<div class="content">
<div class="content-head">
<span>{{ member(message.id).username }}</span>
<span class="timestamp">{{ timestamp(message.created) }}</span>
</div>
<div class="content-body">
<neko-markdown :source="message.content" />
</div>
</div>
</li>
<li :key="index" class="event" v-if="message.type === 'event'">
<span
v-tooltip="{
content: `${member(message.id).username} ${message.content}`,
placement: 'left',
offset: 3,
boundariesElement: 'body',
}"
>
<strong>{{ member(message.id).username }}</strong>
{{ message.content }}
</span>
</li>
</template>
</ul>
<div class="chat-send">
<div class="accent" />
<div class="text-container">
<textarea placeholder="Send a message" @keydown="onKeyDown" v-model="content" />
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.chat {
flex: 1;
flex-direction: column;
display: flex;
max-height: 100%;
max-width: 100%;
overflow-x: hidden;
.chat-history {
flex: 1;
overflow-y: scroll;
overflow-x: hidden;
max-width: 100%;
scrollbar-width: thin;
scrollbar-color: $background-tertiary transparent;
&::-webkit-scrollbar {
width: 8px;
}
&::-webkit-scrollbar-track {
background-color: transparent;
}
&::-webkit-scrollbar-thumb {
background-color: $background-tertiary;
border: 2px solid $background-primary;
border-radius: 4px;
}
&::-webkit-scrollbar-thumb:hover {
background-color: $background-floating;
}
::v-deep *::selection {
background: $text-link;
}
li {
flex: 1;
border-top: 1px solid var(--border-color);
padding: 10px 10px 0px 10px;
display: flex;
flex-direction: row;
overflow: hidden;
user-select: contain;
max-width: 100%;
&.message {
.author {
flex-grow: 0;
flex-shrink: 0;
overflow: hidden;
width: 40px;
height: 40px;
border-radius: 50%;
background: $style-primary;
margin: 5px 10px 10px 0px;
img {
width: 100%;
}
}
.content {
flex: 1;
display: flex;
flex-direction: column;
line-height: 22px;
.content-head {
span {
color: $text-normal;
font-weight: 500;
float: left;
}
.timestamp {
margin-left: 5px;
color: $text-muted;
float: left;
font-size: 0.8em;
line-height: 1.7em;
}
}
::v-deep .content-body {
display: flex;
color: $text-normal;
line-height: 22px;
* {
word-wrap: break-word;
max-width: 225px;
}
a {
color: $text-link;
}
strong {
font-weight: 800;
}
em {
font-style: italic;
}
blockquote {
border-left: 3px $background-accent solid;
padding-left: 3px;
}
.spoiler {
background: $background-tertiary;
padding: 0 2px;
border-radius: 4px;
cursor: pointer;
span {
opacity: 0;
}
&.active {
background: $background-secondary;
cursor: default;
span {
opacity: 1;
}
}
}
code {
font-family: Consolas, Andale Mono WT, Andale Mono, Lucida Console, Lucida Sans Typewriter,
DejaVu Sans Mono, Bitstream Vera Sans Mono, Liberation Mono, Nimbus Mono L, Monaco, Courier New,
Courier, monospace;
background: $background-secondary;
border-radius: 3px;
padding: 0 3px;
font-size: 0.875rem;
line-height: 1.125rem;
text-indent: 0;
white-space: pre-wrap;
}
div {
flex: 1;
pre {
color: $interactive-normal;
border: 1px solid $background-tertiary;
background: $background-secondary;
padding: 8px 6px;
margin: 4px 0;
border-radius: 4px;
display: block;
flex: 1;
code {
display: block;
}
}
}
}
}
}
&.event {
flex: 1;
overflow: hidden;
display: flex;
height: 15px;
color: $text-muted;
span {
white-space: nowrap;
max-width: 250px;
text-overflow: ellipsis;
overflow: hidden;
height: 15px;
strong {
font-weight: 600;
}
i {
float: right;
font-style: italic;
font-size: 10px;
}
}
}
}
}
.chat-send {
flex-shrink: 0;
height: 80px;
max-height: 80px;
padding: 0 10px 10px 10px;
flex-direction: column;
display: flex;
.accent {
width: 100%;
height: 1px;
background: rgba($color: #fff, $alpha: 0.05);
margin: 5px 0 10px 0;
}
.text-container {
flex: 1;
width: 100%;
height: 100%;
background-color: rgba($color: #fff, $alpha: 0.05);
border-radius: 5px;
display: flex;
textarea {
flex: 1;
font-family: $text-family;
border: none;
caret-color: $text-normal;
color: $text-normal;
resize: none;
margin: 5px;
background-color: transparent;
scrollbar-width: thin;
scrollbar-color: $background-tertiary transparent;
&::placeholder {
color: $text-muted;
}
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-track {
background-color: transparent;
}
&::-webkit-scrollbar-thumb {
background-color: $background-tertiary;
border-radius: 4px;
}
&::-webkit-scrollbar-thumb:hover {
background-color: $background-floating;
}
&::selection {
background: $text-link;
}
}
}
}
}
</style>
<script lang="ts">
import { Component, Ref, Watch, Vue } from 'vue-property-decorator'
import { formatRelative } from 'date-fns'
import Markdown from './markdown'
const length = 512 // max length of message
@Component({
name: 'neko-chat',
components: {
'neko-markdown': Markdown,
},
})
export default class extends Vue {
@Ref('history') readonly _history!: HTMLElement
_content = ''
get id() {
return this.$accessor.user.id
}
get history() {
return this.$accessor.chat.history
}
get content() {
return this._content
}
set content(text: string) {
if (text.length > length) {
this._content = text.substring(0, length)
return
}
this._content = text
}
@Watch('history')
onHistroyChange() {
this.$nextTick(() => {
this._history.scrollTop = this._history.scrollHeight
})
}
mounted() {
this.$nextTick(() => {
this._history.scrollTop = this._history.scrollHeight
})
}
member(id: string) {
return this.$accessor.user.members[id]
}
timestamp(time: Date) {
return formatRelative(time, new Date())
}
onClick(event: { target?: HTMLElement; preventDefault(): void }) {
const { target } = event
if (!target) {
return
}
if (target.tagName.toLowerCase() === 'span' && target.classList.contains('spoiler')) {
target.classList.add('active')
event.preventDefault()
}
if (!target.parentElement) {
return
}
if (target.parentElement.tagName.toLowerCase() === 'span' && target.parentElement.classList.contains('spoiler')) {
target.parentElement.classList.add('active')
event.preventDefault()
}
}
onKeyDown(event: KeyboardEvent) {
if (typeof this._content === 'undefined') {
return
}
if (this._content.length == length) {
if (
[8, 16, 17, 18, 20, 33, 34, 35, 36, 37, 38, 39, 40, 45, 46, 91, 93, 144].includes(event.keyCode) ||
(event.ctrlKey && [67, 65, 88].includes(event.keyCode))
) {
return
}
event.preventDefault()
return
}
if (event.keyCode !== 13 || event.shiftKey) {
return
}
if (this._content === '') {
event.preventDefault()
return
}
this.$accessor.chat.sendMessage(this._content)
this._content = ''
event.preventDefault()
}
}
</script>