Add support for sending chat messages

This commit is contained in:
James Lyne 2021-01-07 22:40:05 +00:00
parent 15360b83df
commit 6aba097c85
8 changed files with 186 additions and 64 deletions

View File

@ -35,6 +35,7 @@ import {
DynmapWorldMap
} from "@/dynmap";
import {useStore} from "@/store";
import ChatError from "@/errors/ChatError";
function buildServerConfig(response: any): DynmapServerConfig {
return {
@ -669,5 +670,43 @@ export default {
return sets;
});
},
sendChatMessage(message: string) {
const store = useStore();
if(!store.state.components.chatSending) {
return Promise.reject(new ChatError("Chat is not enabled"));
}
return fetch(window.config.url.sendmessage, {
method: 'POST',
body: JSON.stringify({
name: null,
message: message,
})
}).then((response) => {
if(response.status === 403) { //Rate limited
throw new ChatError(store.state.messages.chatCooldown
.replace('%interval%', store.state.components.chatSending!.cooldown.toString()));
}
if (!response.ok) {
throw new Error('Network request failed');
}
return response.json();
}).then(response => {
if (response.error !== 'none') {
throw new ChatError(store.state.messages.chatNotAllowed);
}
}).catch(e => {
if(!(e instanceof ChatError)) {
console.error('Unexpected error while sending chat message');
console.trace(e);
}
throw e;
});
}
}

View File

