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

479 lines
12 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'">
2020-01-24 04:23:26 +13:00
<div class="author" @contextmenu.stop.prevent="onContext($event, { member: member(message.id) })">
2020-04-05 13:33:19 +12:00
<img :src="`https://api.adorable.io/avatars/40/${member(message.id).displayname}.png`" />
2020-01-23 06:16:40 +13:00
</div>
<div class="content">
<div class="content-head">
2020-04-05 13:33:19 +12:00
<span>{{ member(message.id).displayname }}</span>
2020-01-23 06:16:40 +13:00
<span class="timestamp">{{ timestamp(message.created) }}</span>
</div>
2020-01-31 12:59:22 +13:00
<neko-markdown class="content-body" :source="message.content" />
2020-01-23 06:16:40 +13:00
</div>
</li>
<li :key="index" class="event" v-if="message.type === 'event'">
2020-01-31 12:59:22 +13:00
<div
class="content"
2020-01-23 06:16:40 +13:00
v-tooltip="{
2020-01-31 12:59:22 +13:00
content: timestamp(message.created),
2020-01-23 06:16:40 +13:00
placement: 'left',
offset: 3,
boundariesElement: 'body',
}"
>
2020-04-05 14:57:22 +12:00
<strong v-if="message.id === id">{{ $t('you') }}</strong>
2020-04-05 13:33:19 +12:00
<strong v-else>{{ member(message.id).displayname }}</strong>
2020-01-23 06:16:40 +13:00
{{ message.content }}
2020-01-31 12:59:22 +13:00
</div>
2020-01-23 06:16:40 +13:00
</li>
</template>
</ul>
2020-01-24 04:23:26 +13:00
<neko-context ref="context" />
<div v-if="!muted" class="chat-send">
2020-01-23 06:16:40 +13:00
<div class="accent" />
<div class="text-container">
2020-02-02 10:27:41 +13:00
<textarea ref="input" placeholder="Send a message" @keydown="onKeyDown" v-model="content" />
<neko-emoji v-if="emoji" @picked="onEmojiPicked" @done="emoji = false" />
<i class="emoji-menu fas fa-laugh" @click.stop.prevent="onEmoji"></i>
2020-01-23 06:16:40 +13:00
</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);
2020-01-31 12:59:22 +13:00
padding: 10px 5px 0px 10px;
2020-01-23 06:16:40 +13:00
display: flex;
flex-direction: row;
2020-01-31 12:59:22 +13:00
flex-wrap: nowrap;
2020-01-23 06:16:40 +13:00
overflow: hidden;
2020-01-31 12:59:22 +13:00
user-select: text;
word-wrap: break-word;
2020-01-23 06:16:40 +13:00
&.message {
2020-02-02 09:35:48 +13:00
font-size: 16px;
2020-01-23 06:16:40 +13:00
.author {
flex-grow: 0;
flex-shrink: 0;
overflow: hidden;
width: 40px;
height: 40px;
border-radius: 50%;
background: $style-primary;
2020-01-31 12:59:22 +13:00
margin: 0px 10px 10px 0px;
2020-01-23 06:16:40 +13:00
img {
width: 100%;
}
}
.content {
flex: 1;
display: flex;
flex-direction: column;
2020-01-31 12:59:22 +13:00
box-sizing: border-box;
word-wrap: break-word;
min-width: 0;
2020-01-23 06:16:40 +13:00
.content-head {
2020-01-24 04:23:26 +13:00
cursor: default;
2020-01-31 12:59:22 +13:00
width: 100%;
margin-bottom: 3px;
display: block;
2020-01-24 04:23:26 +13:00
2020-01-23 06:16:40 +13:00
span {
2020-01-31 12:59:22 +13:00
display: inline-block;
2020-01-23 06:16:40 +13:00
color: $text-normal;
font-weight: 500;
}
.timestamp {
color: $text-muted;
2020-01-31 12:59:22 +13:00
font-size: 0.7rem;
font-weight: 500;
margin-left: 0.3rem;
line-height: 12px;
&::first-letter {
text-transform: uppercase;
}
2020-01-23 06:16:40 +13:00
}
}
2020-01-31 12:59:22 +13:00
2020-01-23 06:16:40 +13:00
::v-deep .content-body {
color: $text-normal;
2020-02-02 09:35:48 +13:00
line-height: 22px;
2020-01-31 12:59:22 +13:00
word-wrap: break-word;
overflow-wrap: break-word;
2020-01-23 06:16:40 +13:00
a {
color: $text-link;
}
strong {
font-weight: 800;
}
em {
font-style: italic;
}
blockquote {
border-left: 3px $background-accent solid;
padding-left: 3px;
}
2020-01-31 12:59:22 +13:00
span {
&.spoiler {
background: $background-tertiary;
padding: 0 2px;
border-radius: 4px;
cursor: pointer;
2020-01-23 06:16:40 +13:00
2020-01-31 12:59:22 +13:00
span {
opacity: 0;
}
2020-01-23 06:16:40 +13:00
}
2020-01-31 12:59:22 +13:00
&.spoiler.active {
2020-01-23 06:16:40 +13:00
background: $background-secondary;
cursor: default;
2020-01-31 12:59:22 +13:00
2020-01-23 06:16:40 +13:00
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;
}
2020-01-31 12:59:22 +13:00
pre {
flex: 1;
color: $interactive-normal;
border: 1px solid $background-tertiary;
background: $background-secondary;
padding: 8px 6px;
margin: 4px 0;
border-radius: 4px;
display: block;
2020-01-23 06:16:40 +13:00
flex: 1;
2020-01-31 12:59:22 +13:00
code {
2020-01-23 06:16:40 +13:00
display: block;
}
}
}
}
}
&.event {
color: $text-muted;
2020-01-24 04:23:26 +13:00
cursor: default;
2020-01-23 06:16:40 +13:00
2020-01-31 12:59:22 +13:00
.content {
min-width: 0;
box-sizing: border-box;
word-wrap: break-word;
display: inline-block;
vertical-align: baseline;
line-height: 20px;
2020-01-23 06:16:40 +13:00
strong {
font-weight: 600;
}
i {
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;
2020-02-02 09:35:48 +13:00
position: relative;
2020-01-23 06:16:40 +13:00
display: flex;
2020-02-02 09:35:48 +13:00
.emoji-menu {
2020-01-31 12:59:22 +13:00
width: 20px;
height: 20px;
2020-02-02 09:35:48 +13:00
font-size: 20px;
margin: 8px 5px 0 0;
cursor: pointer;
2020-01-31 12:59:22 +13:00
}
2020-01-23 06:16:40 +13:00
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'
2020-02-02 10:27:41 +13:00
import { Member } from '~/neko/types'
2020-01-23 06:16:40 +13:00
import Markdown from './markdown'
2020-01-24 04:23:26 +13:00
import Content from './context.vue'
2020-02-01 10:45:41 +13:00
import Emoji from './emoji.vue'
2020-01-23 06:16:40 +13:00
const length = 512 // max length of message
@Component({
name: 'neko-chat',
components: {
'neko-markdown': Markdown,
2020-01-24 04:23:26 +13:00
'neko-context': Content,
2020-02-01 10:45:41 +13:00
'neko-emoji': Emoji,
2020-01-23 06:16:40 +13:00
},
})
export default class extends Vue {
2020-02-01 23:43:02 +13:00
@Ref('input') readonly _input!: HTMLTextAreaElement
2020-01-23 06:16:40 +13:00
@Ref('history') readonly _history!: HTMLElement
2020-01-24 04:23:26 +13:00
@Ref('context') readonly _context!: any
2020-01-23 06:16:40 +13:00
2020-02-01 23:43:02 +13:00
emoji = false
2020-01-24 05:54:32 +13:00
content = ''
2020-01-23 06:16:40 +13:00
get id() {
return this.$accessor.user.id
}
2020-01-24 04:23:26 +13:00
get muted() {
return this.$accessor.user.muted
}
2020-01-23 06:16:40 +13:00
get history() {
return this.$accessor.chat.history
}
@Watch('history')
onHistroyChange() {
this.$nextTick(() => {
this._history.scrollTop = this._history.scrollHeight
})
}
2020-01-24 04:23:26 +13:00
@Watch('muted')
onMutedChange(muted: boolean) {
if (muted) {
2020-01-24 05:54:32 +13:00
this.content = ''
2020-01-24 04:23:26 +13:00
}
}
2020-01-23 06:16:40 +13:00
mounted() {
this.$nextTick(() => {
this._history.scrollTop = this._history.scrollHeight
})
}
member(id: string) {
return this.$accessor.user.members[id]
}
timestamp(time: Date) {
2020-01-31 12:59:22 +13:00
const str = formatRelative(time, new Date())
return `${str.charAt(0).toUpperCase()}${str.slice(1)}`
2020-01-23 06:16:40 +13:00
}
2020-02-02 10:27:41 +13:00
onEmoji() {
this.emoji = !this.emoji
this._input.focus()
}
2020-02-01 23:43:02 +13:00
onEmojiPicked(emoji: string) {
const text = `:${emoji}:`
if (this._input.selectionStart || this._input.selectionStart === 0) {
var startPos = this._input.selectionStart
var endPos = this._input.selectionEnd
this.content = this.content.substring(0, startPos) + text + this.content.substring(endPos, this.content.length)
this.$nextTick(() => {
this._input.selectionStart = startPos + text.length
this._input.selectionEnd = startPos + text.length
})
} else {
this.content += text
}
2020-02-02 10:27:41 +13:00
this._input.focus()
2020-02-01 23:43:02 +13:00
this.emoji = false
}
2020-02-02 10:27:41 +13:00
onContext(event: MouseEvent, { member }: { member: Member }) {
if (member.id === this.id) {
return
}
this._context.open(event, { member })
2020-01-24 04:23:26 +13:00
}
2020-01-23 06:16:40 +13:00
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) {
2020-01-24 05:54:32 +13:00
if (this.muted) {
2020-01-23 06:16:40 +13:00
return
}
2020-01-24 05:54:32 +13:00
if (this.content.length > length) {
this.content = this.content.substring(0, length)
}
if (this.content.length == length) {
2020-01-23 06:16:40 +13:00
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
}
2020-01-24 05:54:32 +13:00
if (this.content === '') {
2020-01-23 06:16:40 +13:00
event.preventDefault()
return
}
2020-01-24 05:54:32 +13:00
this.$accessor.chat.sendMessage(this.content)
2020-01-23 06:16:40 +13:00
2020-01-24 05:54:32 +13:00
this.content = ''
2020-01-23 06:16:40 +13:00
event.preventDefault()
}
}
</script>