Skip to content

Commit

Permalink
Improve Chat UX (#50)
Browse files Browse the repository at this point in the history
* Create ChatSettings component, overhaul chat input UI

* Simplify chat component

* Create ChatInput component

isolate textbox input for better performance

* Remove unused components from chat index

* Make chat input clearable

* Fix null message when input is cleared

* Switch autoscroll function

* Begin work on ChatMessages wrapper

* Add counter back to input

* Use ChatMessages container

* Convert ChatMessage to functional component

* Code Cleanup

Remove old chat code and clean up files

* Improve viewerlist scrolling

* Load chat settings async

* Remove vuetify components from functional component

* Reduce chat message line height

* Improve chat message typography

* Revert line height

* ViewerList scroll improvement, restore settings divider
  • Loading branch information
DispatchCommit authored Oct 24, 2019
1 parent 3763ece commit 1b9d7cf
Show file tree
Hide file tree
Showing 11 changed files with 668 additions and 402 deletions.
149 changes: 149 additions & 0 deletions components/Chat/ChatInput.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
<template>
<!-- Chat Input -->
<v-sheet class="px-2 py-2" color="black">
<div class="d-flex">
<v-text-field
ref="chatmessageinput"
:value="getMessage"
:label="`Chat as ${username}...`"
color="yellow"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="true"
single-line
validate-on-blur
outlined
dense
clearable
:error="error"
counter="300"
@change="value => this.setMessage( value )"
@keyup.enter.prevent="sendMessage"
@keyup.prevent="event => lastMessageHandler(event)"
@cut="event => lastMessageHandler(event)"
></v-text-field>
</div>

<div class="d-flex">
<v-menu
v-model="showChatSettings"
:close-on-content-click="false"
:close-on-click="false"
transition="slide-x-transition"
:max-width="320"
top
right
offset-y
>
<template #activator="{ on }">
<v-btn
v-on="on"
small
text
icon
>
<v-icon>settings</v-icon>
</v-btn>
</template>
<chat-settings
@close="showChatSettings = false"
></chat-settings>
</v-menu>

<v-spacer/>

<v-btn
small
color="yellow black--text"
class="px-2"
@click="sendMessage"
>
send
<v-icon small>send</v-icon>
</v-btn>
</div>

</v-sheet>
</template>

<script>
import { mapState, mapMutations } from 'vuex'
const ChatSettings = () => import( '@/components/Chat/ChatSettings' );
export default {
name: 'ChatInput',
components: {
ChatSettings,
},
props: {
username: { type: String },
},
data() {
return {
showChatSettings: false,
messageBuffer: [],
messageBufferIndex: 0,
}
},
methods: {
...mapMutations( 'chat', {
setMessage: 'SET_CHAT_MESSAGE',
}),
sendMessage() {
if ( this.getMessage.length > 300 ) return;
this.$emit( 'send' );
this.messageBuffer.push(this.getMessage);
this.messageBuffer = this.messageBuffer.splice(-10);
this.messageBufferIndex = this.messageBuffer.length;
this.setMessage( '' );
},
lastMessageHandler ( event ) {
if ( !event.srcElement.value || event.srcElement.value === this.messageBuffer[ this.messageBufferIndex ] ) {
// Up Arrow (keyCode 38)
if ( event.key === 'ArrowUp' ) {
this.messageBufferIndex -= ( this.messageBufferIndex > 0 ) ? 1 : 0;
this.setMessage( this.messageBuffer[ this.messageBufferIndex ] );
event.preventDefault();
}
// Down Arrow (keyCode 40)
else if ( event.key === 'ArrowDown' ) {
this.messageBufferIndex += ( this.messageBufferIndex < this.messageBuffer.length ) ? 1 : 0;
if ( this.messageBufferIndex === this.messageBuffer.length ) this.setMessage( '' );
else this.setMessage( this.messageBuffer[ this.messageBufferIndex ] );
event.preventDefault();
}
// Idk why this is needed
else {
// this.message = '';
}
}
if ( event.type === 'cut' ) {
setTimeout( () => {
if ( !event.srcElement.value ) {
this.setMessage( '' );
}
}, 20 );
}
},
},
computed: {
...mapState( 'chat', {
getMessage: 'message',
}),
error () {
return this.getMessage.length > 300;
}
},
}
</script>
87 changes: 47 additions & 40 deletions components/Chat/ChatMessage.vue
Original file line number Diff line number Diff line change
@@ -1,46 +1,44 @@
<template>
<template functional>
<v-flex class="msg">
<v-layout
row
py-1
ml-3
mr-1
>
<v-avatar
class="mr-2"
size="34"
@click="$emit('reply', username)"
<div class="d-flex py-1 ml-3 mr-1">

<!-- Chat Avatar -->
<div
class="v-avatar mr-2 mt-2"
@click="listeners.reply(props.username)"
>
<img v-if="!!avatar" :src="avatar" :alt="username">
<v-icon v-else :style="{ background: color }">person</v-icon>
</v-avatar>

<v-list-item-content class="py-0">
<v-list-item-subtitle>
<v-layout>
<v-flex shrink>
<span class="time">{{ timestamp }}</span>
<span class="username" :style="userStyling" v-html="displayName"></span>
</v-flex>
<v-spacer/>
<v-flex shrink>
<nuxt-link
:to="channel"
>
<kbd>{{ channel }}</kbd>
</nuxt-link>
</v-flex>
</v-layout>
</v-list-item-subtitle>
<slot></slot>
</v-list-item-content>
<img v-if="!!props.avatar" :src="props.avatar" :alt="props.username">
<div v-else class="v-icon notranslate material-icons" :style="{ background: props.color }">person</div>
</div>

<!-- Chat Content -->
<div class="flex-grow-1">

<!-- Message Header -->
<div class="d-flex align-center">

</v-layout>
<!-- Timestamp & Username -->
<div class="flex-grow-1 subtitle-2">
<span class="time">{{ props.timestamp }}</span>
<span class="username" :style="props.userStyling" v-html="props.displayName"></span>
</div>

<!-- Room Label -->
<div class="flex-shrink-1">
<nuxt-link :to="props.channel">
<kbd>{{ props.channel }}</kbd>
</nuxt-link>
</div>
</div>

<!-- Chat Body -->
<slot></slot>
</div>
</div>
</v-flex>
</template>

<script>
export default {
name: 'ChatMessage',
Expand All @@ -64,10 +62,6 @@
type: String,
},
},
data() {
return {}
},
}
</script>

