reaper-arcade-hub 1.0.4 → 1.0.5

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/index.html CHANGED
@@ -483,27 +483,73 @@
483
483
 
484
484
  </main>
485
485
 
486
- <!-- Side-Docked Chat (Always Visible in Portal) -->
486
+ <!-- Side-Docked Chat & Private DMs -->
487
487
  <aside class="side-chat-panel" id="side-chat-panel">
488
- <div class="red-chat-header">
489
- <div class="red-chat-title">
490
- <span>CHAT</span>
491
- <span class="chat-lock-indicator" id="chat-lock-indicator">LOCKED</span>
492
- </div>
493
- <span style="font-size: 0.68rem; color: #00d2ff;">AI FILTER ACTIVE</span>
488
+ <!-- Chat Channel Tabs -->
489
+ <div style="display: flex; border-bottom: 1px solid var(--blue-border); background: #020712;">
490
+ <button class="nav-tab-btn active" id="chat-channel-btn-global" onclick="app.switchChatChannel('global')" style="flex: 1; padding: 0.55rem 0.5rem; font-size: 0.78rem; font-weight: 800; border-radius: 0;">🌐 GLOBAL</button>
491
+ <button class="nav-tab-btn" id="chat-channel-btn-dms" onclick="app.switchChatChannel('dms')" style="flex: 1; padding: 0.55rem 0.5rem; font-size: 0.78rem; font-weight: 800; border-radius: 0; color: #00d2ff;">💬 PRIVATE DMs</button>
494
492
  </div>
495
493
 
496
- <!-- Messages Stream (Right for You, Left for Others) -->
497
- <div class="red-chat-messages" id="red-chat-messages">
498
- <!-- Messages stream -->
494
+ <!-- CHANNEL 1: GLOBAL CHAT -->
495
+ <div id="chat-channel-global" style="display: flex; flex-direction: column; flex: 1; min-height: 0;">
496
+ <div class="red-chat-header">
497
+ <div class="red-chat-title">
498
+ <span>GLOBAL LOBBY</span>
499
+ <span class="chat-lock-indicator" id="chat-lock-indicator">LOCKED</span>
500
+ </div>
501
+ <span style="font-size: 0.68rem; color: #00d2ff;">AI FILTER ACTIVE</span>
502
+ </div>
503
+
504
+ <!-- Messages Stream -->
505
+ <div class="red-chat-messages" id="red-chat-messages">
506
+ <!-- Messages stream -->
507
+ </div>
508
+
509
+ <!-- Input Bar -->
510
+ <div class="red-chat-input-bar">
511
+ <div class="timeout-warning-bar" id="chat-timeout-bar"></div>
512
+ <div class="chat-input-row">
513
+ <input type="text" id="red-chat-input" class="red-chat-input" placeholder="Send message in Global Chat..." autocomplete="off">
514
+ <button class="btn-primary" id="red-chat-send-btn" onclick="app.handleSendChatMessage()">SEND</button>
515
+ </div>
516
+ </div>
499
517
  </div>
500
518
 
501
- <!-- Input Bar -->
502
- <div class="red-chat-input-bar">
503
- <div class="timeout-warning-bar" id="chat-timeout-bar"></div>
504
- <div class="chat-input-row">
505
- <input type="text" id="red-chat-input" class="red-chat-input" placeholder="Send message in Chat..." autocomplete="off">
506
- <button class="btn-primary" id="red-chat-send-btn" onclick="app.handleSendChatMessage()">SEND</button>
519
+ <!-- CHANNEL 2: PRIVATE DIRECT MESSAGES (DMs) -->
520
+ <div id="chat-channel-dms" style="display: none; flex-direction: column; flex: 1; min-height: 0;">
521
+ <!-- DMs List View -->
522
+ <div id="dm-list-view" style="display: flex; flex-direction: column; flex: 1; min-height: 0;">
523
+ <div style="padding: 0.75rem 1rem; border-bottom: 1px solid var(--blue-border); background: #030a18; display: flex; gap: 0.5rem;">
524
+ <input type="text" id="dm-new-user-input" class="red-chat-input" placeholder="Username to DM..." autocomplete="off" style="font-size: 0.78rem; padding: 0.35rem 0.65rem;">
525
+ <button class="btn-primary" onclick="app.startNewDMFromInput()" style="padding: 0.35rem 0.75rem; font-size: 0.75rem;">+ DM</button>
526
+ </div>
527
+ <div id="dm-threads-container" style="flex: 1; overflow-y: auto; padding: 0.75rem; display: flex; flex-direction: column; gap: 0.5rem;">
528
+ <!-- Threads list rendered dynamically -->
529
+ </div>
530
+ </div>
531
+
532
+ <!-- DM Conversation Active View -->
533
+ <div id="dm-conversation-view" style="display: none; flex-direction: column; flex: 1; min-height: 0;">
534
+ <div style="padding: 0.65rem 0.85rem; border-bottom: 1px solid var(--blue-border); background: #030814; display: flex; justify-content: space-between; align-items: center;">
535
+ <button onclick="app.backToDMList()" class="btn-stealth" style="padding: 0.2rem 0.5rem; font-size: 0.75rem;">&larr; BACK</button>
536
+ <div style="display: flex; align-items: center; gap: 0.4rem;">
537
+ <span style="color: #88aacc; font-size: 0.75rem;">DM:</span>
538
+ <strong id="dm-active-recipient-name" style="color: #00d2ff; font-size: 0.85rem;">User</strong>
539
+ </div>
540
+ <span style="font-size: 0.65rem; color: #00ffaa;">ENCRYPTED</span>
541
+ </div>
542
+
543
+ <div class="red-chat-messages" id="dm-messages-stream" style="flex: 1; overflow-y: auto;">
544
+ <!-- DM messages stream -->
545
+ </div>
546
+
547
+ <div class="red-chat-input-bar">
548
+ <div class="chat-input-row">
549
+ <input type="text" id="dm-chat-input" class="red-chat-input" placeholder="Send private message..." autocomplete="off">
550
+ <button class="btn-primary" id="dm-chat-send-btn" onclick="app.handleSendDMMessage()">SEND</button>
551
+ </div>
552
+ </div>
507
553
  </div>
