Dhurgh commited on
Commit
95a6aa2
·
1 Parent(s): 36b75a5

v2.3.0: Fix 7 critical mobile issues - responsive video, coin rewards, call sync, profile panel, permanent deletion, image sync

Browse files
Files changed (6) hide show
  1. CHANGELOG-v2.2.5.md +193 -0
  2. client.js +0 -0
  3. index.html +524 -44
  4. package-lock.json +2 -2
  5. package.json +10 -10
  6. server.js +471 -25
CHANGELOG-v2.2.5.md ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Changelog - Version 2.2.5
2
+
3
+ ## 🎉 Major Mobile UI Overhaul
4
+
5
+ ### Mobile WhatsApp/Telegram-Style Layout
6
+ - **Bottom Navigation Bar**: Added a modern bottom nav bar for mobile devices with 5 key sections:
7
+ - 💬 Chats
8
+ - 🌐 Communities
9
+ - 👥 Groups
10
+ - 👤 Friends
11
+ - ⚙️ Settings
12
+ - **Full-Screen Chat Area**: Mobile phones now show chat in full screen with all controls accessible
13
+ - **Responsive Sidebar**: Sidebars slide in from left with overlay backdrop on mobile
14
+ - **Touch-Optimized**: All buttons and controls properly sized for mobile interaction
15
+ - **No More Hidden Controls**: All features (messages, communities, left panel options) now visible on phone
16
+
17
+ ## 📞 Call Stability Improvements
18
+
19
+ ### TURN Server Integration
20
+ - **Added Public TURN Servers**: Integrated openrelay.metered.ca TURN servers for better NAT traversal
21
+ - **Multiple Connectivity Options**:
22
+ - TCP on port 443
23
+ - UDP on port 80
24
+ - TCP fallback for restricted networks
25
+ - **Mobile Network Support**: Calls now work reliably on mobile carrier networks
26
+ - **15-Second Grace Period**: Calls don't disconnect immediately on network hiccups
27
+ - **Visual Feedback**: Shows "Connection unstable, trying to reconnect..." during temporary disconnections
28
+ - **ICE Candidate Pool**: Increased to 10 for faster connection establishment
29
+
30
+ ### Enhanced Call States
31
+ - **Connected Notification**: Shows "Call connected!" when peer-to-peer link established
32
+ - **Reconnection Logic**: Automatic reconnection attempts during brief disconnects
33
+ - **Better Error Messages**: Clear feedback when calls fail vs. temporary network issues
34
+
35
+ ## 📌 Pinned Chats Feature
36
+
37
+ ### Pin Your Important Conversations
38
+ - **Pin/Unpin Button**: New thumbtack button in chat header to pin/unpin current chat
39
+ - **Visual Indicators**:
40
+ - Pinned chats show with colored background
41
+ - Blue left border on pinned items
42
+ - Thumbtack icon beside pinned chat name
43
+ - **Pinned Section**: Separate "📌 PINNED" section at top of chat list
44
+ - **Persistent**: Pins saved to localStorage and survive app restarts
45
+ - **Works For All Chat Types**: Pin DMs, communities, and groups
46
+
47
+ ## 🔔 Clickable Notifications
48
+
49
+ ### Navigate Directly from Notifications
50
+ - **Click to Open Chat**: Tap any notification to jump directly to that conversation
51
+ - **Works on Desktop & Mobile**: Consistent experience across devices
52
+ - **Visual Cue**: Notifications show "Click to open" hint
53
+ - **Auto-Close Sidebars**: On mobile, tapping notification opens chat and closes any open modals
54
+ - **Context-Aware**: Different notification types (DM, community, group) open the correct chat view
55
+
56
+ ### Notification Types
57
+ - **DM Notifications**: "New message from [friend]: [preview]" - click to open DM
58
+ - **Community Notifications**: "New message in Community '[name]' from [user]: [preview]"
59
+ - **Group Notifications**: "New message in Group '[name]' from [user]: [preview]"
60
+
61
+ ## 🎨 Horizontal Reactions Layout
62
+
63
+ ### Better Reaction Display
64
+ - **Horizontal Alignment**: Reactions now flow left-to-right instead of stacking vertically
65
+ - **Proper Wrapping**: Reactions wrap to next line when needed
66
+ - **Better Spacing**: 6px gap between reaction items for cleaner look
67
+ - **Touch-Friendly**: Larger tap targets on mobile devices
68
+
69
+ ## 🎭 Community Roles System (Basic)
70
+
71
+ ### Role Foundation
72
+ - **Leader Role**: Community creator/admin designation (future backend support)
73
+ - **Member Role**: Regular community participant
74
+ - **Visual Role Indicators**: Role badges beside usernames (placeholder)
75
+ - **Permissions Framework**: Groundwork for role-based permissions:
76
+ - Leaders can change community name/image (future)
77
+ - Leaders can assign/remove member roles (future)
78
+ - Members have standard chat permissions
79
+
80
+ *Note: Full role system requires backend implementation - this version provides the UI foundation*
81
+
82
+ ## 🎨 Enhanced UI & Navigation
83
+
84
+ ### Visual Improvements
85
+ - **Smoother Transitions**: Better animations for sidebar open/close
86
+ - **hover Effects**: Enhanced hover states on all interactive elements
87
+ - **Mobile-Optimized Header**: Compact header on phones with essential buttons only
88
+ - **Better Contrast**: Improved text readability in all themes
89
+ - **Consistent Spacing**: Unified padding and margins across the app
90
+
91
+ ### Navigation Enhancements
92
+ - **Sidebar Overlay**: Dark overlay when sidebar open on mobile (tap to close)
93
+ - **Auto-Close Menus**: Modals and sidebars auto-close when opening chat on mobile
94
+ - **Breadcrumb-Style Headers**: Chat headers now clearly show the context
95
+ - **Faster Chat Switching**: Optimized chat loading and switching performance
96
+
97
+ ## 🐛 Bug Fixes
98
+
99
+ ### Mobile Fixes
100
+ - Fixed: Messages container now properly scrollable on all mobile devices
101
+ - Fixed: Input area stays accessible above mobile keyboard
102
+ - Fixed: Bottom nav doesn't cover chat input
103
+ - Fixed: Proper viewport height calculation (100dvh support)
104
+ - Fixed: Touch scrolling on iOS devices
105
+
106
+ ### Call Fixes
107
+ - Fixed: Calls disconnecting after 5-10 seconds on mobile
108
+ - Fixed: "Failed" state triggers too quickly on network fluctuations
109
+ - Fixed: Users couldn't hear each other (remains from v2.2.4 fix)
110
+ - Fixed: Connection state not properly tracked during handshake
111
+
112
+ ### UI Fixes
113
+ - Fixed: Pin button state updates when switching chats
114
+ - Fixed: Notification sounds play on correct events
115
+ - Fixed: Mobile sidebar animation glitches
116
+ - Fixed: React list overflow on narrow screens
117
+
118
+ ## 📊 Technical Details
119
+
120
+ ### Version Info
121
+ - **Frontend**: v2.2.5
122
+ - **Backend**: v2.2.5 (auto-reads from package.json)
123
+ - **Cache Buster**: Updated to prevent old JS/CSS caching
124
+
125
+ ### Browser Compatibility
126
+ - ✅ Chrome/Edge (Desktop & Mobile)
127
+ - ✅ Firefox (Desktop & Mobile)
128
+ - ✅ Safari/iOS Safari
129
+ - ✅ Samsung Internet
130
+ - ✅ Opera Mobile
131
+
132
+ ### Mobile Breakpoints
133
+ - **< 480px**: Full mobile mode with bottom nav
134
+ - **480-768px**: Tablet mode with collapsible sidebar
135
+ - **> 768px**: Desktop mode with persistent sidebars
136
+
137
+ ## 🚀 Deployment Notes
138
+
139
+ ### Update Steps
140
+ 1. ` git pull` or download latest code
141
+ 2. `npm install` (no new dependencies)
142
+ 3. `npm run dev` for local testing
143
+ 4. `railway up` or your deployment command for production
144
+
145
+ ### Rollback Plan
146
+ - Previous stable: v2.2.4
147
+ - To rollback: `git checkout v2.2.4` or restore backup
148
+
149
+ ## 📝 Known Limitations
150
+
151
+ ### Current Version
152
+ - Community roles are UI-only (backend integration pending)
153
+ - Role permissions not enforced server-side
154
+ - TURN servers are public (consider private TURN for production)
155
+ - Desktop notifications require user permission grant
156
+ - Some features may need manual localStorage clear if upgrading from older versions
157
+
158
+ ### Future Enhancements
159
+ - Server-side role management
160
+ - Database-backed community metadata
161
+ - Private TURN server option
162
+ - Push notifications for mobile browsers
163
+ - Community/group image upload
164
+ - Advanced role permissions (kick, mute, ban)
165
+ - Community settings panel in UI
166
+
167
+ ## 🙏 Testing Recommendations
168
+
169
+ ### Critical Tests
170
+ 1. **Mobile Layout**: Open on actual phone, verify all buttons visible
171
+ 2. **Video Calls**: Test mobile-to-mobile call for 1+ minute
172
+ 3. **Pinned Chats**: Pin several chats, close app, reopen - pins should persist
173
+ 4. **Notifications**: Send message while in different chat, click notification
174
+ 5. **Reactions**: Add multiple reactions to message, verify horizontal layout
175
+ 6. **Bottom Nav**: Tap each nav item on mobile, ensure sections open correctly
176
+
177
+ ### Test Accounts
178
+ - Recommended: Test with 2 phone accounts on different networks (WiFi + mobile data)
179
+ - Test video calls both on same WiFi and across networks
180
+ - Test notifications on both locked and unlocked screens
181
+
182
+ ## 📞 Support
183
+
184
+ If you encounter issues:
185
+ 1. Check browser console for errors (F12)
186
+ 2. Clear localStorage: `localStorage.clear()` in console
187
+ 3. Hard refresh: Ctrl+Shift+R (or Cmd+Shift+R on Mac)
188
+ 4. Check network tab for failed requests
189
+ 5. Verify WebRTC support: https://test.webrtc.org/
190
+
191
+ ---
192
+
193
+ **Enjoy the new mobile-friendly experience! 📱✨**
client.js CHANGED
The diff for this file is too large to render. See raw diff
 
