Start chat implementation

This commit is contained in:
James Lyne 2020-12-17 14:50:12 +00:00
parent aabbe1c70d
commit d1fff4e120
14 changed files with 246 additions and 27 deletions

View File

@ -17,12 +17,14 @@
<template> <template>
<Map></Map> <Map></Map>
<Sidebar></Sidebar> <Sidebar></Sidebar>
<!-- <Chat v-if="chatEnabled"></Chat>-->
</template> </template>
<script lang="ts"> <script lang="ts">
import {defineComponent, computed, ref, onMounted, onBeforeUnmount, watch} from 'vue'; import {defineComponent, computed, ref, onMounted, onBeforeUnmount, watch} from 'vue';
import Map from './components/Map.vue'; import Map from './components/Map.vue';
import Sidebar from './components/Sidebar.vue'; import Sidebar from './components/Sidebar.vue';
import Chat from './components/Chat.vue';
import {useStore} from "./store"; import {useStore} from "./store";
import {ActionTypes} from "@/store/action-types"; import {ActionTypes} from "@/store/action-types";
import Util from '@/util'; import Util from '@/util';
@ -33,6 +35,7 @@ export default defineComponent({
components: { components: {
Map, Map,
Sidebar, Sidebar,
Chat
}, },
setup() { setup() {
@ -41,6 +44,7 @@ export default defineComponent({
updateInterval = computed(() => store.state.configuration.updateInterval), updateInterval = computed(() => store.state.configuration.updateInterval),
title = computed(() => store.state.configuration.title), title = computed(() => store.state.configuration.title),
currentUrl = computed(() => store.getters.url), currentUrl = computed(() => store.getters.url),
chatEnabled = computed(() => store.state.components.chat),
updatesEnabled = ref(false), updatesEnabled = ref(false),
updateTimeout = ref(0), updateTimeout = ref(0),
configAttempts = ref(0), configAttempts = ref(0),
@ -100,6 +104,10 @@ export default defineComponent({
onBeforeUnmount(() => stopUpdates()); onBeforeUnmount(() => stopUpdates());
parseUrl(); parseUrl();
return {
chatEnabled,
}
}, },
}); });
</script> </script>

View File

@ -16,21 +16,25 @@
import { import {
DynmapArea, DynmapArea,
DynmapChat,
DynmapCircle, DynmapCircle,
DynmapComponentConfig, DynmapComponentConfig,
DynmapConfigurationResponse, DynmapConfigurationResponse,
DynmapLine, DynmapLine,
DynmapWorldMap,
DynmapMarker, DynmapMarker,
DynmapMarkerSet, DynmapMarkerSet,
DynmapMarkerSetUpdates, DynmapMarkerSetUpdates,
DynmapMessageConfig, DynmapMessageConfig,
DynmapPlayer, DynmapPlayer,
DynmapServerConfig, DynmapTileUpdate, DynmapUpdate, DynmapServerConfig,
DynmapUpdateResponse, DynmapUpdates, DynmapTileUpdate,
DynmapWorld DynmapUpdate,
DynmapUpdateResponse,
DynmapUpdates,
DynmapWorld,
DynmapWorldMap
} from "@/dynmap"; } from "@/dynmap";
import { Sanitizer } from "@esri/arcgis-html-sanitizer"; import {Sanitizer} from "@esri/arcgis-html-sanitizer";
import {useStore} from "@/store"; import {useStore} from "@/store";
const sanitizer = new Sanitizer(); const sanitizer = new Sanitizer();
@ -319,7 +323,7 @@ function buildUpdates(data: Array<any>): DynmapUpdates {
const updates = { const updates = {
markerSets: new Map<string, DynmapMarkerSetUpdates>(), markerSets: new Map<string, DynmapMarkerSetUpdates>(),
tiles: [] as DynmapTileUpdate[], tiles: [] as DynmapTileUpdate[],
chat: [], chat: [] as DynmapChat[],
}, },
dropped = { dropped = {
stale: 0, stale: 0,
@ -327,7 +331,7 @@ function buildUpdates(data: Array<any>): DynmapUpdates {
noId: 0, noId: 0,
unknownType: 0, unknownType: 0,
unknownCType: 0, unknownCType: 0,
incompleteTile: 0, incomplete: 0,
notImplemented: 0, notImplemented: 0,
}, },
lastUpdate = useStore().state.updateTimestamp; lastUpdate = useStore().state.updateTimestamp;
@ -394,13 +398,33 @@ function buildUpdates(data: Array<any>): DynmapUpdates {
} }
case 'chat': case 'chat':
//TODO if(!entry.account || !entry.message || !entry.timestamp) {
dropped.notImplemented++; dropped.incomplete++;
continue;
}
if(entry.timestamp < lastUpdate) {
dropped.stale++;
continue;
}
if(entry.source !== 'player') {
dropped.notImplemented++;
continue;
}
updates.chat.push({
type: 'chat',
account: entry.account,
message: entry.message,
timestamp: entry.timestamp,
channel: entry.channel || undefined,
});
break; break;
case 'tile': case 'tile':
if(!entry.name || !entry.timestamp) { if(!entry.name || !entry.timestamp) {
dropped.incompleteTile++; dropped.incomplete++;
continue; continue;
} }
@ -422,6 +446,10 @@ function buildUpdates(data: Array<any>): DynmapUpdates {
} }
} }
updates.chat = updates.chat.sort((one, two) => {
return one.timestamp - two.timestamp;
});
console.debug(`Updates: ${accepted} accepted. Rejected: `, dropped); console.debug(`Updates: ${accepted} accepted. Rejected: `, dropped);
return updates; return updates;