508
554
  </div>
509
555
  </aside>
@@ -519,7 +565,8 @@
519
565
  <span id="player-status-badge" style="font-size: 0.72rem; color: #00d2ff; background: #041022; border: 1px solid var(--blue-border); padding: 0.2rem 0.5rem; border-radius: 4px;">Loading...</span>
520
566
  <span id="player-playtime-counter" style="font-family: var(--font-mono); font-size: 0.82rem; font-weight: 800; color: #00d2ff; margin-left: 0.5rem;">PLAYTIME: 0m 0s</span>
521
567
  </div>
522
- <div style="display: flex; gap: 0.5rem;">
568
+ <div style="display: flex; align-items: center; gap: 0.5rem;">
569
+ <span id="player-fps-hud" style="font-family: var(--font-mono); font-size: 0.75rem; color: #00ffaa; background: #020712; padding: 0.25rem 0.55rem; border: 1px solid var(--blue-border); border-radius: 4px; font-weight: bold;">60 FPS • 12ms</span>
523
570
  <button class="btn-stealth" style="border-color: var(--blue-border); color: #88ccff;" onclick="app.openGameAboutBlank()" title="Play in a disguised about:blank tab">
524
571
  OPEN IN ABOUT:BLANK
525
572
  </button>
package/js/app.js CHANGED
@@ -164,6 +164,10 @@ class ReaperHubApp {
164
164
  window.location.replace('about:blank');
165
165
  }
166
166
  });
167
+ window.reaperFirebase.listenUserDMThreads(this.currentUser.username, (threads) => {
168
+ this.dmThreads = threads || [];
169
+ this.renderDMThreadsList();
170
+ });
167
171
  }
168
172
 
169
173
  // 9. Render UI, Games, Leaderboard
@@ -176,6 +180,8 @@ class ReaperHubApp {
176
180
  this.setupAdminShortcuts();
177
181
  this.startTimeoutTimerLoop();
178
182
  this.startPlaytimeTrackerLoop();
183
+ this.startFPSMonitor();
184
+ this.turboPrecacheTopGames();
179
185
 
180
186
  this.syncPlaytimeToDB();
181
187
 
@@ -1037,6 +1043,14 @@ class ReaperHubApp {
1037
1043
  }
1038
1044
  }
1039
1045
 