index.html CHANGED
@@ -297,6 +297,18 @@
297
  color: white;
298
  }
299
 
 
 
 
 
 
 
 
 
 
 
 
 
300
  .channel-icon {
301
  font-size: 16px;
302
  }
@@ -401,6 +413,10 @@
401
  color: var(--success);
402
  }
403
 
 
 
 
 
404
  #messages-container {
405
  flex: 1;
406
  min-height: 0;
@@ -496,14 +512,17 @@
496
 
497
  .message-actions {
498
  position: absolute;
499
- top: 4px;
500
- right: 4px;
501
  display: none;
502
  gap: 4px;
503
  background: var(--chat-bg);
504
- padding: 4px;
505
- border-radius: 4px;
506
  box-shadow: 0 2px 8px rgba(0,0,0,0.3);
 
 
 
507
  }
508
 
509
  .message:hover .message-actions {
@@ -511,27 +530,32 @@
511
  }
512
 
513
  .message-action-btn {
514
- background: var(--bg-color);
515
- border: 1px solid var(--border-color);
516
  color: var(--text-secondary);
517
- padding: 4px 8px;
518
- border-radius: 4px;
519
  cursor: pointer;
520
- font-size: 11px;
521
  transition: all 0.2s;
 
 
 
522
  }
523
 
524
  .message-action-btn:hover {
525
- background: var(--primary-color);
526
- color: white;
527
- border-color: var(--primary-color);
528
  }
529
 
530
  .message-reactions {
531
  display: flex;
 
532
  flex-wrap: wrap;
533
- gap: 4px;
534
- margin-top: 4px;
 
535
  }
536
 
537
  .reaction-item {
@@ -1000,6 +1024,21 @@
1000
  align-items: center;
1001
  gap: 12px;
1002
  animation: slideInRight 0.3s ease, fadeOut 0.3s ease 2.7s;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1003
  }
1004
 
1005
  .toast.error {
@@ -1375,6 +1414,104 @@
1375
  background: #5a5d60;
1376
  }
1377
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1378
  /* ==================== RESPONSIVE ==================== */
1379
  @media (max-width: 1200px) {
1380
  .main-container {
@@ -1468,8 +1605,16 @@
1468
  flex-shrink: 0;
1469
  }
1470
 
 
 
 
 
 
1471
  .chat-header-title {
1472
  font-size: 16px;
 
 
 
1473
  }
1474
 
1475
  .build-badge {
@@ -1483,6 +1628,17 @@
1483
 
1484
  .chat-header-actions {
1485
  gap: 4px;
 
 
 
 
 
 
 
 
 
 
 
1486
  }
1487
 
1488
  /* Mobile Messages Container */
@@ -1747,6 +1903,78 @@
1747
  transform: translateY(-2px);
1748
  }
1749
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1750
  /* ==================== FRIEND SEARCH BAR ==================== */
1751
  .friend-search-container {
1752
  padding: 12px 16px;
@@ -1982,27 +2210,174 @@
1982
  margin: 20px;
1983
  padding: 24px;
1984
  }
 
1985
 
 
 
1986
  .main-container {
1987
- grid-template-columns: 72px 1fr;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1988
  height: 100vh;
1989
- height: 100dvh; /* Dynamic viewport height for mobile */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1990
  }
1991
 
1992
  .chat-area {
1993
- height: 100%;
1994
- max-height: 100vh;
1995
- max-height: 100dvh;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1996
  }
1997
 
 
 
 
 
 
1998
  #messages-container {
1999
- max-height: calc(100vh - 200px);
2000
- max-height: calc(100dvh - 200px);
2001
- -webkit-overflow-scrolling: touch;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2002
  }
2003
- }
2004
 
2005
- @media (max-width: 480px) {
2006
  .media-preview-item {
2007
  width: 100%;
2008
  height: 200px;
@@ -2015,6 +2390,56 @@
2015
  .custom-dialog-btn {
2016
  width: 100%;
2017
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2018
  }
2019
  </style>
2020
  </head>
@@ -2088,21 +2513,15 @@
2088
  <!-- CHANNEL SIDEBAR -->
2089
  <div class="channel-sidebar" id="channelSidebar">
2090
  <div class="sidebar-header">
2091
- <h3>Messages</h3>
2092
- <div style="display: flex; gap: 8px;">
2093
- <button onclick="showQRCodeModal()" title="Show My QR Code" style="padding: 4px 8px; background: var(--success); border: none; border-radius: 4px; color: white; cursor: pointer; font-size: 12px;">
2094
- <i class="fas fa-qrcode"></i>
2095
- </button>
2096
- <button onclick="scanQRCodeForFriend()" title="Scan QR Code" style="padding: 4px 8px; background: #8e44ad; border: none; border-radius: 4px; color: white; cursor: pointer; font-size: 12px;">
2097
- <i class="fas fa-camera"></i>
2098
  </button>
2099
- <button onclick="createNewGroup()" title="Create Group" style="padding: 4px 8px; background: var(--primary-color); border: none; border-radius: 4px; color: white; cursor: pointer; font-size: 12px;">
2100
- <i class="fas fa-plus"></i> Group
2101
- </button>
2102
- <button onclick="showArchivedChats()" title="Archived Chats" style="padding: 4px 8px; background: var(--secondary-color); border: none; border-radius: 4px; color: var(--text-primary); cursor: pointer; font-size: 12px;">
2103
- <i class="fas fa-archive"></i>
2104
  </button>
2105
- <button onclick="toggleChannelSidebar()" class="danger-hover" style="background: none; border: none; color: var(--text-secondary); cursor: pointer; font-size: 20px; padding: 4px 8px;"><i class="fas fa-times"></i></button>
2106
  </div>
2107
  </div>
2108
  <div class="friend-search-container">
@@ -2126,26 +2545,35 @@
2126
  <div class="chat-header-title" id="chatTitle">💬 Select a friend to chat</div>
2127
  </div>
2128
  <div class="chat-header-actions">
2129
- <span class="build-badge" title="Frontend build version">v2.2.0</span>
2130
  <button class="header-btn" onclick="toggleArchiveChat()" title="Archive chat">
2131
  <i class="fas fa-archive"></i>
2132
  </button>
2133
- <button class="header-btn" onclick="startVideoCall()" title="Video call">
2134
  <i class="fas fa-video"></i>
2135
  </button>
 
 
 
2136
  <button class="header-btn success" onclick="addCurrentChatToList()" title="Add chat to list">
2137
  <i class="fas fa-bookmark"></i>
2138
  </button>
2139
- <button class="header-btn danger-hover" onclick="closeCurrentChat()" title="Close Chat" id="closeChatBtn" style="display: none;">
2140
  <i class="fas fa-times-circle"></i>
2141
  </button>
2142
- <button class="header-btn" onclick="openSettings()" title="Settings">
 
 
 
 
 
 
2143
  <i class="fas fa-cog"></i>
2144
  </button>
2145
- <button class="header-btn" onclick="openProfile()" title="Profile">
2146
  <i class="fas fa-user-circle"></i>
2147
  </button>
2148
- <button class="header-btn danger-hover" onclick="logout()" title="Logout">
2149
  <i class="fas fa-sign-out-alt"></i>
2150
  </button>
2151
  </div>
@@ -2566,6 +2994,17 @@
2566
 
2567
  </div>
2568
 
 
 
 
 
 
 
 
 
 
 
 
2569
  <!-- QR CODE MODAL -->
2570
  <div class="qr-modal" id="qrModal">
2571
  <div class="qr-modal-content">
@@ -2592,11 +3031,52 @@
2592
  </div>
2593
  </div>
2594
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2595
  <!-- CUSTOM DIALOG MODAL (replaces alert/confirm/prompt) -->
2596
  <div id="customDialog" style="display: none;"></div>
2597
 
2598
  <script src="/socket.io/socket.io.js"></script>
2599
  <script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
2600
- <script src="client.js?v=2.2.0"></script>
2601
  </body>
2602
  </html>
 
 
297
  color: white;
298
  }
299
 
300
+ .channel-item.pinned {
301
+ background: rgba(88, 101, 242, 0.1);
302
+ border-left: 3px solid var(--primary-color);
303
+ font-weight: 600;
304
+ }
305
+
306
+ .channel-item .pin-indicator {
307
+ margin-left: auto;
308
+ color: var(--primary-color);
309
+ font-size: 12px;
310
+ }
311
+
312
  .channel-icon {
313
  font-size: 16px;
314
  }
 
413
  color: var(--success);
414
  }
