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-04-06 22:25:07 +12:00
|
|
|
<textarea ref="input" :placeholder="$t('send_a_message')" @keydown="onKeyDown" v-model="content" />
|
2020-02-02 10:27:41 +13:00
|
|
|
<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>
|