2310 lines
154 KiB
HTML
2310 lines
154 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta name="theme-color" content="#1a1a2e">
|
|
<title>Nostr DM Dashboard</title>
|
|
<!-- Fully inlined manifest for PWA + Safari -->
|
|
<link rel="manifest" href='data:application/json;base64,ewogICJuYW1lIjogIk5vc3RyIERNIERhc2hib2FyZCIsCiAgInNob3J0X25hbWUiOiAiTm9zdHIgRE1zIiwKICAic3RhcnRfdXJsIjogIj9zb3VyY2U9cHdhIiwKICAic2NvcGUiOiAiLyIsCiAgImRpc3BsYXkiOiAic3RhbmRhbG9uZSIsCiAgImJhY2tncm91bmRfY29sb3IiOiAiIzFhMWEyZSIsCiAgInRoZW1lX2NvbG9yIjogIiMxYTFhMmUiLAogICJkZXNjcmlwdGlvbiI6ICJTZWN1cmUgTm9zdHIgZGlyZWN0IG1lc3NhZ2luZyBkYXNoYm9hcmQiLAogICJpY29ucyI6IFsKICAgIHsKICAgICAgInNyYyI6ICJkYXRhOmltYWdlL3BuZztiYXNlNjQs{{ICON_BASE64}}IiwKICAgICAgInNpemVzIjogIjE5MngxOTIiLAogICAgICAidHlwZSI6ICJpbWFnZS9wbmciCiAgICB9LAogICAgewogICAgICAic3JjIjogImRhdGE6aW1hZ2UvcG5nO2Jhc2U2NCw{{ICON_BASE64}}IiwKICAgICAgInNpemVzIjogIjUxMng1MTIiLAogICAgICAidHlwZSI6ICJpbWFnZS9wbmciCiAgICB9CiAgXQp9'>
|
|
<!-- Apple / Safari specific -->
|
|
<link rel="apple-touch-icon" sizes="192x192" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAAAXNSR0IArs4c6QAAAPJlWElmTU0AKgAAAAgABwESAAMAAAABAAEAAAEaAAUAAAABAAAAYgEbAAUAAAABAAAAagEoAAMAAAABAAIAAAExAAIAAAAiAAAAcgEyAAIAAAAUAAAAlIdpAAQAAAABAAAAqAAAAAAAAABIAAAAAQAAAEgAAAABQWRvYmUgUGhvdG9zaG9wIDI2LjEwIChNYWNpbnRvc2gpADIwMjU6MTA6MTMgMTg6NTg6MjMAAASQBAACAAAAFAAAAN6gAQADAAAAAQABAACgAgAEAAAAAQAAAICgAwAEAAAAAQAAAIAAAAAAMjAyNToxMDoxMyAxODo1NjoxNgALj7ylAAAACXBIWXMAAAsTAAALEwEAmpwYAAAIzmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgICAgICAgICAgIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIKICAgICAgICAgICAgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iCiAgICAgICAgICAgIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiCiAgICAgICAgICAgIHhtbG5zOnRpZmY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vdGlmZi8xLjAvIgogICAgICAgICAgICB4bWxuczpwaG90b3Nob3A9Imh0dHA6Ly9ucy5hZG9iZS5jb20vcGhvdG9zaG9wLzEuMC8iPgogICAgICAgICA8ZGM6Zm9ybWF0PmltYWdlL3BuZzwvZGM6Zm9ybWF0PgogICAgICAgICA8eG1wOk1vZGlmeURhdGU+MjAyNS0xMC0xM1QxODo1ODoyMy0wNDowMDwveG1wOk1vZGlmeURhdGU+CiAgICAgICAgIDx4bXA6Q3JlYXRvclRvb2w+QWRvYmUgUGhvdG9zaG9wIDI2LjEwIChNYWNpbnRvc2gpPC94bXA6Q3JlYXRvclRvb2w+CiAgICAgICAgIDx4bXA6Q3JlYXRlRGF0ZT4yMDI1LTEwLTEzVDE4OjU2OjE2LTA0OjAwPC94bXA6Q3JlYXRlRGF0ZT4KICAgICAgICAgPHhtcDpNZXRhZGF0YURhdGU+MjAyNS0xMC0xM1QxODo1ODoyMy0wNDowMDwveG1wOk1ldGFkYXRhRGF0ZT4KICAgICAgICAgPHhtcE1NOkhpc3Rvcnk+CiAgICAgICAgICAgIDxyZGY6U2VxPgogICAgICAgICAgICAgICA8cmRmOmxpIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OnNvZnR3YXJlQWdlbnQ+QWRvYmUgUGhvdG9zaG9wIDI2LjEwIChNYWNpbnRvc2gpPC9zdEV2dDpzb2Z0d2FyZUFnZW50PgogICAgICAgICAgICAgICAgICA8c3RFdnQ6d2hlbj4yMDI1LTEwLTEzVDE4OjU2OjE2LTA0OjAwPC9zdEV2dDp3aGVuPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6aW5zdGFuY2VJRD54bXAuaWlkOjNhYjg0YTZjLWY4ZDQtNDc4Ny1hZjM5LTQ5ODYzMjZkMTdmYTwvc3RFdnQ6aW5zdGFuY2VJRD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5jcmVhdGVkPC9zdEV2dDphY3Rpb24+CiAgICAgICAgICAgICAgIDwvcmRmOmxpPgogICAgICAgICAgICAgICA8cmRmOmxpIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OnNvZnR3YXJlQWdlbnQ+QWRvYmUgUGhvdG9zaG9wIDI2LjEwIChNYWNpbnRvc2gpPC9zdEV2dDpzb2Z0d2FyZUFnZW50PgogICAgICAgICAgICAgICAgICA8c3RFdnQ6Y2hhbmdlZD4vPC9zdEV2dDpjaGFuZ2VkPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6d2hlbj4yMDI1LTEwLTEzVDE4OjU4OjIzLTA0OjAwPC9zdEV2dDp3aGVuPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6aW5zdGFuY2VJRD54bXAuaWlkOjA1ZGVlYTU2LTIwNzctNDMyNy1iY2JkLWYyMWI5NmVjNTU5YTwvc3RFdnQ6aW5zdGFuY2VJRD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5zYXZlZDwvc3RFdnQ6YWN0aW9uPgogICAgICAgICAgICAgICA8L3JkZjpsaT4KICAgICAgICAgICAgPC9yZGY6U2VxPgogICAgICAgICA8L3htcE1NOkhpc3Rvcnk+CiAgICAgICAgIDx4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ+eG1wLmRpZDozYWI4NGE2Yy1mOGQ0LTQ3ODctYWYzOS00OTg2MzI2ZDE3ZmE8L3htcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD4KICAgICAgICAgPHhtcE1NOkRvY3VtZW50SUQ+eG1wLmRpZDozYWI4NGE2Yy1mOGQ0LTQ3ODctYWYzOS00OTg2MzI2ZDE3ZmE8L3htcE1NOkRvY3VtZW50SUQ+CiAgICAgICAgIDx4bXBNTTpJbnN0YW5jZUlEPnhtcC5paWQ6MDVkZWVhNTYtMjA3Ny00MzI3LWJjYmQtZjIxYjk2ZWM1NTlhPC94bXBNTTpJbnN0YW5jZUlEPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICAgICA8cGhvdG9zaG9wOkNvbG9yTW9kZT4zPC9waG90b3Nob3A6Q29sb3JNb2RlPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KIp26SgAAPldJREFUeAHtfQmcXkWxb32zZrYkkz2TfSckBBISEiAoCrKJC4ui16feq4iKCwJ6Rd9P7/MiVx9uqA8Rt+d1A0QFBdkFkwDZA4Tse0ISsm+z7/f/r+46X58z30xmJjNZfr/0zDndXV1VXV1V3adP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">
|
|
<link rel="mask-icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAAAXNSR0IArs4c6QAAAPJlWElmTU0AKgAAAAgABwESAAMAAAABAAEAAAEaAAUAAAABAAAAYgEbAAUAAAABAAAAagEoAAMAAAABAAIAAAExAAIAAAAiAAAAcgEyAAIAAAAUAAAAlIdpAAQAAAABAAAAqAAAAAAAAABIAAAAAQAAAEgAAAABQWRvYmUgUGhvdG9zaG9wIDI2LjEwIChNYWNpbnRvc2gpADIwMjU6MTA6MTMgMTg6NTg6MjMAAASQBAACAAAAFAAAAN6gAQADAAAAAQABAACgAgAEAAAAAQAAAICgAwAEAAAAAQAAAIAAAAAAMjAyNToxMDoxMyAxODo1NjoxNgALj7ylAAAACXBIWXMAAAsTAAALEwEAmpwYAAAIzmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgICAgICAgICAgIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIKICAgICAgICAgICAgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iCiAgICAgICAgICAgIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiCiAgICAgICAgICAgIHhtbG5zOnRpZmY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vdGlmZi8xLjAvIgogICAgICAgICAgICB4bWxuczpwaG90b3Nob3A9Imh0dHA6Ly9ucy5hZG9iZS5jb20vcGhvdG9zaG9wLzEuMC8iPgogICAgICAgICA8ZGM6Zm9ybWF0PmltYWdlL3BuZzwvZGM6Zm9ybWF0PgogICAgICAgICA8eG1wOk1vZGlmeURhdGU+MjAyNS0xMC0xM1QxODo1ODoyMy0wNDowMDwveG1wOk1vZGlmeURhdGU+CiAgICAgICAgIDx4bXA6Q3JlYXRvclRvb2w+QWRvYmUgUGhvdG9zaG9wIDI2LjEwIChNYWNpbnRvc2gpPC94bXA6Q3JlYXRvclRvb2w+CiAgICAgICAgIDx4bXA6Q3JlYXRlRGF0ZT4yMDI1LTEwLTEzVDE4OjU2OjE2LTA0OjAwPC94bXA6Q3JlYXRlRGF0ZT4KICAgICAgICAgPHhtcDpNZXRhZGF0YURhdGU+MjAyNS0xMC0xM1QxODo1ODoyMy0wNDowMDwveG1wOk1ldGFkYXRhRGF0ZT4KICAgICAgICAgPHhtcE1NOkhpc3Rvcnk+CiAgICAgICAgICAgIDxyZGY6U2VxPgogICAgICAgICAgICAgICA8cmRmOmxpIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OnNvZnR3YXJlQWdlbnQ+QWRvYmUgUGhvdG9zaG9wIDI2LjEwIChNYWNpbnRvc2gpPC9zdEV2dDpzb2Z0d2FyZUFnZW50PgogICAgICAgICAgICAgICAgICA8c3RFdnQ6d2hlbj4yMDI1LTEwLTEzVDE4OjU2OjE2LTA0OjAwPC9zdEV2dDp3aGVuPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6aW5zdGFuY2VJRD54bXAuaWlkOjNhYjg0YTZjLWY4ZDQtNDc4Ny1hZjM5LTQ5ODYzMjZkMTdmYTwvc3RFdnQ6aW5zdGFuY2VJRD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5jcmVhdGVkPC9zdEV2dDphY3Rpb24+CiAgICAgICAgICAgICAgIDwvcmRmOmxpPgogICAgICAgICAgICAgICA8cmRmOmxpIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OnNvZnR3YXJlQWdlbnQ+QWRvYmUgUGhvdG9zaG9wIDI2LjEwIChNYWNpbnRvc2gpPC9zdEV2dDpzb2Z0d2FyZUFnZW50PgogICAgICAgICAgICAgICAgICA8c3RFdnQ6Y2hhbmdlZD4vPC9zdEV2dDpjaGFuZ2VkPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6d2hlbj4yMDI1LTEwLTEzVDE4OjU4OjIzLTA0OjAwPC9zdEV2dDp3aGVuPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6aW5zdGFuY2VJRD54bXAuaWlkOjA1ZGVlYTU2LTIwNzctNDMyNy1iY2JkLWYyMWI5NmVjNTU5YTwvc3RFdnQ6aW5zdGFuY2VJRD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5zYXZlZDwvc3RFdnQ6YWN0aW9uPgogICAgICAgICAgICAgICA8L3JkZjpsaT4KICAgICAgICAgICAgPC9yZGY6U2VxPgogICAgICAgICA8L3htcE1NOkhpc3Rvcnk+CiAgICAgICAgIDx4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ+eG1wLmRpZDozYWI4NGE2Yy1mOGQ0LTQ3ODctYWYzOS00OTg2MzI2ZDE3ZmE8L3htcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD4KICAgICAgICAgPHhtcE1NOkRvY3VtZW50SUQ+eG1wLmRpZDozYWI4NGE2Yy1mOGQ0LTQ3ODctYWYzOS00OTg2MzI2ZDE3ZmE8L3htcE1NOkRvY3VtZW50SUQ+CiAgICAgICAgIDx4bXBNTTpJbnN0YW5jZUlEPnhtcC5paWQ6MDVkZWVhNTYtMjA3Ny00MzI3LWJjYmQtZjIxYjk2ZWM1NTlhPC94bXBNTTpJbnN0YW5jZUlEPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICAgICA8cGhvdG9zaG9wOkNvbG9yTW9kZT4zPC9waG90b3Nob3A6Q29sb3JNb2RlPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KIp26SgAAPldJREFUeAHtfQmcXkWxb32zZrYkkz2TfSckBBISEiAoCrKJC4ui16feq4iKCwJ6Rd9P7/MiVx9uqA8Rt+d1A0QFBdkFkwDZA4Tse0ISsm+z7/f/r+46X58z30xmJjNZfr/0zDndXV1VXV1V3adP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" color="#1a1a2e">
|
|
<meta name="apple-mobile-web-app-title" content="Nostr DMs">
|
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
<script src="https://unpkg.com/nostr-tools@2.7.2/lib/nostr.bundle.js"></script>
|
|
<style>
|
|
/* ============================================
|
|
CSS VARIABLES - All colors and values in one place
|
|
============================================ */
|
|
:root {
|
|
--primary: #fdad01;
|
|
--primary-dark: #ff9500;
|
|
--primary-gradient: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
|
|
|
|
--bg-dark: #1a1a2e;
|
|
--bg-darker: #16213e;
|
|
--bg-overlay: rgba(30, 30, 40, 0.7);
|
|
--bg-panel: rgba(25, 25, 35, 0.6);
|
|
--bg-header: rgba(20, 20, 30, 0.6);
|
|
--bg-input: rgba(40, 40, 50, 0.6);
|
|
|
|
--text-primary: #ffffff;
|
|
--text-secondary: rgba(255, 255, 255, 0.7);
|
|
--text-dim: rgba(255, 255, 255, 0.45);
|
|
--text-dimmer: rgba(255, 255, 255, 0.3);
|
|
|
|
--border: rgba(255, 255, 255, 0.08);
|
|
--border-light: rgba(255, 255, 255, 0.12);
|
|
|
|
--radius: 12px;
|
|
--radius-lg: 16px;
|
|
--radius-xl: 20px;
|
|
|
|
--shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
|
--shadow-lg: 0 20px 60px rgba(0, 0, 0, 0.5);
|
|
}
|
|
|
|
/* ============================================
|
|
BASE STYLES
|
|
============================================ */
|
|
*, *::before, *::after {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
html {
|
|
height: 100%;
|
|
overflow: hidden;
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
background: var(--bg-dark);
|
|
color: var(--text-primary);
|
|
height: 100%;
|
|
min-height: 100vh;
|
|
min-height: -webkit-fill-available;
|
|
overflow: hidden;
|
|
-webkit-text-size-adjust: 100%;
|
|
touch-action: manipulation;
|
|
}
|
|
|
|
/* ============================================
|
|
CONTAINER
|
|
============================================ */
|
|
.container {
|
|
background: linear-gradient(135deg, var(--bg-dark) 0%, var(--bg-darker) 100%);
|
|
width: 100%;
|
|
height: 100%;
|
|
min-height: 100vh;
|
|
min-height: -webkit-fill-available;
|
|
display: flex;
|
|
flex-direction: column;
|
|
position: relative;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
@media (min-width: 768px) {
|
|
body {
|
|
padding: 20px;
|
|
background: linear-gradient(135deg, var(--bg-dark) 0%, var(--bg-darker) 100%);
|
|
}
|
|
.container {
|
|
background: var(--bg-overlay);
|
|
backdrop-filter: blur(20px);
|
|
border: 1px solid var(--border);
|
|
box-shadow: var(--shadow-lg);
|
|
border-radius: 24px;
|
|
width: 95vw;
|
|
height: 95vh;
|
|
max-width: 1600px;
|
|
margin: 0 auto;
|
|
min-height: unset;
|
|
}
|
|
}
|
|
|
|
/* ============================================
|
|
HEADER
|
|
============================================ */
|
|
.header {
|
|
background: var(--bg-header);
|
|
backdrop-filter: blur(10px);
|
|
padding: 16px 20px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
border-bottom: 1px solid var(--border);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.header h1 {
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
letter-spacing: -0.5px;
|
|
}
|
|
|
|
.header-actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
align-items: center;
|
|
}
|
|
|
|
@media (min-width: 768px) {
|
|
.header { padding: 20px 24px; }
|
|
.header h1 { font-size: 20px; }
|
|
}
|
|
|
|
/* ============================================
|
|
BUTTONS
|
|
============================================ */
|
|
.btn {
|
|
background: var(--primary-gradient);
|
|
color: #000;
|
|
border: none;
|
|
padding: 12px 20px;
|
|
border-radius: var(--radius);
|
|
font-size: 14px;
|
|
font-weight: 700;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
box-shadow: 0 4px 16px rgba(253, 173, 1, 0.3);
|
|
}
|
|
|
|
.btn:hover {
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 6px 20px rgba(253, 173, 1, 0.4);
|
|
}
|
|
|
|
.btn:active {
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.btn:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
transform: none;
|
|
}
|
|
|
|
.btn-secondary {
|
|
background: rgba(60, 60, 70, 0.8);
|
|
color: var(--text-primary);
|
|
border: 1px solid var(--border-light);
|
|
box-shadow: none;
|
|
}
|
|
|
|
.btn-secondary:hover {
|
|
background: rgba(70, 70, 80, 0.9);
|
|
box-shadow: var(--shadow);
|
|
}
|
|
|
|
.btn-small {
|
|
padding: 8px 16px;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.btn-icon {
|
|
padding: 10px;
|
|
width: 40px;
|
|
height: 40px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
@media (min-width: 768px) {
|
|
.btn { padding: 14px 28px; font-size: 15px; }
|
|
.btn-small { padding: 10px 20px; font-size: 14px; }
|
|
}
|
|
|
|
/* ============================================
|
|
INPUTS
|
|
============================================ */
|
|
input[type="text"] {
|
|
width: 100%;
|
|
padding: 12px 16px;
|
|
border: 1px solid var(--border-light);
|
|
border-radius: var(--radius);
|
|
font-size: 14px;
|
|
background: var(--bg-input);
|
|
color: var(--text-primary);
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
input::placeholder {
|
|
color: var(--text-dimmer);
|
|
}
|
|
|
|
input:focus {
|
|
outline: none;
|
|
border-color: rgba(253, 173, 1, 0.5);
|
|
background: rgba(40, 40, 50, 0.9);
|
|
box-shadow: 0 0 0 3px rgba(253, 173, 1, 0.1);
|
|
}
|
|
|
|
/* ============================================
|
|
LOGIN SECTION
|
|
============================================ */
|
|
.login-section {
|
|
padding: 40px 20px;
|
|
text-align: center;
|
|
background: var(--bg-panel);
|
|
border-radius: var(--radius-lg);
|
|
margin: 20px;
|
|
}
|
|
|
|
.login-section h2 {
|
|
margin-bottom: 10px;
|
|
font-size: 24px;
|
|
}
|
|
|
|
.input-group {
|
|
margin-bottom: 20px;
|
|
text-align: left;
|
|
}
|
|
|
|
.input-group label {
|
|
display: block;
|
|
margin-bottom: 8px;
|
|
color: var(--text-secondary);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.instructions {
|
|
background: rgba(253, 173, 1, 0.1);
|
|
border-left: 4px solid var(--primary);
|
|
padding: 15px;
|
|
margin: 20px 0;
|
|
border-radius: 8px;
|
|
text-align: left;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.instructions strong {
|
|
color: var(--primary);
|
|
}
|
|
|
|
.instructions a {
|
|
color: var(--primary);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.instructions a:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.instructions ol {
|
|
margin: 10px 0 10px 20px;
|
|
}
|
|
|
|
/* ============================================
|
|
DASHBOARD
|
|
============================================ */
|
|
.dashboard {
|
|
display: none;
|
|
flex: 1;
|
|
overflow: hidden;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.dashboard.active {
|
|
display: flex;
|
|
}
|
|
|
|
.dashboard-content {
|
|
display: flex;
|
|
flex-direction: column;
|
|
flex: 1;
|
|
overflow: hidden;
|
|
position: relative;
|
|
}
|
|
|
|
@media (min-width: 768px) {
|
|
.dashboard-content {
|
|
flex-direction: row;
|
|
}
|
|
}
|
|
|
|
/* ============================================
|
|
SIDEBAR
|
|
============================================ */
|
|
.sidebar {
|
|
width: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: var(--bg-panel);
|
|
backdrop-filter: blur(10px);
|
|
height: 100%;
|
|
position: absolute;
|
|
inset: 0;
|
|
z-index: 2;
|
|
transition: transform 0.3s ease;
|
|
}
|
|
|
|
.sidebar.hidden-mobile {
|
|
transform: translateX(-100%);
|
|
}
|
|
|
|
@media (min-width: 768px) {
|
|
.sidebar {
|
|
width: 380px;
|
|
min-width: 380px;
|
|
border-right: 1px solid var(--border);
|
|
position: relative;
|
|
transform: none !important;
|
|
}
|
|
}
|
|
|
|
.sidebar-header {
|
|
padding: 16px 20px;
|
|
border-bottom: 1px solid var(--border);
|
|
background: var(--bg-header);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.sidebar-header h2 {
|
|
font-size: 18px;
|
|
margin-bottom: 12px;
|
|
font-weight: 600;
|
|
letter-spacing: -0.5px;
|
|
}
|
|
|
|
@media (min-width: 768px) {
|
|
.sidebar-header {
|
|
padding: 24px 24px 20px;
|
|
}
|
|
.sidebar-header h2 {
|
|
font-size: 20px;
|
|
margin-bottom: 16px;
|
|
}
|
|
}
|
|
|
|
.conversations-list {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.conversations-list::-webkit-scrollbar {
|
|
width: 6px;
|
|
}
|
|
|
|
.conversations-list::-webkit-scrollbar-track {
|
|
background: transparent;
|
|
}
|
|
|
|
.conversations-list::-webkit-scrollbar-thumb {
|
|
background: rgba(255, 255, 255, 0.15);
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.conversation-item {
|
|
padding: 16px 20px;
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
|
|
cursor: pointer;
|
|
transition: background 0.2s ease;
|
|
position: relative;
|
|
}
|
|
|
|
.conversation-item::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
bottom: 0;
|
|
width: 3px;
|
|
background: var(--primary);
|
|
transform: scaleY(0);
|
|
transition: transform 0.2s ease;
|
|
}
|
|
|
|
.conversation-item:hover {
|
|
background: rgba(40, 40, 50, 0.4);
|
|
}
|
|
|
|
.conversation-item.active {
|
|
background: rgba(253, 173, 1, 0.08);
|
|
}
|
|
|
|
.conversation-item.active::before {
|
|
transform: scaleY(1);
|
|
}
|
|
|
|
.conversation-item .contact-name {
|
|
font-weight: 600;
|
|
margin-bottom: 6px;
|
|
font-size: 14px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.conversation-item .last-message {
|
|
font-size: 13px;
|
|
color: var(--text-dim);
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.conversation-item .timestamp {
|
|
font-size: 11px;
|
|
color: var(--text-dimmer);
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.unread-badge {
|
|
background: var(--primary);
|
|
color: #000;
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
padding: 4px 10px;
|
|
border-radius: 12px;
|
|
min-width: 24px;
|
|
text-align: center;
|
|
}
|
|
|
|
@media (min-width: 768px) {
|
|
.conversation-item {
|
|
padding: 20px 24px;
|
|
}
|
|
.conversation-item .contact-name {
|
|
font-size: 15px;
|
|
margin-bottom: 8px;
|
|
}
|
|
.conversation-item .last-message {
|
|
font-size: 14px;
|
|
}
|
|
.conversation-item .timestamp {
|
|
font-size: 12px;
|
|
margin-top: 6px;
|
|
}
|
|
}
|
|
|
|
/* ============================================
|
|
CHAT AREA
|
|
============================================ */
|
|
.chat-area {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: rgba(20, 20, 30, 0.3);
|
|
position: absolute;
|
|
inset: 0;
|
|
z-index: 3;
|
|
transform: translateX(100%);
|
|
transition: transform 0.3s ease;
|
|
}
|
|
|
|
.chat-area.visible-mobile {
|
|
transform: translateX(0);
|
|
}
|
|
|
|
@media (min-width: 768px) {
|
|
.chat-area {
|
|
position: relative;
|
|
transform: none !important;
|
|
}
|
|
}
|
|
|
|
.chat-header {
|
|
padding: 16px 20px;
|
|
border-bottom: 1px solid var(--border);
|
|
background: var(--bg-header);
|
|
backdrop-filter: blur(10px);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.back-button {
|
|
background: rgba(60, 60, 70, 0.8);
|
|
border: 1px solid var(--border-light);
|
|
color: var(--text-primary);
|
|
padding: 8px 12px;
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
transition: background 0.2s;
|
|
}
|
|
|
|
.back-button:hover {
|
|
background: rgba(70, 70, 80, 0.9);
|
|
}
|
|
|
|
@media (min-width: 768px) {
|
|
.back-button { display: none; }
|
|
.chat-header { padding: 24px 32px; }
|
|
}
|
|
|
|
.contact-info h3 {
|
|
font-size: 16px;
|
|
margin-bottom: 4px;
|
|
font-weight: 600;
|
|
letter-spacing: -0.3px;
|
|
}
|
|
|
|
.npub {
|
|
font-size: 11px;
|
|
color: var(--text-dimmer);
|
|
font-family: 'SF Mono', Monaco, monospace;
|
|
}
|
|
|
|
@media (min-width: 768px) {
|
|
.contact-info h3 {
|
|
font-size: 18px;
|
|
margin-bottom: 6px;
|
|
}
|
|
.npub {
|
|
font-size: 13px;
|
|
}
|
|
}
|
|
|
|
.chat-messages {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 20px 16px;
|
|
background: rgba(18, 18, 28, 0.4);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
|
|
.chat-messages::-webkit-scrollbar {
|
|
width: 8px;
|
|
}
|
|
|
|
.chat-messages::-webkit-scrollbar-track {
|
|
background: transparent;
|
|
}
|
|
|
|
.chat-messages::-webkit-scrollbar-thumb {
|
|
background: rgba(255, 255, 255, 0.15);
|
|
border-radius: 4px;
|
|
}
|
|
|
|
@media (min-width: 768px) {
|
|
.chat-messages {
|
|
padding: 32px;
|
|
gap: 16px;
|
|
}
|
|
}
|
|
|
|
.message {
|
|
padding: 14px 16px;
|
|
border-radius: var(--radius-lg);
|
|
max-width: 85%;
|
|
word-wrap: break-word;
|
|
animation: slideIn 0.3s ease;
|
|
}
|
|
|
|
@keyframes slideIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(10px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
.message.sent {
|
|
background: rgba(60, 60, 70, 0.7);
|
|
backdrop-filter: blur(10px);
|
|
border: 1px solid var(--border-light);
|
|
margin-left: auto;
|
|
align-self: flex-end;
|
|
}
|
|
|
|
.message.received {
|
|
background: var(--primary-gradient);
|
|
color: #000;
|
|
box-shadow: 0 4px 20px rgba(253, 173, 1, 0.25);
|
|
font-weight: 500;
|
|
align-self: flex-start;
|
|
}
|
|
|
|
.message .content {
|
|
font-size: 15px;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.message .time {
|
|
font-size: 11px;
|
|
opacity: 0.5;
|
|
margin-top: 8px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.message.received .time {
|
|
opacity: 0.6;
|
|
}
|
|
|
|
@media (min-width: 768px) {
|
|
.message {
|
|
padding: 16px 20px;
|
|
border-radius: 18px;
|
|
max-width: 65%;
|
|
}
|
|
}
|
|
|
|
.chat-input-area {
|
|
padding: 16px 20px;
|
|
background: var(--bg-header);
|
|
backdrop-filter: blur(10px);
|
|
border-top: 1px solid var(--border);
|
|
display: flex;
|
|
gap: 10px;
|
|
align-items: center;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.chat-input-area input {
|
|
font-size: 16px; /* Prevents zoom on iOS */
|
|
}
|
|
|
|
@media (min-width: 768px) {
|
|
.chat-input-area {
|
|
padding: 24px 32px;
|
|
gap: 12px;
|
|
}
|
|
.chat-input-area input {
|
|
padding: 16px 20px;
|
|
border-radius: 14px;
|
|
}
|
|
}
|
|
|
|
/* ============================================
|
|
UTILITY CLASSES
|
|
============================================ */
|
|
.hidden {
|
|
display: none !important;
|
|
}
|
|
|
|
.empty-state {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 100%;
|
|
color: rgba(255, 255, 255, 0.35);
|
|
gap: 16px;
|
|
}
|
|
|
|
.empty-state h3 {
|
|
color: rgba(255, 255, 255, 0.5);
|
|
font-size: 20px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.empty-state-icon {
|
|
font-size: 64px;
|
|
opacity: 0.3;
|
|
}
|
|
|
|
.spinner {
|
|
border: 3px solid rgba(255, 255, 255, 0.1);
|
|
border-top: 3px solid var(--primary);
|
|
border-radius: 50%;
|
|
width: 20px;
|
|
height: 20px;
|
|
animation: spin 1s linear infinite;
|
|
display: inline-block;
|
|
margin-right: 10px;
|
|
}
|
|
|
|
@keyframes spin {
|
|
0% { transform: rotate(0deg); }
|
|
100% { transform: rotate(360deg); }
|
|
}
|
|
|
|
.loading-overlay {
|
|
position: absolute;
|
|
inset: 0;
|
|
background: rgba(25, 25, 35, 0.95);
|
|
backdrop-filter: blur(10px);
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 16px;
|
|
font-size: 15px;
|
|
color: rgba(255, 255, 255, 0.6);
|
|
}
|
|
|
|
.status {
|
|
padding: 10px 20px;
|
|
font-size: 13px;
|
|
text-align: center;
|
|
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.status.success {
|
|
background: #d4edda;
|
|
color: #155724;
|
|
}
|
|
|
|
.status.error {
|
|
background: #f8d7da;
|
|
color: #721c24;
|
|
}
|
|
|
|
.cache-info {
|
|
font-size: 11px;
|
|
color: var(--text-dimmer);
|
|
margin-top: 8px;
|
|
text-align: center;
|
|
}
|
|
|
|
.notification-badge {
|
|
background: #ff3b30;
|
|
color: white;
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
padding: 2px 6px;
|
|
border-radius: 10px;
|
|
min-width: 18px;
|
|
text-align: center;
|
|
position: absolute;
|
|
top: -4px;
|
|
right: -4px;
|
|
}
|
|
|
|
/* ============================================
|
|
MODAL
|
|
============================================ */
|
|
.modal-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, 0.8);
|
|
backdrop-filter: blur(10px);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 1000;
|
|
padding: 20px;
|
|
}
|
|
|
|
.modal {
|
|
background: rgba(30, 30, 40, 0.95);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-xl);
|
|
padding: 32px;
|
|
max-width: 500px;
|
|
width: 100%;
|
|
box-shadow: var(--shadow-lg);
|
|
}
|
|
|
|
.modal h3 {
|
|
color: var(--primary);
|
|
margin-bottom: 16px;
|
|
font-size: 22px;
|
|
}
|
|
|
|
.modal p {
|
|
color: var(--text-secondary);
|
|
margin-bottom: 12px;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.modal .btn-group {
|
|
display: flex;
|
|
gap: 12px;
|
|
margin-top: 24px;
|
|
}
|
|
|
|
.modal .btn-group .btn {
|
|
flex: 1;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div id="status" class="status hidden"></div>
|
|
|
|
<div id="loginSection" class="login-section">
|
|
<h2>📬 Nostr Dashboard</h2>
|
|
<p style="color: rgba(255, 255, 255, 0.7); margin-bottom: 30px;">Connect with your Nostr bunker to access your DMs</p>
|
|
|
|
|
|
<div class="input-group">
|
|
<label for="bunkerInput">Bunker URI</label>
|
|
<input type="text" id="bunkerInput" placeholder="bunker://pubkey?relay=wss://...&secret=...">
|
|
</div>
|
|
|
|
<button class="btn" onclick="connectWithBunker()" id="connectBtn">Connect to Bunker</button>
|
|
|
|
<div id="bunkerWaiting" class="hidden" style="margin-top: 20px; padding: 20px; background: rgba(253, 173, 1, 0.1); border-radius: 12px; text-align: center;">
|
|
<div class="spinner"></div>
|
|
<p style="color: rgba(255, 255, 255, 0.8); margin-top: 15px; font-size: 15px;">Waiting for approval from your remote signer...</p>
|
|
<p style="color: rgba(255, 255, 255, 0.5); margin-top: 8px; font-size: 13px;">Check your bunker device to approve this connection</p>
|
|
</div>
|
|
|
|
<div class="instructions">
|
|
<strong>How to connect:</strong>
|
|
<ol>
|
|
<li>Get your bunker URI from your remote signer (like <a href="https://nsec.app" target="_blank">nsec.app</a>, Amber, or <a href="https://nsec.btcforplebs.com" target="_blank">nsec.btcforplebs.com</a>)</li>
|
|
<li>Paste the bunker:// URI above</li>
|
|
<li>Click "Connect to Bunker"</li>
|
|
<li>Approve the connection request on your remote signer device</li>
|
|
<li>Your DMs will load automatically once approved</li>
|
|
</ol>
|
|
|
|
<div style="margin-top: 15px; padding: 12px; background: rgba(253, 173, 1, 0.15); border-radius: 8px;">
|
|
<strong style="color: #fdad01;">💡 What's a bunker?</strong>
|
|
<p style="margin-top: 8px; font-size: 13px;">A bunker is a remote signer that keeps your private keys secure. Your keys never leave the bunker - all signing happens remotely via encrypted Nostr messages.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="dashboard" class="dashboard">
|
|
<div class="header">
|
|
<h1 id="dashboardHeader">Dashboard</h1>
|
|
<div class="header-actions">
|
|
<button class="btn btn-secondary btn-small" onclick="showNotificationSettings()" id="notifBtn" title="Notification Settings">
|
|
<span id="notifIcon">🔔</span>
|
|
</button>
|
|
<!-- Notifications Panel -->
|
|
<div id="notificationsPanel" class="hidden" style="position: absolute; top: 50px; right: 20px; width: 300px; max-height: 400px; overflow-y: auto; background: rgba(20,20,30,0.95); backdrop-filter: blur(10px); border: 1px solid rgba(255,255,255,0.1); border-radius: 12px; padding: 12px; z-index: 20;">
|
|
<ul id="notifList" style="list-style: none; margin: 0; padding: 0;"></ul>
|
|
</div>
|
|
<button class="btn btn-secondary btn-small" onclick="disconnect()">Logout</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="dashboard-content">
|
|
<div class="sidebar">
|
|
<div class="sidebar-header">
|
|
<h2>Conversations</h2>
|
|
<input type="text" class="search-box" id="searchBox" placeholder="Search conversations..." oninput="filterConversations()">
|
|
<div class="cache-info" id="cacheInfo"></div>
|
|
<button class="btn btn-small btn-secondary" id="markAllReadBtn" style="margin-top: 10px; width: 100%;" onclick="markAllAsRead()">Mark All as Read</button>
|
|
</div>
|
|
<div class="conversations-list" id="conversationsList">
|
|
<div class="loading-overlay">
|
|
<div class="spinner"></div>
|
|
Loading DMs...
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="chat-area">
|
|
<div id="emptyState" class="empty-state">
|
|
<div class="empty-state-icon">💬</div>
|
|
<h3>Select a conversation</h3>
|
|
<p>Choose a contact from the sidebar to view messages</p>
|
|
</div>
|
|
|
|
<div id="chatView" class="hidden" style="display: flex; flex-direction: column; height: 100%;">
|
|
<div class="chat-header">
|
|
<button class="back-button" onclick="backToConversations()">
|
|
<span>←</span>
|
|
<span>Back</span>
|
|
</button>
|
|
<div class="contact-info">
|
|
<h3 id="chatContactName">Contact</h3>
|
|
<div class="npub" id="chatContactNpub"></div>
|
|
</div>
|
|
<div style="width: 60px;"></div>
|
|
</div>
|
|
|
|
<div class="chat-messages" id="chatMessages"></div>
|
|
|
|
<div class="chat-input-area">
|
|
<input type="text" id="messageInput" placeholder="Type a message..." onkeypress="handleKeyPress(event)">
|
|
<button class="btn" onclick="sendDM()">Send</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const { SimplePool, nip19, getPublicKey, generateSecretKey, finalizeEvent, nip44, nip04 } = window.NostrTools;
|
|
|
|
let pool;
|
|
let currentRelays = [
|
|
'wss://relay.primal.net',
|
|
'wss://relay.btcforplebs.com',
|
|
'wss://relay.damus.io',
|
|
'wss://nos.lol'
|
|
];
|
|
let userPubkey;
|
|
let userPrivkey;
|
|
let nip46Connection;
|
|
let subscriptions = [];
|
|
let conversations = new Map();
|
|
let currentChatPubkey = null;
|
|
let lastSyncTimestamp = 0;
|
|
let serviceWorkerRegistration = null;
|
|
let selectedConversationIndex = -1;
|
|
let conversationsList = [];
|
|
let profileCache = new Map();
|
|
|
|
// Cache management
|
|
const CACHE_VERSION = 2; // Incremented for encrypted cache
|
|
const CACHE_KEY_PREFIX = 'nostr_dm_cache_';
|
|
const MAX_CACHE_AGE = 7 * 24 * 60 * 60 * 1000;
|
|
|
|
// ============================================
|
|
// 🔐 ENCRYPTED CACHE UTILITIES
|
|
// ============================================
|
|
const CacheEncryption = {
|
|
// Derive an encryption key from the user's pubkey
|
|
async deriveKey(pubkey) {
|
|
const encoder = new TextEncoder();
|
|
const keyMaterial = await crypto.subtle.importKey(
|
|
'raw',
|
|
encoder.encode(pubkey),
|
|
'PBKDF2',
|
|
false,
|
|
['deriveKey']
|
|
);
|
|
|
|
const salt = encoder.encode('nostr-dm-cache-v1');
|
|
|
|
return crypto.subtle.deriveKey(
|
|
{
|
|
name: 'PBKDF2',
|
|
salt: salt,
|
|
iterations: 100000,
|
|
hash: 'SHA-256'
|
|
},
|
|
keyMaterial,
|
|
{ name: 'AES-GCM', length: 256 },
|
|
false,
|
|
['encrypt', 'decrypt']
|
|
);
|
|
},
|
|
|
|
// Encrypt data
|
|
async encrypt(data, pubkey) {
|
|
try {
|
|
const key = await this.deriveKey(pubkey);
|
|
const encoder = new TextEncoder();
|
|
const dataBuffer = encoder.encode(JSON.stringify(data));
|
|
|
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
|
|
const encryptedBuffer = await crypto.subtle.encrypt(
|
|
{ name: 'AES-GCM', iv: iv },
|
|
key,
|
|
dataBuffer
|
|
);
|
|
|
|
const combined = new Uint8Array(iv.length + encryptedBuffer.byteLength);
|
|
combined.set(iv, 0);
|
|
combined.set(new Uint8Array(encryptedBuffer), iv.length);
|
|
|
|
return btoa(String.fromCharCode(...combined));
|
|
} catch (error) {
|
|
console.error('Encryption failed:', error);
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
// Decrypt data
|
|
async decrypt(encryptedData, pubkey) {
|
|
try {
|
|
const key = await this.deriveKey(pubkey);
|
|
|
|
const combined = new Uint8Array(
|
|
atob(encryptedData).split('').map(c => c.charCodeAt(0))
|
|
);
|
|
|
|
const iv = combined.slice(0, 12);
|
|
const data = combined.slice(12);
|
|
|
|
const decryptedBuffer = await crypto.subtle.decrypt(
|
|
{ name: 'AES-GCM', iv: iv },
|
|
key,
|
|
data
|
|
);
|
|
|
|
const decoder = new TextDecoder();
|
|
return JSON.parse(decoder.decode(decryptedBuffer));
|
|
} catch (error) {
|
|
console.error('Decryption failed:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
};
|
|
|
|
// Check if running as PWA
|
|
function isPWA() {
|
|
return window.matchMedia('(display-mode: standalone)').matches ||
|
|
window.navigator.standalone === true ||
|
|
document.referrer.includes('android-app://');
|
|
}
|
|
|
|
// Keyboard navigation for conversations
|
|
document.addEventListener('keydown', (e) => {
|
|
if (!document.getElementById('dashboard').classList.contains('active')) return;
|
|
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
|
|
|
if (e.key === 'ArrowDown') {
|
|
e.preventDefault();
|
|
navigateConversations(1);
|
|
} else if (e.key === 'ArrowUp') {
|
|
e.preventDefault();
|
|
navigateConversations(-1);
|
|
} else if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
if (selectedConversationIndex >= 0 && conversationsList[selectedConversationIndex]) {
|
|
openChat(conversationsList[selectedConversationIndex]);
|
|
}
|
|
} else if (e.key === 'Escape') {
|
|
e.preventDefault();
|
|
if (currentChatPubkey) {
|
|
backToConversations();
|
|
}
|
|
}
|
|
});
|
|
|
|
// ============================================
|
|
// 📱 SWIPE BACK GESTURE (iOS-style)
|
|
// ============================================
|
|
let touchStartX = 0;
|
|
let touchStartY = 0;
|
|
let touchCurrentX = 0;
|
|
let isSwiping = false;
|
|
|
|
function initSwipeBack() {
|
|
const chatArea = document.querySelector('.chat-area');
|
|
if (!chatArea) return;
|
|
|
|
chatArea.addEventListener('touchstart', (e) => {
|
|
// Only enable swipe from left edge (first 50px)
|
|
if (e.touches[0].clientX > 50) return;
|
|
|
|
// Don't interfere if user is scrolling the messages
|
|
const target = e.target;
|
|
if (target.closest('.chat-messages')) {
|
|
touchStartX = e.touches[0].clientX;
|
|
touchStartY = e.touches[0].clientY;
|
|
} else {
|
|
touchStartX = e.touches[0].clientX;
|
|
touchStartY = e.touches[0].clientY;
|
|
}
|
|
}, { passive: true });
|
|
|
|
chatArea.addEventListener('touchmove', (e) => {
|
|
if (!touchStartX) return;
|
|
|
|
touchCurrentX = e.touches[0].clientX;
|
|
const touchCurrentY = e.touches[0].clientY;
|
|
|
|
const deltaX = touchCurrentX - touchStartX;
|
|
const deltaY = touchCurrentY - touchStartY;
|
|
|
|
// Only swipe if horizontal movement is greater than vertical
|
|
if (Math.abs(deltaX) > Math.abs(deltaY) && deltaX > 10) {
|
|
isSwiping = true;
|
|
|
|
// Apply transform for visual feedback
|
|
if (deltaX > 0 && deltaX < 300) {
|
|
chatArea.style.transform = `translateX(${deltaX}px)`;
|
|
chatArea.style.transition = 'none';
|
|
|
|
// Fade in sidebar as we swipe
|
|
const sidebar = document.querySelector('.sidebar');
|
|
sidebar.style.opacity = Math.min(deltaX / 150, 1);
|
|
}
|
|
}
|
|
}, { passive: true });
|
|
|
|
chatArea.addEventListener('touchend', (e) => {
|
|
if (!isSwiping) {
|
|
touchStartX = 0;
|
|
touchStartY = 0;
|
|
return;
|
|
}
|
|
|
|
const deltaX = touchCurrentX - touchStartX;
|
|
|
|
// If swiped more than 100px, go back
|
|
if (deltaX > 100) {
|
|
chatArea.style.transition = 'transform 0.3s ease';
|
|
chatArea.style.transform = 'translateX(100%)';
|
|
|
|
setTimeout(() => {
|
|
backToConversations();
|
|
chatArea.style.transform = '';
|
|
chatArea.style.transition = '';
|
|
const sidebar = document.querySelector('.sidebar');
|
|
sidebar.style.opacity = '';
|
|
}, 300);
|
|
} else {
|
|
// Snap back
|
|
chatArea.style.transition = 'transform 0.3s ease';
|
|
chatArea.style.transform = '';
|
|
const sidebar = document.querySelector('.sidebar');
|
|
sidebar.style.opacity = '';
|
|
}
|
|
|
|
touchStartX = 0;
|
|
touchStartY = 0;
|
|
touchCurrentX = 0;
|
|
isSwiping = false;
|
|
});
|
|
|
|
// Cancel swipe if touch is cancelled
|
|
chatArea.addEventListener('touchcancel', () => {
|
|
if (isSwiping) {
|
|
chatArea.style.transition = 'transform 0.3s ease';
|
|
chatArea.style.transform = '';
|
|
const sidebar = document.querySelector('.sidebar');
|
|
sidebar.style.opacity = '';
|
|
}
|
|
touchStartX = 0;
|
|
touchStartY = 0;
|
|
touchCurrentX = 0;
|
|
isSwiping = false;
|
|
});
|
|
}
|
|
|
|
// Initialize swipe back after DOM is ready
|
|
window.addEventListener('DOMContentLoaded', () => {
|
|
initSwipeBack();
|
|
});
|
|
|
|
function navigateConversations(direction) {
|
|
if (conversationsList.length === 0) return;
|
|
|
|
selectedConversationIndex += direction;
|
|
|
|
if (selectedConversationIndex < 0) {
|
|
selectedConversationIndex = conversationsList.length - 1;
|
|
} else if (selectedConversationIndex >= conversationsList.length) {
|
|
selectedConversationIndex = 0;
|
|
}
|
|
|
|
highlightSelectedConversation();
|
|
}
|
|
|
|
function highlightSelectedConversation() {
|
|
const items = document.querySelectorAll('.conversation-item');
|
|
items.forEach((item, index) => {
|
|
if (index === selectedConversationIndex) {
|
|
item.style.background = 'rgba(253, 173, 1, 0.15)';
|
|
item.style.outline = '2px solid rgba(253, 173, 1, 0.5)';
|
|
item.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
} else if (!item.classList.contains('active')) {
|
|
item.style.background = '';
|
|
item.style.outline = '';
|
|
}
|
|
});
|
|
}
|
|
|
|
window.addEventListener('DOMContentLoaded', () => {
|
|
if (!isPWA() && document.getElementById('pwaInstallBanner')) {
|
|
document.getElementById('pwaInstallBanner').classList.remove('hidden');
|
|
}
|
|
});
|
|
|
|
function getCacheKey(pubkey) {
|
|
return `${CACHE_KEY_PREFIX}${pubkey}_v${CACHE_VERSION}`;
|
|
}
|
|
|
|
function getProfileCacheKey(pubkey) {
|
|
return `nostr_profile_${pubkey}`;
|
|
}
|
|
|
|
// Fetch profile metadata for a pubkey
|
|
async function fetchProfile(pubkey) {
|
|
const cached = profileCache.get(pubkey);
|
|
if (cached) return cached;
|
|
|
|
try {
|
|
const stored = localStorage.getItem(getProfileCacheKey(pubkey));
|
|
if (stored) {
|
|
const profile = JSON.parse(stored);
|
|
const age = Date.now() - profile.timestamp;
|
|
if (age < 24 * 60 * 60 * 1000) {
|
|
profileCache.set(pubkey, profile);
|
|
return profile;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error('Error loading cached profile:', e);
|
|
}
|
|
|
|
try {
|
|
const profileData = await new Promise((resolve) => {
|
|
let resolved = false;
|
|
const timeout = setTimeout(() => {
|
|
if (!resolved) {
|
|
resolved = true;
|
|
resolve(null);
|
|
}
|
|
}, 5000);
|
|
|
|
const sub = pool.subscribeMany(
|
|
currentRelays,
|
|
[{ kinds: [0], authors: [pubkey], limit: 1 }],
|
|
{
|
|
onevent: (event) => {
|
|
if (!resolved) {
|
|
resolved = true;
|
|
clearTimeout(timeout);
|
|
sub.close();
|
|
try {
|
|
const content = JSON.parse(event.content);
|
|
resolve(content);
|
|
} catch (e) {
|
|
resolve(null);
|
|
}
|
|
}
|
|
},
|
|
oneose: () => {
|
|
if (!resolved) {
|
|
resolved = true;
|
|
clearTimeout(timeout);
|
|
resolve(null);
|
|
}
|
|
}
|
|
}
|
|
);
|
|
});
|
|
|
|
if (profileData) {
|
|
const profile = {
|
|
name: profileData.name || profileData.display_name || null,
|
|
displayName: profileData.display_name || profileData.name || null,
|
|
picture: profileData.picture || null,
|
|
nip05: profileData.nip05 || null,
|
|
timestamp: Date.now()
|
|
};
|
|
|
|
profileCache.set(pubkey, profile);
|
|
|
|
try {
|
|
localStorage.setItem(getProfileCacheKey(pubkey), JSON.stringify(profile));
|
|
} catch (e) {
|
|
console.error('Error saving profile to cache:', e);
|
|
}
|
|
|
|
return profile;
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching profile:', error);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function getDisplayName(pubkey) {
|
|
const profile = profileCache.get(pubkey);
|
|
if (profile && (profile.displayName || profile.name)) {
|
|
return profile.displayName || profile.name;
|
|
}
|
|
|
|
const npub = nip19.npubEncode(pubkey);
|
|
return npub.substring(0, 12) + '...' + npub.substring(npub.length - 4);
|
|
}
|
|
|
|
async function fetchAllProfiles() {
|
|
const pubkeys = Array.from(conversations.keys());
|
|
console.log('Fetching profiles for', pubkeys.length, 'contacts...');
|
|
|
|
const batchSize = 5;
|
|
for (let i = 0; i < pubkeys.length; i += batchSize) {
|
|
const batch = pubkeys.slice(i, i + batchSize);
|
|
await Promise.all(batch.map(pubkey => fetchProfile(pubkey)));
|
|
renderConversations();
|
|
}
|
|
|
|
console.log('✅ Profile fetching complete');
|
|
}
|
|
|
|
// 🔐 ENCRYPTED saveMessagesToCache
|
|
async function saveMessagesToCache(pubkey) {
|
|
try {
|
|
const cacheData = {
|
|
version: CACHE_VERSION,
|
|
timestamp: Date.now(),
|
|
conversations: Array.from(conversations.entries()).map(([key, value]) => ({
|
|
pubkey: key,
|
|
messages: value.messages,
|
|
lastMessage: value.lastMessage,
|
|
unread: value.unread,
|
|
lastReadTimestamp: value.lastReadTimestamp || 0
|
|
})),
|
|
lastSyncTimestamp: lastSyncTimestamp
|
|
};
|
|
|
|
const encrypted = await CacheEncryption.encrypt(cacheData, pubkey);
|
|
|
|
localStorage.setItem(getCacheKey(pubkey), encrypted);
|
|
console.log('💾🔒 Saved', conversations.size, 'conversations to ENCRYPTED cache');
|
|
updateCacheInfo();
|
|
} catch (error) {
|
|
console.error('Failed to save cache:', error);
|
|
if (error.name === 'QuotaExceededError') {
|
|
clearOldCaches();
|
|
}
|
|
}
|
|
}
|
|
|
|
// 🔓 ENCRYPTED loadMessagesFromCache
|
|
async function loadMessagesFromCache(pubkey) {
|
|
try {
|
|
const cached = localStorage.getItem(getCacheKey(pubkey));
|
|
if (!cached) {
|
|
console.log('No cache found');
|
|
return false;
|
|
}
|
|
|
|
const cacheData = await CacheEncryption.decrypt(cached, pubkey);
|
|
|
|
if (cacheData.version !== CACHE_VERSION) {
|
|
console.log('Cache version mismatch, clearing');
|
|
localStorage.removeItem(getCacheKey(pubkey));
|
|
return false;
|
|
}
|
|
|
|
const cacheAge = Date.now() - cacheData.timestamp;
|
|
if (cacheAge > MAX_CACHE_AGE) {
|
|
console.log('Cache too old, clearing');
|
|
localStorage.removeItem(getCacheKey(pubkey));
|
|
return false;
|
|
}
|
|
|
|
conversations.clear();
|
|
cacheData.conversations.forEach(conv => {
|
|
conversations.set(conv.pubkey, {
|
|
messages: conv.messages,
|
|
lastMessage: conv.lastMessage,
|
|
unread: conv.unread || 0,
|
|
lastReadTimestamp: conv.lastReadTimestamp || 0
|
|
});
|
|
});
|
|
|
|
lastSyncTimestamp = cacheData.lastSyncTimestamp || 0;
|
|
|
|
console.log('✅🔓 Loaded', conversations.size, 'conversations from ENCRYPTED cache');
|
|
console.log('Last sync:', new Date(lastSyncTimestamp * 1000).toLocaleString());
|
|
|
|
renderConversations();
|
|
updateCacheInfo();
|
|
return true;
|
|
} catch (error) {
|
|
console.error('Failed to load cache:', error);
|
|
localStorage.removeItem(getCacheKey(pubkey));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function clearOldCaches() {
|
|
const keys = Object.keys(localStorage);
|
|
keys.forEach(key => {
|
|
if (key.startsWith(CACHE_KEY_PREFIX)) {
|
|
try {
|
|
const data = JSON.parse(localStorage.getItem(key));
|
|
const age = Date.now() - data.timestamp;
|
|
if (age > MAX_CACHE_AGE) {
|
|
localStorage.removeItem(key);
|
|
console.log('Cleared old cache:', key);
|
|
}
|
|
} catch (e) {
|
|
localStorage.removeItem(key);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function updateCacheInfo() {
|
|
const info = document.getElementById('cacheInfo');
|
|
if (conversations.size > 0) {
|
|
const msgCount = Array.from(conversations.values())
|
|
.reduce((sum, conv) => sum + conv.messages.length, 0);
|
|
const lastSync = lastSyncTimestamp > 0
|
|
? new Date(lastSyncTimestamp * 1000).toLocaleTimeString()
|
|
: 'Never';
|
|
info.textContent = `${conversations.size} chats • ${msgCount} messages • Last sync: ${lastSync}`;
|
|
} else {
|
|
info.textContent = '';
|
|
}
|
|
}
|
|
|
|
function showNotificationSettings() {
|
|
const panel = document.getElementById('notificationsPanel');
|
|
panel.classList.toggle('hidden');
|
|
renderNotifications();
|
|
}
|
|
|
|
|
|
|
|
// --- Live Nostr Notifications Panel ---
|
|
let notifications = [];
|
|
|
|
function addNotificationToList(pubkey, text, timestamp) {
|
|
const list = document.getElementById('notifList');
|
|
const li = document.createElement('li');
|
|
li.style.padding = '6px 0';
|
|
li.style.borderBottom = '1px solid rgba(255,255,255,0.08)';
|
|
|
|
const displayName = getDisplayName(pubkey);
|
|
const relativeTime = getRelativeTime(timestamp);
|
|
|
|
li.textContent = `${displayName} ${text} • ${relativeTime}`;
|
|
list.prepend(li);
|
|
notifications.unshift({ pubkey, text, timestamp });
|
|
}
|
|
|
|
function getRelativeTime(timestamp) {
|
|
const now = Math.floor(Date.now() / 1000);
|
|
const diff = now - timestamp;
|
|
|
|
if (diff < 60) return `${diff} seconds ago`;
|
|
if (diff < 3600) return `${Math.floor(diff / 60)} minutes ago`;
|
|
if (diff < 86400) return `${Math.floor(diff / 3600)} hours ago`;
|
|
return `${Math.floor(diff / 86400)} days ago`;
|
|
}
|
|
|
|
function processNotification(event) {
|
|
let text = '';
|
|
switch(event.kind) {
|
|
case 7:
|
|
text = 'reacted to your note';
|
|
break;
|
|
case 9735:
|
|
text = `zapped you ${event.content} sats`;
|
|
break;
|
|
case 3:
|
|
text = 'followed you';
|
|
break;
|
|
}
|
|
if (text) addNotificationToList(event.pubkey, text, event.created_at);
|
|
}
|
|
|
|
function subscribeNotifications() {
|
|
if (!pool || !userPubkey) return;
|
|
|
|
const notifFilters = [
|
|
{ kinds: [7, 9735], '#p': [userPubkey], limit: 50 },
|
|
{ kinds: [3], authors: [userPubkey], limit: 50 }
|
|
];
|
|
|
|
pool.subscribeMany(currentRelays, notifFilters, {
|
|
onevent: async (event) => {
|
|
processNotification(event);
|
|
}
|
|
});
|
|
}
|
|
|
|
if (document.getElementById('notifBtn')) {
|
|
document.getElementById('notifBtn').addEventListener('click', () => {
|
|
document.getElementById('notificationsPanel').classList.toggle('hidden');
|
|
});
|
|
}
|
|
|
|
window.addEventListener('DOMContentLoaded', async () => {
|
|
console.log('🚀 Page loaded, checking for saved session...');
|
|
const restored = await restoreSession();
|
|
if (!restored) {
|
|
console.log('No valid session, showing login screen');
|
|
}
|
|
});
|
|
|
|
class NIP46Client {
|
|
constructor(appName) {
|
|
this.localPrivkey = generateSecretKey();
|
|
this.localPubkey = getPublicKey(this.localPrivkey);
|
|
this.appName = appName;
|
|
this.secret = this.generateSecret();
|
|
this.relays = currentRelays;
|
|
this.pendingRequests = new Map();
|
|
this.remotePubkey = null;
|
|
this.connected = false;
|
|
this.conversationKey = null;
|
|
}
|
|
|
|
generateSecret() {
|
|
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
|
}
|
|
|
|
async handleResponse(event) {
|
|
if (!this.remotePubkey) {
|
|
this.remotePubkey = event.pubkey;
|
|
this.conversationKey = nip44.v2.utils.getConversationKey(this.localPrivkey, this.remotePubkey);
|
|
}
|
|
|
|
if (event.pubkey !== this.remotePubkey) return;
|
|
|
|
try {
|
|
const decrypted = nip44.v2.decrypt(event.content, this.conversationKey);
|
|
const response = JSON.parse(decrypted);
|
|
|
|
if (response.result === this.secret || response.result === 'ack') {
|
|
this.connected = true;
|
|
return;
|
|
}
|
|
|
|
const pending = this.pendingRequests.get(response.id);
|
|
if (pending) {
|
|
this.pendingRequests.delete(response.id);
|
|
if (response.error) {
|
|
pending.reject(new Error(response.error));
|
|
} else {
|
|
pending.resolve(response.result);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Decrypt error:', error);
|
|
}
|
|
}
|
|
|
|
async sendRequest(method, params) {
|
|
if (!this.connected) throw new Error('Not connected');
|
|
|
|
const id = Math.random().toString(36).substring(7);
|
|
const request = { id, method, params };
|
|
|
|
const encrypted = nip44.v2.encrypt(JSON.stringify(request), this.conversationKey);
|
|
|
|
const event = finalizeEvent({
|
|
kind: 24133,
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
tags: [['p', this.remotePubkey]],
|
|
content: encrypted
|
|
}, this.localPrivkey);
|
|
|
|
await this.pool.publish(this.relays, event);
|
|
|
|
return new Promise((resolve, reject) => {
|
|
this.pendingRequests.set(id, { resolve, reject });
|
|
setTimeout(() => {
|
|
if (this.pendingRequests.has(id)) {
|
|
this.pendingRequests.delete(id);
|
|
reject(new Error('Timeout'));
|
|
}
|
|
}, 30000);
|
|
});
|
|
}
|
|
|
|
async getPublicKey() {
|
|
return await this.sendRequest('get_public_key', []);
|
|
}
|
|
|
|
async nip04Encrypt(pubkey, plaintext) {
|
|
return await this.sendRequest('nip04_encrypt', [pubkey, plaintext]);
|
|
}
|
|
|
|
async nip04Decrypt(pubkey, ciphertext) {
|
|
return await this.sendRequest('nip04_decrypt', [pubkey, ciphertext]);
|
|
}
|
|
|
|
async signEvent(event) {
|
|
const signed = await this.sendRequest('sign_event', [JSON.stringify(event)]);
|
|
return JSON.parse(signed);
|
|
}
|
|
|
|
close() {
|
|
if (this.sub) this.sub.close();
|
|
if (this.pool) this.pool.close(this.relays);
|
|
}
|
|
}
|
|
|
|
function showStatus(message, type = 'info') {
|
|
const status = document.getElementById('status');
|
|
status.textContent = message;
|
|
status.className = 'status';
|
|
if (type === 'success') status.classList.add('success');
|
|
if (type === 'error') status.classList.add('error');
|
|
status.classList.remove('hidden');
|
|
|
|
if (type !== 'error') {
|
|
setTimeout(() => status.classList.add('hidden'), 3000);
|
|
}
|
|
}
|
|
|
|
async function connectWithBunker() {
|
|
const bunkerUri = document.getElementById('bunkerInput').value.trim();
|
|
|
|
if (!bunkerUri || !bunkerUri.startsWith('bunker://')) {
|
|
showStatus('Please enter a valid bunker:// URI', 'error');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
showStatus('Parsing bunker URI...', 'info');
|
|
document.getElementById('bunkerWaiting').classList.remove('hidden');
|
|
document.getElementById('connectBtn').disabled = true;
|
|
|
|
const url = new URL(bunkerUri);
|
|
const remotePubkey = url.hostname || url.pathname.replace('//', '');
|
|
const relays = url.searchParams.getAll('relay');
|
|
const secret = url.searchParams.get('secret');
|
|
|
|
if (!remotePubkey) {
|
|
throw new Error('Invalid bunker URI: missing pubkey');
|
|
}
|
|
|
|
if (relays.length === 0) {
|
|
relays.push(...currentRelays);
|
|
}
|
|
|
|
console.log('Connecting to bunker:', { remotePubkey, relays, secret });
|
|
|
|
nip46Connection = new NIP46Client('BTC for Plebs DMs');
|
|
nip46Connection.remotePubkey = remotePubkey;
|
|
nip46Connection.relays = relays;
|
|
nip46Connection.pool = new SimplePool();
|
|
|
|
nip46Connection.conversationKey = nip44.v2.utils.getConversationKey(
|
|
nip46Connection.localPrivkey,
|
|
remotePubkey
|
|
);
|
|
|
|
showStatus('Sending connect request to remote signer...', 'info');
|
|
|
|
const connectRequest = {
|
|
id: 'connect_' + Math.random().toString(36).substring(7),
|
|
method: 'connect',
|
|
params: [
|
|
nip46Connection.localPubkey,
|
|
secret || '',
|
|
'nip04_encrypt,nip04_decrypt,sign_event:4,get_public_key',
|
|
{
|
|
app_name: 'Nostr DM Dashboard',
|
|
app_icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAAAXNSR0IArs4c6QAAAPJlWElmTU0AKgAAAAgABwESAAMAAAABAAEAAAEaAAUAAAABAAAAYgEbAAUAAAABAAAAagEoAAMAAAABAAIAAAExAAIAAAAiAAAAcgEyAAIAAAAUAAAAlIdpAAQAAAABAAAAqAAAAAAAAABIAAAAAQAAAEgAAAABQWRvYmUgUGhvdG9zaG9wIDI2LjEwIChNYWNpbnRvc2gpADIwMjU6MTA6MTMgMTg6NTg6MjMAAASQBAACAAAAFAAAAN6gAQADAAAAAQABAACgAgAEAAAAAQAAAICgAwAEAAAAAQAAAIAAAAAAMjAyNToxMDoxMyAxODo1NjoxNgALj7ylAAAACXBIWXMAAAsTAAALEwEAmpwYAAAIzmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgICAgICAgICAgIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIKICAgICAgICAgICAgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iCiAgICAgICAgICAgIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiCiAgICAgICAgICAgIHhtbG5zOnRpZmY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vdGlmZi8xLjAvIgogICAgICAgICAgICB4bWxuczpwaG90b3Nob3A9Imh0dHA6Ly9ucy5hZG9iZS5jb20vcGhvdG9zaG9wLzEuMC8iPgogICAgICAgICA8ZGM6Zm9ybWF0PmltYWdlL3BuZzwvZGM6Zm9ybWF0PgogICAgICAgICA8eG1wOk1vZGlmeURhdGU+MjAyNS0xMC0xM1QxODo1ODoyMy0wNDowMDwveG1wOk1vZGlmeURhdGU+CiAgICAgICAgIDx4bXA6Q3JlYXRvclRvb2w+QWRvYmUgUGhvdG9zaG9wIDI2LjEwIChNYWNpbnRvc2gpPC94bXA6Q3JlYXRvclRvb2w+CiAgICAgICAgIDx4bXA6Q3JlYXRlRGF0ZT4yMDI1LTEwLTEzVDE4OjU2OjE2LTA0OjAwPC94bXA6Q3JlYXRlRGF0ZT4KICAgICAgICAgPHhtcDpNZXRhZGF0YURhdGU+MjAyNS0xMC0xM1QxODo1ODoyMy0wNDowMDwveG1wOk1ldGFkYXRhRGF0ZT4KICAgICAgICAgPHhtcE1NOkhpc3Rvcnk+CiAgICAgICAgICAgIDxyZGY6U2VxPgogICAgICAgICAgICAgICA8cmRmOmxpIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OnNvZnR3YXJlQWdlbnQ+QWRvYmUgUGhvdG9zaG9wIDI2LjEwIChNYWNpbnRvc2gpPC9zdEV2dDpzb2Z0d2FyZUFnZW50PgogICAgICAgICAgICAgICAgICA8c3RFdnQ6d2hlbj4yMDI1LTEwLTEzVDE4OjU2OjE2LTA0OjAwPC9zdEV2dDp3aGVuPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6aW5zdGFuY2VJRD54bXAuaWlkOjNhYjg0YTZjLWY4ZDQtNDc4Ny1hZjM5LTQ5ODYzMjZkMTdmYTwvc3RFdnQ6aW5zdGFuY2VJRD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5jcmVhdGVkPC9zdEV2dDphY3Rpb24+CiAgICAgICAgICAgICAgIDwvcmRmOmxpPgogICAgICAgICAgICAgICA8cmRmOmxpIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OnNvZnR3YXJlQWdlbnQ+QWRvYmUgUGhvdG9zaG9wIDI2LjEwIChNYWNpbnRvc2gpPC9zdEV2dDpzb2Z0d2FyZUFnZW50PgogICAgICAgICAgICAgICAgICA8c3RFdnQ6Y2hhbmdlZD4vPC9zdEV2dDpjaGFuZ2VkPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6d2hlbj4yMDI1LTEwLTEzVDE4OjU4OjIzLTA0OjAwPC9zdEV2dDp3aGVuPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6aW5zdGFuY2VJRD54bXAuaWlkOjA1ZGVlYTU2LTIwNzctNDMyNy1iY2JkLWYyMWI5NmVjNTU5YTwvc3RFdnQ6aW5zdGFuY2VJRD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5zYXZlZDwvc3RFdnQ6YWN0aW9uPgogICAgICAgICAgICAgICA8L3JkZjpsaT4KICAgICAgICAgICAgPC9yZGY6U2VxPgogICAgICAgICA8L3htcE1NOkhpc3Rvcnk+CiAgICAgICAgIDx4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ+eG1wLmRpZDozYWI4NGE2Yy1mOGQ0LTQ3ODctYWYzOS00OTg2MzI2ZDE3ZmE8L3htcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD4KICAgICAgICAgPHhtcE1NOkRvY3VtZW50SUQ+eG1wLmRpZDozYWI4NGE2Yy1mOGQ0LTQ3ODctYWYzOS00OTg2MzI2ZDE3ZmE8L3htcE1NOkRvY3VtZW50SUQ+CiAgICAgICAgIDx4bXBNTTpJbnN0YW5jZUlEPnhtcC5paWQ6MDVkZWVhNTYtMjA3Ny00MzI3LWJjYmQtZjIxYjk2ZWM1NTlhPC94bXBNTTpJbnN0YW5jZUlEPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICAgICA8cGhvdG9zaG9wOkNvbG9yTW9kZT4zPC9waG90b3Nob3A6Q29sb3JNb2RlPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KIp26SgAAPldJREFUeAHtfQmcXkWxb32zZrYkkz2TfSckBBISEiAoCrKJC4ui16feq4iKCwJ6Rd9P7/MiVx9uqA8Rt+d1A0QFBdkFkwDZA4Tse0ISsm+z7/f/r+46X58z30xmJjNZfr/0zDndXV1VXV1V3adP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'
|
|
}
|
|
]
|
|
};
|
|
const encrypted = nip44.v2.encrypt(
|
|
JSON.stringify(connectRequest),
|
|
nip46Connection.conversationKey
|
|
);
|
|
|
|
const connectEvent = finalizeEvent({
|
|
kind: 24133,
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
tags: [['p', remotePubkey]],
|
|
content: encrypted
|
|
}, nip46Connection.localPrivkey);
|
|
|
|
await nip46Connection.pool.publish(relays, connectEvent);
|
|
|
|
console.log('Connect request sent, waiting for approval...');
|
|
|
|
const response = await new Promise((resolve, reject) => {
|
|
const timeout = setTimeout(() => {
|
|
reject(new Error('Connection timeout - no approval from bunker'));
|
|
}, 120000);
|
|
|
|
nip46Connection.pendingRequests.set(connectRequest.id, { resolve, reject });
|
|
|
|
nip46Connection.sub = nip46Connection.pool.subscribeMany(
|
|
relays,
|
|
[{
|
|
kinds: [24133],
|
|
'#p': [nip46Connection.localPubkey],
|
|
since: Math.floor(Date.now() / 1000) - 10
|
|
}],
|
|
{
|
|
onevent: async (event) => {
|
|
if (event.pubkey !== remotePubkey) return;
|
|
|
|
try {
|
|
const decrypted = nip44.v2.decrypt(event.content, nip46Connection.conversationKey);
|
|
const response = JSON.parse(decrypted);
|
|
|
|
console.log('Received response:', response);
|
|
|
|
if (response.id === connectRequest.id) {
|
|
if (response.result === 'auth_url' && response.error) {
|
|
console.log('Auth URL received:', response.error);
|
|
showStatus('Approve connection on your bunker device...', 'info');
|
|
return;
|
|
}
|
|
|
|
if (response.result === 'ack') {
|
|
clearTimeout(timeout);
|
|
nip46Connection.connected = true;
|
|
resolve(response);
|
|
return;
|
|
}
|
|
}
|
|
|
|
const pending = nip46Connection.pendingRequests.get(response.id);
|
|
if (pending) {
|
|
nip46Connection.pendingRequests.delete(response.id);
|
|
if (response.error) {
|
|
pending.reject(new Error(response.error));
|
|
} else {
|
|
pending.resolve(response.result);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error handling bunker response:', error);
|
|
}
|
|
}
|
|
}
|
|
);
|
|
});
|
|
|
|
console.log('Connection approved!', response);
|
|
|
|
userPubkey = await nip46Connection.getPublicKey();
|
|
pool = nip46Connection.pool;
|
|
|
|
const sessionData = {
|
|
bunkerUri: bunkerUri,
|
|
remotePubkey: remotePubkey,
|
|
relays: relays,
|
|
localPrivkey: Array.from(nip46Connection.localPrivkey).map(b => b.toString(16).padStart(2, '0')).join(''),
|
|
localPubkey: nip46Connection.localPubkey,
|
|
userPubkey: userPubkey,
|
|
timestamp: Date.now()
|
|
};
|
|
localStorage.setItem('bunker_session', JSON.stringify(sessionData));
|
|
console.log('💾 Saved bunker session');
|
|
|
|
document.getElementById('bunkerWaiting').classList.add('hidden');
|
|
showStatus('Connected to bunker successfully!', 'success');
|
|
|
|
await initDashboard();
|
|
|
|
} catch (error) {
|
|
console.error('Bunker connection error:', error);
|
|
document.getElementById('bunkerWaiting').classList.add('hidden');
|
|
document.getElementById('connectBtn').disabled = false;
|
|
showStatus('Failed to connect: ' + error.message, 'error');
|
|
}
|
|
}
|
|
|
|
async function initDashboard() {
|
|
console.log('🎯 Initializing dashboard...');
|
|
document.getElementById('loginSection').style.display = 'none';
|
|
const dashboardEl = document.getElementById('dashboard');
|
|
dashboardEl.style.display = 'flex';
|
|
dashboardEl.classList.add('active');
|
|
|
|
console.log('✅ Dashboard visible, loading DMs...');
|
|
console.log('User pubkey:', userPubkey);
|
|
console.log('Relays:', currentRelays);
|
|
|
|
try {
|
|
const profile = await fetchProfile(userPubkey);
|
|
const displayName = (profile && (profile.displayName || profile.name)) || 'Account';
|
|
document.getElementById('dashboardHeader').textContent = `${displayName} Dashboard`;
|
|
} catch (e) {
|
|
document.getElementById('dashboardHeader').textContent = `Dashboard`;
|
|
}
|
|
|
|
const cachedLoaded = await loadMessagesFromCache(userPubkey);
|
|
if (cachedLoaded && conversations.size > 0) {
|
|
document.querySelector('.loading-overlay')?.remove();
|
|
showStatus('Loaded from encrypted cache, syncing new messages...', 'info');
|
|
}
|
|
|
|
loadDMs();
|
|
subscribeNotifications();
|
|
}
|
|
|
|
async function loadDMs() {
|
|
console.log('📬 Subscribing to DMs...');
|
|
console.log('🚀 Starting DM sync...');
|
|
showStatus('Syncing new messages...', 'info');
|
|
const overlay = document.querySelector('.loading-overlay');
|
|
if (overlay) {
|
|
overlay.classList.remove('hidden');
|
|
const msg = overlay.querySelector('div:nth-child(2)');
|
|
if (msg) msg.textContent = 'Syncing messages...';
|
|
}
|
|
let pendingDecrypts = 0;
|
|
console.log('Current relays:', currentRelays);
|
|
console.log('User pubkey:', userPubkey);
|
|
console.log('Last sync timestamp:', lastSyncTimestamp, '(' + new Date(lastSyncTimestamp * 1000).toLocaleString() + ')');
|
|
|
|
const filters = lastSyncTimestamp > 0 ? [
|
|
{ kinds: [4], authors: [userPubkey], since: lastSyncTimestamp, limit: 500 },
|
|
{ kinds: [4], '#p': [userPubkey], since: lastSyncTimestamp, limit: 500 }
|
|
] : [
|
|
{ kinds: [4], authors: [userPubkey], limit: 200 },
|
|
{ kinds: [4], '#p': [userPubkey], limit: 200 }
|
|
];
|
|
|
|
console.log('Subscribing with filters:', JSON.stringify(filters, null, 2));
|
|
|
|
let eventCount = 0;
|
|
let initialSyncComplete = false;
|
|
|
|
const sub = pool.subscribeMany(
|
|
currentRelays,
|
|
filters,
|
|
{
|
|
onevent: async (event) => {
|
|
eventCount++;
|
|
console.log('📨 Received DM event #' + eventCount + ':', event.id, 'created:', new Date(event.created_at * 1000).toLocaleString());
|
|
pendingDecrypts++;
|
|
const wasNew = await processDM(event);
|
|
pendingDecrypts--;
|
|
if (wasNew) {
|
|
console.log(`💬 New message from ${event.pubkey.slice(0, 12)}...`);
|
|
// Play notification sound or show notification for new messages during real-time
|
|
if (initialSyncComplete && event.pubkey !== userPubkey) {
|
|
console.log('🔔 Real-time message received!');
|
|
}
|
|
}
|
|
if (event.created_at > lastSyncTimestamp) {
|
|
lastSyncTimestamp = event.created_at;
|
|
}
|
|
},
|
|
oneose: () => {
|
|
console.log('⏹️ Initial sync complete, subscription staying open for real-time messages...');
|
|
|
|
// Update timestamp to NOW so future messages are considered new
|
|
const now = Math.floor(Date.now() / 1000);
|
|
if (lastSyncTimestamp < now) {
|
|
lastSyncTimestamp = now;
|
|
console.log('Updated lastSyncTimestamp to current time:', now);
|
|
}
|
|
|
|
renderConversations();
|
|
|
|
// Wait for any pending decryptions from initial sync
|
|
const checkCompletion = setInterval(() => {
|
|
if (pendingDecrypts === 0) {
|
|
clearInterval(checkCompletion);
|
|
console.log('✅ Initial sync processed, now listening for real-time messages...');
|
|
initialSyncComplete = true;
|
|
saveMessagesToCache(userPubkey);
|
|
if (overlay) overlay.classList.add('hidden');
|
|
showStatus(eventCount > 0 ? `Synced ${eventCount} messages` : 'Up to date', 'success');
|
|
fetchAllProfiles();
|
|
} else {
|
|
console.log(`⏳ Waiting for ${pendingDecrypts} pending decryptions...`);
|
|
if (overlay) {
|
|
const msg = overlay.querySelector('div:nth-child(2)');
|
|
if (msg) msg.textContent = `Decrypting ${pendingDecrypts} messages...`;
|
|
}
|
|
}
|
|
}, 300);
|
|
|
|
// CRITICAL: Don't close the subscription! It needs to stay open for real-time messages
|
|
}
|
|
}
|
|
);
|
|
|
|
subscriptions.push(sub);
|
|
console.log('✅ Subscription active and will remain open for real-time messages');
|
|
|
|
if (!window.autoSaveInterval) {
|
|
window.autoSaveInterval = setInterval(() => {
|
|
if (userPubkey) {
|
|
saveMessagesToCache(userPubkey);
|
|
}
|
|
}, 30000);
|
|
}
|
|
|
|
// Add visibility change handler to reconnect if needed
|
|
if (!window.visibilityChangeHandler) {
|
|
window.visibilityChangeHandler = () => {
|
|
if (document.visibilityState === 'visible' && userPubkey) {
|
|
console.log('🔄 Page became visible, checking connection...');
|
|
// Subscriptions should still be active, but we can sync any missed messages
|
|
const timeSinceLastSync = Math.floor(Date.now() / 1000) - lastSyncTimestamp;
|
|
if (timeSinceLastSync > 60) {
|
|
console.log('⏰ Been away for a while, syncing missed messages...');
|
|
// Force a quick sync for any missed messages
|
|
syncMissedMessages();
|
|
}
|
|
}
|
|
};
|
|
document.addEventListener('visibilitychange', window.visibilityChangeHandler);
|
|
}
|
|
}
|
|
|
|
// New function to sync missed messages when returning to app
|
|
async function syncMissedMessages() {
|
|
console.log('🔄 Syncing messages missed while away...');
|
|
const syncFilters = [
|
|
{ kinds: [4], authors: [userPubkey], since: lastSyncTimestamp, limit: 100 },
|
|
{ kinds: [4], '#p': [userPubkey], since: lastSyncTimestamp, limit: 100 }
|
|
];
|
|
|
|
const events = await pool.querySync(currentRelays, syncFilters);
|
|
console.log(`Found ${events.length} messages while away`);
|
|
|
|
for (const event of events) {
|
|
await processDM(event);
|
|
}
|
|
|
|
if (events.length > 0) {
|
|
renderConversations();
|
|
saveMessagesToCache(userPubkey);
|
|
showStatus(`Synced ${events.length} new messages`, 'success');
|
|
}
|
|
|
|
// Update timestamp
|
|
lastSyncTimestamp = Math.floor(Date.now() / 1000);
|
|
}
|
|
|
|
async function processDM(event) {
|
|
try {
|
|
const otherPubkey = event.pubkey === userPubkey
|
|
? event.tags.find(t => t[0] === 'p')?.[1]
|
|
: event.pubkey;
|
|
if (!otherPubkey) return false;
|
|
|
|
let decryptedContent;
|
|
if (nip46Connection) {
|
|
decryptedContent = await nip46Connection.nip04Decrypt(otherPubkey, event.content);
|
|
} else {
|
|
decryptedContent = await nip04.decrypt(userPrivkey, otherPubkey, event.content);
|
|
}
|
|
|
|
if (!conversations.has(otherPubkey)) {
|
|
conversations.set(otherPubkey, {
|
|
messages: [],
|
|
lastMessage: null,
|
|
unread: 0,
|
|
lastReadTimestamp: 0
|
|
});
|
|
}
|
|
|
|
const conversation = conversations.get(otherPubkey);
|
|
|
|
if (conversation.messages.find(m => m.id === event.id)) return false;
|
|
|
|
const message = {
|
|
id: event.id,
|
|
content: decryptedContent,
|
|
timestamp: event.created_at,
|
|
from: event.pubkey,
|
|
isSent: event.pubkey === userPubkey
|
|
};
|
|
|
|
const index = conversation.messages.findIndex(m => m.timestamp > message.timestamp);
|
|
if (index === -1) {
|
|
conversation.messages.push(message);
|
|
} else {
|
|
conversation.messages.splice(index, 0, message);
|
|
}
|
|
|
|
conversation.lastMessage = conversation.messages[conversation.messages.length - 1];
|
|
|
|
if (!message.isSent && otherPubkey !== currentChatPubkey && message.timestamp > conversation.lastReadTimestamp) {
|
|
conversation.unread++;
|
|
}
|
|
|
|
renderConversations();
|
|
if (currentChatPubkey === otherPubkey) renderChat(otherPubkey);
|
|
|
|
if (event.created_at > lastSyncTimestamp) lastSyncTimestamp = event.created_at;
|
|
|
|
await saveMessagesToCache(userPubkey);
|
|
return true;
|
|
|
|
} catch (err) {
|
|
console.error('DM error:', err);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function renderConversations() {
|
|
const listEl = document.getElementById('conversationsList');
|
|
|
|
const loading = listEl.querySelector('.loading-overlay');
|
|
if (loading) loading.remove();
|
|
|
|
const sorted = Array.from(conversations.entries())
|
|
.sort((a, b) => {
|
|
const timeA = a[1].lastMessage?.timestamp || 0;
|
|
const timeB = b[1].lastMessage?.timestamp || 0;
|
|
return timeB - timeA;
|
|
});
|
|
|
|
conversationsList = sorted.map(([pubkey]) => pubkey);
|
|
|
|
listEl.innerHTML = sorted.map(([pubkey, conv], index) => {
|
|
const displayName = getDisplayName(pubkey);
|
|
const npub = nip19.npubEncode(pubkey);
|
|
const shortNpub = npub.substring(0, 12) + '...' + npub.substring(npub.length - 4);
|
|
|
|
const lastMsg = conv.lastMessage;
|
|
const time = lastMsg ? new Date(lastMsg.timestamp * 1000).toLocaleString() : '';
|
|
const preview = lastMsg ? (lastMsg.content.substring(0, 50) + (lastMsg.content.length > 50 ? '...' : '')) : 'No messages';
|
|
const unreadBadge = conv.unread > 0 ? `<span class="unread-badge">${conv.unread}</span>` : '';
|
|
const active = currentChatPubkey === pubkey ? 'active' : '';
|
|
|
|
const profile = profileCache.get(pubkey);
|
|
const hasName = profile && (profile.displayName || profile.name);
|
|
const subtitle = hasName ? `<div style="font-size: 11px; color: rgba(255, 255, 255, 0.35); margin-top: 2px;">${shortNpub}</div>` : '';
|
|
|
|
return `
|
|
<div class="conversation-item ${active}" onclick="openChat('${pubkey}')" data-index="${index}">
|
|
<div class="contact-name">${escapeHtml(displayName)}${unreadBadge}</div>
|
|
${subtitle}
|
|
<div class="last-message">${escapeHtml(preview)}</div>
|
|
<div class="timestamp">${time}</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
|
|
if (selectedConversationIndex >= 0) {
|
|
highlightSelectedConversation();
|
|
}
|
|
|
|
updateCacheInfo();
|
|
}
|
|
|
|
function openChat(pubkey) {
|
|
currentChatPubkey = pubkey;
|
|
const conversation = conversations.get(pubkey);
|
|
|
|
selectedConversationIndex = conversationsList.indexOf(pubkey);
|
|
|
|
if (conversation) {
|
|
conversation.unread = 0;
|
|
if (conversation.lastMessage) {
|
|
conversation.lastReadTimestamp = conversation.lastMessage.timestamp;
|
|
}
|
|
saveMessagesToCache(userPubkey);
|
|
renderConversations();
|
|
}
|
|
|
|
document.getElementById('emptyState').classList.add('hidden');
|
|
document.getElementById('chatView').classList.remove('hidden');
|
|
|
|
if (window.innerWidth < 768) {
|
|
document.querySelector('.sidebar').classList.add('hidden-mobile');
|
|
document.querySelector('.chat-area').classList.add('visible-mobile');
|
|
}
|
|
|
|
const displayName = getDisplayName(pubkey);
|
|
const npub = nip19.npubEncode(pubkey);
|
|
|
|
document.getElementById('chatContactName').textContent = displayName;
|
|
document.getElementById('chatContactNpub').textContent = npub;
|
|
|
|
renderChat(pubkey);
|
|
|
|
setTimeout(() => {
|
|
document.getElementById('messageInput')?.focus();
|
|
}, 100);
|
|
}
|
|
|
|
function backToConversations() {
|
|
document.querySelector('.sidebar').classList.remove('hidden-mobile');
|
|
document.querySelector('.chat-area').classList.remove('visible-mobile');
|
|
currentChatPubkey = null;
|
|
|
|
selectedConversationIndex = -1;
|
|
highlightSelectedConversation();
|
|
}
|
|
|
|
function renderChat(pubkey) {
|
|
const messagesEl = document.getElementById('chatMessages');
|
|
const conversation = conversations.get(pubkey);
|
|
|
|
if (!conversation) {
|
|
messagesEl.innerHTML = '<div style="text-align: center; color: #999; padding: 20px;">No messages yet</div>';
|
|
return;
|
|
}
|
|
|
|
messagesEl.innerHTML = conversation.messages.map(msg => {
|
|
const msgClass = msg.isSent ? 'sent' : 'received';
|
|
const time = new Date(msg.timestamp * 1000).toLocaleTimeString();
|
|
|
|
return `
|
|
<div class="message ${msgClass}">
|
|
<div class="content">${escapeHtml(msg.content)}</div>
|
|
<div class="time">${time}</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
|
|
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
}
|
|
|
|
async function sendDM() {
|
|
const input = document.getElementById('messageInput');
|
|
const content = input.value.trim();
|
|
|
|
if (!content || !currentChatPubkey) return;
|
|
|
|
try {
|
|
showStatus('Sending...', 'info');
|
|
|
|
let encryptedContent;
|
|
if (nip46Connection) {
|
|
encryptedContent = await nip46Connection.nip04Encrypt(currentChatPubkey, content);
|
|
} else {
|
|
encryptedContent = await nip04.encrypt(userPrivkey, currentChatPubkey, content);
|
|
}
|
|
|
|
const event = {
|
|
kind: 4,
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
tags: [['p', currentChatPubkey]],
|
|
content: encryptedContent
|
|
};
|
|
|
|
let signedEvent;
|
|
if (nip46Connection) {
|
|
signedEvent = await nip46Connection.signEvent(event);
|
|
} else {
|
|
signedEvent = finalizeEvent(event, userPrivkey);
|
|
}
|
|
|
|
await pool.publish(currentRelays, signedEvent);
|
|
|
|
const conversation = conversations.get(currentChatPubkey);
|
|
if (conversation) {
|
|
const message = {
|
|
id: signedEvent.id,
|
|
content: content,
|
|
timestamp: signedEvent.created_at,
|
|
from: userPubkey,
|
|
isSent: true
|
|
};
|
|
|
|
if (!conversation.messages.find(m => m.id === message.id)) {
|
|
conversation.messages.push(message);
|
|
conversation.lastMessage = message;
|
|
}
|
|
}
|
|
|
|
input.value = '';
|
|
renderChat(currentChatPubkey);
|
|
renderConversations();
|
|
|
|
await saveMessagesToCache(userPubkey);
|
|
|
|
showStatus('Sent!', 'success');
|
|
|
|
} catch (error) {
|
|
console.error('Send error:', error);
|
|
showStatus('Failed to send: ' + error.message, 'error');
|
|
}
|
|
}
|
|
|
|
function handleKeyPress(event) {
|
|
if (event.key === 'Enter') {
|
|
sendDM();
|
|
}
|
|
}
|
|
|
|
function filterConversations() {
|
|
const searchTerm = document.getElementById('searchBox').value.toLowerCase();
|
|
const items = document.querySelectorAll('.conversation-item');
|
|
|
|
items.forEach(item => {
|
|
const text = item.textContent.toLowerCase();
|
|
if (text.includes(searchTerm)) {
|
|
item.style.display = '';
|
|
} else {
|
|
item.style.display = 'none';
|
|
}
|
|
});
|
|
}
|
|
|
|
function markAllAsRead() {
|
|
conversations.forEach(conv => {
|
|
conv.unread = 0;
|
|
if (conv.lastMessage) {
|
|
conv.lastReadTimestamp = conv.lastMessage.timestamp;
|
|
}
|
|
});
|
|
saveMessagesToCache(userPubkey);
|
|
renderConversations();
|
|
showStatus('All conversations marked as read', 'success');
|
|
}
|
|
|
|
function disconnect() {
|
|
subscriptions.forEach(sub => sub.close());
|
|
subscriptions = [];
|
|
|
|
if (pool) pool.close(currentRelays);
|
|
if (nip46Connection) nip46Connection.close();
|
|
|
|
localStorage.removeItem('bunker_session');
|
|
console.log('🗑️ Cleared saved session (kept message cache)');
|
|
|
|
userPubkey = null;
|
|
userPrivkey = null;
|
|
nip46Connection = null;
|
|
conversations.clear();
|
|
currentChatPubkey = null;
|
|
lastSyncTimestamp = 0;
|
|
|
|
document.getElementById('dashboard').style.display = 'none';
|
|
document.getElementById('loginSection').style.display = 'block';
|
|
document.getElementById('conversationsList').innerHTML = '<div class="loading-overlay"><div class="spinner"></div>Loading DMs...</div>';
|
|
document.getElementById('chatMessages').innerHTML = '';
|
|
document.getElementById('emptyState').classList.remove('hidden');
|
|
document.getElementById('chatView').classList.add('hidden');
|
|
document.getElementById('cacheInfo').textContent = '';
|
|
|
|
showStatus('Disconnected', 'info');
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
async function restoreSession() {
|
|
try {
|
|
const savedSession = localStorage.getItem('bunker_session');
|
|
if (!savedSession) {
|
|
console.log('No saved session found');
|
|
return false;
|
|
}
|
|
|
|
const sessionData = JSON.parse(savedSession);
|
|
const sessionAge = Date.now() - sessionData.timestamp;
|
|
const oneWeek = 7 * 24 * 60 * 60 * 1000;
|
|
|
|
if (sessionAge > oneWeek) {
|
|
console.log('Session expired (older than 1 week)');
|
|
localStorage.removeItem('bunker_session');
|
|
return false;
|
|
}
|
|
|
|
console.log('🔄 Restoring session from ' + new Date(sessionData.timestamp).toLocaleString());
|
|
showStatus('Restoring session...', 'info');
|
|
|
|
nip46Connection = new NIP46Client('BTC for Plebs DMs');
|
|
nip46Connection.remotePubkey = sessionData.remotePubkey;
|
|
nip46Connection.relays = sessionData.relays;
|
|
nip46Connection.localPubkey = sessionData.localPubkey;
|
|
|
|
nip46Connection.localPrivkey = new Uint8Array(
|
|
sessionData.localPrivkey.match(/.{1,2}/g).map(byte => parseInt(byte, 16))
|
|
);
|
|
|
|
nip46Connection.pool = new SimplePool();
|
|
nip46Connection.conversationKey = nip44.v2.utils.getConversationKey(
|
|
nip46Connection.localPrivkey,
|
|
sessionData.remotePubkey
|
|
);
|
|
nip46Connection.connected = true;
|
|
|
|
const nip46Sub = nip46Connection.pool.subscribeMany(
|
|
sessionData.relays,
|
|
[{
|
|
kinds: [24133],
|
|
'#p': [nip46Connection.localPubkey],
|
|
since: Math.floor(Date.now() / 1000) - 60
|
|
}],
|
|
{
|
|
onevent: async (event) => {
|
|
await nip46Connection.handleResponse(event);
|
|
}
|
|
}
|
|
);
|
|
|
|
nip46Connection.sub = nip46Sub;
|
|
|
|
userPubkey = sessionData.userPubkey;
|
|
pool = nip46Connection.pool;
|
|
currentRelays = sessionData.relays;
|
|
|
|
showStatus('Session restored successfully!', 'success');
|
|
await initDashboard();
|
|
|
|
return true;
|
|
|
|
} catch (error) {
|
|
console.error('Failed to restore session:', error);
|
|
localStorage.removeItem('bunker_session');
|
|
showStatus('Session restore failed, please log in again', 'error');
|
|
return false;
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|