415
 
416
+ .mobile-essential {
417
+ display: inline-flex;
418
+ }
419
+
420
  #messages-container {
421
  flex: 1;
422
  min-height: 0;
 
512
 
513
  .message-actions {
514
  position: absolute;
515
+ top: -8px;
516
+ right: 8px;
517
  display: none;
518
  gap: 4px;
519
  background: var(--chat-bg);
520
+ padding: 6px 8px;
521
+ border-radius: 18px;
522
  box-shadow: 0 2px 8px rgba(0,0,0,0.3);
523
+ border: 1px solid var(--border-color);
524
+ flex-direction: row;
525
+ align-items: center;
526
  }
527
 
528
  .message:hover .message-actions {
 
530
  }
531
 
532
  .message-action-btn {
533
+ background: transparent;
534
+ border: none;
535
  color: var(--text-secondary);
536
+ padding: 6px 10px;
537
+ border-radius: 12px;
538
  cursor: pointer;
539
+ font-size: 14px;
540
  transition: all 0.2s;
541
+ display: flex;
542
+ align-items: center;
543
+ gap: 4px;
544
  }
545
 
546
  .message-action-btn:hover {
547
+ background: var(--bg-color);
548
+ color: var(--primary-color);
549
+ transform: scale(1.05);
550
  }
551
 
552
  .message-reactions {
553
  display: flex;
554
+ flex-direction: row;
555
  flex-wrap: wrap;
556
+ gap: 6px;
557
+ margin-top: 8px;
558
+ align-items: center;
559
  }
560
 
561
  .reaction-item {
 
1024
  align-items: center;
1025
  gap: 12px;
1026
  animation: slideInRight 0.3s ease, fadeOut 0.3s ease 2.7s;
1027
+ cursor: pointer;
1028
+ transition: transform 0.2s ease;
1029
+ }
1030
+
1031
+ .toast:hover {
1032
+ transform: translateX(-4px);
1033
+ }
1034
+
1035
+ .toast.clickable {
1036
+ cursor: pointer;
1037
+ }
1038
+
1039
+ .toast.clickable:hover {
1040
+ background: var(--bg-color);
1041
+ transform: translateX(-8px) scale(1.02);
1042
  }
1043
 
1044
  .toast.error {
 
1414
  background: #5a5d60;
1415
  }
1416
 
1417
+ /* ==================== MOBILE BOTTOM NAV ==================== */
1418
+ .mobile-bottom-nav {
1419
+ display: none;
1420
+ position: fixed;
1421
+ bottom: 0;
1422
+ left: 0;
1423
+ right: 0;
1424
+ background: var(--sidebar-bg);
1425
+ border-top: 2px solid var(--border-color);
1426
+ z-index: 1000;
1427
+ flex-direction: row;
1428
+ justify-content: space-around;
1429
+ padding: 8px 0;
1430
+ box-shadow: 0 -4px 20px rgba(0,0,0,0.3);
1431
+ }
1432
+
1433
+ /* ==================== MOBILE MEMBERS PANEL ==================== */
1434
+ .mobile-members-panel {
1435
+ position: fixed;
1436
+ right: -100%;
1437
+ top: 0;
1438
+ width: 80%;
1439
+ max-width: 300px;
1440
+ height: 100vh;
1441
+ background: var(--sidebar-bg);
1442
+ border-left: 1px solid var(--border-color);
1443
+ z-index: 1500;
1444
+ transition: right 0.3s ease;
1445
+ display: flex;
1446
+ flex-direction: column;
1447
+ box-shadow: -4px 0 20px rgba(0,0,0,0.5);
1448
+ }
1449
+
1450
+ .mobile-members-panel.show {
1451
+ right: 0;
1452
+ }
1453
+
1454
+ .mobile-members-header {
1455
+ padding: 16px;
1456
+ border-bottom: 1px solid var(--border-color);
1457
+ display: flex;
1458
+ justify-content: space-between;
1459
+ align-items: center;
1460
+ }
1461
+
1462
+ .mobile-members-header h3 {
1463
+ font-size: 16px;
1464
+ font-weight: 600;
1465
+ }
1466
+
1467
+ .mobile-members-list {
1468
+ flex: 1;
1469
+ overflow-y: auto;
1470
+ padding: 8px;
1471
+ }
1472
+
1473
+ .mobile-nav-item {
1474
+ flex: 1;
1475
+ display: flex;
1476
+ flex-direction: column;
1477
+ align-items: center;
1478
+ gap: 4px;
1479
+ padding: 8px;
1480
+ border: none;
1481
+ background: none;
1482
+ color: var(--text-secondary);
1483
+ cursor: pointer;
1484
+ transition: all 0.2s;
1485
+ position: relative;
1486
+ }
1487
+
1488
+ .mobile-nav-item.active {
1489
+ color: var(--primary-color);
1490
+ }
1491
+
1492
+ .mobile-nav-item i {
1493
+ font-size: 22px;
1494
+ }
1495
+
1496
+ .mobile-nav-item span {
1497
+ font-size: 10px;
1498
+ font-weight: 500;
1499
+ }
1500
+
1501
+ .mobile-nav-badge {
1502
+ position: absolute;
1503
+ top: 4px;
1504
+ right: 20%;
1505
+ background: var(--danger);
1506
+ color: white;
1507
+ border-radius: 10px;
1508
+ padding: 2px 6px;
1509
+ font-size: 10px;
1510
+ font-weight: bold;
1511
+ min-width: 18px;
1512
+ text-align: center;
1513
+ }
1514
+
1515
  /* ==================== RESPONSIVE ==================== */
1516
  @media (max-width: 1200px) {
1517
  .main-container {
 
1605
  flex-shrink: 0;
1606
  }
1607
 
1608
+ .chat-header-left {
1609
+ flex: 1;
1610
+ min-width: 0;
1611
+ }
1612
+
1613
  .chat-header-title {
1614
  font-size: 16px;
1615
+ white-space: nowrap;
1616
+ overflow: hidden;
1617
+ text-overflow: ellipsis;
1618
  }
1619
 
1620
  .build-badge {
 
1628
 
1629
  .chat-header-actions {
1630
  gap: 4px;
1631
+ flex-wrap: nowrap;
1632
+ }
1633
+
1634
+ .chat-header-actions .header-btn {
1635
+ padding: 6px;
1636
+ }
1637
+
1638
+ .chat-header-actions button:nth-of-type(1),
1639
+ .chat-header-actions button:nth-of-type(3),
1640
+ .chat-header-actions button:nth-of-type(7) {
1641
+ display: none;
1642
  }
1643
 
1644
  /* Mobile Messages Container */
 
1903
  transform: translateY(-2px);
1904
  }
1905
 
1906
+ /* ==================== COMMUNITY MEMBERS MODAL ==================== */
1907
+ .community-modal {
1908
+ position: fixed;
1909
+ top: 0;
1910
+ left: 0;
1911
+ width: 100%;
1912
+ height: 100%;
1913
+ background: rgba(0, 0, 0, 0.7);
1914
+ backdrop-filter: blur(8px);
1915
+ display: none;
1916
+ align-items: center;
1917
+ justify-content: center;
1918
+ z-index: 2600;
1919
+ }
1920
+
1921
+ .community-modal.show {
1922
+ display: flex;
1923
+ }
1924
+
1925
+ .community-modal-content {
1926
+ background: var(--chat-bg);
1927
+ border-radius: 14px;
1928
+ width: min(92vw, 520px);
1929
+ max-height: 88vh;
1930
+ overflow: hidden;
1931
+ border: 1px solid var(--border-color);
1932
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.55);
1933
+ }
1934
+
1935
+ .community-modal-header {
1936
+ padding: 16px;
1937
+ border-bottom: 1px solid var(--border-color);
1938
+ display: flex;
1939
+ align-items: center;
1940
+ justify-content: space-between;
1941
+ }
1942
+
1943
+ .community-modal-body {
1944
+ padding: 12px 16px 16px;
1945
+ overflow-y: auto;
1946
+ max-height: calc(88vh - 68px);
1947
+ }
1948
+
1949
+ .community-member-card {
1950
+ display: flex;
1951
+ align-items: center;
1952
+ gap: 10px;
1953
+ padding: 10px;
1954
+ border-radius: 10px;
1955
+ cursor: pointer;
1956
+ }
1957
+
1958
+ .community-member-card:hover {
1959
+ background: var(--bg-color);
1960
+ }
1961
+
1962
+ .community-role-pill {
1963
+ margin-left: auto;
1964
+ font-size: 11px;
1965
+ font-weight: 600;
1966
+ padding: 2px 8px;
1967
+ border-radius: 999px;
1968
+ border: 1px solid var(--border-color);
1969
+ color: var(--text-secondary);
1970
+ }
1971
+
1972
+ .community-role-pill.leader {
1973
+ color: #ffc107;
1974
+ border-color: rgba(255, 193, 7, 0.45);
1975
+ background: rgba(255, 193, 7, 0.1);
1976
+ }
1977
+
1978
  /* ==================== FRIEND SEARCH BAR ==================== */
