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 +64 -17
- package/js/app.js +198 -6
- package/js/firebase-client.js +73 -0
- package/js/security-shield.js +139 -2
- package/main/index.html +64 -17
- package/main/js/app.js +198 -6
- package/main/js/firebase-client.js +73 -0
- package/main/js/security-shield.js +139 -2
- package/package.json +4 -3
- package/verify/js/firebase-client.js +73 -0
- package/verify/js/security-shield.js +139 -2
package/index.html
CHANGED
|
@@ -483,27 +483,73 @@
|
|
|
483
483
|
|
|
484
484
|
</main>
|
|
485
485
|
|
|
486
|
-
<!-- Side-Docked Chat
|
|
486
|
+
<!-- Side-Docked Chat & Private DMs -->
|
|
487
487
|
<aside class="side-chat-panel" id="side-chat-panel">
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
-
<!--
|
|
497
|
-
<div
|
|
498
|
-
|
|
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
|
-
<!--
|
|
502
|
-
<div
|
|
503
|
-
|
|
504
|
-
<div
|
|
505
|
-
<
|
|
506
|
-
|
|
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;">← 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
|
|
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>
|
package/js/firebase-client.js
CHANGED
|
@@ -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 {
|
package/js/security-shield.js
CHANGED
|
@@ -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
|
-
|
|
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 & 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');
|