@ -20,13 +20,21 @@
<ChatMessage v-for="message in messages" :key="message.timestamp" :message="message"></ChatMessage>
<li v-if="!messages.length" class="message message--skeleton">No chat messages yet...</li>
</ul>
<form v-if="sendingEnabled" class="chat__form" @submit.prevent="sendMessage">
<div v-if="sendingError" class="chat__error">{{ sendingError }}</div>
<input ref="chatInput" v-model="enteredMessage" class="chat__input" type="text" :maxlength="messageMaxLength"
placeholder="Type your chat message here..." :disabled="sendingMessage">
<button class="chat__send" :disabled="!enteredMessage || sendingMessage">Send</button>
</form>
</section>
</template>
<script lang="ts">
import {defineComponent, computed} from "@vue/runtime-core";
import {defineComponent, ref, computed, watch} from "@vue/runtime-core";
import {useStore} from "@/store";
import ChatMessage from "@/components/chat/ChatMessage.vue";
import {ActionTypes} from "@/store/action-types";
import ChatError from "@/errors/ChatError";
export default defineComponent({
components: {
@ -35,16 +43,68 @@
setup() {
const store = useStore(),
componentSettings = computed(() => store.state.components.chatBox),
chatBoxVisible = computed(() => store.state.ui.visibleElements.has('chat')),
sendingEnabled = computed(() => {
return store.state.components.chatSending &&
(!store.state.components.chatSending.loginRequired || store.state.loggedIn);
}),
messageMaxLength = computed(() => store.state.components.chatSending?.maxLength),
chatInput = ref<HTMLInputElement | null>(null),
enteredMessage = ref<string>(""),
sendingMessage = ref<boolean>(false),
sendingError = ref<string | null>(null),
messages = computed(() => {
if(componentSettings.value!.messageHistory) {
return store.state.chat.messages.slice(0, componentSettings.value!.messageHistory);
} else {
return store.state.chat.messages;
}
});
}),
sendMessage = () => {
const message = enteredMessage.value.trim().substring(0, messageMaxLength.value);
if(!message) {
return;
}
sendingMessage.value = true;
sendingError.value = null;
store.dispatch(ActionTypes.SEND_CHAT_MESSAGE, message).then(() => {
enteredMessage.value = "";
sendingError.value = null;
}).catch(e => {
if(e instanceof ChatError) {
sendingError.value = e.message;
} else {
sendingError.value = `An unexpected error occurred. See console for details.`;
}
}).finally(() => {
sendingMessage.value = false;
requestAnimationFrame(() => chatInput.value!.focus());
});
};
watch(chatBoxVisible, newValue => {
if(newValue && sendingEnabled.value) {
requestAnimationFrame(() => chatInput.value!.focus());
}
});
return {
chatInput,
enteredMessage,
sendMessage,
messages,
sendingEnabled,
sendingMessage,
sendingError,
messageMaxLength
}
}
})
@ -88,6 +148,33 @@
}
}
.chat__form {
display: flex;
flex-wrap: wrap;
align-items: stretch;
margin: 1.5rem -1.5rem -1.5rem;
.chat__input {
border-bottom-left-radius: $global-border-radius;
flex-grow: 1;
}
.chat__send {
padding-left: 1rem;
padding-right: 1rem;
border-radius: 0 0 $global-border-radius 0;
}
.chat__error {
background-color: #771616;
color: #eeeeee;
font-size: 1.6rem;
padding: 0.5rem 1rem;
line-height: 2rem;
width: 100%;
}
}
@media (max-width: 25rem), (max-height: 30rem) {
bottom: 6.5rem;
left: 6.5rem;

22
src/errors/ChatError.ts Normal file
View File

@ -0,0 +1,22 @@
/*
* Copyright 2021 James Lyne
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export default class ChatError extends Error {
constructor(message: string) {
super(message);
this.name = "ChatError";
}
}

View File

@ -26,6 +26,7 @@
text-align: center;
position: relative;
transition: color 0.2s ease-in, background-color 0.2s ease-in;
font-size: 1.6rem;
&:hover, &.active {
background-color: $global-focus-color;

View File

@ -115,6 +115,28 @@ button {
@extend %button;
}
input {
appearance: none;
background-color: #333333;
box-shadow: none;
color: #cccccc;
font-size: 1.6rem;
padding: 1rem;
border: 0.2rem solid #333333;
border-radius: 0;
&:focus {
color: #eeeeee;
outline-color: #eeeeee;
}
&[disabled] {
background-color: #333333;
border-color: #333333;
cursor: not-allowed;
}
}
.checkbox {
display: flex;
position: relative;
@ -406,26 +428,6 @@ button {
* Chat
*/
.chatinput {
width: 608px;
height: 16px;
outline: none;
color: #fff;
border: 0;
//background: rgba(0, 0, 0, 0.6) url(../assets/images/chat_cursor.png) no-repeat 1px center;
margin: 4px;
padding: 1px 1px 1px 15px;
}
.chatsendbutton {
background-color: #bbb;
}
.loginbutton {
color: #000;
font-family: sans-serif;
@ -438,48 +440,6 @@ button {
margin: 0;
}
.messagelist {
color: white;
overflow: hidden;
width: 622px;
max-height: 6em;
margin: 4px 4px 0 4px;
padding: 1px;
}
.scrollback:hover {
overflow-y: auto !important;
}
.messagerow {
position: relative;
max-height: 200px;
left: 0;
bottom: 0;
color: #fff;
font-weight: bold;
}
.messageicon {
position: relative;
top: 1px;
left: 0;
}
.messagetext {
position: relative;
top: -3px;
left: 0;
}
.balloonmessage {
word-wrap: break-word;
}
.dynmap .mapMarker .markerName_offline_players {
font-style: italic;
}

View File

@ -24,4 +24,5 @@ export enum ActionTypes {
POP_CIRCLE_UPDATES = "popCircleUpdates",
POP_LINE_UPDATES = "popLineUpdates",
POP_TILE_UPDATES = "popTileUpdates",
SEND_CHAT_MESSAGE = "sendChatMessage",
}

View File

@ -70,6 +70,10 @@ export interface Actions {
{commit}: AugmentedActionContext,
payload: number
): Promise<DynmapTileUpdate[]>
[ActionTypes.SEND_CHAT_MESSAGE](
{commit}: AugmentedActionContext,
payload: string
): Promise<void>
}
export const actions: ActionTree<State, State> & Actions = {
@ -239,4 +243,8 @@ export const actions: ActionTree<State, State> & Actions = {
return Promise.resolve(updates);
},
[ActionTypes.SEND_CHAT_MESSAGE]({commit, state}, message: string): Promise<void> {
return API.sendChatMessage(message);
},
}

View File

@ -30,6 +30,8 @@ export type State = {
messages: DynmapMessageConfig;
components: DynmapComponentConfig;
loggedIn: boolean;
worlds: Map<string, DynmapWorld>;
maps: Map<string, DynmapWorldMap>;
players: Map<string, DynmapPlayer>;
@ -95,6 +97,8 @@ export const state: State = {
anonymousQuit: '',
},
loggedIn: false,
worlds: new Map(), //Defined (loaded) worlds with maps from configuration.json
maps: new Map(), //Defined maps from configuration.json
players: new Map(), //Online players from world.json