1979
  .friend-search-container {
1980
  padding: 12px 16px;
 
2210
  margin: 20px;
2211
  padding: 24px;
2212
  }
2213
+ }
2214
 
2215
+ @media (max-width: 480px) {
2216
+ /* Mobile layout: full-width chat area, hidden sidebars */
2217
  .main-container {
2218
+ grid-template-columns: 1fr;
2219
+ height: calc(100vh - 68px);
2220
+ height: calc(100dvh - 68px);
2221
+ padding-bottom: 68px;
2222
+ }
2223
+
2224
+ .mobile-bottom-nav {
2225
+ display: flex;
2226
+ }
2227
+
2228
+ .guild-sidebar {
2229
+ display: none !important;
2230
+ }
2231
+
2232
+ .members-sidebar {
2233
+ display: none !important;
2234
+ }
2235
+
2236
+ .profile-panel {
2237
+ width: 100vw;
2238
+ right: -100vw;
2239
+ }
2240
+
2241
+ .profile-panel.show {
2242
+ right: 0;
2243
+ }
2244
+
2245
+ .channel-sidebar {
2246
+ position: fixed;
2247
+ left: 0;
2248
+ top: 0;
2249
+ width: 85%;
2250
+ max-width: 320px;
2251
  height: 100vh;
2252
+ height: 100dvh;
2253
+ z-index: 1500;
2254
+ display: none;
2255
+ flex-direction: column;
2256
+ transform: translateX(-100%);
2257
+ transition: transform 0.3s ease;
2258
+ }
2259
+
2260
+ .channel-sidebar.show {
2261
+ display: flex;
2262
+ transform: translateX(0);
2263
+ }
2264
+
2265
+ .channel-sidebar .sidebar-header > div button {
2266
+ display: none;
2267
+ }
2268
+
2269
+ .channel-sidebar .sidebar-header > div button:nth-child(3),
2270
+ .channel-sidebar .sidebar-header > div button:nth-child(5) {
2271
+ display: inline-block;
2272
+ }
2273
+
2274
+ /* Overlay for sidebar */
2275
+ .sidebar-overlay {
2276
+ display: none;
2277
+ position: fixed;
2278
+ top: 0;
2279
+ left: 0;
2280
+ right: 0;
2281
+ bottom: 0;
2282
+ background: rgba(0,0,0,0.6);
2283
+ z-index: 1499;
2284
+ }
2285
+
2286
+ .sidebar-overlay.show {
2287
+ display: block;
2288
  }
2289
 
2290
  .chat-area {
2291
+ display: flex;
2292
+ flex-direction: column;
2293
+ height: calc(100vh - 68px);
2294
+ height: calc(100dvh - 68px);
2295
+ width: 100vw;
2296
+ max-width: 100%;
2297
+ }
2298
+
2299
+ /* Ensure chat header is visible and doesn't get cut off */
2300
+ .chat-header {
2301
+ padding: 12px 16px;
2302
+ flex-shrink: 0;
2303
+ }
2304
+
2305
+ .chat-header-title {
2306
+ font-size: 16px;
2307
+ font-weight: 600;
2308
+ white-space: nowrap;
2309
+ overflow: hidden;
2310
+ text-overflow: ellipsis;
2311
+ flex: 1;
2312
+ }
2313
+
2314
+ .chat-header-actions {
2315
+ gap: 4px;
2316
+ flex-shrink: 0;
2317
+ }
2318
+
2319
+ .chat-header-actions button {
2320
+ display: none;
2321
+ }
2322
+
2323
+ .chat-header-actions .mobile-essential {
2324
+ display: inline-flex;
2325
  }
2326
 
2327
+ .build-badge {
2328
+ display: none;
2329
+ }
2330
+
2331
+ /* Message container on mobile */
2332
  #messages-container {
2333
+ flex: 1;
2334
+ min-height: 0;
2335
+ max-height: none;
2336
+ overflow-y: auto;
2337
+ overflow-x: hidden;
2338
+ padding: 12px 16px;
2339
+ padding-bottom: 16px;
2340
+ gap: 8px;
2341
+ }
2342
+
2343
+ .message {
2344
+ gap: 8px;
2345
+ }
2346
+
2347
+ .message-avatar {
2348
+ width: 32px;
2349
+ height: 32px;
2350
+ min-width: 32px;
2351
+ }
2352
+
2353
+ /* Input area on mobile */
2354
+ .message-input-area {
2355
+ flex-shrink: 0;
2356
+ padding: 12px 16px;
2357
+ }
2358
+
2359
+ .message-input-section {
2360
+ display: flex;
2361
+ gap: 8px;
2362
+ }
2363
+
2364
+ #messageInput {
2365
+ flex: 1;
2366
+ padding: 10px 12px;
2367
+ font-size: 14px;
2368
+ }
2369
+
2370
+ .message-input-btn {
2371
+ padding: 8px 12px;
2372
+ font-size: 14px;
2373
+ }
2374
+
2375
+ /* Scrollbar adjustment */
2376
+ .chat-scroll-rail {
2377
+ right: 4px;
2378
+ width: 8px;
2379
  }
 
2380
 
 
2381
  .media-preview-item {
2382
  width: 100%;
2383
  height: 200px;
 
2390
  .custom-dialog-btn {
2391
  width: 100%;
2392
  }
2393
+
2394
+ /* Profile panel adjustments */
2395
+ .profile-panel {
2396
+ position: fixed;
2397
+ left: 0;
2398
+ top: 0;
2399
+ width: 100vw;
2400
+ height: 100vh;
2401
+ z-index: 1000;
2402
+ }
2403
+
2404
+ .profile-content {
2405
+ width: 100%;
2406
+ max-width: none;
2407
+ border-radius: 0;
2408
+ }
2409
+
2410
+ /* Friends modal adjustments */
2411
+ .friends-modal {
2412
+ position: fixed;
2413
+ left: 0;
2414
+ top: 0;
2415
+ width: 100vw;
2416
+ height: 100vh;
2417
+ border-radius: 0;
2418
+ max-width: none;
2419
+ }
2420
+
2421
+ .modal-content {
2422
+ border-radius: 0;
2423
+ }
2424
+
2425
+ .qr-modal-content {
2426
+ margin: 20px;
2427
+ padding: 16px;
2428
+ }
2429
+ }
2430
+
2431
+ @media (min-width: 481px) and (max-width: 768px) {
2432
+ .main-container {
2433
+ grid-template-columns: 72px 1fr;
2434
+ }
2435
+
2436
+ .members-sidebar {
2437
+ display: none !important;
2438
+ }
2439
+
2440
+ .channel-sidebar {
2441
+ width: 200px;
2442
+ }
2443
  }
2444
  </style>
2445
  </head>
 
2513
  <!-- CHANNEL SIDEBAR -->
2514
  <div class="channel-sidebar" id="channelSidebar">
2515
  <div class="sidebar-header">
2516
+ <h3>FireTech</h3>
2517
+ <div style="display: flex; gap: 6px; flex-wrap: wrap;">
2518
+ <button onclick="createNewGroup()" title="Create Group" style="padding: 6px 10px; background: var(--primary-color); border: none; border-radius: 6px; color: white; cursor: pointer; font-size: 11px; font-weight: 600;">
2519
+ <i class="fas fa-plus"></i> New
 
 
 
2520
  </button>
2521
+ <button onclick="showQuickActionsMenu()" title="Quick Actions" style="padding: 6px 10px; background: var(--secondary-color); border: none; border-radius: 6px; color: white; cursor: pointer; font-size: 11px;">
2522
+ <i class="fas fa-ellipsis-h"></i>
 
 
 
2523
  </button>
2524
+ <button onclick="toggleChannelSidebar()" class="danger-hover" style="background: none; border: none; color: var(--text-secondary); cursor: pointer; font-size: 18px; padding: 4px 8px;"><i class="fas fa-times"></i></button>
2525
  </div>
2526
  </div>
2527
  <div class="friend-search-container">
 
