|
|
@@ -7,6 +7,9 @@
|
|
|
<span class="text-sm font-bold">{{ t("chat.title") }}</span>
|
|
|
<span v-if="messages.length" class="text-[10px] bg-primary/10 text-primary px-1.5 py-0.5 rounded-full font-bold">{{ messages.length }}</span>
|
|
|
<span class="w-2 h-2 rounded-full" :class="wsConnected ? 'bg-emerald-500' : 'bg-rose-500'" :title="wsConnected ? 'Connected' : 'Disconnected'" />
|
|
|
+ <span v-if="otherPartyOnline" class="text-[10px] text-emerald-500 font-bold ml-2 flex items-center gap-1 animate-in fade-in zoom-in duration-300">
|
|
|
+ <Eye class="w-3 h-3" /> Online
|
|
|
+ </span>
|
|
|
</div>
|
|
|
<button v-if="closable" @click="$emit('close')" class="p-1 hover:bg-secondary rounded-lg transition-colors">
|
|
|
<X class="w-4 h-4 text-muted-foreground" />
|
|
|
@@ -76,8 +79,9 @@
|
|
|
<script setup lang="ts">
|
|
|
import { ref, onMounted, onUnmounted, nextTick, watch } from "vue";
|
|
|
import { useI18n } from "vue-i18n";
|
|
|
-import { MessageCircle, Send, Loader2, ShieldCheck, X } from "lucide-vue-next";
|
|
|
+import { MessageCircle, Send, Loader2, ShieldCheck, X, Eye } from "lucide-vue-next";
|
|
|
import { getOrderMessages, sendOrderMessage } from "@/lib/api";
|
|
|
+import { useAuthStore } from "@/stores/auth";
|
|
|
|
|
|
const WS_BASE_URL = "ws://localhost:8000";
|
|
|
|
|
|
@@ -97,6 +101,31 @@ const isSending = ref(false);
|
|
|
const wsConnected = ref(false);
|
|
|
const messagesContainer = ref<HTMLElement | null>(null);
|
|
|
const textareaRef = ref<HTMLTextAreaElement | null>(null);
|
|
|
+const otherPartyOnline = ref(false);
|
|
|
+const authStore = useAuthStore();
|
|
|
+
|
|
|
+function playDing() {
|
|
|
+ try {
|
|
|
+ const AudioContext = window.AudioContext || (window as any).webkitAudioContext;
|
|
|
+ if (!AudioContext) return;
|
|
|
+ const ctx = new AudioContext();
|
|
|
+ const osc = ctx.createOscillator();
|
|
|
+ const gainNode = ctx.createGain();
|
|
|
+
|
|
|
+ osc.type = "sine";
|
|
|
+ osc.frequency.setValueAtTime(880, ctx.currentTime);
|
|
|
+ osc.frequency.exponentialRampToValueAtTime(440, ctx.currentTime + 0.1);
|
|
|
+
|
|
|
+ gainNode.gain.setValueAtTime(0.3, ctx.currentTime);
|
|
|
+ gainNode.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.3);
|
|
|
+
|
|
|
+ osc.connect(gainNode);
|
|
|
+ gainNode.connect(ctx.destination);
|
|
|
+
|
|
|
+ osc.start();
|
|
|
+ osc.stop(ctx.currentTime + 0.3);
|
|
|
+ } catch(e) {}
|
|
|
+}
|
|
|
|
|
|
let ws: WebSocket | null = null;
|
|
|
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
|
@@ -115,10 +144,24 @@ function connectWebSocket() {
|
|
|
ws.onmessage = (event) => {
|
|
|
try {
|
|
|
const msg = JSON.parse(event.data);
|
|
|
+ if (msg.type === "presence") {
|
|
|
+ const myRole = authStore.user?.role === 'admin' ? 'admin' : 'user';
|
|
|
+ const otherRole = myRole === 'admin' ? 'user' : 'admin';
|
|
|
+ otherPartyOnline.value = msg.online_roles.includes(otherRole);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
// Avoid duplicates (we already optimistically added our own message)
|
|
|
if (!messages.value.find(m => m.id === msg.id)) {
|
|
|
messages.value.push(msg);
|
|
|
scrollToBottom();
|
|
|
+
|
|
|
+ // Check if we should play a sound
|
|
|
+ const myRole = authStore.user?.role === 'admin' ? 'admin' : 'user';
|
|
|
+ const isMsgFromMe = (msg.is_from_admin && myRole === 'admin') || (!msg.is_from_admin && myRole === 'user');
|
|
|
+ if (!isMsgFromMe) {
|
|
|
+ playDing();
|
|
|
+ }
|
|
|
}
|
|
|
} catch (e) {
|
|
|
console.error("WS parse error:", e);
|