diff --git a/src/App.vue b/src/App.vue index f386eaf..d051c00 100644 --- a/src/App.vue +++ b/src/App.vue @@ -17,12 +17,14 @@ diff --git a/src/api.ts b/src/api.ts index 065b6c0..32cd0bb 100644 --- a/src/api.ts +++ b/src/api.ts @@ -16,21 +16,25 @@ import { DynmapArea, + DynmapChat, DynmapCircle, DynmapComponentConfig, DynmapConfigurationResponse, DynmapLine, - DynmapWorldMap, DynmapMarker, DynmapMarkerSet, DynmapMarkerSetUpdates, DynmapMessageConfig, DynmapPlayer, - DynmapServerConfig, DynmapTileUpdate, DynmapUpdate, - DynmapUpdateResponse, DynmapUpdates, - DynmapWorld + DynmapServerConfig, + DynmapTileUpdate, + DynmapUpdate, + DynmapUpdateResponse, + DynmapUpdates, + DynmapWorld, + DynmapWorldMap } from "@/dynmap"; -import { Sanitizer } from "@esri/arcgis-html-sanitizer"; +import {Sanitizer} from "@esri/arcgis-html-sanitizer"; import {useStore} from "@/store"; const sanitizer = new Sanitizer(); @@ -319,7 +323,7 @@ function buildUpdates(data: Array): DynmapUpdates { const updates = { markerSets: new Map(), tiles: [] as DynmapTileUpdate[], - chat: [], + chat: [] as DynmapChat[], }, dropped = { stale: 0, @@ -327,7 +331,7 @@ function buildUpdates(data: Array): DynmapUpdates { noId: 0, unknownType: 0, unknownCType: 0, - incompleteTile: 0, + incomplete: 0, notImplemented: 0, }, lastUpdate = useStore().state.updateTimestamp; @@ -394,13 +398,33 @@ function buildUpdates(data: Array): DynmapUpdates { } case 'chat': - //TODO - dropped.notImplemented++; + if(!entry.account || !entry.message || !entry.timestamp) { + 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; case 'tile': if(!entry.name || !entry.timestamp) { - dropped.incompleteTile++; + dropped.incomplete++; continue; } @@ -422,6 +446,10 @@ function buildUpdates(data: Array): DynmapUpdates { } } + updates.chat = updates.chat.sort((one, two) => { + return one.timestamp - two.timestamp; + }); + console.debug(`Updates: ${accepted} accepted. Rejected: `, dropped); return updates; diff --git a/src/assets/icons/chat.svg b/src/assets/icons/chat.svg new file mode 100644 index 0000000..9a3e4ca --- /dev/null +++ b/src/assets/icons/chat.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/components/Chat.vue b/src/components/Chat.vue new file mode 100644 index 0000000..f6b6a58 --- /dev/null +++ b/src/components/Chat.vue @@ -0,0 +1,43 @@ + + + + + + + \ No newline at end of file diff --git a/src/components/Map.vue b/src/components/Map.vue index bc4f4c2..4201793 100644 --- a/src/components/Map.vue +++ b/src/components/Map.vue @@ -24,6 +24,7 @@ + @@ -37,6 +38,7 @@ import MarkerSetLayer from "@/components/map/layer/MarkerSetLayer.vue"; import CoordinatesControl from "@/components/map/control/CoordinatesControl.vue"; import ClockControl from "@/components/map/control/ClockControl.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 {MutationTypes} from "@/store/mutation-types"; import {Coordinate, DynmapPlayer} from "@/dynmap"; @@ -52,6 +54,7 @@ export default defineComponent({ CoordinatesControl, ClockControl, LinkControl, + ChatControl, LogoControl }, @@ -67,6 +70,7 @@ export default defineComponent({ coordinatesControlEnabled = computed(() => store.getters.coordinatesControlEnabled), clockControlEnabled = computed(() => store.getters.clockControlEnabled), linkControlEnabled = computed(() => store.state.components.linkControl), + chatEnabled = computed(() => store.state.components.chat), logoControls = computed(() => store.state.components.logoControls), currentWorld = computed(() => store.state.currentWorld), @@ -86,6 +90,7 @@ export default defineComponent({ coordinatesControlEnabled, clockControlEnabled, linkControlEnabled, + chatEnabled, logoControls, followTarget, panTarget, diff --git a/src/components/map/control/ChatControl.vue b/src/components/map/control/ChatControl.vue new file mode 100644 index 0000000..c54c74a --- /dev/null +++ b/src/components/map/control/ChatControl.vue @@ -0,0 +1,43 @@ + + + diff --git a/src/dynmap.d.ts b/src/dynmap.d.ts index d3cb476..72ab80a 100644 --- a/src/dynmap.d.ts +++ b/src/dynmap.d.ts @@ -79,6 +79,7 @@ interface DynmapComponentConfig { clockControl ?: ClockControlOptions; linkControl: boolean; logoControls: Array; + chat?: DynmapChatConfig; } interface DynmapMarkersConfig { @@ -95,6 +96,13 @@ interface DynmapPlayerMarkersConfig { smallFaces: boolean; } +interface DynmapChatConfig { + allowUrlName: boolean; + showPlayerFaces: boolean; + messageLifetime: number; + messageHistory: number; +} + interface DynmapWorld { seaLevel: number; name: string; @@ -274,4 +282,13 @@ interface DynmapParsedUrl { map?: string; location?: Coordinate; zoom?: number; -} \ No newline at end of file +} + +interface DynmapChat { + type: 'chat' | 'playerjoin' | 'playerleave'; + account: string; + channel?: string; + message?: string; + // source?: string; + timestamp: number; +} diff --git a/src/leaflet/control/ChatControl.ts b/src/leaflet/control/ChatControl.ts new file mode 100644 index 0000000..dc6825b --- /dev/null +++ b/src/leaflet/control/ChatControl.ts @@ -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 = ` + + + `; + + return chatButton; + } +} diff --git a/src/scss/leaflet/_controls.scss b/src/scss/leaflet/_controls.scss index 2f13fd9..1870a87 100644 --- a/src/scss/leaflet/_controls.scss +++ b/src/scss/leaflet/_controls.scss @@ -70,7 +70,9 @@ } .leaflet-control-link, -.leaflet-control-loading { +.leaflet-control-chat, +.leaflet-control-loading, +.leaflet-control-layers-toggle { @extend %button; width: 5rem; height: 5rem; @@ -150,13 +152,17 @@ .leaflet-top { padding-top: 1rem; flex-direction: column; + top: 0; + bottom: 7rem; + align-items: flex-start; .leaflet-control { order: 2; min-width: 5rem; + margin-top: 1rem; - & + .leaflet-control { - margin-top: 1rem; + &:first-child { + margin-top: 0; } } @@ -179,6 +185,11 @@ margin-top: 1rem !important; } } + + .leaflet-control-chat { + margin-top: auto; + order: 1000; + } } .leaflet-bottom { diff --git a/src/scss/style.scss b/src/scss/style.scss index 12a55f1..f91d0dc 100644 --- a/src/scss/style.scss +++ b/src/scss/style.scss @@ -425,20 +425,29 @@ button { */ .chat { + @extend %panel; position: absolute; - bottom: 0; - left: 32px; - z-index:50; + bottom: 7rem; + left: 7rem; + width: 50rem; + max-width: calc(100% - 8rem); + max-height: 20rem; + display: flex; + box-sizing: border-box; - border-color: rgba(0,0,0,0.5); - background: rgba(0,0,0,0.6); + .chat__messages { + display: flex; + flex-direction: column-reverse; + list-style: none; + overflow: auto; + margin: 0; + padding: 0; - border-style: solid; - border-width: 1px 1px 0 1px; - - border-radius: 3px 3px 0 0; - - margin-left: 10px; + .message { + font-size: 1.6rem; + margin-bottom: 0.5rem + } + } } .chatinput { diff --git a/src/store/actions.ts b/src/store/actions.ts index 1301c9f..025b363 100644 --- a/src/store/actions.ts +++ b/src/store/actions.ts @@ -136,6 +136,7 @@ export const actions: ActionTree & Actions = { commit(MutationTypes.INCREMENT_REQUEST_ID, undefined); commit(MutationTypes.ADD_MARKER_SET_UPDATES, update.updates.markerSets); commit(MutationTypes.ADD_TILE_UPDATES, update.updates.tiles); + commit(MutationTypes.ADD_CHAT, update.updates.chat); return dispatch(ActionTypes.SET_PLAYERS, update.players).then(() => { return update; diff --git a/src/store/mutation-types.ts b/src/store/mutation-types.ts index 3de17ca..a19ecce 100644 --- a/src/store/mutation-types.ts +++ b/src/store/mutation-types.ts @@ -25,6 +25,7 @@ export enum MutationTypes { SET_UPDATE_TIMESTAMP = 'setUpdateTimestamp', ADD_MARKER_SET_UPDATES = 'addMarkerSetUpdates', ADD_TILE_UPDATES = 'addTileUpdates', + ADD_CHAT = 'addChat', POP_MARKER_UPDATES = 'popMarkerUpdates', POP_AREA_UPDATES = 'popAreaUpdates', POP_CIRCLE_UPDATES = 'popCircleUpdates', diff --git a/src/store/mutations.ts b/src/store/mutations.ts index b8858c7..0ac5f02 100644 --- a/src/store/mutations.ts +++ b/src/store/mutations.ts @@ -33,7 +33,7 @@ import { DynmapPlayer, DynmapServerConfig, DynmapTileUpdate, DynmapWorld, - DynmapWorldState, DynmapParsedUrl + DynmapWorldState, DynmapParsedUrl, DynmapChat } from "@/dynmap"; import {DynmapProjection} from "@/leaflet/projection/DynmapProjection"; @@ -53,6 +53,7 @@ export type Mutations = { [MutationTypes.SET_UPDATE_TIMESTAMP](state: S, time: Date): void [MutationTypes.ADD_MARKER_SET_UPDATES](state: S, updates: Map): void [MutationTypes.ADD_TILE_UPDATES](state: S, updates: Array): void + [MutationTypes.ADD_CHAT](state: State, chat: Array): void [MutationTypes.POP_MARKER_UPDATES](state: S, payload: {markerSet: string, amount: number}): Array [MutationTypes.POP_AREA_UPDATES](state: S, payload: {markerSet: string, amount: number}): Array @@ -194,6 +195,10 @@ export const mutations: MutationTree & Mutations = { state.pendingTileUpdates = state.pendingTileUpdates.concat(updates); }, + [MutationTypes.ADD_CHAT](state: State, chat: Array) { + state.chat.unshift(...chat); + }, + [MutationTypes.POP_MARKER_UPDATES](state: State, {markerSet, amount}): Array { if(!state.markerSets.has(markerSet)) { console.log(`Marker set ${markerSet} doesn't exist`); diff --git a/src/store/state.ts b/src/store/state.ts index 7dff6cd..75dc6c0 100644 --- a/src/store/state.ts +++ b/src/store/state.ts @@ -20,7 +20,7 @@ import { DynmapMessageConfig, DynmapPlayer, DynmapServerConfig, DynmapTileUpdate, - DynmapWorld, DynmapWorldState, Coordinate, DynmapParsedUrl + DynmapWorld, DynmapWorldState, Coordinate, DynmapParsedUrl, DynmapChat } from "@/dynmap"; import {DynmapProjection} from "@/leaflet/projection/DynmapProjection"; @@ -33,6 +33,7 @@ export type State = { maps: Map; players: Map; markerSets: Map; + chat: DynmapChat[]; pendingSetUpdates: Map; pendingTileUpdates: Array; @@ -88,6 +89,7 @@ export const state: State = { 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 + chat: [], 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