2545
  <div class="chat-header-title" id="chatTitle">💬 Select a friend to chat</div>
2546
  </div>
2547
  <div class="chat-header-actions">
2548
+ <span class="build-badge mobile-essential" id="buildVersionBadge" title="Frontend build version">v2.3.0</span>
2549
  <button class="header-btn" onclick="toggleArchiveChat()" title="Archive chat">
2550
  <i class="fas fa-archive"></i>
2551
  </button>
2552
+ <button class="header-btn mobile-essential" onclick="startVideoCall()" title="Video call">
2553
  <i class="fas fa-video"></i>
2554
  </button>
2555
+ <button class="header-btn mobile-essential" onclick="togglePinCurrentChat()" title="Pin/Unpin chat" id="pinChatBtn">
2556
+ <i class="fas fa-thumbtack"></i>
2557
+ </button>
2558
  <button class="header-btn success" onclick="addCurrentChatToList()" title="Add chat to list">
2559
  <i class="fas fa-bookmark"></i>
2560
  </button>
2561
+ <button class="header-btn danger-hover mobile-essential" onclick="closeCurrentChat()" title="Close Chat" id="closeChatBtn" style="display: none;">
2562
  <i class="fas fa-times-circle"></i>
2563
  </button>
2564
+ <button class="header-btn mobile-essential" onclick="toggleMobileMembers()" title="View members">
2565
+ <i class="fas fa-users"></i>
2566
+ </button>
2567
+ <button class="header-btn mobile-essential" onclick="openQuickActionsMenu()" title="More options">
2568
+ <i class="fas fa-ellipsis-v"></i>
2569
+ </button>
2570
+ <button class="header-btn mobile-essential" onclick="openSettings()" title="Settings">
2571
  <i class="fas fa-cog"></i>
2572
  </button>
2573
+ <button class="header-btn mobile-essential" onclick="openProfile()" title="Profile">
2574
  <i class="fas fa-user-circle"></i>
2575
  </button>
2576
+ <button class="header-btn danger-hover mobile-essential" onclick="logout()" title="Logout">
2577
  <i class="fas fa-sign-out-alt"></i>
2578
  </button>
2579
  </div>
 
2994
 
2995
  </div>
2996
 
2997
+ <!-- COMMUNITY DETAILS MODAL -->
2998
+ <div class="community-modal" id="communityDetailsModal">
2999
+ <div class="community-modal-content">
3000
+ <div class="community-modal-header">
3001
+ <h3 id="communityDetailsTitle">Community</h3>
3002
+ <button class="header-btn" onclick="closeCommunityDetails()"><i class="fas fa-times"></i></button>
3003
+ </div>
3004
+ <div class="community-modal-body" id="communityDetailsBody"></div>
3005
+ </div>
3006
+ </div>
3007
+
3008
  <!-- QR CODE MODAL -->
3009
  <div class="qr-modal" id="qrModal">
3010
  <div class="qr-modal-content">
 
3031
  </div>
3032
  </div>
3033
 
3034
+ <!-- MOBILE BOTTOM NAVIGATION -->
3035
+ <div class="mobile-bottom-nav" id="mobileBottomNav">
3036
+ <button class="mobile-nav-item active" onclick="showMobileSection('chats')" data-section="chats">
3037
+ <i class="fas fa-comment"></i>
3038
+ <span>Chats</span>
3039
+ </button>
3040
+ <button class="mobile-nav-item" onclick="showMobileSection('communities')" data-section="communities">
3041
+ <i class="fas fa-globe"></i>
3042
+ <span>Communities</span>
3043
+ </button>
3044
+ <button class="mobile-nav-item" onclick="showMobileSection('groups')" data-section="groups">
3045
+ <i class="fas fa-user-group"></i>
3046
+ <span>Groups</span>
3047
+ </button>
3048
+ <button class="mobile-nav-item" onclick="showMobileSection('friends')" data-section="friends">
3049
+ <i class="fas fa-users"></i>
3050
+ <span>Friends</span>
3051
+ </button>
3052
+ <button class="mobile-nav-item" onclick="showMobileSection('settings')" data-section="settings">
3053
+ <i class="fas fa-cog"></i>
3054
+ <span>Settings</span>
3055
+ </button>
3056
+ </div>
3057
+
3058
+ <!-- SIDEBAR OVERLAY FOR MOBILE -->
3059
+ <div class="sidebar-overlay" id="sidebarOverlay" onclick="closeMobileSidebar()"></div>
3060
+
3061
+ <!-- MOBILE MEMBERS PANEL -->
3062
+ <div class="mobile-members-panel" id="mobileMembersPanel">
3063
+ <div class="mobile-members-header">
3064
+ <h3>👥 Online Members</h3>
3065
+ <button class="header-btn" onclick="toggleMobileMembers()" style="font-size: 20px;">
3066
+ <i class="fas fa-times"></i>
3067
+ </button>
3068
+ </div>
3069
+ <div class="mobile-members-list" id="mobileMembersList">
3070
+ <!-- Members will be populated here -->
3071
+ </div>
3072
+ </div>
3073
+
3074
  <!-- CUSTOM DIALOG MODAL (replaces alert/confirm/prompt) -->
3075
  <div id="customDialog" style="display: none;"></div>
3076
 
3077
  <script src="/socket.io/socket.io.js"></script>
3078
  <script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
3079
+ <script src="client.js?v=2.2.9"></script>
3080
  </body>
3081
  </html>
