Refined chat scroll handling & styling.
Moved chat functionality from parent VirtualLotteryPage to isolate within Chat component. Chat has better handling for username validation. When receiving or sending messages to chat the scroll bar position more user-friendly when loading more pages, sending message or scrolling back in history while receiving messages.
This commit is contained in:
@@ -40,22 +40,14 @@
|
|||||||
<hr />
|
<hr />
|
||||||
<div class="middle-elements">
|
<div class="middle-elements">
|
||||||
<Attendees :attendees="attendees" class="outer-attendees" />
|
<Attendees :attendees="attendees" class="outer-attendees" />
|
||||||
<Chat
|
<Chat class="outer-chat" />
|
||||||
class="outer-chat"
|
|
||||||
:chatHistory="chatHistory"
|
|
||||||
:historyPageSize="historyPageSize"
|
|
||||||
:usernameAllowed="usernameAllowed"
|
|
||||||
@loadMoreHistory="loadMoreHistory"
|
|
||||||
@message="sendMessage"
|
|
||||||
@username="setUsername"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<Vipps class="vipps" :amount="1" />
|
<Vipps class="vipps" :amount="1" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { attendees, winners, getChatHistory, prelottery } from "@/api";
|
import { attendees, winners, prelottery } from "@/api";
|
||||||
import Chat from "@/ui/Chat";
|
import Chat from "@/ui/Chat";
|
||||||
import Vipps from "@/ui/Vipps";
|
import Vipps from "@/ui/Vipps";
|
||||||
import Attendees from "@/ui/Attendees";
|
import Attendees from "@/ui/Attendees";
|
||||||
@@ -74,21 +66,10 @@ export default {
|
|||||||
socket: null,
|
socket: null,
|
||||||
attendeesFetched: false,
|
attendeesFetched: false,
|
||||||
winnersFetched: false,
|
winnersFetched: false,
|
||||||
chatHistory: [],
|
|
||||||
historyPage: 0,
|
|
||||||
historyPageSize: 100,
|
|
||||||
lastHistoryPage: false,
|
|
||||||
usernameAccepted: false,
|
|
||||||
username: null,
|
|
||||||
wasDisconnected: false,
|
wasDisconnected: false,
|
||||||
emitUsernameOnConnect: false,
|
|
||||||
ticketsBought: {}
|
ticketsBought: {}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
created() {
|
|
||||||
getChatHistory(0, this.historyPageSize)
|
|
||||||
.then(messages => this.chatHistory = messages);
|
|
||||||
},
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.track();
|
this.track();
|
||||||
this.getAttendees();
|
this.getAttendees();
|
||||||
@@ -97,23 +78,10 @@ export default {
|
|||||||
this.socket = io(`${BASE_URL}`);
|
this.socket = io(`${BASE_URL}`);
|
||||||
this.socket.on("color_winner", msg => {});
|
this.socket.on("color_winner", msg => {});
|
||||||
|
|
||||||
this.socket.on("chat", msg => {
|
|
||||||
this.chatHistory.push(msg);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.socket.on("disconnect", msg => {
|
this.socket.on("disconnect", msg => {
|
||||||
this.wasDisconnected = true;
|
this.wasDisconnected = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.socket.on("connect", msg => {
|
|
||||||
if (
|
|
||||||
this.emitUsernameOnConnect ||
|
|
||||||
(this.wasDisconnected && this.username != null)
|
|
||||||
) {
|
|
||||||
this.setUsername(this.username);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.socket.on("winner", async msg => {
|
this.socket.on("winner", async msg => {
|
||||||
this.currentWinnerDrawn = true;
|
this.currentWinnerDrawn = true;
|
||||||
this.currentWinner = { name: msg.name, color: msg.color };
|
this.currentWinner = { name: msg.name, color: msg.color };
|
||||||
@@ -132,14 +100,6 @@ export default {
|
|||||||
this.socket.on("new_attendee", async msg => {
|
this.socket.on("new_attendee", async msg => {
|
||||||
this.getAttendees();
|
this.getAttendees();
|
||||||
});
|
});
|
||||||
this.socket.on("accept_username", accepted => {
|
|
||||||
this.usernameAccepted = accepted;
|
|
||||||
if (!accepted) {
|
|
||||||
this.username = null;
|
|
||||||
} else {
|
|
||||||
window.localStorage.setItem("username", this.username);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.socket.disconnect();
|
this.socket.disconnect();
|
||||||
@@ -153,27 +113,6 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
setUsername: function(username) {
|
|
||||||
this.username = username;
|
|
||||||
if (!this.socket || !this.socket.emit) {
|
|
||||||
this.emitUsernameOnConnect = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.socket.emit("username", { username });
|
|
||||||
},
|
|
||||||
sendMessage: function(msg) {
|
|
||||||
this.socket.emit("chat", { message: msg });
|
|
||||||
},
|
|
||||||
loadMoreHistory: function() {
|
|
||||||
const { historyPage, historyPageSize } = this;
|
|
||||||
const page = historyPage + 1;
|
|
||||||
|
|
||||||
getChatHistory(page * historyPageSize, historyPageSize)
|
|
||||||
.then(messages => {
|
|
||||||
this.chatHistory = messages.concat(this.chatHistory);
|
|
||||||
this.historyPage = page;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
getWinners: async function() {
|
getWinners: async function() {
|
||||||
let response = await winners();
|
let response = await winners();
|
||||||
if (response) {
|
if (response) {
|
||||||
|
|||||||
176
src/ui/Chat.vue
176
src/ui/Chat.vue
@@ -2,23 +2,24 @@
|
|||||||
<div class="chat-container">
|
<div class="chat-container">
|
||||||
<hr />
|
<hr />
|
||||||
<h2>Chat</h2>
|
<h2>Chat</h2>
|
||||||
<div class="history" ref="history">
|
<div class="history" ref="history" v-if="chatHistory.length > 0">
|
||||||
<div class="opaque-skirt"></div>
|
<div class="opaque-skirt"></div>
|
||||||
<div v-if="existsMore" class="fetch-older-history">
|
<div v-if="hasMorePages" class="fetch-older-history">
|
||||||
<button @click="$emit('loadMoreHistory')">Hent eldre meldinger</button>
|
<button @click="loadMoreHistory">Hent eldre meldinger</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="history-message"
|
<div class="history-message"
|
||||||
v-for="(history, index) in chatHistory"
|
v-for="(history, index) in chatHistory"
|
||||||
:key="`${history.username}-${history.timestamp}-${index}`"
|
:key="`${history.username}-${history.timestamp}-${index}`"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<span class="user-name">{{ history.username }}</span>
|
<span class="username">{{ history.username }}</span>
|
||||||
<span class="timestamp">{{ getTime(history.timestamp) }}</span>
|
<span class="timestamp">{{ getTime(history.timestamp) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="message">{{ history.message }}</span>
|
<span class="message">{{ history.message }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="usernameSet" class="input">
|
<div v-if="username" class="input">
|
||||||
<input @keyup.enter="sendMessage" type="text" v-model="message" placeholder="Melding.." />
|
<input @keyup.enter="sendMessage" type="text" v-model="message" placeholder="Melding.." />
|
||||||
<button @click="sendMessage">Send</button>
|
<button @click="sendMessage">Send</button>
|
||||||
<button @click="removeUsername">Logg ut</button>
|
<button @click="removeUsername">Logg ut</button>
|
||||||
@@ -37,63 +38,103 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { getChatHistory } from "@/api";
|
||||||
|
import io from "socket.io-client";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
|
||||||
usernameAllowed: {
|
|
||||||
type: Boolean
|
|
||||||
},
|
|
||||||
chatHistory: {
|
|
||||||
type: Array
|
|
||||||
},
|
|
||||||
historyPageSize: {
|
|
||||||
type: Number
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
socket: null,
|
||||||
|
chatHistory: [],
|
||||||
|
hasMorePages: true,
|
||||||
message: "",
|
message: "",
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
temporaryUsername: null,
|
temporaryUsername: null,
|
||||||
username: null,
|
username: null
|
||||||
usernameSet: false,
|
|
||||||
existsMore: true
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
created() {
|
||||||
|
getChatHistory(1, this.pageSize)
|
||||||
|
.then(resp => {
|
||||||
|
this.chatHistory = resp.messages;
|
||||||
|
this.hasMorePages = resp.total != resp.messages.length;
|
||||||
|
});
|
||||||
|
const username = window.localStorage.getItem('username');
|
||||||
|
if (username) {
|
||||||
|
this.username = username;
|
||||||
|
this.emitUsernameOnConnect = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
watch: {
|
watch: {
|
||||||
chatHistory: {
|
chatHistory: {
|
||||||
handler: function(newVal, oldVal) {
|
handler: function(newVal, oldVal) {
|
||||||
if (this.$refs && this.$refs.history) {
|
if (oldVal.length == 0) {
|
||||||
const firstMessages = oldVal.length == 0;
|
|
||||||
const diffLargerThanOne = newVal.length - oldVal.length > 1;
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
if (firstMessages || diffLargerThanOne == false) {
|
|
||||||
this.scrollToBottomOfHistory();
|
this.scrollToBottomOfHistory();
|
||||||
} else {
|
|
||||||
this.scrollToStartOfNewMessages();
|
|
||||||
// what shows the load more button - if we scroll page and less than page size
|
|
||||||
// come back we have reached a limit
|
|
||||||
this.existsMore = newVal.length - oldVal.length == this.historyPageSize
|
|
||||||
}
|
}
|
||||||
}, 100);
|
else if (newVal && newVal.length == oldVal.length) {
|
||||||
|
if (this.isScrollPositionAtBottom()) {
|
||||||
|
this.scrollToBottomOfHistory();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const prevOldestMessage = oldVal[0];
|
||||||
|
this.scrollToMessageElement(prevOldestMessage);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
deep: true
|
deep: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
let username = window.localStorage.getItem("username");
|
const BASE_URL = __APIURL__ || window.location.origin;
|
||||||
if (username) {
|
this.socket = io(`${BASE_URL}`);
|
||||||
this.username = username;
|
this.socket.on("chat", msg => {
|
||||||
this.usernameSet = true;
|
this.chatHistory.push(msg);
|
||||||
this.$emit("username", username);
|
});
|
||||||
|
|
||||||
|
this.socket.on("disconnect", msg => {
|
||||||
|
this.wasDisconnected = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.on("connect", msg => {
|
||||||
|
if (
|
||||||
|
this.emitUsernameOnConnect ||
|
||||||
|
(this.wasDisconnected && this.username != null)
|
||||||
|
) {
|
||||||
|
this.setUsername(this.username);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.on("accept_username", msg => {
|
||||||
|
const { reason, success, username } = msg;
|
||||||
|
this.usernameAccepted = success;
|
||||||
|
|
||||||
|
if (success !== true) {
|
||||||
|
this.username = null;
|
||||||
|
alert(reason)
|
||||||
|
} else {
|
||||||
|
this.usernameAllowed = true;
|
||||||
|
this.username = username;
|
||||||
|
window.localStorage.setItem("username", username);
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
pad: function(num) {
|
loadMoreHistory() {
|
||||||
|
let { page, pageSize } = this;
|
||||||
|
page = page + 1;
|
||||||
|
|
||||||
|
getChatHistory(page, pageSize)
|
||||||
|
.then(resp => {
|
||||||
|
this.chatHistory = resp.messages.concat(this.chatHistory);
|
||||||
|
this.page = page;
|
||||||
|
this.hasMorePages = resp.total != this.chatHistory.length;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
pad(num) {
|
||||||
if (num > 9) return num;
|
if (num > 9) return num;
|
||||||
return `0${num}`;
|
return `0${num}`;
|
||||||
},
|
},
|
||||||
getTime: function(timestamp) {
|
getTime(timestamp) {
|
||||||
let date = new Date(timestamp);
|
let date = new Date(timestamp);
|
||||||
const timeString = `${this.pad(date.getHours())}:${this.pad(
|
const timeString = `${this.pad(date.getHours())}:${this.pad(
|
||||||
date.getMinutes()
|
date.getMinutes()
|
||||||
@@ -104,40 +145,50 @@ export default {
|
|||||||
}
|
}
|
||||||
return `${date.toLocaleDateString()} ${timeString}`;
|
return `${date.toLocaleDateString()} ${timeString}`;
|
||||||
},
|
},
|
||||||
sendMessage: function() {
|
sendMessage() {
|
||||||
this.$emit("message", this.message);
|
const message = { message: this.message };
|
||||||
this.message = "";
|
this.socket.emit("chat", message);
|
||||||
|
this.message = '';
|
||||||
|
this.scrollToBottomOfHistory();
|
||||||
},
|
},
|
||||||
removeUsername: function() {
|
setUsername(username=undefined) {
|
||||||
|
if (this.temporaryUsername) {
|
||||||
|
username = this.temporaryUsername;
|
||||||
|
}
|
||||||
|
const message = { username: username };
|
||||||
|
this.socket.emit("username", message);
|
||||||
|
},
|
||||||
|
removeUsername() {
|
||||||
this.username = null;
|
this.username = null;
|
||||||
this.temporaryUsername = null;
|
this.temporaryUsername = null;
|
||||||
this.usernameSet = false;
|
|
||||||
window.localStorage.removeItem("username");
|
window.localStorage.removeItem("username");
|
||||||
this.$emit("username", null);
|
|
||||||
},
|
},
|
||||||
setUsername: function() {
|
isScrollPositionAtBottom() {
|
||||||
if (
|
const { history } = this.$refs;
|
||||||
this.temporaryUsername.length > 3 &&
|
if (history) {
|
||||||
this.temporaryUsername.length < 30
|
return history.offsetHeight + history.scrollTop >= history.scrollHeight;
|
||||||
) {
|
|
||||||
this.username = this.temporaryUsername;
|
|
||||||
this.usernameSet = true;
|
|
||||||
this.$emit("username", this.username);
|
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
},
|
},
|
||||||
scrollToBottomOfHistory() {
|
scrollToBottomOfHistory() {
|
||||||
if (this.$refs && this.$refs.history) {
|
setTimeout(() => {
|
||||||
const { history } = this.$refs;
|
const { history } = this.$refs;
|
||||||
history.scrollTop = history.scrollHeight;
|
history.scrollTop = history.scrollHeight;
|
||||||
}
|
}, 1);
|
||||||
},
|
},
|
||||||
scrollToStartOfNewMessages() {
|
scrollToMessageElement(message) {
|
||||||
const { history } = this.$refs;
|
const elemTimestamp = this.getTime(message.timestamp);
|
||||||
const histLength = history.children.length;
|
const self = this;
|
||||||
const pages = Math.floor(histLength / 100);
|
const getTimeStamp = (elem) => elem.getElementsByClassName('timestamp')[0].innerText;
|
||||||
|
const prevOldestMessageInNewList = (elem) => getTimeStamp(elem) == elemTimestamp;
|
||||||
|
|
||||||
const messageToScrollTo = history.children[histLength - ((pages * 100) + 3)]
|
setTimeout(() => {
|
||||||
history.scrollTop = messageToScrollTo.offsetTop;
|
const { history } = self.$refs;
|
||||||
|
const childrenElements = Array.from(history.getElementsByClassName('history-message'));
|
||||||
|
|
||||||
|
const elemInNewList = childrenElements.find(prevOldestMessageInNewList);
|
||||||
|
history.scrollTop = elemInNewList.offsetTop - 70
|
||||||
|
}, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -179,6 +230,7 @@ input {
|
|||||||
.history {
|
.history {
|
||||||
height: 75%;
|
height: 75%;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
&-message {
|
&-message {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -186,7 +238,7 @@ input {
|
|||||||
margin: 0.35rem 0;
|
margin: 0.35rem 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
.user-name {
|
.username {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 1.05rem;
|
font-size: 1.05rem;
|
||||||
margin-right: 0.3rem;
|
margin-right: 0.3rem;
|
||||||
@@ -217,7 +269,7 @@ input {
|
|||||||
& .fetch-older-history {
|
& .fetch-older-history {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin: 0.2rem 0 0.5rem;
|
margin: 1rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include mobile {
|
@include mobile {
|
||||||
|
|||||||
Reference in New Issue
Block a user