View File

@ -0,0 +1,6 @@
<svg width="255.35pt" height="204.89pt" version="1.0" viewBox="0 0 255.35 204.89" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(-2.1211 -27.577)" stroke-width="0">
<path transform="scale(.75)" d="m84.5 36.77c-45.246 0-81.672 36.424-81.672 81.67v56.217c0 45.246 36.426 81.67 81.672 81.67h177.13c45.246 0 81.672-36.424 81.672-81.67v-56.217c0-45.246-36.426-81.67-81.672-81.67zm0.07031 107.41a23.375 23.375 0 0 1 0.12695 0 23.375 23.375 0 0 1 23.375 23.375 23.375 23.375 0 0 1-23.375 23.375 23.375 23.375 0 0 1-23.375-23.375 23.375 23.375 0 0 1 23.248-23.375zm90.25 0a23.375 23.375 0 0 1 0.12696 0 23.375 23.375 0 0 1 23.375 23.375 23.375 23.375 0 0 1-23.375 23.375 23.375 23.375 0 0 1-23.375-23.375 23.375 23.375 0 0 1 23.248-23.375zm90.312 0a23.375 23.375 0 0 1 0.12696 0 23.375 23.375 0 0 1 23.375 23.375 23.375 23.375 0 0 1-23.375 23.375 23.375 23.375 0 0 1-23.375-23.375 23.375 23.375 0 0 1 23.248-23.375z"/>
<path d="m84.929 180.81-27.39 49.754c-0.45922 1.8202 0.23674 2.7233 3.9555 0.91854 18.017-8.7437 98.169-50.072 98.169-50.072" style="paint-order:normal"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

43
src/components/Chat.vue Normal file
View File

@ -0,0 +1,43 @@
<!--
- Copyright 2020 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.
-->
<template>
<section class="chat">
<ul class="chat__messages">
<li class="message" v-for="message in chat" :key="message.timestamp">{{ message.message || 'aaaa' }}</li>
</ul>
</section>
</template>
<script lang="ts">
import {defineComponent, computed} from "@vue/runtime-core";
import {useStore} from "@/store";
export default defineComponent({
setup() {
const store = useStore(),
chat = computed(() => store.state.chat);
return {
chat,
}
}
})
</script>
<style lang="scss">
</style>

View File

