Compare commits

...

8 Commits

Author SHA1 Message Date
sam af822e7c63 messaging++ 2026-04-15 11:36:05 +05:45
sam 4ac4f32b45 fix build 2026-04-12 08:44:17 +05:45
sam 286a0fcadc turnoffable chats 2026-04-11 11:18:36 +05:45
sam 013584da06 Merge branch 'main' into dms-rebased 2026-04-10 09:50:27 +05:45
sam 19df70baed updated dm route 2026-04-09 20:13:35 +05:45
sam cf7384523a reinstate messages settings 2026-04-09 18:10:57 +05:45
sam 64729b9804 change route/menu 2026-04-05 14:47:04 +05:45
sam 954339c3b9 rework dms pr 2026-04-05 13:13:53 +05:45
25 changed files with 952 additions and 5 deletions
+3
View File
@@ -37,6 +37,9 @@ deploy.sh
# Build-time configuration
ditto.json
# DM message sounds (copied from node_modules by postinstall)
public/sounds/
# Android build outputs and sensitive files
*.aab
resources/
+1
View File
@@ -0,0 +1 @@
legacy-peer-deps=true
+15
View File
@@ -0,0 +1,15 @@
FROM node:22-alpine AS builder
WORKDIR /app
RUN apk add --no-cache git
COPY package*.json ./
COPY .npmrc ./
COPY scripts/ ./scripts/
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
+6
View File
@@ -0,0 +1,6 @@
services:
web:
build: .
restart: unless-stopped
expose:
- "80"
+31
View File
@@ -0,0 +1,31 @@
services:
web:
image: nginx:alpine
ports:
- "8082:80"
volumes:
- ./nginx.dev.conf:/etc/nginx/conf.d/default.conf:ro
- ./dist:/usr/share/nginx/html:ro
restart: unless-stopped
depends_on:
- vite
networks:
- ditto-network
vite:
image: node:22-alpine
working_dir: /app
# Use host node_modules (no anonymous volume) so new deps added after merge
# are picked up after a plain "npm install" on the host and container restart.
command: sh -c "npm install && npm run dev"
volumes:
- .:/app
environment:
- NODE_ENV=development
networks:
- ditto-network
restart: unless-stopped
networks:
ditto-network:
driver: bridge
+30
View File
@@ -0,0 +1,30 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/javascript application/json;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
location / {
try_files $uri $uri/ /index.html;
}
}
+35
View File
@@ -0,0 +1,35 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/javascript application/json;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
location / {
resolver 127.0.0.11 valid=10s;
set $vite_backend http://vite:8080;
proxy_pass $vite_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400;
}
}
+142 -3
View File
@@ -90,6 +90,7 @@
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.2.8",
"@samthomson/nostr-messaging": "^0.14.0",
"@sentry/react": "^10.42.0",
"@tanstack/react-query": "^5.56.2",
"@unhead/addons": "^2.0.10",
@@ -180,6 +181,7 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -2467,6 +2469,7 @@
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.stat": "2.0.5",
@@ -2480,6 +2483,7 @@
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 8"
@@ -2489,6 +2493,7 @@
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.scandir": "2.1.5",
@@ -5714,6 +5719,7 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5727,6 +5733,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5740,6 +5747,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5753,6 +5761,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5766,6 +5775,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5779,6 +5789,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5792,6 +5803,7 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5805,6 +5817,7 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5818,6 +5831,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5831,6 +5845,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5844,6 +5859,7 @@
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5857,6 +5873,7 @@
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5870,6 +5887,7 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5883,6 +5901,7 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5896,6 +5915,7 @@
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5909,6 +5929,7 @@
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5922,6 +5943,7 @@
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5935,6 +5957,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5948,6 +5971,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5961,6 +5985,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5974,6 +5999,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5987,6 +6013,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -6000,6 +6027,7 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -6013,6 +6041,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -6026,12 +6055,46 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@samthomson/nostr-messaging": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/@samthomson/nostr-messaging/-/nostr-messaging-0.14.0.tgz",
"integrity": "sha512-Ykqo+XJEPBPcfwZxHG8yvVseaUsvdzvfPrU5wY996H7tKbzeiJEIi6MVTaTkhUvdojeTne7HcPTm9As+hrKK3A==",
"license": "MIT",
"dependencies": {
"blurhash": "^2.0.5",
"buffer": "^6.0.3",
"fuse.js": "^7.1.0",
"idb": "^8.0.3",
"nostr-tools": "^2.13.0",
"react-blurhash": "^0.3.0"
},
"peerDependencies": {
"@nostrify/nostrify": ">=0.47.0",
"@nostrify/react": "^0.2.0",
"@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-popover": "^1.1.0",
"@radix-ui/react-scroll-area": "^1.1.0",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.4",
"@tanstack/react-query": "^5.56.2",
"clsx": "^2.0.0",
"lucide-react": "^0.462.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.0.0",
"tailwind-merge": "^2.0.0"
}
},
"node_modules/@scure/base": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz",
@@ -6586,6 +6649,7 @@
"version": "19.2.14",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
"dev": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
@@ -6595,7 +6659,7 @@
"version": "19.2.3",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"devOptional": true,
"dev": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^19.2.0"
@@ -7283,12 +7347,14 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
"dev": true,
"license": "MIT"
},
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"dev": true,
"license": "ISC",
"dependencies": {
"normalize-path": "^3.0.0",
@@ -7302,6 +7368,7 @@
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
"dev": true,
"license": "MIT"
},
"node_modules/argparse": {
@@ -7468,6 +7535,7 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -7510,6 +7578,7 @@
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"license": "MIT",
"dependencies": {
"fill-range": "^7.1.1"
@@ -7634,6 +7703,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
@@ -7767,6 +7837,7 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dev": true,
"license": "MIT",
"dependencies": {
"anymatch": "~3.1.2",
@@ -7791,6 +7862,7 @@
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
@@ -7918,6 +7990,7 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
@@ -7968,6 +8041,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
"dev": true,
"license": "MIT",
"bin": {
"cssesc": "bin/cssesc"
@@ -8298,6 +8372,7 @@
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/dijkstrajs": {
@@ -8310,6 +8385,7 @@
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
"dev": true,
"license": "MIT"
},
"node_modules/dom-accessibility-api": {
@@ -8722,6 +8798,7 @@
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.stat": "^2.0.2",
@@ -8738,6 +8815,7 @@
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
@@ -8764,6 +8842,7 @@
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
"integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
"dev": true,
"license": "ISC",
"dependencies": {
"reusify": "^1.0.4"
@@ -8802,6 +8881,7 @@
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"license": "MIT",
"dependencies": {
"to-regex-range": "^5.0.1"
@@ -8881,6 +8961,7 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@@ -8895,11 +8976,25 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/fuse.js": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.3.0.tgz",
"integrity": "sha512-plz8RVjfcDedTGfVngWH1jmJvBvAwi1v2jecfDerbEnMcmOYUEEwKFTHbNoCiYyzaK2Ws8lABkTCcRSqCY1q4w==",
"license": "Apache-2.0",
"engines": {
"node": ">=10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/krisk"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
@@ -8935,6 +9030,7 @@
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.3"
@@ -8977,6 +9073,7 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
@@ -9265,6 +9362,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"license": "MIT",
"dependencies": {
"binary-extensions": "^2.0.0"
@@ -9277,6 +9375,7 @@
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"dev": true,
"license": "MIT",
"dependencies": {
"hasown": "^2.0.2"
@@ -9318,6 +9417,7 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -9336,6 +9436,7 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-extglob": "^2.1.1"
@@ -9406,6 +9507,7 @@
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.12.0"
@@ -9454,6 +9556,7 @@
"version": "1.21.7",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"dev": true,
"license": "MIT",
"bin": {
"jiti": "bin/jiti.js"
@@ -9888,6 +9991,7 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
"integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14"
@@ -9900,6 +10004,7 @@
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"dev": true,
"license": "MIT"
},
"node_modules/locate-path": {
@@ -10359,6 +10464,7 @@
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 8"
@@ -10950,6 +11056,7 @@
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"license": "MIT",
"dependencies": {
"braces": "^3.0.3",
@@ -11027,6 +11134,7 @@
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"any-promise": "^1.0.0",
@@ -11096,6 +11204,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -11166,6 +11275,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
@@ -11335,6 +11445,7 @@
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true,
"license": "MIT"
},
"node_modules/pathe": {
@@ -11370,6 +11481,7 @@
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
@@ -11382,6 +11494,7 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -11391,6 +11504,7 @@
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
"integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
@@ -11463,6 +11577,7 @@
"version": "15.1.0",
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
"integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
"dev": true,
"license": "MIT",
"dependencies": {
"postcss-value-parser": "^4.0.0",
@@ -11480,6 +11595,7 @@
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
"integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
"dev": true,
"license": "MIT",
"dependencies": {
"camelcase-css": "^2.0.1"
@@ -11499,6 +11615,7 @@
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
"integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -11534,6 +11651,7 @@
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
"integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -11559,6 +11677,7 @@
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
"dev": true,
"license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
@@ -11586,6 +11705,7 @@
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true,
"license": "MIT"
},
"node_modules/powershell-utils": {
@@ -11920,6 +12040,7 @@
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
"dev": true,
"funding": [
{
"type": "github",
@@ -12223,6 +12344,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
"integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
"dev": true,
"license": "MIT",
"dependencies": {
"pify": "^2.3.0"
@@ -12247,6 +12369,7 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"license": "MIT",
"dependencies": {
"picomatch": "^2.2.1"
@@ -12447,6 +12570,7 @@
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
"integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-core-module": "^2.16.0",
@@ -12477,6 +12601,7 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
"dev": true,
"license": "MIT",
"engines": {
"iojs": ">=1.0.0",
@@ -12632,7 +12757,7 @@
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
"integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.8"
@@ -12919,6 +13044,7 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
"dev": true,
"funding": [
{
"type": "github",
@@ -13251,6 +13377,7 @@
"version": "3.35.1",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
"integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.2",
@@ -13286,6 +13413,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -13315,6 +13443,7 @@
"version": "3.4.17",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
"integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
"dev": true,
"license": "MIT",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
@@ -13361,6 +13490,7 @@
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
"dev": true,
"license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
@@ -13391,6 +13521,7 @@
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
"dev": true,
"license": "MIT",
"dependencies": {
"any-promise": "^1.0.0"
@@ -13400,6 +13531,7 @@
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
"integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
"dev": true,
"license": "MIT",
"dependencies": {
"thenify": ">= 3.1.0 < 4"
@@ -13442,6 +13574,7 @@
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
@@ -13458,6 +13591,7 @@
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0.0"
@@ -13475,6 +13609,7 @@
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
@@ -13537,6 +13672,7 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-number": "^7.0.0"
@@ -13618,6 +13754,7 @@
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/tslib": {
@@ -13643,7 +13780,7 @@
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"devOptional": true,
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@@ -13970,6 +14107,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true,
"license": "MIT"
},
"node_modules/vaul": {
@@ -15718,6 +15856,7 @@
"version": "2.8.3",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
"integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==",
"dev": true,
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
+1
View File
@@ -97,6 +97,7 @@
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.2.8",
"@samthomson/nostr-messaging": "^0.14.0",
"@sentry/react": "^10.42.0",
"@tanstack/react-query": "^5.56.2",
"@unhead/addons": "^2.0.10",
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+8
View File
@@ -0,0 +1,8 @@
#!/bin/bash
# Copy default message sounds from @samthomson/nostr-messaging package
if [ -d "node_modules/@samthomson/nostr-messaging/assets/sounds" ]; then
mkdir -p public/sounds
cp node_modules/@samthomson/nostr-messaging/assets/sounds/*.mp3 public/sounds/
echo "Copied message sounds to public/sounds/"
fi
+4 -1
View File
@@ -16,7 +16,7 @@ import NostrProvider from "@/components/NostrProvider";
import { NostrSync } from "@/components/NostrSync";
import { PlausibleProvider } from "@/components/PlausibleProvider";
import { SentryProvider } from "@/components/SentryProvider";
import { DMProviderWrapper } from "@/components/DMProviderWrapper";
import { TooltipProvider } from "@/components/ui/tooltip";
import { useNsecPasteGuard } from "@/hooks/useNsecPasteGuard";
@@ -124,6 +124,7 @@ const hardcodedConfig: AppConfig = {
sidebarOrder: [
"feed",
"notifications",
"dms",
"search",
"blobbi",
"badges",
@@ -207,6 +208,7 @@ export function App() {
<NativeNotifications />
<NWCProvider>
<DMProviderWrapper>
<DMProvider config={dmConfig}>
<EmotionDevProvider>
<TooltipProvider>
@@ -216,6 +218,7 @@ export function App() {
</TooltipProvider>
</EmotionDevProvider>
</DMProvider>
</DMProviderWrapper>
</NWCProvider>
</NostrProvider>
</NostrLoginProvider>
+4
View File
@@ -79,6 +79,8 @@ const WikipediaPage = lazy(() => import("./pages/WikipediaPage").then(m => ({ de
const WorldPage = lazy(() => import("./pages/WorldPage").then(m => ({ default: m.WorldPage })));
const FollowPage = lazy(() => import("./pages/FollowPage").then(m => ({ default: m.FollowPage })));
const RemoteLoginSuccessPage = lazy(() => import("./pages/RemoteLoginSuccessPage").then(m => ({ default: m.RemoteLoginSuccessPage })));
const MessagesPage = lazy(() => import("./pages/MessagesPage").then(m => ({ default: m.MessagesPage })));
const MessagingSettings = lazy(() => import("./pages/MessagingSettings"));
const pollsDef = getExtraKindDef("polls")!;
const colorsDef = getExtraKindDef("colors")!;
@@ -160,6 +162,8 @@ export function AppRouter() {
<Route path="/" element={<HomePage />} />
<Route path="/feed" element={<Index />} />
<Route path="/notifications" element={<NotificationsPage />} />
<Route path="/chats" element={<MessagesPage />} />
<Route path="/settings/messaging" element={<MessagingSettings />} />
<Route path="/search" element={<SearchPage />} />
<Route path="/trends" element={<TrendsPage />} />
<Route path="/profile" element={<ProfileRedirect />} />
+110
View File
@@ -0,0 +1,110 @@
import { type ReactNode, useMemo } from 'react';
import { useNostr } from '@nostrify/react';
import { DMProvider } from '@samthomson/nostr-messaging/core';
import type { NostrEvent } from '@nostrify/nostrify';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useAppContext } from '@/hooks/useAppContext';
import { useNostrPublish } from '@/hooks/useNostrPublish';
import { useUploadFile } from '@/hooks/useUploadFile';
import { useAuthorsBatch } from '@/hooks/useAuthorsBatch';
import { useProfileSupplementary } from '@/hooks/useProfileData';
import { useIsMobile } from '@/hooks/useIsMobile';
import { toast } from '@/hooks/useToast';
import { getDisplayName } from '@/lib/getDisplayName';
import { APP_NEW_MESSAGE_SOUNDS } from '@/lib/messagingSounds';
interface DMProviderWrapperProps {
children: ReactNode;
}
export function DMProviderWrapper({ children }: DMProviderWrapperProps) {
const { nostr } = useNostr();
const { user } = useCurrentUser();
const { config } = useAppContext();
const { mutateAsync: publishEvent } = useNostrPublish();
const { mutateAsync: uploadFileMutation } = useUploadFile();
const isMobile = useIsMobile();
// Get the current user's follows
const { data: profileData } = useProfileSupplementary(user?.pubkey);
const follows = useMemo(() => profileData?.following ?? [], [profileData]);
// Wrap publishEvent to match the expected signature
const handlePublishEvent = async (event: Omit<NostrEvent, 'id' | 'pubkey' | 'sig'>): Promise<void> => {
await publishEvent(event);
};
// Wrap uploadFile to return just the URL string
const handleUploadFile = async (file: File): Promise<string> => {
const tags = await uploadFileMutation(file);
return tags[0][1]; // Return the URL from the first tag
};
// Wrap getDisplayName to match the expected signature
const handleGetDisplayName = (pubkey: string, metadata?: Parameters<typeof getDisplayName>[0]) => {
return getDisplayName(metadata, pubkey);
};
// Wrap toast to match the expected signature
const handleNotify = (options: { title?: string; description?: string; variant?: 'default' | 'destructive' }) => {
toast({
title: options.title,
description: options.description,
variant: options.variant,
});
};
const messaging = useMemo(() => config.messaging ?? {}, [config.messaging]);
// Discovery relays for DM inbox discovery
const discoveryRelays = useMemo(() => {
if (messaging.discoveryRelays?.length) {
return messaging.discoveryRelays;
}
return config.relayMetadata.relays
.filter(r => r.read)
.map(r => r.url);
}, [messaging.discoveryRelays, config.relayMetadata.relays]);
const relayMode = messaging.relayMode ?? 'hybrid';
const messagingEnabled = messaging.enabled ?? false;
const renderInlineMedia = messaging.renderInlineMedia ?? true;
const soundEnabled = messaging.soundEnabled ?? false;
const soundId = messaging.soundId ?? APP_NEW_MESSAGE_SOUNDS[0]?.id ?? '';
const devMode = messaging.devMode ?? false;
return (
<DMProvider
nostr={nostr}
user={user ?? null}
messagingConfig={{
enabled: messagingEnabled,
discoveryRelays,
relayMode,
renderInlineMedia,
devMode,
appName: config.appName,
appDescription: `Direct messages on ${config.appName}`,
soundPref: {
options: APP_NEW_MESSAGE_SOUNDS,
value: { enabled: soundEnabled, soundId },
onChange: () => {},
},
}}
onNotify={handleNotify}
getDisplayName={handleGetDisplayName}
fetchAuthorsBatch={useAuthorsBatch}
publishEvent={handlePublishEvent}
uploadFile={handleUploadFile}
follows={follows}
ui={{
showShorts: false,
showSearch: true,
isMobile,
}}
>
{children}
</DMProvider>
);
}
+12
View File
@@ -241,6 +241,18 @@ export interface AppConfig {
savedFeeds: SavedFeed[];
/** Image upload quality: "compressed" resizes/optimizes, "original" uploads as-is. Default: "compressed". */
imageQuality: 'compressed' | 'original';
/** Messaging configuration (custom sounds, discovery relays, etc.) */
messaging?: {
/** Whether direct messaging is enabled for this account/session. Default: false. */
enabled?: boolean;
customSoundUrl?: string;
discoveryRelays?: string[];
relayMode?: 'discovery' | 'hybrid' | 'strict_outbox';
renderInlineMedia?: boolean;
soundEnabled?: boolean;
soundId?: string;
devMode?: boolean;
};
/** Hex pubkey of the curator whose follow list defines the Ditto feed. */
curatorPubkey?: string;
/** Wildcard domain used for iframe sandboxing (e.g. "iframe.diy"). Default: "iframe.diy". */
+14
View File
@@ -0,0 +1,14 @@
import { useAuthors } from './useAuthors';
/**
* Batch fetch author profiles for DM messaging integration.
*
* This hook wraps useAuthors to match the interface expected by
* @samthomson/nostr-messaging's DMProvider.
*
* @param pubkeys - Array of pubkeys to fetch profiles for
* @returns Query result with map of pubkey -> AuthorData
*/
export function useAuthorsBatch(pubkeys: string[]) {
return useAuthors(pubkeys);
}
+8
View File
@@ -0,0 +1,8 @@
/**
* Re-exports DM hooks from the @samthomson/nostr-messaging package.
* Separated from DMProviderWrapper to avoid Fast Refresh warnings.
*/
export {
useDMContext,
useConversationMessages,
} from '@samthomson/nostr-messaging/core';
+10
View File
@@ -0,0 +1,10 @@
import { DEFAULT_NEW_MESSAGE_SOUNDS, type NewMessageSoundOption } from '@samthomson/nostr-messaging/core';
export const APP_NEW_MESSAGE_SOUNDS: NewMessageSoundOption[] = [
...DEFAULT_NEW_MESSAGE_SOUNDS,
{
id: 'ditto',
label: 'Ditto',
url: '/custom-sounds/ditto.mp3',
},
];
+10
View File
@@ -245,6 +245,16 @@ export const AppConfigSchema = z.object({
})
).optional().default([]),
imageQuality: z.enum(['compressed', 'original']),
messaging: z.object({
enabled: z.boolean().optional(),
customSoundUrl: z.string().optional(),
discoveryRelays: z.array(z.string().url()).optional(),
relayMode: z.enum(['discovery', 'hybrid', 'strict_outbox']).optional(),
renderInlineMedia: z.boolean().optional(),
soundEnabled: z.boolean().optional(),
soundId: z.string().optional(),
devMode: z.boolean().optional(),
}).optional(),
curatorPubkey: z.string().regex(/^[0-9a-f]{64}$/i).optional(),
sandboxDomain: z.string().optional(),
});
+8 -1
View File
@@ -15,7 +15,7 @@ import {
Earth,
Film,
HelpCircle,
Mail,
MessageSquare,
MessageSquareMore,
Mic,
@@ -110,6 +110,13 @@ export const SIDEBAR_ITEMS: SidebarItemDef[] = [
icon: Bell,
requiresAuth: true,
},
{
id: "dms",
label: "Chats",
path: "/chats",
icon: Mail,
requiresAuth: true,
},
{ id: "search", label: "Search", path: "/search", icon: Search },
{ id: "trends", label: "Trends", path: "/trends", icon: TrendingUp },
{
+38
View File
@@ -0,0 +1,38 @@
import { DMMessagingInterface } from '@samthomson/nostr-messaging/ui';
import { Link } from 'react-router-dom';
import { useLayoutOptions } from '@/contexts/LayoutContext';
import { useAppContext } from '@/hooks/useAppContext';
export function MessagesPage() {
const { config } = useAppContext();
const messagingEnabled = config.messaging?.enabled ?? false;
// Hide the right sidebar and expand the main content area for messaging.
// noOverscroll: avoid pb-overscroll on the main column so this fixed-height layout doesn't get extra scroll.
useLayoutOptions({
rightSidebar: null,
noMaxWidth: true,
noOverscroll: true,
wrapperClassName: 'max-w-full',
});
return (
<div className="h-dvh flex flex-col">
{messagingEnabled ? (
<DMMessagingInterface />
) : (
<div className="flex-1 flex items-center justify-center p-6">
<div className="max-w-md text-center space-y-3">
<h2 className="text-xl font-semibold">Chats are turned off</h2>
<p className="text-sm text-muted-foreground">
Enable messaging in Settings to start using chats.
</p>
<Link to="/settings/messaging" className="inline-block text-sm text-primary hover:underline">
Open Messaging Settings
</Link>
</div>
</div>
)}
</div>
);
}
+453
View File
@@ -0,0 +1,453 @@
import { useSeoMeta } from '@unhead/react';
import { ArrowLeft } from 'lucide-react';
import { Link } from 'react-router-dom';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
import { Button } from '@/components/ui/button';
import { useAppContext } from '@/hooks/useAppContext';
import { useDMContext } from '@/hooks/useDMHooks';
import { RelayListManager } from '@/components/RelayListManager';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { Badge } from '@/components/ui/badge';
import { RefreshCw, AlertCircle, Play } from 'lucide-react';
import { useState, useEffect, useCallback, useRef } from 'react';
import { getMediaCacheStats, type RelayMode } from '@samthomson/nostr-messaging/core';
import { IntroImage } from '@/components/IntroImage';
import { APP_NEW_MESSAGE_SOUNDS } from '@/lib/messagingSounds';
export default function MessagingSettings() {
const { config, updateConfig } = useAppContext();
const {
subscriptions,
messagingState,
isLoading: dmIsLoading,
clearCacheAndRefetch,
} = useDMContext();
const messaging = config.messaging ?? {};
const [mediaCacheStats, setMediaCacheStats] = useState<{ count: number; size: number } | null>(null);
useSeoMeta({
title: 'Messages | Settings | Ditto',
description: 'Configure your direct messaging settings.',
});
useEffect(() => {
getMediaCacheStats().then(setMediaCacheStats).catch(() => {
setMediaCacheStats({ count: 0, size: 0 });
});
}, []);
const preloadedSoundsRef = useRef<Map<string, HTMLAudioElement>>(new Map());
useEffect(() => {
const map = new Map<string, HTMLAudioElement>();
APP_NEW_MESSAGE_SOUNDS.forEach((sound) => {
const audio = new Audio(sound.url);
audio.volume = 0.5;
audio.preload = 'auto';
map.set(sound.url, audio);
});
preloadedSoundsRef.current = map;
return () => {
map.clear();
};
}, []);
const relayMode = messaging.relayMode ?? 'hybrid';
const messagingEnabled = messaging.enabled ?? false;
const renderInlineMedia = messaging.renderInlineMedia ?? true;
const soundEnabled = messaging.soundEnabled ?? false;
const soundId = messaging.soundId ?? APP_NEW_MESSAGE_SOUNDS[0]?.id ?? '';
const devMode = messaging.devMode ?? false;
const handleMessagingEnabledChange = (checked: boolean) => {
updateConfig((prev) => ({
...prev,
messaging: { ...messaging, enabled: checked },
}));
};
const handleRelayModeChange = (mode: string) => {
updateConfig((prev) => ({
...prev,
messaging: { ...messaging, relayMode: mode as RelayMode },
}));
};
const handleRenderInlineMediaChange = (checked: boolean) => {
updateConfig((prev) => ({
...prev,
messaging: { ...messaging, renderInlineMedia: checked },
}));
};
const handleSoundIdChange = (id: string) => {
updateConfig((prev) => ({
...prev,
messaging: { ...messaging, soundEnabled: true, soundId: id },
}));
};
const handleDevModeChange = (checked: boolean) => {
updateConfig((prev) => ({
...prev,
messaging: { ...messaging, devMode: checked },
}));
};
const handlePlaySound = useCallback((soundUrl: string) => {
try {
const preloaded = preloadedSoundsRef.current.get(soundUrl);
if (preloaded) {
preloaded.currentTime = 0;
preloaded.play().catch(() => {});
} else {
const audio = new Audio(soundUrl);
audio.volume = 0.5;
audio.play().catch(() => {});
}
} catch {
// Ignore errors
}
}, []);
const handleClearCache = async () => {
if (confirm('This will clear all cached messages and re-fetch from relays. Continue?')) {
await clearCacheAndRefetch();
const stats = await getMediaCacheStats();
setMediaCacheStats(stats);
}
};
const formatBytes = (bytes: number): string => {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
};
const conversationCount = messagingState ? Object.keys(messagingState.conversationMetadata).length : 0;
const totalMessages = messagingState
? Object.values(messagingState.conversationMessages).reduce((sum, msgs) => sum + msgs.length, 0)
: 0;
const lastSync = messagingState?.syncState?.lastCacheTime
? new Date(messagingState.syncState.lastCacheTime).toLocaleString()
: 'Never';
return (
<main className="">
<div className="px-4 pt-4 pb-3">
<div className="flex items-center gap-4">
<Link to="/settings" className="p-2 -ml-2 rounded-full hover:bg-secondary transition-colors">
<ArrowLeft className="size-5" />
</Link>
<div>
<h1 className="text-xl font-bold">Messages</h1>
<p className="text-sm text-muted-foreground mt-0.5">
Configure direct messaging settings, relays, and cache
</p>
</div>
</div>
</div>
<div className="p-4">
<div className="flex items-center gap-4 px-3 pt-2 pb-4">
<IntroImage src="/messaging-intro.png" />
<div className="min-w-0">
<h2 className="text-sm font-semibold">Direct Messaging</h2>
<p className="text-xs text-muted-foreground mt-1 leading-relaxed">
Manage your encrypted messaging settings and relay connections
</p>
</div>
</div>
<div className="space-y-6">
<Card className="bg-card/50 backdrop-blur-sm border-border/50">
<CardHeader>
<CardTitle>Messaging</CardTitle>
<CardDescription>
Enable or disable chats in Ditto
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="messaging-enabled">Enable Messaging</Label>
<p className="text-sm text-muted-foreground">
Turn chats on to use inbox, sync, and messaging features
</p>
</div>
<Switch
id="messaging-enabled"
checked={messagingEnabled}
onCheckedChange={handleMessagingEnabledChange}
/>
</div>
</CardContent>
</Card>
{!messagingEnabled && (
<Card className="bg-card/50 backdrop-blur-sm border-border/50">
<CardContent className="pt-6">
<p className="text-sm text-muted-foreground">
Messaging is currently off. Enable it above to reveal relay, cache, and advanced chat settings.
</p>
</CardContent>
</Card>
)}
{messagingEnabled && (
<>
<Card className="bg-card/50 backdrop-blur-sm border-border/50">
<CardHeader>
<CardTitle>General</CardTitle>
<CardDescription>
Configure how messages are displayed and notified
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="render-inline-media">Render Inline Media</Label>
<p className="text-sm text-muted-foreground">
Show images and videos directly in messages
</p>
</div>
<Switch
id="render-inline-media"
checked={renderInlineMedia}
onCheckedChange={handleRenderInlineMediaChange}
/>
</div>
<div className="border-t border-border/50 pt-6">
<div className="space-y-3">
<Label>Sound</Label>
<p className="text-sm text-muted-foreground mb-3">
Play a sound when a DM arrives
</p>
<RadioGroup
className="space-y-2"
value={soundEnabled ? soundId : 'none'}
onValueChange={(val) => {
if (val === 'none') {
updateConfig((prev) => ({
...prev,
messaging: { ...messaging, soundEnabled: false },
}));
} else {
handleSoundIdChange(val);
}
}}
>
<div className="flex min-h-9 items-center justify-between space-x-3">
<div className="flex items-center space-x-3">
<RadioGroupItem value="none" id="sound-none" />
<Label htmlFor="sound-none" className="font-normal cursor-pointer">
None
</Label>
</div>
</div>
{APP_NEW_MESSAGE_SOUNDS.map((sound) => (
<div
key={sound.id}
className="group flex min-h-9 items-center justify-between space-x-3"
>
<div className="flex items-center space-x-3">
<RadioGroupItem value={sound.id} id={`sound-${sound.id}`} />
<Label
htmlFor={`sound-${sound.id}`}
className="font-normal cursor-pointer"
>
{sound.label}
</Label>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => handlePlaySound(sound.url)}
className="opacity-0 group-hover:opacity-100 transition-opacity"
>
<Play className="h-4 w-4" />
</Button>
</div>
))}
</RadioGroup>
</div>
</div>
</CardContent>
</Card>
<Card className="bg-card/50 backdrop-blur-sm border-border/50">
<CardHeader>
<CardTitle>Relay Mode</CardTitle>
<CardDescription>
Control how relays are chosen for direct messages
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3">
<Label>Connection Mode</Label>
<RadioGroup value={relayMode} onValueChange={handleRelayModeChange}>
<div className="flex items-start space-x-3 space-y-0">
<RadioGroupItem value="discovery" id="mode-discovery" />
<div className="space-y-1">
<Label htmlFor="mode-discovery" className="font-normal cursor-pointer">
Discovery Only
</Label>
<p className="text-sm text-muted-foreground">
Only relays from the discovery list; fastest, may miss messages
</p>
</div>
</div>
<div className="flex items-start space-x-3 space-y-0">
<RadioGroupItem value="hybrid" id="mode-hybrid" />
<div className="space-y-1">
<Label htmlFor="mode-hybrid" className="font-normal cursor-pointer">
Hybrid <Badge variant="secondary" className="ml-2">Recommended</Badge>
</Label>
<p className="text-sm text-muted-foreground">
Discovery relays + user inbox relays
</p>
</div>
</div>
<div className="flex items-start space-x-3 space-y-0">
<RadioGroupItem value="strict_outbox" id="mode-strict" />
<div className="space-y-1">
<Label htmlFor="mode-strict" className="font-normal cursor-pointer">
Strict Outbox
</Label>
<p className="text-sm text-muted-foreground">
Only each user's published inbox relays (NIP-65/NIP-17). More private, but not everyone publishes relay lists yet - you may miss DMs to or from people who don't.
</p>
</div>
</div>
</RadioGroup>
</div>
</CardContent>
</Card>
<Card className="bg-card/50 backdrop-blur-sm border-border/50">
<CardHeader>
<CardTitle>Relays</CardTitle>
<CardDescription>
Discovery relays, NIP-65 inbox/outbox, and DM inbox
</CardDescription>
</CardHeader>
<CardContent>
<RelayListManager />
</CardContent>
</Card>
<Card className="bg-card/50 backdrop-blur-sm border-border/50">
<CardHeader>
<CardTitle>Cache & Storage</CardTitle>
<CardDescription>
View cache status and manage stored messages
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-3">
<Label>Connection Status</Label>
<div className="grid grid-cols-2 gap-3">
<div className="bg-secondary/20 p-3 rounded-lg">
<div className="text-sm font-medium mb-1">NIP-04 (Legacy)</div>
<Badge variant={subscriptions.isNIP4Connected ? 'default' : 'secondary'}>
{subscriptions.isNIP4Connected ? 'Connected' : 'Disconnected'}
</Badge>
</div>
<div className="bg-secondary/20 p-3 rounded-lg">
<div className="text-sm font-medium mb-1">NIP-17 (Private)</div>
<Badge variant={subscriptions.isNIP17Connected ? 'default' : 'secondary'}>
{subscriptions.isNIP17Connected ? 'Connected' : 'Disconnected'}
</Badge>
</div>
</div>
</div>
<div className="border-t border-border/50 pt-6">
<div className="space-y-3">
<Label>Cache Statistics</Label>
<div className="bg-secondary/20 p-4 rounded-lg space-y-2">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Conversations:</span>
<span className="font-medium">{conversationCount}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Total Messages:</span>
<span className="font-medium">{totalMessages}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Last Sync:</span>
<span className="font-medium">{lastSync}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Media Files Cached:</span>
<span className="font-medium">{mediaCacheStats?.count ?? '...'}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Media Cache Size:</span>
<span className="font-medium">
{mediaCacheStats ? formatBytes(mediaCacheStats.size) : '...'}
</span>
</div>
</div>
</div>
</div>
<div className="border-t border-border/50 pt-6">
<Button
variant="destructive"
onClick={handleClearCache}
disabled={dmIsLoading}
className="w-full"
>
<RefreshCw className="h-4 w-4 mr-2" />
Clear Cache & Refetch
</Button>
<p className="text-sm text-muted-foreground mt-2">
This will clear all cached messages and re-fetch from relays. Use this if messages are missing or out of sync.
</p>
</div>
</CardContent>
</Card>
<Card className="bg-card/50 backdrop-blur-sm border-border/50">
<CardHeader>
<CardTitle>Advanced</CardTitle>
<CardDescription>
Developer and debugging options
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-start justify-between">
<div className="space-y-0.5">
<Label htmlFor="dev-mode">Developer Mode</Label>
<p className="text-sm text-muted-foreground">
Show extra debug UI (seal payload, decryption details)
</p>
<div className="flex items-center gap-2 mt-2">
<AlertCircle className="h-4 w-4 text-muted-foreground" />
<span className="text-xs text-muted-foreground">
Only enable if you need to debug message encryption
</span>
</div>
</div>
<Switch
id="dev-mode"
checked={devMode}
onCheckedChange={handleDevModeChange}
/>
</div>
</CardContent>
</Card>
</>
)}
</div>
</div>
</main>
);
}
+8
View File
@@ -57,6 +57,14 @@ const settingsSections: SettingsSection[] = [
path: '/settings/notifications',
requiresAuth: true,
},
{
id: 'messaging',
label: 'Messages',
description: 'Direct messaging settings, relays, and cache',
illustration: '/messaging-intro.png',
path: '/settings/messaging',
requiresAuth: true,
},
{
id: 'advanced',
label: 'Advanced',
+1
View File
@@ -8,6 +8,7 @@ export default {
"./components/**/*.{ts,tsx}",
"./app/**/*.{ts,tsx}",
"./src/**/*.{ts,tsx}",
"./node_modules/@samthomson/nostr-messaging/dist/**/*.js",
],
prefix: "",
theme: {