Expand All @@ -89,6 +83,19 @@
.v-avatar {
cursor: pointer;
user-select: none;
align-items: center;
border-radius: 50%;
display: inline-flex;
justify-content: center;
line-height: normal;
position: relative;
text-align: center;
vertical-align: middle;
height: 32px;
min-width: 32px;
width: 32px;
}
.v-list-item__avatar {
Expand Down
113 changes: 113 additions & 0 deletions components/Chat/ChatMessages.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<template>
<v-flex
id="inner-chat"
fill-height
style="overflow: hidden;"
>
<div
id="chat-scroll"
ref="scroller"
style="overflow-y: scroll; will-change: transform;"
>
<chat-message
v-for="item in messages"
:key="item.timestamp"
:username="item.username"
:display-name="item.username"
:user-styling="{ color: item.userColor ? item.userColor : '#9e9e9e' }"
:channel="item.channel"
:timestamp="getTime(item.timestamp)"
:avatar="item.avatar"
:color="item.color"
@reply="addUserTag"
>
<div
class="body-2"
:style="{ lineHeight: 1.5, }"
v-html="item.message"
></div>
</chat-message>
</div>
</v-flex>
</template>

<script>
import moment from 'moment'
import ChatMessage from '@/components/Chat/ChatMessage'
export default {
name: 'ChatMessages',
components: {
ChatMessage,
},
props: {
messages: { type: Array },
showTimestamps: { type: Boolean },
},
data() {
return {
chatContainer: null,
}
},
methods: {
addUserTag ( user ) {
this.$emit('reply', user);
},
checkIfBottom () {
const scrollTop = this.chatContainer.scrollTop;
const clientHeight = this.chatContainer.clientHeight; // or offsetHeight
const scrollHeight = this.chatContainer.scrollHeight;
return scrollTop + clientHeight >= scrollHeight - document.querySelector("#chat-scroll > div:last-child").clientHeight;
},
async scrollToBottom ( force ) {
// If we are NOT at the bottom && NOT forcing scroll, bail early
if ( !this.checkIfBottom() && !force ) return;
// Scroll to last message
this.chatContainer.scroll({
top: this.chatContainer.scrollHeight,
behavior: 'smooth',
});
setTimeout( () => {
this.chatContainer.scroll({
top: this.chatContainer.scrollHeight,
behavior: 'smooth',
});
}, 500 );
},
jumpToBottom () {
this.chatContainer.scrollTop = this.chatContainer.scrollHeight + 750;
},
getTime ( timestamp ) {
return this.showTimestamps ? `[${moment( timestamp ).format( 'HH:mm' )}]` : '';
},
onScroll ( event ) {
console.log( `At Bottom: ${this.checkIfBottom()}` );
},
},
computed: {},
watch: {
/*messages: async function () {
// await this.scrollToBottom(true);
},*/
},
async mounted () {
this.chatContainer = this.$refs.scroller;
// this.chatContainer.addEventListener( 'scroll', e => this.onScroll(e) );
},
}
</script>
Loading

0 comments on commit 1b9d7cf

Please sign in to comment.