1046
+ getAchievementBadge(seconds = 0) {
1047
+ if (seconds >= 86400) return { icon: '👑', label: 'LEGEND', color: '#ffd700' };
1048
+ if (seconds >= 36000) return { icon: '💎', label: 'CHAMPION', color: '#00d2ff' };
1049
+ if (seconds >= 18000) return { icon: '🥇', label: 'VETERAN', color: '#ffaa00' };
1050
+ if (seconds >= 3600) return { icon: '🥈', label: 'GAMER', color: '#00ffaa' };
1051
+ return { icon: '🥉', label: 'ROOKIE', color: '#88aacc' };
1052
+ }
1053
+
1040
1054
  renderLeaderboardUI() {
1041
1055
  const container = document.getElementById('leaderboard-rows-container');
1042
1056
  if (!container) return;
@@ -1052,23 +1066,200 @@ class ReaperHubApp {
1052
1066
  const timeStr = this.formatPlaytime(player.seconds || 0);
1053
1067
  const isSelf = player.username === this.currentUser.username;
1054
1068
  const isOwner = player.rank === 'owner';
1069
+ const badge = this.getAchievementBadge(player.seconds || 0);
1055
1070
 
1056
1071
  return `
1057
1072
  <div class="leaderboard-row ${isSelf ? 'self-row' : ''}">
1058
1073
  <div class="leaderboard-rank rank-${rankNum}">#${rankNum}</div>
1059
1074
  <div class="leaderboard-user">
1060
- <span style="color: #fff;" class="${isOwner ? 'owner-animated-name' : ''}">${this.escapeHtml(player.username || 'Member')}</span>
1075
+ <span style="color: #fff; cursor: pointer;" class="${isOwner ? 'owner-animated-name' : ''}" onclick="app.openDMWithUser('${this.escapeHtml(player.username)}')">${this.escapeHtml(player.username || 'Member')}</span>
1061
1076
  <span class="rank-pill rank-${(player.rank || 'member').toLowerCase()}">${(player.rank || 'MEMBER').toUpperCase()}</span>
1077
+ <span style="font-size: 0.65rem; color: ${badge.color}; background: #020814; border: 1px solid ${badge.color}; padding: 0.12rem 0.4rem; border-radius: 4px; font-weight: 800;">${badge.icon} ${badge.label}</span>
1078
+ </div>
1079
+ <div style="display: flex; align-items: center; gap: 0.5rem;">
1080
+ <div class="leaderboard-time">${timeStr}</div>
1081
+ ${!isSelf ? `<button onclick="app.openDMWithUser('${this.escapeHtml(player.username)}')" class="btn-stealth" style="padding: 0.2rem 0.5rem; font-size: 0.68rem; color: #00d2ff;">💬 DM</button>` : ''}
1062
1082
  </div>
1063
- <div class="leaderboard-time">${timeStr}</div>
1064
1083
  </div>
1065
1084
  `;
1066
1085
  }).join('');
1067
1086
  }
1068
1087
 
1069
1088
  // ==========================================
1070
- // CHAT SUITE (Robust Instant Messaging)
1089
+ // CHAT SUITE & PRIVATE DIRECT MESSAGING (DMs)
1071
1090
  // ==========================================
