watercooler 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/public/app.js +85 -8
- package/public/index.html +269 -8
- package/server.ts +60 -10
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -47,6 +47,13 @@ function init() {
|
|
|
47
47
|
controls.maxPolarAngle = Math.PI / 2 - 0.1;
|
|
48
48
|
controls.minDistance = 20;
|
|
49
49
|
controls.maxDistance = 80;
|
|
50
|
+
controls.enableZoom = true;
|
|
51
|
+
controls.zoomSpeed = 0.8;
|
|
52
|
+
controls.enablePan = false; // Disable pan on touch for better mobile UX
|
|
53
|
+
controls.touches = {
|
|
54
|
+
ONE: THREE.TOUCH.ROTATE,
|
|
55
|
+
TWO: THREE.TOUCH.DOLLY_PAN
|
|
56
|
+
};
|
|
50
57
|
|
|
51
58
|
// Lighting
|
|
52
59
|
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
|
|
@@ -90,6 +97,18 @@ function init() {
|
|
|
90
97
|
mouse = new THREE.Vector2();
|
|
91
98
|
renderer.domElement.addEventListener('click', onHouseClick);
|
|
92
99
|
|
|
100
|
+
// Add touch support for mobile
|
|
101
|
+
renderer.domElement.addEventListener('touchstart', onHouseTouchStart, { passive: false });
|
|
102
|
+
renderer.domElement.addEventListener('touchend', onHouseTouchEnd, { passive: false });
|
|
103
|
+
|
|
104
|
+
// Disable context menu on mobile for better UX
|
|
105
|
+
renderer.domElement.addEventListener('contextmenu', (e) => e.preventDefault());
|
|
106
|
+
|
|
107
|
+
// Handle orientation change
|
|
108
|
+
window.addEventListener('orientationchange', () => {
|
|
109
|
+
setTimeout(onWindowResize, 100);
|
|
110
|
+
});
|
|
111
|
+
|
|
93
112
|
animate();
|
|
94
113
|
}
|
|
95
114
|
|
|
@@ -354,9 +373,21 @@ function animate() {
|
|
|
354
373
|
}
|
|
355
374
|
|
|
356
375
|
function onWindowResize() {
|
|
357
|
-
|
|
376
|
+
const width = window.innerWidth;
|
|
377
|
+
const height = window.innerHeight;
|
|
378
|
+
camera.aspect = width / height;
|
|
358
379
|
camera.updateProjectionMatrix();
|
|
359
|
-
renderer.setSize(
|
|
380
|
+
renderer.setSize(width, height);
|
|
381
|
+
|
|
382
|
+
// Adjust camera position for better mobile view
|
|
383
|
+
if (width < 768) {
|
|
384
|
+
// On mobile, position camera slightly higher and further back
|
|
385
|
+
camera.position.y = Math.max(camera.position.y, 35);
|
|
386
|
+
camera.position.z = Math.max(camera.position.z, 45);
|
|
387
|
+
controls.minDistance = 30; // Prevent zooming too close on mobile
|
|
388
|
+
} else {
|
|
389
|
+
controls.minDistance = 20;
|
|
390
|
+
}
|
|
360
391
|
}
|
|
361
392
|
|
|
362
393
|
// API and UI Functions
|
|
@@ -370,9 +401,14 @@ async function loadData() {
|
|
|
370
401
|
]);
|
|
371
402
|
|
|
372
403
|
config = await configRes.json();
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
404
|
+
const messagesData = await messagesRes.json();
|
|
405
|
+
const recipientsData = await coworkersRes.json();
|
|
406
|
+
const allMessagesData = await allMessagesRes.json();
|
|
407
|
+
|
|
408
|
+
// Validate responses are arrays (not error objects)
|
|
409
|
+
messages = Array.isArray(messagesData) ? messagesData : [];
|
|
410
|
+
recipients = Array.isArray(recipientsData) ? recipientsData : [];
|
|
411
|
+
allMessages = Array.isArray(allMessagesData) ? allMessagesData : [];
|
|
376
412
|
|
|
377
413
|
updateUI();
|
|
378
414
|
updateVillage();
|
|
@@ -391,7 +427,10 @@ window.toggleSendPanel = function() {
|
|
|
391
427
|
|
|
392
428
|
window.toggleMessagesPanel = function() {
|
|
393
429
|
const panel = document.getElementById('messages-panel');
|
|
430
|
+
const btn = document.getElementById('toggle-messages-btn');
|
|
394
431
|
panel.classList.toggle('open');
|
|
432
|
+
btn.style.opacity = panel.classList.contains('open') ? '0' : '1';
|
|
433
|
+
btn.style.pointerEvents = panel.classList.contains('open') ? 'none' : 'auto';
|
|
395
434
|
};
|
|
396
435
|
|
|
397
436
|
function updateUI() {
|
|
@@ -524,9 +563,47 @@ async function sendMessage() {
|
|
|
524
563
|
|
|
525
564
|
// House click handler
|
|
526
565
|
function onHouseClick(event) {
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
566
|
+
handleHouseInteraction(event.clientX, event.clientY);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Touch handlers for mobile
|
|
570
|
+
let touchStartX = 0;
|
|
571
|
+
let touchStartY = 0;
|
|
572
|
+
|
|
573
|
+
function onHouseTouchStart(event) {
|
|
574
|
+
if (event.touches.length === 1) {
|
|
575
|
+
touchStartX = event.touches[0].clientX;
|
|
576
|
+
touchStartY = event.touches[0].clientY;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function onHouseTouchEnd(event) {
|
|
581
|
+
if (event.changedTouches.length === 1) {
|
|
582
|
+
const touchEndX = event.changedTouches[0].clientX;
|
|
583
|
+
const touchEndY = event.changedTouches[0].clientY;
|
|
584
|
+
|
|
585
|
+
// Check if touch moved significantly (if so, it's a drag/pan, not a tap)
|
|
586
|
+
const moveDistance = Math.sqrt(
|
|
587
|
+
Math.pow(touchEndX - touchStartX, 2) +
|
|
588
|
+
Math.pow(touchEndY - touchStartY, 2)
|
|
589
|
+
);
|
|
590
|
+
|
|
591
|
+
// Only trigger if touch didn't move much (tap vs swipe)
|
|
592
|
+
if (moveDistance < 20) {
|
|
593
|
+
handleHouseInteraction(touchEndX, touchEndY);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Common house interaction handler
|
|
599
|
+
function handleHouseInteraction(clientX, clientY) {
|
|
600
|
+
// Calculate normalized device coordinates
|
|
601
|
+
const rect = renderer.domElement.getBoundingClientRect();
|
|
602
|
+
const x = ((clientX - rect.left) / rect.width) * 2 - 1;
|
|
603
|
+
const y = -((clientY - rect.top) / rect.height) * 2 + 1;
|
|
604
|
+
|
|
605
|
+
mouse.x = x;
|
|
606
|
+
mouse.y = y;
|
|
530
607
|
|
|
531
608
|
raycaster.setFromCamera(mouse, camera);
|
|
532
609
|
|
package/public/index.html
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>Watercooler
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
|
6
|
+
<title>Watercooler</title>
|
|
7
7
|
<style>
|
|
8
8
|
* {
|
|
9
9
|
margin: 0;
|
|
@@ -320,7 +320,7 @@
|
|
|
320
320
|
font-size: 0.9rem;
|
|
321
321
|
font-weight: 600;
|
|
322
322
|
cursor: pointer;
|
|
323
|
-
z-index:
|
|
323
|
+
z-index: 101;
|
|
324
324
|
transition: all 0.3s ease;
|
|
325
325
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
|
326
326
|
}
|
|
@@ -382,6 +382,239 @@
|
|
|
382
382
|
border-radius: 3px;
|
|
383
383
|
}
|
|
384
384
|
|
|
385
|
+
/* Mobile responsive styles */
|
|
386
|
+
@media (max-width: 768px) {
|
|
387
|
+
/* Send panel - full width at bottom */
|
|
388
|
+
.send-panel {
|
|
389
|
+
top: auto;
|
|
390
|
+
bottom: 0;
|
|
391
|
+
left: 0;
|
|
392
|
+
right: 0;
|
|
393
|
+
width: 100%;
|
|
394
|
+
max-width: 100%;
|
|
395
|
+
border-radius: 16px 16px 0 0;
|
|
396
|
+
border-bottom: none;
|
|
397
|
+
border-left: none;
|
|
398
|
+
border-right: none;
|
|
399
|
+
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.3);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.send-panel.collapsed {
|
|
403
|
+
width: 100%;
|
|
404
|
+
min-width: 100%;
|
|
405
|
+
transform: translateY(calc(100% - 75px));
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.send-header {
|
|
409
|
+
padding: 14px 20px;
|
|
410
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
.send-header h2 {
|
|
414
|
+
font-size: 1rem;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
.collapse-btn {
|
|
418
|
+
font-size: 1.4rem;
|
|
419
|
+
padding: 8px 16px;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
.send-form {
|
|
423
|
+
padding: 20px;
|
|
424
|
+
max-height: 50vh;
|
|
425
|
+
overflow-y: auto;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
.recipient-select, .message-input, .send-btn {
|
|
429
|
+
font-size: 16px; /* Prevents zoom on iOS */
|
|
430
|
+
padding: 14px 16px;
|
|
431
|
+
margin-bottom: 12px;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
.message-input {
|
|
435
|
+
min-height: 80px;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/* Messages panel - nearly full screen on mobile */
|
|
439
|
+
.messages-panel {
|
|
440
|
+
top: auto;
|
|
441
|
+
bottom: 0;
|
|
442
|
+
left: 0;
|
|
443
|
+
right: 0;
|
|
444
|
+
width: 100%;
|
|
445
|
+
max-width: 100%;
|
|
446
|
+
max-height: 90vh;
|
|
447
|
+
height: 90vh;
|
|
448
|
+
border-radius: 16px 16px 0 0;
|
|
449
|
+
transform: translateY(100%);
|
|
450
|
+
transition: transform 0.3s ease;
|
|
451
|
+
display: flex;
|
|
452
|
+
flex-direction: column;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
.messages-panel.open {
|
|
456
|
+
right: 0;
|
|
457
|
+
transform: translateY(0);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
.messages-header {
|
|
461
|
+
padding: 20px 24px;
|
|
462
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
463
|
+
flex-shrink: 0;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
.messages-header h2 {
|
|
467
|
+
font-size: 1.2rem;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
.close-btn {
|
|
471
|
+
font-size: 1.8rem;
|
|
472
|
+
padding: 8px;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
.messages-container {
|
|
476
|
+
padding: 20px;
|
|
477
|
+
flex: 1;
|
|
478
|
+
max-height: calc(90vh - 80px);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
.message-card {
|
|
482
|
+
padding: 16px 16px 16px 24px;
|
|
483
|
+
margin-bottom: 12px;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
.message-sender {
|
|
487
|
+
font-size: 0.9rem;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
.message-time {
|
|
491
|
+
font-size: 0.75rem;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
.message-text {
|
|
495
|
+
font-size: 0.9rem;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/* Toggle messages button - repositioned */
|
|
499
|
+
.toggle-messages-btn {
|
|
500
|
+
top: 20px;
|
|
501
|
+
right: 20px;
|
|
502
|
+
padding: 14px 18px;
|
|
503
|
+
font-size: 1rem;
|
|
504
|
+
border-radius: 30px;
|
|
505
|
+
z-index: 101;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/* Toast notification - centered */
|
|
509
|
+
.toast {
|
|
510
|
+
left: 20px;
|
|
511
|
+
right: 20px;
|
|
512
|
+
bottom: auto;
|
|
513
|
+
top: 50%;
|
|
514
|
+
transform: translateY(-50%) translateY(20px);
|
|
515
|
+
text-align: center;
|
|
516
|
+
padding: 16px 24px;
|
|
517
|
+
font-size: 1rem;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
.toast.show {
|
|
521
|
+
transform: translateY(-50%) translateY(0);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/* House dialog - full screen on mobile */
|
|
525
|
+
.house-dialog-content {
|
|
526
|
+
width: 100%;
|
|
527
|
+
max-width: 100%;
|
|
528
|
+
max-height: 95vh;
|
|
529
|
+
height: 95vh;
|
|
530
|
+
border-radius: 16px 16px 0 0;
|
|
531
|
+
margin: 0;
|
|
532
|
+
position: fixed;
|
|
533
|
+
bottom: 0;
|
|
534
|
+
left: 0;
|
|
535
|
+
right: 0;
|
|
536
|
+
display: flex;
|
|
537
|
+
flex-direction: column;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
.house-dialog-header {
|
|
541
|
+
padding: 20px 24px;
|
|
542
|
+
flex-shrink: 0;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
.house-dialog-header h2 {
|
|
546
|
+
font-size: 1.2rem;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
.house-dialog-tabs {
|
|
550
|
+
padding: 0 12px;
|
|
551
|
+
flex-shrink: 0;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
.tab-btn {
|
|
555
|
+
padding: 16px 12px;
|
|
556
|
+
font-size: 0.9rem;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
.house-dialog-body {
|
|
560
|
+
padding: 20px;
|
|
561
|
+
max-height: calc(95vh - 140px);
|
|
562
|
+
flex: 1;
|
|
563
|
+
overflow-y: auto;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
.house-dialog-body .message-card {
|
|
567
|
+
padding: 16px 16px 16px 24px;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/* Empty state */
|
|
571
|
+
.empty-state {
|
|
572
|
+
padding: 60px 20px;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
.empty-state p {
|
|
576
|
+
font-size: 1rem;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/* Extra small screens */
|
|
581
|
+
@media (max-width: 480px) {
|
|
582
|
+
.send-panel.collapsed {
|
|
583
|
+
transform: translateY(calc(100% - 70px));
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
.send-header {
|
|
587
|
+
padding: 12px 16px;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
.send-header h2 {
|
|
591
|
+
font-size: 0.9rem;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
.send-form {
|
|
595
|
+
padding: 16px;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
.recipient-select, .message-input, .send-btn {
|
|
599
|
+
font-size: 16px;
|
|
600
|
+
padding: 12px 14px;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
.toggle-messages-btn {
|
|
604
|
+
padding: 12px 16px;
|
|
605
|
+
font-size: 0.9rem;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
.messages-panel {
|
|
609
|
+
max-height: 75vh;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
.tab-btn {
|
|
613
|
+
font-size: 0.8rem;
|
|
614
|
+
padding: 12px 8px;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
385
618
|
/* House Dialog */
|
|
386
619
|
.house-dialog {
|
|
387
620
|
display: none;
|
|
@@ -392,7 +625,7 @@
|
|
|
392
625
|
bottom: 0;
|
|
393
626
|
background: rgba(0, 0, 0, 0.7);
|
|
394
627
|
z-index: 300;
|
|
395
|
-
align-items:
|
|
628
|
+
align-items: flex-end;
|
|
396
629
|
justify-content: center;
|
|
397
630
|
}
|
|
398
631
|
|
|
@@ -400,15 +633,43 @@
|
|
|
400
633
|
display: flex;
|
|
401
634
|
}
|
|
402
635
|
|
|
636
|
+
/* Desktop override for house dialog */
|
|
637
|
+
@media (min-width: 769px) {
|
|
638
|
+
.house-dialog {
|
|
639
|
+
align-items: center;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
.house-dialog-content {
|
|
643
|
+
position: relative;
|
|
644
|
+
bottom: auto;
|
|
645
|
+
left: auto;
|
|
646
|
+
right: auto;
|
|
647
|
+
border-radius: 20px;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/* Desktop house dialog styles */
|
|
652
|
+
@media (min-width: 769px) {
|
|
653
|
+
.house-dialog-content {
|
|
654
|
+
background: rgba(255, 255, 255, 0.15);
|
|
655
|
+
backdrop-filter: blur(20px);
|
|
656
|
+
border-radius: 20px;
|
|
657
|
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
658
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
|
659
|
+
width: 500px;
|
|
660
|
+
max-width: 90%;
|
|
661
|
+
max-height: 80vh;
|
|
662
|
+
display: flex;
|
|
663
|
+
flex-direction: column;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/* Base house dialog styles (shared) */
|
|
403
668
|
.house-dialog-content {
|
|
404
669
|
background: rgba(255, 255, 255, 0.15);
|
|
405
670
|
backdrop-filter: blur(20px);
|
|
406
|
-
border-radius: 20px;
|
|
407
671
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
408
672
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
|
409
|
-
width: 500px;
|
|
410
|
-
max-width: 90%;
|
|
411
|
-
max-height: 80vh;
|
|
412
673
|
display: flex;
|
|
413
674
|
flex-direction: column;
|
|
414
675
|
}
|
package/server.ts
CHANGED
|
@@ -7,13 +7,14 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
7
7
|
const __dirname = path.dirname(__filename);
|
|
8
8
|
|
|
9
9
|
const app = express();
|
|
10
|
-
const PORT = process.env.PORT || 3000;
|
|
11
10
|
|
|
12
11
|
// Parse CLI args
|
|
13
12
|
const args = process.argv.slice(2);
|
|
14
13
|
let user: string | null = null;
|
|
15
14
|
let mailboxPath: string | null = null;
|
|
16
15
|
let coworkerPath: string | null = null;
|
|
16
|
+
let port: number = parseInt(process.env.PORT || '3000', 10);
|
|
17
|
+
let host: string = process.env.HOST || '0.0.0.0';
|
|
17
18
|
|
|
18
19
|
for (let i = 0; i < args.length; i++) {
|
|
19
20
|
if (args[i] === '--user' || args[i] === '-u') {
|
|
@@ -22,11 +23,16 @@ for (let i = 0; i < args.length; i++) {
|
|
|
22
23
|
mailboxPath = args[++i];
|
|
23
24
|
} else if (args[i] === '--coworkers' || args[i] === '-c') {
|
|
24
25
|
coworkerPath = args[++i];
|
|
26
|
+
} else if (args[i] === '--port' || args[i] === '-p') {
|
|
27
|
+
const p = parseInt(args[++i], 10);
|
|
28
|
+
if (!isNaN(p)) port = p;
|
|
29
|
+
} else if (args[i] === '--host' || args[i] === '-h') {
|
|
30
|
+
host = args[++i];
|
|
25
31
|
}
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
if (!user || !mailboxPath) {
|
|
29
|
-
console.error('Usage: watercooler --user <name> --mailbox <path> [--coworkers <path>]');
|
|
35
|
+
console.error('Usage: watercooler --user <name> --mailbox <path> [--coworkers <path>] [--port <number>] [--host <address>]');
|
|
30
36
|
process.exit(1);
|
|
31
37
|
}
|
|
32
38
|
|
|
@@ -35,7 +41,7 @@ console.log(` Mailbox: ${mailboxPath}`);
|
|
|
35
41
|
if (coworkerPath) {
|
|
36
42
|
console.log(` Coworker DB: ${coworkerPath}`);
|
|
37
43
|
}
|
|
38
|
-
console.log(` URL: http
|
|
44
|
+
console.log(` URL: http://${host}:${port}`);
|
|
39
45
|
|
|
40
46
|
// Databases
|
|
41
47
|
let db: Database.Database | null = null;
|
|
@@ -58,6 +64,20 @@ if (coworkerPath) {
|
|
|
58
64
|
}
|
|
59
65
|
}
|
|
60
66
|
|
|
67
|
+
// Helper: Check if table exists
|
|
68
|
+
function tableExists(database: Database.Database | null, tableName: string): boolean {
|
|
69
|
+
if (!database) return false;
|
|
70
|
+
try {
|
|
71
|
+
const stmt = database.prepare(`
|
|
72
|
+
SELECT name FROM sqlite_master
|
|
73
|
+
WHERE type='table' AND name=?
|
|
74
|
+
`);
|
|
75
|
+
return !!stmt.get(tableName);
|
|
76
|
+
} catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
61
81
|
// Middleware
|
|
62
82
|
app.use(express.json());
|
|
63
83
|
app.use(express.static(path.join(__dirname, 'public')));
|
|
@@ -66,6 +86,10 @@ app.use(express.static(path.join(__dirname, 'public')));
|
|
|
66
86
|
app.get('/api/messages', (req, res) => {
|
|
67
87
|
try {
|
|
68
88
|
if (!db) throw new Error('Database not connected');
|
|
89
|
+
if (!tableExists(db, 'messages')) {
|
|
90
|
+
res.json([]);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
69
93
|
const stmt = db.prepare(`
|
|
70
94
|
SELECT * FROM messages
|
|
71
95
|
WHERE recipient = ?
|
|
@@ -81,6 +105,10 @@ app.get('/api/messages', (req, res) => {
|
|
|
81
105
|
app.get('/api/messages/sent', (req, res) => {
|
|
82
106
|
try {
|
|
83
107
|
if (!db) throw new Error('Database not connected');
|
|
108
|
+
if (!tableExists(db, 'messages')) {
|
|
109
|
+
res.json([]);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
84
112
|
const stmt = db.prepare(`
|
|
85
113
|
SELECT * FROM messages
|
|
86
114
|
WHERE sender = ?
|
|
@@ -96,6 +124,10 @@ app.get('/api/messages/sent', (req, res) => {
|
|
|
96
124
|
app.get('/api/messages/all', (req, res) => {
|
|
97
125
|
try {
|
|
98
126
|
if (!db) throw new Error('Database not connected');
|
|
127
|
+
if (!tableExists(db, 'messages')) {
|
|
128
|
+
res.json([]);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
99
131
|
const stmt = db.prepare(`
|
|
100
132
|
SELECT * FROM messages
|
|
101
133
|
ORDER BY timestamp DESC
|
|
@@ -125,12 +157,7 @@ app.get('/api/coworkers', (req, res) => {
|
|
|
125
157
|
console.log('No coworkerDb connection available');
|
|
126
158
|
}
|
|
127
159
|
|
|
128
|
-
//
|
|
129
|
-
const recipientRows = db.prepare('SELECT DISTINCT recipient FROM messages').all() as Array<{recipient: string}>;
|
|
130
|
-
recipientRows.forEach(row => allCoworkers.add(row.recipient.toLowerCase()));
|
|
131
|
-
|
|
132
|
-
const senderRows = db.prepare('SELECT DISTINCT sender FROM messages').all() as Array<{sender: string}>;
|
|
133
|
-
senderRows.forEach(row => allCoworkers.add(row.sender.toLowerCase()));
|
|
160
|
+
// Note: Messages table is in a different database, not queried here
|
|
134
161
|
|
|
135
162
|
// Remove current user
|
|
136
163
|
allCoworkers.delete(user!.toLowerCase());
|
|
@@ -147,6 +174,10 @@ app.get('/api/coworkers', (req, res) => {
|
|
|
147
174
|
app.get('/api/recipients', (req, res) => {
|
|
148
175
|
try {
|
|
149
176
|
if (!db) throw new Error('Database not connected');
|
|
177
|
+
if (!tableExists(db, 'messages')) {
|
|
178
|
+
res.json([]);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
150
181
|
const stmt = db.prepare(`SELECT DISTINCT recipient FROM messages`);
|
|
151
182
|
res.json(stmt.all().map((r: any) => r.recipient));
|
|
152
183
|
} catch (err: any) {
|
|
@@ -158,6 +189,21 @@ app.get('/api/recipients', (req, res) => {
|
|
|
158
189
|
app.post('/api/send', (req, res) => {
|
|
159
190
|
try {
|
|
160
191
|
if (!db) throw new Error('Database not connected');
|
|
192
|
+
|
|
193
|
+
// Auto-create messages table if it doesn't exist
|
|
194
|
+
if (!tableExists(db, 'messages')) {
|
|
195
|
+
db.exec(`
|
|
196
|
+
CREATE TABLE messages (
|
|
197
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
198
|
+
recipient TEXT NOT NULL,
|
|
199
|
+
sender TEXT NOT NULL,
|
|
200
|
+
message TEXT NOT NULL,
|
|
201
|
+
timestamp INTEGER NOT NULL,
|
|
202
|
+
read INTEGER DEFAULT 0
|
|
203
|
+
)
|
|
204
|
+
`);
|
|
205
|
+
}
|
|
206
|
+
|
|
161
207
|
const { to, message } = req.body;
|
|
162
208
|
const stmt = db.prepare(`
|
|
163
209
|
INSERT INTO messages (recipient, sender, message, timestamp, read)
|
|
@@ -174,6 +220,10 @@ app.post('/api/send', (req, res) => {
|
|
|
174
220
|
app.post('/api/messages/:id/read', (req, res) => {
|
|
175
221
|
try {
|
|
176
222
|
if (!db) throw new Error('Database not connected');
|
|
223
|
+
if (!tableExists(db, 'messages')) {
|
|
224
|
+
res.status(404).json({ error: 'Messages table not found' });
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
177
227
|
db.prepare('UPDATE messages SET read = 1 WHERE id = ?').run(req.params.id);
|
|
178
228
|
res.json({ success: true });
|
|
179
229
|
} catch (err: any) {
|
|
@@ -186,6 +236,6 @@ app.get('/api/config', (req, res) => {
|
|
|
186
236
|
res.json({ user, mailbox: mailboxPath, coworker: coworkerPath });
|
|
187
237
|
});
|
|
188
238
|
|
|
189
|
-
app.listen(
|
|
239
|
+
app.listen(port, host, () => {
|
|
190
240
|
console.log('\n✅ Watercooler running!');
|
|
191
241
|
});
|