Skip to content

Commit 1855123

Browse files
committed
wip chat
1 parent fba94da commit 1855123

File tree

12 files changed

+688
-124
lines changed

12 files changed

+688
-124
lines changed

config/ng-extend.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22
"volumes_url": "middleauth+https://global.daf-apis.com/info/api/v2/ngl_info",
33
"volumes_enabled": ["stroeh_mouse_retina"],
44
"default_state": "middleauth+https://global.daf-apis.com/nglstate/api/v1/6646116310319104",
5-
"leaderboard_url": "https://pyrdev.eyewire.org/eyewire2-leaderboard"
5+
"leaderboard_url": "https://pyrdev.eyewire.org/eyewire2-leaderboard",
6+
"chat_url": "wss://pyrdev.eyewire.org/chat"
67
}

package-lock.json

Lines changed: 152 additions & 90 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
"dependencies": {
1111
"neuroglancer": "github:seung-lab/neuroglancer#4a27ed47ebea434f1804cffbb60a4fe5bd471b66",
1212
"pinia": "^2.1.7",
13-
"vue": "^3.4.21"
13+
"reconnecting-websocket": "^4.4.0",
14+
"vue": "^3.5.13"
1415
},
1516
"browserslist": [
1617
"last 2 Chrome versions",

src/Chatbox.vue

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
<script setup lang="ts">
2+
import { onMounted, onUnmounted, useTemplateRef } from "vue";
3+
import { storeToRefs } from "pinia";
4+
import { useChatStore, useStatsStore } from '#src/store-pyr.js';
5+
import HologramPanel from "#src/components/HologramPanel.vue";
6+
7+
//import sendImage from '../images/send.svg';
8+
//import chevronImage from '../images/chevron.svg';
9+
10+
const scrollEl = useTemplateRef<HTMLElement>('scrollEl');
11+
12+
const store = useChatStore();
13+
const { chatMessages, unreadMessages } = storeToRefs(store);
14+
const { sendMessage, markLastMessageRead } = store;
15+
const { leaderboardEntries } = storeToRefs(useStatsStore());
16+
17+
/*function encodeSVG(svg: string) {
18+
return "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg);
19+
}*/
20+
21+
onMounted(() => {
22+
//(document.querySelector(".nge-chatbox-messageprompt > img")! as HTMLImageElement).src = encodeSVG(chevronImage);
23+
//(document.querySelector(".nge-chatbox-submit > button > img")! as HTMLImageElement).src = encodeSVG(sendImage);
24+
25+
scrollEl.value?.addEventListener("scroll", handleScroll);
26+
});
27+
28+
onUnmounted(() => {
29+
// const scrollEl = document.querySelector(".nge-chatbox-scroll")!;
30+
scrollEl.value?.removeEventListener("scroll", handleScroll);
31+
});
32+
33+
function submitMessage() {
34+
const messageEl = <HTMLInputElement>(
35+
document.getElementById("chatMessage")
36+
);
37+
const message = messageEl.value;
38+
messageEl.value = "";
39+
if (message.trim() !== "") {
40+
sendMessage(message);
41+
}
42+
}
43+
44+
function getTrophy(name: string): string {
45+
const places: string[] = ["🥇", "🥈", "🥉"];
46+
for (let i = 0; i < places.length; i++) {
47+
if (leaderboardEntries.value.length > i && leaderboardEntries.value[i].name === name) {
48+
return places[i];
49+
}
50+
}
51+
return "";
52+
}
53+
54+
function scrollToBottom() {
55+
const el = scrollEl.value;
56+
if (el) {
57+
el.scrollTo(0, el.scrollHeight);
58+
}
59+
}
60+
61+
function handleScroll() {
62+
const el = scrollEl.value;
63+
if (el) {
64+
const scrollAtBottom = Math.ceil(el.scrollTop) + el.offsetHeight >= el.scrollHeight;
65+
if (scrollAtBottom) {
66+
markLastMessageRead();
67+
}
68+
}
69+
}
70+
71+
</script>
72+
73+
<template>
74+
<hologram-panel class="nge-chatbox-hologram" id="chatbox-hologram">
75+
<div class="nge-chatbox" tabindex="1">
76+
<button class="exit" @click="$emit('hide')">×</button>
77+
<div class="nge-chatbox-filler"></div>
78+
<div class="nge-chatbox-grid">
79+
<div ref="scrollEl" class="nge-chatbox-scroll">
80+
<div class="nge-chatbox-messages">
81+
<span class="nge-chatbox-item" v-for="(message, index) of chatMessages"
82+
:key="'message' + index">
83+
<div class="nge-chatbox-info" v-if="message.type === 'users'">
84+
<div class="nge-chatbox-info-content">{{ message.name }}</div>
85+
<div class="nge-chatbox-info-content">Type !help to see available commands.</div>
86+
</div>
87+
88+
<div class="nge-chatbox-info" v-if="message.type === 'join'">
89+
<div class="nge-chatbox-info-content">{{ message.name }} joined chat.</div>
90+
</div>
91+
92+
<div class="nge-chatbox-info" v-if="message.type === 'leave'">
93+
<div class="nge-chatbox-info-content">{{ message.name }} left chat.</div>
94+
</div>
95+
96+
<div class="nge-chatbox-info" v-if="message.type === 'disconnected'">
97+
<div class="nge-chatbox-info-content">Your message was not sent because you were
98+
disconnected from chat. Try reloading the page.</div>
99+
</div>
100+
101+
<div class="nge-chatbox-time" v-if="message.type === 'time'">{{ message.time }}</div>
102+
103+
<div class="nge-chatbox-message" v-if="message.type === 'message'" :title="message.time">
104+
<span v-for="(part, partIndex) of message.parts"
105+
:key="'messagepart' + index + '-' + partIndex">
106+
<span v-if="part.type === 'sender'"
107+
:class="'nge-chatbox-message-text sender ' + message.rank">{{ part.text }}{{
108+
getTrophy(message.name) }}:
109+
</span>
110+
<span v-if="part.type === 'text'" class="nge-chatbox-message-text">{{ part.text
111+
}}</span>
112+
<a v-if="part.type === 'link'" class="nge-chatbox-message-text" target="_blank"
113+
:href="part.text">{{ part.text }}</a>
114+
</span>
115+
</div>
116+
</span>
117+
</div>
118+
</div>
119+
</div>
120+
<form class="nge-chatbox-sendmessage" @submit.prevent="submitMessage" autocomplete="off">
121+
<!--<div class="nge-chatbox-messageprompt"><img src="insert-svg" width="15" style="transform: rotate(90deg);" /></div>-->
122+
<div class="nge-chatbox-inputbox"><input type="text" id="chatMessage" placeholder=">" /></div>
123+
<!--<div class="nge-chatbox-submit"><button type="submit"><img src="insert-svg" width="15" /></button></div>-->
124+
</form>
125+
<div class="nge-chatbox-unread" v-show="unreadMessages" @click="scrollToBottom()">NEW MESSAGES</div>
126+
</div>
127+
</hologram-panel>
128+
</template>
129+
130+
<style>
131+
.nge-chatbox-hologram {
132+
width: 250px;
133+
height: 300px;
134+
bottom: 10px;
135+
left: 10px;
136+
}
137+
138+
.nge-chatbox {
139+
min-height: 0;
140+
}
141+
142+
.nge-chatbox-title {
143+
/*background-color: #000;*/
144+
font-size: 1.15em;
145+
padding: 10px;
146+
display: grid;
147+
grid-template-columns: auto min-content min-content;
148+
}
149+
150+
.nge-chatbox-title-button {
151+
align-self: end;
152+
}
153+
154+
.nge-chatbox-title-button>img {
155+
padding-left: 10px;
156+
padding-right: 10px;
157+
}
158+
159+
.nge-chatbox-grid {
160+
display: grid;
161+
height: 250px;
162+
margin-top: 16px;
163+
}
164+
165+
.nge-chatbox-scroll {
166+
overflow: auto;
167+
}
168+
169+
.nge-chatbox-messages {
170+
font-size: 0.75em;
171+
padding-top: 10px;
172+
padding-left: 15px;
173+
padding-right: 15px;
174+
}
175+
176+
.nge-chatbox-message {
177+
overflow-wrap: break-word;
178+
padding-top: 0.5em;
179+
padding-bottom: 0.15em;
180+
}
181+
182+
.nge-chatbox-message-text.sender {
183+
font-weight: bold;
184+
}
185+
186+
.nge-chatbox-message-text.sender.admin {
187+
color: #E6C760;
188+
}
189+
190+
.nge-chatbox-message-text.sender.eyewirer {
191+
color: #0292AE;
192+
}
193+
194+
.nge-chatbox-message-text.sender.researcher {
195+
color: #0FB18B;
196+
}
197+
198+
.nge-chatbox-message-text {
199+
color: white;
200+
}
201+
202+
.nge-chatbox-info-content {
203+
padding: 0.5em;
204+
/*text-align: center;*/
205+
font-style: italic;
206+
}
207+
208+
.nge-chatbox-time {
209+
padding-top: 0.15em;
210+
text-align: center;
211+
font-size: 0.85em;
212+
}
213+
214+
.nge-chatbox-sendmessage {
215+
align-self: end;
216+
z-index: 90;
217+
display: grid;
218+
grid-template-columns: min-content auto min-content;
219+
padding-top: 5px;
220+
padding-left: 10px;
221+
padding-right: 10px;
222+
padding-bottom: 12px;
223+
}
224+
225+
.nge-chatbox-messageprompt {
226+
padding-top: 5px;
227+
}
228+
229+
.nge-chatbox-sendmessage button {
230+
padding: 5px !important;
231+
}
232+
233+
.nge-chatbox-inputbox>input {
234+
color: #fff;
235+
background-color: #2226;
236+
width: 220px;
237+
border: 1px solid #01ffffba;
238+
border-radius: 4px;
239+
}
240+
241+
.nge-chatbox-unread {
242+
z-index: 90;
243+
user-select: none;
244+
background: var(--color-light-bg);
245+
font-size: 0.9em;
246+
text-align: center;
247+
border-radius: 10px;
248+
cursor: pointer;
249+
position: relative;
250+
top: -50px;
251+
}
252+
</style>

src/chat_socket.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import ReconnectingWebSocket from "reconnecting-websocket";
2+
import { Config } from "#src/config.js";
3+
4+
declare const CONFIG: Config | undefined;
5+
6+
let ws: ReconnectingWebSocket | null = null;
7+
8+
export function connectChatSocket() {
9+
if (!CONFIG) return;
10+
console.log("connecting to", CONFIG.chat_url);
11+
ws = new ReconnectingWebSocket(CONFIG.chat_url);
12+
}
13+
14+
export default function getChatSocket(): ReconnectingWebSocket {
15+
if (!ws) {
16+
throw new Error("Chat websocket must be connected before use");
17+
}
18+
return ws!;
19+
}

src/components/App.vue

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
<script setup lang="ts">
2+
import Chatbox from "#src/Chatbox.vue";
23
import ExtensionBar from "#src/components/ExtensionBar.vue";
34
import Leaderboard from "#src/components/Leaderboard.vue";
4-
import { useStatsStore } from "#src/store-pyr.js";
5+
import { useChatStore, useStatsStore } from "#src/store-pyr.js";
56
import { storeToRefs } from "pinia";
6-
const store = useStatsStore();
7-
const { showLeaderboard } = storeToRefs(store);
7+
const { showLeaderboard } = storeToRefs(useStatsStore());
8+
const { showChat } = storeToRefs(useChatStore());
89
</script>
910

1011
<template>
@@ -16,6 +17,7 @@ const { showLeaderboard } = storeToRefs(store);
1617
<div id="content">
1718
<div id="neuroglancer-container"></div>
1819
</div>
20+
<Chatbox v-if="showChat" @hide="showChat = false" class="ng-extend"></Chatbox>
1921
</div>
2022
</template>
2123

src/components/ExtensionBar.vue

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@ import DropdownList from "#src/components/DropdownList.vue";
66
import { loginSession, useLoginStore, useVolumesStore } from "#src/store.js";
77
import logoImage from "#src/eyewire-logo.svg?raw";
88
9-
import { useStatsStore } from "#src/store-pyr.js";
9+
import { useChatStore, useStatsStore } from "#src/store-pyr.js";
1010
import { storeToRefs } from "pinia";
11-
const store = useStatsStore();
12-
const { showLeaderboard } = storeToRefs(store);
11+
const { showLeaderboard } = storeToRefs(useStatsStore());
12+
const { showChat } = storeToRefs(useChatStore());
13+
1314
1415
1516
const login = useLoginStore();
@@ -89,9 +90,14 @@ function logout(session: loginSession) {
8990
<dropdown-list dropdown-group="extension-bar-right" id="hamburger" class="rightMost">
9091
<template #buttonTitle>☰</template>
9192
<template #listItems>
93+
<li v-if="showChat === false">
94+
<div @click="showChat = true" class="logoutButton button">
95+
<span>Show Chat</span>
96+
</div>
97+
</li>
9298
<li v-if="showLeaderboard === false">
93-
<div class="logoutButton button">
94-
<span @click="showLeaderboard = true">Show Leaderboard</span>
99+
<div @click="showLeaderboard = true" class="logoutButton button">
100+
<span>Show Leaderboard</span>
95101
</div>
96102
</li>
97103
<li>

src/components/HologramPanel.vue

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,13 @@ onMounted(() => {
2020
});
2121
2222
function clickHeader(event: MouseEvent) {
23-
event.preventDefault();
2423
pos3 = event.clientX;
2524
pos4 = event.clientY;
2625
document.onmouseup = release;
2726
document.onmousemove = drag;
2827
}
2928
3029
function drag(e: MouseEvent) {
31-
e.preventDefault();
3230
pos1 = pos3 - e.clientX;
3331
pos2 = pos4 - e.clientY;
3432
pos3 = e.clientX;
@@ -82,6 +80,26 @@ function release() {
8280
/*border: 2px solid #a46fe2aa;*/
8381
}
8482
83+
.pyr-hologram-panel button.exit {
84+
position: absolute;
85+
right: 0;
86+
border: none;
87+
padding: 0;
88+
opacity: 0.75;
89+
transition: opacity 0.2s;
90+
font-size: 22px;
91+
line-height: 22px;
92+
font-weight: 300;
93+
width: 22px;
94+
margin: 6px 6px 0 0;
95+
z-index: 1;
96+
}
97+
98+
.ng-extend button.exit:hover {
99+
background-color: initial;
100+
opacity: 1;
101+
}
102+
85103
/*
86104
.pyr-hologram-content {
87105
position: absolute;

0 commit comments

Comments
 (0)