@mooncompany/uplink-chat 0.5.0
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.
Potentially problematic release.
This version of @mooncompany/uplink-chat might be problematic. Click here for more details.
- package/LICENSE +21 -0
- package/README.md +185 -0
- package/bin/uplink.js +279 -0
- package/middleware/error-handler.js +69 -0
- package/package.json +93 -0
- package/public/css/agents.36b98c0f.css +1469 -0
- package/public/css/agents.css +1469 -0
- package/public/css/app.a6a7f8f5.css +2731 -0
- package/public/css/app.css +2731 -0
- package/public/css/artifacts.css +444 -0
- package/public/css/commands.css +55 -0
- package/public/css/connection.css +131 -0
- package/public/css/dashboard.css +233 -0
- package/public/css/developer.css +328 -0
- package/public/css/files.css +123 -0
- package/public/css/markdown.css +156 -0
- package/public/css/message-actions.css +278 -0
- package/public/css/mobile.css +614 -0
- package/public/css/panels-unified.css +483 -0
- package/public/css/premium.css +415 -0
- package/public/css/realtime.css +189 -0
- package/public/css/satellites.css +401 -0
- package/public/css/shortcuts.css +185 -0
- package/public/css/split-view.4def0262.css +673 -0
- package/public/css/split-view.css +673 -0
- package/public/css/theme-generator.css +391 -0
- package/public/css/themes.css +387 -0
- package/public/css/timestamps.css +54 -0
- package/public/css/variables.css +78 -0
- package/public/dist/bundle.b55050c4.js +15757 -0
- package/public/favicon.svg +24 -0
- package/public/img/agents/ada.png +0 -0
- package/public/img/agents/clarice.png +0 -0
- package/public/img/agents/dennis-nedry.png +0 -0
- package/public/img/agents/elliot-alderson.png +0 -0
- package/public/img/agents/main.png +0 -0
- package/public/img/agents/scotty.png +0 -0
- package/public/img/agents/top-flight-security.png +0 -0
- package/public/index.html +1083 -0
- package/public/js/agents-data.js +234 -0
- package/public/js/agents-ui.js +72 -0
- package/public/js/agents.js +1525 -0
- package/public/js/app.js +79 -0
- package/public/js/appearance-settings.js +111 -0
- package/public/js/artifacts.js +432 -0
- package/public/js/audio-queue.js +168 -0
- package/public/js/bootstrap.js +54 -0
- package/public/js/chat.js +1211 -0
- package/public/js/commands.js +581 -0
- package/public/js/connection-api.js +121 -0
- package/public/js/connection.js +1231 -0
- package/public/js/context-tracker.js +271 -0
- package/public/js/core.js +172 -0
- package/public/js/dashboard.js +452 -0
- package/public/js/developer.js +432 -0
- package/public/js/encryption.js +124 -0
- package/public/js/errors.js +122 -0
- package/public/js/event-bus.js +77 -0
- package/public/js/fetch-utils.js +171 -0
- package/public/js/file-handler.js +229 -0
- package/public/js/files.js +352 -0
- package/public/js/gateway-chat.js +538 -0
- package/public/js/logger.js +112 -0
- package/public/js/markdown.js +190 -0
- package/public/js/message-actions.js +431 -0
- package/public/js/message-renderer.js +288 -0
- package/public/js/missed-messages.js +235 -0
- package/public/js/mobile-debug.js +95 -0
- package/public/js/notifications.js +367 -0
- package/public/js/offline-queue.js +178 -0
- package/public/js/onboarding.js +543 -0
- package/public/js/panels.js +156 -0
- package/public/js/premium.js +412 -0
- package/public/js/realtime-voice.js +844 -0
- package/public/js/satellite-sync.js +256 -0
- package/public/js/satellite-ui.js +175 -0
- package/public/js/satellites.js +1516 -0
- package/public/js/settings.js +1087 -0
- package/public/js/shortcuts.js +381 -0
- package/public/js/split-chat.js +1234 -0
- package/public/js/split-resize.js +211 -0
- package/public/js/splitview.js +340 -0
- package/public/js/storage.js +408 -0
- package/public/js/streaming-handler.js +324 -0
- package/public/js/stt-settings.js +316 -0
- package/public/js/theme-generator.js +661 -0
- package/public/js/themes.js +164 -0
- package/public/js/timestamps.js +198 -0
- package/public/js/tts-settings.js +575 -0
- package/public/js/ui.js +267 -0
- package/public/js/update-notifier.js +143 -0
- package/public/js/utils/constants.js +165 -0
- package/public/js/utils/sanitize.js +93 -0
- package/public/js/utils/sse-parser.js +195 -0
- package/public/js/voice.js +883 -0
- package/public/manifest.json +58 -0
- package/public/moon_texture.jpg +0 -0
- package/public/sw.js +221 -0
- package/public/three.min.js +6 -0
- package/server/channel.js +529 -0
- package/server/chat.js +270 -0
- package/server/config-store.js +362 -0
- package/server/config.js +159 -0
- package/server/context.js +131 -0
- package/server/gateway-commands.js +211 -0
- package/server/gateway-proxy.js +318 -0
- package/server/index.js +22 -0
- package/server/logger.js +89 -0
- package/server/middleware/auth.js +188 -0
- package/server/middleware.js +218 -0
- package/server/openclaw-discover.js +308 -0
- package/server/premium/index.js +156 -0
- package/server/premium/license.js +140 -0
- package/server/realtime/bridge.js +837 -0
- package/server/realtime/index.js +349 -0
- package/server/realtime/tts-stream.js +446 -0
- package/server/routes/agents.js +564 -0
- package/server/routes/artifacts.js +174 -0
- package/server/routes/chat.js +311 -0
- package/server/routes/config-settings.js +345 -0
- package/server/routes/config.js +603 -0
- package/server/routes/files.js +307 -0
- package/server/routes/index.js +18 -0
- package/server/routes/media.js +451 -0
- package/server/routes/missed-messages.js +107 -0
- package/server/routes/premium.js +75 -0
- package/server/routes/push.js +156 -0
- package/server/routes/satellite.js +406 -0
- package/server/routes/status.js +251 -0
- package/server/routes/stt.js +35 -0
- package/server/routes/voice.js +260 -0
- package/server/routes/webhooks.js +203 -0
- package/server/routes.js +206 -0
- package/server/runtime-config.js +336 -0
- package/server/share.js +305 -0
- package/server/stt/faster-whisper.js +72 -0
- package/server/stt/groq.js +51 -0
- package/server/stt/index.js +196 -0
- package/server/stt/openai.js +49 -0
- package/server/sync.js +244 -0
- package/server/tailscale-https.js +175 -0
- package/server/tts.js +646 -0
- package/server/update-checker.js +172 -0
- package/server/utils/filename.js +129 -0
- package/server/utils.js +147 -0
- package/server/watchdog.js +318 -0
- package/server/websocket/broadcast.js +359 -0
- package/server/websocket/connections.js +339 -0
- package/server/websocket/index.js +215 -0
- package/server/websocket/routing.js +277 -0
- package/server/websocket/sync.js +102 -0
- package/server.js +404 -0
- package/utils/detect-tool-usage.js +93 -0
- package/utils/errors.js +158 -0
- package/utils/html-escape.js +84 -0
- package/utils/id-sanitize.js +94 -0
- package/utils/response.js +130 -0
- package/utils/with-retry.js +105 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// ============================================
|
|
2
|
+
// THEMES MODULE
|
|
3
|
+
// CSS-based theme system using data-theme attribute
|
|
4
|
+
// ============================================
|
|
5
|
+
|
|
6
|
+
import { UplinkCore } from './core.js';
|
|
7
|
+
import { UplinkStorage } from './storage.js';
|
|
8
|
+
|
|
9
|
+
// Available themes (visual definitions live in themes.css)
|
|
10
|
+
// 'custom' is added dynamically by theme-generator.js
|
|
11
|
+
const THEMES = ['midnight', 'daylight', 'ember', 'forest', 'phantom', 'custom'];
|
|
12
|
+
const DEFAULT_THEME = 'midnight';
|
|
13
|
+
|
|
14
|
+
// Theme accent colors for the visual picker
|
|
15
|
+
const THEME_COLORS = {
|
|
16
|
+
midnight: { accent: '#00f0ff', bg: '#000000' },
|
|
17
|
+
daylight: { accent: '#c84b4b', bg: '#f8f5f1' },
|
|
18
|
+
ember: { accent: '#f48c06', bg: '#03071e' },
|
|
19
|
+
forest: { accent: '#a3b18a', bg: '#131813' },
|
|
20
|
+
phantom: { accent: '#a0a0a0', bg: '#000000' },
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function init() {
|
|
24
|
+
const current = document.documentElement.getAttribute('data-theme');
|
|
25
|
+
if (!current || !THEMES.includes(current)) {
|
|
26
|
+
document.documentElement.setAttribute('data-theme', DEFAULT_THEME);
|
|
27
|
+
}
|
|
28
|
+
buildPicker();
|
|
29
|
+
console.log('Themes: Initialized (' + (current || DEFAULT_THEME) + ')');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function apply(themeName) {
|
|
33
|
+
const theme = THEMES.includes(themeName) ? themeName : DEFAULT_THEME;
|
|
34
|
+
|
|
35
|
+
// Premium check: only allow free themes if not premium
|
|
36
|
+
if (window.UplinkPremium && !window.UplinkPremium.isActive()) {
|
|
37
|
+
const status = window.UplinkPremium.getStatus();
|
|
38
|
+
const allowed = (status && status.themes) || [DEFAULT_THEME];
|
|
39
|
+
if (!allowed.includes(theme)) {
|
|
40
|
+
window.UplinkPremium.showUpgradeModal('Premium themes');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
document.documentElement.setAttribute('data-theme', theme);
|
|
46
|
+
|
|
47
|
+
// Sync hidden select
|
|
48
|
+
const select = document.getElementById('themeSelect');
|
|
49
|
+
if (select) select.value = theme;
|
|
50
|
+
|
|
51
|
+
// Update visual picker
|
|
52
|
+
updatePickerState(theme);
|
|
53
|
+
|
|
54
|
+
// Dispatch event for any listeners
|
|
55
|
+
window.dispatchEvent(new CustomEvent('uplink:themeChange', {
|
|
56
|
+
detail: { theme: theme }
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function get() {
|
|
61
|
+
return document.documentElement.getAttribute('data-theme') || DEFAULT_THEME;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── Visual theme picker ──
|
|
65
|
+
|
|
66
|
+
function buildPicker() {
|
|
67
|
+
const picker = document.getElementById('themePicker');
|
|
68
|
+
if (!picker) return;
|
|
69
|
+
|
|
70
|
+
const current = get();
|
|
71
|
+
const isPremium = window.UplinkPremium ? window.UplinkPremium.isActive() : true;
|
|
72
|
+
const allowedThemes = isPremium ? THEMES : ((window.UplinkPremium && window.UplinkPremium.getStatus().themes) || [DEFAULT_THEME]);
|
|
73
|
+
|
|
74
|
+
// Build items for built-in themes (not 'custom' — that's handled by theme generator)
|
|
75
|
+
const builtIn = THEMES.filter(function(t) { return t !== 'custom'; });
|
|
76
|
+
|
|
77
|
+
picker.innerHTML = builtIn.map(function(theme) {
|
|
78
|
+
const colors = THEME_COLORS[theme];
|
|
79
|
+
const isActive = current === theme;
|
|
80
|
+
const isLocked = !isPremium && !allowedThemes.includes(theme);
|
|
81
|
+
const label = theme.charAt(0).toUpperCase() + theme.slice(1);
|
|
82
|
+
const swatchStyle = 'background:linear-gradient(135deg,' + colors.bg + ' 50%,' + colors.accent + ' 100%)';
|
|
83
|
+
|
|
84
|
+
return '<div class="theme-picker-item' + (isActive ? ' active' : '') + '"' +
|
|
85
|
+
' data-theme="' + theme + '"' +
|
|
86
|
+
(isLocked ? ' data-premium-locked="true"' : '') +
|
|
87
|
+
' role="option" aria-selected="' + isActive + '"' +
|
|
88
|
+
' title="' + label + (isLocked ? ' (Premium)' : '') + '">' +
|
|
89
|
+
'<div class="theme-picker-swatch" style="' + swatchStyle + '"></div>' +
|
|
90
|
+
'<span class="theme-picker-name">' + label + '</span>' +
|
|
91
|
+
(isLocked ? '<span class="theme-picker-lock">🔒</span>' : '') +
|
|
92
|
+
'</div>';
|
|
93
|
+
}).join('');
|
|
94
|
+
|
|
95
|
+
// If custom theme is active, add it
|
|
96
|
+
if (current === 'custom') {
|
|
97
|
+
var customAccent = '#8b5cf6';
|
|
98
|
+
if (window.UplinkThemeGenerator && window.UplinkThemeGenerator.getAccent) {
|
|
99
|
+
customAccent = window.UplinkThemeGenerator.getAccent();
|
|
100
|
+
}
|
|
101
|
+
picker.innerHTML += '<div class="theme-picker-item active" data-theme="custom" role="option" aria-selected="true">' +
|
|
102
|
+
'<div class="theme-picker-swatch" style="background:' + customAccent + '"></div>' +
|
|
103
|
+
'<span class="theme-picker-name">✦ Custom</span>' +
|
|
104
|
+
'</div>';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Click handlers
|
|
108
|
+
picker.querySelectorAll('.theme-picker-item').forEach(function(item) {
|
|
109
|
+
item.addEventListener('click', function() {
|
|
110
|
+
var theme = item.dataset.theme;
|
|
111
|
+
if (item.dataset.premiumLocked === 'true') {
|
|
112
|
+
if (window.UplinkPremium) window.UplinkPremium.showUpgradeModal('Premium themes');
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
apply(theme);
|
|
116
|
+
// Save preference
|
|
117
|
+
UplinkStorage.saveSettings({ theme: theme });
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function updatePickerState(activeTheme) {
|
|
123
|
+
var picker = document.getElementById('themePicker');
|
|
124
|
+
if (!picker) return;
|
|
125
|
+
|
|
126
|
+
// Update active state on existing items
|
|
127
|
+
picker.querySelectorAll('.theme-picker-item').forEach(function(item) {
|
|
128
|
+
var isActive = item.dataset.theme === activeTheme;
|
|
129
|
+
item.classList.toggle('active', isActive);
|
|
130
|
+
item.setAttribute('aria-selected', isActive);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// If switching to custom and no custom item exists, rebuild
|
|
134
|
+
if (activeTheme === 'custom' && !picker.querySelector('[data-theme="custom"]')) {
|
|
135
|
+
buildPicker();
|
|
136
|
+
}
|
|
137
|
+
// If switching away from custom, remove the custom item
|
|
138
|
+
if (activeTheme !== 'custom') {
|
|
139
|
+
var customItem = picker.querySelector('[data-theme="custom"]');
|
|
140
|
+
if (customItem) customItem.remove();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Rebuild picker when premium status changes
|
|
145
|
+
window.addEventListener('uplink:premiumChange', function() {
|
|
146
|
+
buildPicker();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Export API
|
|
150
|
+
export const UplinkThemes = {
|
|
151
|
+
apply: apply,
|
|
152
|
+
get: get,
|
|
153
|
+
list: () => [...THEMES],
|
|
154
|
+
default: DEFAULT_THEME,
|
|
155
|
+
buildPicker: buildPicker,
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export { apply, get, buildPicker, DEFAULT_THEME, THEMES };
|
|
159
|
+
|
|
160
|
+
// Backward compat: assign to window
|
|
161
|
+
window.UplinkThemes = UplinkThemes;
|
|
162
|
+
|
|
163
|
+
// Register and init
|
|
164
|
+
UplinkCore.registerModule('themes', init);
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// ============================================
|
|
2
|
+
// TIMESTAMPS MODULE
|
|
3
|
+
// Show message timestamps below bubble text
|
|
4
|
+
// ============================================
|
|
5
|
+
|
|
6
|
+
const STORAGE_KEY = 'uplink-timestamps';
|
|
7
|
+
let enabled = false;
|
|
8
|
+
let messagesObserver = null;
|
|
9
|
+
|
|
10
|
+
function init() {
|
|
11
|
+
loadSetting();
|
|
12
|
+
addSettingsUI();
|
|
13
|
+
applyTimestamps();
|
|
14
|
+
|
|
15
|
+
// Re-apply when messages load from gateway (they arrive after init)
|
|
16
|
+
window.addEventListener('uplink:satellite-switched', function() {
|
|
17
|
+
if (enabled) setTimeout(applyTimestamps, 200);
|
|
18
|
+
});
|
|
19
|
+
// Also re-apply periodically during initial load to catch gateway messages
|
|
20
|
+
if (enabled) {
|
|
21
|
+
var retries = 0;
|
|
22
|
+
var reapplyInterval = setInterval(function() {
|
|
23
|
+
applyTimestamps();
|
|
24
|
+
retries++;
|
|
25
|
+
if (retries >= 5) clearInterval(reapplyInterval);
|
|
26
|
+
}, 1000);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Cleanup previous observer if init called again
|
|
30
|
+
if (messagesObserver) {
|
|
31
|
+
messagesObserver.disconnect();
|
|
32
|
+
messagesObserver = null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Watch for new messages
|
|
36
|
+
const messagesEl = document.getElementById('messages');
|
|
37
|
+
if (messagesEl) {
|
|
38
|
+
messagesObserver = new MutationObserver((mutations) => {
|
|
39
|
+
if (!enabled) return;
|
|
40
|
+
mutations.forEach(mutation => {
|
|
41
|
+
mutation.addedNodes.forEach(node => {
|
|
42
|
+
if (node.classList?.contains('message')) {
|
|
43
|
+
addTimestampToMessage(node);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
messagesObserver.observe(messagesEl, { childList: true });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.log('Timestamps: Initialized');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function loadSetting() {
|
|
55
|
+
enabled = localStorage.getItem(STORAGE_KEY) === 'true';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function saveSetting() {
|
|
59
|
+
localStorage.setItem(STORAGE_KEY, enabled.toString());
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function addSettingsUI() {
|
|
63
|
+
const settingsPanel = document.getElementById('settingsPanel');
|
|
64
|
+
if (!settingsPanel) {
|
|
65
|
+
setTimeout(addSettingsUI, 100);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (document.getElementById('timestampsRow')) return;
|
|
70
|
+
|
|
71
|
+
const row = document.createElement('div');
|
|
72
|
+
row.className = 'setting-row';
|
|
73
|
+
row.id = 'timestampsRow';
|
|
74
|
+
row.innerHTML = `
|
|
75
|
+
<div>
|
|
76
|
+
<div class="setting-label">Message timestamps</div>
|
|
77
|
+
<div class="setting-desc">Show when messages were sent</div>
|
|
78
|
+
</div>
|
|
79
|
+
<div class="toggle ${enabled ? 'on' : ''}" id="timestampsToggle" tabindex="0" role="switch" aria-checked="${enabled}" aria-label="Toggle message timestamps"></div>
|
|
80
|
+
`;
|
|
81
|
+
|
|
82
|
+
// Insert into General section, after text size row
|
|
83
|
+
const generalSection = document.getElementById('section-general');
|
|
84
|
+
if (generalSection) {
|
|
85
|
+
generalSection.appendChild(row);
|
|
86
|
+
} else {
|
|
87
|
+
settingsPanel.appendChild(row);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const toggle = document.getElementById('timestampsToggle');
|
|
91
|
+
toggle.addEventListener('click', () => {
|
|
92
|
+
enabled = !enabled;
|
|
93
|
+
toggle.classList.toggle('on', enabled);
|
|
94
|
+
toggle.setAttribute('aria-checked', enabled.toString());
|
|
95
|
+
saveSetting();
|
|
96
|
+
applyTimestamps();
|
|
97
|
+
});
|
|
98
|
+
toggle.addEventListener('keydown', (e) => {
|
|
99
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
100
|
+
e.preventDefault();
|
|
101
|
+
toggle.click();
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get the time for a message from its data-time attribute
|
|
108
|
+
*/
|
|
109
|
+
function getMessageTime(msg) {
|
|
110
|
+
var timeStr = msg.dataset.time;
|
|
111
|
+
if (timeStr) return new Date(parseInt(timeStr, 10));
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Add a timestamp element inside a message bubble
|
|
117
|
+
*/
|
|
118
|
+
function addTimestampToMessage(msg) {
|
|
119
|
+
if (!msg || msg.classList.contains('system')) return;
|
|
120
|
+
if (!msg.querySelector('.message-timestamp')) {
|
|
121
|
+
var time = getMessageTime(msg);
|
|
122
|
+
var ts = document.createElement('span');
|
|
123
|
+
ts.className = 'message-timestamp';
|
|
124
|
+
ts.textContent = time ? formatTime(time) : '';
|
|
125
|
+
msg.appendChild(ts);
|
|
126
|
+
}
|
|
127
|
+
if (enabled) {
|
|
128
|
+
msg.classList.add('show-timestamp');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Apply or remove timestamps on all existing messages
|
|
134
|
+
*/
|
|
135
|
+
function applyTimestamps() {
|
|
136
|
+
var messages = document.querySelectorAll('#messages .message');
|
|
137
|
+
|
|
138
|
+
messages.forEach(function(msg) {
|
|
139
|
+
if (msg.classList.contains('system')) return;
|
|
140
|
+
|
|
141
|
+
if (enabled) {
|
|
142
|
+
if (!msg.querySelector('.message-timestamp')) {
|
|
143
|
+
var time = getMessageTime(msg);
|
|
144
|
+
var ts = document.createElement('span');
|
|
145
|
+
ts.className = 'message-timestamp';
|
|
146
|
+
ts.textContent = time ? formatTime(time) : '';
|
|
147
|
+
msg.appendChild(ts);
|
|
148
|
+
}
|
|
149
|
+
msg.classList.add('show-timestamp');
|
|
150
|
+
} else {
|
|
151
|
+
msg.classList.remove('show-timestamp');
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function formatTime(date) {
|
|
157
|
+
const now = new Date();
|
|
158
|
+
const isToday = date.toDateString() === now.toDateString();
|
|
159
|
+
|
|
160
|
+
if (isToday) {
|
|
161
|
+
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const yesterday = new Date(now);
|
|
165
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
166
|
+
if (date.toDateString() === yesterday.toDateString()) {
|
|
167
|
+
return 'Yesterday ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return date.toLocaleDateString([], { month: 'short', day: 'numeric' }) + ' ' +
|
|
171
|
+
date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Cleanup function
|
|
175
|
+
function destroy() {
|
|
176
|
+
if (messagesObserver) {
|
|
177
|
+
messagesObserver.disconnect();
|
|
178
|
+
messagesObserver = null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Expose API
|
|
183
|
+
export const UplinkTimestamps = {
|
|
184
|
+
enable: () => { enabled = true; saveSetting(); applyTimestamps(); },
|
|
185
|
+
disable: () => { enabled = false; saveSetting(); applyTimestamps(); },
|
|
186
|
+
toggle: () => { enabled = !enabled; saveSetting(); applyTimestamps(); },
|
|
187
|
+
isEnabled: () => enabled,
|
|
188
|
+
format: formatTime,
|
|
189
|
+
destroy
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
import { UplinkCore } from './core.js';
|
|
193
|
+
|
|
194
|
+
// Backward compat: assign to window
|
|
195
|
+
window.UplinkTimestamps = UplinkTimestamps;
|
|
196
|
+
|
|
197
|
+
// Register and init
|
|
198
|
+
UplinkCore.registerModule('timestamps', init);
|