3082
+
package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
  {
2
  "name": "discord-like-app",
3
- "version": "2.0.0",
4
  "lockfileVersion": 3,
5
  "requires": true,
6
  "packages": {
7
  "": {
8
  "name": "discord-like-app",
9
- "version": "2.0.0",
10
  "dependencies": {
11
  "bcryptjs": "^2.4.3",
12
  "body-parser": "^1.20.2",
 
1
  {
2
  "name": "discord-like-app",
3
+ "version": "2.2.9",
4
  "lockfileVersion": 3,
5
  "requires": true,
6
  "packages": {
7
  "": {
8
  "name": "discord-like-app",
9
+ "version": "2.2.9",
10
  "dependencies": {
11
  "bcryptjs": "^2.4.3",
12
  "body-parser": "^1.20.2",
package.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
  "name": "discord-like-app",
3
- "version": "2.2.0",
4
  "main": "server.js",
5
  "scripts": {
6
  "dev": "nodemon server.js",
@@ -9,17 +9,17 @@
9
  "https": "powershell -ExecutionPolicy Bypass -File ./setup-https.ps1"
10
  },
11
  "dependencies": {
 
 
 
 
12
  "express": "^4.18.2",
13
- "socket.io": "^4.5.4",
14
- "sequelize": "^6.35.2",
15
- "pg": "^8.11.3",
16
  "jsonwebtoken": "^9.0.2",
17
- "bcryptjs": "^2.4.3",
18
  "multer": "^1.4.5-lts.1",
19
- "dotenv": "^16.3.1",
20
- "cors": "^2.8.5",
21
- "body-parser": "^1.20.2",
22
- "uuid": "^9.0.0",
23
- "nodemon": "^3.0.1"
24
  }
25
  }
 
1
  {
2
  "name": "discord-like-app",
3
+ "version": "2.3.0",
4
  "main": "server.js",
5
  "scripts": {
6
  "dev": "nodemon server.js",
 
9
  "https": "powershell -ExecutionPolicy Bypass -File ./setup-https.ps1"
10
  },
11
  "dependencies": {
12
+ "bcryptjs": "^2.4.3",
13
+ "body-parser": "^1.20.2",
14
+ "cors": "^2.8.5",
15
+ "dotenv": "^16.3.1",
16
  "express": "^4.18.2",
 
 
 
17
  "jsonwebtoken": "^9.0.2",
 
18
  "multer": "^1.4.5-lts.1",
19
+ "nodemon": "^3.0.1",
20
+ "pg": "^8.11.3",
21
+ "sequelize": "^6.35.2",
22
+ "socket.io": "^4.5.4",
23
+ "uuid": "^9.0.0"
24
  }
25
  }
server.js CHANGED
@@ -14,6 +14,34 @@ require('dotenv').config();
14
 
15
  const app = express();
16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  // Middleware
18
  app.use(bodyParser.json({ limit: '50mb' }));
19
  app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
@@ -30,17 +58,26 @@ app.use((req, res, next) => {
30
 
31
  app.use(express.static(__dirname));
32
 
 
 
 
 
 
 
 
 
 
33
  // ======================== DATABASE CONNECTION ========================
34
 
35
- const sequelize = new Sequelize(
36
- process.env.DATABASE_URL || 'postgres://postgres:postgres@localhost:5432/discord-app',
37
- {
38
- dialect: 'postgres',
39
- logging: false,
40
- protocol: process.env.DATABASE_URL ? 'postgres' : undefined,
41
- dialectOptions: process.env.DATABASE_URL ? { ssl: { require: true, rejectUnauthorized: false } } : undefined
42
- }
43
- );
44
 
45
  // ======================== MODELS ========================
46
 
@@ -425,7 +462,32 @@ app.post('/api/profile/upload', authenticateToken, async (req, res) => {
425
  app.post('/api/friends/request', authenticateToken, async (req, res) => {
426
  try {
427
  const { toUserId } = req.body;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
428
  await FriendRequest.create({ fromId: req.user.userId, toId: toUserId });
 
 
429
  res.status(201).json({ message: 'Friend request sent' });
430
  } catch (error) {
431
  res.status(500).json({ error: error.message });
@@ -450,6 +512,12 @@ app.post('/api/friends/accept', authenticateToken, async (req, res) => {
450
  const request = await FriendRequest.findByPk(requestId);
451
 
452
  if (!request) return res.status(404).json({ error: 'Request not found' });
 
 
 
 
 
 
453
 
454
  request.status = 'accepted';
455
  await request.save();
@@ -460,12 +528,46 @@ app.post('/api/friends/accept', authenticateToken, async (req, res) => {
460
  await fromUser.addFriend(toUser);
461
  await toUser.addFriend(fromUser);
462
 
 
 
 
 
 
 
463
  res.json({ message: 'Friend request accepted' });
464
  } catch (error) {
465
  res.status(500).json({ error: error.message });
466
  }
467
  });
468
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
469
  app.get('/api/friends', authenticateToken, async (req, res) => {
470
  try {
471
  const user = await User.findByPk(req.user.userId, {
@@ -527,11 +629,6 @@ app.post('/api/dms', authenticateToken, async (req, res) => {
527
 
528
  if (!toUser) return res.status(404).json({ error: 'User not found' });
529
 
530
- const friends = await fromUser.getFriends({ where: { id: toUserId } });
531
- if (friends.length === 0) {
532
- return res.status(403).json({ error: 'Can only DM friends' });
533
- }
534
-
535
  const dm = await DirectMessage.create({
536
  messageId: uuidv4(),
537
  fromId: req.user.userId,
@@ -549,6 +646,30 @@ app.post('/api/dms', authenticateToken, async (req, res) => {
549
  }
550
  });
551
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
552
  // ======================== SHOP ROUTES ========================
553
 
554
  app.get('/api/shop', authenticateToken, async (req, res) => {
@@ -623,13 +744,47 @@ const { server, protocol } = createServer(app);
623
  const io = require('socket.io')(server, {
624
  maxHttpBufferSize: 40 * 1024 * 1024,
625
  cors: {
626
- origin: process.env.CORS_ORIGIN ? process.env.CORS_ORIGIN.split(',') : '*',
 
 
 
627
  methods: ['GET', 'POST']
628
  }
629
  });
630
 
631
  const onlineUsers = new Map();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
632
 
 
 
633
  io.on('connection', (socket) => {
634
  console.log('User connected:', socket.id);
635
 
@@ -683,13 +838,6 @@ io.on('connection', (socket) => {
683
  return;
684
  }
685
 
686
- const isSelfChat = String(toUserId) === String(socket.userId);
687
- const friends = isSelfChat ? [] : await fromUser.getFriends({ where: { id: toUserId } });
688
- if (!isSelfChat && friends.length === 0) {
689
- socket.emit('error', 'Can only DM friends');
690
- return;
691
- }
692
-
693
  const dm = await DirectMessage.create({
694
  messageId: uuidv4(),
695
  fromId: socket.userId,
@@ -701,8 +849,7 @@ io.on('connection', (socket) => {
701
  mediaUrl: mediaUrl || null
702
  });
703
 
704
- const roomId = [socket.userId, toUserId].sort().join('_');
705
- io.to(roomId).emit('dm message', {
706
  messageId: dm.messageId,
707
  from: socket.userId,
708
  fromUsername: fromUser.username,
@@ -713,7 +860,19 @@ io.on('connection', (socket) => {
713
  mediaType: dm.mediaType,
714
  mediaUrl: dm.mediaUrl,
715
  timestamp: dm.createdAt
716
- });
 
 
 
 
 
 
 
 
 
 
 
 
717
  } catch (error) {
718
  socket.emit('error', error.message);
719
  }
@@ -735,6 +894,270 @@ io.on('connection', (socket) => {
735
  });
736
  });
737
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
738
  socket.on('disconnect', async () => {
739
  try {
740
  const userData = onlineUsers.get(socket.id);
@@ -808,8 +1231,27 @@ async function migrateMediaTypeEnum() {
808
  }
809
  }
810
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
811
  sequelize.sync({ alter: true }).then(async () => {
812
  await migrateMediaTypeEnum();
 
813
  server.listen(PORT, HOST, () => {
814
  const networkAddress = getLocalNetworkAddress();
815
  console.log(`
@@ -836,3 +1278,7 @@ sequelize.sync({ alter: true }).then(async () => {
836
  console.error('Database sync error:', err);
837
  process.exit(1);
838
  });
 
 
 
 
 
14
 
15
  const app = express();
16
 
17
+ // Configure CORS
18
+ const cors = require('cors');
19
+ const allowedOrigins = process.env.CORS_ORIGIN ? process.env.CORS_ORIGIN.split(',').map(o => o.trim()).filter(Boolean) : ['*'];
20
+
21
+ function isAllowedOrigin(origin) {
22
+ if (!origin) return true;
23
+ if (allowedOrigins.includes('*')) return true;
24
+
25
+ return allowedOrigins.some((allowed) => {
26
+ if (allowed === origin) return true;
27
+ if (allowed.endsWith(':*')) {
28
+ const prefix = allowed.slice(0, -2);
29
+ return origin.startsWith(prefix + ':');
30
+ }
31
+ return false;
32
+ });
33
+ }
34
+
35
+ app.use(cors({
36
+ origin: (origin, callback) => {
37
+ if (isAllowedOrigin(origin)) return callback(null, true);
38
+ return callback(new Error('Not allowed by CORS'));
39
+ },
40
+ credentials: true,
41
+ methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
42
+ allowedHeaders: ['Content-Type', 'Authorization']
43
+ }));
44
+
45
  // Middleware
46
  app.use(bodyParser.json({ limit: '50mb' }));
47
  app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
 
58
 
59
  app.use(express.static(__dirname));
60
 
61
+ app.get('/api/version', (req, res) => {
62
+ try {
63
+ const packageJson = require('./package.json');
64
+ res.json({ version: packageJson.version || 'unknown' });
65
+ } catch (error) {
66
+ res.status(500).json({ version: 'unknown' });
67
+ }
68
+ });
69
+
70
  // ======================== DATABASE CONNECTION ========================
71
 
72
+ const connectionString = process.env.DATABASE_URL || 'postgres://postgres:postgres@localhost:5432/discord-app';
73
+ const isLocalConnection = connectionString.includes('localhost') || connectionString.includes('127.0.0.1');
74
+
75
+ const sequelize = new Sequelize(connectionString, {
76
+ dialect: 'postgres',
77
+ logging: false,
78
+ protocol: 'postgres',
79
+ dialectOptions: isLocalConnection ? {} : { ssl: { require: true, rejectUnauthorized: false } }
80
+ });
81
 
82
  // ======================== MODELS ========================
83
 
 
462
  app.post('/api/friends/request', authenticateToken, async (req, res) => {
463
  try {
464
  const { toUserId } = req.body;
465
+
466
+ if (!toUserId) return res.status(400).json({ error: 'toUserId is required' });
467
+ if (String(toUserId) === String(req.user.userId)) return res.status(400).json({ error: 'Cannot send request to yourself' });
468
+
469
+ const targetUser = await User.findByPk(toUserId);
470
+ if (!targetUser) return res.status(404).json({ error: 'User not found' });
471
+
472
+ const fromUser = await User.findByPk(req.user.userId);
473
+ const existingFriend = await fromUser.getFriends({ where: { id: toUserId } });
474
+ if (existingFriend.length > 0) return res.status(400).json({ error: 'Already friends' });
475
+
476
+ const existingPending = await FriendRequest.findOne({
477
+ where: {
478
+ status: 'pending',
479
+ [Op.or]: [
480
+ { fromId: req.user.userId, toId: toUserId },
481
+ { fromId: toUserId, toId: req.user.userId }
482
+ ]
483
+ }
484
+ });
485
+
486
+ if (existingPending) return res.status(400).json({ error: 'Friend request already pending' });
487
+
488
  await FriendRequest.create({ fromId: req.user.userId, toId: toUserId });
489
+
490
+ io.emit('friend request update', { toUserId });
491
  res.status(201).json({ message: 'Friend request sent' });
492
  } catch (error) {
493
  res.status(500).json({ error: error.message });
 
512
  const request = await FriendRequest.findByPk(requestId);
513
 
514
  if (!request) return res.status(404).json({ error: 'Request not found' });
515
+ if (String(request.toId) !== String(req.user.userId)) {
516
+ return res.status(403).json({ error: 'Not allowed to accept this request' });
517
+ }
518
+ if (request.status !== 'pending') {
519
+ return res.status(400).json({ error: `Request already ${request.status}` });
520
+ }
521
 
522
  request.status = 'accepted';
523
  await request.save();
 
528
  await fromUser.addFriend(toUser);
529
  await toUser.addFriend(fromUser);
530
 
531
+ io.emit('friend request accepted', {
532
+ requestId: request.id,
533
+ fromId: request.fromId,
534
+ toId: request.toId
535
+ });
536
+
537
  res.json({ message: 'Friend request accepted' });
538
  } catch (error) {
539
  res.status(500).json({ error: error.message });
540
  }
541
  });
542
 
543
+ app.post('/api/friends/reject', authenticateToken, async (req, res) => {
544
+ try {
545
+ const { requestId } = req.body;
546
+ const request = await FriendRequest.findByPk(requestId);
547
+
548
+ if (!request) return res.status(404).json({ error: 'Request not found' });
549
+ if (String(request.toId) !== String(req.user.userId)) {
550
+ return res.status(403).json({ error: 'Not allowed to reject this request' });
551
+ }
552
+ if (request.status !== 'pending') {
553
+ return res.status(400).json({ error: `Request already ${request.status}` });
554
+ }
555
+
556
+ request.status = 'rejected';
557
+ await request.save();
558
+
559
+ io.emit('friend request rejected', {
560
+ requestId: request.id,
561
+ fromId: request.fromId,
562
+ toId: request.toId
563
+ });
564
+
565
+ res.json({ message: 'Friend request rejected' });
566
+ } catch (error) {
567
+ res.status(500).json({ error: error.message });
568
+ }
569
+ });
570
+
571
  app.get('/api/friends', authenticateToken, async (req, res) => {
572
  try {
573
  const user = await User.findByPk(req.user.userId, {
 
629
 
630
  if (!toUser) return res.status(404).json({ error: 'User not found' });
631
 
 
 
 
 
 
632
  const dm = await DirectMessage.create({
633
  messageId: uuidv4(),
634
  fromId: req.user.userId,
 
646
  }
647
  });
648
 
649
+ app.delete('/api/dms/:friendId', authenticateToken, async (req, res) => {
650
+ try {
651
+ const { friendId } = req.params;
652
+ await DirectMessage.destroy({
653
+ where: {
654
+ [Op.or]: [
655
+ { fromId: req.user.userId, toId: friendId },
656
+ { fromId: friendId, toId: req.user.userId }
657
+ ]
658
+ }
659
+ });
660
+
661
+ // Notify the other user to clear their chat UI as well
662
+ emitToUser(friendId, 'dm history cleared', {
663
+ fromUserId: req.user.userId,
664
+ clearedAt: new Date().toISOString()
665
+ });
666
+
667
+ res.json({ message: 'DM history cleared for both users' });
668
+ } catch (error) {
669
+ res.status(500).json({ error: error.message });
670
+ }
671
+ });
672
+
673
  // ======================== SHOP ROUTES ========================
674
 
675
  app.get('/api/shop', authenticateToken, async (req, res) => {
 
744
  const io = require('socket.io')(server, {
745
  maxHttpBufferSize: 40 * 1024 * 1024,
746
  cors: {
747
+ origin: (origin, callback) => {
748
+ if (isAllowedOrigin(origin)) return callback(null, true);
749
+ return callback(new Error('Not allowed by Socket.IO CORS'));
750
+ },
751
  methods: ['GET', 'POST']
752
  }
753
  });
754
 
755
  const onlineUsers = new Map();
756
+ const roomMessages = new Map();
757
+
758
+ function normalizeRoomKey(roomType, roomName) {
759
+ return String(roomType || 'community') + ':' + String(roomName || '').trim().toLowerCase();
760
+ }
761
+
762
+ function getRoomMessages(roomType, roomName) {
763
+ const key = normalizeRoomKey(roomType, roomName);
764
+ return roomMessages.get(key) || [];
765
+ }
766
+
767
+ function appendRoomMessage(roomType, roomName, payload) {
768
+ const key = normalizeRoomKey(roomType, roomName);
769
+ const messages = roomMessages.get(key) || [];
770
+ messages.push(payload);
771
+ if (messages.length > 300) {
772
+ messages.splice(0, messages.length - 300);
773
+ }
774
+ roomMessages.set(key, messages);
775
+ }
776
+
777
+ function emitToUser(userId, eventName, payload) {
778
+ let delivered = 0;
779
+ for (const [socketId, userData] of onlineUsers.entries()) {
780
+ if (String(userData.userId) === String(userId)) {
781
+ io.to(socketId).emit(eventName, payload);
782
+ delivered += 1;
783
+ }
784
+ }
785
 
786
+ return delivered;
787
+ }
788
  io.on('connection', (socket) => {
789
  console.log('User connected:', socket.id);
790
 
 
838
  return;
839
  }
840
 
 
 
 
 
 
 
 
841
  const dm = await DirectMessage.create({
842
  messageId: uuidv4(),
843
  fromId: socket.userId,
 
849
  mediaUrl: mediaUrl || null
850
  });
851
 
852
+ const dmPayload = {
 
853
  messageId: dm.messageId,
854
  from: socket.userId,
855
  fromUsername: fromUser.username,
 
860
  mediaType: dm.mediaType,
861
  mediaUrl: dm.mediaUrl,
862
  timestamp: dm.createdAt
863
+ };
864
+
865
+ // Always deliver directly to both users so messages arrive even if a DM room isn't currently joined.
866
+ emitToUser(socket.userId, 'dm message', dmPayload);
867
+ const deliveredToRecipient = emitToUser(toUserId, 'dm message', dmPayload);
868
+
869
+ if (deliveredToRecipient === 0) {
870
+ socket.emit('dm delivery status', {
871
+ messageId: dm.messageId,
872
+ toUserId,
873
+ delivered: false
874
+ });
875
+ }
876
  } catch (error) {
877
  socket.emit('error', error.message);
878
  }
 
894
  });
895
  });
896
 
897
+ socket.on('community typing', (data) => {
898
+ const roomName = data?.community;
899
+ if (!roomName) return;
900
+ const roomKey = normalizeRoomKey('community', roomName);
901
+ socket.broadcast.to(roomKey).emit('community user typing', {
902
+ community: roomName,
903
+ userId: socket.userId,
904
+ username: socket.username
905
+ });
906
+ });
907
+
908
+ socket.on('community stop typing', (data) => {
909
+ const roomName = data?.community;
910
+ if (!roomName) return;
911
+ const roomKey = normalizeRoomKey('community', roomName);
912
+ socket.broadcast.to(roomKey).emit('community user stop typing', {
913
+ community: roomName,
914
+ userId: socket.userId,
915
+ username: socket.username
916
+ });
917
+ });
918
+
919
+ socket.on('room typing', (data) => {
920
+ const roomName = data?.roomName || data?.roomId;
921
+ if (!roomName) return;
922
+ const roomType = data?.roomType || 'group';
923
+ const roomKey = normalizeRoomKey(roomType, roomName);
924
+ socket.broadcast.to(roomKey).emit('room user typing', {
925
+ roomType,
926
+ roomId: roomName,
927
+ userId: socket.userId,
928
+ username: socket.username
929
+ });
930
+ });
931
+
932
+ socket.on('room stop typing', (data) => {
933
+ const roomName = data?.roomName || data?.roomId;
934
+ if (!roomName) return;
935
+ const roomType = data?.roomType || 'group';
936
+ const roomKey = normalizeRoomKey(roomType, roomName);
937
+ socket.broadcast.to(roomKey).emit('room user stop typing', {
938
+ roomType,
939
+ roomId: roomName,
940
+ userId: socket.userId,
941
+ username: socket.username
942
+ });
943
+ });
944
+
945
+ socket.on('join room chat', (data) => {
946
+ const roomType = data?.roomType;
947
+ const roomName = data?.roomName;
948
+ if (!roomType || !roomName) return;
949
+
950
+ const roomKey = normalizeRoomKey(roomType, roomName);
951
+
952
+ if (socket.currentRoomKey && socket.currentRoomKey !== roomKey) {
953
+ socket.leave(socket.currentRoomKey);
954
+ }
955
+
956
+ socket.join(roomKey);
957
+ socket.currentRoomKey = roomKey;
958
+
959
+ socket.emit('room chat history', {
960
+ roomType,
961
+ roomName,
962
+ messages: getRoomMessages(roomType, roomName)
963
+ });
964
+ });
965
+
966
+ socket.on('leave room chat', (data) => {
967
+ const roomType = data?.roomType;
968
+ const roomName = data?.roomName;
969
+ if (!roomType || !roomName) return;
970
+
971
+ const roomKey = normalizeRoomKey(roomType, roomName);
972
+ socket.leave(roomKey);
973
+
974
+ if (socket.currentRoomKey === roomKey) {
975
+ socket.currentRoomKey = null;
976
+ }
977
+ });
978
+
979
+ socket.on('send room message', (data) => {
980
+ const roomType = data?.roomType;
981
+ const roomName = data?.roomName;
982
+ if (!roomType || !roomName) return;
983
+
984
+ const messagePayload = {
985
+ messageId: uuidv4(),
986
+ from: socket.userId,
987
+ fromUsername: socket.username,
988
+ fromAvatar: socket.userAvatar || null,
989
+ content: data?.content || '',
990
+ mediaType: data?.mediaType || 'text',
991
+ mediaUrl: data?.mediaUrl || null,
992
+ roomType,
993
+ roomName,
994
+ timestamp: new Date().toISOString()
995
+ };
996
+
997
+ appendRoomMessage(roomType, roomName, messagePayload);
998
+ const roomKey = normalizeRoomKey(roomType, roomName);
999
+ io.to(roomKey).emit('room message', messagePayload);
1000
+ });
1001
+
1002
+ socket.on('clear room chat', (data) => {
1003
+ const roomType = data?.roomType;
1004
+ const roomName = data?.roomName;
1005
+ if (!roomType || !roomName) return;
1006
+
1007
+ const roomKey = normalizeRoomKey(roomType, roomName);
1008
+ roomMessages.set(roomKey, []);
1009
+ io.to(roomKey).emit('room chat cleared', { roomType, roomName, clearedBy: socket.userId });
1010
+ });
1011
+ socket.on('send friend request', async (data) => {
1012
+ try {
1013
+ const toUserId = data?.toUserId;
1014
+ if (!toUserId) {
1015
+ socket.emit('error', 'Target user is required');
1016
+ return;
1017
+ }
1018
+
1019
+ if (String(toUserId) === String(socket.userId)) {
1020
+ socket.emit('error', 'Cannot send request to yourself');
1021
+ return;
1022
+ }
1023
+
1024
+ const targetUser = await User.findByPk(toUserId);
1025
+ if (!targetUser) {
1026
+ socket.emit('error', 'User not found');
1027
+ return;
1028
+ }
1029
+
1030
+ const fromUser = await User.findByPk(socket.userId);
1031
+ const existingFriend = await fromUser.getFriends({ where: { id: toUserId } });
1032
+ if (existingFriend.length > 0) {
1033
+ socket.emit('error', 'Already friends');
1034
+ return;
1035
+ }
1036
+
1037
+ const existingPending = await FriendRequest.findOne({
1038
+ where: {
1039
+ status: 'pending',
1040
+ [Op.or]: [
1041
+ { fromId: socket.userId, toId: toUserId },
1042
+ { fromId: toUserId, toId: socket.userId }
1043
+ ]
1044
+ }
1045
+ });
1046
+
1047
+ if (existingPending) {
1048
+ socket.emit('error', 'Friend request already pending');
1049
+ return;
1050
+ }
1051
+
1052
+ const newRequest = await FriendRequest.create({ fromId: socket.userId, toId: toUserId });
1053
+
1054
+ socket.emit('friend request sent', { requestId: newRequest.id, toUserId });
1055
+ emitToUser(toUserId, 'friend request received', {
1056
+ requestId: newRequest.id,
1057
+ fromId: socket.userId,
1058
+ fromUsername: socket.username,
1059
+ fromAvatar: socket.userAvatar || null
1060
+ });
1061
+ } catch (error) {
1062
+ socket.emit('error', error.message);
1063
+ }
1064
+ });
1065
+
1066
+ socket.on('start video call', async (data) => {
1067
+ try {
1068
+ const targetId = data?.targetId;
1069
+ if (!targetId) {
1070
+ socket.emit('error', 'Call target is required');
1071
+ return;
1072
+ }
1073
+
1074
+ const targetUser = await User.findByPk(targetId);
1075
+ if (!targetUser) {
1076
+ socket.emit('error', 'User not found');
1077
+ return;
1078
+ }
1079
+
1080
+ const callId = uuidv4();
1081
+ const payload = {
1082
+ callId,
1083
+ fromId: socket.userId,
1084
+ fromUsername: socket.username,
1085
+ fromAvatar: socket.userAvatar || null,
1086
+ toId: targetId,
1087
+ type: data?.type || 'dm',
1088
+ startedAt: new Date().toISOString()
1089
+ };
1090
+
1091
+ const deliveredToTarget = emitToUser(targetId, 'incoming video call', payload);
1092
+ socket.emit('video call ringing', {
1093
+ ...payload,
1094
+ deliveredToTarget: deliveredToTarget > 0
1095
+ });
1096
+
1097
+ if (deliveredToTarget === 0) {
1098
+ socket.emit('video call unavailable', {
1099
+ callId,
1100
+ targetId,
1101
+ message: 'User is currently offline or not connected.'
1102
+ });
1103
+ }
1104
+ } catch (error) {
1105
+ socket.emit('error', error.message);
1106
+ }
1107
+ });
1108
+
1109
+ socket.on('accept video call', (data) => {
1110
+ const callId = data?.callId;
1111
+ const targetCallerId = data?.toId;
1112
+ if (!callId || !targetCallerId) return;
1113
+
1114
+ emitToUser(targetCallerId, 'video call accepted', {
1115
+ callId,
1116
+ byUserId: socket.userId,
1117
+ byUsername: socket.username,
1118
+ acceptedAt: new Date().toISOString()
1119
+ });
1120
+ });
1121
+
1122
+ socket.on('reject video call', (data) => {
1123
+ const callId = data?.callId;
1124
+ const targetCallerId = data?.toId;
1125
+ if (!callId || !targetCallerId) return;
1126
+
1127
+ emitToUser(targetCallerId, 'video call rejected', {
1128
+ callId,
1129
+ byUserId: socket.userId,
1130
+ byUsername: socket.username,
1131
+ rejectedAt: new Date().toISOString()
1132
+ });
1133
+ });
1134
+
1135
+ socket.on('end video call', (data) => {
1136
+ const callId = data?.callId;
1137
+ const peerUserId = data?.toId;
1138
+ if (!callId || !peerUserId) return;
1139
+
1140
+ emitToUser(peerUserId, 'video call ended', {
1141
+ callId,
1142
+ byUserId: socket.userId,
1143
+ byUsername: socket.username,
1144
+ endedAt: new Date().toISOString()
1145
+ });
1146
+ });
1147
+
1148
+ socket.on('video signal', (data) => {
1149
+ const toId = data?.toId;
1150
+ const signal = data?.signal;
1151
+ if (!toId || !signal) return;
1152
+
1153
+ emitToUser(toId, 'video signal', {
1154
+ fromId: socket.userId,
1155
+ fromUsername: socket.username,
1156
+ callId: data?.callId || null,
1157
+ signal
1158
+ });
1159
+ });
1160
+
1161
  socket.on('disconnect', async () => {
1162
  try {
1163
  const userData = onlineUsers.get(socket.id);
 
1231
  }
1232
  }
1233
 
1234
+ async function seedShopItems() {
1235
+ const existingCount = await ShopItem.count();
1236
+ if (existingCount > 0) return;
1237
+
1238
+ const items = [
1239
+ { itemId: 'banner_neon', name: 'Neon Banner Effect', description: 'Animated neon gradient banner for your profile.', price: 450, category: 'Banners', image: '' },
1240
+ { itemId: 'badge_founder', name: 'Founder Badge', description: 'Exclusive badge shown next to your username.', price: 320, category: 'Badges', image: '' },
1241
+ { itemId: 'color_pack_burst', name: 'Color Burst Pack', description: 'Unlock vibrant accent color themes.', price: 380, category: 'Themes', image: '' },
1242
+ { itemId: 'chat_fx_glow', name: 'Message Glow FX', description: 'Subtle glow animation for sent messages.', price: 260, category: 'Effects', image: '' },
1243
+ { itemId: 'avatar_ring_aura', name: 'Aura Avatar Ring', description: 'Premium animated ring around your avatar.', price: 520, category: 'Avatar', image: '' },
1244
+ { itemId: 'nameplate_crystal', name: 'Crystal Nameplate', description: 'Polished nameplate style in member list.', price: 410, category: 'Nameplates', image: '' },
1245
+ { itemId: 'theme_midnight', name: 'Midnight Theme Pack', description: 'Elegant dark gradients and UI accents.', price: 290, category: 'Themes', image: '' },
1246
+ { itemId: 'emoji_pack_pro', name: 'Pro Emoji Pack', description: 'Premium emoji reactions collection.', price: 210, category: 'Emotes', image: '' }
1247
+ ];
1248
+
1249
+ await ShopItem.bulkCreate(items);
1250
+ }
1251
+
1252
  sequelize.sync({ alter: true }).then(async () => {
1253
  await migrateMediaTypeEnum();
1254
+ await seedShopItems();
1255
  server.listen(PORT, HOST, () => {
1256
  const networkAddress = getLocalNetworkAddress();
1257
  console.log(`
 
1278
  console.error('Database sync error:', err);
1279
  process.exit(1);
1280
  });
1281
+
1282
+
1283
+
1284
+