@ -24,6 +24,7 @@
<CoordinatesControl v-if="coordinatesControlEnabled" :leaflet="leaflet"></CoordinatesControl> <CoordinatesControl v-if="coordinatesControlEnabled" :leaflet="leaflet"></CoordinatesControl>
<LinkControl v-if="linkControlEnabled" :leaflet="leaflet"></LinkControl> <LinkControl v-if="linkControlEnabled" :leaflet="leaflet"></LinkControl>
<ClockControl v-if="clockControlEnabled" :leaflet="leaflet"></ClockControl> <ClockControl v-if="clockControlEnabled" :leaflet="leaflet"></ClockControl>
<!-- <ChatControl v-if="chatEnabled" :leaflet="leaflet"></ChatControl>-->
</div> </div>
</template> </template>
@ -37,6 +38,7 @@ import MarkerSetLayer from "@/components/map/layer/MarkerSetLayer.vue";
import CoordinatesControl from "@/components/map/control/CoordinatesControl.vue"; import CoordinatesControl from "@/components/map/control/CoordinatesControl.vue";
import ClockControl from "@/components/map/control/ClockControl.vue"; import ClockControl from "@/components/map/control/ClockControl.vue";
import LinkControl from "@/components/map/control/LinkControl.vue"; import LinkControl from "@/components/map/control/LinkControl.vue";
import ChatControl from "@/components/map/control/ChatControl.vue";
import LogoControl from "@/components/map/control/LogoControl.vue"; import LogoControl from "@/components/map/control/LogoControl.vue";
import {MutationTypes} from "@/store/mutation-types"; import {MutationTypes} from "@/store/mutation-types";
import {Coordinate, DynmapPlayer} from "@/dynmap"; import {Coordinate, DynmapPlayer} from "@/dynmap";
@ -52,6 +54,7 @@ export default defineComponent({
CoordinatesControl, CoordinatesControl,
ClockControl, ClockControl,
LinkControl, LinkControl,
ChatControl,
LogoControl LogoControl
}, },
@ -67,6 +70,7 @@ export default defineComponent({
coordinatesControlEnabled = computed(() => store.getters.coordinatesControlEnabled), coordinatesControlEnabled = computed(() => store.getters.coordinatesControlEnabled),
clockControlEnabled = computed(() => store.getters.clockControlEnabled), clockControlEnabled = computed(() => store.getters.clockControlEnabled),
linkControlEnabled = computed(() => store.state.components.linkControl), linkControlEnabled = computed(() => store.state.components.linkControl),
chatEnabled = computed(() => store.state.components.chat),
logoControls = computed(() => store.state.components.logoControls), logoControls = computed(() => store.state.components.logoControls),
currentWorld = computed(() => store.state.currentWorld), currentWorld = computed(() => store.state.currentWorld),
@ -86,6 +90,7 @@ export default defineComponent({
coordinatesControlEnabled, coordinatesControlEnabled,
clockControlEnabled, clockControlEnabled,
linkControlEnabled, linkControlEnabled,
chatEnabled,
logoControls, logoControls,
followTarget, followTarget,
panTarget, panTarget,

View File

@ -0,0 +1,43 @@
<!--
- Copyright 2020 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.
-->
<script lang="ts">
import {defineComponent, onMounted, onUnmounted} from "@vue/runtime-core";
import {ChatControl} from "@/leaflet/control/ChatControl";
import DynmapMap from "@/leaflet/DynmapMap";
export default defineComponent({
props: {
leaflet: {
type: Object as () => DynmapMap,
required: true,
}
},
setup(props) {
const control = new ChatControl({
position: 'topleft',
});
onMounted(() => props.leaflet.addControl(control));
onUnmounted(() => props.leaflet.removeControl(control));
},
render() {
return null;
}
})
</script>

19
src/dynmap.d.ts vendored
View File

@ -79,6 +79,7 @@ interface DynmapComponentConfig {
clockControl ?: ClockControlOptions; clockControl ?: ClockControlOptions;
linkControl: boolean; linkControl: boolean;
logoControls: Array<LogoControlOptions>; logoControls: Array<LogoControlOptions>;
chat?: DynmapChatConfig;
} }
interface DynmapMarkersConfig { interface DynmapMarkersConfig {
@ -95,6 +96,13 @@ interface DynmapPlayerMarkersConfig {
smallFaces: boolean; smallFaces: boolean;
} }
interface DynmapChatConfig {
allowUrlName: boolean;
showPlayerFaces: boolean;
messageLifetime: number;
messageHistory: number;
}
interface DynmapWorld { interface DynmapWorld {
seaLevel: number; seaLevel: number;
name: string; name: string;
@ -274,4 +282,13 @@ interface DynmapParsedUrl {
map?: string; map?: string;
location?: Coordinate; location?: Coordinate;
zoom?: number; zoom?: number;
} }
interface DynmapChat {
type: 'chat' | 'playerjoin' | 'playerleave';
account: string;
channel?: string;
message?: string;
// source?: string;
timestamp: number;
}

View File

@ -0,0 +1,40 @@
/*
* Copyright 2020 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.
*/
import {Control, ControlOptions, DomUtil, Map} from 'leaflet';
import chat from '@/assets/icons/chat.svg';
export class ChatControl extends Control {
// @ts-ignore
options: ControlOptions
constructor(options: ControlOptions) {
super(options);
}
onAdd(map: Map) {
const chatButton = DomUtil.create('button', 'leaflet-control-chat') as HTMLButtonElement;
chatButton.type = 'button';
chatButton.title = 'Chat';
chatButton.innerHTML = `
<svg class="svg-icon" viewBox="${chat.viewBox}">
<use xlink:href="${chat.url}" />
</svg>`;
return chatButton;
}
}

View File

@ -70,7 +70,9 @@
} }
.leaflet-control-link, .leaflet-control-link,
.leaflet-control-loading { .leaflet-control-chat,
.leaflet-control-loading,
.leaflet-control-layers-toggle {
@extend %button; @extend %button;
width: 5rem; width: 5rem;
height: 5rem; height: 5rem;
@ -150,13 +152,17 @@
.leaflet-top { .leaflet-top {
padding-top: 1rem; padding-top: 1rem;
flex-direction: column; flex-direction: column;
top: 0;
bottom: 7rem;
align-items: flex-start;
.leaflet-control { .leaflet-control {
order: 2; order: 2;
min-width: 5rem; min-width: 5rem;
margin-top: 1rem;
& + .leaflet-control { &:first-child {
margin-top: 1rem; margin-top: 0;
} }
} }
@ -179,6 +185,11 @@
margin-top: 1rem !important; margin-top: 1rem !important;
} }
} }
.leaflet-control-chat {
margin-top: auto;
order: 1000;
}
} }
.leaflet-bottom { .leaflet-bottom {

View File

@ -425,20 +425,29 @@ button {
*/ */
.chat { .chat {
@extend %panel;
position: absolute; position: absolute;
bottom: 0; bottom: 7rem;
left: 32px; left: 7rem;
z-index:50; width: 50rem;
max-width: calc(100% - 8rem);
max-height: 20rem;
display: flex;
box-sizing: border-box;
border-color: rgba(0,0,0,0.5); .chat__messages {
background: rgba(0,0,0,0.6); display: flex;
flex-direction: column-reverse;
list-style: none;
overflow: auto;
margin: 0;
padding: 0;
border-style: solid; .message {
border-width: 1px 1px 0 1px; font-size: 1.6rem;
margin-bottom: 0.5rem
border-radius: 3px 3px 0 0; }
}
margin-left: 10px;
} }
.chatinput { .chatinput {

View File

@ -136,6 +136,7 @@ export const actions: ActionTree<State, State> & Actions = {
commit(MutationTypes.INCREMENT_REQUEST_ID, undefined); commit(MutationTypes.INCREMENT_REQUEST_ID, undefined);
commit(MutationTypes.ADD_MARKER_SET_UPDATES, update.updates.markerSets); commit(MutationTypes.ADD_MARKER_SET_UPDATES, update.updates.markerSets);
commit(MutationTypes.ADD_TILE_UPDATES, update.updates.tiles); commit(MutationTypes.ADD_TILE_UPDATES, update.updates.tiles);
commit(MutationTypes.ADD_CHAT, update.updates.chat);
return dispatch(ActionTypes.SET_PLAYERS, update.players).then(() => { return dispatch(ActionTypes.SET_PLAYERS, update.players).then(() => {
return update; return update;

View File

@ -25,6 +25,7 @@ export enum MutationTypes {
SET_UPDATE_TIMESTAMP = 'setUpdateTimestamp', SET_UPDATE_TIMESTAMP = 'setUpdateTimestamp',
ADD_MARKER_SET_UPDATES = 'addMarkerSetUpdates', ADD_MARKER_SET_UPDATES = 'addMarkerSetUpdates',
ADD_TILE_UPDATES = 'addTileUpdates', ADD_TILE_UPDATES = 'addTileUpdates',
ADD_CHAT = 'addChat',
POP_MARKER_UPDATES = 'popMarkerUpdates', POP_MARKER_UPDATES = 'popMarkerUpdates',
POP_AREA_UPDATES = 'popAreaUpdates', POP_AREA_UPDATES = 'popAreaUpdates',
POP_CIRCLE_UPDATES = 'popCircleUpdates', POP_CIRCLE_UPDATES = 'popCircleUpdates',

View File

@ -33,7 +33,7 @@ import {
DynmapPlayer, DynmapPlayer,
DynmapServerConfig, DynmapTileUpdate, DynmapServerConfig, DynmapTileUpdate,
DynmapWorld, DynmapWorld,
DynmapWorldState, DynmapParsedUrl DynmapWorldState, DynmapParsedUrl, DynmapChat
} from "@/dynmap"; } from "@/dynmap";
import {DynmapProjection} from "@/leaflet/projection/DynmapProjection"; import {DynmapProjection} from "@/leaflet/projection/DynmapProjection";
@ -53,6 +53,7 @@ export type Mutations<S = State> = {
[MutationTypes.SET_UPDATE_TIMESTAMP](state: S, time: Date): void [MutationTypes.SET_UPDATE_TIMESTAMP](state: S, time: Date): void
[MutationTypes.ADD_MARKER_SET_UPDATES](state: S, updates: Map<string, DynmapMarkerSetUpdates>): void [MutationTypes.ADD_MARKER_SET_UPDATES](state: S, updates: Map<string, DynmapMarkerSetUpdates>): void
[MutationTypes.ADD_TILE_UPDATES](state: S, updates: Array<DynmapTileUpdate>): void [MutationTypes.ADD_TILE_UPDATES](state: S, updates: Array<DynmapTileUpdate>): void
[MutationTypes.ADD_CHAT](state: State, chat: Array<DynmapChat>): void
[MutationTypes.POP_MARKER_UPDATES](state: S, payload: {markerSet: string, amount: number}): Array<DynmapMarkerUpdate> [MutationTypes.POP_MARKER_UPDATES](state: S, payload: {markerSet: string, amount: number}): Array<DynmapMarkerUpdate>
[MutationTypes.POP_AREA_UPDATES](state: S, payload: {markerSet: string, amount: number}): Array<DynmapAreaUpdate> [MutationTypes.POP_AREA_UPDATES](state: S, payload: {markerSet: string, amount: number}): Array<DynmapAreaUpdate>
@ -194,6 +195,10 @@ export const mutations: MutationTree<State> & Mutations = {
state.pendingTileUpdates = state.pendingTileUpdates.concat(updates); state.pendingTileUpdates = state.pendingTileUpdates.concat(updates);
}, },
[MutationTypes.ADD_CHAT](state: State, chat: Array<DynmapChat>) {
state.chat.unshift(...chat);
},
[MutationTypes.POP_MARKER_UPDATES](state: State, {markerSet, amount}): Array<DynmapMarkerUpdate> { [MutationTypes.POP_MARKER_UPDATES](state: State, {markerSet, amount}): Array<DynmapMarkerUpdate> {
if(!state.markerSets.has(markerSet)) { if(!state.markerSets.has(markerSet)) {
console.log(`Marker set ${markerSet} doesn't exist`); console.log(`Marker set ${markerSet} doesn't exist`);

View File

@ -20,7 +20,7 @@ import {
DynmapMessageConfig, DynmapMessageConfig,
DynmapPlayer, DynmapPlayer,
DynmapServerConfig, DynmapTileUpdate, DynmapServerConfig, DynmapTileUpdate,
DynmapWorld, DynmapWorldState, Coordinate, DynmapParsedUrl DynmapWorld, DynmapWorldState, Coordinate, DynmapParsedUrl, DynmapChat
} from "@/dynmap"; } from "@/dynmap";
import {DynmapProjection} from "@/leaflet/projection/DynmapProjection"; import {DynmapProjection} from "@/leaflet/projection/DynmapProjection";
@ -33,6 +33,7 @@ export type State = {
maps: Map<string, DynmapWorldMap>; maps: Map<string, DynmapWorldMap>;
players: Map<string, DynmapPlayer>; players: Map<string, DynmapPlayer>;
markerSets: Map<string, DynmapMarkerSet>; markerSets: Map<string, DynmapMarkerSet>;
chat: DynmapChat[];
pendingSetUpdates: Map<string, DynmapMarkerSetUpdates>; pendingSetUpdates: Map<string, DynmapMarkerSetUpdates>;
pendingTileUpdates: Array<DynmapTileUpdate>; pendingTileUpdates: Array<DynmapTileUpdate>;
@ -88,6 +89,7 @@ export const state: State = {
worlds: new Map(), //Defined (loaded) worlds with maps from configuration.json worlds: new Map(), //Defined (loaded) worlds with maps from configuration.json
maps: new Map(), //Defined maps from configuration.json maps: new Map(), //Defined maps from configuration.json
players: new Map(), //Online players from world.json players: new Map(), //Online players from world.json
chat: [],
markerSets: new Map(), //Markers from world_markers.json. Contents of each set isn't reactive for performance reasons. markerSets: new Map(), //Markers from world_markers.json. Contents of each set isn't reactive for performance reasons.
pendingSetUpdates: new Map(), //Pending updates to markers/areas/etc for each marker set pendingSetUpdates: new Map(), //Pending updates to markers/areas/etc for each marker set