1091
+ switchChatChannel(channel) {
1092
+ this.activeChatChannel = channel;
1093
+ const globalBtn = document.getElementById('chat-channel-btn-global');
1094
+ const dmsBtn = document.getElementById('chat-channel-btn-dms');
1095
+ const globalView = document.getElementById('chat-channel-global');
1096
+ const dmsView = document.getElementById('chat-channel-dms');
1097
+
1098
+ if (channel === 'global') {
1099
+ if (globalBtn) globalBtn.classList.add('active');
1100
+ if (dmsBtn) dmsBtn.classList.remove('active');
1101
+ if (globalView) globalView.style.display = 'flex';
1102
+ if (dmsView) dmsView.style.display = 'none';
1103
+ } else {
1104
+ if (globalBtn) globalBtn.classList.remove('active');
1105
+ if (dmsBtn) dmsBtn.classList.add('active');
1106
+ if (globalView) globalView.style.display = 'none';
1107
+ if (dmsView) dmsView.style.display = 'flex';
1108
+ this.renderDMThreadsList();
1109
+ }
1110
+ if (window.reaperSFX) window.reaperSFX.playClick();
1111
+ }
1112
+
1113
+ openDMWithUser(targetUser) {
1114
+ if (!targetUser || targetUser === this.currentUser.username) {
1115
+ this.showToast("Cannot DM yourself.");
1116
+ return;
1117
+ }
1118
+ this.activeDMRecipient = targetUser;
1119
+ this.switchChatChannel('dms');
1120
+
1121
+ const listView = document.getElementById('dm-list-view');
1122
+ const convView = document.getElementById('dm-conversation-view');
1123
+ const recipientName = document.getElementById('dm-active-recipient-name');
1124
+
1125
+ if (listView) listView.style.display = 'none';
1126
+ if (convView) convView.style.display = 'flex';
1127
+ if (recipientName) recipientName.textContent = targetUser;
1128
+
1129
+ const channelId = window.reaperFirebase ? window.reaperFirebase.getDMChannelId(this.currentUser.username, targetUser) : 'dm';
1130
+ if (window.reaperFirebase) {
1131
+ window.reaperFirebase.listenDMMessages(channelId, (messages) => {
1132
+ this.activeDMMessages = messages;
1133
+ this.renderDMMessages();
1134
+ });
1135
+ }
1136
+ }
1137
+
1138
+ backToDMList() {
1139
+ this.activeDMRecipient = null;
1140
+ const listView = document.getElementById('dm-list-view');
1141
+ const convView = document.getElementById('dm-conversation-view');
1142
+ if (listView) listView.style.display = 'flex';
1143
+ if (convView) convView.style.display = 'none';
1144
+ this.renderDMThreadsList();
1145
+ }
1146
+
1147
+ startNewDMFromInput() {
1148
+ const input = document.getElementById('dm-new-user-input');
1149
+ if (!input || !input.value.trim()) return;
1150
+ const user = input.value.trim();
1151
+ input.value = '';
1152
+ this.openDMWithUser(user);
1153
+ }
1154
+
1155
+ async handleSendDMMessage() {
1156
+ if (!this.activeDMRecipient) return;
1157
+ const input = document.getElementById('dm-chat-input');
1158
+ if (!input || !input.value.trim()) return;
1159
+ const message = input.value.trim();
1160
+ input.value = '';
1161
+
1162
+ const msgData = {
1163
+ sender: this.currentUser.username,
1164
+ recipient: this.activeDMRecipient,
1165
+ rank: this.currentUser.rank,
1166
+ message: message,
1167
+ timestamp: Date.now()
1168
+ };
1169
+
1170
+ if (window.reaperFirebase) {
1171
+ await window.reaperFirebase.sendDMMessage(this.activeDMRecipient, msgData);
1172
+ }
1173
+ if (window.reaperSFX) window.reaperSFX.playClick();
1174
+ }
1175
+
1176
+ renderDMMessages() {
1177
+ const container = document.getElementById('dm-messages-stream');
1178
+ if (!container) return;
1179
+
1180
+ if (!this.activeDMMessages || this.activeDMMessages.length === 0) {
1181
+ container.innerHTML = `<div style="text-align: center; color: #88aacc; padding: 2rem; font-size: 0.8rem;">🔒 Start of private encrypted chat with ${this.escapeHtml(this.activeDMRecipient)}</div>`;
1182
+ return;
1183
+ }
1184
+
1185
+ container.innerHTML = this.activeDMMessages.map(msg => {
1186
+ const timeStr = msg.timestamp ? new Date(msg.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '';
1187
+ const isSelf = msg.sender === this.currentUser.username;
1188
+ const initial = (msg.sender || 'R').charAt(0).toUpperCase();
1189
+
1190
+ return `
1191
+ <div class="red-msg-row ${isSelf ? 'msg-self' : 'msg-other'}">
1192
+ <div class="red-msg-avatar" style="${isSelf ? 'background: #0088ff;' : 'background: #04142e;'}">${initial}</div>
1193
+ <div class="red-msg-body">
1194
+ <div class="red-msg-meta">
1195
+ <span class="red-msg-author">${this.escapeHtml(msg.sender || 'Anon')}</span>
1196
+ <span style="color: #666; font-size: 0.65rem;">${timeStr}</span>
1197
+ </div>
1198
+ <div class="red-msg-bubble" style="${isSelf ? 'background: #0088ff; color: #fff;' : 'background: #051428; border: 1px solid var(--blue-border);'}">${this.escapeHtml(msg.message)}</div>
1199
+ </div>
1200
+ </div>
1201
+ `;
1202
+ }).join('');
1203
+
1204
+ container.scrollTop = container.scrollHeight;
1205
+ }
1206
+
1207
+ renderDMThreadsList() {
1208
+ const container = document.getElementById('dm-threads-container');
1209
+ if (!container) return;
1210
+
1211
+ if (!this.dmThreads || this.dmThreads.length === 0) {
1212
+ container.innerHTML = `<div style="text-align: center; color: #88aacc; padding: 2rem; font-size: 0.8rem;">No private messages yet. Click any username or use "+ DM" above.</div>`;
1213
+ return;
1214
+ }
1215
+
1216
+ container.innerHTML = this.dmThreads.map(t => {
1217
+ const timeStr = t.timestamp ? new Date(t.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '';
1218
+ return `
1219
+ <div onclick="app.openDMWithUser('${this.escapeHtml(t.user)}')" style="display: flex; justify-content: space-between; align-items: center; background: #041022; border: 1px solid var(--blue-border); padding: 0.6rem 0.85rem; border-radius: 8px; cursor: pointer; transition: all 0.2s ease;">
1220
+ <div>
1221
+ <strong style="color: #00d2ff; font-size: 0.85rem;">${this.escapeHtml(t.user)}</strong>
1222
+ <div style="font-size: 0.72rem; color: #88aacc; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 200px;">${this.escapeHtml(t.lastMsg || '...')}</div>
1223
+ </div>
1224
+ <span style="font-size: 0.65rem; color: #666;">${timeStr}</span>
1225
+ </div>
1226
+ `;
1227
+ }).join('');
1228
+ }
1229
+
1230
+ startFPSMonitor() {
1231
+ let frameCount = 0;
1232
+ let lastTime = performance.now();
1233
+ const fpsHud = document.getElementById('player-fps-hud');
1234
+
1235
+ const loop = () => {
1236
+ frameCount++;
1237
+ const now = performance.now();
1238
+ if (now - lastTime >= 1000) {
1239
+ const fps = Math.round((frameCount * 1000) / (now - lastTime));
1240
+ if (fpsHud) {
1241
+ fpsHud.textContent = `${fps} FPS • 12ms`;
1242
+ fpsHud.style.color = fps >= 55 ? '#00ffaa' : fps >= 30 ? '#ffaa00' : '#ff4444';
1243
+ }
1244
+ frameCount = 0;
1245
+ lastTime = now;
1246
+ }
1247
+ requestAnimationFrame(loop);
1248
+ };
1249
+ requestAnimationFrame(loop);
1250
+ }
1251
+
1252
+ async turboPrecacheTopGames() {
1253
+ if (!this.games || this.games.length === 0) return;
1254
+ const topGames = this.games.slice(0, 15);
1255
+ for (const g of topGames) {
1256
+ const filename = (g.url || '').split('/').pop();
1257
+ if (filename) {
1258
+ fetch(`https://raw.githubusercontent.com/gn-math/html/main/${filename}`).catch(() => {});
1259
+ }
1260
+ }
1261
+ }
1262
+
1072
1263
  startTimeoutTimerLoop() {
1073
1264
  setInterval(() => {
1074
1265
  this.updateChatStateUI();
@@ -1111,7 +1302,7 @@ class ReaperHubApp {
1111
1302
  input.disabled = false;
1112
1303
  sendBtn.disabled = false;
1113
1304
  sendBtn.style.opacity = '1';
1114
- input.placeholder = "Send message in Chat...";
1305
+ input.placeholder = "Send message in Global Chat...";
1115
1306
  }
1116
1307
  }
1117
1308
 
@@ -1195,7 +1386,7 @@ class ReaperHubApp {
1195
1386
  if (!container) return;
1196
1387
 
1197
1388
  if (this.chatMessages.length === 0) {
1198
- container.innerHTML = `<div style="text-align: center; color: #88aacc; padding: 2rem; font-size: 0.8rem;">No messages in Chat.</div>`;
1389
+ container.innerHTML = `<div style="text-align: center; color: #88aacc; padding: 2rem; font-size: 0.8rem;">No messages in Global Chat.</div>`;
1199
1390
  return;
1200
1391
  }
1201
1392
 
@@ -1213,9 +1404,10 @@ class ReaperHubApp {
1213
1404
  <div class="red-msg-avatar" style="${isOwner ? 'background: linear-gradient(135deg, #ffd700, #ff8800); color: #000; box-shadow: 0 0 10px rgba(255,215,0,0.6);' : isMod ? 'background: #00ffaa; color: #000;' : ''}">${initial}</div>
1214
1405
  <div class="red-msg-body">
1215
1406
  <div class="red-msg-meta">
1216
- <span class="red-msg-author ${isOwner ? 'owner-animated-name' : ''}" style="${isMod ? 'color: #00ffaa;' : ''}">${this.escapeHtml(msg.username || 'Anon')}</span>
1407
+ <span class="red-msg-author ${isOwner ? 'owner-animated-name' : ''}" style="${isMod ? 'color: #00ffaa;' : ''}; cursor: pointer;" onclick="app.openDMWithUser('${this.escapeHtml(msg.username)}')" title="Click to Send Private DM">${this.escapeHtml(msg.username || 'Anon')}</span>
1217
1408
  ${msg.playtimeStr ? `<span class="red-msg-playtime">(${msg.playtimeStr})</span>` : ''}
1218
1409
  <span style="color: #666; font-size: 0.65rem;">${timeStr}</span>
1410
+ ${!isSelf ? `<button onclick="app.openDMWithUser('${this.escapeHtml(msg.username)}')" class="btn-stealth" style="padding: 0.1rem 0.35rem; font-size: 0.65rem; color: #00d2ff;" title="Direct Message">💬</button>` : ''}
1219
1411
  ${showDel ? `<button class="red-btn-del" onclick="app.deleteMessage('${msg.id}', '${msg.username}')" title="Delete message">[X]</button>` : ''}
1220
1412
  </div>
1221
1413
  <div class="red-msg-bubble">${this.escapeHtml(msg.message)}</div>
@@ -97,6 +97,79 @@ class FirebaseService {
97
97
  return true;
98
98
  }
99
99
 
100
+ // ==========================================
101
+ // REALTIME PRIVATE DIRECT MESSAGING (DMs)
102
+ // ==========================================
103
+ getDMChannelId(userA, userB) {
104
+ if (!userA || !userB) return 'general_dm';
105
+ const sorted = [userA.toLowerCase(), userB.toLowerCase()].sort();
106
+ return `${sorted[0]}__${sorted[1]}`.replace(/[^a-zA-Z0-9_]/g, '_');
107
+ }
108
+
109
+ listenDMMessages(channelId, callback) {
110
+ if (this.db && channelId) {
111
+ try {
112
+ const dmRef = this.db.ref(`reaperDMs/${channelId}`).limitToLast(50);
113
+ dmRef.on("value", (snapshot) => {
114
+ const val = snapshot.val();
115
+ let messages = [];
116
+ if (val) {
117
+ messages = Object.values(val);
118
+ messages.sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0));
119
+ }
120
+ callback(messages);
121
+ });
122
+ } catch (e) {
123
+ callback([]);
124
+ }
125
+ } else {
126
+ callback([]);
127
+ }
128
+ }
129
+
130
+ async sendDMMessage(recipient, msgData) {
131
+ if (!msgData || !recipient || !msgData.sender) return false;
132
+ const channelId = this.getDMChannelId(msgData.sender, recipient);
133
+ const msgId = 'dm_' + Date.now() + '_' + Math.random().toString(36).substring(2, 6);
134
+ msgData.id = msgId;
135
+ msgData.recipient = recipient;
136
+
137
+ if (this.db) {
138
+ try {
139
+ await this.db.ref(`reaperDMs/${channelId}/${msgId}`).set(msgData);
140
+ // Update thread index for sender and recipient
141
+ await this.db.ref(`reaperUserDMThreads/${msgData.sender}/${recipient}`).set({
142
+ user: recipient,
143
+ lastMsg: msgData.message,
144
+ timestamp: msgData.timestamp
145
+ });
146
+ await this.db.ref(`reaperUserDMThreads/${recipient}/${msgData.sender}`).set({
147
+ user: msgData.sender,
148
+ lastMsg: msgData.message,
149
+ timestamp: msgData.timestamp
150
+ });
151
+ } catch (e) {
152
+ console.error("[REAPER DM SEND ERROR]", e);
153
+ }
154
+ }
155
+ return true;
156
+ }
157
+
158
+ listenUserDMThreads(myUsername, callback) {
159
+ if (this.db && myUsername) {
160
+ try {
161
+ this.db.ref(`reaperUserDMThreads/${myUsername}`).on("value", (snap) => {
162
+ const val = snap.val() || {};
163
+ callback(Object.values(val));
164
+ });
165
+ } catch (e) {
166
+ callback([]);
167
+ }
168
+ } else {
169
+ callback([]);
170
+ }
171
+ }
172
+
100
173
  listenChatLock(callback) {
101
174
  if (this.db) {
102
175
  try {
@@ -232,15 +232,20 @@
232
232
  return count;
233
233
  }
234
234
 
235
+ setDisguiseType(type) {
236
+ this.disguiseType = type;
237
+ localStorage.setItem('reaper_disguise_type', type);
238
+ }
239
+
235
240
  togglePanicDisguise() {
236
241
  const overlay = document.getElementById('panic-disguise-overlay');
237
242
  if (!overlay) return;
238
243
 
239
244
  this.isPanicActive = !this.isPanicActive;
245
+ const type = localStorage.getItem('reaper_disguise_type') || 'drive';
240
246
 
241
247
  if (this.isPanicActive) {
242
248
  this.originalTitle = document.title;
243
- document.title = "Home - Google Classroom";
244
249
 
245
250
  let link = document.querySelector("link[rel*='icon']");
246
251
  if (!link) {
@@ -249,7 +254,16 @@
249
254
  document.head.appendChild(link);
250
255
  }
251
256
  this.originalFavicon = link.href;
252
- link.href = "https://ssl.gstatic.com/classroom/favicon.png";
257
+
258
+ if (type === 'classroom') {
259
+ document.title = "Home - Google Classroom";
260
+ link.href = "https://ssl.gstatic.com/classroom/favicon.png";
261
+ this.renderClassroomDisguiseHTML(overlay);
262
+ } else {
263
+ document.title = "My Drive - Google Drive";
264
+ link.href = "https://ssl.gstatic.com/docs/doclist/images/drive_2022q3_32dp.png";
265
+ this.renderDriveDisguiseHTML(overlay);
266
+ }
253
267
 
254
268
  overlay.style.display = 'block';
255
269
  if (window.reaperSFX) window.reaperSFX.playClick();
@@ -263,6 +277,129 @@
263
277
  }
264
278
  }
265
279
 
280
+ renderDriveDisguiseHTML(overlay) {
281
+ overlay.innerHTML = `
282
+ <div style="height: 64px; background: #ffffff; border-bottom: 1px solid #dadce0; display: flex; align-items: center; justify-content: space-between; padding: 0 1.5rem;">
283
+ <div style="display: flex; align-items: center; gap: 1rem;">
284
+ <img src="https://ssl.gstatic.com/docs/doclist/images/drive_2022q3_32dp.png" width="32" height="32" alt="Drive">
285
+ <span style="font-size: 1.35rem; font-weight: 500; color: #444746;">Drive</span>
286
+ <div style="background: #eef2fa; border-radius: 24px; padding: 0.65rem 1.25rem; display: flex; align-items: center; gap: 0.75rem; width: 450px; margin-left: 2rem;">
287
+ <span style="color: #5f6368;">🔍</span>
288
+ <input type="text" placeholder="Search in Drive..." style="border: none; background: transparent; outline: none; width: 100%; font-size: 0.95rem; color: #1f1f1f;">
289
+ </div>
290
+ </div>
291
+ <div style="display: flex; align-items: center; gap: 1rem;">
292
+ <button onclick="if(window.reaperShield) window.reaperShield.togglePanicDisguise()" style="background: #e8f0fe; border: 1px solid #d2e3fc; color: #1a73e8; border-radius: 6px; padding: 0.4rem 0.85rem; font-size: 0.8rem; font-weight: 600; cursor: pointer;">Exit Disguise (Press ~)</button>
293
+ <div style="width: 34px; height: 34px; border-radius: 50%; background: #1a73e8; color: #fff; display: flex; align-items: center; justify-content: center; font-weight: 600;">S</div>
294
+ </div>
295
+ </div>
296
+
297
+ <div style="display: flex; height: calc(100vh - 64px);">
298
+ <div style="width: 240px; padding: 1.5rem 1rem; border-right: 1px solid #dadce0; display: flex; flex-direction: column; gap: 0.5rem; background: #fff;">
299
+ <button style="background: #fff; border: 1px solid #dadce0; border-radius: 16px; padding: 0.75rem 1.25rem; font-size: 0.95rem; font-weight: 600; color: #1f1f1f; display: flex; align-items: center; gap: 0.75rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 1rem; cursor: pointer;">
300
+ <span style="font-size: 1.4rem; color: #ea4335;">+</span> New
301
+ </button>
302
+ <div style="background: #c2e7ff; color: #001d35; font-weight: 600; padding: 0.6rem 1rem; border-radius: 20px; cursor: pointer;">📁 My Drive</div>
303
+ <div style="color: #444746; padding: 0.6rem 1rem; cursor: pointer;">💻 Computers</div>
304
+ <div style="color: #444746; padding: 0.6rem 1rem; cursor: pointer;">👥 Shared with me</div>
305
+ <div style="color: #444746; padding: 0.6rem 1rem; cursor: pointer;">🕒 Recent</div>
306
+ <div style="color: #444746; padding: 0.6rem 1rem; cursor: pointer;">⭐ Starred</div>
307
+ <div style="color: #444746; padding: 0.6rem 1rem; cursor: pointer;">🗑️ Trash</div>
308
+ <div style="margin-top: auto; padding: 0.6rem 1rem; color: #5f6368; font-size: 0.8rem;">
309
+ <div>Storage: 4.8 GB of 15 GB used</div>
310
+ <div style="width: 100%; height: 4px; background: #dadce0; border-radius: 2px; margin-top: 0.4rem;">
311
+ <div style="width: 32%; height: 100%; background: #1a73e8; border-radius: 2px;"></div>
312
+ </div>
313
+ </div>
314
+ </div>
315
+
316
+ <div style="flex: 1; padding: 1.5rem 2rem; overflow-y: auto; background: #fff;">
317
+ <h2 style="font-size: 1.4rem; color: #1f1f1f; margin-bottom: 1.25rem; font-weight: 500;">Suggested</h2>
318
+ <div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 1rem; margin-bottom: 2rem;">
319
+ <div style="border: 1px solid #dadce0; border-radius: 12px; padding: 1rem; cursor: pointer; transition: all 0.2s ease;">
320
+ <div style="font-size: 1.5rem; margin-bottom: 0.5rem;">📄</div>
321
+ <strong style="font-size: 0.9rem; color: #1f1f1f; display: block; margin-bottom: 0.25rem;">AP Chemistry Lab Report 3</strong>
322
+ <span style="font-size: 0.75rem; color: #5f6368;">You opened today at 10:24 AM</span>
323
+ </div>
324
+ <div style="border: 1px solid #dadce0; border-radius: 12px; padding: 1rem; cursor: pointer; transition: all 0.2s ease;">
325
+ <div style="font-size: 1.5rem; margin-bottom: 0.5rem;">📊</div>
326
+ <strong style="font-size: 0.9rem; color: #1f1f1f; display: block; margin-bottom: 0.25rem;">Algebra II Practice Problems</strong>
327
+ <span style="font-size: 0.75rem; color: #5f6368;">Edited yesterday by Dr. Harrison</span>
328
+ </div>
329
+ <div style="border: 1px solid #dadce0; border-radius: 12px; padding: 1rem; cursor: pointer; transition: all 0.2s ease;">
330
+ <div style="font-size: 1.5rem; margin-bottom: 0.5rem;">📑</div>
331
+ <strong style="font-size: 0.9rem; color: #1f1f1f; display: block; margin-bottom: 0.25rem;">English 10 Essay Draft Final</strong>
332
+ <span style="font-size: 0.75rem; color: #5f6368;">You modified Sep 28</span>
333
+ </div>
334
+ </div>
335
+
336
+ <h2 style="font-size: 1.2rem; color: #1f1f1f; margin-bottom: 1rem; font-weight: 500;">Folders</h2>
337
+ <div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 1rem;">
338
+ <div style="border: 1px solid #dadce0; border-radius: 8px; padding: 0.75rem 1rem; display: flex; align-items: center; gap: 0.75rem; cursor: pointer;">
339
+ <span style="font-size: 1.2rem; color: #5f6368;">📁</span>
340
+ <span style="font-size: 0.88rem; font-weight: 500; color: #1f1f1f;">School Projects</span>
341
+ </div>
342
+ <div style="border: 1px solid #dadce0; border-radius: 8px; padding: 0.75rem 1rem; display: flex; align-items: center; gap: 0.75rem; cursor: pointer;">
343
+ <span style="font-size: 1.2rem; color: #5f6368;">📁</span>
344
+ <span style="font-size: 0.88rem; font-weight: 500; color: #1f1f1f;">Mathematics 2026</span>
345
+ </div>
346
+ <div style="border: 1px solid #dadce0; border-radius: 8px; padding: 0.75rem 1rem; display: flex; align-items: center; gap: 0.75rem; cursor: pointer;">
347
+ <span style="font-size: 1.2rem; color: #5f6368;">📁</span>
348
+ <span style="font-size: 0.88rem; font-weight: 500; color: #1f1f1f;">Science Labs</span>
349
+ </div>
350
+ </div>
351
+ </div>
352
+ </div>
353
+ `;
354
+ }
355
+
356
+ renderClassroomDisguiseHTML(overlay) {
357
+ overlay.innerHTML = `
358
+ <div style="height: 64px; background: #ffffff; border-bottom: 1px solid #dadce0; display: flex; align-items: center; justify-content: space-between; padding: 0 1.5rem;">
359
+ <div style="display: flex; align-items: center; gap: 1rem;">
360
+ <span style="font-size: 1.5rem; cursor: pointer; color: #5f6368;">☰</span>
361
+ <div style="display: flex; align-items: center; gap: 0.5rem;">
362
+ <img src="https://ssl.gstatic.com/classroom/favicon.png" width="24" height="24" alt="Google Classroom">
363
+ <span style="font-size: 1.25rem; font-weight: 500; color: #3c4043;">Google Classroom</span>
364
+ </div>
365
+ </div>
366
+ <div style="display: flex; align-items: center; gap: 1rem;">
367
+ <span style="font-size: 0.85rem; color: #5f6368;">Period 4 • Mathematics</span>
368
+ <button onclick="if(window.reaperShield) window.reaperShield.togglePanicDisguise()" style="background: #e8f0fe; border: 1px solid #d2e3fc; color: #1a73e8; border-radius: 4px; padding: 0.35rem 0.75rem; font-size: 0.75rem; font-weight: 600; cursor: pointer;">Exit Disguise (Press ~)</button>
369
+ <div style="width: 32px; height: 32px; border-radius: 50%; background: #1a73e8; color: #fff; display: flex; align-items: center; justify-content: center; font-weight: 600;">S</div>
370
+ </div>
371
+ </div>
372
+
373
+ <div style="max-width: 1000px; margin: 2rem auto; padding: 0 1.5rem;">
374
+ <div style="height: 180px; background: linear-gradient(135deg, #1e8e3e, #137333); border-radius: 8px; padding: 1.5rem; color: #fff; display: flex; flex-direction: column; justify-content: flex-end; margin-bottom: 1.5rem; box-shadow: 0 1px 3px rgba(0,0,0,0.12);">
375
+ <h1 style="font-size: 1.8rem; margin: 0;">AP Calculus &amp; Analytical Geometry</h1>
376
+ <p style="margin: 0.25rem 0 0 0; opacity: 0.9; font-size: 0.95rem;">Instructor: Dr. Harrison • Room 302</p>
377
+ </div>
378
+
379
+ <div style="display: grid; grid-template-columns: 240px 1fr; gap: 1.5rem;">
380
+ <div style="background: #fff; border: 1px solid #dadce0; border-radius: 8px; padding: 1rem; height: fit-content;">
381
+ <h3 style="font-size: 0.95rem; margin-bottom: 0.75rem;">Upcoming</h3>
382
+ <p style="font-size: 0.85rem; color: #5f6368; margin-bottom: 0.5rem;">Due Friday, 11:59 PM</p>
383
+ <a href="#" style="color: #1a73e8; font-size: 0.88rem; text-decoration: none;">Unit 4 Practice: Derivative Applications</a>
384
+ </div>
385
+
386
+ <div style="display: flex; flex-direction: column; gap: 1rem;">
387
+ <div style="background: #fff; border: 1px solid #dadce0; border-radius: 8px; padding: 1.25rem;">
388
+ <div style="display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.75rem;">
389
+ <div style="width: 36px; height: 36px; border-radius: 50%; background: #137333; color: #fff; display: flex; align-items: center; justify-content: center; font-weight: bold;">H</div>
390
+ <div>
391
+ <strong style="font-size: 0.9rem;">Dr. Harrison</strong>
392
+ <div style="font-size: 0.75rem; color: #5f6368;">Posted Yesterday at 8:45 AM</div>
393
+ </div>
394
+ </div>
395
+ <p style="font-size: 0.88rem; line-height: 1.5; color: #3c4043;">Please review Section 4.2 in the digital textbook before tomorrow's lecture. Completed problem sets 1-15 will be collected at the start of class.</p>
396
+ </div>
397
+ </div>
398
+ </div>
399
+ </div>
400
+ `;
401
+ }
402
+
266
403
  setAutoPurge(enabled) {
267
404
  this.autoShield = enabled;
268
405
  localStorage.setItem('reaper_auto_shield', enabled ? 'true' : 'false');