Implement chat balloons
This commit is contained in:
parent
022a5914c2
commit
ac2dd1f399
@ -127,6 +127,7 @@ function buildComponents(response: any): DynmapComponentConfig {
|
|||||||
showLabels: false,
|
showLabels: false,
|
||||||
},
|
},
|
||||||
chat: undefined,
|
chat: undefined,
|
||||||
|
chatBalloons: false,
|
||||||
playerMarkers: undefined,
|
playerMarkers: undefined,
|
||||||
coordinatesControl: undefined,
|
coordinatesControl: undefined,
|
||||||
linkControl: false,
|
linkControl: false,
|
||||||
@ -198,6 +199,10 @@ function buildComponents(response: any): DynmapComponentConfig {
|
|||||||
messageLifetime: component.messagettl || Infinity,
|
messageLifetime: component.messagettl || Infinity,
|
||||||
messageHistory: component.scrollback || Infinity,
|
messageHistory: component.scrollback || Infinity,
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "chatballoon":
|
||||||
|
components.chatBalloons = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -15,11 +15,12 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {defineComponent, computed, ref} from "@vue/runtime-core";
|
import {defineComponent, computed, ref, onMounted, onUnmounted} from "@vue/runtime-core";
|
||||||
import {LayerGroup} from 'leaflet';
|
import {LayerGroup} from 'leaflet';
|
||||||
import {DynmapPlayer} from "@/dynmap";
|
import {DynmapChat, DynmapPlayer} from "@/dynmap";
|
||||||
import {useStore} from "@/store";
|
import {useStore} from "@/store";
|
||||||
import {PlayerMarker} from "@/leaflet/marker/PlayerMarker";
|
import {PlayerMarker} from "@/leaflet/marker/PlayerMarker";
|
||||||
|
import {Popup} from "leaflet";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
@ -33,21 +34,168 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
setup() {
|
setup(props) {
|
||||||
|
let chatBalloonCutoff = 0; //Not reactive to avoid unnecessary playerChat recalculations
|
||||||
|
|
||||||
const store = useStore(),
|
const store = useStore(),
|
||||||
componentSettings = computed(() => store.state.components.playerMarkers),
|
componentSettings = computed(() => store.state.components.playerMarkers),
|
||||||
currentProjection = computed(() => store.state.currentProjection),
|
currentProjection = computed(() => store.state.currentProjection),
|
||||||
currentWorld = computed(() => store.state.currentWorld),
|
currentWorld = computed(() => store.state.currentWorld),
|
||||||
visible = ref(false),
|
chatBalloonsEnabled = computed(() => store.state.components.chatBalloons),
|
||||||
|
|
||||||
marker = undefined as PlayerMarker | undefined;
|
//Whether the marker is currently visible
|
||||||
|
markerVisible = ref(false),
|
||||||
|
|
||||||
|
//The player marker
|
||||||
|
marker = new PlayerMarker(props.player, {
|
||||||
|
smallFace: componentSettings.value!.smallFaces,
|
||||||
|
showSkinFace: componentSettings.value!.showSkinFaces,
|
||||||
|
showBody: componentSettings.value!.showBodies,
|
||||||
|
showHealth: componentSettings.value!.showHealth,
|
||||||
|
interactive: false,
|
||||||
|
pane: 'players',
|
||||||
|
}),
|
||||||
|
|
||||||
|
//Popup for chat messages, if chat balloons are enabled
|
||||||
|
chatBalloon = new Popup({
|
||||||
|
autoClose: false,
|
||||||
|
autoPan: false,
|
||||||
|
keepInView: false,
|
||||||
|
closeButton: false,
|
||||||
|
closeOnEscapeKey: false,
|
||||||
|
closeOnClick: false,
|
||||||
|
className: 'leaflet-popup--chat',
|
||||||
|
minWidth: 0,
|
||||||
|
}),
|
||||||
|
|
||||||
|
chatBalloonVisible = ref(false),
|
||||||
|
|
||||||
|
//Timeout for closing the chat balloon
|
||||||
|
chatBalloonTimeout = ref(0),
|
||||||
|
|
||||||
|
//Cutoff time for chat messages
|
||||||
|
//Only messages newer than this time will be shown in the chat balloon
|
||||||
|
//Used to prevent old seen messages reappearing in some situations
|
||||||
|
|
||||||
|
//Chat messages to show in the popup
|
||||||
|
playerChat = computed(() => {
|
||||||
|
const messages: DynmapChat[] = [];
|
||||||
|
|
||||||
|
if(!chatBalloonsEnabled.value) {
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
for(const message of store.state.chat.messages) {
|
||||||
|
//Stop looking if we reach messages we've already seen
|
||||||
|
if(message.timestamp <= chatBalloonCutoff) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Limit to 5 messages
|
||||||
|
if(messages.length === 5) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(message.type === 'chat' && message.playerAccount === props.player.account) {
|
||||||
|
messages.push(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//If no suitable messages are found, set the cutoff to the newest messages timestamp
|
||||||
|
//This prevents searching the whole message list again for players who don't chat
|
||||||
|
if(!messages.length && store.state.chat.messages.length) {
|
||||||
|
chatBalloonCutoff = store.state.chat.messages[0].timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
return messages;
|
||||||
|
}),
|
||||||
|
|
||||||
|
updateChatBalloon = () => {
|
||||||
|
const content = playerChat.value.reduceRight<string>((previousValue, currentValue) => {
|
||||||
|
return previousValue + `<span>${currentValue.message}</span>`;
|
||||||
|
}, '');
|
||||||
|
|
||||||
|
//Update balloon if content has changed
|
||||||
|
if(content != chatBalloon.getContent() || !chatBalloonVisible.value) {
|
||||||
|
chatBalloon.setContent(content);
|
||||||
|
|
||||||
|
if(!chatBalloonVisible.value) {
|
||||||
|
props.layerGroup.addLayer(chatBalloon);
|
||||||
|
chatBalloonVisible.value = true;
|
||||||
|
|
||||||
|
//Set cutoff to oldest visible message
|
||||||
|
chatBalloonCutoff = playerChat.value[playerChat.value.length - 1].timestamp - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Reset close timer
|
||||||
|
if(chatBalloonTimeout.value) {
|
||||||
|
clearTimeout(chatBalloonTimeout.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
chatBalloonTimeout.value = setTimeout(() => closeChatBalloon(), 8000);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
closeChatBalloon = () => {
|
||||||
|
props.layerGroup.removeLayer(chatBalloon);
|
||||||
|
chatBalloonVisible.value = false;
|
||||||
|
|
||||||
|
//Prevent showing any currently visible chat messages again
|
||||||
|
if(playerChat.value[0]) {
|
||||||
|
chatBalloonCutoff = playerChat.value[0].timestamp;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
enableLayer = () => {
|
||||||
|
if(!markerVisible.value) {
|
||||||
|
const latLng = currentProjection.value.locationToLatLng(props.player.location);
|
||||||
|
|
||||||
|
props.layerGroup.addLayer(marker);
|
||||||
|
marker.setLatLng(latLng);
|
||||||
|
chatBalloon.setLatLng(latLng);
|
||||||
|
markerVisible.value = true;
|
||||||
|
|
||||||
|
//Prevent showing chat messages which were sent while the player was hidden
|
||||||
|
chatBalloonCutoff = new Date().getTime();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
disableLayer = () => {
|
||||||
|
if(markerVisible.value) {
|
||||||
|
props.layerGroup.removeLayer(marker);
|
||||||
|
props.layerGroup.removeLayer(chatBalloon);
|
||||||
|
markerVisible.value = false;
|
||||||
|
|
||||||
|
closeChatBalloon();
|
||||||
|
|
||||||
|
if(chatBalloonTimeout.value) {
|
||||||
|
clearTimeout(chatBalloonTimeout.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if(currentWorld.value && currentWorld.value.name === props.player.location.world) {
|
||||||
|
enableLayer();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => disableLayer());
|
||||||
|
|
||||||
return {
|
return {
|
||||||
marker,
|
|
||||||
visible,
|
|
||||||
componentSettings,
|
componentSettings,
|
||||||
currentProjection,
|
currentProjection,
|
||||||
currentWorld,
|
currentWorld,
|
||||||
|
chatBalloonsEnabled,
|
||||||
|
|
||||||
|
marker,
|
||||||
|
markerVisible,
|
||||||
|
|
||||||
|
chatBalloon,
|
||||||
|
playerChat,
|
||||||
|
updateChatBalloon,
|
||||||
|
|
||||||
|
enableLayer,
|
||||||
|
disableLayer
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -56,72 +204,41 @@ export default defineComponent({
|
|||||||
deep: true,
|
deep: true,
|
||||||
handler(newValue) {
|
handler(newValue) {
|
||||||
if(this.currentWorld && newValue.location.world === this.currentWorld.name) {
|
if(this.currentWorld && newValue.location.world === this.currentWorld.name) {
|
||||||
if(!this.visible) {
|
if(!this.markerVisible) {
|
||||||
this.enableLayer();
|
this.enableLayer();
|
||||||
} else {
|
} else {
|
||||||
this.marker!.setLatLng(this.currentProjection.locationToLatLng(newValue.location));
|
const latLng = this.currentProjection.locationToLatLng(newValue.location);
|
||||||
this.marker!.getIcon().update();
|
|
||||||
|
this.marker.setLatLng(latLng);
|
||||||
|
this.chatBalloon.setLatLng(latLng);
|
||||||
|
this.marker.getIcon().update();
|
||||||
}
|
}
|
||||||
} else if(this.visible) {
|
} else if(this.markerVisible) {
|
||||||
this.disableLayer();
|
this.disableLayer();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
playerChat(newValue: DynmapChat[]) {
|
||||||
|
if(!this.chatBalloonsEnabled || !this.markerVisible || !newValue.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateChatBalloon();
|
||||||
|
},
|
||||||
currentWorld(newValue) {
|
currentWorld(newValue) {
|
||||||
if(newValue.name === this.player.location.world) {
|
if(newValue.name === this.player.location.world) {
|
||||||
this.enableLayer();
|
this.enableLayer();
|
||||||
} else if(this.visible) {
|
} else if(this.markerVisible) {
|
||||||
this.disableLayer();
|
this.disableLayer();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
currentProjection() {
|
currentProjection() {
|
||||||
this.marker!.setLatLng(this.currentProjection.locationToLatLng(this.player.location));
|
this.marker.setLatLng(this.currentProjection.locationToLatLng(this.player.location));
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
mounted() {
|
|
||||||
this.marker = new PlayerMarker(this.player, {
|
|
||||||
smallFace: this.componentSettings!.smallFaces,
|
|
||||||
showSkinFace: this.componentSettings!.showSkinFaces,
|
|
||||||
showBody: this.componentSettings!.showBodies,
|
|
||||||
showHealth: this.componentSettings!.showHealth,
|
|
||||||
interactive: false,
|
|
||||||
pane: 'players',
|
|
||||||
});
|
|
||||||
|
|
||||||
if(this.currentWorld && this.currentWorld.name === this.player.location.world) {
|
|
||||||
this.enableLayer();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
unmounted() {
|
|
||||||
if(this.marker) {
|
|
||||||
this.layerGroup.removeLayer(this.marker);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
|
||||||
enableLayer() {
|
|
||||||
if(this.marker && !this.visible) {
|
|
||||||
this.layerGroup.addLayer(this.marker);
|
|
||||||
this.marker.setLatLng(this.currentProjection.locationToLatLng(this.player.location));
|
|
||||||
this.visible = true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
disableLayer() {
|
|
||||||
if(this.marker && this.visible) {
|
|
||||||
this.layerGroup.removeLayer(this.marker);
|
|
||||||
this.visible = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
1
src/dynmap.d.ts
vendored
1
src/dynmap.d.ts
vendored
@ -81,6 +81,7 @@ interface DynmapComponentConfig {
|
|||||||
linkControl: boolean;
|
linkControl: boolean;
|
||||||
logoControls: Array<LogoControlOptions>;
|
logoControls: Array<LogoControlOptions>;
|
||||||
chat?: DynmapChatConfig;
|
chat?: DynmapChatConfig;
|
||||||
|
chatBalloons: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DynmapMarkersConfig {
|
interface DynmapMarkersConfig {
|
||||||
|
@ -14,16 +14,27 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.leaflet-popup-content-wrapper, .leaflet-popup-tip {
|
.leaflet-popup {
|
||||||
background-color: $global-background;
|
.leaflet-popup-content-wrapper, .leaflet-popup-tip {
|
||||||
color: $global-text-color;
|
background-color: $global-background;
|
||||||
|
color: $global-text-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-popup-content-wrapper {
|
||||||
|
border-radius: $global-border-radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-popup-content {
|
||||||
|
margin: 1.5rem;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.leaflet-popup--chat {
|
||||||
|
.leaflet-popup-content {
|
||||||
|
margin: 0.5rem 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.leaflet-popup-content-wrapper {
|
|
||||||
border-radius: $global-border-radius;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaflet-popup-content {
|
|
||||||
margin: 1.5rem;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
@ -134,6 +134,12 @@ export const state: State = {
|
|||||||
|
|
||||||
//Optional "logo" controls.
|
//Optional "logo" controls.
|
||||||
logoControls: [],
|
logoControls: [],
|
||||||
|
|
||||||
|
//Chat
|
||||||
|
chat: undefined,
|
||||||
|
|
||||||
|
//Show chat messages above player markers
|
||||||
|
chatBalloons: false
|
||||||
},
|
},
|
||||||
|
|
||||||
followTarget: undefined,
|
followTarget: undefined,
|
||||||
|
Loading…
Reference in New Issue
Block a user